You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Bouncy Castle在Java中基于OpenSSL CSR签发证书遇阻

实现与OpenSSL x509命令一致的CSR签发功能

背景

已通过以下OpenSSL命令生成CA证书与密钥:

openssl genrsa -aes256 -out $CANAME.key 4096
openssl req -x509 -new -nodes -key $CANAME.key -sha256 -days 1826 -out $CANAME.crt -subj '/CN=MyOrg Root CA/C=AT/ST=Vienna/L=Vienna/O=MyOrg'

编写了基于BouncyCastle的SignService类用于签发CSR,但signCRT方法返回的是PKCS#7格式的字符串,无法直接查看或验证证书信息。而使用同一份CSR,以下OpenSSL命令能生成可正常查看的X509证书,且体积小很多:

openssl x509 -req -in test.csr -CA $CANAME.crt -CAkey $CANAME.key -CAcreateserial -out openssl-signed.crt -days 730 -sha256

需要修改Java代码,实现与上述OpenSSL命令完全一致的功能。

当前Java代码

public class SignService {

    private PrivateKey privateKey;
    private X509CertificateHolder certificateHolder;

    public SignService(
            String caPathKey,
            String caCert,
            String caKeyPassword
    ) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        loadPrivateKey(caPathKey, caKeyPassword);

        PEMParser pemParser = new PEMParser(new FileReader(caCert));
        this.certificateHolder = (X509CertificateHolder) pemParser.readObject();

    }

    private void loadPrivateKey(String caPathKey, String caKeyPassword) throws IOException {
        PEMParser pemParser = new PEMParser(new FileReader(caPathKey));
        Object object = pemParser.readObject();
        JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
        KeyPair kp;
        if (object instanceof PEMEncryptedKeyPair)
        {
            // Encrypted key - we will use provided password
            PEMEncryptedKeyPair ckp = (PEMEncryptedKeyPair) object;
            PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(caKeyPassword.toCharArray());
            kp = converter.getKeyPair(ckp.decryptKeyPair(decProv));
        }
        else
        {
            // Unencrypted key - no password needed
            PEMKeyPair ukp = (PEMKeyPair) object;
            kp = converter.getKeyPair(ukp);
        }

        this.privateKey = kp.getPrivate();
    }

    public String signCRT(String crs_str) throws NoSuchProviderException, IOException, KeyStoreException, NoSuchAlgorithmException, OperatorCreationException, CMSException {

        PemReader p = new PemReader(new StringReader(crs_str));
        PemObject pemObject = p.readPemObject();
        PKCS10CertificationRequest csr = new PKCS10CertificationRequest(pemObject.getContent());

        AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA1withRSA");
        AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
        X500Name issuer = certificateHolder.getIssuer();
        BigInteger serial = new BigInteger(32, new SecureRandom());
        Date from = new Date();
        Date to = new Date(System.currentTimeMillis() + (365 * 86400000L));

        X509v3CertificateBuilder certgen = new X509v3CertificateBuilder(issuer, serial, from, to, csr.getSubject(), csr.getSubjectPublicKeyInfo());
        certgen.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
        certgen.addExtension(X509Extension.subjectKeyIdentifier, false, new SubjectKeyIdentifier(csr.getSubjectPublicKeyInfo().getEncoded()));
        certgen.addExtension(X509Extension.authorityKeyIdentifier, false, new AuthorityKeyIdentifier(new GeneralNames(new GeneralName(this.certificateHolder.getSubject())), certificateHolder.getSerialNumber()));

        ContentSigner signer = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(PrivateKeyFactory.createKey(this.privateKey.getEncoded()));
        X509CertificateHolder holder = certgen.build(signer);
        byte[] certencoded = holder.toASN1Structure().getEncoded();

        CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
        signer = new JcaContentSignerBuilder("SHA1withRSA").build(this.privateKey);
        generator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(signer, this.certificateHolder));
        generator.addCertificate(new X509CertificateHolder(certencoded));
        generator.addCertificate(new X509CertificateHolder(this.certificateHolder.getEncoded()));
        CMSTypedData content = new CMSProcessableByteArray(certencoded);
        CMSSignedData signeddata = generator.generate(content, true);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write("-----BEGIN PKCS #7 SIGNED DATA-----\n".getBytes("ISO-8859-1"));
        byte[] cert_encoded = Base64.encode(signeddata.getEncoded());
        for (int i = 0; i < cert_encoded.length; i++) {
            if (i > 0 && i % 63 == 0) {
                out.write('\n');
            }
            out.write(cert_encoded[i]);
        }
        out.write("\n-----END PKCS #7 SIGNED DATA-----\n".getBytes("ISO-8859-1"));
        out.close();
        return new String(out.toByteArray(), "ISO-8859-1");
    }
}

