itext7:延迟签名未生成有效的签名 pdf

itext7: deferred signing not producing a valid signed pdf

我正在尝试使用 itext7 通过从外部实体获取签名来签署 pdf。我一定是遗漏了什么,因为延迟的 pdf 签名无效。让我们从延迟签名的代码开始:

public byte[] GetDocHashFromPreparedDocToSign(string pathToOriginalPdf, string pathToPreparedToBeSignedPdf, List<X509Certificate> certificates) {
    var pdfSigner = new PdfSigner(new PdfReader(pathToOriginalPdf),
                                  new FileStream(pathToPreparedToBeSignedPdf, FileMode.Create),
                                  new StampingProperties());
    pdfSigner.SetFieldName(_signatureFieldname);


    var appearance = pdfSigner.GetSignatureAppearance();
    appearance.SetPageRect(new Rectangle(144, 144, 200, 100))
              .SetPageNumber(1)
              .SetCertificate(certificates[0]);

    var container = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
    pdfSigner.SignExternalContainer(container, 8192);
    



   byte[] sha256SigPrefix = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09,
                                 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
                                 0x05, 0x00, 0x04, 0x20 };
    // service needs to receive sha256 prepended
    using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
    var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);        
    var totalHash = new byte[sha256SigPrefix.Length + data.Length];
    sha256SigPrefix.CopyTo(totalHash, 0);
    data.CopyTo(totalHash, sha256SigPrefix.Length);
    return totalHash;
}

该方法接收到原始 pdf 的路径、将包含签名占位符的临时 pdf 的路径以及从原始服务检索到的 X509Certificate 列表。在为签名保留 space 后,该方法计算文件的哈希值并在其前面添加 sha256 前缀(将对文档进行签名的服务需要)。

此信息被发送到将 return 签名的服务。当签名被取回时,会调用下面的方法来用真正的签名填充签名占位符:

public void SignPreparedToBeSignedDoc(string pathToPreparedToBeSignedPdf, string pathToSignedFile, byte[] signature) {
    var document = new PdfDocument(new PdfReader(pathToPreparedToBeSignedPdf));
    using var writer = new FileStream(pathToSignedFile, FileMode.Create);

    var container = new ExternalInjectingSignatureContainer(signature);
    PdfSigner.SignDeferred(document, _signatureFieldname, writer, container);
}

编辑:根据@mkl 评论,我修复了签名部分: 这是 ExternalInjectingSignatureContainer:

internal class ExternalInjectingSignatureContainer : IExternalSignatureContainer {
    private readonly byte[] _signature;

    public ExternalInjectingSignatureContainer(byte[] signature) {
        _signature = signature;
    }

    public byte[] Sign(Stream data){
        var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
        sgn.SetExternalDigest(_signature, null, "RSA");
        return sgn.GetEncodedPKCS7();

    }

    public void ModifySigningDictionary(PdfDictionary signDic) {
    }
}

即使代码运行没有错误,在 adobe 中打开 pdf 也会显示以下错误:

编辑:修复签名代码后,现在错误不同了:它会显示签名信息,但会说文件已被更改或损坏。

此时,似乎正确生成了临时 pdf,但我可能遗漏了一些东西...关于如何调试此问题的任何线索?

谢谢

编辑:针对@mkl 提出的解决方案的评论,我尝试更新代码。我今天还有几分钟的时间来玩这个,我试着遵循提出的指导方针,但我显然仍然遗漏了一些东西。

在展示新代码之前,我想指出之前的更新版本(使用 2 个 IExternalSignatureContainer 实例)似乎工作正常。即,在 adobe 上打开签名的 pdf 只会显示黄色警告,指出签名有问题:

由于该文档正在与测试链一起使用,因此签名似乎工作正常(尽管我可能完全错了)。

因此,为了修复容器的错误使用,我重写了 IExternalSignatureContainerSign 方法的代码。这是我为准备要发送到服务器的文档散列而准备的代码:

public override byte[] Sign(Stream data) {
// create PCKS7 for getting attributes
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// get document hash
DocumentDigest = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
var docBytesHash = sgn.GetAuthenticatedAttributeBytes(DocumentDigest, 
                                PdfSigner.CryptoStandard.CMS, 
                                null, 
                                null);
 //prepend sha256 prefix
 var totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
 _sha256SigPrefix.CopyTo(totalHash, 0);
 docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
 DataToSend = totalHash;
 return new byte[0];
}

因为我必须使用传递给 GetAuthenticatedAttributes 的相同参数调用 GetEncodedPKCS7 方法,所以我还保存了通过 Digest 方法获得的文档哈希。 DataToSend 将被发送到服务器,以便它可以 return 该散列的签名。

这是为延迟签名 (PdfSigner.SignDeferred) 调用的另一个 IExternalSignatureContainer 的代码:

public byte[] Sign(Stream data) {
    // create CMS      
    var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
    // set the signature bytes
    sgn.SetExternalDigest(_signature, null, "RSA");
    // call GetEncoded with the same parameters as the original GetAuthenticatedAtt...
    //_documentHash == DocumentDigest previous sample
    var encodedSig = sgn.GetEncodedPKCS7(_documentHash,
                                         PdfSigner.CryptoStandard.CMS,
                                         null,
                                         null,
                                         null);

    return encodedSig;
}

不幸的是,我一定遗漏了一些东西(或很多东西):

