iText7使用IExternalSignatureContainer進行簽名和驗籤

JDNew發表於2019-02-21

寫在前面

一般情況下我們都是使用iText7自帶的

pdfsigner.detach()
複製程式碼

方法對pdf檔案進行簽名,iText7已經自己封裝好了PKC7,所以這裡還是挺方便的。但如果因為某種需求需要我們自己來進行P7簽名,那麼我們就可以使用

pdfsigner.signExternalContainer()
複製程式碼

來自己實現對pdf的簽名,即itext7只要提供要簽名的資料給我們就行了。

簽名和驗籤大致流程

我們可以看下這幅圖,來自《Acrobat_DigitalSignatures_in_PDF》

Acrobat_DigitalSignatures_in_PDF

大致的意思就是說

  1. 要簽名的時候會把文件轉換成位元組流叫ByteRange
  2. ByteRange有四個數字,分成三部分(以圖為例),我們要用來簽名的資料就在0- 840和960-1200這部分,然後簽名就存放在840~960裡面。
  3. 因此我們驗籤的時候獲取簽名值就來自於840~960也就是Contents裡。
  4. 要驗籤的原文就是ByteRange裡除去簽名值的部分。

IExternalSignatureContainer介紹

我們先看下IExternalSignatureContainer這個介面:

/**
 * Interface to sign a document. The signing is fully done externally, including the container composition.
 * 這是一個用來簽署檔案的介面,它讓所有的簽名都完全來自於外部擴充套件實現。
 * @author Paulo Soares
 */
public interface IExternalSignatureContainer {

    /**
     * Produces the container with the signature.
     * @param data the data to sign
     * @return a container with the signature and other objects, like CRL and OCSP. The container will generally be a PKCS7 one.
     * @throws GeneralSecurityException
     */
    byte[] sign(InputStream data) throws GeneralSecurityException;

    /**
     * Modifies the signature dictionary to suit the container. At least the keys {@link PdfName#Filter} and
     * {@link PdfName#SubFilter} will have to be set.
     * @param signDic the signature dictionary
     */
    void modifySigningDictionary(PdfDictionary signDic);
}
複製程式碼

signExternalContainer()方法介紹

接下來我們看下需要用到IExternalSignatureContainer 的方法 signExternalContainer() 的介紹:

    /**
     * Sign the document using an external container, usually a PKCS7. The signature is fully composed
     * externally, iText will just put the container inside the document.
     * <br><br>
     * NOTE: This method closes the underlying pdf document. This means, that current instance
     * of PdfSigner cannot be used after this method call.
     *
     * @param externalSignatureContainer the interface providing the actual signing
     * @param estimatedSize              the reserved size for the signature
     * @throws GeneralSecurityException
     * @throws IOException
     */
    public void signExternalContainer(IExternalSignatureContainer externalSignatureContainer, int estimatedSize) throws GeneralSecurityException, IOException {
        //省略部分原始碼
        
        //關注這裡,呼叫getRangeStream()方法獲取到要簽名的資料
        //傳入到externalSignatureContainer.sign()方法裡給我們籤
        InputStream data = getRangeStream();
        byte[] encodedSig = externalSignatureContainer.sign(data);
        
        //省略部分原始碼


    /**
     * Gets the document bytes that are hashable when using external signatures. 在使用外部簽名的時候會返回可用於雜湊的檔案位元組。
     * The general sequence is:
     * {@link #preClose(Map)}, {@link #getRangeStream()} and {@link #close(PdfDictionary)}.
     *
     * @return The {@link InputStream} of bytes to be signed.
     * 返回用於簽名的位元組
     */
    protected InputStream getRangeStream() throws IOException {
        RandomAccessSourceFactory fac = new RandomAccessSourceFactory();
        return new RASInputStream(fac.createRanged(getUnderlyingSource(), range));
    }
複製程式碼

可以看到這個方法需要兩個引數IExternalSignatureContainer(擴充套件簽名容器) 和 estimatedSize(預估值)。

開始重寫IExternalSignatureContainer

那麼我們先重寫IExternalSignatureContainer:

注:以下使用到的雜湊方法,簽名方法是做一個說明,畢竟要用到IExternalSignatureContainer表示你已經是有了自己的一套雜湊和簽名工具了

在進行簽名的時候有兩個subFilter可以然後我們進行使用,分別是adbe.pkcs7.detachedadbe.pkcs7.sha1,在pdf1.7文件裡的解釋是

adbe.pkcs7.detached: No data is encapsulated in the PKCS#7 signed-data field.

adbe.pkcs7.sha1: The SHA1 digest of the byte range is encapsulated in the PKCS#7 signed-data field with ContentInfo of type Data.

adbe.pkcs7.detached是目前用得最多的,在這裡我們直接將資料進行p7不帶原文簽名即可;

adbe.pkcs7.sha1則是先對資料進行雜湊,然後再呼叫p7帶原文簽名。不過這種應該是後來的標準裡被廢除了。

Adbe.pkcs7.detached

IExternalSignatureContainer externalP7DetachSignatureContainer = new IExternalSignatureContainer() {
        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
        
            //將要簽名的資料進行 P7不帶原文 簽名
            byte[] result = SignUtil.P7DetachSigned(data);
            
            return result;
        }

        //必須設定 PdfName.Filter 和 PdfName.SubFilter
        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.put(PdfName.Filter, PdfName.Adobe_PPKLite);
            //注意這裡
            signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
        }
    };

