개발

PFX 인증서 생성

샘플 파일 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/

 

Gamlor's Blog: Create HTTPS Certificates in Java with Bouncy Castle

First, we need to generate a self-signed certificate. We use the CertAndKeyGen, set the key size and generate the certificate with the desired name. Then we add extra bits of information, like if the certificate can sign other certificates and the DNS name

gamlor.info

 

 

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