数字 PDF 文档签名

Digital PDF Document Signing

更新 2:

我已经在 https://1drv.ms/u/s!Al69FgQ8jwmZbgiBMXLLM4j5sbU?e=vyGF4m

上传了样本

你能检查一下吗?我卡在了最后一步。但是,请确认其他方法是否正确。

更新 1:

我确认了流量。所以我很清楚。

作为数字签名 PDF 文档流程的一部分,我们希望使用第三方提供 PDF 的签名哈希。 步骤如下:

  1. 有第三方内部系统可以从 word 生成 PDF 文档。
  2. 该 PDF 将被发送到另一个服务,该服务将生成该 PDF 的哈希值
  3. 该散列值将被发送到外部服务以使用私钥进行散列。
  4. 外部系统将发送签名哈希和 public 密钥证书,内部服务将使用它们在 PDF 文档中添加签名。

我有以下问题。

  1. 在上面的第 1 点中,内部服务正在创建 PDF 和签名块。是否需要创建签名块?因为这是延迟签署?
  2. 如果是这样,第2点的服务如何获取PDF文档的原始内容来生成hash。

我们使用现有的带有签名的 PDF 并使用 iText 7 获取原始内容。 这种方法是否正确? FormB.PDF 有签名,通过删除 signaure1 字段,我们得到了原始内容。此过程是否有效且可取?

我们也尝试过使用pdfsigner.getRangeStream()方法,但是在文档中并不清楚,目前还不清楚。请帮助

package com.abc.sd;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;

import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.signatures.SignatureUtil;

public class ItextPdf7 {

    public static void main(String [] args) throws IOException, NoSuchAlgorithmException {
        String filePath ="C:\\abc\\test\\FormB.pdf";
        PdfReader reader = new PdfReader(filePath);
        PdfDocument pdfDoc = new PdfDocument(reader);
        PdfAcroForm form = PdfAcroForm.getAcroForm(pdfDoc, false);
        SignatureUtil signUtil = new SignatureUtil(pdfDoc);
        List<String> names = signUtil.getSignatureNames();
        System.out.println("Signature Name>>>"+names);
      //  System.out.println("Singature Data>>"+signUtil.readSignatureData("Signature1"));


        PdfReader reader1 = new PdfReader(filePath);
        PdfDocument pdfDoc1 = new PdfDocument(reader1, new PdfWriter("C:\\\\abc\\\\test\\\\unsigned_latest_iext7.pdf"));
        PdfAcroForm form1 = PdfAcroForm.getAcroForm(pdfDoc1, true);
        form1.flattenFields();
        pdfDoc1.close();


    }

}

*********************************

我们正在寻求签署 PDF 文档。以下是我理解的步骤。

  1. 消费者将向中央系统发送 PDF 文档摘要。 PDF 的摘要将不包括签名部分

  2. 中央系统会将摘要(使用消费者的私钥 key/public 签名?不确定)发送给消费者

  3. 用户系统将在 PDF 文档的签名部分添加摘要(可能与 public 键一起??)

能否请您帮忙关注一下。

  1. 以上流程我的理解是否正确?任何小型参考指南/link 或任何流程图都会有所帮助。

  2. 使用 .Net 和 Java 可以完成这项工作的库是什么?开源和付费。 iTextSharp 与此相关吗?

  3. 如果客户打开 PDF,将如何进行验证?如果有任何特定的操作需要文件签署?

请帮忙。

这里有很多方面和sub-questions,在问题文本和下面的评论中。这个答案在首先介绍一些背景之后阐明了其中的一些。

一些背景

