要做到自動化使用 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;
}
沒有留言:
張貼留言