BouncyCastle Java๋ก X.509 ์ธ์ฆ์ ๋ง๋ค๊ธฐ
์๋ฐ์์ ์ํธํ ๋๋ ์ ์์๋ช ์ ์ํ ๊ธฐ๋ฅ์ JCA๋ฅผ ํตํด ์ ๊ณตํ๊ธฐ๋ ํ์ง๋ง ์ค์ง์ ์ผ๋ก๋ Bouncy Castle์ด๋ผ๋ ์ํธ ๊ด๋ จ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํ๋ค. ๋ณธ ๊ธ์์๋ BouncyCastle์์ ์ ๊ณตํ๋ ์ฌ๋ฌ ํด๋์ค๋ค์ ํ์ฉํด์ ์ฌ์ค ๋ฃจํธ ์ธ์ฆ์๋ฅผ ๋ง๋ค๊ณ ๊ทธ๊ฒ์ ๊ธฐ๋ฐ์ผ๋ก ํด๋ผ์ด์ธํธ๋ฅผ ์ํ X.509 ์ธ์ฆ์๋ฅผ ๋ฐ๊ธํ๊ธฐ ์ํ ๋ฐฉ๋ฒ์ ์ ๋ฆฌํ๊ณ ์ ํ๋ค. ์ด๋ ๊ฒ ๋ง๋ค์ด์ง๋ X.509 ์ธ์ฆ์๋ ๋ณด์ ๋ ๋ฒจ์ด ๋์ ์์คํ ์์ Mutual TLS ์ธ์ฆ์ ์ํํ ์ ์๋ค.
build.gradledependencies { 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๋ฅผ ํตํด ํ์ธํ ์ ์๋ค.