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()) if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); 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/minimal.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()) if err := os.Rename(tmpfile.Name(), "../testfiles/failed/"+originalFileName); 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") }