简体   繁体   中英

Bouncy castle sign a csr from openssl in java

I generated a ca-certificate and a key with openssl.

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'

Now I want to sign CSRs with those.
I found this question , but I can't use the accepted answer, because the class X509CertificateHolder seams not to exist anymore.

Based on the seconds answer I created this service.

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");
    }
}

The method signCrt does not throw any exceptions and returns a string (I am not sure what).

I can't verify it or even show any information about the certificate.

This openssl command works with the same csr file, I want to do the same thing from java.

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

The certificate created with openssl is much smaller the of my java method (Don't know of this is a useful information).

I don't know what makes you think X509CertificateHolder 'seems not to exist' (BTW not 'seams'). It does exist, and in fact the code you posted USES it -- if it did not exist your code couldn't run at all much less produce 'larger' output.

The answer you linked tells you what the code returns: "a valid PEM-encoded signedData object containing a signed Certificate chain (of the type that can be imported by keytool)". Actually it's not quite valid: (1) it doesn't put any linebreaks in the base64 as PEM officially requires -- which you fixed, although you did it at 63 not 64 as standard; (2) it uses BEGIN/END PKCS #7 SIGNED DATA whereas the standard is BEGIN/END PKCS7 -- or in most cases BEGIN/END CMS because CMS is basically a superset of PKCS7. keytool ignores both of these flaws when reading a 'CA reply' (ie -importcert to an existing privateKey entry), but other software doesn't -- like BouncyCastle and OpenSSL. You already added line breaks; if you change the BEGIN/END labels to PKCS7 and put the result in a file you can read it with among other things

 openssl pkcs7 -in $file -print_certs # add -text for expanded details

However, calling this 'the type that can be imported by keytool' implies it is the only type which is false. keytool can read PKCS7 containing certs -- either PEM or DER -- but it can also read 'bare' X.509 certificates in either PEM or DER, and that is usually more convenient. To do that, replace everything from CMSSignedDataGenerator onward with something like for DER:

Files.write(Paths.get("outputfile"), certencoded)

or for PEM since you have Bouncy:

try( PemWriter w = new PemWriter(new FileWriter("outputfile")) ){
    w.writeObject(new PemObject("CERTIFICATE",certencoded));
}

or if you prefer to do it yourself:

try( Writer w = new FileWriter("outputfile") ){
    w.write("-----BEGIN CERTIFICATE-----\r\n" 
        + Base64.getMimeEncoder().encodeToString(certencoded) 
            // MimeEncoder does linebreaks at 76 by default which 
            // is close enough and less work than doing it by hand
        + "\r\n-----END CERTIFICATE-----\r\n");
}

Note: you may not need the \r -- PEM doesn't actually specify CRLF vs LF linebreaks -- but MimeEncoder uses them on the interior breaks and I like to be consistent.

This -- just the certificate -- is what your openssl x509 -req -CA* alternative produces, and that's why it's much smaller -- one cert is smaller than three certs plus some overhead.

Several more points:

  • PEMParser can handle CSR just fine; you don't need the PemReader+PemObject detour

  • using the entire SubjectPublicKeyInfo for the SubjectKeyIdentifier extension is wrong. OTOH nothing uses the SKI extension in a leaf cert anyway, so simplest to just delete this rather than fix it.

  • in certgen.build() if you use JcaContentSignerBuilder instead of the BcRSA variant, you can pass cakey directly without going through PrivateKeyFactory and the signature scheme directly without going through two AlgorithmIdentifierFinder s -- as the /*cms-sd*/generator.addSignerInfoGenerator later in the code already does.

  • speaking of the signature scheme, don't use SHA1 for cert signatures (or most others). It was already deprecated in 2013 when that answer was written, and in 2017 was publicly broken for collision (see https://shattered.io ) after which many systems either reject it entirely or nag mercilessly for such use, and some related ones like git. In fact many authorities and checklisters now prohibit it for any use, even though some (like HMAC and PBKDF) aren't actually broken. Your openssl x509 -req alternative used SHA256 which is fine*.

  • and lastly, issuing a cert is NOT 'sign[ing] {the|a} CSR'. You can just look at the cert contents and see it's not at all the same as the CSR or even the CSR body. You create (or generate as used in the Bouncy names) a cert and sign the cert -- in response to a CSR.

* at least for now; if and when quantum cryptanalysis works a lot of currently used schemes, including probably SHA256-RSA signature, will be in trouble and will have to be replaced by 'post-quantum' schemes which people are now working to develop. But if this happens, you'll see it on every news channel and site in existence.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM