Java 通过 MakeSignature.signDeferred 进行外部签名时 iText 5 签名无效

Java iText 5 invalid signature when external signing via MakeSignature.signDeferred

我正在使用 iText 5 Java 进行外部签名。首先我创建签名外观,计算签名属性的哈希值并为签名留空。稍后当我从客户端获得签名的哈希值时,我通过 MakeSignature.signDeferred.

将其插入 PDF

但是 PDF reader 显示签名无效。抱怨PDF已被修改。

这是用于签名的代码。我已经删除了很多功能代码以保持代码的基本要素。

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import java.io.*;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Calendar;
import java.util.GregorianCalendar;

public class MklTest {
    static String thisHash;

    static class MyExternalSignatureContainer implements ExternalSignatureContainer {
        protected byte[] sig;
        public MyExternalSignatureContainer(byte[] sig) {
            this.sig = sig;
        }
        public byte[] sign(InputStream is) {
            return sig;
        }

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

    static class EmptyContainer implements ExternalSignatureContainer {
        public EmptyContainer() {
        }
        public byte[] sign(InputStream is) {
            ExternalDigest digest = hashAlgorithm1 -> DigestAlgorithms.getMessageDigest(hashAlgorithm1, null);
            try {
                byte[] hash = DigestAlgorithms.digest(is, digest.getMessageDigest("SHA256"));

                thisHash = Hex.encodeHexString(hash);

                return new byte[0];
            } catch (IOException | GeneralSecurityException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void modifySigningDictionary(PdfDictionary pdfDictionary) {
            pdfDictionary.put(PdfName.FILTER, PdfName.ADOBE_PPKMS);
            pdfDictionary.put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
        }
    }

    public static String emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        PdfStamper stamper = PdfStamper.createSignature(reader, os, '[=12=]');

        Calendar cal = GregorianCalendar.getInstance();
        cal.add(Calendar.MINUTE, 10);

        PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
        appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
        appearance.setCertificate(chain[0]);
        appearance.setReason("Nice");
        appearance.setLocation("Delhi");
        appearance.setSignDate(cal);

        ExternalSignatureContainer external = new EmptyContainer();
        MakeSignature.signExternalContainer(appearance, external, 8192);

        os.close();
        reader.close();

        return thisHash;
    }

    public static Certificate getCert() throws CertificateException {
        String cert = ""; // the cert we get from client
        ByteArrayInputStream userCertificate = new ByteArrayInputStream(Base64.decodeBase64(cert));

        CertificateFactory cf = CertificateFactory.getInstance("X.509");

        return cf.generateCertificate(userCertificate);
    }

    private static ExternalDigest getDigest() {
       return new ExternalDigest() {
            public MessageDigest getMessageDigest(String hashAlgorithm)
                    throws GeneralSecurityException {
                return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
            }
        };
    }

    public static void createSignature(String src, String dest, String fieldname, byte[] signature) throws IOException, DocumentException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        ExternalSignatureContainer external = new MyExternalSignatureContainer(signature);
        MakeSignature.signDeferred(reader, fieldname, os, external);

        reader.close();
        os.close();
    }

    public static void main(String[] args) throws Exception {
        Certificate cert = getCert();
        Certificate[] chain = {cert};

        String src = "/home/spooderman/Downloads/sample.pdf";
        String between = "/tmp/sample_out_between.pdf";
        String dest = "/tmp/sample_out.pdf";
        String fieldName = "sign";

        String hash = emptySignature(src, between, fieldName, chain);

        String signature = "";  // signed hash signature we get from client
        byte[] signatureBytes = Hex.decodeHex(signature.toCharArray());

        PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", null, getDigest(), false);
        sgn.setExternalDigest(signatureBytes, null, "RSA");

       byte[] data = sgn.getEncodedPKCS7(Hex.decodeHex(hash.toCharArray()),null, null, null, MakeSignature.CryptoStandard.CMS);

        createSignature(between, dest, fieldName, data);
    }
}

这是original PDF

这是PDF with empty signature

这是PDF with final signature

PDF 的计算哈希。

954927c9286320e904920b0bf12f7cad387c1a9afd5a92314960a1083593f7dc

这是我从客户端收到的已签名哈希签名。

6c14b965c7e90c3134653a9261b0666dce7a7e28cb605fc3152ad111fa7915a77396799357daf1d37c52163ce6d34bfd96ee743e721b45e929f6d8aced144f094d03dce00f25c6c1fc5aa63c92322780f7de675c194ef17303a643055dbbedfec9d5200994fcdfc3ad9488d568ad3f6cd2d262e360a79ad90b5ffb188723de559f3696dcb223930f842172e4838f7d5e6a44494ced54bca54ed12133ea189d616a10039a222ce61885ad98b8ba0bd83d63b887e2c188ca10bd2f53f92f08c5585b9826553280c19976a0ba29f7789ad6a80010b4a6431d3b6bb8f27999b23d3739de03db6db8ab46acaf38b33bd37a74465744c3f95a093deff26cb44b45e27e

我已经尝试了很多在 Whosebug 上找到的东西,但问题仍然存在。任何正确方向的帮助将不胜感激。

对于客户端部分,我正在使用 Fortify 这使得本地 USB 令牌和 HSM 模块可用于 JS 客户端。

问题是 EmptyContainer.sign 方法只给出 PDF 字节而不是经过身份验证的属性。经过身份验证的属性实际上是需要签名的。感谢 mkl 指出正确的方向。

我修改了 EmptyContainer.sign 方法以创建一个 PdfPKCS7 对象并使用 PDF 散列作为参数之一调用 PdfPKCS7.getAuthenticatedAttributeBytes

getAuthenticatedAttributeBytes() 方法返回的字节进行签名并创建签名字节和原始哈希的 CMS 容器后,我能够成功对 PDF 进行签名。

如果有人需要,这里是代码。到处都是乱七八糟的,但你会得到精华。

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;

import javax.annotation.Nullable;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.GregorianCalendar;

public class MklTest {
    static Certificate[] chain;
    static byte[] toSign;
    static byte[] hash;

