PDF 签名验证

PDF Signature Validation

我正在尝试使用 PDFBox 和 BouncyCastle 验证 PDF 签名。我的代码适用于大多数 PDF:s,但是有一个文件,使用 BouncyCastle 的密码验证失败。我正在使用 pdfbox 1.8、BouncyCastle 1.52。测试输入的pdf文件是从某处随机获取的,似乎是使用iText生成的。 Test pdf file

public void testValidateSignature() throws Exception
{
    byte[] pdfByte;
    PDDocument pdfDoc = null;
    SignerInformationVerifier verifier = null;
    try
    {
        pdfByte = IOUtils.toByteArray( this.getClass().getResourceAsStream( "SignatureVlidationTest.pdf" ) );
        pdfDoc = PDDocument.load( new ByteArrayInputStream( pdfByte ));
        PDSignature signature = pdfDoc.getSignatureDictionaries().get( 0 );

        byte[] signatureAsBytes = signature.getContents( pdfByte );
        byte[] signedContentAsBytes = signature.getSignedContent( pdfByte );
        CMSSignedData cms = new CMSSignedData( new CMSProcessableByteArray( signedContentAsBytes ), signatureAsBytes);
        SignerInformation signerInfo = (SignerInformation)cms.getSignerInfos().getSigners().iterator().next();
        X509CertificateHolder cert = (X509CertificateHolder)cms.getCertificates().getMatches( signerInfo.getSID() ).iterator().next();
        verifier = new JcaSimpleSignerInfoVerifierBuilder( ).setProvider( new BouncyCastleProvider() ).build( cert );

        // result if false
        boolean verifyRt = signerInfo.verify( verifier );

    }
    finally
    {
        if( pdfDoc != null )
        {
            pdfDoc.close();
        }
    }

}

您的代码完全忽略了签名的 SubFilter。适用于具有 SubFilteradbe.pkcs7.detachedETSI.CAdES.detached[=43= 的签名] 但对于具有 SubFilteradbe.pkcs7.sha1adbe.x509.rsa.sha1[ 的签名将失败=43=].

您提供的示例文档已使用 SubFilteradbe.pkcs7.sha1.

的签名进行签名

有关如何创建具有这些 SubFilter 值的签名以及因此必须对其进行验证的详细信息,请参见 PDF 规范 ISO 32000-1 第 12.8 节 数字签名.


这是一种稍微改进的验证方法:

