电子签章(PDF)
电子签章(pdf)
文章目录
- 1. 概念
- 2. 实现pdf 电子签章签名
- 2.1. 环境
- 2.2. pdf 签名接口
- 3. 演示原始签名
- 3.1. 环境准备
- 3.1.1. 导入需要的库
- 3.1.2. 生成RSA证书及密钥 工具
- 3.1.3. pdf工具类
- 3.2. 演示SignatureInterface 接口
- 3.2.1. 实现SignatureInterface接口
- 3.2.2. 验证
- 3.3. 演示ExternalSigningSupport 接口
- 3.3.1. 验证
- 4. 时间戳签名
- 4.1. 时间戳TSAClient
- 4.2. 验证时间戳
- 4.3. 基于SignatureInterface 接口 时间戳签名
- 4.3.1. SignatureInterface接口 实现类
- 4.3.2. 案例
- 4.3.3. 验证
OFD 电子签章书写过后 补充下PDF 如何进行电子签章
OFD 电子签章地址:https://blog.csdn.net/qq_36838700/article/details/139145321
1. 概念
CMS 定义了一种结构化的、基于 ASN.1 编码(通常使用 DER 规则)的二进制格式,用于“打包”数字签名及其相关数据。签名是以PKCS#7格式进行签名的
2. 实现pdf 电子签章签名
2.1. 环境
Java 实现pdf 文件电子签章的库蛮多的 比较有代表的是IText 和 PDFbox
本文已 PDFbox 为例
地址:https://github.com/apache/pdfbox
2.2. pdf 签名接口
public interface SignatureInterface
{/*** 为给定内容创建cms签名** @param content is the content as a (Filter)InputStream* @return signature as a byte array* @throws IOException if something went wrong*/byte[] sign(InputStream content) throws IOException;
}
public interface ExternalSigningSupport
{/*** 获取要签名的PDF内容。使用后必须关闭获取的InputStream** @return content stream** @throws java.io.IOException if something went wrong*/InputStream getContent() throws IOException;/*** 将CMS签名字节设置为PDF** @param signature CMS signature as byte array** @throws IOException if exception occurred during PDF writing*/void setSignature(byte[] signature) throws IOException;
}
上面两个是pdf 实现签名的主要两个接口 但是需要注意使用ExternalSigningSupport 这个接口的时候 需要签完后签名结果 调用setSignature方法
3. 演示原始签名
3.1. 环境准备
3.1.1. 导入需要的库
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.dongdong</groupId><artifactId>test-file-signature</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><pdfbox-version>2.0.31</pdfbox-version></properties><dependencies><dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>${pdfbox-version}</version></dependency><<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15to18</artifactId><version>1.69</version></dependency><dependency><groupId>org.bouncycastle</groupId><artifactId>bcpkix-jdk15on</artifactId><version>1.69</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.26</version></dependency><dependency><groupId>org.ofdrw</groupId><artifactId>ofdrw-full</artifactId><version>2.3.7</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.1</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.17.2</version></dependency></dependencies></project>
3.1.2. 生成RSA证书及密钥 工具
package com.dongdong;import cn.hutool.crypto.SecureUtil;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
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.math.BigInteger;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Date;/*** @author dongdong* */public class RSAUtils {public static KeyPair generateKeyPair() {KeyPair keyPair = SecureUtil.generateKeyPair("RSA");return keyPair;}public static X509Certificate generateSelfSignedCertificate(KeyPair keyPair, X500Name subject)throws Exception {// 设置证书有效期Date notBefore = new Date();Date notAfter = new Date(notBefore.getTime() + (1000L * 60 * 60 * 24 * 365 * 10)); // 10年有效期// 创建一个自签名证书生成器JcaX509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(subject, // issuerBigInteger.valueOf(System.currentTimeMillis()), // serial numbernotBefore, // start datenotAfter, // expiry datesubject, // subjectkeyPair.getPublic()); // public key// 创建一个签名生成器ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA").setProvider(new BouncyCastleProvider()).build(keyPair.getPrivate());return new JcaX509CertificateConverter().setProvider(new BouncyCastleProvider()).getCertificate(certBuilder.build(signer));}public static void main(String[] args) throws Exception {X500Name subject = new X500Name("CN=Test RSA ");KeyPair keyPair = generateKeyPair();X509Certificate certificate = generateSelfSignedCertificate(keyPair, subject);System.out.println("certificate = " + certificate);}
}
3.1.3. pdf工具类
package com.dongdong;import cn.hutool.core.text.CharSequenceUtil;import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.util.Matrix;import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;public class PdfUtil {public static PDRectangle createSignatureRectangle(PDPage page, Rectangle2D humanRect) {float x = (float) humanRect.getX();float y = (float) humanRect.getY();float width = (float) humanRect.getWidth();float height = (float) humanRect.getHeight();PDRectangle pageRect = page.getCropBox();PDRectangle rect = new PDRectangle();// signing should be at the same position regardless of page rotation.switch (page.getRotation()) {case 90:rect.setLowerLeftY(x);rect.setUpperRightY(x + width);rect.setLowerLeftX(y);rect.setUpperRightX(y + height);break;case 180:rect.setUpperRightX(pageRect.getWidth() - x);rect.setLowerLeftX(pageRect.getWidth() - x - width);rect.setLowerLeftY(y);rect.setUpperRightY(y + height);break;case 270:rect.setLowerLeftY(pageRect.getHeight() - x - width);rect.setUpperRightY(pageRect.getHeight() - x);rect.setLowerLeftX(pageRect.getWidth() - y - height);rect.setUpperRightX(pageRect.getWidth() - y);break;case 0:default:rect.setLowerLeftX(x);rect.setUpperRightX(x + width);rect.setLowerLeftY(pageRect.getHeight() - y - height);rect.setUpperRightY(pageRect.getHeight() - y);break;}return rect;}public static InputStream createVisualSignatureTemplate(PDPage srcPage,PDRectangle rect, byte[] imageByte) throws IOException {try (PDDocument doc = new PDDocument()) {PDPage page = new PDPage(srcPage.getMediaBox());doc.addPage(page);PDAcroForm acroForm = new PDAcroForm(doc);doc.getDocumentCatalog().setAcroForm(acroForm);PDSignatureField signatureField = new PDSignatureField(acroForm);PDAnnotationWidget widget = signatureField.getWidgets().get(0);List<PDField> acroFormFields = acroForm.getFields();acroForm.setSignaturesExist(true);acroForm.setAppendOnly(true);acroForm.getCOSObject().setDirect(true);acroFormFields.add(signatureField);widget.setRectangle(rect);PDStream stream = new PDStream(doc);PDFormXObject form = new PDFormXObject(stream);PDResources res = new PDResources();form.setResources(res);form.setFormType(1);PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());float height = bbox.getHeight();Matrix initialScale = null;switch (srcPage.getRotation()) {case 90:form.setMatrix(AffineTransform.getQuadrantRotateInstance(1));initialScale = Matrix.getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());height = bbox.getWidth();break;case 180:form.setMatrix(AffineTransform.getQuadrantRotateInstance(2));break;case 270:form.setMatrix(AffineTransform.getQuadrantRotateInstance(3));initialScale = Matrix.getScaleInstance(bbox.getWidth() / bbox.getHeight(), bbox.getHeight() / bbox.getWidth());height = bbox.getWidth();break;case 0:default:break;}form.setBBox(bbox);PDAppearanceDictionary appearance = new PDAppearanceDictionary();appearance.getCOSObject().setDirect(true);PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());appearance.setNormalAppearance(appearanceStream);widget.setAppearance(appearance);try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {if (initialScale != null) {cs.transform(initialScale);}cs.fill();if (imageByte != null) {PDImageXObject img = PDImageXObject.createFromByteArray(doc, imageByte, "test");int imgHeight = img.getHeight();int imgWidth = img.getWidth();cs.saveGraphicsState();if (srcPage.getRotation() == 90 || srcPage.getRotation() == 270) {cs.transform(Matrix.getScaleInstance(rect.getHeight() / imgWidth * 1.0f, rect.getWidth() / imgHeight * 1.0f));} else {cs.transform(Matrix.getScaleInstance(rect.getWidth() / imgWidth * 1.0f, rect.getHeight() / imgHeight * 1.0f));}cs.drawImage(img, 0, 0);cs.restoreGraphicsState();}}ByteArrayOutputStream baos = new ByteArrayOutputStream();doc.save(baos);return new ByteArrayInputStream(baos.toByteArray());}}}
3.2. 演示SignatureInterface 接口
/**** 演示pdf签名 SignatureInterface接口*/@Testpublic void testRSASignTime() throws Exception {Path src = Paths.get("src/test/resources", "test.pdf");Path pngPath = Paths.get("src/test/resources", "test.png");Path outPath = Paths.get("target/test_sign.pdf");FileOutputStream outputStream = new FileOutputStream(outPath.toFile());X500Name subject = new X500Name("CN=Test RSA ");KeyPair keyPair = RSAUtils.generateKeyPair();X509Certificate cert = RSAUtils.generateSelfSignedCertificate(keyPair, subject);// 下载图片数据try (PDDocument document = PDDocument.load(src.toFile())) {// TODO 签名域的位置 可能需要再计算Rectangle2D humanRect = new Rectangle2D.Float(150, 150,80, 80);PDPage page = document.getPage(0);PDRectangle rect = PdfUtil.createSignatureRectangle(page, humanRect);// 创建数字签名对象PDSignature pdSignature = new PDSignature();pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);pdSignature.setName("123456");pdSignature.setLocation("Location 2121331");pdSignature.setReason("PDF数字签名2222");LocalDateTime localDateTime = LocalDateTime.of(2024, 10, 5, 14, 30, 45);// 选择一个时区,例如系统默认时区ZoneId zoneId = ZoneId.systemDefault();// 将 LocalDateTime 转换为 ZonedDateTimeZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);// 将 ZonedDateTime 转换为 InstantInstant instant = zonedDateTime.toInstant();// 将 Instant 转换为 DateDate date = Date.from(instant);// 创建一个 Calendar 对象并设置时间Calendar instance = Calendar.getInstance(TimeZone.getTimeZone(zoneId.getId()));instance.setTime(date);pdSignature.setSignDate(instance);// 设置签名外观SignatureOptions options = new SignatureOptions();options.setVisualSignature(PdfUtil.createVisualSignatureTemplate(page, rect, Files.readAllBytes(pngPath)));options.setPage(1);document.addSignature(pdSignature, new DefaultSignatureInterface(), options);document.saveIncremental(outputStream);System.out.println(">> 生成文件位置: " + outPath.toAbsolutePath().toAbsolutePath());}}
3.2.1. 实现SignatureInterface接口
package com.dongdong.sign;import com.dongdong.RSAUtils;
import com.dongdong.ValidationTimeStamp;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.encoders.Base64;import java.io.ByteArrayInputStream;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.Arrays;public class DefaultSignatureInterface implements SignatureInterface {@Overridepublic byte[] sign(InputStream content) throws IOException {ValidationTimeStamp validation;try {X500Name subject = new X500Name("CN=Test RSA ");KeyPair keyPair = RSAUtils.generateKeyPair();X509Certificate cert = RSAUtils.generateSelfSignedCertificate(keyPair, subject);CMSSignedDataGenerator gen = new CMSSignedDataGenerator();ContentSigner sha1Signer = new JcaContentSignerBuilder(cert.getSigAlgName()).build(keyPair.getPrivate());gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));gen.addCertificates(new JcaCertStore(Arrays.asList(cert)));CMSProcessableByteArray msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));CMSSignedData signedData = gen.generate(msg, false);return signedData).getEncoded();} catch (Exception e) {System.out.println("e = " + e);}return new byte[]{};}
}
3.2.2. 验证
3.3. 演示ExternalSigningSupport 接口
/*** 测试pdf签名 rsa ExternalSigningSupport 接口*/@Testpublic void testRSASign() throws Exception {Path src = Paths.get("src/test/resources", "test.pdf");Path pngPath = Paths.get("src/test/resources", "test.png");Path outPath = Paths.get("target/test_sign.pdf");FileOutputStream outputStream = new FileOutputStream(outPath.toFile());X500Name subject = new X500Name("CN=Test RSA ");KeyPair keyPair = RSAUtils.generateKeyPair();X509Certificate cert = RSAUtils.generateSelfSignedCertificate(keyPair, subject);// 下载图片数据try (PDDocument document = PDDocument.load(src.toFile())) {// TODO 签名域的位置 可能需要再计算Rectangle2D humanRect = new Rectangle2D.Float(150, 150,80, 80);PDPage page = document.getPage(0);PDRectangle rect = PdfUtil.createSignatureRectangle(page, humanRect);// 创建数字签名对象PDSignature pdSignature = new PDSignature();pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);pdSignature.setName("123456");pdSignature.setLocation("Location 2121331");pdSignature.setReason("PDF数字签名2222");LocalDateTime localDateTime = LocalDateTime.of(2024, 10, 5, 14, 30, 45);// 选择一个时区,例如系统默认时区ZoneId zoneId = ZoneId.systemDefault();// 将 LocalDateTime 转换为 ZonedDateTimeZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);// 将 ZonedDateTime 转换为 InstantInstant instant = zonedDateTime.toInstant();// 将 Instant 转换为 DateDate date = Date.from(instant);// 创建一个 Calendar 对象并设置时间Calendar instance = Calendar.getInstance(TimeZone.getTimeZone(zoneId.getId()));instance.setTime(date);pdSignature.setSignDate(instance);// 设置签名外观SignatureOptions options = new SignatureOptions();options.setVisualSignature(PdfUtil.createVisualSignatureTemplate(page, rect, Files.readAllBytes(pngPath)));options.setPage(1);document.addSignature(pdSignature, options);ExternalSigningSupport signingSupport = document.saveIncrementalForExternalSigning(outputStream);InputStream content = signingSupport.getContent();CMSSignedDataGenerator gen = new CMSSignedDataGenerator();ContentSigner sha1Signer = new JcaContentSignerBuilder(cert.getSigAlgName()).build(keyPair.getPrivate());gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));gen.addCertificates(new JcaCertStore(Arrays.asList(cert)));byte[] contentBytes = IOUtils.toByteArray(content);CMSProcessableByteArray msg = new CMSProcessableByteArray(contentBytes);CMSSignedData signedData = gen.generate(msg, false);signingSupport.setSignature(signedData.getEncoded());document.save(outputStream);System.out.println(">> 生成文件位置: " + outPath.toAbsolutePath().toAbsolutePath());}}
3.3.1. 验证
4. 时间戳签名
4.1. 时间戳TSAClient
/** Licensed to the Apache Software Foundation (ASF) under one or more* contributor license agreements. See the NOTICE file distributed with* this work for additional information regarding copyright ownership.* The ASF licenses this file to You under the Apache License, Version 2.0* (the "License"); you may not use this file except in compliance with* the License. You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/
package com.dongdong;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Random;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.util.Hex;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampRequest;
import org.bouncycastle.tsp.TimeStampRequestGenerator;
import org.bouncycastle.tsp.TimeStampResponse;
import org.bouncycastle.tsp.TimeStampToken;
import org.bouncycastle.tsp.TimeStampTokenInfo;/*** Time Stamping Authority (TSA) Client [RFC 3161].* @author Vakhtang Koroghlishvili* @author John Hewson*/
public class TSAClient
{private static final Log LOG = LogFactory.getLog(TSAClient.class);private final URL url;private final String username;private final String password;private final MessageDigest digest;// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linuxprivate static final Random RANDOM = new SecureRandom();/**** @param url the URL of the TSA service* @param username user name of TSA* @param password password of TSA* @param digest the message digest to use*/public TSAClient(URL url, String username, String password, MessageDigest digest){this.url = url;this.username = username;this.password = password;this.digest = digest;}public TimeStampResponse getTimeStampResponse(byte[] content) throws IOException{digest.reset();byte[] hash = digest.digest(content);// 31-bit positive cryptographic nonceint nonce = RANDOM.nextInt(Integer.MAX_VALUE);// generate TSA requestTimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();tsaGenerator.setCertReq(true);ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));// get TSA responsebyte[] encodedRequest = request.getEncoded();byte[] tsaResponse = getTSAResponse(encodedRequest);TimeStampResponse response = null;try{response = new TimeStampResponse(tsaResponse);response.validate(request);}catch (TSPException e){// You can visualize the hex with an ASN.1 Decoder, e.g. http://ldh.org/asn1.htmlLOG.error("request: " + Hex.getString(encodedRequest));if (response != null){LOG.error("response: " + Hex.getString(tsaResponse));// See https://github.com/bcgit/bc-java/blob/4a10c27a03bddd96cf0a3663564d0851425b27b9/pkix/src/main/java/org/bouncycastle/tsp/TimeStampResponse.java#L159if ("response contains wrong nonce value.".equals(e.getMessage())){LOG.error("request nonce: " + request.getNonce().toString(16));if (response.getTimeStampToken() != null){TimeStampTokenInfo tsi = response.getTimeStampToken().getTimeStampInfo();if (tsi != null && tsi.getNonce() != null){// the nonce of the "wrong" test response is 0x3d3244efLOG.error("response nonce: " + tsi.getNonce().toString(16));}}}}throw new IOException(e);}return response;}/**** @param content* @return the time stamp token* @throws IOException if there was an error with the connection or data from the TSA server,* or if the time stamp response could not be validated*/public TimeStampToken getTimeStampToken(byte[] content) throws IOException{digest.reset();byte[] hash = digest.digest(content);// 31-bit positive cryptographic nonceint nonce = RANDOM.nextInt(Integer.MAX_VALUE);// generate TSA requestTimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();tsaGenerator.setCertReq(true);ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));// get TSA responsebyte[] encodedRequest = request.getEncoded();byte[] tsaResponse = getTSAResponse(encodedRequest);TimeStampResponse response = null;try{response = new TimeStampResponse(tsaResponse);System.out.println(response);response.validate(request);}catch (TSPException e){// You can visualize the hex with an ASN.1 Decoder, e.g. http://ldh.org/asn1.htmlLOG.error("request: " + Hex.getString(encodedRequest));if (response != null){LOG.error("response: " + Hex.getString(tsaResponse));// See https://github.com/bcgit/bc-java/blob/4a10c27a03bddd96cf0a3663564d0851425b27b9/pkix/src/main/java/org/bouncycastle/tsp/TimeStampResponse.java#L159if ("response contains wrong nonce value.".equals(e.getMessage())){LOG.error("request nonce: " + request.getNonce().toString(16));if (response.getTimeStampToken() != null){TimeStampTokenInfo tsi = response.getTimeStampToken().getTimeStampInfo();if (tsi != null && tsi.getNonce() != null){// the nonce of the "wrong" test response is 0x3d3244efLOG.error("response nonce: " + tsi.getNonce().toString(16));}}}}throw new IOException(e);}TimeStampToken timeStampToken = response.getTimeStampToken();if (timeStampToken == null){// https://www.ietf.org/rfc/rfc3161.html#section-2.4.2throw new IOException("Response from " + url +" does not have a time stamp token, status: " + response.getStatus() +" (" + response.getStatusString() + ")");}return timeStampToken;}// gets response data for the given encoded TimeStampRequest data// throws IOException if a connection to the TSA cannot be establishedprivate byte[] getTSAResponse(byte[] request) throws IOException{LOG.debug("Opening connection to TSA server");// todo: support proxy serversURLConnection connection = url.openConnection();connection.setDoOutput(true);connection.setDoInput(true);connection.setRequestProperty("Content-Type", "application/timestamp-query");LOG.debug("Established connection to TSA server");if (username != null && password != null && !username.isEmpty() && !password.isEmpty()){// See https://stackoverflow.com/questions/12732422/ (needs jdk8)// or see implementation in 3.0throw new UnsupportedOperationException("authentication not implemented yet");}// read responseOutputStream output = null;try{output = connection.getOutputStream();output.write(request);}catch (IOException ex){LOG.error("Exception when writing to " + this.url, ex);throw ex;}finally{IOUtils.closeQuietly(output);}LOG.debug("Waiting for response from TSA server");InputStream input = null;byte[] response;try{input = connection.getInputStream();response = IOUtils.toByteArray(input);}catch (IOException ex){LOG.error("Exception when reading from " + this.url, ex);throw ex;}finally{IOUtils.closeQuietly(input);}LOG.debug("Received response from TSA server");return response;}// returns the ASN.1 OID of the given hash algorithmprivate ASN1ObjectIdentifier getHashObjectIdentifier(String algorithm){if (algorithm.equals("MD2")){return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md2.getId());}else if (algorithm.equals("MD5")){return new ASN1ObjectIdentifier(PKCSObjectIdentifiers.md5.getId());}else if (algorithm.equals("SHA-1")){return new ASN1ObjectIdentifier(OIWObjectIdentifiers.idSHA1.getId());}else if (algorithm.equals("SHA-224")){return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha224.getId());}else if (algorithm.equals("SHA-256")){return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha256.getId());}else if (algorithm.equals("SHA-384")){return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha384.getId());}else if (algorithm.equals("SHA-512")){return new ASN1ObjectIdentifier(NISTObjectIdentifiers.id_sha512.getId());}else{return new ASN1ObjectIdentifier(algorithm);}}
}
4.2. 验证时间戳
/** Licensed to the Apache Software Foundation (ASF) under one or more* contributor license agreements. See the NOTICE file distributed with* this work for additional information regarding copyright ownership.* The ASF licenses this file to You under the Apache License, Version 2.0* (the "License"); you may not use this file except in compliance with* the License. You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package com.dongdong;import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.List;import com.yuanfang.sdk.model.timestamp.req.TimeStampRequest;
import com.yuanfang.sdk.model.timestamp.resp.TimeStampBodyAndStampResponse;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.Attributes;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.tsp.TimeStampResponse;
import org.bouncycastle.tsp.TimeStampToken;
import org.bouncycastle.util.encoders.Base64;import static com.dongdong.DefaultTimeStampHook.client;
import static com.dongdong.DefaultTimeStampHook.createTimestampRequest;/*** This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed* TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp)** @author Others* @author Alexis Suter*/
public class ValidationTimeStamp {private TSAClient tsaClient;/*** @param tsaUrl The url where TS-Request will be done.* @throws NoSuchAlgorithmException* @throws MalformedURLException* @throws java.net.URISyntaxException*/public ValidationTimeStamp(String tsaUrl)throws NoSuchAlgorithmException, MalformedURLException, URISyntaxException {if (tsaUrl != null) {MessageDigest digest = MessageDigest.getInstance("SHA-256");this.tsaClient = new TSAClient(new URI(tsaUrl).toURL(), null, null, digest);}}/*** Creates a signed timestamp token by the given input stream.** @param content InputStream of the content to sign* @return the byte[] of the timestamp token* @throws IOException*/public byte[] getTimeStampToken(InputStream content) throws IOException {TimeStampToken timeStampToken = tsaClient.getTimeStampToken(IOUtils.toByteArray(content));return timeStampToken.getEncoded();}/*** Extend cms signed data with TimeStamp first or to all signers** @param signedData Generated CMS signed data* @return CMSSignedData Extended CMS signed data* @throws IOException*/public CMSSignedData addSignedTimeStamp(CMSSignedData signedData)throws IOException {SignerInformationStore signerStore = signedData.getSignerInfos();List<SignerInformation> newSigners = new ArrayList<>();for (SignerInformation signer : signerStore.getSigners()) {// This adds a timestamp to every signer (into his unsigned attributes) in the signature.newSigners.add(signTimeStamp(signer));}// Because new SignerInformation is created, new SignerInfoStore has to be created // and also be replaced in signedData. Which creates a new signedData object.return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));}/*** Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes.** @param signer information about signer* @return information about SignerInformation* @throws IOException*/@SneakyThrowsprivate SignerInformation signTimeStamp(SignerInformation signer)throws IOException {AttributeTable unsignedAttributes = signer.getUnsignedAttributes();ASN1EncodableVector vector = new ASN1EncodableVector();if (unsignedAttributes != null) {vector = unsignedAttributes.toASN1EncodableVector();}TimeStampToken timeStampToken = tsaClient.getTimeStampToken(signer.getSignature());TimeStampToken timeStampToken = response.getTimeStampToken();byte[] token = timeStampToken.getEncoded();ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;ASN1Encodable signatureTimeStamp = new Attribute(oid,new DERSet(ASN1Primitive.fromByteArray(timeStampToken.getEncoded())));vector.add(signatureTimeStamp);Attributes signedAttributes = new Attributes(vector);// There is no other way changing the unsigned attributes of the signer information.// result is never null, new SignerInformation always returned, // see source code of replaceUnsignedAttributesreturn SignerInformation.replaceUnsignedAttributes(signer, new AttributeTable(signedAttributes));}
}
4.3. 基于SignatureInterface 接口 时间戳签名
4.3.1. SignatureInterface接口 实现类
DefaultTimeStampSignatureInterface
package com.dongdong.sign;import com.dongdong.RSAUtils;
import com.dongdong.ValidationTimeStamp;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.util.encoders.Base64;import java.io.ByteArrayInputStream;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.util.Arrays;public class DefaultTimeStampSignatureInterface implements SignatureInterface {private final String tsaUrl;public DefaultSignatureInterface(String tsaUrl, PrivateKey privateKey, X509Certificate certificate) {this.tsaUrl = tsaUrl;}@Overridepublic byte[] sign(InputStream content) throws IOException {ValidationTimeStamp validation;try {X500Name subject = new X500Name("CN=Test RSA ");KeyPair keyPair = RSAUtils.generateKeyPair();X509Certificate cert = RSAUtils.generateSelfSignedCertificate(keyPair, subject);CMSSignedDataGenerator gen = new CMSSignedDataGenerator();ContentSigner sha1Signer = new JcaContentSignerBuilder(cert.getSigAlgName()).build(keyPair.getPrivate());gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));gen.addCertificates(new JcaCertStore(Arrays.asList(cert)));CMSProcessableByteArray msg = new CMSProcessableByteArray(IOUtils.toByteArray(content));CMSSignedData signedData = gen.generate(msg, false);validation = new ValidationTimeStamp(tsaUrl);return validation.addSignedTimeStamp(signedData).getEncoded();} catch (Exception e) {System.out.println("e = " + e);}return new byte[]{};}
}
4.3.2. 案例
/*** pdf 签名(时间戳)SignatureInterface 接口* */@Testpublic void testRSASignTime() throws Exception {Path src = Paths.get("src/test/resources", "test.pdf");Path pngPath = Paths.get("src/test/resources", "test.png");Path outPath = Paths.get("target/test_sign.pdf");FileOutputStream outputStream = new FileOutputStream(outPath.toFile());X500Name subject = new X500Name("CN=Test RSA ");KeyPair keyPair = RSAUtils.generateKeyPair();X509Certificate cert = RSAUtils.generateSelfSignedCertificate(keyPair, subject);try (PDDocument document = PDDocument.load(src.toFile())) {// TODO 签名域的位置 可能需要再计算Rectangle2D humanRect = new Rectangle2D.Float(150, 150,80, 80);PDPage page = document.getPage(0);PDRectangle rect = PdfUtil.createSignatureRectangle(page, humanRect);// 创建数字签名对象PDSignature pdSignature = new PDSignature();pdSignature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);pdSignature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);pdSignature.setName("123456");pdSignature.setLocation("Location 2121331");pdSignature.setReason("PDF数字签名2222");LocalDateTime localDateTime = LocalDateTime.of(2024, 10, 5, 14, 30, 45);// 选择一个时区,例如系统默认时区ZoneId zoneId = ZoneId.systemDefault();// 将 LocalDateTime 转换为 ZonedDateTimeZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);// 将 ZonedDateTime 转换为 InstantInstant instant = zonedDateTime.toInstant();// 将 Instant 转换为 DateDate date = Date.from(instant);// 创建一个 Calendar 对象并设置时间Calendar instance = Calendar.getInstance(TimeZone.getTimeZone(zoneId.getId()));instance.setTime(date);pdSignature.setSignDate(instance);// 设置签名外观SignatureOptions options = new SignatureOptions();options.setVisualSignature(PdfUtil.createVisualSignatureTemplate(page, rect, Files.readAllBytes(pngPath)));options.setPage(1);// https://freetsa.org/tsr 时间戳服务器的地址document.addSignature(pdSignature, new DefaultTimeStampSignatureInterface("https://freetsa.org/tsr"), options);document.saveIncremental(outputStream);System.out.println(">> 生成文件位置: " + outPath.toAbsolutePath().toAbsolutePath());}}