複製程式碼

Adbe.pkcs7.sha1

IExternalSignatureContainer externalP7Sha1SignatureContainer = new IExternalSignatureContainer() {
        @Override
        public byte[] sign(InputStream data) throws GeneralSecurityException {
        
            //對要簽名的資料先進行雜湊
            byte[]hashData = HashUtil.hash(data , "SHA-1");
            //將雜湊後的資料進行 P7帶原文 簽名
            byte[] result = SignUtil.P7AttachSigned(hashData);
            
            return result;
        }

        //必須設定 PdfName.Filter 和 PdfName.SubFilter
        @Override
        public void modifySigningDictionary(PdfDictionary signDic) {
            signDic.put(PdfName.Filter, PdfName.Adobe_PPKLite);
            //注意這裡
            signDic.put(PdfName.SubFilter, PdfName.Adbe_pkcs7_sha1);
        }
    };

複製程式碼

呼叫signExternalContainer()方法

PdfReader pdfReader = new PdfReader(new ByteArrayInputStream(pdfFile));
PdfSigner pdfSigner = new PdfSigner(pdfReader, new FileOutputStream(signedPath), false);

//estimatedSize可以自己設定預估大小
//但建議開啟一個迴圈來判斷,如果太小就增大值,直到簽名成功
pdfSigner.signExternalContainer(externalP7DetachSignatureContainer, estimatedSize);
複製程式碼

如改成這樣:

        //是否簽名成功標誌
        boolean success = false;
        //預估大小
        int estimatedSize = 3000;

        //通過調整預估大小直到簽名成功
        while (!success) {
            try {
                PdfReader pdfReader = new PdfReader(new ByteArrayInputStream(pdfFile));
                PdfSigner pdfSigner = new PdfSigner(pdfReader, new FileOutputStream(signedPath), false);
                pdfSigner.signExternalContainer(externalP7DetachSignatureContainer, estimatedSize);

                success = true;

            } catch (IOException e) {
                e.printStackTrace();
                estimatedSize += 1000;
            } catch (GeneralSecurityException e) {
                e.printStackTrace();
            }
        }
複製程式碼

蓋章

