注:本篇文章的代码里所提到的签名方法(SignUtil)和哈希方法(HashUtil)都只是起说明作用,这两个工具类都需要读者自己去实现(可以参考BouncyCastle库),我无法提供这两个类的代码,敬请谅解。
一般情况下我们都是使用iText7自带的
pdfsigner.detach()
方法对pdf文件进行签名,iText7已经自己封装好了PKC7,所以这里还是挺方便的,可以参考官方示例代码。
但如果因为某种需求需要我们自己来进行P7签名,那么我们就可以使用
pdfsigner.signExternalContainer()
来自己实现对pdf的签名,即itext7只要提供要签名的数据给我们就行了。
我们可以看下这幅图,来自《Acrobat_DigitalSignatures_in_PDF》:
大致的意思就是说
我们先看下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);
}
接下来我们看下需要用到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.
*
* 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:
*注:以下使用到的哈希方法(HashUtil),**签名方法(SignUtil)*是做一个说明,毕竟要用到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带原文签名。不过这种应该是后来的标准里被废除了。
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);
}
};
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);
}
};
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);
}
用我们的自己的签名工具进行签名后,我们可以更进一步的做验签。
/**
* 验签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;
}
/**
* 验签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?
C2_07_SignatureAppearances.java
pdf_reference_1-7.pdf
Why I can’t use SHA1 before PKCS7.detached in iText7?
数字世界中的纸张——理解 PDF