diff --git a/go.mod b/go.mod index fd0be030..b1888f6f 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,9 @@ require ( github.com/adrg/sysfont v0.1.0 github.com/boombuler/barcode v1.0.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 - github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83 github.com/stretchr/testify v1.3.0 + github.com/unidoc/pkcs7 v0.0.0-20200411230602-d883fd70d1df + github.com/unidoc/timestamp v0.0.0-20200412005513-91597fd3793a golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b golang.org/x/text v0.3.2 diff --git a/go.sum b/go.sum index 99e1cd77..6e9d1c48 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/unidoc/pkcs7 v0.0.0-20200411230602-d883fd70d1df h1:1RV3lxQ6L6xGFNhngpP9iMjJPSwvH3p17JNbK9u5274= +github.com/unidoc/pkcs7 v0.0.0-20200411230602-d883fd70d1df/go.mod h1:UEzOZUEpJfDpywVJMUT8QiugqEZC29pDq7kdIZhWCr8= +github.com/unidoc/timestamp v0.0.0-20200412005513-91597fd3793a h1:RLtvUhe4DsUDl66m7MJ8OqBjq8jpWBXPK6/RKtqeTkc= +github.com/unidoc/timestamp v0.0.0-20200412005513-91597fd3793a/go.mod h1:j+qMWZVpZFTvDey3zxUkSgPJZEX33tDgU/QIA0IzCUw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/model/appender_test.go b/model/appender_test.go index c1b920de..e0686079 100644 --- a/model/appender_test.go +++ b/model/appender_test.go @@ -7,6 +7,7 @@ package model_test import ( "bytes" + "crypto" "crypto/rsa" "errors" "fmt" @@ -920,7 +921,8 @@ func validateFile(t *testing.T, fileName string) { handler, _ := sighandler.NewAdobeX509RSASHA1(nil, nil) handler2, _ := sighandler.NewAdobePKCS7Detached(nil, nil) - handlers := []model.SignatureHandler{handler, handler2} + handler3, _ := sighandler.NewDocTimeStamp("", 0) + handlers := []model.SignatureHandler{handler, handler2, handler3} res, err := reader.ValidateSignatures(handlers) if err != nil { @@ -1577,3 +1579,243 @@ func TestAppenderAttemptMultiWrite(t *testing.T) { t.Fatalf("Second invokation of appender.Write should yield an error") } } + +func TestAppenderTimestampSign(t *testing.T) { + f1, err := os.Open(testPdfFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f1.Close() + pdf1, err := model.NewPdfReader(f1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appender, err := model.NewPdfAppender(pdf1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, err := sighandler.NewDocTimeStamp("https://freetsa.org/tsr", crypto.SHA512) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + // Create signature field and appearance. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Appender") + signature.SetReason("TestAppenderSignPage4") + signature.SetDate(time.Now(), "") + + if err := signature.Initialize(); err != nil { + return + } + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Signature1") + sigField.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + + if err = appender.Sign(1, sigField); err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + err = appender.WriteToFile(tempFile("appender-sign-timestamp.pdf")) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + validateFile(t, tempFile("appender-sign-timestamp.pdf")) +} + +func TestSignatureAppearanceWithTimestamp(t *testing.T) { + f, err := os.Open(testPdf3pages) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f.Close() + + pdfReader, err := model.NewPdfReader(f) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + t.Logf("Fields: %d", len(pdfReader.AcroForm.AllFields())) + + appender, err := model.NewPdfAppender(pdfReader) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + pfxData, _ := ioutil.ReadFile(testPKS12Key) + privateKey, cert, err := pkcs12.Decode(pfxData, testPKS12KeyPassword) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, err := sighandler.NewAdobePKCS7Detached(privateKey.(*rsa.PrivateKey), cert) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + // Create signature. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Signature Appearance Name") + signature.SetReason("TestSignatureAppearance Reason") + signature.SetDate(time.Now(), "") + + if err := signature.Initialize(); err != nil { + return + } + + numPages, err := pdfReader.GetNumPages() + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + for i := 0; i < numPages; i++ { + pageNum := i + 1 + + // Annot1 + opts := annotator.NewSignatureFieldOpts() + opts.FontSize = 10 + opts.Rect = []float64{10, 25, 75, 60} + + sigField, err := annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "Jane Doe"), + annotator.NewSignatureLine("Date", "2019.01.03"), + annotator.NewSignatureLine("Reason", "Some reason"), + annotator.NewSignatureLine("Location", "New York"), + annotator.NewSignatureLine("DN", "authority1:name1"), + }, + opts, + ) + sigField.T = core.MakeString(fmt.Sprintf("Signature %d", pageNum)) + + if err = appender.Sign(pageNum, sigField); err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + // Annot2 + opts = annotator.NewSignatureFieldOpts() + opts.FontSize = 8 + opts.Rect = []float64{250, 25, 325, 70} + opts.TextColor = model.NewPdfColorDeviceRGB(255, 0, 0) + + sigField, err = annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "John Doe"), + annotator.NewSignatureLine("Date", "2019.03.14"), + annotator.NewSignatureLine("Reason", "No reason"), + annotator.NewSignatureLine("Location", "London"), + annotator.NewSignatureLine("DN", "authority2:name2"), + }, + opts, + ) + sigField.T = core.MakeString(fmt.Sprintf("Signature2 %d", pageNum)) + + if err = appender.Sign(pageNum, sigField); err != nil { + log.Fatalf("Fail: %v\n", err) + } + + // Annot3 + opts = annotator.NewSignatureFieldOpts() + opts.BorderSize = 1 + opts.FontSize = 10 + opts.Rect = []float64{475, 25, 590, 80} + opts.FillColor = model.NewPdfColorDeviceRGB(255, 255, 0) + opts.TextColor = model.NewPdfColorDeviceRGB(0, 0, 200) + + sigField, err = annotator.NewSignatureField( + signature, + []*annotator.SignatureLine{ + annotator.NewSignatureLine("Name", "John Smith"), + annotator.NewSignatureLine("Date", "2019.02.19"), + annotator.NewSignatureLine("Reason", "Another reason"), + annotator.NewSignatureLine("Location", "Paris"), + annotator.NewSignatureLine("DN", "authority3:name3"), + }, + opts, + ) + sigField.T = core.MakeString(fmt.Sprintf("Signature3 %d", pageNum)) + + if err = appender.Sign(pageNum, sigField); err != nil { + log.Fatalf("Fail: %v\n", err) + } + } + + outDoc := bytes.NewBuffer(nil) + + if err = appender.Write(outDoc); err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + pdf1, err := model.NewPdfReader(bytes.NewReader(outDoc.Bytes())) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appender, err = model.NewPdfAppender(pdf1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, err = sighandler.NewDocTimeStamp("https://freetsa.org/tsr", crypto.SHA512) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + // Create signature field and appearance. + signature = model.NewPdfSignature(handler) + signature.SetName("Test Appender") + signature.SetReason("TestAppenderSignPage4") + signature.SetDate(time.Now(), "") + + if err := signature.Initialize(); err != nil { + return + } + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Signature1") + sigField.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + + if err = appender.Sign(1, sigField); err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + err = appender.WriteToFile(tempFile("appender-signature-appearance-with-timestamp.pdf")) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + validateFile(t, tempFile("appender-signature-appearance-with-timestamp.pdf")) +} diff --git a/model/sighandler/sighandler_pkcs7.go b/model/sighandler/sighandler_pkcs7.go index 1c1e2f28..435f245b 100644 --- a/model/sighandler/sighandler_pkcs7.go +++ b/model/sighandler/sighandler_pkcs7.go @@ -12,7 +12,7 @@ import ( "errors" "fmt" - "github.com/gunnsth/pkcs7" + "github.com/unidoc/pkcs7" "github.com/unidoc/unipdf/v3/core" "github.com/unidoc/unipdf/v3/model" diff --git a/model/sighandler/sighandler_timestamp.go b/model/sighandler/sighandler_timestamp.go new file mode 100644 index 00000000..196827c3 --- /dev/null +++ b/model/sighandler/sighandler_timestamp.go @@ -0,0 +1,211 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package sighandler + +import ( + "bytes" + "crypto" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "time" + + "github.com/unidoc/pkcs7" + "github.com/unidoc/timestamp" + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/model" +) + +// docTimeStamp DocTimeStamp signature handler. +type docTimeStamp struct { + timestampServerURL string + hashAlgorithm crypto.Hash +} + +// NewDocTimeStamp creates a new DocTimeStamp signature handler. +// The timestampServerURL parameter can be empty string for the signature validation. +// The hashAlgorithm parameter can be crypto.SHA1, crypto.SHA256, crypto.SHA384, crypto.SHA512. +func NewDocTimeStamp(timestampServerURL string, hashAlgorithm crypto.Hash) (model.SignatureHandler, error) { + return &docTimeStamp{ + timestampServerURL: timestampServerURL, + hashAlgorithm: hashAlgorithm, + }, nil +} + +// InitSignature initialises the PdfSignature. +func (a *docTimeStamp) InitSignature(sig *model.PdfSignature) error { + handler := *a + sig.Handler = &handler + sig.Filter = core.MakeName("Adobe.PPKLite") + sig.SubFilter = core.MakeName("ETSI.RFC3161") + sig.Reference = nil + digest, err := a.NewDigest(sig) + if err != nil { + return err + } + digest.Write([]byte("calculate the Contents field size")) + return handler.Sign(sig, digest) +} + +func (a *docTimeStamp) getCertificate(sig *model.PdfSignature) (*x509.Certificate, error) { + var certData []byte + switch certObj := sig.Cert.(type) { + case *core.PdfObjectString: + certData = certObj.Bytes() + case *core.PdfObjectArray: + if certObj.Len() == 0 { + return nil, errors.New("no signature certificates found") + } + for _, obj := range certObj.Elements() { + certStr, ok := core.GetString(obj) + if !ok { + return nil, fmt.Errorf("invalid certificate object type in signature certificate chain: %T", obj) + } + certData = append(certData, certStr.Bytes()...) + } + default: + return nil, fmt.Errorf("invalid signature certificate object type: %T", certObj) + } + + certs, err := x509.ParseCertificates(certData) + if err != nil { + return nil, err + } + + return certs[0], nil +} + +// NewDigest creates a new digest. +func (a *docTimeStamp) NewDigest(sig *model.PdfSignature) (model.Hasher, error) { + return bytes.NewBuffer(nil), nil +} + +type timestampInfo struct { + Version int + Policy asn1.RawValue + MessageImprint struct { + HashAlgorithm pkix.AlgorithmIdentifier + HashedMessage []byte + } + SerialNumber asn1.RawValue + GeneralizedTime time.Time +} + +func getHashForOID(oid asn1.ObjectIdentifier) (crypto.Hash, error) { + switch { + case oid.Equal(pkcs7.OIDDigestAlgorithmSHA1), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA1), + oid.Equal(pkcs7.OIDDigestAlgorithmDSA), oid.Equal(pkcs7.OIDDigestAlgorithmDSASHA1), + oid.Equal(pkcs7.OIDEncryptionAlgorithmRSA): + return crypto.SHA1, nil + case oid.Equal(pkcs7.OIDDigestAlgorithmSHA256), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA256): + return crypto.SHA256, nil + case oid.Equal(pkcs7.OIDDigestAlgorithmSHA384), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA384): + return crypto.SHA384, nil + case oid.Equal(pkcs7.OIDDigestAlgorithmSHA512), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA512): + return crypto.SHA512, nil + } + return crypto.Hash(0), pkcs7.ErrUnsupportedAlgorithm +} + +// Validate validates PdfSignature. +func (a *docTimeStamp) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) { + signed := sig.Contents.Bytes() + p7, err := pkcs7.Parse(signed) + if err != nil { + return model.SignatureValidationResult{}, err + } + + if err = p7.Verify(); err != nil { + return model.SignatureValidationResult{}, err + } + + var tsInfo timestampInfo + + _, err = asn1.Unmarshal(p7.Content, &tsInfo) + if err != nil { + return model.SignatureValidationResult{}, err + } + + hAlg, err := getHashForOID(tsInfo.MessageImprint.HashAlgorithm.Algorithm) + if err != nil { + return model.SignatureValidationResult{}, err + } + h := hAlg.New() + buffer := digest.(*bytes.Buffer) + + h.Write(buffer.Bytes()) + sm := h.Sum(nil) + res := model.SignatureValidationResult{ + IsSigned: true, + IsVerified: bytes.Equal(sm, tsInfo.MessageImprint.HashedMessage), + GeneralizedTime: tsInfo.GeneralizedTime, + } + return res, nil +} + +// Sign sets the Contents fields for the PdfSignature. +func (a *docTimeStamp) Sign(sig *model.PdfSignature, digest model.Hasher) error { + buffer := digest.(*bytes.Buffer) + h := a.hashAlgorithm.New() + + if _, err := io.Copy(h, buffer); err != nil { + return err + } + + s := h.Sum(nil) + r := timestamp.Request{ + HashAlgorithm: a.hashAlgorithm, + HashedMessage: s, + Certificates: true, + Extensions: nil, + ExtraExtensions: nil, + } + data, err := r.Marshal() + if err != nil { + return err + } + + resp, err := http.Post(a.timestampServerURL, "application/timestamp-query", bytes.NewBuffer(data)) + if err != nil { + return err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("http status code not ok (got %d)", resp.StatusCode) + } + + var ci struct { + Version asn1.RawValue + Content asn1.RawValue + } + + _, err = asn1.Unmarshal(body, &ci) + if err != nil { + return err + } + + sig.Contents = core.MakeHexString(string(ci.Content.FullBytes)) + return nil +} + +// IsApplicable returns true if the signature handler is applicable for the PdfSignature. +func (a *docTimeStamp) IsApplicable(sig *model.PdfSignature) bool { + if sig == nil || sig.Filter == nil || sig.SubFilter == nil { + return false + } + return (*sig.Filter == "Adobe.PPKMS" || *sig.Filter == "Adobe.PPKLite") && *sig.SubFilter == "ETSI.RFC3161" +} diff --git a/model/signature_handler.go b/model/signature_handler.go index 4bf4e8d4..47937267 100644 --- a/model/signature_handler.go +++ b/model/signature_handler.go @@ -9,6 +9,7 @@ import ( "bytes" "fmt" "io" + "time" "github.com/unidoc/unipdf/v3/common" "github.com/unidoc/unipdf/v3/core" @@ -47,6 +48,9 @@ type SignatureValidationResult struct { // TODO(gunnsth): Add more fields such as ability to access the certificate information (name, CN, etc). // TODO: Also add flags to indicate whether the signature covers the entire file, or the entire portion of // a revision (if incremental updates used). + + // GeneralizedTime is the time at which the time-stamp token has been created by the TSA (RFC 3161). + GeneralizedTime time.Time } func (v SignatureValidationResult) String() string { @@ -88,7 +92,9 @@ func (v SignatureValidationResult) String() string { } else { buf.WriteString("Trusted: Untrusted certificate\n") } - + if !v.GeneralizedTime.IsZero() { + buf.WriteString(fmt.Sprintf("GeneralizedTime: %s\n", v.GeneralizedTime.String())) + } return buf.String() }