Files
pdfsign/sign/sign_test.go
2025-05-01 11:31:06 +02:00

673 lines
18 KiB
Go

package sign
import (
"crypto"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"time"
"github.com/digitorus/pdf"
"github.com/digitorus/pdfsign/revocation"
"github.com/digitorus/pdfsign/verify"
"github.com/mattetti/filebuffer"
)
const signCertPem = `-----BEGIN CERTIFICATE-----
MIICjDCCAfWgAwIBAgIUEeqOicMEtCutCNuBNq9GAQNYD10wDQYJKoZIhvcNAQEL
BQAwVzELMAkGA1UEBhMCTkwxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAoM
CURpZ2l0b3J1czEfMB0GA1UEAwwWUGF1bCB2YW4gQnJvdXdlcnNoYXZlbjAgFw0y
NDExMTMwOTUxMTFaGA8yMTI0MTAyMDA5NTExMVowVzELMAkGA1UEBhMCTkwxEzAR
BgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAoMCURpZ2l0b3J1czEfMB0GA1UEAwwW
UGF1bCB2YW4gQnJvdXdlcnNoYXZlbjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC
gYEAmrvrZiUZZ/nSmFKMsQXg5slYTQjj7nuenczt7KGPVuGA8nNOqiGktf+yep5h
2r87jPvVjVXjJVjOTKx9HMhaFECHKHKV72iQhlw4fXa8iB1EDeGuwP+pTpRWlzur
Q/YMxvemNJVcGMfTE42X5Bgqh6DvkddRTAeeqQDBD6+5VPsCAwEAAaNTMFEwHQYD
VR0OBBYEFETizi2bTLRMIknQXWDRnQ59xI99MB8GA1UdIwQYMBaAFETizi2bTLRM
IknQXWDRnQ59xI99MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADgYEA
OBng+EzD2xA6eF/W5Wh+PthE1MpJ1QvejZBDyCOiplWFUImJAX39ZfTo/Ydfz2xR
4Jw4hOF0kSLxDK4WGtCs7mRB0d24YDJwpJj0KN5+uh3iWk5orY75FSensfLZN7YI
VuUN7Q+2v87FjWsl0w3CPcpjB6EgI5QHsNm13bkQLbQ=
-----END CERTIFICATE-----`
const signKeyPem = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCau+tmJRln+dKYUoyxBeDmyVhNCOPue56dzO3soY9W4YDyc06q
IaS1/7J6nmHavzuM+9WNVeMlWM5MrH0cyFoUQIcocpXvaJCGXDh9dryIHUQN4a7A
/6lOlFaXO6tD9gzG96Y0lVwYx9MTjZfkGCqHoO+R11FMB56pAMEPr7lU+wIDAQAB
AoGADPlKsILV0YEB5mGtiD488DzbmYHwUpOs5gBDxr55HUjFHg8K/nrZq6Tn2x4i
iEvWe2i2LCaSaBQ9H/KqftpRqxWld2/uLbdml7kbPh0+57/jsuZZs3jlN76HPMTr
uYcfG2UiU/wVTcWjQLURDotdI6HLH2Y9MeJhybctywDKWaECQQDNejmEUybbg0qW
2KT5u9OykUpRSlV3yoGlEuL2VXl1w5dUMa3rw0yE4f7ouWCthWoiCn7dcPIaZeFf
5CoshsKrAkEAwMenQppKsLk62m8F4365mPxV/Lo+ODg4JR7uuy3kFcGvRyGML/FS
TB5NI+DoTmGEOZVmZeLEoeeSnO0B52Q28QJAXFJcYW4S+XImI1y301VnKsZJA/lI
KYidc5Pm0hNZfWYiKjwgDtwzF0mLhPk1zQEyzJS2p7xFq0K3XqRfpp3t/QJACW77
sVephgJabev25s4BuQnID2jxuICPxsk/t2skeSgUMq/ik0oE0/K7paDQ3V0KQmMc
MqopIx8Y3pL+f9s4kQJADWxxuF+Rb7FliXL761oa2rZHo4eciey2rPhJIU/9jpCc
xLqE5nXC5oIUTbuSK+b/poFFrtjKUFgxf0a/W2Ktsw==
-----END RSA PRIVATE KEY-----`
func loadCertificateAndKey(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) {
certificate_data_block, _ := pem.Decode([]byte(signCertPem))
if certificate_data_block == nil {
t.Fatalf("failed to parse PEM block containing the certificate")
}
cert, err := x509.ParseCertificate(certificate_data_block.Bytes)
if err != nil {
t.Fatalf("%s", err.Error())
}
key_data_block, _ := pem.Decode([]byte(signKeyPem))
if key_data_block == nil {
t.Fatalf("failed to parse PEM block containing the private key")
}
pkey, err := x509.ParsePKCS1PrivateKey(key_data_block.Bytes)
if err != nil {
t.Fatalf("%s", err.Error())
}
return cert, pkey
}
func verifySignedFile(t *testing.T, tmpfile *os.File, originalFileName string) {
_, err := verify.File(tmpfile)
if err != nil {
t.Fatalf("%s: %s", tmpfile.Name(), err.Error())
err2 := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName)
if err2 != nil {
t.Error(err2)
}
}
}
func TestReaderCanReadPDF(t *testing.T) {
files, err := os.ReadDir("../testfiles")
if err != nil {
t.Fatalf("%s", err.Error())
}
for _, f := range files {
if filepath.Ext(f.Name()) != ".pdf" {
continue
}
t.Run(f.Name(), func(st *testing.T) {
st.Parallel()
input_file, err := os.Open("../testfiles/" + f.Name())
if err != nil {
st.Fatalf("%s: %s", f.Name(), err.Error())
}
defer input_file.Close()
finfo, err := input_file.Stat()
if err != nil {
st.Fatalf("%s: %s", f.Name(), err.Error())
}
size := finfo.Size()
_, err = pdf.NewReader(input_file, size)
if err != nil {
st.Fatalf("%s: %s", f.Name(), err.Error())
}
})
}
}
func TestSignPDF(t *testing.T) {
_ = os.RemoveAll("../testfiles/failed/")
_ = os.MkdirAll("../testfiles/failed/", 0o777)
files, err := os.ReadDir("../testfiles/")
if err != nil {
t.Fatalf("%s", err.Error())
}
cert, pkey := loadCertificateAndKey(t)
certificateChains := [][]*x509.Certificate{}
for _, f := range files {
if filepath.Ext(f.Name()) != ".pdf" {
continue
}
t.Run(f.Name(), func(st *testing.T) {
outputFile, err := os.CreateTemp("", fmt.Sprintf("%s_%s_", t.Name(), f.Name()))
if err != nil {
st.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(outputFile.Name())
}
err = SignFile("../testfiles/"+f.Name(), outputFile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "Test",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: CertificationSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Signer: pkey,
Certificate: cert,
CertificateChains: certificateChains,
TSA: TSA{
URL: "http://timestamp.digicert.com",
},
RevocationData: revocation.InfoArchival{},
RevocationFunction: DefaultEmbedRevocationStatusFunction,
})
if err != nil {
st.Fatalf("%s: %s", f.Name(), err.Error())
}
verifySignedFile(st, outputFile, filepath.Base(f.Name()))
})
}
}
func TestSignPDFFileUTF8(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
signerName := "姓名"
signerLocation := "位置"
inputFilePath := "../testfiles/testfile20.pdf"
originalFileName := filepath.Base(inputFilePath)
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: signerName,
Location: signerLocation,
Reason: "Test with UTF-8",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: CertificationSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", originalFileName, err.Error())
}
info, err := verify.File(tmpfile)
if err != nil {
t.Fatalf("%s: %s", tmpfile.Name(), err.Error())
err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName)
if err != nil {
t.Error(err)
}
} else if len(info.Signers) == 0 {
t.Fatalf("no signers found in %s", tmpfile.Name())
} else {
if info.Signers[0].Name != signerName {
t.Fatalf("expected %q, got %q", signerName, info.Signers[0].Name)
}
if info.Signers[0].Location != signerLocation {
t.Fatalf("expected %q, got %q", signerLocation, info.Signers[0].Location)
}
}
}
func TestSignPDFVisible(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
inputFilePath := "../testfiles/testfile12.pdf"
originalFileName := filepath.Base(inputFilePath)
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "Test with visible signature",
ContactInfo: "None",
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Appearance: Appearance{
Visible: true,
LowerLeftX: 350,
LowerLeftY: 75,
UpperRightX: 600,
UpperRightY: 100,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", originalFileName, err.Error())
}
_, err = verify.File(tmpfile)
if err != nil {
t.Fatalf("%s: %s", tmpfile.Name(), err.Error())
err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName)
if err != nil {
t.Error(err)
}
}
}
func BenchmarkSignPDF(b *testing.B) {
cert, pkey := loadCertificateAndKey(&testing.T{})
certificateChains := [][]*x509.Certificate{}
data, err := os.ReadFile("../testfiles/testfile20.pdf")
if err != nil {
b.Fatalf("%s", err.Error())
}
inputFile := filebuffer.New(data)
size := int64(len(data))
rdr, err := pdf.NewReader(inputFile, size)
if err != nil {
b.Fatalf("%s: %s", "testfile20.pdf", err.Error())
}
for n := 0; n < b.N; n++ {
if _, err := inputFile.Seek(0, 0); err != nil {
b.Fatalf("%s: %s", "testfile20.pdf", err.Error())
}
err = Sign(inputFile, io.Discard, rdr, size, SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "Test",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: CertificationSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Signer: pkey,
Certificate: cert,
CertificateChains: certificateChains,
RevocationData: revocation.InfoArchival{},
})
if err != nil {
b.Fatalf("%s: %s", "testfile20.pdf", err.Error())
}
}
}
func TestSignPDFWithTwoApproval(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
tbsFile := "../testfiles/testfile20.pdf"
for i := 1; i <= 2; i++ {
approvalTMPFile, err := os.CreateTemp("", fmt.Sprintf("%s_%d_", t.Name(), i))
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(approvalTMPFile.Name())
}
err = SignFile(tbsFile, approvalTMPFile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: fmt.Sprintf("Jane %d Doe", i),
Location: "Anywhere",
Reason: fmt.Sprintf("Approval Signature %d", i),
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesAndCRUDAnnotationsPerms,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", "testfile20.pdf", err.Error())
}
verifySignedFile(t, approvalTMPFile, filepath.Base(tbsFile))
tbsFile = approvalTMPFile.Name()
}
}
func TestSignPDFWithCertificationApprovalAndTimeStamp(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
tbsFile := "../testfiles/testfile20.pdf"
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile(tbsFile, tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "Certification Test",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: CertificationSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesAndCRUDAnnotationsPerms,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", filepath.Base(tbsFile), err.Error())
}
verifySignedFile(t, tmpfile, filepath.Base(tbsFile))
tbsFile = tmpfile.Name()
for i := 1; i <= 2; i++ {
approvalTMPFile, err := os.CreateTemp("", fmt.Sprintf("%s_approval_%d_", t.Name(), i))
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(approvalTMPFile.Name())
}
err = SignFile(tbsFile, approvalTMPFile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: fmt.Sprintf("Jane %d Doe", i),
Location: "Anywhere",
Reason: fmt.Sprintf("Approval Signature %d", i),
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesAndCRUDAnnotationsPerms,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", filepath.Base(tbsFile), err.Error())
}
verifySignedFile(t, approvalTMPFile, filepath.Base(tbsFile))
tbsFile = approvalTMPFile.Name()
}
timeStampTMPFile, err := os.CreateTemp("", fmt.Sprintf("%s_timestamp_", t.Name()))
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(timeStampTMPFile.Name())
}
err = SignFile(tbsFile, timeStampTMPFile.Name(), SignData{
Signature: SignDataSignature{
CertType: TimeStampSignature,
},
DigestAlgorithm: crypto.SHA512,
TSA: TSA{
URL: "http://timestamp.entrust.net/TSS/RFC3161sha2TS",
},
})
if err != nil {
t.Fatalf("%s: %s", filepath.Base(tbsFile), err.Error())
}
verifySignedFile(t, timeStampTMPFile, "testfile20.pdf")
}
func TestTimestampPDFFile(t *testing.T) {
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile("../testfiles/testfile20.pdf", tmpfile.Name(), SignData{
Signature: SignDataSignature{
CertType: TimeStampSignature,
},
DigestAlgorithm: crypto.SHA512,
TSA: TSA{
URL: "http://timestamp.entrust.net/TSS/RFC3161sha2TS",
},
})
if err != nil {
t.Fatalf("%s: %s", "testfile20.pdf", err.Error())
}
verifySignedFile(t, tmpfile, "testfile20.pdf")
}
// TestSignPDFWithImage tests signing a PDF with an image in the signature
func TestSignPDFWithImage(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
inputFilePath := "../testfiles/testfile12.pdf"
originalFileName := filepath.Base(inputFilePath)
// Read the signature image file
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature.jpg")
if err != nil {
t.Fatalf("Failed to read signature image: %s", err.Error())
}
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "Test with visible signature and image",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Appearance: Appearance{
Visible: true,
LowerLeftX: 400,
LowerLeftY: 50,
UpperRightX: 600,
UpperRightY: 125,
Image: signatureImage, // Use the signature image
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", originalFileName, err.Error())
}
verifySignedFile(t, tmpfile, originalFileName)
}
// TestSignPDFWithTwoImages tests signing a PDF with two different signatures with images
func TestSignPDFWithTwoImages(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
tbsFile := "../testfiles/testfile12.pdf"
// Read the signature image file
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature.jpg")
if err != nil {
t.Fatalf("Failed to read signature image: %s", err.Error())
}
// First signature
firstSignature, err := os.CreateTemp("", fmt.Sprintf("%s_first_", t.Name()))
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(firstSignature.Name())
}
err = SignFile(tbsFile, firstSignature.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "John Doe",
Location: "Somewhere",
Reason: "First signature with image",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Appearance: Appearance{
Visible: true,
LowerLeftX: 50,
LowerLeftY: 50,
UpperRightX: 250,
UpperRightY: 125,
Image: signatureImage,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("First signature failed: %s", err.Error())
}
verifySignedFile(t, firstSignature, filepath.Base(tbsFile))
// Second signature
secondSignature, err := os.CreateTemp("", fmt.Sprintf("%s_second_", t.Name()))
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(secondSignature.Name())
}
err = SignFile(firstSignature.Name(), secondSignature.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "Jane Doe",
Location: "Elsewhere",
Reason: "Second signature with image",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Appearance: Appearance{
Visible: true,
LowerLeftX: 300,
LowerLeftY: 50,
UpperRightX: 500,
UpperRightY: 125,
Image: signatureImage,
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("Second signature failed: %s", err.Error())
}
verifySignedFile(t, secondSignature, filepath.Base(tbsFile))
}
// TestSignPDFWithWatermarkImage tests signing a PDF with an image and text above
func TestSignPDFWithWatermarkImage(t *testing.T) {
cert, pkey := loadCertificateAndKey(t)
inputFilePath := "../testfiles/testfile12.pdf"
originalFileName := filepath.Base(inputFilePath)
// Read the signature image file
signatureImage, err := os.ReadFile("../testfiles/pdfsign-signature-watermark.jpg")
if err != nil {
t.Fatalf("Failed to read signature image: %s", err.Error())
}
tmpfile, err := os.CreateTemp("", t.Name())
if err != nil {
t.Fatalf("%s", err.Error())
}
if !testing.Verbose() {
defer os.Remove(tmpfile.Name())
}
err = SignFile(inputFilePath, tmpfile.Name(), SignData{
Signature: SignDataSignature{
Info: SignDataSignatureInfo{
Name: "James SuperSmith",
Location: "Somewhere",
Reason: "Test with visible signature and watermark image",
ContactInfo: "None",
Date: time.Now().Local(),
},
CertType: ApprovalSignature,
DocMDPPerm: AllowFillingExistingFormFieldsAndSignaturesPerms,
},
Appearance: Appearance{
Visible: true,
LowerLeftX: 400,
LowerLeftY: 50,
UpperRightX: 600,
UpperRightY: 125,
Image: signatureImage, // Use the signature image
ImageAsWatermark: true, // Set the image as a watermark
},
DigestAlgorithm: crypto.SHA512,
Signer: pkey,
Certificate: cert,
})
if err != nil {
t.Fatalf("%s: %s", originalFileName, err.Error())
}
verifySignedFile(t, tmpfile, originalFileName)
}