Advertisement

Spring Boot集成iText实现电子签章

阅读量:

一 电子签章

1.1 什么是电子签章

遵循《中华人民共和国电子签名法》等相关法律法规和技术标准,在具有法律效力的电子签章中,其CA数字证书必须依靠CA数字证书对文件进行签名,并将其CA数字证书包含于所签名文件之中。

若某份已签署的电子文件中无法显示CA数字证书,则不能视为具有法律效力的电子签名。根据《电子签名法》,签署数字文件时必须使用CA数字证书作为认证依据;虽然法规未强制要求附加电子印章图件。理论上而言,在法律框架内无需对数字签章进行备案。

实际上就是基于电子签名技术增添了一种具有视觉效果的印章图像,并延续了人们惯用的传统盖章显示效果。通过运用先进的电子签名技术和CA数字证书等方法来确保电子文件内容的安全性和签署者的不可否认性。因此,在电子签章过程中,并不仅仅是通过印章图片来进行鉴别是否已签署;而是还需要结合是否采用了高阶的数字签名技术和CA证书来进行验证。

CA数字证书是互联网中用于身份认证的一种权威电子文档。现实中的身份证被视为CA数字证书的一种替代。

现实生活中与办理身份证相近的流程,在电子认证领域中也需要通过"电子认证服务机构"(简称CA机构)进行CA数字证书的申请。中国工业和信息化部及工信部则赋予这些CA机构 authority to create and issue digital certificates, employing asymmetric encryption technology to generate a pair of keys—private key and public key—and attach the holder's true identity. 在电子合同订立过程中,人们可用此来证明自身身份并验证他人身份。

由CA机构颁发的数字证书分为公钥与私键两种类型:其中公键可被普遍使用;而私键仅归发证人所有。在需要对文件进行签名时;发证人应利用其 priv钥匙认证对应的电子文件中的 file hash 值;从而生成电子签名

哈希值是指将PDF文件按照特定算法(目前主流采用的是SHA256算法)进行处理后生成的一个唯一文件标识符。如同独一无二的身份标记,在密码学领域中被广泛应用于文件完整性验证和版权保护等场景。每个PDF文件都对应唯一的哈希值,并且任何两个不同的PDF文件其哈希值必然不一致。然而需要注意的是,在实际应用中由于计算能力限制以及潜在的安全漏洞可能会导致相同哈希指针却指向不同原始内容的情况出现。该算法具有不可逆性特征即仅能通过给定的输入计算出对应的输出但无法反向推导出原始输入数据

该 PDF 文件在签署人处使用私钥证书进行加密后生成的哈希值即为电子签名。该电子签名包含了签署人姓名、身份证号码、证书有效期以及公钥等关键信息。将该电子签名插入到 PDF 文件中的相应位置,则会形成具有数字认证功能的 PDF 文件。

1.2 签名流程

文件电子签名过程,如下图:

其他人接收该PDF文档时即可利用其数字证书中的公钥部分来验证电子签名的有效性。具体来说,在PDF文档中找到带有电子签名的安全区域后提取内容并计算其哈希值。若计算出的安全区域内容与原始文档对应部分的哈希值一致,则表明该文档未遭篡改

电子签名文件验签过程,如下图:

1.3 技术选型

这块主要有两大技术体系:

Apache 开发了一个名为 PDFBox 的开源项目。Adobe 提供了iText框架,并将其划分为iText5和iText7两个版本。

