iText 使用带有智能卡的外部签名对 PDF 进行签名

iText signing PDF using external signature with smart card

我玩 iTextSharp 5.5.7 有一段时间了,但找不到从智能卡为 PDF 制作有效数字签名的正确方法 - Adob​​e Reader 总是说它由和签署未知且无法解码签名的 DER 数据。

我查看了 MakeSignature.cs 代码以供参考,它的作用是:

        Stream data = signatureAppearance.GetRangeStream(); 
        // gets the first hash
        byte[] hash = DigestAlgorithms.Digest(data, hashAlgorithm);
        // gets the second hash or is it not a hash at all ?
        byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, ocsp, crlBytes, sigtype);

然后,按照"sign"中的方法IExternalSignature.cs

"@param message the message you want to be hashed and signed"

        // looks like externalSignature.Sign() should make another hash out of "sh"
        // and use this hash to compute a signature
        byte[] extSignature = externalSignature.Sign(sh); 

所以我理解的签约流程如下:

  1. 源 PDF 已加载
  2. 已创建签名字段为空的新 PDF
  3. 该字段的字节范围被散列(默认情况下为 sha-1 生成 20 个字节,也尝试使用 sha-256 生成 32 个字节)
  4. Hash + 一些其他属性被再次散列(字节数 不同,为什么?毕竟可能不是哈希?)
  5. 第二个散列在外部签名对象内部再次散列
  6. 第三个哈希最终被发送到智能卡以计算 签名
  7. 签名已插入到新 PDF 中

当我使用 Adob​​e Reader 签署 PDF 时,在第 6 步,第三个散列长度为 32 字节。 从智能卡的角度来看,我对 Acrobat 和 iText 执行了相同的步骤,但是对于 iText,签名无效,可能有什么问题?


我使用的代码:

public void StartTest(){
        X509Certificate2 cert = new X509Certificate2();
        cert.Import("cert.cer"); // certificate obtained from smart card

        X509CertificateParser certParse = new Org.BouncyCastle.X509.X509CertificateParser();

        Org.BouncyCastle.X509.X509Certificate[] chain = new Org.BouncyCastle.X509.X509Certificate[] { certParse.ReadCertificate(cert.RawData) };

        // Reader and stamper
        PdfReader pdfReader = new PdfReader("original.pdf");
        Stream signedPdf = new FileStream("signed.pdf", FileMode.Create);
        PdfStamper stamper = PdfStamper.CreateSignature(pdfReader, signedPdf, '[=12=]', null, false);

        // Appearance
        PdfSignatureAppearance appearance = stamper.SignatureAppearance;
        appearance.SignatureCreator = "Me";
        appearance.Reason = "Testing iText";
        appearance.Location = "On my Laptop";
        appearance.SignatureGraphic = Image.GetInstance("img.png"); // visual image
        appearance.SetVisibleSignature(new Rectangle(50, 50, 250, 100), pdfReader.NumberOfPages, "Signature");
        appearance.SignatureRenderingMode = PdfSignatureAppearance.RenderingMode.GRAPHIC_AND_DESCRIPTION;

        // Timestamp
        TSAClientBouncyCastle tsc = new TSAClientBouncyCastle("http://ts.cartaodecidadao.pt/tsa/server", "", "");

        // Digital signature
        IExternalSignature externalSignature = new MyExternalSignature2("SHA-1");
        MyMakeSignature.SignDetached(appearance, externalSignature, chain, null, null, tsc, 0, CryptoStandard.CADES);

        stamper.Close();
}

外部签名实现(class MyExternalSignature2):

    class MyExternalSignature2 : IExternalSignature
{
    private String hashAlgorithm;
    private String encryptionAlgorithm;

    public MyExternalSignature2(String hashAlgorithm)
    {
        this.encryptionAlgorithm = "RSA";
        this.hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigests(hashAlgorithm));
    }

    public virtual byte[] Sign(byte[] message) {

        byte[] hash = null;
        using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
        {
            hash = sha1.ComputeHash(message);
        }

        byte[] sig = MySC.GetSignature(hash);

        return sig;
    }

    public virtual String GetHashAlgorithm() {
        return hashAlgorithm;
    }

    public virtual String GetEncryptionAlgorithm() {
        return encryptionAlgorithm;
    }
}

OP 提供了一些样本签名文件,分析它们很明显我最初的回答是基于误解。

分析提供的签名 PDF

OP 提供了三个由他的代码签名的 PDF,首先是这两个:

检查它们,使用 SHA1 签名的 iText CAdES 的特性变得很明显:对于 CryptoStandard.CADES,iText 使用 SigningCertificateV2 属性,即使对于 SHA1,但规范建议在那里使用 SigningCertificate 属性。为了防止这种特殊性干扰,OP 提供了第三个文件

不过,事实证明,这个怪癖并不是 OP 观察到的原因,Adobe Reader 仍然报告加密库错误。

于是,回到分析。

由于签名算法是 SHA1withRSA/2048 和 SHA256withRSA/2048,可以使用来自相应证书的 public 密钥简单地解密内部签名值。

ex_signed.pdf 成功,但 ex_signed_2.pdf 或 ex_signed_3.pdf 失败。

OP同时在评论中指出:

the difference between those files are certificates used for signatures, in the first i used a certificate for authentication, which is (i think is) acceptable but i have another certificate specifically for digital signatures, which i used in 2nd and 3rd file.

所以我尝试使用第一个文件中的证书对第二个和第三个文件中的签名值进行解码,并且确实有效!因此,第二个和第三个文件声称是由与该替代证书关联的私钥签名的,但实际上是使用与前一个证书关联的私钥签名的。

因此:问题 1:文件 2 和 3 中的签名使用错误的私钥/智能卡上的错误应用程序签名。

为了进一步分析问题,我查看了使用身份验证证书成功解码的签名值:

  • ex_signed.pdf:

      2a8945abe450b2c1cd232249b8f811d352ad0d29
    
  • ex_signed_2.pdf

      cc24acc848002df63733941e34437f8aef1c746c
    
  • ex_signed_3.pdf

      45f8e451f8b9f39f0c1f59eea8b6308fba22176ac62ebd14bbf07e5407aed7e8
    

所以对于 SHA1 有 20 个字节,对于 SHA-256 有 32 个字节。这正是散列值的大小,因此很可能这些只是裸散列值。

不过,这是错误的,XXXwithRSA 签名应包含一个加密结构,该结构包含散列算法的 OID 和散列,采用 ASN.1 表示法:

DigestInfo ::= SEQUENCE {
  digestAlgorithm DigestAlgorithmIdentifier, 
  digest Digest
  }

DigestAlgorithmIdentifier ::= AlgorithmIdentifier

Digest ::= OCTET STRING

对于背景,请参见。 RFC 3447.

这解释了 OP 的观察结果:

the error in the first is described as "BER decoding error",

验证者试图将裸散列解释为使用显然失败的 BER 编码的 ASN.1 序列。

因此:问题 2:智能卡加密裸散列值但必须加密封装该散列的 DigestInfo 对象。

最初的,过时的答案

如果您的 MySC.GetSignature 在签署数据时调用散列(并且不希望数据之前已经散列),您应该替换

public virtual byte[] Sign(byte[] message)
{
    byte[] hash = null;
    using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
    {
        hash = sha1.ComputeHash(message);
    }

    byte[] sig = MySC.GetSignature(hash);

    return sig;
}

类似

public virtual byte[] Sign(byte[] message)
{
    byte[] sig = MySC.GetSignature(message);

    return sig;
}