在Go(Golang)中,我需要签署一份pdf文档,但与其他语言不同的是,没有库可以让这项工作变得更容易.我已经找到了几个付费的,但它们不是一个选项.

首先,我有一个PKCS证书(.p12),我已经使用以下包从中提取了私钥和x509证书:https://pkg.go.dev/software.sslmate.com/src/go-pkcs12

但是当我想签署pdf文档时,我被卡住了,因为我不知道如何正确地将参数传递给执行此类操作的函数.使用的包装是https://pkg.go.dev/github.com/digitorus/pdfsign

我的完整代码是:

package main

import (
    "crypto"
    "fmt"
    "os"
    "time"

    "github.com/digitorus/pdf"
    "github.com/digitorus/pdfsign/revocation"
    "github.com/digitorus/pdfsign/sign"
    gopkcs12 "software.sslmate.com/src/go-pkcs12"
)

func main() { 
    certBytes, err := os.ReadFile("certificate.p12") 

    if err != nil { 
        fmt.Println(err) 
        return
    }

    privateKey, certificate, chainCerts, err := gopkcs12.DecodeChain(certBytes, "MyPassword")

    if err != nil {
        fmt.Println(err)
        return
    }

    input_file, err := os.Open("input-file.pdf")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer input_file.Close()
        
    output_file, err := os.Create("output-file.pdf")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer output_file.Close()
    
    finfo, err := input_file.Stat()
    if err != nil {
        fmt.Println(err)
        return
    }
    size := finfo.Size()
    
    rdr, err := pdf.NewReader(input_file, size)
    if err != nil {
        fmt.Println(err)
        return
    }

    err = sign.Sign(input_file, output_file, rdr, size, sign.SignData{
    Signature: sign.SignDataSignature{
        Info: sign.SignDataSignatureInfo{
            Name:        "John Doe",
            Location:    "Somewhere on the globe",
            Reason:      "My season for siging this document",
            ContactInfo: "How you like",
            Date:        time.Now().Local(),
        },
        CertType:   sign.CertificationSignature,
        DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms,
        },
        Signer:            privateKey,    // crypto.Signer
        Digest算法rithm:   crypto.SHA256, // hash algorithm for the digest creation
        Certificate:       certificate,   // x509.Certificate
        CertificateChains: chainCerts,    // 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 {
        fmt.Println(err)
    } else {
        fmt.Println("Signed PDF written to output.pdf")
    }
}

准确地说,他们是我的问题所在的签名者和证书链参数.我不知道如何正确使用Private Key和chainCerts变量.

错误消息包括:

  • 无法使用PriateKey(接口类型为{}的变量)作为加密. struct 文本中的Signer值:接口{}未实现加密.Signer(缺少方法Public)
  • 不能将chaincerfates(类型为[]*x509证书的变量)用作[][]*x509 struct 文本中的证书值

我是这种语言的新手,所以我仍然不知道深入的概念和数据类型.

我感谢你告诉我我还应该做些什么,或者是为了达成一个成功的结论而遗漏了哪些步骤.或者是否有人知道我如何基于PKCS证书来签署PDF.

推荐答案

使用数字签名对PDF进行签名包括使用公钥加密来生成一对密钥.私钥被用来加密与签名相关的数据,并且只有签名者才能访问它,以及用于解密签名数据以用于验证目的的公钥,如果不是由可信的证书颁发机构发布的,则所述公钥证书必须被添加到证书存储中以使其可信. 在给定的示例中,签名数据存储在一个名为sign.SignData的 struct 中,该 struct 是pdfsign库的一部分,需要一个x509证书和一个实现加密.Signer接口的签名者.

The first step是使用GO标准库中的加密/ECDSA包来生成一对密钥.GenerateKey会将私钥和公钥保存到Private Key变量中.这个返回的PriateKey变量实现了加密的.Signer接口,这是解决您当前面临的问题所必需的.

您可以通过阅读ecdsa.go代码第142行来判断这一点.

您当前使用的是gopkcs12.DecodeChain,它返回私钥,但它没有实现加密.Signer接口,因此出现错误.您可能需要实现一个定制的,但这是另一个问题.

概念:

ECDSA代表椭圆曲线数字签名算法.它是一种用于数字签名的公钥加密算法.有关更多信息,请参考GO标准库文档和NIST网站.

NIST P-384:P-384是美国国家标准与技术研究所(NIST)推荐的椭圆曲线之一,密钥长度为384位.有关数字签名和更多推荐的椭圆曲线的更多信息,请参考NIST网站.我用P-384作为工作液.

The second step将使用GO标准库中的CRYPTO/X509包来生成一个x509证书和一个带有它的链构建器的证书链.这些是您在问题中请求帮助的特定变量,您可以在错误消息中清楚地看到它们不是预期的类型.只需遵循lib指令并使用x509.Cerficate.Verify(),就像我在工作解决方案中所做的那样,这将为您返回正确的类型[][]*x509证书.

有关更多信息,请参考GO标准库文档.

The third step将打开输入pdf文件,并使用GO标准库中的os包创建输出pdf文件.

The fourth step实际上是使用Digitorus/pdfsign库对pdf文件进行签名.

这是我今天编码和测试的有效解决方案,目的是让您重回正轨,进行一些研究并根据您的需求进行修改:

package main

import (
    "crypto"
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/x509"
    "crypto/x509/pkix"
    "fmt"
    "github.com/digitorus/pdf"
    "github.com/digitorus/pdfsign/revocation"
    "github.com/digitorus/pdfsign/sign"
    "log"
    "math/big"
    "os"
    "time"
)

func main() {
    // First step

    privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)

    if err != nil {
        panic(err)
    }

    // Second step

    x509RootCertificate := &x509.Certificate{
        SerialNumber: big.NewInt(2023),
        Subject: pkix.Name{
            Organization:  []string{"Your Organization"},
            Country:       []string{"Your Country"},
            Province:      []string{"Your Province"},
            Locality:      []string{"Your Locality"},
            StreetAddress: []string{"Your Street Address"},
            PostalCode:    []string{"Your Postal Code"},
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(10, 0, 0),
        IsCA:                  true,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
        KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
    }

    rootCertificateBytes, err := x509.CreateCertificate(rand.Reader, x509RootCertificate, x509RootCertificate, &privateKey.PublicKey, privateKey)

    if err != nil {
        panic(err)
    }

    rootCertificate, err := x509.ParseCertificate(rootCertificateBytes)

    if err != nil {
        panic(err)
    }

    roots := x509.NewCertPool()

    roots.AddCert(rootCertificate)

    certificateChain, err := rootCertificate.Verify(x509.VerifyOptions{
        Roots: roots,
    })

    if err != nil {
        panic(err)
    }

    // Third step

    outputFile, err := os.Create("output.pdf")

    if err != nil {
        panic(err)
    }

    defer func(outputFile *os.File) {
        err = outputFile.Close()

        if err != nil {
            log.Println(err)
        }

        fmt.Println("output file closed")
    }(outputFile)

    inputFile, err := os.Open("input.pdf")

    if err != nil {
        panic(err)
    }

    defer func(inputFile *os.File) {
        err = inputFile.Close()

        if err != nil {
            log.Println(err)
        }

        fmt.Println("input file closed")
    }(inputFile)

    fileInfo, err := inputFile.Stat()

    if err != nil {
        panic(err)
    }

    size := fileInfo.Size()

    pdfReader, err := pdf.NewReader(inputFile, size)

    if err != nil {
        panic(err)
    }

    // Fourth step

    err = sign.Sign(inputFile, outputFile, pdfReader, size, sign.SignData{
        Signature: sign.SignDataSignature{
            Info: sign.SignDataSignatureInfo{
                Name:        "Your name",
                Location:    "Your location",
                Reason:      "Your reason",
                ContactInfo: "Your contact info",
                Date:        time.Now().Local(),
            },
            CertType:   sign.CertificationSignature,
            DocMDPPerm: sign.AllowFillingExistingFormFieldsAndSignaturesPerms,
        },
        Signer:            privateKey,       // crypto.Signer
        Digest算法rithm:   crypto.SHA256,    // hash algorithm for the digest creation
        Certificate:       rootCertificate,  // x509.Certificate
        CertificateChains: certificateChain, // x509.Certificate.Verify()
        TSA: sign.TSA{
            URL:      "",
            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 {
        log.Println(err)
    } else {
        log.Println("pdf signed")
    }
}

结果:

go run main.go

2023/12/01 21:53:37 pdf signed
input file closed
output file closed

Go相关问答推荐

golang 的条件储存库

Term~T中的类型不能是类型参数,但可以是引用类型参数的切片

读取JSON数据并在网页上显示

如何在使用中介资源时处理函数中的`defer`

重新赋值变量时未清除动态类型-这是错误吗?

Go安装成功但没有输出简单的Hello World

将字符串格式的x509证书生成主题名称

如何判断范围内的字段?

在嵌套模板中使用变量,它也被定义为 go 模板中的变量?

Gorm 预加载给出了模糊的列错误

在恒等函数中将类型 T 转换为类型 U

如何将文件上传到 Google Drive,并与使用服务帐户和 Golang 的任何人共享

未定义 protoc protoc-gen-go 时间戳

每次有人进入我的网站时如何运行特定功能?

Ginkgo/Gomega panic 测试失败

如何在gorm中处理多个查询

Go lang - 惯用的默认后备

有没有办法在一个 goroutine 返回后延迟后取消上下文?

Go模板中的浮点除法

如何使用 fyne 避免 GUI 应用程序中的循环依赖?