电子签章(PDF)

发布于:2025-07-30 ⋅ 阅读:(21) ⋅ 点赞:(0)

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

4.3.3. 验证

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到