์ž๋ฐ”์—์„œ ์•”ํ˜ธํ™” ๋˜๋Š” ์ „์ž์„œ๋ช…์„ ์œ„ํ•œ ๊ธฐ๋Šฅ์„ JCA๋ฅผ ํ†ตํ•ด ์ œ๊ณตํ•˜๊ธฐ๋Š” ํ•˜์ง€๋งŒ ์‹ค์งˆ์ ์œผ๋กœ๋Š” Bouncy Castle์ด๋ผ๋Š” ์•”ํ˜ธ ๊ด€๋ จ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•œ๋‹ค. ๋ณธ ๊ธ€์—์„œ๋Š” BouncyCastle์—์„œ ์ œ๊ณตํ•˜๋Š” ์—ฌ๋Ÿฌ ํด๋ž˜์Šค๋“ค์„ ํ™œ์šฉํ•ด์„œ ์‚ฌ์„ค ๋ฃจํŠธ ์ธ์ฆ์„œ๋ฅผ ๋งŒ๋“ค๊ณ  ๊ทธ๊ฒƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์œ„ํ•œ X.509 ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ•˜๊ณ ์ž ํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด์ง€๋Š” X.509 ์ธ์ฆ์„œ๋Š” ๋ณด์•ˆ ๋ ˆ๋ฒจ์ด ๋†’์€ ์‹œ์Šคํ…œ์—์„œ Mutual TLS ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

build.gradle
dependencies { implementation 'org.bouncycastle:bcprov-jdk18on:1.75' implementation 'org.bouncycastle:bcpkix-jdk18on:1.75' }

X.509 ์ธ์ฆ์„œ์— ๋Œ€ํ•œ ํ‘œ์ค€์€ RFC5280๋กœ ์ •์˜๋˜์–ด ์žˆ๊ณ  ASN.1 ํ‘œ๊ธฐ๋ฅผ ๋”ฐ๋ฅด๋ฉฐ ์ง€๊ธˆ์€ X.509 v3์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค.

BouncyCastleProvider

BouncyCastle ์ž๋ฐ” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํด๋ž˜์Šค ํŒจ์Šค์— ์ถ”๊ฐ€ํ–ˆ๋”๋ผ๋„ BouncyCastleProvider๋ฅผ JCE Provider๋กœ ์ถ”๊ฐ€ํ•ด์•ผํ•œ๋‹ค. $JAVA_HOME/lib/security/java.security์— ๋ช…์‹œํ•ด๋„ ๋˜์ง€๋งŒ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์„ฑํ•˜๋Š” ํด๋ž˜์Šค์—์„œ ๋Ÿฐํƒ€์ž„ ์‹œ์ ์— BouncyCastleProvider๋ฅผ ๋ณด์•ˆ ํ”„๋กœ๋ฐ”์ด๋”์— ์ถ”๊ฐ€ํ•ด๋„ ๋œ๋‹ค.

import java.security.Security;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
 
static {
    Security.addProvider(new BouncyCastleProvider());
}

Self signed CA ์ธ์ฆ์„œ ๋ฐœ๊ธ‰ํ•˜๊ธฐ

์ผ๋ฐ˜์ ์œผ๋กœ X.509 ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰ํ•˜๋Š” ๊ฒฝ์šฐ openssl ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ‚ค ํŽ˜์–ด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ทธ๊ฒƒ์„ ๊ธฐ๋ฐ˜์œผ๋กœ CSR๊ณผ X.509 ์ธ์ฆ์„œ๋ฅผ ์ƒ์„ฑํ•  ๊ฒƒ์ด๋‹ค. Bouncy Castle ์ž๋ฐ” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅด๋ฅผ ํ†ตํ•ด ๋งŒ๋“œ๋ ค๋Š” ๊ฒฝ์šฐ Deprecated ์„ ์–ธ๋œ X509V3CertificateGenerator๋ฅผ ์‚ฌ์šฉํ•ด๋„ ๋ฌด๋ฐฉํ•  ๊ฒƒ ๊ฐ™์ง€๋งŒ X509v3CertificateBuilder๋ฅผ ์‚ฌ์šฉํ•ด์„œ X509Certificate๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.

X509V3CertificateGenerator์— ๋Œ€ํ•ด์„œ Deprecated ์ฒ˜๋ฆฌํ•œ ์‚ฌ์œ ๋Š” ๋”ฑํžˆ ์•Œ ์ˆ˜ ์—†๋Š” ๊ฒƒ ๊ฐ™๋‹ค.

