使用 iText 外部签名 PDF

External signing PDF with iText

首先,虽然我关注Whosebug已经有一段时间了,但是我还是第一次post编辑东西,所以如果我做错了或者不符合规则,请大家感受免费为我指明正确的方向。

我正在开发一个 PDF 数字签名应用程序,使用 iText5,它依赖于外部服务在我准备 PDF 签名后提供签名哈希。

iText documentation所述,在第一阶段我准备了PDF(在最终实现中,所有PDF都可能是多重签名的,所以我使用追加模式),就像这样:

public static byte[] GetBytesToSign(string unsignedPdf, string tempPdf, string signatureFieldName, List<Org.BouncyCastle.X509.X509Certificate> certificateChain) {
        // we create a reader and a stamper
        using (PdfReader reader = new PdfReader(unsignedPdf)) {
            using (FileStream baos = File.OpenWrite(tempPdf)) {

                List<Org.BouncyCastle.X509.X509Certificate> chain = certificateChain;
                PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '[=10=]', null, true);
                sap                   = pdfStamper.SignatureAppearance;
                sap.Certificate       = certificateChain[0];
                sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
                //sap.SetVisibleSignature(signatureFieldName);
                sap.SignDate          = DateTime.Now;
                PdfSignature dic      = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);  
                dic.Date              = new PdfDate(sap.SignDate);
                dic.Name              = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
                sap.CryptoDictionary  = dic;
                sap.Certificate       = certificateChain[0];
                sap.Acro6Layers       = true;
                sap.Reason            = "test";
                sap.Location          = "test";

                IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                MakeSignature.SignExternalContainer(sap, external, 8192);
                signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
                byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
                //byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

                return hash;
            }
        }
    }

在这一步之后,我将散列发送到外部服务,returns 一个签名的散列。

检查我发送给服务的散列,它似乎是正确的,因为它涵盖了除新签名内容之外的所有 PDF。

然后我使用以下方法结束签名过程:

private byte[] Sign(PdfPKCS7 signatureContainer, List<X509Certificate2> chain2, List<Org.BouncyCastle.X509.X509Certificate> chain, byte[] hash, byte[] signedBytes, string tmpPdf, string signedPdf, string signatureFieldName) {
        System.Security.Cryptography.RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = chain2[0].PublicKey.Key as System.Security.Cryptography.RSACryptoServiceProvider;
        bool verify = publicCertifiedRSACryptoServiceProvider.VerifyHash(hash, "SHA256", signedBytes); //verify if the computed hash is same as signed hash using the cert public key
        Console.WriteLine("PKey signed computed hash is equal to signed hash: " + verify);

        AsnEncodedData asnEncodedData = new AsnEncodedData(signedBytes);
        Console.WriteLine(asnEncodedData.Format(true));
        
        //ITEXT5
        try {
            //Console.WriteLine("Signed bytes: " + Encoding.UTF8.GetString(signedBytes));

            using (PdfReader reader = new PdfReader(tmpPdf)) {
                using (FileStream outputStream = File.OpenWrite(signedPdf)) {
                IExternalSignatureContainer external = new Objects.MyExternalSignatureContainer(signedBytes, chain, signatureContainer);
                MakeSignature.SignDeferred(reader, signatureFieldName, outputStream, external);
                }
            }
            return new byte[] { };
        }
        catch(Exception ex) {
            File.Delete(tmpPdf);
            Console.WriteLine("Error signing file: " + ex.Message);
            return new byte[] { };
        }
    }

在 Sign 方法的开头,我验证了发送到外部服务的哈希值是否与外部服务响应相同,是真实的。

MyExternalSignatureContainer 代码:

public class MyExternalSignatureContainer : IExternalSignatureContainer {
        private readonly byte[] signedBytes;
        public List<Org.BouncyCastle.X509.X509Certificate> Chain;
        private PdfPKCS7 sigField;

        public MyExternalSignatureContainer(byte[] signedBytes) {
            this.signedBytes = signedBytes;
        }

        public MyExternalSignatureContainer(byte[] signedBytes, List<Org.BouncyCastle.X509.X509Certificate> chain, PdfPKCS7 pdfPKCS7) {
            this.signedBytes = signedBytes;
            this.Chain = chain;
            this.sigField = pdfPKCS7;
        }

        public byte[] Sign(Stream data) {
            try {
                sigField.SetExternalDigest(signedBytes, null, "RSA");
                return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
            }
            catch (IOException ioe) {
                throw ioe;
            }
        }

