寫在前面
一般情況下我們都是使用iText7自帶的
pdfsigner.detach()
複製程式碼
方法對pdf檔案進行簽名,iText7已經自己封裝好了PKC7,所以這裡還是挺方便的。但如果因為某種需求需要我們自己來進行P7簽名,那麼我們就可以使用
pdfsigner.signExternalContainer()
複製程式碼
來自己實現對pdf的簽名,即itext7只要提供要簽名的資料給我們就行了。
簽名和驗籤大致流程
我們可以看下這幅圖,來自《Acrobat_DigitalSignatures_in_PDF》:
大致的意思就是說
- 要簽名的時候會把文件轉換成位元組流叫ByteRange
- ByteRange有四個數字,分成三部分(以圖為例),我們要用來簽名的資料就在0- 840和960-1200這部分,然後簽名就存放在840~960裡面。
- 因此我們驗籤的時候獲取簽名值就來自於840~960也就是Contents裡。
- 要驗籤的原文就是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.detached和adbe.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?