boolean validateSignaturesImproved(byte[] pdfByte, String signatureFileName) throws IOException, CMSException, OperatorCreationException, GeneralSecurityException
{
    boolean result = true;
    try (PDDocument pdfDoc = PDDocument.load(pdfByte))
    {
        List<PDSignature> signatures = pdfDoc.getSignatureDictionaries();
        int index = 0;
        for (PDSignature signature : signatures)
        {
            String subFilter = signature.getSubFilter();
            byte[] signatureAsBytes = signature.getContents(pdfByte);
            byte[] signedContentAsBytes = signature.getSignedContent(pdfByte);
            System.out.printf("\nSignature # %s (%s)\n", ++index, subFilter);

            if (signatureFileName != null)
            {
                String fileName = String.format(signatureFileName, index);
                Files.write(new File(RESULT_FOLDER, fileName).toPath(), signatureAsBytes);
                System.out.printf("    Stored as '%s'.\n", fileName);
            }

            final CMSSignedData cms;
            if ("adbe.pkcs7.detached".equals(subFilter) || "ETSI.CAdES.detached".equals(subFilter))
            {
                cms = new CMSSignedData(new CMSProcessableByteArray(signedContentAsBytes), signatureAsBytes);
            }
            else if ("adbe.pkcs7.sha1".equals(subFilter))
            {
                cms = new CMSSignedData(new ByteArrayInputStream(signatureAsBytes));
            }
            else if ("adbe.x509.rsa.sha1".equals(subFilter) || "ETSI.RFC3161".equals(subFilter))
            {
                result = false;
                System.out.printf("!!! SubFilter %s not yet supported.\n", subFilter);
                continue;
            }
            else if (subFilter != null)
            {
                result = false;
                System.out.printf("!!! Unknown SubFilter %s.\n", subFilter);
                continue;
            }
            else
            {
                result = false;
                System.out.println("!!! Missing SubFilter.");
                continue;
            }

            SignerInformation signerInfo = (SignerInformation) cms.getSignerInfos().getSigners().iterator().next();
            X509CertificateHolder cert = (X509CertificateHolder) cms.getCertificates().getMatches(signerInfo.getSID())
                    .iterator().next();
            SignerInformationVerifier verifier = new JcaSimpleSignerInfoVerifierBuilder().setProvider(new BouncyCastleProvider()).build(cert);

            boolean verifyResult = signerInfo.verify(verifier);
            if (verifyResult)
                System.out.println("    Signature verification successful.");
            else
            {
                result = false;
                System.out.println("!!! Signature verification failed!");

                if (signatureFileName != null)
                {
                    String fileName = String.format(signatureFileName + "-sigAttr.der", index);
                    Files.write(new File(RESULT_FOLDER, fileName).toPath(), signerInfo.getEncodedSignedAttributes());
                    System.out.printf("    Encoded signed attributes stored as '%s'.\n", fileName);
                }

            }

            if ("adbe.pkcs7.sha1".equals(subFilter))
            {
                MessageDigest md = MessageDigest.getInstance("SHA1");
                byte[] calculatedDigest = md.digest(signedContentAsBytes);
                byte[] signedDigest = (byte[]) cms.getSignedContent().getContent();
                boolean digestsMatch = Arrays.equals(calculatedDigest, signedDigest);
                if (digestsMatch)
                    System.out.println("    Document SHA1 digest matches.");
                else
                {
                    result = false;
                    System.out.println("!!! Document SHA1 digest does not match!");
                }
            }
        }
    }
    return result;
}

