PDFBox 的签名无效

Signature is invalid with PDFBox

提问人:Qazazazaz 提问时间:10/28/2023 最后编辑:Qazazazaz 更新时间:10/31/2023 访问量:108

问:

我有两个来自不同 TSP(比如 CredA 和 CredB)的基于 CSC 的凭据。我正在尝试使用这两个凭据执行 PDF 签名。 我以两种方式实现了相同的方法,一种是用的,另一种是用.PDFBoxItext

CredA 适用于以下实现,但实施失败。PDFBoxItext


    public static void testSign(InputStream is, PDDocument document, Certificate[] certificateChain, String accessToken) throws Exception {
        try (
                OutputStream output = new FileOutputStream(new File("/[filepath]/", "Signed.pdf"));
        ) {
            PDSignature signature = new PDSignature();

            signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
            signature.setSubFilter(PDSignature.SUBFILTER_ETSI_CADES_DETACHED);
            signature.setName("Test Name");
            signature.setSignDate(Calendar.getInstance());

            SignatureOptions signatureOptions = new SignatureOptions();
            signatureOptions.setPage(0);

            document.addSignature(signature, signatureOptions);
            ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(output);

            X509Certificate cert = (X509Certificate) certificateChain[0];

            ESSCertIDv2 certid = new ESSCertIDv2(
                    new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
                    MessageDigest.getInstance("SHA-256").digest(cert.getEncoded())
            );
            SigningCertificateV2 sigcert = new SigningCertificateV2(certid);
            Attribute attr = new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificateV2, new DERSet(sigcert));

            ASN1EncodableVector v = new ASN1EncodableVector();
            v.add(attr);
            AttributeTable atttributeTable = new AttributeTable(v);
            CMSAttributeTableGenerator attrGen = new DefaultSignedAttributeTableGenerator(atttributeTable);

            org.bouncycastle.asn1.x509.Certificate cert2 = org.bouncycastle.asn1.x509.Certificate.getInstance(ASN1Primitive.fromByteArray(cert.getEncoded()));
            JcaSignerInfoGeneratorBuilder sigb = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build());
            sigb.setSignedAttributeGenerator(attrGen);

            ContentSigner contentSigner = new ContentSigner() {
                private MessageDigest digest = MessageDigest.getInstance("SHA-256");
                private OutputStream stream = OutputStreamFactory.createStream(digest);
                @Override
                public byte[] getSignature() {
                    try {

                        byte[] b = new byte[4096];
                        int count;

                        while ((count = is.read(b)) > 0) {
                            digest.update(b, 0, count);
                        }
                        byte[] hashBytes = digest.digest();

                        java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
                        List<String> hash = Arrays.asList(encoder.encodeToString(hashBytes));
                        byte[] signedHash = signHash(accessToken, hash);
                        return signedHash;
                    } catch (Exception e) {
                        throw new RuntimeException("Exception while signing", e);
                    }
                }

                @Override
                public OutputStream getOutputStream() {
                    return stream;
                }

                @Override
                public AlgorithmIdentifier getAlgorithmIdentifier() {
                    return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
                }
            };

            CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
            gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
            gen.addSignerInfoGenerator(sigb.build(contentSigner, new X509CertificateHolder(cert2)));


            final CMSProcessableByteArray content = new CMSProcessableByteArray(
                    IOUtils.toByteArray(externalSigning.getContent()));
            CMSSignedData signedData = gen.generate(content, false);

            byte[] cmsSignature = signedData.getEncoded();
            externalSigning.setSignature(cmsSignature);
        }
    }

    private static byte[] signHash(String accessToken, List hash) throws Exception {

        String SAD = "";
        JSONParser parser = new JSONParser();
        //CredentialsAuthorize API
        JSONObject requestHeaderCredentialsAuthorize = new JSONObject();
        requestHeaderCredentialsAuthorize.put("Content-type", "application/json");
        requestHeaderCredentialsAuthorize.put("Authorization", "Bearer " + accessToken);

        JSONObject requestPayloadCredentialsAuthorize = new JSONObject();
        requestPayloadCredentialsAuthorize.put("credentialID", keyID);
        requestPayloadCredentialsAuthorize.put("numSignatures", "1");
        requestPayloadCredentialsAuthorize.put("PIN", SecretPIN);

        JSONArray hashArray = new JSONArray();
        hashArray.add(hash.get(0));
        requestPayloadCredentialsAuthorize.put("hash", hashArray);

        JSONObject credentialsAuthorizeResponse = PosttoHttpURLConnection.getResponseHTTP(CSC_CredentialsAuthorize_URL, requestPayloadCredentialsAuthorize.toString(), requestHeaderCredentialsAuthorize);
        if (credentialsAuthorizeResponse != null) {
            JSONObject credentialsAuthorizeResponseObject = (JSONObject) parser.parse(credentialsAuthorizeResponse.get("Response").toString());
            if (credentialsAuthorizeResponse.get("StatusCode").toString().equals("200")) {
                SAD = credentialsAuthorizeResponseObject.get("SAD") == null ? "" : credentialsAuthorizeResponseObject.get("SAD").toString();
                String expires_in = credentialsAuthorizeResponseObject.get("expires_in") == null ? "" : credentialsAuthorizeResponseObject.get("expires_in").toString();
            } else {
                return null;
            }
        }

        String keyAlgorithm = "SHA256withRSA";
        //signHash API
        JSONObject requestHeadersignHash = new JSONObject();
        requestHeadersignHash.put("Content-type", "application/json");
        requestHeadersignHash.put("Authorization", "Bearer " + accessToken);

        JSONObject requestPayloadsignHash = new JSONObject();
        requestPayloadsignHash.put("credentialID", keyID);
        requestPayloadsignHash.put("SAD", SAD);
        requestPayloadsignHash.put("hashAlgo", "2.16.840.1.101.3.4.2.1");
        requestPayloadsignHash.put("signAlgo", "1.2.840.113549.1.1.1");
        requestPayloadsignHash.put("signAlgoParams", keyAlgorithm);
        JSONArray signHashArray = new JSONArray();
        signHashArray.add(hash.get(0));
        requestPayloadsignHash.put("hash", signHashArray);

        JSONObject signHashResponse = PosttoHttpURLConnection.getResponseHTTP(CSC_signHash_URL, requestPayloadsignHash.toString(), requestHeadersignHash);
        if (signHashResponse != null) {
            JSONObject signHashResponseObject = (JSONObject) parser.parse(signHashResponse.get("Response").toString());
            if (signHashResponse.get("StatusCode").toString().equals("200")) {
                JSONArray signedHashArray = (JSONArray) parser.parse(signHashResponseObject.get("signatures").toString());
                String rawSignature = signedHashArray.get(0).toString();
                byte[] signatureBytes = org.bouncycastle.util.encoders.Base64.decode(rawSignature);
                return signatureBytes;
            }
        }
                return null;
    }