集成的 PDF 签名意味着 PDF 中存在许多结构:

  • 签名 AcroForm 表单域。这个表单字段可以有一个小部件注释(一种可视化,可以包含您想要放入其中的任何信息)但它不需要有一个。

  • 此签名表单字段中的一个值。与其他表单字段不同,签名字段的值不是单纯的字符串,而是 key-value 对的字典。内容因签名的确切类型而异。但是,对于可互操作的类型,总是有一个 Contents 条目,其值是一个二进制字符串,包含实际的 PKCS1/PKCS7/CMS/RFC3161 签名或时间戳,它覆盖了整个文件,除了这个二进制字符串。

    (草图有点误导:'<' 和 '>' 十六进制字符串定界符 不是 签名数据的一部分。)

  • 如果类型为 adbe.x509.rsa_sha1Contents 条目包含 PKCS1 签名.签名值字典还必须包含包含签名证书的 Cert 条目。

  • 在类型 ETSI.RFC3161 的情况下,Contents 条目包含一个 RFC 3161 时间戳令牌。

  • 对于类型 ETSI.CAdES.detachedadbe.pkcs7.detachedadbe.pkcs7.sha1 Contents 条目包含一个 CMS 签名容器。由于签名容器可以保存签名证书,因此不需要 Cert 条目来签名证书。

    CMS 签名容器可以包含 "signed attributes" 的结构。如果是,这些属性之一必须是签名 PDF 字节的哈希值(见上文,除了 Contents 之外的所有内容)和实际包装在容器中的签名字节对这些签名属性进行签名。是否允许没有签名属性的变体以及额外需要哪些属性取决于签名的确切类型。

  • ETSI.CAdES.detached 的情况下,CMS 容器 必须 包含已签名的属性。此外,签名属性之一 必须 是引用签名者证书的 ESS signing-certificate 或 signing-certificate-v2 属性。

    这种情况下的 LTV 信息可以稍后添加到 PDF 的增量更新中,它们不需要出现在签名的 PDF 中。

  • adbe.pkcs7.detachedadbe.pkcs7.sha1 的情况下,一般不会需要签名的属性。但是,根据具体的签名策略(法律或合同规定),可能仍然需要签名属性,尤其是 ESS 签名证书签名属性。

    这些签名类型已在 ISO 32000-1 中定义。如果一个人的签名策略仅基于 ISO 32000-1,则 LTV 信息必须存储在 adbe-revocationInfoArchival 属性中,该属性必须是签名属性。

签名前是否需要签名证书?

在评论中,您引用了 iText "PDF and Digital Signatures" 电子书,该电子书似乎说检索签名证书和签名就足够了。

不过,根据上述背景,我们意识到

  • 如果是 adbe.x509.rsa_sha1 签名,签名证书的值必须是 Cert 签名值字典条目。由于此条目不在 Contents 条目中,因此此证书是签名数据的一部分。因此,在签名之前必须知道

  • ETSI.CAdES.detached 签名的情况下,签名属性必须包含 ESS signing-certificate 或 signing-certificate-v2属性。此属性引用签署者证书。因此,在签名之前必须知道

  • adbe.pkcs7.detachedadbe.pkcs7.sha1 的情况下,它取决于实际的签名策略必须遵守是否需要 ESS signing-certificate 或 signing-certificate-v2 属性。因此,这取决于签名前是否需要知道签名证书。

    但是,在仅基于 ISO 32000-1 的签名策略的情况下,如果有的话,LTV 信息必须存储在签名属性中,并且要检索 LTV 信息,显然需要知道哪个证书尝试检索它们,特别是签名者证书。

要回答本题中的问题header,因此:仅在松懈的情况下ignature policy 只要您不需要添加 LTV 信息,您就可以在签名前不知道签名者证书。

如果是 PAdES 签名?

您在评论中提到您需要使用 PAdES 和 LTV。这是否意味着您在签名前需要签名者证书?

好吧,这取决于。

如果使用 PAdES 意味着使用 PAdES 基线配置文件或扩展 PAdES 配置文件 (BES/EPES),则必须创建 ETSI.CAdES.detached签名。因此,您确实需要签名者证书才能签名。

但是如果它只需要 PDF 格式的 CMS 数字签名的 PAdES 配置文件(本质上是 ISO 32000-1 兼容性配置文件),您不需要签名者证书才能签名.