那么这两个该如何选择呢?

  • 相较于iText5与iText7而言, PDFBox的功能较为有限.
  • 关于iText5的相关资料在网络资源中较为丰富.
  • 对于PDFBox来说, 在线资源较为匮乏的情况下出现的问题也更为棘手.
  • PDFBox目前针对自定义电子签名功能的实现较为基础.
  • PDFBox仅支持将电子签名附加在PDF文件中; 比较之下,iText5和i_TEXT7不仅支持这一操作,还提供了更加灵活的绘图功能, 可直接在电子签名上绘制内容.
  • 两者采用的许可证类型存在差异: PDFbox采用的是Apache 2.0许可证(Licenses), 而itext系列则采用AGPL许可证([itextpdf.com/agpl). 需要注意的是, 尽管APACHE 2.0许可证允许商业用途,但AGPL仅在非商业场景下可自由使用.

基于此,在这里松哥将利用 iText5 版本向大家详细讲解如何为 PDF 文件添加签名。

二 实战

2.1 生成数字证书

首先我们需要生成一个数字证书。

该数字证书我们可以基于 JDK 提供的内置功能进行生成,在实践操作中,本节中将由松哥带领大家编写 Java 代码实现数字证书的具体制作流程。

首先介绍 Bouncy Castle,这是一个广为人知的开源加密库。它为 Java 平台提供了丰富的密码学算法实现,包括对称加密、非对称加密、哈希算法以及数字签名等技术。该库因其广泛的算法支持和高度可靠性的特点而受到广泛信任,并被广泛应用于安全应用程序和加密通信协议中。

复制代码

xml

代码解读

复制代码

该依赖项包含了 groupId 命名为 org.bouncycastle 的包,并被包含 artifactId bcpkix-jdk15on 和 version 1.70。
该依赖项包含了 groupId 命名为 org.bouncycastle 的包,并被包含 artifactId bcprov-ext-jdk15on 和 version 1.70。

接下来我们写一个生成数字证书的工具类,如下:

复制代码

java

代码解读

复制代码

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.security.*; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; import java.util.*; /** * @author:江南一点雨 * @site:http://www.javaboy.org * @微信公众号:江南一点雨 * @github:https://github.com/lenve * @gitee:https://gitee.com/lenve */ 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<String, byte[]>(); 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, KeyPair keyPair, Map<String, byte[]> result, String certificateCRL) { ByteArrayInputStream bint = null; X509Certificate cert = null; try { PublicKey publicKey = keyPair.getPublic(); PrivateKey privateKey = keyPair.getPrivate(); Date notBefore = new Date(); Calendar rightNow = Calendar.getInstance(); rightNow.setTime(notBefore); // 日期加1年 rightNow.add(Calendar.YEAR, 1); Date notAfter = rightNow.getTime(); // 证书序列号 BigInteger serial = BigInteger.probablePrime(256, new Random()); X509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( new X500Name(issuerStr), serial, notBefore, notAfter, new X500Name(subjectStr), publicKey); JcaContentSignerBuilder jBuilder = new JcaContentSignerBuilder( "SHA1withRSA"); SecureRandom secureRandom = new SecureRandom(); jBuilder.setSecureRandom(secureRandom); ContentSigner singer = jBuilder.setProvider( new BouncyCastleProvider()).build(privateKey); // 分发点 ASN1ObjectIdentifier cRLDistributionPoints = new ASN1ObjectIdentifier( "2.5.29.31"); GeneralName generalName = new GeneralName( GeneralName.uniformResourceIdentifier, certificateCRL); GeneralNames seneralNames = new GeneralNames(generalName); DistributionPointName distributionPoint = new DistributionPointName( seneralNames); DistributionPoint[] points = new DistributionPoint[1]; points[0] = new DistributionPoint(distributionPoint, null, null); CRLDistPoint cRLDistPoint = new CRLDistPoint(points); builder.addExtension(cRLDistributionPoints, true, cRLDistPoint); // 用途 ASN1ObjectIdentifier keyUsage = new ASN1ObjectIdentifier( "2.5.29.15"); // | KeyUsage.nonRepudiation | KeyUsage.keyCertSign builder.addExtension(keyUsage, true, new KeyUsage( KeyUsage.digitalSignature | KeyUsage.keyEncipherment)); // 基本限制 X509Extension.java ASN1ObjectIdentifier basicConstraints = new ASN1ObjectIdentifier( "2.5.29.19"); builder.addExtension(basicConstraints, true, new BasicConstraints( true)); X509CertificateHolder holder = builder.build(singer); CertificateFactory cf = CertificateFactory.getInstance("X.509"); bint = new ByteArrayInputStream(holder.toASN1Structure() .getEncoded()); cert = (X509Certificate) cf.generateCertificate(bint); byte[] certBuf = holder.getEncoded(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); // 证书数据 result.put("certificateData", certBuf); //公钥 result.put("publicKey", publicKey.getEncoded()); //私钥 result.put("privateKey", privateKey.getEncoded()); //证书有效开始时间 result.put("notBefore", format.format(notBefore).getBytes("utf-8")); //证书有效结束时间 result.put("notAfter", format.format(notAfter).getBytes("utf-8")); } catch (Exception e) { e.printStackTrace(); } finally { if (bint != null) { try { bint.close(); } catch (IOException e) { } } } return cert; } public static void main(String[] args) throws Exception { // CN: 名字与姓氏 OU : 组织单位名称 // O :组织名称 L : 城市或区域名称 E : 电子邮件 // ST: 州或省份名称 C: 单位的两字母国家代码 String issuerStr = "CN=javaboy,OU=产品研发部,O=江南一点雨,C=CN,E=javaboy@gmail.com,L=华南,ST=深圳"; String subjectStr = "CN=javaboy,OU=产品研发部,O=江南一点雨,C=CN,E=javaboy@gmail.com,L=华南,ST=深圳"; String certificateCRL = "http://www.javaboy.org"; Map<String, byte[]> result = createCert("123456", issuerStr, subjectStr, certificateCRL); FileOutputStream outPutStream = new FileOutputStream("keystore.p12"); outPutStream.write(result.get("keyStoreData")); outPutStream.close(); FileOutputStream fos = new FileOutputStream(new File("keystore.cer")); fos.write(result.get("certificateData")); fos.flush(); fos.close(); } }

该工具代码将在项目根目录中创建两份关键文件实体:keystore.p12keystore.cer

其中 keystore.cer 文件常见地采用 DER 或 PEM 格式存储 X.509 公钥证书,并包含公钥信息及详细的证书所有者资料。这些资料可能包括名称、组织机构和地理位置等信息。

keystore.p12 文件是一种基于PKCS#12格式的信息交换标准文件,主要用于记录一个或多个证书及其对应的私钥. .p12 文件是一种加密的标准信息交换文件类型,默认状态下无法解密访问. 这种电子文档设计简洁明了,在不同系统和设备之间传输电子证书与私钥非常便捷.

总结下就是,.cer 文件通常只包含公钥证书,而 .p12 文件可以包含证书和私钥。

2.2 生成印章图片

接下来我们用 Java 代码绘制一个签章图片,如下:

复制代码

java

代码解读

复制代码

public class SealSample { public static void main(String[] args) throws Exception { Seal seal = new Seal(); seal.setSize(200); SealCircle sealCircle = new SealCircle(); sealCircle.setLine(4); sealCircle.setWidth(95); sealCircle.setHeight(95); seal.setBorderCircle(sealCircle); SealFont mainFont = new SealFont(); mainFont.setText("江南一点雨股份有限公司"); mainFont.setSize(22); mainFont.setFamily("隶书"); mainFont.setSpace(22.0); mainFont.setMargin(4); seal.setMainFont(mainFont); SealFont centerFont = new SealFont(); centerFont.setText("★"); centerFont.setSize(60); seal.setCenterFont(centerFont); SealFont titleFont = new SealFont(); titleFont.setText("财务专用章"); titleFont.setSize(16); titleFont.setSpace(8.0); titleFont.setMargin(54); seal.setTitleFont(titleFont); seal.draw("公章1.png"); } }

这里涉及到的一些工具类文末可以下载。

最终生成的签章图片类似下面这样:

现在万事具备,可以给 PDF 签名了。

2.3 PDF 签名

最后,我们可以通过如下代码为 PDF 进行签名。

这里我们通过 iText 来实现电子签章,因此需要先引入 iText:

复制代码

xml

代码解读

复制代码

无法对输入文本进行有效的同义改写以降低重复率

接下来对 PDF 文件进行签名:

复制代码

java

代码解读

复制代码

public class SignPdf2 { /** * @param password pkcs12证书密码 * @param keyStorePath pkcs12证书路径 * @param signPdfSrc 签名pdf路径 * @param signImage 签名图片 * @param x * @param y * @return */ public static byte[] sign(String password, String keyStorePath, String signPdfSrc, String signImage, float x, float y) { File signPdfSrcFile = new File(signPdfSrc); PdfReader reader = null; ByteArrayOutputStream signPDFData = null; PdfStamper stp = null; FileInputStream fos = null; try { BouncyCastleProvider provider = new BouncyCastleProvider(); Security.addProvider(provider); KeyStore ks = KeyStore.getInstance("PKCS12", new BouncyCastleProvider()); fos = new FileInputStream(keyStorePath); // 私钥密码 为Pkcs生成证书是的私钥密码 123456 ks.load(fos, password.toCharArray()); String alias = (String) ks.aliases().nextElement(); PrivateKey key = (PrivateKey) ks.getKey(alias, password.toCharArray()); Certificate[] chain = ks.getCertificateChain(alias); reader = new PdfReader(signPdfSrc); signPDFData = new ByteArrayOutputStream(); // 临时pdf文件 File temp = new File(signPdfSrcFile.getParent(), System.currentTimeMillis() + ".pdf"); stp = PdfStamper.createSignature(reader, signPDFData, '\0', temp, true); stp.setFullCompression(); PdfSignatureAppearance sap = stp.getSignatureAppearance(); sap.setReason("数字签名,不可改变"); // 使用png格式透明图片 Image image = Image.getInstance(signImage); sap.setImageScale(0); sap.setSignatureGraphic(image); sap.setRenderingMode(PdfSignatureAppearance.RenderingMode.GRAPHIC); // 是对应x轴和y轴坐标 sap.setVisibleSignature(new Rectangle(x, y, x + 185, y + 68), 1, UUID.randomUUID().toString().replaceAll("-", "")); stp.getWriter().setCompressionLevel(5); ExternalDigest digest = new BouncyCastleDigest(); ExternalSignature signature = new PrivateKeySignature(key, DigestAlgorithms.SHA512, provider.getName()); MakeSignature.signDetached(sap, digest, signature, chain, null, null, null, 0, MakeSignature.CryptoStandard.CADES); stp.close(); reader.close(); return signPDFData.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { if (signPDFData != null) { try { signPDFData.close(); } catch (IOException e) { } } if (fos != null) { try { fos.close(); } catch (IOException e) { } } } return null; } public static void main(String[] args) throws Exception { byte[] fileData = sign("123456", "keystore.p12", "待签名.pdf",// "公章1.png", 100, 290); FileOutputStream f = new FileOutputStream(new File("已签名.pdf")); f.write(fileData); f.close(); } }

这里所需要的参数基本上前文都提过了,不再多说。

从表面上看,签名结束之后,PDF 文件上多了一个印章,如下:

从核心内容上来看,则是该 PDF 文件新增了一个签名字段,可通过 Adobe 的 PDF 软件查看。例如:

因为显示签名的有效性未知, 由于我们使用的是自生成的数字证书; 假如从权威机构申请数字证书, 则不会显示此提示信息.

全部评论 (0)

还没有任何评论哟~