签名后外部签名导致 pdf 损坏

external signature results in corrupted pdf after signing

我正在尝试从第 3 方签名提供商处签署 pdf 文档。 在创建空签名后,我向他们发送了文档哈希,他们发送了一个时间戳令牌(我们同意这将是一个时间戳签名),然后我将签名添加回 pdf。 api 获取 timestmp、crl 和 ocsp 的调用进行得很顺利,但是一旦我生成带有签名的 pdf,adobe 就会说签名无效并且错误是:

签名验证时出错。

签名包含不正确、无法识别、损坏或可疑的数据。 支持信息:SigDict /Contents 非法数据

这是当前代码:

signatureprovider.java

    public ByteArrayOutputStream sign(byte[] src, Rectangle rect, int signPage) throws Exception {
    log.info("I am in the signing method");
    ByteArrayOutputStream dest = new ByteArrayOutputStream();
    PdfDocumentHandler handler = new PdfDocumentHandler(new ByteArrayInputStream(src), dest);
    handler.prepareForSigning(rect, signPage);
    TrustedTimestampService service = new TrustedTimestampService();
    SignResponseWrapper response = new SignResponseWrapper();
    response = service.signHash(handler.getEncodedDocumentHash());
    String timestampToken = response.getSignResponse().getSignatureObject().getOther().getScSignatureObjects().getScExtendedSignatureObject().getTimestamp().getRFC3161TimeStampToken();
    List<String> encodedCrlEntries = new ArrayList<String>();
    encodedCrlEntries.add(response.getSignResponse().getOptionalOutputs().getScRevocationInformation().getScCRLs().getScCRL());
    List<String> encodedOcspEntries = new ArrayList<String>();
    encodedOcspEntries.add(response.getSignResponse().getOptionalOutputs().getScRevocationInformation().getScOCSPs().getScOCSP());
    byte[] signed = handler.createSignedPdf(Base64.getDecoder().decode(timestampToken), 15000, encodedCrlEntries, encodedOcspEntries);
    dest.flush();
    dest.write(signed);
    handler.close();
    return dest;
}

我会避免添加该服务,它只是发送哈希并获取令牌 pdfdocumenthandler.java

public PdfDocumentHandler(InputStream inputStream, ByteArrayOutputStream outputStream) { this.inputStream = 输入流; this.outputStream = 输出流;

}

public void prepareForSigning(Rectangle rect, int signPage)
    throws IOException, GeneralSecurityException {

    inMemoryStream = new ByteArrayOutputStream();
    inMemoryStream.write(StreamUtil.inputStreamToArray(inputStream));

    boolean hasSignature = hasDocumentSignature();
    pdfReader = new PdfReader(new ByteArrayInputStream(inMemoryStream.toByteArray()), new ReaderProperties());
    inMemoryStream.reset();
    pdfWriter = new PdfWriter(inMemoryStream, new WriterProperties().addXmpMetadata().setPdfVersion(PdfVersion.PDF_1_0));
    StampingProperties stampingProperties = new StampingProperties();
    pdfSigner = new PdfDocumentSigner(pdfReader, inMemoryStream, hasSignature ? stampingProperties.useAppendMode() : stampingProperties);

    pdfSigner.getSignatureAppearance()
        .setReason("myreason")
        .setLocation("mylocation")
        .setContact("mycontact")
        .setPageRect(rect)
        .setPageNumber(signPage)
        .setSignatureCreator("creator");

    pdfSigner.setFieldName("digitalSig");
    try {
        imgStorage = new ImageStorage();
        ImageData imgData = ImageDataFactory.create(Base64.getDecoder().decode(imgStorage.getimgData().getBytes()));
        pdfSigner.getSignatureAppearance().setRenderingMode(RenderingMode.GRAPHIC);
        pdfSigner.getSignatureAppearance().setSignatureGraphic(imgData);
    } catch(Exception e){
        pdfSigner.getSignatureAppearance().setRenderingMode(RenderingMode.DESCRIPTION);
    }
    
    Map<PdfName, PdfObject> signatureDictionary = new HashMap<>();
    signatureDictionary.put(PdfName.Filter, PdfName.Adobe_PPKLite);
    signatureDictionary.put(PdfName.SubFilter, PdfName.ETSI_RFC3161);

    Calendar signDate = Calendar.getInstance();
    pdfSigner.setSignDate(signDate);

    PdfHashSignatureContainer hashSignatureContainer = new PdfHashSignatureContainer("SHA-512", new PdfDictionary(signatureDictionary));
    documentHash = pdfSigner.computeHash(hashSignatureContainer, 15_000);
    log.info("prepared document - outputstream: " + Base64.getEncoder().encodeToString(outputStream.toByteArray()));
    log.info("prepared document - inmemorystream: " + Base64.getEncoder().encodeToString(inMemoryStream.toByteArray()));
    
}

