基于Itext的PDF国密电子签名及其实现

1.基础准备:

1.1 推荐大家阅读https://blog.csdn.net/liumengya007007/article/details/53129323 ,首先完成RSA的电子签名.有一个关于电子签名的直观认识.

1.2一个国密算法的签名/解密工具类(如果没有国密的,任意加解密类都可以,本文提供的PDF签名方法适用于任何一种算法)

1.3 itext 5.5 的源码包,及其相关依赖,因为我们是需要修改itext源码的

2.技术导读:

2.1PDF文档结构:

因为不是专业的,这里只通俗的介绍,先引用下别人的成果

---------------------
作者:J_D_New
来源:CSDN
原文:https://blog.csdn.net/u013066292/article/details/78486055
版权声明:本文为博主原创文章,转载请附上博文链接!

签名和验签大致流程

我们可以看下这幅图,来自《Acrobat_DigitalSignatures_in_PDF》:

Acrobat_DigitalSignatures_in_PDF

大致的意思就是说

    要签名的时候会把文档转换成字节数组byte[],然后用一个叫ByteRange的数组的数据去切割文档的字节数组.
    ByteRange有四个数字,分成三部分(以图为例),我们要用来签名的数据就在0~840和960~1200这部分,然后签名就存放在840~960里面。(注意每个文档的ByteRange的值都是不一样的)
    因此我们验签的时候获取签名值就来自于840~960也就是Contents里。
    要验签的原文就是ByteRange里除去签名值的部分。

2.2 java SPI

因为国密算法的具体实现千差万别,我们不可能为了每一家不同的CA厂商都去单独实现一套国密的签名接口,为了实现可插拔的原则,也是尽可能的甩锅和偷懒(代码写的越多错的越多,做项目的最高境界就是一行代码都不写..全是别人的锅),我们引入spi技术,具体的不多赘述,再抄一篇别人的工作成果

https://blog.csdn.net/sigangjun/article/details/79071850

当然如果只是个人学习使用,无视这里就好,但是多学一点是一点对吧.

3.思路

3.1签名过程

获取PDF文档的字节数组----->创建签名域(大小,外观显示等等)------>读取byteRange将文档的字节数组切割成3部分,按顺序命名为buffer1,buffer2,buffer3-->System.arraycopy把buffer1和buffer3相加获取签名原文orgin(按照Adobe官方的说法是buffer1+buffer3但是实际操作中发现buffer1占了绝大部分的加密主体,buffer3只有很小一部分,而且一些小操作比如添加文档属性等会导致buffer3的变动,所以本文实际加密的是buffer1而非buffer1+buffer3)--->将orgin转码发送spi的加解密接口中,获得加密值signValue--->将signValue写入文档中,也就是Contents中--->获取新的签名后文档

3.2 验证过程

扫描文档签名域读取byteRange-->按照byteRange的值切割文档-->取buffer1(buffer1+buffer3看当时加密的是哪些部分)作为签名原文----->取pdf中Contents的部分,得到签名结果--->将签名原文和结果发送spi验证接口,获取验证结果

4.开工

首先我们仿照RSA的签名过程

重点是在这个部分

我们可以看到MakeSignature有三个方法

我们使用的方法是

参数依次是:签名域,需要实现的spi接口(稍后再说),初始化的Content大小(只能>= 小于会报错),我自己加的Id,用于识别用户的唯一键值对

值得注意的是:Content的大小 itext中自己是写死8192的,我们可以写死也可以根据签名结果动态传参,但是必须保证Content的大小大于你的签名结果.

所以

这部分代码注释掉

替换为(主动寻找spi服务)

ExternalSignatureContainer externalSignatureContainer = null;
ServiceLoader<ExternalSignatureContainer> serviceLoader2 = ServiceLoader.load(ExternalSignatureContainer.class);
Iterator<ExternalSignatureContainer> operationIterator2 = serviceLoader2.iterator();
while (operationIterator2.hasNext()) {
	externalSignatureContainer = operationIterator2.next();
}
MakeSignature.signExternalContainer(sap, externalSignatureContainer, 8192, this.CertificateId);

然后就是喜闻乐见的改源码了

修改signExternalContainer方法,原理上照上文所诉的,下面贴一下参考的代码,涉及到一些底层的get/set方法,自行添加就好.

