前言
最近看到互联网上越来越多的合同都是以PDF的方式发送合同,并让用户感受到真切的法律效用,PDF合同文件都会有签章。
他有两方面好处,一个是让用户看到合同时有个正式的公司签章;另一个是PDF文件的签名能够防止篡改,具有一定的法律效应。
数字签名是什么?
数字签名基于哈希算法和公钥加密算法,对明文报文先用哈希算法计算摘要,然后用私钥对摘要进行加密,得到的值就是原文的数字签名。
数字签名(又称公钥数字签名、电子签章)是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术实现,用于鉴别数字信息的方法。
一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。
数字签名的原理
发件人生成或取得独一无二的加密密码组,包括私钥和公钥。
发件人书写电子邮件
发件人用安全的摘要算法获取电子邮件的信息摘要
发件人再使用私钥对信息摘要进行加密,即可得到数字签名。
发件人将数字签名附在信息之后.
发件人将数字签名和信息(加密或未加密)发送给电子收件人.
收件人使用发件人的公共密码(公钥)确认发件人的电子签名,即将发件人的数字签名通过公钥进行解密,得到信息摘要
收件人使用同样安全的摘要算法,获取信息(加密或未加密)的"信息摘要".
收件人比较两个信息摘要.假如两者相同,则收件人可以确信信息在签发后并未作任何改变
收件人从证明机构处获得认证证书(或者是通过信息发件人获得),这一证书用以确认发件人发出信息上的数字签名的真实性.证明机构在数字签名系统中是一个典型的受委托管理证明业务的第三方.该证书包含发件人的公共密码和姓名(以及其他可能的附加信息),由证明机构在其上进行数字签名.
PDF签章文件结构
PDF签章:将图片放入PDF,PDF+图片一起作为原文,进行签名
PDF签章格式:
adbe.pkcs7.detached(P7不带内容/原文)
adbe.pkcs7.sha1(P7带内容。先对PDF数据做SHA1,再把SHA1数据作为P7内容,相当于做了2次摘要)
adbe.x509.rsa_sha1(数字证书+P1签名):实际场景俗称裸签,哈希算法不一定是SHA1,有可能是SHA256。遇到了一个验滴滴发票的需求,就是用的SHA256做哈希,修改了itext源码来支持验签章的。
ETSI.CAdES.detached(CAdES不带内容)
一次签章
/ByteRange [offset1,len1,offset2,len2],表示PDF的两部分原文的位置。
示例:[0,840,960,240],0和960是偏移量,840和240是长度。
0-840:是签名原文的第一部分,这一部分会放一个图片,和原数据一起成为签名原文;
960-1200:是签名原文的第二部分;
840-960:是需要预留用于放签名数据的,具体预留多少,可以无限大,缺点就是最终文件会很大;也可以先对第一部分签名,就会得出大致的长度,在此基础上,再添加一些长度作为预留。
多次签章
PDF签章代码示例
需要用到itextpdf和BouncyCastleProvider的jar包:
itextpdf-5.5.5-with-asian.jar
bcpkix-jdk15on-1.60.jar
bcprov-jdk15on-1.60.jar
package cn.com.gs.common.util.pdf;
import cn.com.gs.common.define.Constants;
import cn.com.gs.common.exception.NetGSRuntimeException;
import cn.com.gs.common.util.StringUtil;
import com.itextpdf.text.Image;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.ByteArrayOutputStream;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.util.ArrayList;
/**
* 参考文章:https://blog.csdn.net/tomatocc/article/details/80762507
* @author Administator
*/
public class PdfStampUtil {
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* PDF添加图片并签名
* @param pdfData pdf数据
* @param photoData 图片数据
* @param pageNumber 页码
* @param x x坐标
* @param y y坐标
* @param chain 证书链
* @param privateKey 私钥
* @param hashAlg 摘要算法
* @return
* @throws Exception
*/
public byte[] sign(byte[] pdfData, byte[] photoData, int pageNumber, float x, float y,
Certificate[] chain, PrivateKey privateKey, String hashAlg) throws Exception {
PdfReader reader = new PdfReader(pdfData);
/*
* 创建签章工具PdfStamper,
* 第二个参数是输出流,签完的文件放在这个输出流,我们获取
* 最后一个boolean参数是否允许被追加签名
* false的话,pdf文件只允许被签名一次,多次签名,最后一次有效
* true的话,pdf可以被追加签名,验签工具可以识别出每次签名之后文档是否被修改
*/
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PdfStamper stamper = PdfStamper.createSignature(reader, outputStream, '\0', null, true);
// 1.设置PdfSignatureAppearance
PdfSignatureAppearance sap = stamper.getSignatureAppearance();
// 1.1设置图章图片
Image image = Image.getInstance(photoData);
sap.setSignatureGraphic(image);
// 1.2设置图章的显示方式,这里是GRAPHIC只显示图章(还有其他的模式,可以图章和签名描述一同显示),不设置默认是展示描述
sap.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC);
// 1.3设置图章位置,页码,签名域名称,多次追加签名的时候,签名预名称不能一样 图片大小受表单域大小影响(过小导致压缩)
// 签名的坐标,是图章相对于pdf页面的位置坐标,原点为pdf页面左下角
// 四个参数的分别是,图章左下角x,图章左下角y,图章右上角x,图章右上角y
float imageWidth = image.getWidth() * 72f / Constants.DPI;
float imageHeight = image.getHeight() * 72f / Constants.DPI;
float ux = x + imageWidth;
float uy = y + imageHeight;
sap.setVisibleSignature(new Rectangle(x, y, ux, uy), pageNumber, StringUtil.genDigitRandom(10));
// 2.摘要算法
ExternalDigest digest = new BouncyCastleDigest();
// 3.签名算法
ExternalSignature signature = new PrivateKeySignature(privateKey, hashAlg, null);
// 签章
MakeSignature.signDetached(sap, digest, signature, chain,
null, null, null, 0, MakeSignature.CryptoStandard.CADES);
stamper.close();
reader.close();
return outputStream.toByteArray();
}
/**
* PDF验签名
* @param pdfData
* @return
* @throws Exception
*/
public boolean verifySign(byte[] pdfData) throws Exception {
PdfReader reader = new PdfReader(pdfData);
AcroFields fields = reader.getAcroFields();
ArrayList<String> names = fields.getSignatureNames();
for (int i = 0, size = names.size(); i < size; i++) {
String signName = (String) names.get(i);
PdfDictionary dictionary = fields.getSignatureDictionary(signName);
PdfName sub = dictionary.getAsName(PdfName.SUBFILTER);
if (PdfName.ETSI_CADES_DETACHED.equals(sub)) {
PdfPKCS7 pkcs7 = fields.verifySignature(signName);
return pkcs7.verify();
} else {
throw new NetGSRuntimeException("暂不支持的SubFilter类型:" + sub);
}
}
return false;
}
}
PDF示例
解析一个签章文件
RSA签章
/Contents下面的真正内容是一个DER编码的PKCS#7数据对象
SM2签章
/Contents下面的真正内容是符合签章规范的数据,例如38540规范下的签章:
代码示例
public void getSealFromPDFStamp() throws Exception {
String pdfStampPath = "f:/temp/stamp.pdf";
String stampPath = "f:/temp/1.stamp";
byte[] pdfData = FileUtil.getFile(pdfStampPath);
PdfReader reader = null;
try {
reader = new PdfReader(pdfData);
AcroFields af = reader.getAcroFields();
ArrayList<String> names = af.getSignatureNames();
// 获取每一个签名域的 签名值
for (String name : names) {
PdfDictionary dictionary = af.getSignatureDictionary(name);
byte[] bytes = dictionary.getAsString(PdfName.CONTENTS).getBytes();
String hexContents = HexUtil.byte2Hex(bytes);
// 去除尾部填充的0
while (hexContents.endsWith("00"))
hexContents = hexContents.substring(0, hexContents.length() - 2);
FileUtil.storeFile(stampPath, HexUtil.hex2Byte(hexContents));
}
} catch (Exception e) {
throw e;
} finally {
try {
if (reader != null)
reader.close();
} catch (Exception e) {
}
}
}