iTextSharp 中的 Pade LTV 验证抛出公钥不是用于根 CA 证书的证书签名

jru*_*ren 5 c# pdf itext digital-signature

我得到一个Org.BouncyCastle.Security.InvalidKeyException错误消息公钥呈现不证书签名验证用PDF时LtvVerifier

绕过 CRL LDAP URI 的问题后出现了此问题。用于执行验证的代码与上一篇文章相同:

   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;
   }
Run Code Online (Sandbox Code Playgroud)

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

    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();
    }
Run Code Online (Sandbox Code Playgroud)

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

 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>();
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经重新实施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;
Run Code Online (Sandbox Code Playgroud)

这是相关的堆栈跟踪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
Run Code Online (Sandbox Code Playgroud)

以及指向我尝试验证的 pdf链接

mkl*_*mkl 3

经过一番调试后发现

\n\n

当验证证书链不以自签名证书结尾时, iText(Sharp) 5.5.10LtvVerifier以观察到的方式失败。

\n\n

原因

\n\n

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

\n\n

Verifier不幸的是,最后的CertificateVerifier, 假定在null颁发者证书的情况下要验证的证书是自签名的:

\n\n
// Check if the signature is valid\nif (issuerCert != null) {\n    signCert.Verify(issuerCert.GetPublicKey());\n}\n// Also in case, the certificate is self-signed\nelse {\n    signCert.Verify(signCert.GetPublicKey());\n} \n
Run Code Online (Sandbox Code Playgroud)\n\n

(来自CertificateVerifier方法Verify

\n\n

如果最初请求的证书链LtvVerifier未以自签名证书结尾,则最终测试会正确地得出观察到的结果

\n\n
\n

Org.BouncyCastle.Security.InvalidKeyException带有错误消息提供的公钥不适用于证书签名

\n
\n\n

OP 的例子

\n\n

在当前的情况下,我们有一个由以下人员发布的文档时间戳

\n\n

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

\n\n

由...发出

\n\n

cn=AC Administraci\xc3\xb3n P\xc3\xbablica,序列号=Q2826004J,ou=CERES,o=FNMT-RCM,c=ES

\n\n

由...发出

\n\n

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

\n\n

这是自签名的。

\n\n

在这种情况下,中间证书AC Administraci\xc3\xb3n P\xc3\xbablica已位于欧洲受信任列表中(参见西班牙的 TL 管理器,“信任服务提供商”,“F\xc3\xa1brica Nacional de Moneda”) y 音色 - Real Casa de la Moneda (FNMT-RCM)”、“信任服务”、“Certificados reconocidos para su uso en el \xc3\xa1mbito de...”、“数字身份”)。

\n\n

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

\n\n

结果是观察到的误差LtvVerifier

\n\n

该怎么办?

\n\n

好吧,由于我们已经开始创建上一个问题中涉及的类的自己的副本,因此对它们进行更多更改应该是一种选择。

\n\n

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

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

我们只需要删除标记线

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

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

\n