如何使用iText添加PAdES-LTV

我试图在没有LTV格式的已签名PDF文档中启用LTV。 我在所有情况下都找到了相同的示例,如链接中所述如何为时间戳签名启用LTV , 启用iText LTV – 如何添加更多CRL? ,它定义了获得预期结果的程序。 碰巧我不工作,它没有给我任何错误,但我没有添加LTV。

一些想法为什么在执行以下代码时没有给我任何错误但是我不添加LTV。

这是我尝试添加LTV的方法:

public void addLtv(String src, String dest, OcspClient ocsp, CrlClient crl, TSAClient tsa) throws IOException, DocumentException, GeneralSecurityException { PdfReader r = new PdfReader(src); FileOutputStream fos = new FileOutputStream(dest); PdfStamper stp = PdfStamper.createSignature(r, fos, '\0', null, true); LtvVerification v = stp.getLtvVerification(); AcroFields fields = stp.getAcroFields(); List names = fields.getSignatureNames(); String sigName = names.get(names.size() - 1); PdfPKCS7 pkcs7 = fields.verifySignature(sigName); if (pkcs7.isTsp()) { v.addVerification(sigName, ocsp, crl, LtvVerification.CertificateOption.SIGNING_CERTIFICATE, LtvVerification.Level.OCSP_CRL, LtvVerification.CertificateInclusion.NO); } else { for (String name : names) { v.addVerification(name, ocsp, crl, LtvVerification.CertificateOption.WHOLE_CHAIN, LtvVerification.Level.OCSP_CRL, LtvVerification.CertificateInclusion.NO); } } PdfSignatureAppearance sap = stp.getSignatureAppearance(); LtvTimestamp.timestamp(sap, tsa, null); } 

我正在使用的版本:

  • itext:5.5.11
  • java:8

正如本评论中所述

我想要的是Adobe LTV启用

该任务与PAdES相关性较小(即使使用了PAdES中引入的机制),但侧重于Adobe专有签名配置文件, “LTV启用”签名

遗憾的是,未正确指定此专有签名配置文件。 所有Adobe告诉我们的是

启用LTV意味着validation文件所需的所有信息(减去根证书)都包含在其中。

(有关详细信息和背景,请阅读此答案 )

因此,实现LTV的方式使示例签名涉及一些试验和错误,并且我不能保证Adobe将接受此代码的输出作为Adobe LTrobat版本中的“LTV启用”。

此外,当前的iText 5签名API不足以完成任务,因为(事实certificate)Adobe要求iText代码不会创建某些其他可选结构。 解决这个问题的最简单方法是在两个方面更新iText类LtvVerification ,所以我将在这里描述。 或者,可以使用Javareflection或复制并调整相当多的代码; 如果您无法更新iText,如下所示,您将不得不选择一种这样的替代方法。

LTV启用签名PDF的签名

本节显示了LTV启用文档的代码添加和更改,例如OP的示例PDF sign_without_LTV.pdf

使用iText的LtvVerification类的方法

这是使用iText签名API中的LtvVerification类的原始代码。 不幸的是,必须将该function添加到该类。

修补LtvVerification

iText 5 LtvVerification类仅提供接受签名字段名称的addVerification方法。 对于未绑定到表单字段的签名,我们还需要这些方法的function,例如,对于OCSP响应签名。 为此,我添加了该方法的以下重载:

 public boolean addVerification(PdfName signatureHash, Collection ocsps, Collection crls, Collection certs) throws IOException, GeneralSecurityException { if (used) throw new IllegalStateException(MessageLocalization.getComposedMessage("verification.already.output")); ValidationData vd = new ValidationData(); if (ocsps != null) { for (byte[] ocsp : ocsps) { vd.ocsps.add(buildOCSPResponse(ocsp)); } } if (crls != null) { for (byte[] crl : crls) { vd.crls.add(crl); } } if (certs != null) { for (byte[] cert : certs) { vd.certs.add(cert); } } validated.put(signatureHash, vd); return true; } 