但此配置文件特别暗示:如果存在,任何撤销信息都应是 PDF 签名的签名属性。 因此,"PAdES and LTV" 您再次 在签名之前需要 签名者证书。

如何在不知道签名者证书的情况下创建 PDF 签名

所以有些设置在计算实际签名之前不需要签名者证书。不过,通常情况下,安全 API 仍然需要尽早提供证书。

使用 Bouncy Castle low-level API,您可以按如下方式进行操作。 (我假设您使用的是 SHA256withRSA。)

首先准备PDF并确定哈希值

byte[] Hash = null;

using (PdfReader reader = new PdfReader("original.pdf"))
using (FileStream fout = new FileStream("prepared.pdf", FileMode.Create))
{
    StampingProperties sp = new StampingProperties();
    sp.UseAppendMode();

    PdfSigner pdfSigner = new PdfSigner(reader, fout, sp);
    pdfSigner.SetFieldName("Signature");

    PdfSignatureAppearance appearance = pdfSigner.GetSignatureAppearance();
    appearance.SetPageNumber(1);

    int estimatedSize = 12000;
    ExternalHashingSignatureContainer container = new ExternalHashingSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
    pdfSigner.SignExternalContainer(container, estimatedSize);
    Hash = container.Hash;
}

现在要签名的 PDF 字节的哈希值在 Hash

这里用到的ExternalHashingSignatureContainerclass就是下面这个帮手class:

public class ExternalHashingSignatureContainer : ExternalBlankSignatureContainer
{
    public ExternalHashingSignatureContainer(PdfName filter, PdfName subFilter) : base(filter, subFilter)
    { }

    public override byte[] Sign(Stream data)
    {
        SHA256 sha = new SHA256CryptoServiceProvider();
        Hash = sha.ComputeHash(data);
        return new byte[0];
    }

    public byte[] Hash { get; private set; }
}

对于上面在 Hash 变量中计算的散列,您现在可以请求 PKCS#1 签名和签名者证书。那么就可以构建CMS容器如下:

byte[] signatureBytes = THE_RETRIEVED_SIGNATURE_BYTES;
byte[] certificateBytes = THE_RETRIEVED_CERTIFICATE_BYTES;

X509Certificate x509Certificate = new X509CertificateParser().ReadCertificate(certificateBytes);

SignerIdentifier sid = new SignerIdentifier(new IssuerAndSerialNumber(x509Certificate.IssuerDN, x509Certificate.SerialNumber));
AlgorithmIdentifier digAlgorithm = new AlgorithmIdentifier(NistObjectIdentifiers.IdSha256);
Attributes authenticatedAttributes = null;
AlgorithmIdentifier digEncryptionAlgorithm = new AlgorithmIdentifier(Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.Sha256WithRsaEncryption);
Asn1OctetString encryptedDigest = new DerOctetString(signatureBytes);
Attributes unauthenticatedAttributes = null;
SignerInfo signerInfo = new SignerInfo(sid, digAlgorithm, authenticatedAttributes, digEncryptionAlgorithm, encryptedDigest, unauthenticatedAttributes);

Asn1EncodableVector digestAlgs = new Asn1EncodableVector();
digestAlgs.Add(signerInfo.DigestAlgorithm);
Asn1Set digestAlgorithms = new DerSet(digestAlgs);
ContentInfo contentInfo = new ContentInfo(CmsObjectIdentifiers.Data, null);
Asn1EncodableVector certs = new Asn1EncodableVector();
certs.Add(x509Certificate.CertificateStructure.ToAsn1Object());
Asn1Set certificates = new DerSet(certs);
Asn1EncodableVector signerInfs = new Asn1EncodableVector();
signerInfs.Add(signerInfo);
Asn1Set signerInfos = new DerSet(signerInfs);
SignedData signedData = new SignedData(digestAlgorithms, contentInfo, certificates, null, signerInfos);

