OpenPDF的学习文档(第十二章·PDF签名)

什么是数字签名

  • 本质是经过加密计算的一串数据。
  • 提供以下三个保障:
    • 身份真实性: 可验证的,是经过 XXX签署的。
    • 不可否认性: XXX签署过的PDF,XXX不可以否认。
    • 内容完整性: 签名之后,PDF再次被修改,针对XXX的验签会失败。
  • 签名的体现
    • 真正的签名数据:隐藏在 PDF 文件底层结构里的二进制数据包,不可见。
    • 所有在页面上能看到的签名,都是外观表达,本质上是PDF的一个注释
    • 图片:扫描图,电子印章。
    • 一段文字:签署人+签署时间。
    • 图文混合:印章图片+签署人+签署时间。
    • 完全不可见:可以不在页面上展示。

证书、加密、国密算法

  • 需要了解的东西
    • 证书链、信任源(Certificate Chain / CA Root)

    • 公钥证书(X.509 Digital Certificate)

    • 私钥(Private Key)

    • 完整链条:

      • 生成公私密钥对:常用OpenSSL
        • 如果Windows没有安装OpenSSL
          • # 更新索引
            winget source update
            # 搜索openssl,找到对应的ID
            winget search openssl
            # 安装 OpenSSL
            winget install ShiningLight.OpenSSL.Light
            # 查看是否安装成功
            openssl version
            
          • 如果不行,手动把环境变量设置到Path中。
          • 一般是这个地址:C:\Program Files\OpenSSL-Win64\bin
        • 生成 RSA 2048 位的私钥文件:
          • openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out C:/TMP/private_key.pem
          • image
        • 从私钥中提取对应的公钥文件
          • openssl pkey -in C:/TMP/private_key.pem -pubout -out C:/TMP/public_key.pem
          • image
        • 国密
          • # 1. 生成 SM2 椭圆曲线私钥
            openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:SM2 -out sm2_private_key.pem
            
            # 2. 从 SM2 私钥中提取 SM2 公钥
            openssl pkey -in sm2_private_key.pem -pubout -out sm2_public_key.pem
            
            # 3. (可选)使用 SM2 私钥直接生成带 SM3 摘要的 CSR 申请文件
            openssl req -new -key sm2_private_key.pem -sm3 -out sm2_request.csr -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=ZhangSan"
            
      • 基于私钥生成CSR申请文件:
        • 交互式生成
        • openssl req -new -key C:/TMP/private_key.pem -out C:/TMP/request.csr
        • image
        • image
        • 信息一定要认真填写,后面向CA申请证书是要花钱的。
        • 验证一下文件request.csr是否正确
        • openssl req -in C:/TMP/request.csr -noout -text
        • image
      • 千万不要将private_key.pem泄露出去
      • 将CSR文件交给CA:
        • 如果是面向公网,就必须找到公信 CA,交钱办理证书
          • image
        • 如果是面向系统内部,可内部生成。
          • 使用OpenSSL充当CA
          • openssl genrsa -out C:/TMP/ca_private.key 2048 CA的私钥
          • openssl req -x509 -new -nodes -key C:/TMP/ca_private.key -sha256 -days 3650 -out C:/TMP/ca_cert.crt CA交互式生成crt文件。
          • image
          • 为上述生成的request.csr使用ca_cert.crt + ca_private.key来签发证书。
          • openssl x509 -req -in C:/TMP/request.csr -CA C:/TMP/ca_cert.crt -CAkey C:/TMP/ca_private.key -CAcreateserial -out C:/TMP/user_cert.crt -days 365 -sha256
          • 自建CA成功,生成证书user_cert.crt
          • 将自建的ca_cert.crt根证书导入到系统,否则PDF阅读器不认这个签名。
          • image
      • 将私钥+CA颁发的证书合并为PFX/P12:
        • 散碎的文件不能直接被Java识别,需要打包为.p12格式。
        • openssl pkcs12 -export -out C:/TMP/signer.p12 -inkey C:/TMP/private_key.pem -in C:/TMP/user_cert.crt -certfile C:/TMP/ca_cert.crt
        • 会要求输入密码,需要牢记这个密码。
        • 最后得到signer.p12,使用它来进行签名验签。

添加签名

@Slf4j
public class SignUtils {