        public void ModifySigningDictionary(PdfDictionary signDic) {
        }
    }

问题是当我在 Acrobat 中打开 PDF 时,它指出文档在应用签名后已被修改或损坏。

(如果我在 PDF-XChange 中打开相同的 PDF,它说 PDF 没有被修改)。

到目前为止我已经尝试过但没有成功的方法:

不确定外部服务是否使用 SHA256,我已经尝试将摘要更改为预签名的 SHA1,导致 Acrobat 中出现“格式错误”Reader。

就像 StackOverlow 中关于同一问题的另一个 post 中所述(我无法找到 post 到 link 它),一个潜在的问题是使用不同的流对于临时文件。我已经尝试过使用相同的流,但运气不佳。

PDF 样本:

Original file

Temp File

Signed File

发送到服务的 Base64 哈希:

XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI=

响应中发送的 Base64 签名哈希:

CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==


后更新:

根据回答,我只修改了一个阶段的文档签名代码,最后得到了以下方法:

using (PdfReader reader = new PdfReader(fileLocation)) {
    using (FileStream baos = File.OpenWrite(tmpFile)) {

        List<Org.BouncyCastle.X509.X509Certificate> chain = Chain;
        PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '[=13=]', null, true);
        PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
        sap.Certificate = Chain[0];
        sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
        //sap.SetVisibleSignature(signatureFieldName);
        sap.SignDate = DateTime.Now;
        PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        dic.Date = new PdfDate(sap.SignDate);
        dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
        sap.CryptoDictionary = dic;
        sap.Certificate = Chain[0];
        sap.Acro6Layers = true;
        //sap.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS;
        sap.Reason = "test";
        sap.Location = "test";

        IExternalSignature signature = new Objects.RemoteSignature(client, signatureRequest);
        MakeSignature.SignDetached(sap, signature, Chain, null, null, null, 8192, CryptoStandard.CMS);

    }
}

和 IExternalSignature 实现:

public virtual byte[] Sign(byte[] message) {
    IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
    byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
    //
    // Request signature for hash value messageHash
    // and return signature bytes
    //
    signatureRequest.Hash = messageHash;
    SignatureService.SignatureResponse signatureResponse = client.Signature(signatureRequest);

    if (signatureResponse.Status.Code == "00") {
         return signatureResponse.DocumentSignature;
    }
    else {
        throw new Exception("Error signing file: " + signatureResponse.Status.Message);
    }
}

signatureResponse.DocumentSignature表示服务返回的带符号字节。

在结果 PDF 中,现在出现 BER 解码错误。

Analyzing your example PDF you appear to declare the wrong certificate as signer certificate

虽然我知道当前证书无效,但它是由服务提供的,在之前的服务实现中,我将发送整个 PDF 进行签名,已签名的 PDF 也使用此证书进行了签名.

一个问题: 知道在两阶段签名中我能够使用此证书对 PDF 进行签名(签名错误后更改或损坏的文档除外),应该'此方法也适用于相同的证书?

目前,发生的事情是这样的:

检查签名:

同样,如果我在 PDF-XChange 中打开相同的 PDF,则签名有效且文档未被修改。要求是 PDF 在 Acrobat 中有效,但我对读者之间的这种差异感到困惑。

Result PDF


更新 2

I.e. you only have to prefix your hash with the byte sequence 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

将此 SHA256 前缀添加到消息摘要后,生成的 PDF 现在已正确签名。

Will Adobe Reader accept the fixed signature?

I doubt it. The key usage of the signer certificate only contains the value for signing other certificates.

当前证书仅用于测试。在生产环境我相信外部服务提供的证书是有效的。

关于这个问题我还有两个问题:

For your code this means that you have to pack the hash into a DigestInfo structure before sending it to the service.

问:您是如何检查签名容器并得出不正确的结论的?

问:在我的初始代码中,我进行了两阶段签名。在单一签名方法中应用的相同主体是否仍然有效,即应用 SHA256 前缀执行预签名字节并在使用生成的签名字节设置摘要后?

您的代码中存在许多问题。

首先,您的代码混合了不同的 iText 签名 API 代。较早的 API 一代需要您非常接近 PDF 内部结构,而较新的 API 版本(从版本 5.3.x 开始)API 实现为一层较旧的 API 并且不需要您了解这些内部结构。