    static class MyExternalSignatureContainer implements ExternalSignatureContainer {
        protected byte[] sig;
        public MyExternalSignatureContainer(byte[] sig) {
            this.sig = sig;
        }
        public byte[] sign(InputStream is) {
            return sig;
        }

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

    static class EmptyContainer implements ExternalSignatureContainer {
        public EmptyContainer() {
        }
        public byte[] sign(InputStream is) {
            try {
                ExternalDigest digest = getDigest();
                String hashAlgorithm = getHashAlgorithm();

                Certificate[] certs = {null};

                hash = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
                    PdfPKCS7 sgn = getPkcs(certs);

                toSign = sgn.getAuthenticatedAttributeBytes(hash, getOscp(), null,
                        MakeSignature.CryptoStandard.CMS);

                return new byte[0];
            } catch (IOException | GeneralSecurityException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void modifySigningDictionary(PdfDictionary pdfDictionary) {
            pdfDictionary.put(PdfName.FILTER, PdfName.ADOBE_PPKMS);
            pdfDictionary.put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
        }
    }

    public static String getHashAlgorithm() {
        return "SHA256";
    }

    public static byte[] getOscp() {
        byte[] ocsp = null;
        OcspClient ocspClient = new OcspClientBouncyCastle(new OCSPVerifier(null, null));

        if (chain.length >= 2) {
            ocsp = ocspClient.getEncoded((X509Certificate)chain[0], (X509Certificate)chain[1], null);
        }

        return ocsp;
    }

    public static PdfPKCS7 getPkcs() throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException {
        return new PdfPKCS7(null, chain, getHashAlgorithm(), null, getDigest(), false);
    }

    public static PdfPKCS7 getPkcs(@Nullable  Certificate[] certChain) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException {
        //noinspection ConstantConditions
        return new PdfPKCS7(null, certChain, getHashAlgorithm(), null, getDigest(), false);
    }

    public static void emptySignature(String src, String dest, String fieldname) throws IOException, DocumentException, GeneralSecurityException {
        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        PdfStamper stamper = PdfStamper.createSignature(reader, os, '[=10=]');

        Calendar cal = GregorianCalendar.getInstance();
        cal.add(Calendar.MINUTE, 10);

        PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
        appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
        appearance.setReason("Nice");
        appearance.setLocation("Delhi");
        appearance.setSignDate(cal);

        ExternalSignatureContainer external = new EmptyContainer();
        MakeSignature.signExternalContainer(appearance, external, 8192);

        os.close();
        reader.close();
    }

    public static void setChain() throws CertificateException {
        String cert = ""; // the cert we get from client
        ByteArrayInputStream userCertificate = new ByteArrayInputStream(Base64.decodeBase64(cert));

        CertificateFactory cf = CertificateFactory.getInstance("X.509");

        chain = new Certificate[]{cf.generateCertificate(userCertificate)};
    }

    private static ExternalDigest getDigest() {
       return new ExternalDigest() {
            public MessageDigest getMessageDigest(String hashAlgorithm)
                    throws GeneralSecurityException {
                return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
            }
        };
    }

    public static TSAClient getTsa() {
        return new TSAClientBouncyCastle("http://timestamp.digicert.com", null, null, 4096, "SHA-512");
    }

    public static void createSignature(String src, String dest, String fieldname, byte[] hash, byte[] signature) throws IOException, DocumentException, GeneralSecurityException {
        PdfPKCS7 sgn = getPkcs();
        sgn.setExternalDigest(signature, null, "RSA");

        byte[] encodedSig = sgn.getEncodedPKCS7(hash, getTsa(), getOscp(), null,
                MakeSignature.CryptoStandard.CMS);

        PdfReader reader = new PdfReader(src);
        FileOutputStream os = new FileOutputStream(dest);
        ExternalSignatureContainer external = new MyExternalSignatureContainer(encodedSig);
        MakeSignature.signDeferred(reader, fieldname, os, external);

        reader.close();
        os.close();
    }

    public static void main(String[] args) throws Exception {
        setChain();

        String src = "/home/spooderman/Downloads/sample.pdf";
        String between = "/tmp/sample_out_between.pdf";
        String dest = "/tmp/sample_out.pdf";
        String fieldName = "sign";

        emptySignature(src, between, fieldName);
        System.out.println(Hex.encodeHexString(toSign));

        String signature = "";  // signed hash signature we get from client
        byte[] signatureBytes = Hex.decodeHex(signature.toCharArray());

        createSignature(between, dest, fieldName, hash, signatureBytes);
    }
}