一. 电子签章
1.1 什么是电子签章
基于《中华人民共和国电子签名法》等相关法规和技术规范,具有法律效力的电子签章一定是需要使用 CA 数字证书进行对文件签名,并把 CA 数字证书存放在签名后文件中。
如果一份签名后的电子文件中无法查看到 CA 数字证书,仅存在一个公章图片,那么就不属于法律意义上的电子签名。电子签名法规定电子文件签署时一定要使用CA数字证书,并没有要求一定需要含有电子印章图片,理论上电子签章不需要到公安局进行备案。
实际上,电子签章是在电子签名技术的基础上添加了印章图像外观,沿袭了人们所习惯的传统盖章可视效果。电子签章使用电子签名技术来保障电子文件内容的防篡改性和签署者的不可否认性。因此,电子签章中,印章图片并不是唯一鉴别是否签章的条件,还要鉴别是否使用高级电子签名技术和 CA 数字证书。
CA 数字证书是在互联网中用于识别身份的一种具有权威性的电子文档。CA 数字证书相当于现实中的身份证。
现实中,如同个人需要去公安局申请办理身份证一样,CA 数字证书需要在“电子认证服务机构”(简称 CA 机构)进行申请办理。中国工业和信息化部、工信部授权 CA 机构来制作、签发数字证书,用非对称加密的方式,生成一对密码即私钥与公开密钥,并绑定了数字证书持有者的真实身份,人们可以在电子合同的缔约过程中用它来证明自己的身份和验证对方的身份。
CA 机构颁发的数字证书为公钥证书和私钥证书:公钥证书是对外公开、任何人都可以使用的,而私钥是专属于签署人所有的。当需要签署文档时,签署人使用私钥证书对电子文件(文档哈希值)进行加密,形成电子签名。(注:文档哈希值计算时包含待签 PDF 文档内容、印章图片和印章坐标位置信息)。
哈希值是指将 PDF 文件按照一定的算法(目前主流是 SHA256 算法),形成一个唯一的文件代码,类似于人类的指纹,任何一个 PDF 文件只有一个哈希值,且不同 PDF 文件的哈希值不可能相同,而相同哈希值的 PDF 文件的内容肯定相同。哈希算法是不可逆的,从哈希值无法推导出 PDF 原文内容。
经签署人的私钥证书加密之后的 PDF 原文哈希值就是电子签名,电子签名中有签署人的姓名、身份证号码、证书有效期、公钥等信息,电子签名放在 PDF 原文的签名域中,就形成了带有电子签名的 PDF 文件。
1.2 签名流程
文件电子签名过程,如下图:
其他人收到这个文件,即可使用PDF文件的签名域中存储的公钥证书对电子签名进行解密,解密出来的文件哈希值如果与原文的哈希值一致,则代表这个文件没有被篡改。
电子签名文件验签过程,如下图:
1.3 技术选型
这块主要有两大技术体系:
- 开源组织 Apache 的 PDFBox。
- Adobe 的 iText,其中 iText 又分为 iText5 和 iText7。
那么这两个该如何选择呢?
- PDFBox 的功能相对较弱,iText5 和 iText7 的功能非常强悍。
- iText5 资料网上相对较多,如果出现问题容易找到解决方案。
- PDFBox 和 iText7 的网上资料相对较少,如果出现问题不易找到相关解决方案。
- PDFBox 目前提供的自定义签章接口不完整;而 iText5 和 iText7 提供了处理自定义签章的相关实现。
- PDFBox 只能实现把签章图片加签到 PDF 文件;iText5 和 iText7 除了可以把签章图片加签到 PDF 文件,还可以实现直接对签章进行绘制,把文件绘制到签章上。
- PDFBox 和 iText5/iText7 使用的协议不一样。PDFBox 使用的是 APACHE LICENSE VERSION 2.0(Licenses);iText5/iText7 使用的是 AGPL(https://itextpdf.com/agpl)。PDFBox 免费使用,AGPL 商用收费。
因此这里就以 iText5 为例来和小伙伴们演示如何给一个 PDF 文件签名。
二. 实战
2.1 生成数字证书
首先我们需要生成一个数字证书。
这个数字证书我们可以利用 JDK 自带的工具生成,为了贴近实战,松哥这里使用 Java 代码生成,生成数字证书的方式如下。
首先引入 Bouncy Castle,Bouncy Castle 是一个广泛使用的开源加密库,它为 Java 平台提供了丰富的密码学算法实现,包括对称加密、非对称加密、哈希算法、数字签名等。这个库由于其广泛的算法支持和可靠性而备受信任,被许多安全应用和加密通信协议所采用 。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-ext-jdk15on</artifactId>
<version>1.70</version>
</dependency>
接下来我们写一个生成数字证书的工具类,如下:
package com.victor.common.utils.pkcs;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.*;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.*;
public class PkcsUtils {
/**
* 生成证书
* @return
* @throws NoSuchAlgorithmException
*/
private static KeyPair getKey() throws NoSuchAlgorithmException {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", new BouncyCastleProvider());
generator.initialize(1024);
// 证书中的密钥 公钥和私钥
KeyPair keyPair = generator.generateKeyPair();
return keyPair;
}
/**
* 生成证书
* @param password
* @param issuerStr
* @param subjectStr
* @param certificateCRL
* @return
*/
public static Map<String, byte[]> createCert(String password, String issuerStr, String subjectStr, String certificateCRL) {
Map<String, byte[]> result = new HashMap<>();
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
// 标志生成PKCS12证书
KeyStore keyStore = KeyStore.getInstance("PKCS12", new BouncyCastleProvider());
keyStore.load(null, null);
KeyPair keyPair = getKey();
// issuer与 subject相同的证书就是CA证书
X509Certificate cert = generateCertificateV3(issuerStr, subjectStr, keyPair, result, certificateCRL);
// 证书序列号
keyStore.setKeyEntry("cretkey", keyPair.getPrivate(), password.toCharArray(), new X509Certificate[]{
cert});
cert.verify(keyPair.getPublic());
keyStore.store(out, password.toCharArray());
byte[] keyStoreData = out.toByteArray();
result.put("keyStoreData", keyStoreData);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 生成证书
*
* @param issuerStr
* @param subjectStr
* @param keyPair
* @param result
* @param certificateCRL
* @return
*/
public static X509Certificate generateCertificateV3(String issuerStr, String subjectStr