在 iTextSharp 中 Pades LTV 验证抛出 Public 密钥不用于根 CA 证书的证书签名

Pades LTV verification in iTextSharp throws Public key presented not for certificate signature for root CA certificate

在使用 LtvVerifier 验证 pdf 时,我收到 Org.BouncyCastle.Security.InvalidKeyException 错误消息 Public key presented not for certificate signature

这个问题是在之后出现的。用于执行验证的代码和前面一样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;
   }

从证书链构建 X509 证书列表的辅助函数:

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

自定义验证器,刚从示例中复制:

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

我已经按照上一个问题中的说明重新实现了 LtvVerifierCrlVerifier。 CRL 验证完成。

证书链包括用于签署 PDF 的证书和 CA 根证书。函数 CrlVerifier.Verify 在调用下一行时抛出上述异常:

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

这是 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

还有一个link to the pdf that I try to validate

调试后发现

iText(Sharp) 5.5.10 LtvVerifier 在验证证书链未以 self-signed 证书结尾的证书时,以观察到的方式失败。

原因

原因很简单:LtvVerifier 建立了 Verifier 个实例的序列(OcspVerifierCrlVerifierRootStoreVerifierCertificateVerifier ; 最后一个通过 base class 调用链接)。然后它请求相关签名的签名证书的证书链,并为链中的每个证书调用由该证书及其颁发者组成的证书对的 Verifier 序列;如果是链中的最终证书,null 将作为颁发者证书转发。

不幸的是,最终 Verifier,即 CertificateVerifier,假设 null 颁发者证书要验证的证书是 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());
} 

(来自CertificateVerifier方法Verify

如果 LtvVerifier 最初请求的证书链没有以 self-signed 证书结尾,则最终测试正确地导致观察到的

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

OP 的例子

在手头的案例中,我们有

发布的文档时间戳

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

发布者

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

发布者

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

即self-signed。

在这种情况下,中间证书 AC Administración Pública 已经在欧洲信任列表中(参见 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").

因此,建立信任不需要超过前两个证书,不需要自签名根证书。因此,只有前两个证书嵌入时间戳中并作为证书链返回给 LtvVerifier,而不是自签名根。

结果是 LtvVerifier 中观察到的错误。

怎么办?

好吧,由于我们已经开始在 中创建我们自己的相关 classes 副本,因此可以选择对它们进行更多更改。

在这种情况下,应该另外更改 RootStoreVerifier。它的 Verify 方法如下所示:

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

我们只需要删除标记的行

                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;

在内部 try 块中。由于我们刚刚确定证书 signCert 是由信任锚签署的,因此无论如何都不需要 base.Verify