使用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"); }
关键修改点
- 移除PKCS#7相关代码:直接输出X509证书的PEM格式,不再打包成CMS签名数据
- 签名算法改为SHA256withRSA:和OpenSSL命令的
-sha256参数一致 - 有效期改为730天:对齐OpenSSL命令的
-days 730参数 - 使用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
相关产品推荐
相关产品推荐