JcaX509v3CertificateBuilder์™€ JcaContentSignerBuilder๋ฅผ ์‚ฌ์šฉํ•ด์„œ X509CertificateHolder๋ฅผ ์ƒ์„ฑํ•˜๊ณ  JcaX509CertificateConverter๋ฅผ ์ด์šฉํ•˜์—ฌ X509Certificate๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋Œ€๋žต์ ์ธ ์ฝ”๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์œผ๋‹ˆ ์ฐธ๊ณ ํ•ด๋ณด๋„๋ก ํ•˜์ž.

String securityProvider = BouncyCastleProvider.PROVIDER_NAME;
JcaX509ExtensionUtils x509ExtensionUtils = new JcaX509ExtensionUtils();
JcaX509CertificateConverter x509CertificateConverter = new JcaX509CertificateConverter().setProvider(securityProvider);
String signatureAlgorithm = "sha256WithRSA";

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", securityProvider);
keyPairGenerator.initialize(4096, new SecureRandom());

KeyPair rootKeyPair = keyPairGenerator.generateKeyPair();

X500Name issuer = new X500NameBuilder()
    .addRDN(BCStyle.CN, "Mambo Org")
    .build();

BigInteger serialNumber = new BigInteger(128, new SecureRandom());

ZonedDateTime now = ZonedDateTime.now();
Date notBefore = Date.from(now.toInstant());
Date notAfter = Date.from(now.plus(30, ChronoUnit.YEARS).toInstant());
SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(rootKeyPair.getPublic().getEncoded());

X509CertificateHolder rootCertHolder = new JcaX509v3CertificateBuilder(issuer, serialNumber, notBefore, notAfter, issuer, publicKeyInfo)
                .addExtension(Extension.basicConstraints, true, new BasicConstraints(true))
                .addExtension(Extension.subjectKeyIdentifier, false, x509ExtensionUtils.createSubjectKeyIdentifier(rootKeyPair.getPublic()))
                .build(new JcaContentSignerBuilder(signatureAlgorithm).build(rootKeyPair.getPrivate()));

X509Certificate rootCert = x509CertificateConverter.getCertificate(rootCertHolder);
X500Name

์ž๋ฐ”์—์„œ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณตํ•˜๋Š” X500Principal ๋Œ€์‹ ์— BouncyCastle์˜ X500Name ํด๋ž˜์Šค๋กœ FQDN, ๋ฐœ๊ธ‰์ž์™€ ์†Œ์œ ์ž์™€ ๊ฐ™์€ ์ฃผ์ฒด(Subject) ์ •๋ณด๋ฅผ ๊ธฐ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋˜ํ•œ, RDN(Relative Distinguished Names) ๋ผ๊ณ ๋„ ํ•˜๋Š”๋ฐ X500NameBuilder์—์„œ๋Š” addRDN ์ด๋ผ๋Š” ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•œ๋‹ค. ๋” ์ž์„ธํ•œ ๋‚ด์šฉ์€ What is a Distinguished Name (DN)?๋ฅผ ์ฐธ๊ณ ํ•ด๋ณด์ž.

SubjectPublickeyInfo

X.509 ์ธ์ฆ์„œ์— ํฌํ•จ๋˜๋Š” SubjectPublickeyInfo์— ๋Œ€ํ•œ ํด๋ž˜์Šค๋กœ DER ์ธ์ฝ”๋”ฉ๋œ ๊ณต๊ฐœํ‚ค๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

BasicConstraints

๋‹ค๋ฅธ ์ธ์ฆ์„œ๋ฅผ ๋ฐœ๊ธ‰ํ•  ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฒƒ์œผ๋กœ CA ์ธ์ฆ์„œ๋ผ๋Š” ๊ฒƒ์„ ์˜๋ฏธ๋กœ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

X.509 ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ์„œ ๋ฐœ๊ธ‰ํ•˜๊ธฐ

์•ž์„œ ๋ฃจํŠธ CA ์ธ์ฆ์„œ ๋ฐœ๊ธ‰์„ ์ดํ•ดํ•˜์˜€๋‹ค๋ฉด CA ์ธ์ฆ์„œ๋ฅผ ๋งŒ๋“œ๋Š”๋ฐ ์‚ฌ์šฉ๋œ ๋น„๋ฐ€ํ‚ค๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์œ„ํ•œ X.509 ์ธ์ฆ์„œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ์•Œ์•„๋ณด๋„๋ก ํ•˜์ž.