我完全没听懂你的意思吗?

编辑:再一次,在@mkl 的带领下,我能够让它发挥作用。就像他说的,你需要散列 GetAuthenticatedAttributeBytes 值:

public override byte[] Sign(Stream data) {
// create PCKS7 for getting attributes
var sgn = new PdfPKCS7(null, _certificates.ToArray(), DigestAlgorithms.SHA256, false);
// get document hash
DocumentDigest = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
// get attributes
var docBytes = sgn.GetAuthenticatedAttributeBytes(DocumentDigest, 
                                PdfSigner.CryptoStandard.CMS, 
                                null, 
                                null);
// hash dochBytes 
using var hashMemoryStream = new MemoryStream(docBytes, false);
var docBytesHash = DigestAlgorithms.Digest(hashMemoryStream, 
                                            DigestAlgorithms.SHA256);
 //prepend sha256 prefix
 var totalHash = new byte[_sha256SigPrefix.Length + docBytesHash.Length];
 _sha256SigPrefix.CopyTo(totalHash, 0);
 docBytesHash.CopyTo(totalHash, _sha256SigPrefix.Length);
 DataToSend = totalHash;
 return new byte[0];
}

再次感谢。

有两个明显的问题,散列错误的数据和注入错误类型的签名:

散列错误数据

你这样计算要签名的哈希:

using var stream = File.OpenRead(pathToPreparedToBeSignedPdf);
var data = DigestAlgorithms.Digest(stream, DigestAlgorithms.SHA256);        

这是不正确的。

签名的 PDF 基本上具有以下结构(阅读 here 了解更多详情):

(顺便说一下,草图不是 100% 正确的,因为签名值周围的尖括号分隔符“<”和“>”也不能被散列。)

您在 pathToPreparedToBeSignedPdf 准备的 PDF 具有相同的结构,只是“签名值”还不是实际的 签名 值,而是一个占位符 8192 十六进制-编码的零字节(8192 因为那是你在 pdfSigner.SignExternalContainer 中给出的数字)。

不过,正如您在草图中看到的那样,签名值(或在您的情况下为占位符)不得经过哈希处理以进行签名。

除了占位符之外,检索准备好的 PDF 的最简单方法是在您用于准备 PDF 的 IExternalSignatureContainer 实现中,因为它的 Sign 方法作为参数获取包含该内容的流。因此,不要使用 ExternalBlankSignatureContainer,而是使用 中的 ExternalEmptySignatureContainer:

public class ExternalEmptySignatureContainer : IExternalSignatureContainer
{
    public void ModifySigningDictionary(PdfDictionary signDic)
    {
        signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
        signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
    }

    public byte[] Sign(Stream data)
    {
        // Store the data to sign and return an empty array
        Data = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
        return new byte[0];
    }

    public byte[] Data;
}

并在准备好从其 Data 成员中检索 byte[] 之后:

var container = new ExternalEmptySignatureContainer();
pdfSigner.SignExternalContainer(container, 8192);
byte[] hash = container.Data;

在您的情况下,您可能必须在 sha256SigPrefix 前面加上一个完整的编码 DigestInfo 对象。

注入错误类型的签名

此外,考虑到您的屏幕截图

您显然注入了错误类型的签名。您设置了一个子过滤器 PdfName.Adbe_pkcs7_detached,这意味着要嵌入的签名是一个 CMS 签名容器,其中有一个 SignerInfo 对 PDF 的签名字节进行签名。不过,错误消息表明您嵌入的签名容器已损坏或不是 CMS 签名容器。

编辑后:注入内容不正确的签名容器

为了解决上一个问题“注入错误类型的签名”,您更改了 ExternalInjectingSignatureContainer 以像这样为您的签名字节构建 CMS 签名容器:

var sgn = new PdfPKCS7(null, _certificates.ToArray(), "SHA256", false);
sgn.SetExternalDigest(_signature, null, "RSA");
return sgn.GetEncodedPKCS7();

不幸的是,这错误地使用了 PdfPKCS7 class 导致 CMS 签名容器包含不正确的数据。

(更正:生成的CMS容器本身并不是不正确,而是结构极其简单。现在许多配置文件 - 例如欧洲 PAdES - 需要额外的签名属性,并且为了满足这些配置文件,您必须按照以下文本中的描述进行修复。如果您只需要 Adob​​e Reader 来显示有效,那么上面的代码就足够了给你。)

要解决这个问题,您可以自己构建一个 CMS 签名容器,使用 iText PdfPKCS7 class 或其他方式(例如 BouncyCastle 或内置 .Net classes) ,或者您可以让 iText 为您完成。

最简单的方法是后一种方法。您实际上根本不需要延迟签名,您只需实现 IExternalSignature 以便它调用您的远程服务:

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

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

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

现在您只需签名就可以了

var pdfSigner = new PdfSigner(new PdfReader(pathToOriginalPdf),
                              new FileStream(pathToSignedFile, FileMode.Create),
                              new StampingProperties());
pdfSigner.SetFieldName(_signatureFieldname);

var appearance = pdfSigner.GetSignatureAppearance();
appearance.SetPageRect(new Rectangle(144, 144, 200, 100))
          .SetPageNumber(1)
          .SetCertificate(certificates[0]);

var signature = new RemoteSignature();
pdfSigner.SignDetached(signature, certificates, null, null, null, 0, PdfSigner.CryptoStandard.CMS);