此外,最终VRI词典中的(根据规范可选)时间条目是必需的。 因此,我在outputDss方法中添加了一行,如下所示:

 ... if (ocsp.size() > 0) vri.put(PdfName.OCSP, writer.addToBody(ocsp, false).getIndirectReference()); if (crl.size() > 0) vri.put(PdfName.CRL, writer.addToBody(crl, false).getIndirectReference()); if (cert.size() > 0) vri.put(PdfName.CERT, writer.addToBody(cert, false).getIndirectReference()); // v--- added line vri.put(PdfName.TU, new PdfDate()); // ^--- added line vrim.put(vkey, writer.addToBody(vri, false).getIndirectReference()); ... 

一些低级辅助方法

需要一些在安全原语上运行的辅助方法。 这些方法主要是从现有的iText类中收集的(因为它们是私有的,因此无法使用)或从代码中派生出来:

 static X509Certificate getOcspSignerCertificate(byte[] basicResponseBytes) throws CertificateException, OCSPException, OperatorCreationException { JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME); BasicOCSPResponse borRaw = BasicOCSPResponse.getInstance(basicResponseBytes); BasicOCSPResp bor = new BasicOCSPResp(borRaw); for (final X509CertificateHolder x509CertificateHolder : bor.getCerts()) { X509Certificate x509Certificate = converter.getCertificate(x509CertificateHolder); JcaContentVerifierProviderBuilder jcaContentVerifierProviderBuilder = new JcaContentVerifierProviderBuilder(); jcaContentVerifierProviderBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME); final PublicKey publicKey = x509Certificate.getPublicKey(); ContentVerifierProvider contentVerifierProvider = jcaContentVerifierProviderBuilder.build(publicKey); if (bor.isSignatureValid(contentVerifierProvider)) return x509Certificate; } return null; } static PdfName getOcspSignatureKey(byte[] basicResponseBytes) throws NoSuchAlgorithmException, IOException { BasicOCSPResponse basicResponse = BasicOCSPResponse.getInstance(basicResponseBytes); byte[] signatureBytes = basicResponse.getSignature().getBytes(); DEROctetString octetString = new DEROctetString(signatureBytes); byte[] octetBytes = octetString.getEncoded(); byte[] octetHash = hashBytesSha1(octetBytes); PdfName octetName = new PdfName(Utilities.convertToHex(octetHash)); return octetName; } static PdfName getCrlSignatureKey(byte[] crlBytes) throws NoSuchAlgorithmException, IOException, CRLException, CertificateException { CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509CRL crl = (X509CRL)cf.generateCRL(new ByteArrayInputStream(crlBytes)); byte[] signatureBytes = crl.getSignature(); DEROctetString octetString = new DEROctetString(signatureBytes); byte[] octetBytes = octetString.getEncoded(); byte[] octetHash = hashBytesSha1(octetBytes); PdfName octetName = new PdfName(Utilities.convertToHex(octetHash)); return octetName; } static X509Certificate getIssuerCertificate(X509Certificate certificate) throws IOException, StreamParsingException { String url = getCACURL(certificate); if (url != null && url.length() > 0) { HttpURLConnection con = (HttpURLConnection)new URL(url).openConnection(); if (con.getResponseCode() / 100 != 2) { throw new IOException(MessageLocalization.getComposedMessage("invalid.http.response.1", con.getResponseCode())); } InputStream inp = (InputStream) con.getContent(); byte[] buf = new byte[1024]; ByteArrayOutputStream bout = new ByteArrayOutputStream(); while (true) { int n = inp.read(buf, 0, buf.length); if (n <= 0) break; bout.write(buf, 0, n); } inp.close(); X509CertParser parser = new X509CertParser(); parser.engineInit(new ByteArrayInputStream(bout.toByteArray())); return (X509Certificate) parser.engineRead(); } return null; } static String getCACURL(X509Certificate certificate) { ASN1Primitive obj; try { obj = getExtensionValue(certificate, Extension.authorityInfoAccess.getId()); if (obj == null) { return null; } ASN1Sequence AccessDescriptions = (ASN1Sequence) obj; for (int i = 0; i < AccessDescriptions.size(); i++) { ASN1Sequence AccessDescription = (ASN1Sequence) AccessDescriptions.getObjectAt(i); if ( AccessDescription.size() != 2 ) { continue; } else if (AccessDescription.getObjectAt(0) instanceof ASN1ObjectIdentifier) { ASN1ObjectIdentifier id = (ASN1ObjectIdentifier)AccessDescription.getObjectAt(0); if ("1.3.6.1.5.5.7.48.2".equals(id.getId())) { ASN1Primitive description = (ASN1Primitive)AccessDescription.getObjectAt(1); String AccessLocation = getStringFromGeneralName(description); if (AccessLocation == null) { return "" ; } else { return AccessLocation ; } } } } } catch (IOException e) { return null; } return null; } static ASN1Primitive getExtensionValue(X509Certificate certificate, String oid) throws IOException { byte[] bytes = certificate.getExtensionValue(oid); if (bytes == null) { return null; } ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(bytes)); ASN1OctetString octs = (ASN1OctetString) aIn.readObject(); aIn = new ASN1InputStream(new ByteArrayInputStream(octs.getOctets())); return aIn.readObject(); } static String getStringFromGeneralName(ASN1Primitive names) throws IOException { ASN1TaggedObject taggedObject = (ASN1TaggedObject) names ; return new String(ASN1OctetString.getInstance(taggedObject, false).getOctets(), "ISO-8859-1"); } static byte[] hashBytesSha1(byte[] b) throws NoSuchAlgorithmException { MessageDigest sh = MessageDigest.getInstance("SHA1"); return sh.digest(b); } 