CredB 适用于以下实现,但使用上述实现失败。ITextPDFBox

            String access_token = getAccessTokenAPI();
            JSONParser parser = new JSONParser();


            String CredentialsInfoAlgoValue = "1.2.840.113549.1.1.1";
            JSONArray certificates = getCredentialsInfoAPI();

            //Hash Doc
            int contentEstimated = 32768;
            PdfReader readerpdf = new PdfReader(filePath);
            ByteArrayOutputStream fout = new ByteArrayOutputStream();
            PdfStamper stamperpdf = PdfStamper.createSignature(readerpdf, fout, '\0', null, true);
            PdfSignatureAppearance appearance = stamperpdf.getSignatureAppearance();
            appearance.setReason("Demo");
            appearance.setLocation("10.80.100.46");
            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.MINUTE, 5);
            appearance.setSignDate(cal);
            appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
            appearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
            appearance.setImage(null);

            appearance.setAcro6Layers(false);
            String[] str = "285,300,415,370".split(",");
            Float[] intarray = new Float[str.length];
            int i = 0;
            for (String strs : str) {
                intarray[i] = Float.parseFloat(strs.trim());
                i++;
            }

            int[] pages = {Integer.parseInt("1")};
            List<Rectangle> listRectangle = new ArrayList<>();
            listRectangle.add(new Rectangle(intarray[0], intarray[1], intarray[2], intarray[3]));
            appearance.setVisibleSignature(new Rectangle(intarray[0], intarray[1], intarray[2], intarray[3]), pages[0], null);
            HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
            exc.put(PdfName.CONTENTS, new Integer(contentEstimated * 2 + 2));
            PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
            dic.setReason(appearance.getReason());
            dic.setLocation(appearance.getLocation());
            dic.setContact(appearance.getContact());
            Calendar calDate = Calendar.getInstance();
            dic.setDate(new PdfDate(calDate));
            appearance.setCryptoDictionary(dic);
            appearance.setLayer2Text("Signed by: " + "Demo User" + " \nReason: Demo");
            appearance.setLayer2Font(new Font(Font.FontFamily.HELVETICA, 6, Font.NORMAL, BaseColor.BLACK));
            appearance.preClose(exc);

            Certificate[] chain = new Certificate[certificates.size()];
            int c = 1;
            for (int k = 0; k < certificates.size(); k++) {
                X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(org.bouncycastle.util.encoders.Base64.decode(certificates.get(k).toString().getBytes())));
                String subject = getSubjectType(cert);
                if (subject.equalsIgnoreCase("EndEntity")) {
                    chain[0] = cert;
                } else {
                    chain[c++] = cert;
                }
            }

            ExternalDigest externalDigest = new ExternalDigest() {
                public MessageDigest getMessageDigest(String hashAlgorithm)
                        throws GeneralSecurityException {
                    return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
                }
            };
            PdfPKCS7 sgn = null;
            byte hash[] = null;
            byte ocsp[] = null;

            InputStream data = appearance.getRangeStream();;
            if (CredentialsInfoAlgoValue.equals("1.2.840.10045.4.3.3")) {
                sgn = new PdfPKCS7(null, chain, "SHA384", null, externalDigest, false);
                hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA384"));
            } else if (CredentialsInfoAlgoValue.equals("1.2.840.10045.4.3.4")) {
                sgn = new PdfPKCS7(null, chain, "SHA512", null, externalDigest, false);
                hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA512"));
            } else {
                sgn = new PdfPKCS7(null, chain, "SHA256", null, externalDigest, false);
                hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));
            }

            byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, calDate, ocsp, null, MakeSignature.CryptoStandard.CMS);

            String stringDocumentHash = new String(org.bouncycastle.util.encoders.Base64.encode(sh));
            String SAD = getCredentialsAuthoriseAPI();
            
            String rawSignature = getSignHashAPI();
            byte[] signatureBytes = org.bouncycastle.util.encoders.Base64.decode(rawSignature);
            ByteArrayOutputStream os = fout;
            sgn.setExternalDigest(signatureBytes, null, "RSA");
            Collection<byte[]> crlBytes = null;
            TSAClient tsaClient = null;
            byte[] pkcs7 = sgn.getEncodedPKCS7(hash, calDate, tsaClient, ocsp, crlBytes, MakeSignature.CryptoStandard.CMS);
            byte[] paddedSig = new byte[32768];
            System.arraycopy(pkcs7, 0, paddedSig, 0, pkcs7.length);
            PdfDictionary dic2 = new PdfDictionary();
            dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));
            appearance.close(dic2);

            System.out.println("Signed PDF Document saved at: " + filePath + "_signed.pdf");
            String outputFile1 = filePath+"_signed.pdf";
            Path signedFile = Paths.get(outputFile1);
            Files.write(signedFile, os.toByteArray());
            return;