KeyPair clientKeyPair = keyPairGenerator.generateKeyPair();

X500Name client = new X500NameBuilder()
    .addRDN(BCStyle.CN, "Mambo")
    .build();

BigInteger clientSN = new BigInteger(128, new SecureRandom());
ZonedDateTime clientNow = ZonedDateTime.now();
Date clientNotBefore = Date.from(clientNow.toInstant());
Date clientNotAfter = Date.from(clientNow.plus(1, ChronoUnit.YEARS).toInstant());
SubjectPublicKeyInfo clientPublicKeyInfo = SubjectPublicKeyInfo.getInstance(clientKeyPair.getPublic().getEncoded());

X509CertificateHolder clientCertHolder = new JcaX509v3CertificateBuilder(issuer, clientSN, clientNotBefore, clientNotAfter, client, clientPublicKeyInfo)
    .addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
    .addExtension(Extension.authorityKeyIdentifier, false, x509ExtensionUtils.createAuthorityKeyIdentifier(rootCert))
    .addExtension(Extension.subjectKeyIdentifier, false, x509ExtensionUtils.createSubjectKeyIdentifier(clientKeyPair.getPublic()))
    .addExtension(Extension.keyUsage, false, new KeyUsage(KeyUsage.digitalSignature))
    .build(new JcaContentSignerBuilder(signatureAlgorithm).build(rootKeyPair.getPrivate()));

X509Certificate clientCert = x509CertificateConverter.getCertificate(clientCertHolder);

BasicConstraints๋กœ CA๊ฐ€ ์•„๋‹ˆ๋ฉฐ AuthorityKeyIdentifier๋กœ ๋ฐœ๊ธ‰์ž์˜ ๊ณต๊ฐœํ‚ค๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค. ๊ทธ๋ฆฌ๊ณ  KeyUsage๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐœ๊ธ‰ํ•œ X.509 ์ธ์ฆ์„œ๊ฐ€ ์ „์ž์„œ๋ช… ์šฉ๋„์ž„์„ ํ™•์žฅ(Extension)์— ๋ช…์‹œํ–ˆ๋‹ค.

X509Certificate ์ธ์ฆ์„œ ๊ฒ€์ฆํ•˜๊ธฐ

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญ ์‹œ ํฌํ•จํ•ด์„œ ์ „๋‹ฌํ•œ X.509 ์ธ์ฆ์„œ๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ˜น์€ ์‹œ์Šคํ…œ์—์„œ ๋ฐœ๊ธ‰ํ•œ ๊ฒƒ์ธ์ง€๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๊ณผ์ •์ด ํ•„์š”ํ•˜๋‹ค. java.security.cert.X509Certificate ํด๋ž˜์Šค์—๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ตฌํ˜„๋œ verify ํ•จ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ•˜๊ฒŒ ๋ฃจํŠธ ์ธ์ฆ์„œ์˜ ๊ณต๊ฐœํ‚ค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ์„œ๋ฅผ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค.

clientCert.checkValidity();
clientCert.verify(rootCert.getPublicKey(), securityProvider);

X.509 ์ธ์ฆ์„œ ๋ฐ ํ‚ค ํŽ˜์–ด ์ €์žฅํ•˜๊ธฐ

์ผ๋ฐ˜์ ์œผ๋กœ X.509 ์ธ์ฆ์„œ์— ํฌํ•จ๋˜๋Š” ๊ณต๊ฐœํ‚ค๋Š” X509EncodedSpec์— ๋”ฐ๋ผ ๋ฐ”์ด๋„ˆ๋ฆฌ ํ˜•ํƒœ์˜ DER๋กœ ์ธ์ฝ”๋”ฉ๋˜์–ด ํฌํ•จ๋œ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ X.509 ์ธ์ฆ์„œ์™€ ํ‚ค ํŽ˜์–ด๋ฅผ ๊ตํ™˜ํ•  ๋•Œ์—๋Š” PEM ํŒŒ์ผ ํ˜•์‹์„ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ํŽธ์œผ๋กœ BouncyCastle ์ž๋ฐ” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ํฌํ•จ๋œ PemWriter์™€ PemObject๋ฅผ ํ™œ์šฉํ•ด์„œ X.509 ์ธ์ฆ์„œ์™€ ๋น„๋ฐ€ํ‚ค๋ฅผ PEM ํŒŒ์ผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.


