The CAdES-BES is an extension to CMS signature standards, mostly used by European institutions. In Java, CMS is supported by the Bouncy Castle library, but there is no direct support for the CAdES-BES required extensions.
In this article we will:
- Generate test certificates.
- Sign a message with a CAdES-BES signature.
- Verify created signature.
Glossary
- CMS - Cryptographic Message Syntax for protected messages.
- CAdES - CMS Advanced Electronic Signatures
- CAdES-BES - CAdES Basic Electronic Signature
- PEM - Privacy-Enhanced Mail file format for storing cryptographic data.
- X.509 - Public key certificate standard
- ASN.1 - Abstract Syntax Notation One
- CA - Certificate Authority
- CSR - Certificate signing request
- CRL - Certificate revocation list
Test certificates
To be able to sing a message and later verify the signature and certificates in it, we will create:
- CA key and certificate into files
ca-key.pem
andca-cert.pem
. - Signer's key and certificate
signer-key.pem
andsigner-cert.pem
. - Bundle of the signer key and certificate together with CA certificate
signer-key-store.p12
.
Following command generates a private key and self-signed CA certificate. Both, the key and the certificate will be later used for signing another certificates.
openssl genrsa 2048 > ca-key.pem
openssl req -new -x509 -nodes \
-days 365000 \
-subj '/CN=Test CA' \
-key ca-key.pem \
-out ca-cert.pem
For the signer a key with CSR are generated first. Then, a certificate is generated based on the CSR and signed by the CA certificate.
openssl req -newkey rsa:2048 -nodes \
-days 365000 \
-subj '/CN=Test Signer' \
-keyout signer-key.pem \
-out signer-req.csr
openssl x509 -req \
-set_serial 01 \
-days 365000 \
-in signer-req.csr \
-out signer-cert.pem \
-CA ca-cert.pem \
-CAkey ca-key.pem
Even though the generated key and certificate can be directly loaded by Java, usually a certificate is provided in the PKCS #12 file format. Together with a key and CA certificates.
The PCKS #12 file can be loaded into Java as a key store. Java can find correct certificate in the store by alias. In this case the alias is signer-cert-alias
.
openssl pkcs12 -export \
-name signer-cert-alias \
-inkey signer-key.pem \
-in signer-cert.pem \
-chain \
-caname ca-cert-alias \
-CAfile ca-cert.pem \
-passout pass:password \
-out signer-key-store.p12
Signing a message
Loading certificate into Java
In the java.security.*
package, the Java runtime library offers set of functions and interfaces for working with certificates. The certificate can be the read directly from a (file) input stream.
InputStream inputStream = Files.newInputStream(Path.of("signer-cert.pem"));
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate x509Certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
Note the certificate factory method does accept additional provider argument. By setting the argument, we can use a different implementation of security tools, while still using standard Java API.
For simple tasks like loading a certificate, there is no need to use a different provider. Later, for creating a signature we will use Bouncy Castle provider.
Here is an example of loading the certificate with Bouncy Castle provider. This implementation uses a statically registered provider identified by the BouncyCastleProvider.PROVIDER_NAME
. Without the registration a missing provider error will occur.
Security.addProvider(new BouncyCastleProvider());
CertificateFactory.getInstance("X.509", BouncyCastleProvider.PROVIDER_NAME);
Loading private key into Java
There is no direct Java support for keys stored in PEM files. So, to load a key we have to strip it off its envelope and decode it from the Base64 format.
String pem = Files.readString(Path.of("signer-key.pem"))
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("[\r\n]", "");
byte[] decoded = Base64.getDecoder().decode(pem);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
Loading key and certificate from a PKCS #12 file
Alternative way is to load both key and certificate into a key store and then retrieve it from there.
InputStream inputStream = Files.newInputStream(Path.of("signer-key-store.p12"));
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(inputStream, "password".toCharArray());
X509Certificate x509Certificate = (X509Certificate) keyStore.getCertificate("signer-cert-alias");
PrivateKey privateKey = (PrivateKey) keyStore.getKey("signer-cert-alias", "password".toCharArray());
Creating a signature
According to the CAdES-BES specification there are three additional mandatory attributes in a signature.
Content-type
- ASN.1 code:1.2.840.113549.1.9.3
)Message-digest
- ASN.1 code:1.2.840.113549.1.9.4
)Signing-Certificate
ORSigning-Certificate-v2
- ASN.1 code:1.2.840.113549.1.9.16.2.47
First two attributes are part of the CMS. Third attribute has to be added. Following implementation uses the Signing-Certificate-v2
attribute with SHA-256 digest of the certificate as a value.
byte[] message = "Hello World!".getBytes()
// Prepare signing certificate
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] certificateDigest = sha256.digest(x509Certificate.getEncoded());
AlgorithmIdentifier algoIdSha256 = new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256);
ESSCertIDv2 essCert1 = new ESSCertIDv2(algoIdSha256, certificateDigest);
SigningCertificateV2 signingCertificate = new SigningCertificateV2(new ESSCertIDv2[]{essCert1});
// Prepare signed message digest provider
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder()
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build();
// Prepare message signer
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(privateKey);
// Prepare signature additional info generator
JcaSignerInfoGeneratorBuilder builder = new JcaSignerInfoGeneratorBuilder(digestCalculatorProvider);
builder.setSignedAttributeGenerator(attributes -> {
CMSAttributeTableGenerator tableGenerator = new DefaultSignedAttributeTableGenerator();
// At this moment Content-Type and Message-Digest attributes are already present. So, to be compliant with CAdES-BES we have to add Signing-Certificate attribute.
return tableGenerator.getAttributes(attributes)
.add(PKCSObjectIdentifiers.id_aa_signingCertificateV2, signingCertificate);
});
SignerInfoGenerator signerInfoGenerator = builder.build(contentSigner, x509Certificate);
// Prepare CMS signed data generator
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
generator.addSignerInfoGenerator(signerInfoGenerator);
generator.addCertificates(new JcaCertStore(singleton(x509Certificate)));
// Sign
CMSTypedData cmsTypedData = new CMSProcessableByteArray(message);
CMSSignedData cmsSignedData = generator.generate(cmsTypedData);
The code creates a detached signature not containing the signed message. To include the message call generator.generate(cmsTypedData, true)
.
The signature can be easily encoded into a byte array by cmsSignedData.getEncoded()
.
Signature verification
To verify a signature we want to:
- Compare message digest with the the signature to ensure the message was actually signed by the signature.
- Verify signature was signed by a bundled certificate.
- Check the certificate is not expired.
- Verify that certificate was issued by known CA.
- Check certificate was not revoked / invalidated.
Message digest check
Note the CMSSignedData
may contains collection of signatures. In the "real world" application, we wan to ensure at least one signature is present and all signatures are verified.
for (SignerInformation signerInfo : cmsSignedData.getSignerInfos()) {
MessageDigest messageDigest = MessageDigest.getInstance(signerInfo.getDigestAlgOID());
byte[] digest = messageDigest.digest(message);
if (!MessageDigest.isEqual(digest, signerInfo.getContentDigest())) {
// Message digest is not equal to the signed digest
throw new IllegalStateException();
}
}
Signature verification
We will use certificates bundled with the signature to verify signature itself. Certificates will be validated by a CA certificate later.
JcaSimpleSignerInfoVerifierBuilder verifierBuilder = new JcaSimpleSignerInfoVerifierBuilder();
for (SignerInformation signerInfo : cmsSignedData.getSignerInfos()) {
Collection<X509CertificateHolder> certificateHolders = cmsSignedData.getCertificates().getMatches(signerInfo.getSID());
for (X509CertificateHolder certificateHolder : certificateHolders) {
SignerInformationVerifier verifier = verifierBuilder.build(certificateHolder);
if (!signerInfo.verify(verifier)) {
// Signature is not valid
throw new IllegalStateException();
}
}
}
Signature expiration check
The CMSSignedData
is Bouncy Castle proprietary API, which returns the X509CertificateHolder
classes. To be able to check it with standard Java security API we have to convert it into the X509Certificate
class.
JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
Collection<X509CertificateHolder> certificateHolders = cmsSignedData.getCertificates().getMatches(null);
for (X509CertificateHolder certificateHolder : certificateHolders) {
X509Certificate certificate = certificateConverter.getCertificate(certificateHolder);
// Throws an exception if certificate has expired
certificate.checkValidity();
}
Certificate verification by the CA certificate
We can load the CA certificate from ca-cert.pem
file the same way we loaded the signing certificate.
Certificate caCert = // load from the ca-cert.pem file
JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
Collection<X509CertificateHolder> certificateHolders = cmsSignedData.getCertificates().getMatches(null);
for (X509CertificateHolder certificateHolder : certificateHolders) {
X509Certificate certificate = certificateConverter.getCertificate(certificateHolder);
// Throws an exception if certificate was not signed by CA public key
certificate.verify(caCert.getPublicKey());
}
Find certificate in a CRL
First we will generate empty CRL then add certificate serial number, effectively revoking the certificate. This procedure is slightly more complicated, because the OpenSSL CA command requires some configuration and database files, and there is no way of generating CRL without these.
echo "00" > ca-crl-number.dat
echo "00" > ca-ca-serial.dat
touch ca-database.dat
cat << CONFIG > ca.cnf
default_ca = ca
[ ca ]
certificate = ca-cert.pem
private_key = ca-key.pem
crlnumber = ca-crl-number.dat
serial = ca-serial.dat
database = ca-database.dat
default_crl_days = 360000
default_days = 360000
default_md = sha256
new_certs_dir = .
policy = optional
CONFIG
# Revoke Cert
openssl ca -config ca.cnf \
-revoke signer-cert.pem
# Generate CRL
openssl ca -config ca.cnf \
-gencrl \
-out ca-crl.pem
CRL can be loaded into Java.
InputStream inputStream = Files.newInputStream(Path.of("ca-crl.pem"));
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
CRL crl = certificateFactory.generateCRL(inputStream);
Finally, the certificate can be checked against the CRL.
JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
Collection<X509CertificateHolder> certificateHolders = cmsSignedData.getCertificates().getMatches(null);
for (X509CertificateHolder certificateHolder : certificateHolders) {
X509Certificate certificate = certificateConverter.getCertificate(certificateHolder);
// Throw an exception if certificate is revokes
if (crl.isRevoked(certificate)) {
throw new IllegalStateException();
}
}