文章目录
参考文章:http://www.zzvips.com/article/195911.html
PDF文件结构
数据类型
直接对象
八种直接对象类型 | 约束 | 示例 |
---|---|---|
Boolean Objects | 值为true、false | |
Numeric Objects | 包含整型和浮点型 | 12 0.01 |
String Objects | 由一系列0-255之间的字节组成,总长度不能超过65535 文字字符用()括起来 16进制数据用<>括起来 字符串对象必须被完整的读入 | (Brillig) <0123456789ABCDEF> |
Name Objects | 由一个前导/ 和后面一系列字符组成,最大长度为127。 Name是一个原子符号且具有唯一性,大写字符与小字符被当为不同字符 | /Name1 |
Array Objects | 用[]包含的一组对象,可是以任何pdf对象(包括array) 数组元素个数不能超过8191 | [ 549 3.14 false ( Ralph ) /SomeName ] |
Dictionary Objects | 用<< >>包含的若干组条目 每组条目都是由key和value组成,其中key必须是name对象,Value可以是任何pdf的对象 一个dictionary内的key唯一 | ![]() |
Stream Objects | 流的数据被包含在stream 和endstream对中 流的数据是0 或多个字节 PDF 应用程序可以增量读入流对象 流对象的长度是无限制的 | ![]() |
Null Objects | 用null表示,代表空。如果一个key的值为null,则这个key可以被忽略 |
间接对象
任何对象都可以被标注为一个间接对象,在文档的任何地方都可以引用对象
间接对象的定义方法是:对象号码,然后后代号码,再跟一对关键字obj 和endobj
备注:后台号码指更新次数,一般都是0
示例:它的对象号码是12,后代号码是0,值是Brilling
间接对象的引用方法是:对象号码,加后代号码、再加关键字R
例如:12 0 R
文档结构
一个pdf文档分为4个部分:
1. 文件头,指明该文件所遵从的pdf规范的版本,它出现在pdf文档的第一行
2. 文件体,pdf文件的主要部分,由一系列的对象组成
3. 交叉引用表,为了能对间接对象进行随机存取而高效的一个间接对象的地址索引表
4. 文件尾,声明了交叉引用表的地址,指明了文件体的根对象,从而能够找到pdf文件中各个对象的位置,实现随机访问。
即:文件尾是解析文档的入口
用记事本工具打开PDF,示例:
文件头
处于第一行,图示为PDF版本为1.7
文件尾
可以存在多个trailer,从文件最后向上查找,以trailer开始,%%EOF结尾。
从文件尾可以看出:
文件根对象:/Root 1 0 R,对象1里面定义了页码Pages、语言及其他文档信息;
交叉引用表
理论参考:PDF格式分析(十二)Cross-Reference Table 交叉引用表
找一个pdf实战一下
以xref开始,图中,“0 41”表示对象号为0开始的连续41个对象。
xref
0 41 //说明下面各行所描述的对象号是从0开始,并且有41个对象
0000000017 65535 f // object 0:偏移量17,始终包含一个产生号为65535的条目, f表示空闲,65535 f不可更改,可以看作是文件头
0000000017 00000 n // object 1:偏移量17,产生号为00000,n表示在使用,可以更改(0表示未被修改过)
0000000166 00000 n // object 2:偏移量166,产生号为00000,n表示在使用,可以更改(0表示未被修改过)
解析PDF工具
PDFXplorer
下载地址:https://www.o2sol.com/pdfxplorer/download.htm
工具有三个视图:按结构、按页码、按交叉引用表,方便各维度查看
此工具有比较好的树形菜单,可以帮助初学者了解PDF结构;
对于高阶一点的朋友,可以用txt直接定位到签名的位置,想要结构化展示时,再使用工具找到对应obj即可。
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 RSA签章代码示例
需要用到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;
}
}
测试
public class UtilTest {
/**
* pdf签章
* @throws Exception
*/
@Test
public void pdfStampTest() throws Exception {
String password = "11111111";
String pfxPath = Constants.FILE_PATH + "/key/rsa/rsapfx3des-sha1.pfx";
PdfStampUtil pdfUtil = new PdfStampUtil();
// 读取keystore ,获得私钥
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream(pfxPath), password.toCharArray());
String alias = ks.aliases().nextElement();
PrivateKey pk = (PrivateKey) ks.getKey(alias, password.toCharArray());
// 得到证书链
Certificate[] chain = ks.getCertificateChain(alias);
//签章
byte[] pdfData = FileUtil.getFile(Constants.FILE_PATH + "2页.pdf");
byte[] photoData = FileUtil.getFile(Constants.FILE_PATH + "999.png");
byte[] signedData = pdfUtil.sign(pdfData, photoData,1,100, 100, chain, pk, DigestAlgorithms.SHA1);
FileUtil.storeFile(Constants.FILE_OUT_PATH + "stamp.pdf", signedData);
System.out.println("签章成功,文件存储路径为:" + Constants.FILE_OUT_PATH + "stamp.pdf");
}
@Test
public void pdfVerifyTest() throws Exception {
byte[] pdfData = FileUtil.getFile(Constants.FILE_OUT_PATH + "stamp.pdf");
PdfStampUtil pdfUtil = new PdfStampUtil();
boolean verify = pdfUtil.verifySign(pdfData);
System.out.println("验签结果:" + verify);
}
}
PDF签章文件解析
解析一个签章文件
此处展示了如何从签章文件中,拿出RSA的签名结构
RSA签章
/Contents下面的真正内容是一个DER编码的PKCS#7数据对象
SM2签章
有些厂商实现了PDF的SM2签章,/Contents下面的真正内容是符合签章规范的数据。
基于密标规范GMT 0031-2014 《安全电子签章密码技术规范》的由于暂未数据,不做展示。
基于国标规范GB/T 38540-2020《 信息安全技术 安全电子签章密码技术规范》的示例如下:
下面展示一个从签章文件里,获取SM2签章结构的代码示例:
@Test
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) {
}
}
}