    private static final String p12Path = "C:/TMP/sign/signer.p12";
    private static final char[] p12Password = "liang".toCharArray();
    private static final PrivateKey pk;
    private static final Certificate[] chain;
    static {
        try {
            // 注册 Bouncy Castle 密码学提供者
            Security.addProvider(new BouncyCastleProvider());
            // 加载 PKCS12 (.p12) 密钥库,获取私钥和证书链
            KeyStore ks = KeyStore.getInstance("PKCS12");
            ks.load(Files.newInputStream(Paths.get(p12Path)), p12Password);
            String alias = ks.aliases().nextElement();
            pk = (PrivateKey) ks.getKey(alias, p12Password);
            chain = ks.getCertificateChain(alias);
        } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
                 UnrecoverableKeyException e) {
            throw new RuntimeException(e);
        }
    }


    public static byte[] sgin(PdfReader reader) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        // 对文档签名
        PdfStamper stamper = PdfStamper.createSignature(reader, os, "\u0000", null, true);
        // 添加可见属性
        PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
        // 设置密码参数
        appearance.setCrypto(pk, chain, null, PdfSignatureAppearance.WINCER_SIGNED);
        // 签名理由
        appearance.setReason("合同签署 / 文件盖章");
        // 签名地点
        appearance.setLocation("北京");
        // 设置可见盖章框的范围
        appearance.setVisibleSignature(new Rectangle(100, 100, 300, 200), 1, "Signature1");
        // 完成签名并保存文件
        stamper.close();
        return os.toByteArray();
    }
}
@Test
public void readPDF() throws IOException {
    PdfReader reader = new PdfReader("C:/TMP/sign/abc.pdf");
    byte[] bytes = SignUtils.sgin(reader);
    reader.close();
    Files.write(Paths.get("C:/TMP/sign/abc_sign.pdf"),bytes);
}

image

public static byte[] sgin(PdfReader reader, Image image) throws IOException {
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    // 对文档签名
    PdfStamper stamper = PdfStamper.createSignature(reader, os, "\u0000", null, true);
    // 添加可见属性
    PdfSignatureAppearance appearance = stamper.getSignatureAppearance();

    // 签名理由
    appearance.setReason("合同签署 / 文件盖章");
    // 签名地点
    appearance.setLocation("北京");
    // 设置密码参数
    appearance.setCrypto(pk, chain, null, PdfSignatureAppearance.SELF_SIGNED);
    if (image != null) {
        appearance.setSignatureGraphic(image);
        // 图片和描述信息
        appearance.setRender(PdfSignatureAppearance.SignatureRenderGraphic);
        String uniqueFieldName = "Signature_" + System.currentTimeMillis();
        // 设置可见盖章框的范围
        appearance.setVisibleSignature(new Rectangle(100, 100, 300, 200), 1, uniqueFieldName);
    }
    // 完成签名并保存文件
    stamper.close();
    return os.toByteArray();
}

image

验证签名

public static void 验签 () throws IOException, SignatureException{

    // 读取PDF
    PdfReader reader = new PdfReader(Files.newInputStream(Paths.get("C:/TMP/007.PDF")));
    // 获取AcroFields的只读版本
    AcroFields fields = reader.getAcroFields();
    // 获取已签名的字段名称
    List<String> names = fields.getSignedFieldNames();

    System.out.println("签名数量: " + names.size());

    for (String name : names) {
        System.out.println("======== 签名域: " + name + " ========");

        // 是否是整个文档的最后一次修改
        System.out.println("覆盖整个文档: " +
                fields.signatureCoversWholeDocument(name));

        // 取出签名对象
        PdfPKCS7 pkcs7 = fields.verifySignature(name);

        // 验证签名值是否正确(最关键)
        boolean integrity = pkcs7.verify();
        System.out.println("签名完整性: " + integrity);

        // 签名时间
        Calendar signDate = pkcs7.getSignDate();
        System.out.println("签名时间: " + signDate.getTime());

        // 签名证书
        X509Certificate cert = pkcs7.getSigningCertificate();
        System.out.println("签名证书: " + cert.getSubjectX500Principal());
        try {
            cert.checkValidity();
        } catch (CertificateExpiredException e) {
            System.out.println("签名证书已过期: " + cert.getSubjectX500Principal());
        } catch (CertificateNotYetValidException e) {
            System.out.println("签名证书无效: " + cert.getSubjectX500Principal());
        }

        // 是否包含时间戳
        if (pkcs7.getTimeStampDate() != null) {
            System.out.println("时间戳时间: " +
                    pkcs7.getTimeStampDate().getTime());
        }
        //
    }
    reader.close();

}
posted @ 2026-08-01 15:23  铁锅炖大鹅ing  阅读(20)  评论(0)    收藏  举报