chiao kang
chiao kang

Reputation: 73

Using iText (signdeferred) to create PDF digital signature, invalid signature problem appears when verifying signature

I am a Chinese software developer, I am now implementing such a function, using Android client to digitally sign PDF, my implementation is like this

  1. Create a blank signature on the server
  2. Send PDF hash with blank signature to Android client, and Android client signs hash
  3. Use makesignature. Signdeferred () to merge the signature content in the server Now I encounter such a problem that the PDF after signing cannot be verified by the PDF reader. It shows that the PDF file has been tampered, It should be noted that I use sm3withsm2 algorithm. Adobe reader can't verify it. We have our own reader

https://drive.google.com/file/d/127nVvJ0qtSdG53jM0_GUP-WORYrQ5TBo/view?usp=sharing Now I add the PDF file address, who can help me analyze the problem

 /**
 * @return name.ldd.electSign.webservice.pdf.presign.PresignResponseDetail
 * @Author 焦康
 * @Description 预签章
 * @Date 2021/4/25 14:35
 * @Param [pdfReader, tempFile, temp, chain, positions, fieldName, calendar, hashName]
 **/
public PreSignResponseDetail preSign(PreSignDAO preSignDAO) throws IOException, DocumentException, GeneralSecurityException {
    log.debug("执行预签章");
    FileOutputStream fileOutputStream = new FileOutputStream(preSignDAO.getTempPath());
    // 设置印章信息
    preSignDAO.getPdfReader().setAppendable(true);
    PdfStamper pdfStamper = PdfStamper.createSignature(preSignDAO.getPdfReader(), fileOutputStream, '\0', null, true);
    pdfStamper.getWriter().setCompressionLevel(PdfStream.BEST_COMPRESSION);
    PdfSignatureAppearance pdfSignatureAppearance = pdfStamper.getSignatureAppearance();
    pdfSignatureAppearance.setReason("");
    pdfSignatureAppearance.setLocation("");
    pdfSignatureAppearance.setContact("");
    pdfSignatureAppearance.setLayer2Text("");
    pdfSignatureAppearance.setLayer4Text("");
    pdfSignatureAppearance.setSignatureCreator("");
    pdfSignatureAppearance.setOpacity(preSignDAO.getSealOpacity());
    pdfSignatureAppearance.setCertificationLevel(PdfSignatureAppearance.NOT_CERTIFIED);
    pdfSignatureAppearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION);
    pdfSignatureAppearance.setCertificate(preSignDAO.getCertificateChain()[0]);
    pdfSignatureAppearance.setVisibleSignature(new Rectangle(200, 200, 300, 300), preSignDAO.getPage(), preSignDAO.getFieldName());
    // 设置签名图片
    //读取图章图片,这个image是itext包的image
    Image image = Image.getInstance(preSignDAO.getSealImagePath());
    pdfSignatureAppearance.setImage(image);
    // 开始预签章
    ExternalSignatureContainer externalBlankSignatureContainer = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    MakeSignature.signExternalContainer(pdfSignatureAppearance, externalBlankSignatureContainer, 8192);
    // 计算hash
    InputStream inputStream = pdfSignatureAppearance.getRangeStream();
    BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
    byte[] hash = DigestAlgorithms.digest(inputStream, bouncyCastleDigest.getMessageDigest(preSignDAO.getHashAlgorithm()));
    PdfPKCS7 pkcs7 = new PdfPKCS7(null, preSignDAO.getCertificateChain(), preSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
    byte[] sh = pkcs7.getAuthenticatedAttributeBytes(hash, Calendar.getInstance(), null, null, MakeSignature.CryptoStandard.CMS);
    String stringSh = new sun.misc.BASE64Encoder().encode(sh);
    return new PreSignResponseDetail(preSignDAO.getSourcePath(), preSignDAO.getTempPath(), hash, stringSh.replace("\r\n",""), preSignDAO.getFieldName());

}
/**
     * @Author 焦康
     * @Description 数据签名
     * @Date 2021/4/30 14:57
     * @Param [Pin, CN, signData, listener]
     * @return void
     **/
    public void ZAYK_SignData(final String Pin,final String CN,final String signData,HttpsCallbackListener listener){
        httpsCallbackListener = listener;
        new Thread(new Runnable() {
            @Override
            public void run() {
                String result = null;
                try {
                    byte[] bytes = Base64.decode(signData,Base64.DEFAULT);
                    result = service.SDZM_SignData(bytes,CN,Pin);
                    JSONObject jsonObject = JSONObject.parseObject(result);
                    result = jsonObject.getString("signValue");
                    Log.e("SDZM_SignData",result);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                sendMessage(result);
            }
        }).start();
    }
/**
     * @return
     * @Author 焦康
     * @Description 延迟签章
     * @Date 2021/4/25 17:01
     * @Param
     **/
    public DeferredSignResponseDetail DeferredSign(DeferredSignDAO deferredSignDAO) {
        log.debug("执行延迟签章");
        BouncyCastleDigest bouncyCastleDigest = new BouncyCastleDigest();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        PdfReader pdfReader = null;
        FileOutputStream os = null;
        try {
            PdfPKCS7 sgn = new PdfPKCS7(null, deferredSignDAO.getCertificateChain(), deferredSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
            sgn.setExternalDigest(deferredSignDAO.getSignData(), null, deferredSignDAO.getDigestEncryptionAlgorithm());
            byte[] sig = sgn.getEncodedPKCS7(deferredSignDAO.getHashData(), calendar, null, null, null, MakeSignature.CryptoStandard.CMS);
            pdfReader = new PdfReader(deferredSignDAO.getTempPath());
            os = new FileOutputStream("D:\\ruoyi\\uploadPath\\pdf\\Dest\\输出");
            ExternalSignatureContainer external = new MyExternalSignatureContainer(sig);
            MakeSignature.signDeferred(pdfReader, deferredSignDAO.getFieldName(), os, external);
        } catch (Exception e) {
            log.error("延迟签章出错:", e);
        } finally {
            if (pdfReader != null) {
                pdfReader.close();
            }
            try {
                if (os != null) {
                    os.close();
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return new DeferredSignResponseDetail(deferredSignDAO.getTargetPath() + ".pdf");
    }
public class MyExternalSignatureContainer implements ExternalSignatureContainer {
    byte[] sig = null;
    
    public MyExternalSignatureContainer(byte[] sig) {
        this.sig = sig;
    }

    @Override
    public byte[] sign(InputStream paramInputStream) throws GeneralSecurityException {
        return this.sig;
    }

    @Override
    public void modifySigningDictionary(PdfDictionary paramPdfDictionary) {
    }
}

Upvotes: 3

Views: 626

Answers (1)

mkl
mkl

Reputation: 96029

When calculating the hash of the to-be-signed attributes, you use the then current time as value of the signing time attribute:

PdfPKCS7 pkcs7 = new PdfPKCS7(null, preSignDAO.getCertificateChain(), preSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
byte[] sh = pkcs7.getAuthenticatedAttributeBytes(hash, Calendar.getInstance(), null, null, MakeSignature.CryptoStandard.CMS);
String stringSh = new sun.misc.BASE64Encoder().encode(sh);

Later, when you have received a naked signature for that hash, you re-build the signed attributes using the then current time:

Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
...
PdfPKCS7 sgn = new PdfPKCS7(null, deferredSignDAO.getCertificateChain(), deferredSignDAO.getHashAlgorithm(), null, bouncyCastleDigest, false);
sgn.setExternalDigest(deferredSignDAO.getSignData(), null, deferredSignDAO.getDigestEncryptionAlgorithm());
byte[] sig = sgn.getEncodedPKCS7(deferredSignDAO.getHashData(), calendar, null, null, null, MakeSignature.CryptoStandard.CMS);

Thus, the signing time in the signed attributes of the final CMS container differs from the signing time used for hashing, so the signature signs the wrong hash.

To fix this use the identical signing time value in both places!


By the way, if you had used the current iText 5 version, you would not have had that problem: Because that signing time attribute is optional (as long as one sets the signing time in the signature dictionary) and even forbidden for PAdES signatures, the current iText 5 version does not set that attribute anymore and getAuthenticatedAttributeBytes and getEncodedPKCS7 don't have a Calendar parameter anymore...


As I have not yet dealt with the Chinese algorithms, I cannot tell whether they are applied correctly in your code or in your example file.

One warning, though: PdfPKCS7 has been implemented with DSA and RSA in mind; X9.62 ECDSA support has been added later, and that with an error (which still is present in iText 7) which Adobe Acrobat ignores but not necessarily other validators; before using PdfPKCS7 for any other algorithms, therefore, you have to verify that that class handles those other algorithms properly, and improve the class if it does not.

Upvotes: 3

Related Questions