(摘自ValidateSignature.java

此方法考虑 SubFilter 值并正确处理具有 SubFilter 值的签名 adbe.pkcs7.sha1。它还不支持 adbe.x509.rsa.sha1ETSI.RFC3161 签名/时间戳,但至少提供了适当的输出。

在对我的其他回答的评论中,OP 问了一个相关问题

This time it is adbe.pkcs7.detached signature, it also fails in cryptographic validation. I extracted the signedContent and signature, run a unit test with original BC source code, the failure is in Arrays.constantTimeAreEqual(sig, expected) when compare the signature with computed expected digest. test pdf

(严格来说,这样一个单独的(如果相关的话)问题应该作为一个单独的堆栈溢出问题来提出,但仍然很有趣,可以对其进行调查。)

###TL;博士

有问题的签名属性没有正确的 DER 编码,它们只是以不同的、也可能的 BER 编码呈现。一些验证器按原样采用已签名的属性,一些验证器在验证前强制执行 DER 编码。因此,后者间接拒绝编码不正确的签名属性。 Adobe Reader 是前者的样本,BouncyCastle 是后者的样本。

###详细

我的其他答案中的改进方法validateSignaturesImproved也显示验证失败但作为帮助它输出编码的签名属性.与签名容器的相应部分相比,此输出显示了问题。


一些背景知识:

除最原始的 CMS 签名容器外,其他所有签名容器都不直接对文档数据进行签名,而是使用一组所谓的 签名属性 进行签名,这些属性又包含文档的哈希值数据。

PDF文件中嵌入的签名容器中的数据有一定的编码规则。一方面,基本编码规则 (BER) 允许不同的方式对同一类型的数据进行编码;例如集合的元素可以以任何顺序出现。并且有区分编码规则(DER),它只允许一种方式对给定数据进行编码;例如有一个预定义的顺序,其中必须给出集合的元素。

根据 CMS 规范RFC 5652:

SignedAttributes MUST be DER encoded, even if the rest of the structure is BER encoded.

(Section 5.3 - SignerInfo Type)

严格来说PDF规范ISO 32000-1更为严格,它要求:

When PKCS#7 signatures are used, the value of Contents shall be a DER-encoded PKCS#7 binary data object containing the signature. The PKCS#7 object shall conform to RFC3852 Cryptographic Message Syntax.

(第 12.8.3.3 节 - ISO 32000 中使用的 PKCS#7 签名)

(RFC 3852 是 RFC 5652 的前身,已被其淘汰。)

因此,对于 PDF 中可互操作的 CMS 签名,整个签名容器必须采用 DER 编码。


手头的签名确实不是太琐碎并且使用了签名属性。签名容器包含这些签名属性:

SEQUENCE {
  OBJECT IDENTIFIER contentType (1 2 840 113549 1 9 3)
  SET {
    OBJECT IDENTIFIER data (1 2 840 113549 1 7 1)
    }
  }
SEQUENCE {
  OBJECT IDENTIFIER signingTime (1 2 840 113549 1 9 5)
  SET {
    UTCTime 07/12/2016 16:11:08 GMT
    }
  }
SEQUENCE {
  OBJECT IDENTIFIER
    signingCertificateV2 (1 2 840 113549 1 9 16 2 47)
  SET {
    SEQUENCE {
      SEQUENCE {
        SEQUENCE {
          OCTET STRING
            43 D1 C4 40 09 EB 32 46 B0 5C 2D A8 81 71 54 48
            F4 A3 9D 6F E3 6B 5C 9E 8F 4B 07 6D 10 55 D2 C8
          }
        }
      }
    }
  }
SEQUENCE {
  OBJECT IDENTIFIER messageDigest (1 2 840 113549 1 9 4)
  SET {
    OCTET STRING
      E9 23 CC 92 ED 09 3B CE 51 78 DE 86 E0 F0 C8 6E
      9B CD 82 CB 35 A0 BC 66 38 BC 13 DE F3 7D C7 BC
    }
  }

但是这些集合元素的正确 DER 顺序应该是这样的:

SEQUENCE {
  OBJECT IDENTIFIER contentType (1 2 840 113549 1 9 3)
  SET {
    OBJECT IDENTIFIER data (1 2 840 113549 1 7 1)
    }
  }
SEQUENCE {
  OBJECT IDENTIFIER signingTime (1 2 840 113549 1 9 5)
  SET {
    UTCTime 07/12/2016 16:11:08 GMT
    }
  }
SEQUENCE {
  OBJECT IDENTIFIER messageDigest (1 2 840 113549 1 9 4)
  SET {
    OCTET STRING
      E9 23 CC 92 ED 09 3B CE 51 78 DE 86 E0 F0 C8 6E
      9B CD 82 CB 35 A0 BC 66 38 BC 13 DE F3 7D C7 BC
    }
  }
SEQUENCE {
  OBJECT IDENTIFIER signingCertificateV2 (1 2 840 113549 1 9 16 2 47)
  SET {
    SEQUENCE {
      SEQUENCE {
        SEQUENCE {
          OCTET STRING
            43 D1 C4 40 09 EB 32 46 B0 5C 2D A8 81 71 54 48
            F4 A3 9D 6F E3 6B 5C 9E 8F 4B 07 6D 10 55 D2 C8
          }
        }
      }
    }
  }

如您所见,最后两个属性在签名容器中的 DER 顺序不正确。


在验证签名数据时,BouncyCastle 首先将签名容器解析为对象表示并忘记原始字节。为了检索用于散列的签名属性,它创建与内部对象表示相对应的 DER 编码。因此,BouncyCastle 对后一组进行哈希处理。

另一方面,

Adobe Reader 似乎采用已签名的属性,因为它们是在嵌入式签名容器中编码的。因此,它散列前一组。

原来的签名软件显然也在第一个(无效!)命令中对签名属性进行了签名。因此,Adobe Reader 成功验证了签名,而 BouncyCastle 没有。

严格来说,这是 Adob​​e Reader 中的一个错误。另一方面,现实世界中使用的许多 PDF 签名产品太笨了,无法正确排序签名属性,因此(也)接受给定顺序的签名属性可能只是正确的方法,即使关于警告结构性问题是合适的。


而且像DocuSign这样的大型签名服务还没有学习签名容器创建的基础知识,真是太可惜了。