Pades LTV verification in iTextSharp throws Public

2019-06-26 20:04发布

问题:

I'm getting an Org.BouncyCastle.Security.InvalidKeyException with error message Public key presented not for certificate signature when validating a pdf with LtvVerifier.

This problem has arisen after circumventing an issue with CRL LDAP URIs. The code used to perform the verification is the same as the previous post:

   public static bool Validate(byte[] pdfIn, X509Certificate2 cert)
    {
        using (var reader = new PdfReader(pdfIn))
        {
            var fields = reader.AcroFields;
            var signames = fields.GetSignatureNames();

            if (!signames.Any(n => fields.SignatureCoversWholeDocument(n)))
                throw new Exception("None signature covers all document");

            var verifications = signames.Select(n => fields.VerifySignature(n));

            var invalidSignature = verifications.Where(v => !v.Verify());
            var invalidTimeStamp = verifications.Where(v => !v.VerifyTimestampImprint());

            if (invalidSignature.Any())
                throw new Exception("Invalid signature found");
        }

        using (var reader = new PdfReader(pdfIn))
        {
            var ltvVerifier = new LtvVerifier(reader)
            {
                OnlineCheckingAllowed = false,
                CertificateOption = LtvVerification.CertificateOption.WHOLE_CHAIN,
                Certificates = GetChain(cert).ToList(),
                VerifyRootCertificate = false,
                Verifier = new MyVerifier(null)
            };

            var ltvResult = new List<VerificationOK> { };
            ltvVerifier.Verify(ltvResult);

            if (!ltvResult.Any())
                throw new Exception("Ltv verification failed");
        }
        return true;
   }

Auxiliary function that builds a List of X509Certificates from the certificate chain:

    private static X509.X509Certificate[] GetChain(X509Certificate2 myCert)
    {
        var x509Chain = new X509Chain();
        x509Chain.Build(myCert);

        var chain = new List<X509.X509Certificate>();
        foreach(var cert in x509Chain.ChainElements)
        {
            chain.Add(
                DotNetUtilities.FromX509Certificate(cert.Certificate)
                );
        }

        return chain.ToArray();
    }

A custom verifier, just copied from sample:

 class MyVerifier : CertificateVerifier
{
    public MyVerifier(CertificateVerifier verifier) : base(verifier) { }

    override public List<VerificationOK> Verify(
        X509.X509Certificate signCert, X509.X509Certificate issuerCert, DateTime signDate)
    {
        Console.WriteLine(signCert.SubjectDN + ": ALL VERIFICATIONS DONE");
        return new List<VerificationOK>();
    }
}

I have re-implemented LtvVerifier and CrlVerifier as explained in the previous question. The CRL validation is done ok.

The certificate chain includes the certificate that was used to sign the PDF and the CA root certificate. The function CrlVerifier.Verify is throwing the mentioned exception when calling the next line:

 if (verifier != null)
                result.AddRange(verifier.Verify(signCert, issuerCert, signDate));
            // verify using the previous verifier in the chain (if any)
            return result;