/**
	 * 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.
	 * @param sap the PdfSignatureAppearance
	 * @param externalSignatureContainer the interface providing the actual signing
	 * @param estimatedSize the reserved size for the signature
	 * @throws GeneralSecurityException
	 * @throws IOException
	 * @throws DocumentException 
	 */
	public static void signExternalContainer(PdfSignatureAppearance sap,
			ExternalSignatureContainer externalSignatureContainer, int estimatedSize, String CertificateId)
			throws GeneralSecurityException, IOException, DocumentException {
		PdfSignature dic = new PdfSignature(null, null);
		dic.setReason(sap.getReason());
		dic.setLocation(sap.getLocation());
		dic.setSignatureCreator(sap.getSignatureCreator());
		dic.setContact(sap.getContact());
		dic.setDate(new PdfDate(sap.getSignDate())); // time-stamp will
														// over-rule this
		externalSignatureContainer.modifySigningDictionary(dic);
		sap.setCryptoDictionary(dic);
		HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
		exc.put(PdfName.CONTENTS, new Integer(estimatedSize * 2 + 2));
		sap.preClose(exc);
		// 获取签名原文
		byte[] src = sap.getBout();
		int range[] = { (int) sap.getRange()[0], (int) sap.getRange()[1], (int) sap.getRange()[2],
				(int) sap.getRange()[3] };
		System.out.println(range[0] + "," + range[1] + "," + range[2] + "," + range[3]);
		int length = range[1] + range[3];
		System.out.println("length: " + length);
		byte[] byffer1 = new byte[range[1]];
		// byte[] buffer2=new byte[range[3]];
		System.arraycopy(src, 0, byffer1, 0, range[1]);
		// System.arraycopy(src, range[2], buffer2, 0, range[3]);
		// System.out.println("buffer1: " + Base64Util.encode(byffer1));
		// System.out.println("buffer2: "+Base64Util.encode(buffer2));
		// byte[] dest = new byte[length];
		// System.arraycopy(src, 0, dest, 0, range[1]);
		// System.arraycopy(src, range[2], dest, range[1], range[3]);
		// System.arraycopy(src2, range[2], dest, range[1], range[4]);
		// System.out.println("验证原文: "+Base64Util.encode(dest));
		// 测试结束----
		// InputStream data = sap.getRangeStream();
		InputStream data = new ByteArrayInputStream(byffer1);
		byte[] encodedSig = externalSignatureContainer.signByCertificateID(data, CertificateId);
		// if (estimatedSize < encodedSig.length)
		// throw new IOException("Not enough space");
		// byte[] paddedSig = new byte[estimatedSize];
		// System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length);
		PdfDictionary dic2 = new PdfDictionary();
		dic2.put(PdfName.CONTENTS, new PdfString(encodedSig).setHexWriting(true));
		sap.close(dic2);
	}

这一步做完之后,本质上我们关于Itext的开发就OK了,剩下的是和CA的加解密接口开发联调...

新建一个工程,创建两个类导入修改后的itext包/工程

一个实现 itext 5.5 ExternalSignatureContainer

modifySigningDictionary()照抄

signByCertificateID()是我修改的原接口,原始接口是实现的是sign()方法,加入CertId是因为CA的接口需要,这里是以吉大正元的SM2接口为例,其实没有接口也无所谓,随便写个加密解密就是了.自己定义一个ADBE_GM_DETACHED数值随意这个只是用作标识的

package cn.biceng;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.security.GeneralSecurityException;

import com.eseals.itextpdf.text.pdf.PdfDictionary;
import com.eseals.itextpdf.text.pdf.PdfName;
import com.eseals.itextpdf.text.pdf.security.ExternalSignatureContainer;
import com.eseals.util.Base64Util;

import cn.com.jit.assp.dsign.DSign;

public class MySignaturnContainer implements ExternalSignatureContainer {

	@Override
	public void modifySigningDictionary(PdfDictionary signDic) {
		signDic.put(PdfName.FILTER, PdfName.ADOBE_PPKLITE);
		// 注意这里
		signDic.put(PdfName.SUBFILTER, PdfName.ADBE_GM_DETACHED);
	}

	@Override
	public byte[] signByCertificateID(InputStream signData, String CertId) throws GeneralSecurityException {
		try {
			DSign signDs = new DSign();
			signDs.initConfig("d:/cssconfig.properties");
			ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
			byte[] buff = new byte[8192];
			int rc = 0;
			while ((rc = signData.read(buff, 0, 100)) > 0) {
				swapStream.write(buff, 0, rc);
			}
			byte[] srccode = swapStream.toByteArray(); // 需要签名的原文
//			System.out.println("签名原文: " + Base64Util.encode(srccode));
//			System.out.println("签名原文长度: " + Base64Util.encode(srccode).length());
			String signReturn = signDs.detachSign(CertId, Base64Util.encode(srccode).getBytes());
//			System.out.println("签名返回值: " + signReturn);
//			System.out.println("签名返回值长度:" + signReturn.length());
			return Base64Util.decode(signReturn);
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

}

另一个是验证的时候用到的接口

验证的部分涉及到一些公司内部的东西就不方便发了,这里告诉大家怎么提取原文和签名结果,大家自行调用验证接口就可以了,本文的MyVerfiyResult就是关于验证接口的封装实现,没什么营养.

        byte[] temp = data;
		PdfReader reader = null;
		try {
			reader = new PdfReader(data);
		} catch (BadPasswordException badPwd) {
			reader = new PdfReader(data, ownerPassword.getBytes());
		} catch (Exception e) {
			throw e;
		}
		AcroFields af = reader.getAcroFields();
	    for (String name : af.getSignatureNames()) {
			GmStampVerifyResult result=new GmStampVerifyResult();
			// 获取源数据
			PdfDictionary v = af.getSignatureDictionary(name);
			PdfArray b = v.getAsArray(PdfName.BYTERANGE);
			long[] gaps = b.asLongArray();
			int range[] = { (int) gaps[0], (int) gaps[1], (int) gaps[2], (int) gaps[3] };
			//获取原文 此处是buffer1,看加密过程中决定是否改buffer1+buffer3
			byte[] buffer1 = new byte[range[1]];
			System.arraycopy(temp, 0, buffer1, 0, range[1]);
			byte[] originData = buffer1;
			// 获取签名值
			PdfDictionary pdfDictionary = af.getSignatureDictionary(name);
			PdfString contents = pdfDictionary.getAsString(PdfName.CONTENTS);
         }

结语:涉及到源码的改动,只能描述大概的流程,一些边角的东西只有在实际操作中才会暴露出来,有兴趣的小伙伴可以动手试试看改造

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丨LucKy丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值