(如在MakeLtvEnabled中 )

它们尚未优化,当然可以使它们更高性能和更优雅。

添加LTV信息

基于这些添加和帮助,可以使用此方法makeLtvEnabled添加LTV启用签名所需的LTV信息:

 public void makeLtvEnabled(PdfStamper stp, OcspClient ocspClient, CrlClient crlClient) throws IOException, GeneralSecurityException, StreamParsingException, OperatorCreationException, OCSPException { stp.getWriter().addDeveloperExtension(new PdfDeveloperExtension(PdfName.ADBE, new PdfName("1.7"), 8)); LtvVerification v = stp.getLtvVerification(); AcroFields fields = stp.getAcroFields(); Map moreToCheck = new HashMap<>(); ArrayList names = fields.getSignatureNames(); for (String name : names) { PdfPKCS7 pdfPKCS7 = fields.verifySignature(name); List certificatesToCheck = new ArrayList<>(); certificatesToCheck.add(pdfPKCS7.getSigningCertificate()); while (!certificatesToCheck.isEmpty()) { X509Certificate certificate = certificatesToCheck.remove(0); addLtvForChain(certificate, ocspClient, crlClient, (ocsps, crls, certs) -> { try { v.addVerification(name, ocsps, crls, certs); } catch (IOException | GeneralSecurityException e) { e.printStackTrace(); } }, moreToCheck::put ); } } while (!moreToCheck.isEmpty()) { PdfName key = moreToCheck.keySet().iterator().next(); X509Certificate certificate = moreToCheck.remove(key); addLtvForChain(certificate, ocspClient, crlClient, (ocsps, crls, certs) -> { try { v.addVerification(key, ocsps, crls, certs); } catch (IOException | GeneralSecurityException e) { e.printStackTrace(); } }, moreToCheck::put ); } } void addLtvForChain(X509Certificate certificate, OcspClient ocspClient, CrlClient crlClient, VriAdder vriAdder, BiConsumer moreSignersAndCertificates) throws GeneralSecurityException, IOException, StreamParsingException, OperatorCreationException, OCSPException { List ocspResponses = new ArrayList<>(); List crls = new ArrayList<>(); List certs = new ArrayList<>(); while (certificate != null) { System.out.println(certificate.getSubjectX500Principal().getName()); X509Certificate issuer = getIssuerCertificate(certificate); certs.add(certificate.getEncoded()); byte[] ocspResponse = ocspClient.getEncoded(certificate, issuer, null); if (ocspResponse != null) { System.out.println(" with OCSP response"); ocspResponses.add(ocspResponse); X509Certificate ocspSigner = getOcspSignerCertificate(ocspResponse); if (ocspSigner != null) { System.out.printf(" signed by %s\n", ocspSigner.getSubjectX500Principal().getName()); } moreSignersAndCertificates.accept(getOcspSignatureKey(ocspResponse), ocspSigner); } else { Collection crl = crlClient.getEncoded(certificate, null); if (crl != null && !crl.isEmpty()) { System.out.printf(" with %s CRLs\n", crl.size()); crls.addAll(crl); for (byte[] crlBytes : crl) { moreSignersAndCertificates.accept(getCrlSignatureKey(crlBytes), null); } } } certificate = issuer; } vriAdder.accept(ocspResponses, crls, certs); } interface VriAdder { void accept(Collection ocsps, Collection crls, Collection certs); } 