public byte[] createSignedPdf(byte[] externalSignature, int estimatedSize, List<String> encodedCrlEntries,
                            List<String> encodedOcspEntries) {
    if (pdfSigner.getCertificationLevel() == PdfSigner.CERTIFIED_NO_CHANGES_ALLOWED) {
        throw new RuntimeException(String.format("Could not apply signature because source file contains a certification that does not allow "
                                                   + "any changes to the document with id %s"));
    }
    if (estimatedSize < externalSignature.length) {
        throw new RuntimeException(String.format("Not enough space for signature in the document. The estimated size needs to be " +
                                                   " %d bytes.", externalSignature.length));
    }

    try {
        pdfSigner.signWithAuthorizedSignature(new PdfSignatureContainer(externalSignature), estimatedSize);

        if (null != encodedCrlEntries || null!=encodedOcspEntries) {
            extendDocumentWithCrlOcspMetadata(encodedCrlEntries, encodedOcspEntries);
        } else {
            log.info("No CRL and OCSP entries were received to be embedded into the PDF");
            outputStream.write(inMemoryStream.toByteArray());
        }
        //outputStream.write(inMemoryStream.toByteArray());
        closeResource(inMemoryStream);
        closeResource(outputStream);
        log.info("complete document - outputstream: " + Base64.getEncoder().encodeToString(outputStream.toByteArray()));
        log.info("complete document - inmemorystream: " + Base64.getEncoder().encodeToString(inMemoryStream.toByteArray()));
    } catch (IOException | GeneralSecurityException e) {
        throw new RuntimeException(String.format("Failed to embed the signature in the document"), e);
    }
    return outputStream.toByteArray();
}

private void extendDocumentWithCrlOcspMetadata(List<String> encodedCrlEntries, List<String> encodedOcspEntries) {
    if (pdfSigner.getCertificationLevel() == PdfSigner.CERTIFIED_NO_CHANGES_ALLOWED) {
        throw new RuntimeException(String.format("Could not apply revocation information (LTV) to the DSS Dictionary. Document contains a " +
                                                   "certification that does not allow any changes"));
    }
    log.info("document without crl and ocsp - outputstream: " + Base64.getEncoder().encodeToString(outputStream.toByteArray()));
    log.info("document without crl and ocsp - inmemorystream: " + Base64.getEncoder().encodeToString(inMemoryStream.toByteArray()));
    List<byte[]> crl = mapEncodedEntries(encodedCrlEntries, this::mapEncodedCrl);
    List<byte[]> ocsp = mapEncodedEntries(encodedOcspEntries, this::mapEncodedOcsp);

    try (InputStream documentStream = new ByteArrayInputStream(inMemoryStream.toByteArray());
         PdfReader reader = new PdfReader(documentStream);
         PdfWriter writer = new PdfWriter(outputStream);
         PdfDocument pdfDocument = new PdfDocument(reader, writer, new StampingProperties().preserveEncryption().useAppendMode())) {
        LtvVerification validation = new LtvVerification(pdfDocument);
        List<String> signatureNames = new SignatureUtil(pdfDocument).getSignatureNames();
        String signatureName = signatureNames.get(signatureNames.size() - 1);
        boolean isSignatureVerificationAdded = validation.addVerification(signatureName, ocsp, crl, null);
        validation.merge();
        logSignatureVerificationInfo(isSignatureVerificationAdded);

    } catch (Exception e) {
        throw new RuntimeException(String.format("Failed to embed the signature(s) in the document(s) and close the streams"));
    }
    
}

public String getId() {
    return id;
}

public String getEncodedDocumentHash() {
    return Base64.getEncoder().encodeToString(documentHash);
}

private void logSignatureVerificationInfo(boolean isSignatureVerificationAdded) {
    if (isSignatureVerificationAdded) {
        log.info("Merged LTV validation information to the output stream");
    } else {
        log.warn("Failed to merge LTV validation information to the output stream");
    }
}

private List<byte[]> mapEncodedEntries(List<String> encodedEntries, Function<String, byte[]> mapperFunction) {
    return Objects.nonNull(encodedEntries)
           ? encodedEntries.stream().map(mapperFunction).collect(Collectors.toList())
           : Collections.emptyList();
}

private byte[] mapEncodedCrl(String encodedCrl) {
    try (InputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(encodedCrl))) {
        X509CRL x509crl = (X509CRL) CertificateFactory.getInstance("X.509").generateCRL(inputStream);
        logCrlInfo(x509crl);
        return x509crl.getEncoded();
    } catch (IOException | CertificateException | CRLException e) {
        throw new RuntimeException(String.format("Failed to map the received encoded CRL entry"), e);
    }
}

private byte[] mapEncodedOcsp(String encodedOcsp) {
    try (InputStream inputStream = new ByteArrayInputStream(Base64.getDecoder().decode(encodedOcsp))) {
        OCSPResp ocspResp = new OCSPResp(inputStream);
        BasicOCSPResp basicResp = (BasicOCSPResp) ocspResp.getResponseObject();
        logOcspInfo(ocspResp, basicResp);
        return basicResp.getEncoded();
    } catch (IOException | OCSPException e) {
        throw new RuntimeException(String.format("Failed to map the received encoded OCSP entry"), e);
    }
}

