【问题标题】:how can I add PAdES-LTV using iText如何使用 iText 添加 PAdES-LTV
【发布时间】:2018-07-16 22:39:30
【问题描述】:

我正在尝试在没有 LTV 格式的已签名 PDF 文档中启用 LTV。我在所有情况下都找到了相同的示例,如链接How to enable LTV for a timestamp signatureiText LTV enabled - how to add more CRLs? 中所述,它定义了获得预期结果的过程。碰巧我没有工作,它没有给我任何错误,但我没有添加 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<String> 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

【问题讨论】:

标签: java itext pades


【解决方案1】:

正如这条评论所证明的那样

我想要的是启用 Adob​​e LTV

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

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

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

(详情和背景请阅读this answer

因此,实现一种LTV 启用示例签名的方法涉及一些试验和错误,我不能保证 Adob​​e 会在即将到来的 Adob​​e Acrobat 版本中接受此代码的输出为“启用 LTV” .

此外,当前的 iText 5 签名 API 不足以完成任务,因为(事实证明)Adobe 需要 iText 代码不会创建的某些其他可选结构(但请参阅下面的 PPS)。解决这个问题最简单的方法是从两个方面更新 iText 类LtvVerification,所以我将在此处描述这种方式。或者,可以使用 Java 反射或复制和调整相当多的代码;如果您无法如下所示更新 iText,则必须选择一种这样的替代方法。

LTV 启用已签名 PDF 的签名

本部分显示了代码添加和更改,可以通过这些添加和更改来启用 LTV 启用文档,例如 OP 的示例 PDF sign_without_LTV.pdf

使用 iText 的 LtvVerification 类的方法

这是使用 iText 签名 API 中的 LtvVerification 类的原始代码。不幸的是,为此必须向该类添加一个功能。

打补丁LtvVerification

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

public boolean addVerification(PdfName signatureHash, Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> 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 词典中需要一个(根据规范可选的)时间条目(但请参阅下面的 PPS)。因此,我在outputDss 方法中添加了a 行,如下所示:

...
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<PdfName, X509Certificate> moreToCheck = new HashMap<>();

    ArrayList<String> names = fields.getSignatureNames();
    for (String name : names)
    {
        PdfPKCS7 pdfPKCS7 = fields.verifySignature(name);
        List<X509Certificate> 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<PdfName, X509Certificate> moreSignersAndCertificates) throws GeneralSecurityException, IOException, StreamParsingException, OperatorCreationException, OCSPException {
    List<byte[]> ocspResponses = new ArrayList<>();
    List<byte[]> crls = new ArrayList<>();
    List<byte[]> 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<byte[]> 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<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> 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 意味着验证文件所需的所有信息(减去根证书)都包含在

但他们没有告诉我们他们希望将信息嵌入文件中的准确程度。

起初我只是收集所有这些信息并确保将其添加到适用的 PDF 文档安全存储字典(CertsOCSPs CRL)。

但即使验证文件所需的所有信息(减去根证书)都包含在中,Adobe Acrobat 并不认为文件“已启用 LTV”。

然后我使用 Adob​​e Acrobat 启用了 LTV 文档并分析了差异。事实证明,以下额外数据也是必要的(但请参阅下面的 PPS):

  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 信息不会导致 Adob​​e Acrobat 报告的“启用 LTV”签名,这并不奇怪。

PS

显然,我必须在 Adob​​e Acrobat 中添加对某些证书的信任,才能让它完全考虑 OP 文档“启用 LTV”的上述代码的结果。我选择了根证书“CA RAIZ NACIONAL - COSTA RICA v2”。

PPS (2020-03-02)

同时,Adobe Acrobat 显然不再需要 VRI 字典(更不用说 TU 时间戳)在测试启用 LTV 时考虑 DSS 中的撤销信息状态,请参阅this answer 的“DSS 中的可选元素”部分。

因此,很可能上述解决方案可以稍微简化。

【讨论】:

  • 337/5000 非常感谢。我试图测试你的代码,但是在第二个方法 makeLtvEnabled 中,在 'v.addVerification (key, ocsps, crls, certs);我收到错误 Ltv Verification is not applicable for the arguments (PdfName, Collection , Collection , Collection ) 因为变量键不是字符串
  • 您似乎没有完成我在“修补 LtvVerification”部分中描述的操作 - 您缺少的方法是我添加的方法。我将尝试展示一个解决方案,而无需修补该类,而是使用反射。
  • @Gaara 我在我的问题中添加了“使用自己的实用程序类的方法”部分,它提供了一个单独的实用程序类,您可以使用它启用 LTV,而无需修补 iText。跨度>
  • @Gaara 太棒了! (我不能 100% 确定,因为我不知道您在 Adob​​e Reader 中使用哪些哥斯达黎加证书作为信任锚,所以这可能仍然存在相关差异。)
  • 如果我理解,有效的信任证书是您使用的“CA RAIZ NACIONAL - COSTA RICA v2”。现在,在此之后并对我所做的代码进行了一些调整,我还尝试添加一个时间戳(我还没有实现),但是代码已经完美地添加了 LTV。再次非常感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-03
  • 2015-10-29
  • 2023-03-26
相关资源
最近更新 更多