Add timestamp signature handler (#301)

* Add timestamp signature handler

* Add timestamp signature handler test

* fix PR issues

* fix PR issues

* fix PR issues

* Fix

Co-authored-by: Gunnsteinn Hall <gunnsteinn.hall@gmail.com>
This commit is contained in:
Alexey Pavlyukov 2020-04-22 23:21:53 +03:00 committed by GitHub
parent 6678fc040a
commit a69d788171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 468 additions and 4 deletions

3
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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"))
}

View File

@ -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"

View File

@ -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"
}

View File

@ -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()
}