And this is the relevant stack trace of Org.BouncyCastle.Security.InvalidKeyException:

   in Org.BouncyCastle.X509.X509Certificate.CheckSignature(AsymmetricKeyParameter publicKey, ISigner signature)
   in Org.BouncyCastle.X509.X509Certificate.Verify(AsymmetricKeyParameter key)
   in iTextSharp.text.pdf.security.CertificateVerifier.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate)
   in iTextSharp.text.pdf.security.RootStoreVerifier.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate)
   in PdfCommon.CrlVerifierSkippingLdap.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate) in c:\Projects\digit\Fuentes\PdfCommon\CrlVerifierSkippingLdap.cs:line 76
   in iTextSharp.text.pdf.security.OcspVerifier.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate)
   in PdfCommon.LtvVerifierSkippingLdap.Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime sigDate) in c:\Projects\digit\Fuentes\PdfCommon\LtvVerifierSkippingLdap.cs:line 221
   in PdfCommon.LtvVerifierSkippingLdap.VerifySignature() in c:\Projects\digit\Fuentes\PdfCommon\LtvVerifierSkippingLdap.cs:line 148
   in PdfCommon.LtvVerifierSkippingLdap.Verify(List`1 result) in c:\Projects\digit\Fuentes\PdfCommon\LtvVerifierSkippingLdap.cs:line 112

And a link to the pdf that I try to validate

回答1:

After some debugging it turns out that

iText(Sharp) 5.5.10 LtvVerifier fails in the observed manner when verifying certificates with certificate chains not ending in a self-signed certificate.

The cause

The reason is pretty simple: LtvVerifier establishes a sequence of Verifier instances (OcspVerifier, CrlVerifier, RootStoreVerifier, CertificateVerifier; the last one chained via base class calls). Then it requests the certificate chain of the signing certificate of the signature in question and for each certificate of the chain calls the Verifier sequence for the certificate couple consisting of this certificate and its issuer; in case of the final certificate in the chain, null is forwarded as issuer certificate.

Unfortunately the final Verifier, the CertificateVerifier, assumes in case of a null issuer certificate that the certificate to verify is self-signed:

// Check if the signature is valid
if (issuerCert != null) {
    signCert.Verify(issuerCert.GetPublicKey());
}
// Also in case, the certificate is self-signed
else {
    signCert.Verify(signCert.GetPublicKey());
} 

(from CertificateVerifier method Verify)

If the certificate chain the LtvVerifier initially requested does not end in a self-signed certificate, that final test correctly results in the observed

Org.BouncyCastle.Security.InvalidKeyException with error message Public key presented not for certificate signature

The OP's example

In the case at hand we have a document timestamp issued by

cn=AUTORIDAD DE SELLADO DE TIEMPO FNMT-RCM, ou=CERES, o=FNMT-RCM, c=ES

issued by

cn=AC Administración Pública, serialNumber=Q2826004J, ou=CERES, o=FNMT-RCM, c=ES

issued by

ou=AC RAIZ FNMT-RCM, o=FNMT-RCM, c=ES

which is self-signed.

In this case already the intermediary certificate, AC Administración Pública, is on the European trusted list (cf. the TL manager for Spain, "Trust Service Provider", "Fábrica Nacional de Moneda y Timbre - Real Casa de la Moneda (FNMT-RCM)", "Trust Service", "Certificados reconocidos para su uso en el ámbito de... ", "Digital Identity").

Thus, one does not need more than the first two certificates to establish trust, the self signed root certificate is not needed. As a consequence only these first two certificates are embedded in the time stamp and returned as certificate chain to the LtvVerifier, not the self signed root.

The result is the observed error in the LtvVerifier.

What to do?

Well, as we already started creating our own copies of the involved classes in the previous question, changing them a bit more should be an option.

In this case one should additionally change the RootStoreVerifier. Its Verify method looks like this:

override public List<VerificationOK> Verify(X509Certificate signCert, X509Certificate issuerCert, DateTime signDate) {
    LOGGER.Info("Root store verification: " + signCert.SubjectDN);
    // verify using the CertificateVerifier if root store is missing
    if (certificates == null)
        return base.Verify(signCert, issuerCert, signDate);
    try {
        List<VerificationOK> result = new List<VerificationOK>();
        // loop over the trusted anchors in the root store
        foreach (X509Certificate anchor in certificates) {
            try {
                signCert.Verify(anchor.GetPublicKey());
                LOGGER.Info("Certificate verified against root store");
                result.Add(new VerificationOK(signCert, this, "Certificate verified against root store."));
                result.AddRange(base.Verify(signCert, issuerCert, signDate));
                return result;
            } catch (GeneralSecurityException) {}
        }
        result.AddRange(base.Verify(signCert, issuerCert, signDate));
        return result;
    } catch (GeneralSecurityException) {
        return base.Verify(signCert, issuerCert, signDate);
    }
}

We merely have to remove the marked line

                signCert.Verify(anchor.GetPublicKey());
                LOGGER.Info("Certificate verified against root store");
                result.Add(new VerificationOK(signCert, this, "Certificate verified against root store."));
                // vvv remove
                result.AddRange(base.Verify(signCert, issuerCert, signDate));
                // ^^^ remove
                return result;

in the inner try block. As we here have just established that the certificate signCert is signed by a trust anchor, there is no need for the base.Verify anyways.