电子签章(pdf)
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, // issuer
BigInteger.valueOf(System.currentTimeMillis()), // serial number
notBefore, // start date
notAfter, // expiry date
subject, // subject
keyPair.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接口
*/
@Test
public 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 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
// 将 ZonedDateTime 转换为 Instant
Instant instant = zonedDateTime.toInstant();
// 将 Instant 转换为 Date
Date 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 {
@Override
public 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 接口
*/
@Test
public 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 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
// 将 ZonedDateTime 转换为 Instant
Instant instant = zonedDateTime.toInstant();
// 将 Instant 转换为 Date
Date 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 Linux
private 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 nonce
int nonce = RANDOM.nextInt(Integer.MAX_VALUE);
// generate TSA request
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
tsaGenerator.setCertReq(true);
ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
// get TSA response
byte[] 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.html
LOG.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#L159
if ("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 0x3d3244ef
LOG.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 nonce
int nonce = RANDOM.nextInt(Integer.MAX_VALUE);
// generate TSA request
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
tsaGenerator.setCertReq(true);
ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
// get TSA response
byte[] 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.html
LOG.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#L159
if ("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 0x3d3244ef
LOG.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.2
throw 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 established
private byte[] getTSAResponse(byte[] request) throws IOException
{
LOG.debug("Opening connection to TSA server");
// todo: support proxy servers
URLConnection 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.0
throw new UnsupportedOperationException("authentication not implemented yet");
}
// read response
OutputStream 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 algorithm
private 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
*/
@SneakyThrows
private 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 replaceUnsignedAttributes
return 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;
}
@Override
public 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 接口
*
*/
@Test
public 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 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
// 将 ZonedDateTime 转换为 Instant
Instant instant = zonedDateTime.toInstant();
// 将 Instant 转换为 Date
Date 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());
}
}