( MakeLtvEnabled as makeLtvEnabledV2

用法示例

对于INPUT_PDF的签名PDF和结果输出流RESULT_STREAM您可以使用上面的方法,如下所示:

 PdfReader pdfReader = new PdfReader(INPUT_PDF); PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true); OcspClient ocsp = new OcspClientBouncyCastle(); CrlClient crl = new CrlClientOnline(); makeLtvEnabledV2(pdfStamper, ocsp, crl); pdfStamper.close(); 

( MakeLtvEnabled测试方法testV2

限制

上述方法仅在一些简化限制下工作,特别是:

  • 签名时间戳被忽略,
  • 检索到的CRL被认为是直接和完整的,
  • 假定使用AIA条目可以构建完整的证书链。

如果您无法接受这些限制,则可以相应地改进代码。

使用自己的实用程序类的方法

为了避免修补iText类,这种方法从上面的方法获取所需的代码,从iText的签名API LtvVerification类,并将所有代码合并到一个新的实用程序类中。 此类可以LTV启用文档而无需修补的iText版本。

AdobeLtvEnabling

此类将上面的代码和一些LtvVerification代码组合到LTV启用文档的实用程序类中。

不幸的是,在此处复制它会将消息大小超出堆栈溢出的30000字符限制。 您可以从github检索代码:

AdobeLtvEnabling.java

用法示例

对于INPUT_PDF的签名PDF和结果输出流RESULT_STREAM您可以使用上面的类,如下所示:

 PdfReader pdfReader = new PdfReader(INPUT_PDF); PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true); AdobeLtvEnabling adobeLtvEnabling = new AdobeLtvEnabling(pdfStamper); OcspClient ocsp = new OcspClientBouncyCastle(); CrlClient crl = new CrlClientOnline(); adobeLtvEnabling.enable(ocsp, crl); pdfStamper.close(); 

( MakeLtvEnabled测试方法testV3

限制

由于此实用程序类仅重新打包第一种方法的代码,因此适用相同的限制。

在幕后

正如一开始所提到的,所有Adobe都告诉我们“LTV启用”签名配置文件就是这样

启用LTV意味着validation文件所需的所有信息(减去根证书)都包含在其中

但他们并没有告诉我们他们希望信息是如何嵌入文件中的。

起初,我只收集了所有这些信息,并确保将其添加到PDF( CertsOCSPCRL )的适用文档安全存储区词典中。

但即使validation文件所需的所有信息(减去根证书)都包含在其中 ,Adobe Acrobat也不会将文件视为“启用LTV”。

然后我使用Adobe Acrobat启用了该文档并分析了差异。 事实certificate,以下额外数据也是必要的:

  1. 对于每个OCSP响应的签名,Adobe Acrobat需要存在相应的VRI字典。 在OP的示例PDF中,此VRI字典根本不需要包含任何证书,CRL或OCSP响应,但VRI字典需要存在。

    相反,这对于CRL的签名不是必需的。 这看起来有点武断。

    根据ISO 32000-2和ETSI EN 319 142-1的规范,这些VRI词典的使用纯粹是可选的 。 对于PAdES BASELINE签名,甚至建议不要使用VRI词典!

  2. Adobe Acrobat期望每个VRI词典都包含一个TU条目,记录相应VRI词典的创建时间。 (可能TS也会这样做,我没有测试过)。

    根据ISO 32000-2和ETSI EN 319 142-1的规范,这些TU条目的使用纯粹是可选的 。 对于PAdES签名,甚至建议不要使用TUTS条目!

因此,默认情况下,应用程序根据PDF规范添加的LTV信息不会导致Adobe Acrobat报告的“LTV启用”签名也就不足为奇了。

PS

显然,我必须在Adobe Acrobat中为某些证书添加信任,以使其考虑OP的文档“LTV enabled”的上述代码的结果。 我选择了根证书“CA RAIZ NACIONAL - COSTA RICA v2”。