private void logCrlInfo(X509CRL x509crl) {
    int revokedCertificatesNo = Objects.isNull(x509crl.getRevokedCertificates()) ? 0 : x509crl.getRevokedCertificates().size();

    String message = "Embedding CRL response... ["
                     + "IssuerDN: " + x509crl.getIssuerX500Principal() + " "
                     + "This update: " + x509crl.getThisUpdate() + " "
                     + "Next update: " + x509crl.getNextUpdate() + " "
                     + "No. of revoked certificates: " + revokedCertificatesNo
                     + "]";
    log.info(message);
}

private void logOcspInfo(OCSPResp ocspResp, BasicOCSPResp basicResp) {
    SingleResp response = basicResp.getResponses()[0];
    BigInteger serialNumber = response.getCertID().getSerialNumber();
    X509CertificateHolder firstCertificate = basicResp.getCerts()[0];

    String message = "Embedding OCSP response... ["
                     + "Status: " + (ocspResp.getStatus() == 0 ? "OK" : "NOK") + " "
                     + "Produced at: " + basicResp.getProducedAt() + " "
                     + "This update: " + response.getThisUpdate() + " "
                     + "Next update: " + response.getNextUpdate() + " "
                     + "X509 cert issuer: " + firstCertificate.getIssuer() + " "
                     + "X509 cert subject: " + firstCertificate.getSubject() + " "
                     + "Certificate ID: " + serialNumber.toString() + "(" + serialNumber.toString(16).toUpperCase() + ")"
                     + "]";
    log.info(message);
}

private boolean hasDocumentSignature() throws IOException {
    try (ByteArrayInputStream is = new ByteArrayInputStream(inMemoryStream.toByteArray());
         PdfReader reader = new PdfReader(is, new ReaderProperties());
         PdfDocument pdfDocument = new PdfDocument(reader)) {
        SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
        return signatureUtil.getSignatureNames().size() > 0;
    }
}

pdfdocumentsigner.java

public class PdfDocumentSigner extends PdfSigner {

public PdfDocumentSigner(PdfReader reader, OutputStream outputStream, boolean properties) throws IOException {
    super(reader, outputStream, properties);
}

public byte[] computeHash(IExternalSignatureContainer externalHashContainer, int estimatedSize) throws GeneralSecurityException, IOException {
    if (closed) {
        throw new PdfException(PdfException.ThisInstanceOfPdfSignerAlreadyClosed);
    }

    PdfSignature signatureDictionary = new PdfSignature();
    PdfSignatureAppearance appearance = getSignatureAppearance();
    signatureDictionary.setReason(appearance.getReason());
    signatureDictionary.setLocation(appearance.getLocation());
    signatureDictionary.setSignatureCreator(appearance.getSignatureCreator());
    signatureDictionary.setContact(appearance.getContact());
    signatureDictionary.setDate(new PdfDate(getSignDate()));
    externalHashContainer.modifySigningDictionary(signatureDictionary.getPdfObject());
    cryptoDictionary = signatureDictionary;

    Map<PdfName, Integer> exc = new HashMap<>();
    exc.put(PdfName.Contents, estimatedSize * 2 + 2);
    preClose(exc);

    InputStream dataRangeStream = getRangeStream();
    return externalHashContainer.sign(dataRangeStream);
}

public void signWithAuthorizedSignature(IExternalSignatureContainer externalSignatureContainer, int estimatedSize)
    throws GeneralSecurityException, IOException {
    InputStream dataRangeStream = getRangeStream();
    byte[] authorizedSignature = externalSignatureContainer.sign(dataRangeStream);

    if (estimatedSize < authorizedSignature.length) {
        throw new IOException(String.format("Not enough space. The estimated signature size [%d bytes] is less than the received authorized "
                                            + "signature [%d bytes] which needs to be embedded into the document.", estimatedSize,
                                            authorizedSignature.length));
    }

    byte[] paddedSignature = new byte[estimatedSize];
    System.arraycopy(authorizedSignature, 0, paddedSignature, 0, authorizedSignature.length);

    PdfDictionary dic2 = new PdfDictionary();
    dic2.put(PdfName.Contents, new PdfString(paddedSignature).setHexWriting(true));
    close(dic2);

    closed = true;
}

我一直在寻找,似乎我没有遗漏任何东西,谁能帮我解决这个问题? 这里是最终的 pdf 示例

sample pdf

pdfWriter = new PdfWriter(inMemoryStream, new WriterProperties().addXmpMetadata().setPdfVersion(PdfVersion.PDF_1_0));

这就是问题所在,将其更改为

pdfWriter = new PdfWriter(inMemoryStream, new WriterProperties().addXmpMetadata().setPdfVersion(PdfVersion.PDF_1_3));

效果很好