샘플 파일 test.cer과 test.p12를 이용하여 PFX 인증서를 생성합니다.
(공인인증서 der 파일과 key 파일을 적용해도 가능합니다.)
package PFXTestPackage;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
public class PFXTest {
static String certificatePath = "test.cer";
static String privateKeyPath = "test.p12";
static char[] p12Password = "password".toCharArray();
public static void main(String[] args) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
X509Certificate certificate = (X509Certificate) certFactory
.generateCertificate(new FileInputStream(certificatePath));
KeyPairGenerator rsaGen = KeyPairGenerator.getInstance("RSA");
final KeyPair pair = rsaGen.generateKeyPair();
// pkcs12 key store 생성
KeyStore pkcs12 = KeyStore.getInstance("PKCS12");
pkcs12.load(null, null);
pkcs12.setKeyEntry("privatekeyalias", pair.getPrivate(), "entrypassphrase".toCharArray(),
new Certificate[] { certificate });
// PFX 파일 저장
try (FileOutputStream p12 = new FileOutputStream("PFXTest1.pfx")) {
pkcs12.store(p12, "password".toCharArray());
}
}
}
BerReader로 인증서를 열어보면 다음과 같습니다.
encryptedData 구조에 pbeWithSHAAnd40BitRC2-CBC 알고리즘이 default로 사용되는 것을 알 수 있습니다.
다음으로 샘플 파일을 이용하지 않고 인증서를 직접 생성합니다. 다음 블로그에서 참고했습니다.
https://gamlor.info/posts-output/2019-10-29-java-create-certs-bouncy/en/
package PFXTestPackage;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
public class PFXTest {
public static void main(String[] args) throws Exception {
GeneratedCert rootCA = createCertificate("Test_rootCA", /* domain= */null, /* issuer= */null, /* isCa= */true);
GeneratedCert issuer = createCertificate("Test_issuer", /* domain= */null, rootCA, /* isCa= */true);
GeneratedCert domain = createCertificate("JooLib", "JooLib", issuer, /* isCa= */false);
GeneratedCert otherD = createCertificate("test", "test", issuer, /* isCa= */false);
char[] Password = "password".toCharArray();
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// Key store expects a load first to initialize.
keyStore.load(null, Password);
// domain 인증서에 개인키와 인증서 체인을 저장한다.
keyStore.setKeyEntry("JooLib", domain.privateKey, Password,
new X509Certificate[] { domain.certificate, issuer.certificate, rootCA.certificate });
keyStore.setKeyEntry("test", otherD.privateKey, Password,
new X509Certificate[] { otherD.certificate, issuer.certificate, rootCA.certificate });
X509Certificate[] certs = new X509Certificate[] { rootCA.certificate, domain.certificate };
try (FileOutputStream store = new FileOutputStream("PFXTest2.pfx")) {
keyStore.store(store, Password);
}
}
// 인증서 체인을 생성하기 위해 발급자(issuer)의 인증서와 개인키를 가지고 있어야 한다.
final static class GeneratedCert {
public final PrivateKey privateKey;
public final X509Certificate certificate;
public GeneratedCert(PrivateKey privateKey, X509Certificate certificate) {
this.privateKey = privateKey;
this.certificate = certificate;
}
}
/**
* @param cnName The CN={name} of the certificate. When the certificate is for a
* domain it should be the domain name
* @param domain Nullable. The DNS domain for the certificate.
* @param issuer Issuer who signs this certificate. Null for a self-signed
* certificate
* @param isCA Can this certificate be used to sign other certificates
* @return Newly created certificate with its private key
*/
private static GeneratedCert createCertificate(String cnName, String domain, GeneratedCert issuer, boolean isCA)
throws Exception {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
KeyPair certKeyPair = keyGen.generateKeyPair();
X500Name name = new X500Name("CN=" + cnName);
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
Instant validFrom = Instant.now();
Instant validUntil = validFrom.plus(10 * 360, ChronoUnit.DAYS);
// issuer가 없을 시 self-sign
X500Name issuerName;
PrivateKey issuerKey;
if (issuer == null) {
issuerName = name;
issuerKey = certKeyPair.getPrivate();
} else {
issuerName = new X500Name(issuer.certificate.getSubjectDN().getName());
issuerKey = issuer.privateKey;
}
// 인증서 정보 빌드
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(issuerName, serialNumber,
Date.from(validFrom), Date.from(validUntil), name, certKeyPair.getPublic());
if (isCA) {
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(isCA));
}
// DNS 주소
if (domain != null) {
builder.addExtension(Extension.subjectAlternativeName, false,
new GeneralNames(new GeneralName(GeneralName.dNSName, domain)));
}
// 인증서 서명
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA").build(issuerKey);
X509CertificateHolder certHolder = builder.build(signer);
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
return new GeneratedCert(certKeyPair.getPrivate(), cert);
}
}
마찬가지로 BerReader로 열어봅시다. 여기서 Octet String을 확장하면 값이 잘 들어갔음을 확인할 수 있습니다.
PFX 파일을 이와 같이 만들 수 있습니다.
여기서 PKCS7 EncryptedData Type의 알고리즘을 변경해봅시다.
The Bouncy Castle FIPS Java API in 100 Examples (Final Draft)을 참고하였습니다.
public static byte[] createPfxPdu(char[] passwd, PrivateKey privKey, X509Certificate[] certs)
throws GeneralSecurityException, OperatorCreationException, PKCSException, IOException {
JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
PKCS12SafeBagBuilder caCertBagBuilder = new JcaPKCS12SafeBagBuilder(certs[1]);
caCertBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName,
new DERBMPString("CA Certificate"));
// store the key certificate
PKCS12SafeBagBuilder eeCertBagBuilder = new JcaPKCS12SafeBagBuilder(certs[0]);
eeCertBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName,
new DERBMPString("End Entity Key"));
eeCertBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
extUtils.createSubjectKeyIdentifier(certs[0].getPublicKey()));
// store the private key
PKCS12SafeBagBuilder keyBagBuilder = new JcaPKCS12SafeBagBuilder(privKey,
new JcePKCSPBEOutputEncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC)
.setProvider("BC").build(passwd));
keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString("End Entity Key"));
keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
extUtils.createSubjectKeyIdentifier(certs[0].getPublicKey()));
// create the actual PKCS#12 blob.
PKCS12PfxPduBuilder pfxPduBuilder = new PKCS12PfxPduBuilder();
PKCS12SafeBag[] safeBags = new PKCS12SafeBag[2];
safeBags[0] = eeCertBagBuilder.build();
safeBags[1] = caCertBagBuilder.build();
pfxPduBuilder
.addEncryptedData(new JcePKCSPBEOutputEncryptorBuilder(PKCSObjectIdentifiers.pbeWithSHAAnd128BitRC2_CBC)
.setProvider("BC").build(passwd), safeBags);
pfxPduBuilder.addData(keyBagBuilder.build());
return pfxPduBuilder.build(new JcePKCS12MacCalculatorBuilder().setProvider("BC"), passwd).getEncoded();
}
public static void main(String[] args) throws Exception {
GeneratedCert rootCA = createCertificate("do_not_trust_test_certs_root", /* domain= */null, /* issuer= */null,
/* isCa= */true);
GeneratedCert issuer = createCertificate("do_not_trust_test_certs_issuer", /* domain= */null, rootCA,
/* isCa= */true);
GeneratedCert domain = createCertificate("local.gamlor.info", "local.gamlor.info", issuer, /* isCa= */false);
char[] Password = "password".toCharArray();
// create the actual PKCS#12 blob.
X509Certificate[] certs = new X509Certificate[] { rootCA.certificate, domain.certificate };
byte[] pdu = createPfxPdu(Password, domain.privateKey, certs);
System.out.println(Hex.toHexString(pdu));
try (FileOutputStream store = new FileOutputStream("PFXTest3.pfx")) {
store.write(pdu);
}
}
createPfxPdu 함수에서 PKCSObjectIdentifiers.pbeWithSHAAnd128BitRC2_CBC의 알고리즘을 변경할 수 있습니다.
bouncycastle에서 지원 가능한 알고리즘은 다음과 같습니다.
- pbeWithSHAAnd128BitRC2_CBC
- pbeWithSHAAnd128BitRC4
- pbeWithSHAAnd2_KeyTripleDES_CBC
- pbeWithSHAAnd3_KeyTripleDES_CBC
- pbewithSHAAnd40BitRC2_CBC (default)
- pbeWithSHAAnd40BitRC4
- AES128_CBC
- AES192_CBC
- AES256_CBC
- SEED_CBC
openssl에서는 다음 알고리즘을 지원합니다.
- pbewithSHAAnd40BitRC2_CBC (default)
- pbeWithSHAAnd3_KeyTripleDES_CBC
- AES-128-CBC
- AES-192-CBC
- AES-256-CBC
AES CBC 모드는 Windows에서 열리지만 Mac에서는 열리지 않습니다.
AES 및 SEED 사용시 구조가 변경됩니다.
예시)
JcePKCSPBEOutputEncryptorBuilder(CMSAlgorithm.AES128_CBC)
bouncycastle을 이용하여 PFX 인증서 생성 및 지원하는 알고리즘을 확인했습니다.
잘못된 내용이나 궁금하신 내용을 알려주시면 감사하겠습니다.
'개발' 카테고리의 다른 글
ulimit 명령어와 Select, Poll 함수 (0) | 2020.10.12 |
---|---|
The file couldn’t be opened because you don’t have permission to view it. (0) | 2020.10.07 |
호출 규약 (0) | 2020.09.22 |
BouncyCastle (0) | 2020.06.19 |
구조체 메모리 할당 (0) | 2020.05.21 |