contentInfo = new ContentInfo(CmsObjectIdentifiers.SignedData, signedData);

byte[] Signature = contentInfo.GetDerEncoded();

现在 CMS 签名容器字节在 Signature

对于以上内容,请使用这些 BouncyCastle usings

using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Cms;
using Org.BouncyCastle.Asn1.Nist;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.X509;

您现在可以像这样将签名容器字节嵌入到 PDF 中:

using (PdfReader reader = new PdfReader("prepared.pdf"))
using (PdfDocument document = new PdfDocument(reader))
using (FileStream fout = new FileStream("signed.pdf", FileMode.Create))
{
    PdfSigner.SignDeferred(document, "Signature", fout, new ExternalPrecalculatedSignatureContainer(Signature));
}

这里用到的ExternalPrecalculatedSignatureContainerclass就是下面这个帮手class:

public class ExternalPrecalculatedSignatureContainer : ExternalBlankSignatureContainer
{
    public ExternalPrecalculatedSignatureContainer(byte[] cms) : base(new PdfDictionary())
    {
        Cms = cms;
    }

    public override byte[] Sign(Stream data)
    {
        return Cms;
    }

    public byte[] Cms { get; private set; }
}

但是,如上所述,此签名容器不是 CAdES 容器。因此,您的 PDF 签名不会是真正的 PAdES 签名(基线或扩展配置文件),但最多是 ISO 32000-1 兼容性 PAdES 签名。

你的测试代码中的问题基于以上

您的 Client 方法 createSignedData 如下所示:

public byte[] createSignedData(byte[] sh)
{
    string dire = Directory.GetParent(Directory.GetParent(Directory.GetCurrentDirectory()).ToString()).ToString();
    string PROPERTIES = dire + "\resources\signkey.properties";
    Properties properties = new Properties();
    properties.Load(new FileStream(PROPERTIES, FileMode.Open, FileAccess.Read));
    String path = properties.GetProperty("PRIVATE");
    char[] pass = properties.GetProperty("PASSWORD").ToCharArray();
    string alias = null;
    Pkcs12Store pk12;
    pk12 = new Pkcs12Store(new FileStream(path, FileMode.Open, FileAccess.Read), pass);
    foreach (var a in pk12.Aliases)
    {
        alias = ((string)a);
        if (pk12.IsKeyEntry(alias))
            break;
    }

    ICipherParameters pk = pk12.GetKey(alias).Key;
    IExternalSignature pks = new PrivateKeySignature(pk, DigestAlgorithms.SHA256);
    byte[] data = pks.Sign(sh);
    return data;

}

不幸的是 PrivateKeySignature.Sign 期望消息在 sh 参数中签名,特别是首先对其进行哈希处理。另一方面,在您的用例中 sh 已经是要签名的消息的哈希值。因此,您实际上在应该哈希一次的地方哈希了两次。

您可以通过更换

来解决这个问题
IExternalSignature pks = new PrivateKeySignature(pk, DigestAlgorithms.SHA256);
byte[] data = pks.Sign(sh);

在上面的代码中

StaticDigest digest = new StaticDigest();
digest.AlgorithmName = "SHA-256";
digest.Digest = sh;
RsaDigestSigner signer = new RsaDigestSigner(digest);
signer.Init(true, pk);
byte[] data = signer.GenerateSignature();

这里StaticDigest是下面的帮手class:

public class StaticDigest : IDigest
{
    public string AlgorithmName { get; set; }
    public byte[] Digest { get; set; }

    public void BlockUpdate(byte[] input, int inOff, int length)
    { }

    public int DoFinal(byte[] output, int outOff)
    {
        Array.Copy(Digest, 0, output, outOff, Digest.Length);
        return Digest.Length;
    }

    public int GetByteLength()
    {
        return 64;
    }

    public int GetDigestSize()
    {
        return Digest.Length;
    }

    public void Reset()
    { }

    public void Update(byte input)
    { }
}

在此之后更改您的测试项目 returns 数学上有效的签名。