2024年10月8日 星期二

Java 使用 BouncyCastleProvider 從 CA Certificate 簽出子證書

要做到自動化使用 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;
}


沒有留言:

張貼留言

© Mac Taylor, 歡迎自由轉貼。
Background Email Pattern by Toby Elliott
Since 2014