I've been banging my head against the wall with this problem for about 20 hours now and I am probably missing something easy. However, I've gotten to the point where I think I need help. I have read dozens of explanations for how to do different parts of the problem, but I cannot figure out how to bring them all together.
I have a DER-encoded detached PKCS#7 digital signature. The signature conforms to RFC 3852 (Cryptographic Message Syntax). For my project I need to step through each of the steps needed to verify a signature and be able to tell which step the verification failed on. I am using BouncyCastle in Java.
To my understanding there are six basic steps needed to verify a digital signature
- Verify that the root certificate is a trusted certificate
- Verify the certificate chain from the root certificate to the signing certificate
- Verify that the signer's name is who you would expect it to be
- Verify that the certificates are not expired
- Verify that the certificates do not appear on a CRL (for simplicity assume that the CRL is already downloaded locally)
- Verify the digest in the signature matches the digest of the document
EDIT: Several comments requested adding an OSCP check to the list.
In the BouncyCastle test code, I was able to find the following example. It seems to accomplish 2/6, however, it is not clear that it does any of the of the tasks. If anyone could point me in the right direction for the rest of the tasks it would be greatly appreciated.
CMSSignedData s = ...
byte[] contentDigest = ...
Store certStore = s.getCertificates();
Store crlStore = s.getCRLs();
SignerInformationStore signers = s.getSignerInfos();
Collection c = signers.getSigners();
Iterator it = c.iterator();
while (it.hasNext())
{
SignerInformation signer = (SignerInformation)it.next();
Collection certCollection = certStore.getMatches(signer.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder cert = (X509CertificateHolder)certIt.next();
assertEquals(true, signer.verify(new JcaSimpleSignerInfoVerifierBuilder().setProvider(BC).build(cert)));
if (contentDigest != null)
{
assertTrue(MessageDigest.isEqual(contentDigest, signer.getContentDigest()));
}
}
Collection certColl = certStore.getMatches(null);
Collection crlColl = crlStore.getMatches(null);
assertEquals(certColl.size(), s.getCertificates().getMatches(null).size());
assertEquals(crlColl.size(), s.getCRLs().getMatches(null).size());
The most complex in a CMS signed data validation is the X.509 validation part. For each signer in the signed data The steps are as follows:
1. Find the signer certificate
SignerInformation signerInfo = (SignerInformation)it.next();
Collection certCollection = certStore.getMatches(signerInfo.getSID());
Iterator certIt = certCollection.iterator();
X509CertificateHolder signerCertificateHolder = (X509CertificateHolder)certIt.next();
I assume here there is only one matching certificate.
2. Find a certificate chain from the signer certificate to a trusted root certificate
Here you are to inspect the certificates in the Store
in order to find the certificate chain. Use the subject DN/Issuer DN and Subject Key Identifier/Authority Key Identifier matches to build that chain.
3. Validate the chain
This step a recursive validation of every certificate in the chain. For each certificate, starting from the certificate directly under the root certificate you need to check:
- the certificate signature with the issuer public key
- the validity date of the certificate
- the revocation status of the certificate
Hopefully for steps 2 & 3 you can use the built-in PKIX certificate path builder and validation mechanisms:
KeyStore trustAnchors = getTrustAnchors();
X509CertSelector target = new X509CertSelector();
target.setCertificate(signerCertificate);
PKIXBuilderParameters params = new PKIXBuilderParameters(anchors, target);
CertStoreParameters additionalCerts = new CollectionCertStoreParameters(allOtherCerts)
params.addCertStore(CertStore.getInstance("Collection", additionalCerts));
CertStoreParameters revocationObjects = new CollectionCertStoreParameters(allCRLs);
params.addCertStore(CertStore.getInstance("Collection", revocationObjects));
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
PKIXCertPathBuilderResult r = (PKIXCertPathBuilderResult) builder.build(params);
/* if the build method returns without exception, the certificate chain is valid */
For the sake of readability I omited the convertion of objects between java(x).crypto
an Bouncycastle types.
4. Validate the SignerInfo signature with the public key
JcaSimpleSignerInfoVerifierBuilder builder = new JcaSimpleSignerInfoVerifierBuilder();
SignerInformationVerifier verifier = builder.build(signerCertificateHolder);
assertTrue(signerInfo.verify(verifier));
5. Validate that the document digest matches the signed digest
byte[] contentDigest = computeDigest(originalDoc, signerInfo.getDigestAlgOID());
assertArrayEquals(contentDigest, signer.getContentDigest());