谁能帮我确定两种实现之间的区别(除了外观)。我想使用实现同时运行 CredA 和 CredB,但 CredB 仅适用于错误,并且失败并出现错误。PDFBoxITextPDFBoxInvalid Signature(Document has been altered or corrupted since it was signed)

Java PDFBOX 数字签名

评论

0赞 Qazazazaz 10/28/2023
@mkl 请提出建议。
1赞 mkl 10/28/2023
您能否分享一个由您的代码签名的示例 PDF?乍一看,pdfbox签名代码看起来没问题,因此问题很可能是一个小细节。分析已签名的 PDF 有助于识别该细节。
0赞 Qazazazaz 10/29/2023
@mkl 感谢您的回复。我发现 PDFBox 示例适用于其他一些凭据。我已经更新了问题。你能现在检查一下吗?我希望我的两个凭据都可以与 PDFBox 一起使用。一个与 IText 一起使用,另一个与 PDFBox 一起使用。
0赞 mkl 10/29/2023
如果它适用于某些凭据而不适用于其他凭据,则分析已签名的文档就更为重要。

答:

1赞 mkl 10/30/2023 #1

基于 iText 的代码和基于 PDFBox 的代码之间的区别在于

  • 在前一种情况 (iText) 中,您发送实际的经过身份验证的属性进行签名(您调用 Carrier 对象,但它实际上是一个包含经过身份验证的属性的字符串,文档哈希只是这些属性之一),但是stringDocumentHash

  • 在后一种情况下 (PDFBox),您发送数据的哈希值进行签名。(在本例中,代码中不清楚该数据是什么。InputStream is

因此,显然 CredB TSP(与 iText 一起使用)希望您发送实际字节进行签名并自行计算其哈希值,而 CredA TSP(与 PDFBox 一起使用)希望您发送要签名的字节的哈希值并按原样使用该值。

要使您的 PDFBox 代码适用于两个 TSP,您必须将代码更改为仅选择性地对经过身份验证的字节进行哈希处理(并根据使用的 TSP 请求哈希),或者您必须编排您的 CredB 访问,以期望原始数据签名,而不是其哈希。


不幸的是,当将代码复制到您的问题中时,两个版本都出现了一些错误,导致它们无法编译。因此,上述分析只是松散地基于该代码。相反,它主要基于您提供的示例文件。


顺便说一句,签名者证书的主题是 和 条目开头的空白问题.........,PostalCode=\ xxxx,ST=\ xxxxx,...PostalCodeST


PS:请尝试以下操作,而不是代码中的代码,以便使用CredB和PDFBox对运行进行签名:ContentSigner

ContentSigner contentSigner = new ContentSigner() {
    private ByteArrayOutputStream stream = new ByteArrayOutputStream();

    @Override
    public byte[] getSignature() {
        try {
            java.util.Base64.Encoder encoder = java.util.Base64.getEncoder();
            List<String> hash = Arrays.asList(encoder.encodeToString(stream.toByteArray()));
            byte[] signedHash = signHash(accessToken, hash);
            return signedHash;
        } catch (Exception e) {
            throw new RuntimeException("Exception while signing", e);
        }
    }

    @Override
    public OutputStream getOutputStream() {
        return stream;
    }

    @Override
    public AlgorithmIdentifier getAlgorithmIdentifier() {
        return new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.11"));
    }
}

由于我既没有您的代码的可运行版本,也没有 CredB 凭据(或等效凭据),因此我无法测试自己。

评论

0赞 Qazazazaz 10/31/2023
感谢@mkl的详细分析。“Inputstream is”仅是文档流。但是,作为调试的一部分,我已经尝试用 IText one 替换 PDFBox 方法的哈希生成部分。我添加了使用 ExternalDigest 生成 signedAttributes 的代码,以代替在 PDFBox 实现中生成哈希摘要。它仍然失败,并出现相同的错误。
0赞 mkl 10/31/2023
“Inputstream is”仅是文档流。- 你在这里不需要那个流。实际上,我假设您之前将该流读到最后,因此它在这里的行为就像一个空流。
0赞 mkl 10/31/2023
“请使用建议的修复程序检查更新的文件” - 我无法识别此处计算的哈希值。我将在我的答案中添加一个替代方案,供您尝试使用 PDFBox 和 CredB。ContentSigner
0赞 Qazazazaz 10/31/2023
谢谢@mkl。添加后请告诉我。期待从您那里了解更多信息。
1赞 mkl 11/7/2023
您使用的证书由某个测试 CA 颁发,该 CA 既不在 AATL 上,也不在 EUTL 上。因此,Adobe Acrobat将始终显示“无法识别作者”。除非您将 Adobe Acrobat 配置为信任来自该证书链的证书,否则无论是显式(在 Adobe Acrobat 设置中)还是隐式(在操作系统设置中)。显式配置该信任时,可以选择该信任是仅用于常规签名还是也用于证书签名。同样,您可以配置受信任的操作系统证书。