public class PemUtil {
    private PemUtil() {
    }

    public static String toPem(String type, byte[] encoded) throws IOException {
        try (StringWriter writer = new StringWriter();
             PemWriter pemWriter = new JcaPEMWriter(writer)) {
            pemWriter.writeObject(new PemObject(type, encoded));
            pemWriter.flush();
            return writer.toString();
        }
    }

    public static String toPem(PrivateKey privateKey) throws IOException {
        Assert.notNull(privateKey, "private key is required");
        return toPem("PRIVATE KEY", privateKey.getEncoded());
    }

    public static String toPem(X509Certificate certificate) throws IOException, CertificateEncodingException {
        Assert.notNull(certificate, "certificate is required");
        return toPem("CERTIFICATE", certificate.getEncoded());
    }
}


String rootCertPem = PemUtil.toPem(rootCert); //ca.pem
String clientCertPem = PemUtil.toPem(clientCert); //client.pem
String clientPrivateKeyPem = PemUtil.toPem(clientKeyPair.getPrivate()); //client.key

PEM ํ˜•์‹์œผ๋กœ Base64๋กœ ๊ตฌ์„ฑ๋œ ๋ฌธ์ž์—ด์„ ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ณธ ๊ธ€์—์„œ ๋‹ค๋ฃจ์ง€ ์•Š๋Š”๋‹ค.

X.509 ์ธ์ฆ์„œ ๋ฐ ํ‚ค ํŽ˜์–ด ๋ถˆ๋Ÿฌ์˜ค๊ธฐ

ํด๋ผ์ด์–ธํŠธ์˜ X.509 ์ธ์ฆ์„œ๋Š” ์š”์ฒญ์— ํฌํ•จ๋˜์–ด HttpServletRequest๋กœ ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ ์ธ์ฆ์„œ ๊ฒ€์ฆ์„ ์œ„ํ•œ CA ์ธ์ฆ์„œ์™€ ๋น„๋ฐ€ํ‚ค๋Š” ๋ณ„๋„์˜ ํŒŒ์ผ์ด๋‚˜ ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ ๋ถˆ๋Ÿฌ์™€์•ผํ•œ๋‹ค. ๊ณต๊ฐœํ‚ค์™€ ๋น„๋ฐ€ํ‚ค์— ๋Œ€ํ•ด์„œ๋Š” JcaPEMKeyConverter๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

public class PemUtil {
    private PemUtil() {
    }

    public static PemObject loadPem(byte[] encoded) throws IOException {
        try (StringReader reader = new StringReader(new String(encoded, StandardCharsets.UTF_8));
             PEMParser pemParser = new PEMParser(reader)) {
            return pemParser.readPemObject();
        }
    }

    private static Object loadObject(byte[] encoded) throws IOException {
        try (StringReader reader = new StringReader(new String(encoded, StandardCharsets.UTF_8));
             PEMParser pemParser = new PEMParser(reader)) {
            return pemParser.readObject();
        }
    }

    public static X509Certificate loadCertificate(byte[] encoded) throws IOException, CertificateException {
        PemObject obj = loadPem(encoded);
        CertificateFactory factory = CertificateFactory.getInstance("X.509");
        return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(obj.getContent()));
    }

    public static PublicKey loadPublicKey(byte[] encoded) throws IOException {
        Object obj = loadObject(encoded);
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
        SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(obj);
        return converter.getPublicKey(publicKeyInfo);
    }

    public static PrivateKey loadPrivateKey(byte[] encoded) throws IOException {
        Object obj = loadObject(encoded);
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
        PrivateKeyInfo privateKeyInfo = PrivateKeyInfo.getInstance(obj);
        return converter.getPrivateKey(privateKeyInfo);
    }
}

๋ณธ ๊ธ€์—์„œ ๋‹ค๋ฃฌ ์˜ˆ์ œ ์ฝ”๋“œ์— ๋Œ€ํ•œ ๋ณด๋‹ค ์ž์„ธํ•œ ๊ฒƒ์€ X509Test.java๋ฅผ ํ†ตํ•ด ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

์ฐธ๊ณ  ๋งํฌ