当前位置: 首页 > ops >正文

电子签章(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());}}

4.3.3. 验证

在这里插入图片描述

http://www.xdnf.cn/news/16439.html

相关文章:

  • 【0基础PS】PS工具详解--选择工具--对象选择工具
  • 【Linux | 网络】传输层(UDP和TCP) - 两万字详细讲解!!
  • 利用软件定义无线USRP X410、X440 电推进无线原型设计
  • ksql连接数据库免输入密码交互
  • 设计模式(十四)行为型:职责链模式详解
  • 飞牛NAS本地化部署n8n打造个人AI工作流中心
  • 【Java系统接口幂等性解决实操】
  • SpringSecurity实战:核心配置技巧
  • 记录几个SystemVerilog的语法——时钟块和进程通信
  • 盛最多水的容器-leetcode
  • 洛谷 P10446 64位整数乘法-普及-
  • 详解力扣高频SQL50题之1164. 指定日期的产品价格【中等】
  • 3,Windows11安装docker保姆级教程
  • LeetCode 76:最小覆盖子串
  • mybatis的insert(pojo),会返回pojo吗
  • Petalinux生成文件的关系
  • 力扣面试150题--二进制求和
  • mmap机制
  • 2.qt调试日志输出
  • 《C++》STL--string详解(上)
  • vue3报错:this.$refs.** undefined
  • 在Podman/Docker容器中为Luckfox Lyra Zero W编译SDK:终极排错指南
  • Linux实战:从零搭建基于LNMP+NFS+DNS的WordPress博客系统
  • yolo11分类一键训练工具免安装环境windows版使用教程
  • 小白成长之路-Ansible自动化(一)
  • 20250707-2-Kubernetes 网络-Ingress暴露应用(http与https)_笔记
  • LeetCode 60:排列序列
  • 10.模块与包:站在巨人的肩膀上
  • MySQL ROUTER安装部署
  • 网络配置实验报告:主机间通信配置