当前位置: 首页 > 知识库问答 >
问题:

使用iText进行外部签名PDF

隗驰
2023-03-14

首先,虽然我关注StackOverflow已经有相当一段时间了,但这是我第一次发布一些东西,所以如果我做错了或者不按规则做的话,请随时为我指出正确的方向。

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

如iText文档中所述,在第一阶段,我准备了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, '\0', 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;
            }
        }
    }

在这一步之后,我将哈希发送给外部服务,外部服务返回一个签名哈希。

检查我发送给服务的散列,它似乎是正确的,因为它覆盖了除新签名内容之外的所有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[] { };
        }
    }

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未被修改)。

就像StackOverlow中关于同一问题的另一篇文章中所说的那样(我无法找到链接它的文章),一个潜在的问题将是为临时文件使用不同的流。我已经尝试过使用同样的流,但也没有运气。

PDF示例:

原始文件

发送到服务得Base64哈希:

<代码>XYFAS/SISA/TK5HCL035RPBJOCZRH9E5RGIAMPQGKJI=

已发送Base64签名哈希响应:

MKL回答后更新:

根据答案,我只在一个阶段就更改了文档的代码签名,最后使用了以下方法:

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, '\0', 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);
    }
}

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

一个问题:知道在一个两阶段的签名中,我能够用这个证书签署PDF(除了签名错误后被修改或损坏的文档),这个方法不应该也适用于同一个证书吗?

目前正在发生的情况是:

检查签名:

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

结果PDF

在将这个SHA256前缀添加到消息摘要之后,得到的PDF现在已经正确签名了。

Adobe Reader是否接受固定签名?

我怀疑.签名者证书的密钥用法仅包含用于对其他证书进行签名的值。

问:在我最初的代码中,我有一个两阶段的签名。在单符号方法中应用的相同原则是否仍然有效,即应用SHA256前缀执行预签名字节,并在使用结果的有符号字节设置摘要之后?

共有1个答案

乐正烨熠
2023-03-14

您的代码中有许多问题。

首先,您的代码混合了不同代的iText签名API。有较老的API一代,它要求您非常接近PDF内部,有较新的API(自5.3.x版本以来),它作为较老的API的一个层实现,并且不要求您了解这些内部。

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

(在某些情况下,可以混合使用这些API,但您应该真正知道自己在做什么,但仍然可能会出错……)

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

Makesignature.Sign*方法隐式关闭基础的PDFStamperSignatureOutcome对象,因此不应假定此后使用这些对象会产生合理的信息。

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

因此,sap.getRangeStream()可能返回错误的东西。(它可能仍然返回正确的数据,但您不应该指望这一点。)

GetBytestosign返回签名的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;

但是,稍后,您的代码接受该返回值,对其签名,并尝试将返回的签名字节嵌入到pdfpkcs7签名容器中。这是错误的,必须为签名容器的签名者信息的身份验证属性创建签名字节,而不是为文档哈希创建签名字节。

(顺便说一下,在这里,您使用了旧的签名API,但没有理解它,因此,使用它是错误的。)

MyExternalSignatureContainer中,您在两次调用中使用签名字节:

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

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

(在这里,您又一次使用了旧的签名API,但没有理解它,又一次错误地使用了它。)

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

    null

首先,如果我正确理解您的意思,您从其他服务器请求签名,而其他服务器的反应很快,因此在等待签名时不需要释放所有资源。在这种情况下,不需要两个阶段的签名过程,您应该一步到位。您所需要的只是一个自定义的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);

在更新您的问题,您添加了一个PDF签名与以上应用的更改。通过分析签名容器中的签名字节,我们可以清楚地看到,您的签名服务被设计成非常哑的,它应用PKCS1 v1.5填充和RSA加密,但它假定其输入已经打包到digestinfo结构中。根据我的经验,这是一个不常见的假设,您应该告诉您的签名提供者正确地记录这一点。

对于您的代码,这意味着您必须将散列打包到digestinfo结构中,然后再将其发送到服务。

RFC 8017第9.2节注释1解释了一种简单的方法:

对于附录B.1中提到的九个哈希函数,digestinfo值的DER编码T等于:

    ...
    SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.
    ...
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数字签名指南,您将看到有效的密钥用法扩展包括

  • 不存在,即根本没有密钥用法扩展,或
  • 具有以下一个或多个值:
    • 不可否认
    • SignTransaction(仅11.1.09)
    • 数字签名(11.0.10及更高版本)

 类似资料:
  • 第一个选项是不可行的,因为PDF的签名没有时间戳(这是一个非常重要的必要条件),所以选择了第二个选项。 这是我们的代码,首先,我们得到签名外观,并计算散列: 在这一点上,我们有了文档的哈希代码。然后我们将散列发送到webservice,我们得到签名的散列代码。 按照mkl的建议,我遵循了这本书的4.3.3部分PDF文档的数字签名,现在我的代码如下: 第一部分,当我们计算散列时: 在第二部分,我们得

  • 我已经玩弄了一段时间的iTextSharp 5.5.7,找不到正确的方法从智能卡为PDF制作一个有效的数字签名-Adobe Reader总是说它的签名人和未知,并且不能解码签名的DER数据。 我查看了Makesignature.cs代码以供参考,以及is的功能: 然后,根据iExternalSignature.cs中的“Sign”方法 “@param message要进行散列和签名的邮件” 所以我

  • 我有一个pdf文件,需要数字签名,签名是由外部服务提供。在启动签名之前,我没有证书链。我尝试了下面的代码,但是得到了Sigdict/Contents非法的数据消息。 创建文档哈希并将其发布到外部服务以对其进行签名的源 方法getPKCS7DataFromDigitalSignatureResponse(responseXML)用于解析外部服务响应,它将PKCS7作为字符串返回。我试图找出问题的根源

  • 我正在使用iText 5 Java进行外部签名。首先,我创建签名外观,计算签名属性的哈希值,并为签名保留空白位置。稍后,当我从客户端获得签名的散列时,我通过。 这是用于签名的代码。我已经删除了很多功能代码,以保持代码的基本功能。 这是原始PDF。 这是空签名的PDF。 这是带有最终签名的PDF。 计算PDF的哈希值。 这是我从客户端收到的签名哈希签名。 我在stackoverflow上做了很多尝试

  • 问题内容: 如何使用iText签名pdf?我正在通过此LINK进行操作, 但不了解my_private_key.pfx。我真的需要数字签名证书吗?请澄清一下。提前致谢。 问题答案: 您在问题中提到的文档很好。您必须创建数字签名文件。 该链接具有使用PKCS文件和签署PDF文档的工具。它声称使用iText,因此您应该能够理解这些步骤。源代码在这里

  • 我正在尝试使用iText(sharp,5.5.13版)创建自定义数字签名,用户可以从四个位置(顶部、底部、左侧和右侧)设置图像位置,如下所示: 刚度: 左: 顶部: 底部: 到目前为止,我试着处理签名的第0层,但我认为我做得不对,因为签名细节是在第2层设置的。 然而,这只是设置图像位置的初始草图。在下面的代码中,我加载图像并将其放入一个块中(想法取自此示例) 结果或多或少是预期的,但有两个问题:签