假設我們現在需要為檔案進行蓋章,我們可以準備一張圖章圖片,將它新增到簽名域裡。

    /**
     * 對pdf進行簽名圖片操作(新增簽章)
     *
     * @param pdfSigner
     * @param imgBytes 圖片檔案
     * @param leftBottomX 圖片的左下方x座標
     * @param leftBottomY 圖片的左下方y座標
     * @param imgWidth 圖片的寬度
     * @param imgHeight 圖片的高度
     * @param pageNum 頁碼
     */
    private void doImageStamp(PdfSigner pdfSigner, byte[] imgBytes, int leftBottomX, int leftBottomY, int imgWidth, int imgHeight, int pageNum) {

        ImageData imageData = ImageDataFactory.create(imgBytes);

        PdfSignatureAppearance appearance = pdfSigner.getSignatureAppearance();
        Rectangle rectangle = new Rectangle(leftBottomX , leftBottomY , imgWidth , imgHeight);

        appearance.setPageRect(rectangle)
                .setPageNumber(pageNum)
                .setSignatureGraphic(imageData)
       .setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);

    }
複製程式碼

驗籤

用我們的自己的簽名工具進行簽名後,我們可以更進一步的做驗籤。

Adbe.pkcs7.detached驗籤

    /**
     * 驗籤pdf
     *
     * @param pdf 簽名好的pdf
     * @return 驗簽結果 true/false
     */
    public boolean verifyPdf(byte[] pdf) {

        boolean result = false;

        try {
            PdfReader pdfReader = new PdfReader(new ByteArrayInputStream(pdf));
            PdfDocument pdfDocument = new PdfDocument(pdfReader);
            SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
            List<String> signedNames = signatureUtil.getSignatureNames();

            //遍歷簽名的內容並做驗籤
            for (String signedName : signedNames) {

                //獲取源資料
                byte[] originData = getOriginData(pdfReader, signatureUtil, signedName);

                //獲取簽名值
                byte[] signedData = getSignData(signatureUtil , signedName);

                //校驗簽名
                result = SignUtil.verifyP7DetachData(originData , signedData);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }
複製程式碼

Adbe.pkcs7.sha1驗籤

    /**
     * 驗籤pdf
     *
     * @param pdf 簽名好的pdf
     * @return 驗簽結果 true/false
     */
    public boolean verifyPdf(byte[] pdf) {

        boolean result = false;

        try {
            PdfReader pdfReader = new PdfReader(new ByteArrayInputStream(pdf));
            PdfDocument pdfDocument = new PdfDocument(pdfReader);
            SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
            List<String> signedNames = signatureUtil.getSignatureNames();

            //遍歷簽名的內容並做驗籤
            for (String signedName : signedNames) {

                //獲取簽名值
                byte[] signedData = getSignData(signatureUtil , signedName);

                //校驗簽名
                result = SignUtil.verifyP7AttachData(signedData);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return result;
    }
複製程式碼

獲取源資料和簽名資料方法

    /**
     * 獲取簽名資料
     * @param signatureUtil
     * @param signedName
     * @return
     */
    private byte[] getSignData(SignatureUtil signatureUtil, String signedName) {
        PdfDictionary pdfDictionary = signatureUtil.getSignatureDictionary(signedName);
        PdfString contents = pdfDictionary.getAsString(PdfName.Contents);
        return contents.getValueBytes();
    }

    /**
     * 獲取源資料(如果subFilter使用的是Adbe.pkcs7.detached就需要在驗籤的時候獲取 源資料 並與 簽名資料 進行 p7detach 校驗)
     * @param pdfReader
     * @param signatureUtil
     * @param signedName
     * @return
     */
    private byte[] getOriginData(PdfReader pdfReader, SignatureUtil signatureUtil, String signedName) {

        byte[] originData = null;

        try {
            PdfSignature pdfSignature = signatureUtil.getSignature(signedName);
            PdfArray pdfArray = pdfSignature.getByteRange();
            RandomAccessFileOrArray randomAccessFileOrArray = pdfReader.getSafeFile();
            InputStream rg = new RASInputStream(new RandomAccessSourceFactory().createRanged(randomAccessFileOrArray.createSourceView(), SignatureUtil.asLongArray(pdfArray)));
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buf = new byte[8192];
            int n = 0;
            while (-1 != (n = rg.read(buf))) {
                outputStream.write(buf, 0, n);
            }

            originData = outputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return originData;

    }
複製程式碼

獲取簽名資訊

當我們對pdf進行簽名後,可以獲取到這份pdf裡的簽名資訊。

/**
 * 獲取簽名資訊實體類
 */
public class PdfSignInfo {

    private Date signDate;
    private String digestAlgorithm;
    private String reason;
    private String location;
    private String signatureName;
    private String encryptionAlgorithm;
    private String signerName;
    private String contactInfo;
    private int revisionNumber;
    
    public int getRevisionNumber() {
        return revisionNumber;
    }

    public String getContactInfo() {
        return contactInfo;
    }

    public String getSignerName() {
        return signerName;
    }

    public void setSignerName(String signerName) {
        this.signerName = signerName;
    }

    public String getEncryptionAlgorithm() {
        return encryptionAlgorithm;
    }

    public void setEncryptionAlgorithm(String encryptionAlgorithm) {
        this.encryptionAlgorithm = encryptionAlgorithm;
    }

    public String getSignatureName() {
        return signatureName;
    }

    public void setSignatureName(String signatureName) {
        this.signatureName = signatureName;
    }

    public void setSignDate(Date signDate) {
        this.signDate = signDate;
    }

    public Date getSignDate() {
        return signDate;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    public String getDigestAlgorithm() {
        return digestAlgorithm;
    }

    public void setDigestAlgorithm(String digestAlgorithm) {
        this.digestAlgorithm = digestAlgorithm;
    }

    public void setContactInfo(String contactInfo) {
        this.contactInfo = contactInfo;
    }

    public void setRevisionNumber(int revisionNumber) {
        this.revisionNumber = revisionNumber;
    }
}
複製程式碼
    /**
     * 解析返回簽名資訊
     * @param pdf
     * @return
     */
    public List<PdfSignInfo> getPdfSignInfo(byte[] pdf){

        //新增BC庫支援
        BouncyCastleProvider provider = new BouncyCastleProvider();
        Security.addProvider(provider);

        List<PdfSignInfo> signInfoList = new ArrayList<>();

        try {
            PdfReader  pdfReader = new PdfReader(new ByteArrayInputStream(pdf));
            PdfDocument pdfDocument = new PdfDocument(pdfReader);

            SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);

            List<String> signedNames = signatureUtil.getSignatureNames();

            //遍歷簽名資訊
            for (String signedName : signedNames) {

                PdfSignInfo pdfSignInfo = new PdfSignInfo();
                pdfSignInfo.setSignatureName(signedName);
                pdfSignInfo.setRevisionNumber(signatureUtil.getRevision(signedName));

                PdfPKCS7 pdfPKCS7 = signatureUtil.verifySignature(signedName , "BC");

                pdfSignInfo.setSignDate(pdfPKCS7.getSignDate().getTime());
                pdfSignInfo.setDigestAlgorithm(pdfPKCS7.getDigestAlgorithm());
                pdfSignInfo.setLocation(pdfPKCS7.getLocation());
                pdfSignInfo.setReason(pdfPKCS7.getReason());
                pdfSignInfo.setEncryptionAlgorithm(pdfPKCS7.getEncryptionAlgorithm());

                X509Certificate signCert = pdfPKCS7.getSigningCertificate();

                pdfSignInfo.setSignerName(CertificateInfo.getSubjectFields(signCert).getField("CN"));

                PdfDictionary sigDict = signatureUtil.getSignatureDictionary(signedName);
                PdfString contactInfo = sigDict.getAsString(PdfName.ContactInfo);
                if (contactInfo != null) {
                    pdfSignInfo.setContactInfo(contactInfo.toString());
                }

                signInfoList.add(pdfSignInfo);

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return signInfoList;

    }
複製程式碼

參考

How can I get ByteRange with iText7?

SignatureTest.java

C2_07_SignatureAppearances.java

pdf_reference_1-7.pdf

Why I can`t use SHA1 before PKCS7.detached in iText7?

相關文章