V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
cdx
V2EX  ›  Go 编程语言

求助:怎么使用 go 实现 pdf 的数字签名与校验

  •  
  •   cdx · 12 天前 · 1018 次点击

    目前有业务需要给 pdf 合同进行电子签名,目前使用的是 https://github.com/digitorus/pdfsign 这个库,但是碰到一个问题,首先是能够正常签名,但是校验时出现了一些问题。

    当对已经签名过的 pdf 文件在末尾添加几个随机字节,此时使用 pdfsign 去检测,是没有办法检测到该文件已经被篡改了,在 adobe reader 中是能够检测到该文件有问题。

    这里不讨论公司是否拥有这个资质的问题

    有几个需要帮助的地方:

    1. 目前市面上常见的 pdf 签名方式是否与 pdfsign 类似
    2. 怎么解决这个问题
    3. 是否有其他依赖库实现 pdf 的数字签名与校验

    代码:

    package main
    
    import (
    	"crypto"
    	"crypto/rsa"
    	"crypto/x509"
    	"encoding/json"
    	"encoding/pem"
    	"errors"
    	"github.com/digitorus/pdf"
    	"github.com/digitorus/pdfsign/revocation"
    	"github.com/digitorus/pdfsign/sign"
    	"github.com/digitorus/pdfsign/verify"
    	"log"
    	"os"
    	"time"
    )
    
    func main() {
    	err := run("a.pdf", "b.pdf")
    	if err != nil {
    		panic(err)
    	}
    	data, err := os.ReadFile("b.pdf")
    	if err != nil {
    		panic(err)
    	}
    	data = append(data, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}...)
    	err = os.WriteFile("c.pdf", data, 0644)
    	if err != nil {
    		panic(err)
    	}
    	verifyPdf("c.pdf")
    }
    
    
    func verifyPdf(pdfName string) {
    	input_file, err := os.Open(pdfName)
    	if err != nil {
    		panic(err)
    	}
    	defer input_file.Close()
    
    	resp, err := verify.File(input_file)
    	if err != nil {
    		panic(err)
    	}
    	jsonData, err := json.MarshalIndent(resp, "", "\t")
    	if err != nil {
    		panic(err)
    	}
    	// 将 jsonData 的数据写入文件
    	err = os.WriteFile("verify.json", jsonData, 0644)
    	return
    }
    
    func run(input, output string) error {
    	input_file, err := os.Open(input)
    	if err != nil {
    		panic(err)
    	}
    	defer input_file.Close()
    
    	output_file, err := os.Create(output)
    	if err != nil {
    		panic(err)
    	}
    	defer output_file.Close()
    
    	finfo, err := input_file.Stat()
    	if err != nil {
    		panic(err)
    	}
    	size := finfo.Size()
    
    	rdr, err := pdf.NewReader(input_file, size)
    	if err != nil {
    		panic(err)
    	}
    
    	certificate_data, err := os.ReadFile("certificate.crt")
    	if err != nil {
    		panic(err)
    	}
    	certificate_data_block, _ := pem.Decode(certificate_data)
    	if certificate_data_block == nil {
    		//log.Fatal(errors.New("failed to parse PEM block containing the certificate"))
    		panic(err)
    	}
    
    	cert, err := x509.ParseCertificate(certificate_data_block.Bytes)
    	if err != nil {
    		panic(err)
    	}
    
    	privateKeyFs, err := os.ReadFile("private_key.pem")
    	if err != nil {
    		panic(err)
    	}
    	key_data_block, _ := pem.Decode(privateKeyFs)
    	if key_data_block == nil {
    		panic(errors.New("failed to parse PEM block containing the private key"))
    	}
    	// 尝试解析 PKCS#1 格式的私钥
    	pkey, err := x509.ParsePKCS1PrivateKey(key_data_block.Bytes)
    	if err != nil {
    		var t any
    		t, err = x509.ParsePKCS8PrivateKey(key_data_block.Bytes)
    		pkey = t.(*rsa.PrivateKey)
    		if err != nil {
    			panic(err)
    		}
    	}
    
    	certificate_chains := make([][]*x509.Certificate, 0)
    	err = sign.Sign(input_file, output_file, rdr, size, sign.SignData{
    		Signature: sign.SignDataSignature{
    			Info: sign.SignDataSignatureInfo{
    				Name:        "xx",
    				Location:    "xx",
    				Reason:      "xx",
    				ContactInfo: "xxx",
    				Date:        time.Now().Local(),
    			},
    			CertType:   sign.CertificationSignature,
    			DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms,
    		},
    		Signer:            pkey,               // crypto.Signer
    		DigestAlgorithm:   crypto.SHA256,      // hash algorithm for the digest creation
    		Certificate:       cert,               // x509.Certificate
    		CertificateChains: certificate_chains, // x509.Certificate.Verify()
    		TSA: sign.TSA{
    			URL:      "https://freetsa.org/tsr",
    			Username: "",
    			Password: "",
    		},
    
    		// The follow options are likely to change in a future release
    		//
    		// cache revocation data when bulk signing
    		RevocationData: revocation.InfoArchival{},
    		// custom revocation lookup
    		RevocationFunction: sign.DefaultEmbedRevocationStatusFunction,
    	})
    	if err != nil {
    		panic(err)
    	} else {
    		log.Println("Signed PDF written to " + output)
    	}
    	return nil
    }
    
    /*
    自签私钥与证书生成
    1. 生成私钥
    openssl genpkey -algorithm RSA -out private_key.pem
    2. 创建证书签名请求 (CSR)
    openssl req -new -key private_key.pem -out csr.pem
    3. 签发自签证书
    openssl x509 -req -days 365 -in csr.pem -signkey private_key.pem -out certificate.crt
    */
    
    
    5 条回复    2024-09-24 23:21:17 +08:00
    tool2dx
        1
    tool2dx  
       12 天前
    "当对已经签名过的 pdf 文件在末尾添加几个随机字节"

    写点代码去掉就可以了。pdf 可以用软件遍历出文件结尾地址。后面如果是随机字节,并不会影响到签名本体,这部分内容本来就是多余的,又不会去读。
    DefoliationM
        2
    DefoliationM  
       12 天前 via Android
    试试随机改几个字节,而不是添加到末尾?
    cdx
        3
    cdx  
    OP
       12 天前 via iPhone
    @DefoliationM
    @tool2dx
    直接对文档进行修改,检验的检测结果还是能通过
    xxmaqzas
        4
    xxmaqzas  
       11 天前
    不能把签过的 md5 存数据库么,直接校验文件的 md5 值
    cdx
        5
    cdx  
    OP
       11 天前
    @xxmaqzas 这样得到了文档的人没办法检测这个文档是否已经被篡改了,还需要借助系统
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2215 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 08:29 · PVG 16:29 · LAX 01:29 · JFK 04:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.