PDF签名验证

时间:2016-12-13 08:46:11

标签: validation itext bouncycastle pdfbox signature

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

* {
  margin: 0;
  padding: 0;
}
*, *:before, *:after {
    box-sizing: border-box;
}
section.wrap {
  width: 100%;
}
section.inner:before {
  content: '';
  display: block;
  clear: both;
}

div {
  width: 33.33%;
  height: 40px;
  background-color: powderblue;
  float: left;
  border-top: solid 0px white;
  border-right: solid 5px white;
  border-bottom: solid 5px white;
  border-left: solid 0px white;
}

div:nth-of-type(3n + 3) {
    border-right: 0;
}

2 个答案:

答案 0 :(得分:1)

您的代码完全忽略签名的 SubFilter 。它适用于 SubFilter adbe.pkcs7.detached ETSI.CAdES.detached 的签名,但对于 SubFilter的签名将失败 adbe.pkcs7.sha1 adbe.x509.rsa.sha1

您提供的示例文档已使用 SubFilter adbe.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.sha1 ETSI.RFC3161 签名/时间戳,但至少会提供适当的输出。

答案 1 :(得分:0)

在对我的其他答复的评论中,OP询问了相关问题

  

这次是adbe.pkcs7.detached签名,它在加密验证中也失败了。我提取了signedContent和签名,使用原始BC源代码运行单元测试,当将签名与计算的预期摘要进行比较时,失败在Arrays.constantTimeAreEqual(sig,expected)中。 test pdf

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

TL; DR

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

详细

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

一些背景知识:

除最原始的CMS签名容器之外的所有容器都不直接签署文档数据,而是签署一组所谓的签名属性,其中包含文档数据的哈希值。

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

根据CMS规范RFC 5652

  

SignedAttributes必须是DER编码的,即使其余部分也是如此         结构是BER编码的。

Section 5.3 - SignerInfo Type

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

  

当使用PKCS#7签名时,Contents的值应为包含签名的DER编码的PKCS#7二进制数据对象。 PKCS#7对象应符合RFC3852加密消息语法。

(第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则没有。

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

即使像DocuSign这样的大型签名服务还没有学习签名容器创建的基础知识,真的很可惜。