问题根源

当前代码最后将生成的X509证书打包成了PKCS#7签名数据(CMS格式),而OpenSSL的x509 -req命令直接输出的是X509证书的PEM格式,这是两者体积和格式差异的核心原因。

修改后的代码

调整signCRT方法,直接输出X509证书的PEM格式,同时对齐OpenSSL命令的参数(SHA256签名、730天有效期):

public String signCRT(String crs_str) throws IOException, OperatorCreationException {
    // 读取CSR
    PemReader p = new PemReader(new StringReader(crs_str));
    PemObject pemObject = p.readPemObject();
    PKCS10CertificationRequest csr = new PKCS10CertificationRequest(pemObject.getContent());

    // 对齐OpenSSL的签名算法:SHA256withRSA
    AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");
    AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
    
    X500Name issuer = certificateHolder.getIssuer();
    BigInteger serial = new BigInteger(32, new SecureRandom());
    Date from = new Date();
    // 对齐OpenSSL的有效期:730天
    Date to = new Date(System.currentTimeMillis() + (730 * 86400000L));

    // 构建证书
    X509v3CertificateBuilder certgen = new X509v3CertificateBuilder(
            issuer, 
            serial, 
            from, 
            to, 
            csr.getSubject(), 
            csr.getSubjectPublicKeyInfo()
    );
    // 添加基础约束(非CA)
    certgen.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
    // 添加主题密钥标识
    certgen.addExtension(X509Extension.subjectKeyIdentifier, false, new SubjectKeyIdentifier(csr.getSubjectPublicKeyInfo().getEncoded()));
    // 添加颁发者密钥标识
    certgen.addExtension(X509Extension.authorityKeyIdentifier, false, 
            new AuthorityKeyIdentifier(
                    new GeneralNames(new GeneralName(certificateHolder.getSubject())), 
                    certificateHolder.getSerialNumber()
            )
    );

    // 签名证书
    ContentSigner signer = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
            .build(PrivateKeyFactory.createKey(privateKey.getEncoded()));
    X509CertificateHolder holder = certgen.build(signer);

    // 将证书转为PEM格式输出(和OpenSSL一致)
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    PEMWriter pemWriter = new PEMWriter(new OutputStreamWriter(out, "ISO-8859-1"));
    pemWriter.writeObject(holder);
    pemWriter.close();
    
    return out.toString("ISO-8859-1");
}

关键修改点

  1. 移除PKCS#7相关代码:直接输出X509证书的PEM格式,不再打包成CMS签名数据
  2. 签名算法改为SHA256withRSA:和OpenSSL命令的-sha256参数一致
  3. 有效期改为730天:对齐OpenSSL命令的-days 730参数
  4. 使用PEMWriter简化PEM格式生成:避免手动拼接Base64和换行,更可靠

验证方法

将生成的字符串保存为.crt文件,用以下OpenSSL命令验证:

openssl x509 -in generated.crt -text -noout

输出的证书信息应与OpenSSL命令生成的openssl-signed.crt格式一致,且能正常通过CA验证:

openssl verify -CAfile $CANAME.crt generated.crt

内容的提问来源于stack exchange,提问作者Ace of Spade

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 09:06:37