要做到自動化使用 Root CA 頒發簽名的方式,除了可以直接讓程式去執行 Terminal 的 Command Line,也可以直接用內建的工具直接簽發,好處是一站式完成,不需要另外處理呼叫的問題。
由於加密演算法需要額外掛上,這裡使用的是 bouncycastle 的 Library 完成的,以本篇記載的方式是使用以下版本,需要安裝到 pom.xml 的 Library:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on<artifactId> <version>1.70</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on<artifactId> <version>1.70</version> </dependency>
使用 Java 證書簽名需要了解的各個部分
1. PEM 格式與轉換
一般常見的 Certificate 表示出來的檔案內容是類似:
-----BEGIN CERTIFICATE-----
....
-----END CERTIFICATE-----
其他的還有 BEGIN RSA PRIVATE KEY, BEGIN CERTIFICATE REQUEST 等。
這裡面的表示法幾乎是 PEM (後綴可能會是 .pem, .crt),他是 Base64 + ASCII 製作成的編碼,這裡要把 ----CERTIFICATE---- 這些去掉,然後放進去解碼,但是 PEMParser 或 X509Certificate 內部就會完成這件事情,首先是針對 Public Key 的讀取:
public static X509Certificate readPublicKey(String input) { ByteArrayInputStream caInputStream = new ByteArrayInputStream(input.getBytes()); CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) certFactory.generateCertificate(caInputStream); return cert.getPublicKey(); } X509Certificate publicKey = readRootCACertificate("----BEGIN CERTIFICATE....etc");
2. 讀取 Root CA Public Key Certificate
這個方法是用來讀取 Root CA 檔案的,假設 content 就是 CA Certificate 檔案的字串內容:
public static X509Certificate readRootCACertificate(String content) { Security.addProvider(new BouncyCastleProvider()); PublicKey caPublicKey = readPublicKey(content); CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); X509Certificate crt = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(content.getBytes())); return crt; } X509Certificate rootCAPublicKey = readRootCACertificate("----BEGIN CERTIFICATE....etc");
3. 讀取 Root CA Private Key
這個方法是用來讀取 Private Key 的,由於 PKCS8 / PKCS1 的 PrivateKey 還會再加上一層密碼,所以會需要分出不同的格式處理,如果是沒有加密碼的版本,就會落到最後一個情形。
public static PrivateKey readPrivateKey(String s, char[] password) { Security.addProvider(new BouncyCastleProvider()); PrivateKeyInfo privateKeyInfo; try (PEMParser pemParser = new PEMParser(new StringReader(s))) { Object obj = pemParser.readObject(); if (obj instanceof PKCS8EncryptedPrivateKeyInfo) { PKCS8EncryptedPrivateKeyInfo epki = (PKCS8EncryptedPrivateKeyInfo) o; JcePKCSPBEInputDecryptorProviderBuilder builder = new JcePKCSPBEInputDecryptorProviderBuilder().setProvider("BC"); InputDecryptorProvider idp = builder.build(password); privateKeyInfo = epki.decryptPrivateKeyInfo(idp); } else if (obj instanceof PEMEncryptedKeyPair) { // pkcs1-format PEMEncryptedKeyPair epki = (PEMEncryptedKeyPair) o; PEMKeyPair pkp = epki.decryptKeyPair(new BcPEMDecryptorProvider(password)); privateKeyInfo = pkp.getPrivateKeyInfo(); } else if (obj instanceof PEMKeyPair) { // non-encrypted private key PEMKeyPair pkp = (PEMKeyPair) o; privateKeyInfo = pkp.getPrivateKeyInfo(); } else { throw new PKCSException("Unknown Private Key Format"); } JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); return converter.getPrivateKey(pki); } } PrivateKey rootCAPrivateKey = readPrivateKey("----BEGIN RSA PRIVATE KEY....etc", "MY-PASSWORD");
4. 讀取 CSR Certificate
這個是讀取 CSR 的方法:
public static PKCS10CertificationRequest readCSR(String content) { Security.addProvider(new BouncyCastleProvider()); PEMParser parser = new PEMParser(new StringReader(content)); Object obj = parser.readObject(); parser.close(); // 檢查 pem 符合哪一種格式,直接用 instanceof 來判定 if (obj instanceof CertificationRequest) { CertificationRequest csr = (CertificationRequest) obj; return new PKCS10CertificationRequest(csr); } if (obj instanceof PKCS10CertificationRequest) { return (PKCS10CertificationRequest) obj; } throw new CertificateException("CSR is not valid"); } } PKCS10CertificationRequest csr = readCSR("----BEGIN CERTIFICATE REQUEST......etc");
5. 產生 CSR
產生 CSR 需要注意,流程是先去建立 KeyPair,這是由 Public Key + Private Key 組成的對應金鑰,然後再用 Subject 附加訊息 Encode 到 CSR 文件上。
// 產生一個隨機的 KeyPair (Public Key 和 Private Key) protected static KeyPair generateKeyPair() { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); return keyPairGenerator.generateKeyPair(); } // 產生一個 CSR protected static PKCS10CertificationRequest generateCSR(KeyPair keyPair, String subjectDN) { Security.addProvider(new BouncyCastleProvider()); // 加入 subject (CN/City/Country...etc) 到上面 X500Name certificateSubj = new X500Name(subjectDN); // 用 subject 和 public key 加入證書產生的資訊列 PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder(certificateSubj, keyPair.getPublic()); JcaContentSignerBuilder csrSignerBuilder = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC"); // 把 private key 設定為簽章方式 ContentSigner signer = csrSignerBuilder.build(keyPair.getPrivate()); // 產生 CSR return p10Builder.build(signer); } KeyPair kp = generateKeyPair(); PKCS10CertificateRequest csr = generateCSR(kp, "CN=localhost");
CA 簽發主流程
準備簽發證書的變數:
// Root CA 簽名 CSR 的方式 // 1. 先讀取 CA Public Key X509Certificate rootCACert = readRootCACertificate("rootca.crt 文字內容"); // 2. 讀取 CA Private Key PrivateKey rootCAPrivateKey = readPrivateKey(key文件內容字串, "密碼"); // 3. 讀取 CSR PKCS10CertificationRequest userProvideCSR = readCSR("CSR文件文字內容");
// 4. 正式簽名 CSR X509Certificate newCertificate = sign(csr, rootCACert, rootCAPrivateKey);
Signing CSR 呼叫流程
public static X509Certificate sign(PKCS10CertificationRequest userProvideCSR, X509Certificate rootCAPublicCertificate, PrivateKey rootCAPrivateKey) { Security.addProvider(new BouncyCastleProvider()); // 新的證書的序列號碼 BigInteger newCertificateSerialNumber = new BigInteger(Long.toString(new SecureRandom().nextLong())); // CA 的 Domain Name (拿原始 Root CA 簽名的 Domain Name 作為原始的 X500Name 名稱 String rootCertificateDomainName = rootCAPublicCertificate.getIssuerDN().getName(); X500Name rootCertIssuer = new X500Name(rootCertificateDomainName); // 證書建立工具 X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder( rootCertIssuer, newCertificateSerialNumber, // 從現在起有效 Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()), // 10 年才到期 Date.from(LocalDateTime.now().plusYears(10).atZone(ZoneId.systemDefault()).toInstant()), // user 提供要簽名的 CSR 其中的 Subject 訊息 userProvideCSR.getSubject(), // user 提供要簽名的 CSR 本身的 public key userProvideCSR.getSubjectPublicKeyInfo() ); // 簽發證書要附屬的 Extension JcaX509ExtensionUtils certificateExtensionUtils = new JcaX509ExtensionUtils(); // 增加一些證書附加的訊息 // 這個新增進去的 BasicConstraints 說明這個簽名出來的證書不是一個 CA (也不可以當作 CA) certificateBuilder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false)); // 把 Root CA 的 Public Key 加入到簽名證書的 AIA 識別號碼,這樣證書顯示就可以跟 Root CA 建立聯級關係 certificateBuilder.addExtension(Extension.authorityKeyIdentifier, false, certificateExtensionUtils.createAuthorityKeyIdentifier(rootCAPublicCertificate.getPublicKey())); // ? // 加入 Subject Key certificateBuilder.addExtension(Extension.subjectKeyIdentifier, false, certificateExtensionUtils.createSubjectKeyIdentifier(userProvideCSR.getSubjectPublicKeyInfo())); // 加入這個 Key 的可使用方式 certificateBuilder.addExtension(Extension.keyUsage, false, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature)); // 把 alternative name 名稱加入,就是除了 CSR 上的 Common Name (CN) 以外,額外可以提出信任憑證的網域名稱或是 IP,這是給 SSL / TLS (HTTPS) 上用的 certificateBuilder.addExtension(Extension.subjectAlternativeName, false, new DERSequence(new ASN1Encodable[]{ new GeneralName(GeneralName.dNSName, "localhost"), new GeneralName(GeneralName.iPAddress, "127.0.0.1") })); // 額外加入可以使用的方式 (用來做 mTLS 的 Client Auth 提出 Request)、(用來做 Server 的 Server Auth TLS/HTTPS 開 Server 用的憑證認證) final ExtendedKeyUsage extKeyUsage = new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}); certificateBuilder.addExtension(Extension.extendedKeyUsage, false, extKeyUsage); // 把一些 CRL 資訊加上, DERIA5String crlUriDer = new DERIA5String("http://cr.example.com/ca.crl"); GeneralName gn = new GeneralName(GeneralName.uniformResourceIdentifier, crlUriDer); DERSequence gnDer = new DERSequence(gn); GeneralNames gns = GeneralNames.getInstance(gnDer); DistributionPointName dpn = new DistributionPointName(0, gns); DistributionPoint distp = new DistributionPoint(dpn, null, null); DERSequence distpDer = new DERSequence(distp); certificateBuilder.addExtension(Extension.cRLDistributionPoints, false, distpDer); // 把 OSCP 資訊加上 GeneralName ocspName = new GeneralName(GeneralName.uniformResourceIdentifier, "http://ocsp.example.com/"); AccessDescription ocspAccessPoint = new AccessDescription(AccessDescription.id_ad_ocsp, ocspName); // 把 root ca 原來的證書位置資訊加上 GeneralName authorityInfoAccessUrl = new GeneralName(GeneralName.uniformResourceIdentifier, "http://example.com/rootca.crt"); AccessDescription caIssuerAccessPoint = new AccessDescription(AccessDescription.id_ad_caIssuers, authorityInfoAccessUrl); // 剛才兩個 OCSP / Access Point 都是 Access Description 的內容,要加上到證書的建立器上 AuthorityInformationAccess authorityInformationAccessPoint = new AuthorityInformationAccess(new AccessDescription[]{caIssuerAccessPoint, ocspAccessPoint}); certificateBuilder.addExtension(Extension.authorityInfoAccess, false, authorityInformationAccessPoint); // 限定可使用名稱: 限定固定的 DNS 名稱或 IP 名稱才可以做憑證的認證 GeneralSubtree[] permittedNameConstraints = new GeneralSubtree[]{ // 還有其他的名稱類型像是: otherName, rfc822Name, dNSName, x400Address, directoryName, ediPartyName, uniformResourceIdentifier, iPAddress, registeredID new GeneralSubtree(new GeneralName(GeneralName.dNSName, "example.com")), new GeneralSubtree(new GeneralName(GeneralName.iPAddress, "10.0.0.5/32")) }; // 限定要排除的名稱: 這些名稱不可以用於憑證使用 GeneralSubtree[] excludedNameConstraints = new GeneralSubtree[]{ new GeneralSubtree(new GeneralName(GeneralName.iPAddress, "0.0.0.0/0.0.0.0")), new GeneralSubtree(new GeneralName(GeneralName.iPAddress, "0:0:0:0:0:0:0:0/0:0:0:0:0:0:0:0")), new GeneralSubtree(new GeneralName(GeneralName.iPAddress, "127.0.0.1/32")), new GeneralSubtree(new GeneralName(GeneralName.dNSName, "localhost")), }; // 加入要限定、排除的名稱限制 certificateBuilder.addExtension(Extension.nameConstraints, true, new NameConstraints(permittedNameConstraints, excludedNameConstraints)); // 載入 Root CA 的 Private Key, 簽名的 Algorithm 是 SHA256withRSA, Provider 是 BC (BouncyCastle) JcaContentSignerBuilder caSignerBuilder = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC"); // 使用了 Root CA 的 Private Key 載入簽名前的準備 ContentSigner csrContentSigner = caSignerBuilder.build(rootCAPrivateKey); // 簽名,得到新的證書,這裡是指證書的持有者這個變數 X509CertificateHolder newCertificateHolder = certificateBuilder.build(csrContentSigner); // 把證書的類型轉換成 BC 的類型 X509Certificate newCertificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(newCertificateHolder); // 驗證這個 newCertificate 是不是來自 rootCA 簽發的 newCertificate.verify(rootCAPublicCertificate.getPublicKey(), "BC"); return newCertificate; }
沒有留言:
張貼留言