“PDF 文档的数字签名”白皮书侧重于展示较新的 API,只有第 4.3.3 节“使用客户端创建的签名在服务器上签署文档”使用旧的API 因为用例不允许使用较新的 API.

不过,您的用例确实允许使用较新的 API,因此您应该尝试只使用它。

(在某些情况下,可以混合使用 API,但你应该真正知道自己在做什么,并且仍然可能会犯错...)

但现在有一些更具体的问题:

正在处理封闭的对象

MakeSignature.Sign* 方法隐式关闭了底层 PdfStamperSignatureAppearance 对象,因此不应假定此后使用这些对象会产生合理的信息。

但是在GetBytesToSign你会

MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");

因此,sap.GetRangeStream() 可能 return 有问题。 (可能它仍然 return 是正确的数据,但你不应该指望它。)

签署错误的字节

GetBytesToSign returns 已签名 PDF 文档范围的哈希值:

signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

return hash;

不过,稍后,您的代码会采用该 return 值,对其进行签名,并尝试将 returned 签名字节嵌入到 PdfPKCS7 签名容器中。这是错误的,必须为签名容器的签名者信息的经过身份验证的属性创建签名字节,而不是文档哈希。

(顺便说一下,这里你使用了旧的签名 API 而不理解它,因此使用不正确。)

将带符号的字节放在错误的位置

MyExternalSignatureContainer 中,您在两个调用中使用了带符号的字节:

sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);

第一个电话是正确的,他们属于这里。不过,在第二次调用中,应该使用签名文档范围的原始哈希值。

(你又在不理解的情况下使用了旧的签名API,又一次错误地使用了它。)

###提供了错误的证书

分析您的示例 PDF,您似乎将错误的证书声明为签名者证书。我认为是因为

  • 其public密钥无法正确解密签名字节和
  • 该证书是 CA 证书,而不是最终实体证书,使用了不适当的密钥来签署 PDF 文档。

如何改进代码

首先,如果我没理解错的话,你是在向其他服务器请求签名,而其他服务器反应很快,所以在等待签名时不需要释放所有资源。在这种情况下,不需要两阶段签名过程,您应该一步完成。您只需要一个自定义 IExternalSignature 实现,例如

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        //
        // Request signature for hash value messageHash
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(messageHash);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

并像这样使用它来签名:

PdfReader reader = new PdfReader(...);
PdfStamper pdfStamper = PdfStamper.CreateSignature(...);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
// set sap properties for signing
IExternalSignature signature = new RemoteSignature();
MakeSignature.SignDetached(sap, signature, chain, null, null, null, 0, CryptoStandard.CMS);

IExternalSignature 实施更新

在您的问题更新中,您添加了应用上述更改签名的 PDF。分析签名容器中的签名字节很明显,您的签名服务设计得非常笨,它应用 PKCS1 v1.5 填充和 RSA 加密,但它假定其输入已经打包到 DigestInfo 结构中。根据我的经验,这是一个不常见的假设,您应该告诉您的签名提供者正确记录这一点。

对于您的代码,这意味着 必须在将哈希发送到服务之前将其打包到 DigestInfo 结构中。

RFC 8017 section 9.2 note 1:

中解释了一个简单的方法

For the nine hash functions mentioned in Appendix B.1, the DER encoding T of the DigestInfo value is equal to the following:

    ...
    SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.
    ...

即您只需要在哈希前面加上字节序列 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

因此,对于需要调用者将摘要打包到 DigestInfo 结构中的服务,RemoteSignature class 的变体可能如下所示:

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        byte[] sha256Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
        byte[] digestInfo = new byte[sha256Prefix.Length + messageHash.Length];
        sha256Prefix.CopyTo(digestInfo, 0);
        messageHash.CopyTo(digestInfo, sha256Prefix.Length);
        //
        // Request signature for DigestInfo value digestInfo
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_DIGEST_INFO(digestInfo);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

Adobe Reader 会接受固定签名吗?

我怀疑。签署者证书的密钥用法仅包含签署其他证书的值。

如果您查看 Adobe Digital Signatures Guide for IT,您会发现有效的密钥用法扩展是

  • 不存在,即根本没有密钥使用扩展,或者
  • 存在以下一个或多个值:
    • nonRepudiation
    • signTransaction(仅限 11.0.09)
    • digitalSignature(11.0.10 及更高版本)

因此,您证书的 signCertificate 值可能有问题。