From 61e20223eccf005e5e8b01b3f813829f2f86d812 Mon Sep 17 00:00:00 2001 From: Aleksei Pavliukov Date: Wed, 19 Dec 2018 18:36:15 +0300 Subject: [PATCH 01/20] add digital sign API prototype --- common/crypto/pkcs7/.gitignore | 24 + common/crypto/pkcs7/.travis.yml | 7 + common/crypto/pkcs7/LICENSE | 22 + common/crypto/pkcs7/README.md | 8 + common/crypto/pkcs7/ber.go | 248 +++++ common/crypto/pkcs7/ber_test.go | 98 ++ common/crypto/pkcs7/pkcs7.go | 979 ++++++++++++++++++ common/crypto/pkcs7/pkcs7_test.go | 680 ++++++++++++ common/crypto/pkcs7/x509.go | 135 +++ pdf/model/appearance.go | 47 + pdf/model/appender.go | 198 +++- pdf/model/appender_test.go | 92 ++ pdf/model/fields.go | 14 +- pdf/model/signature.go | 177 +++- pdf/model/signature_handler.go | 270 +++++ pdf/model/signature_handler_pkcs7.go | 126 +++ .../testdata/SampleSignedPDFDocument.pdf | Bin 0 -> 272318 bytes pdf/model/testdata/ks12 | Bin 0 -> 2587 bytes pdf/model/writer.go | 13 + 19 files changed, 3119 insertions(+), 19 deletions(-) create mode 100644 common/crypto/pkcs7/.gitignore create mode 100644 common/crypto/pkcs7/.travis.yml create mode 100644 common/crypto/pkcs7/LICENSE create mode 100644 common/crypto/pkcs7/README.md create mode 100644 common/crypto/pkcs7/ber.go create mode 100644 common/crypto/pkcs7/ber_test.go create mode 100644 common/crypto/pkcs7/pkcs7.go create mode 100644 common/crypto/pkcs7/pkcs7_test.go create mode 100644 common/crypto/pkcs7/x509.go create mode 100644 pdf/model/appearance.go create mode 100644 pdf/model/signature_handler.go create mode 100644 pdf/model/signature_handler_pkcs7.go create mode 100644 pdf/model/testdata/SampleSignedPDFDocument.pdf create mode 100644 pdf/model/testdata/ks12 diff --git a/common/crypto/pkcs7/.gitignore b/common/crypto/pkcs7/.gitignore new file mode 100644 index 00000000..daf913b1 --- /dev/null +++ b/common/crypto/pkcs7/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/common/crypto/pkcs7/.travis.yml b/common/crypto/pkcs7/.travis.yml new file mode 100644 index 00000000..bc120437 --- /dev/null +++ b/common/crypto/pkcs7/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.8 + - 1.9 + - "1.10" + - tip diff --git a/common/crypto/pkcs7/LICENSE b/common/crypto/pkcs7/LICENSE new file mode 100644 index 00000000..75f32090 --- /dev/null +++ b/common/crypto/pkcs7/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Andrew Smith + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/common/crypto/pkcs7/README.md b/common/crypto/pkcs7/README.md new file mode 100644 index 00000000..bfd948f3 --- /dev/null +++ b/common/crypto/pkcs7/README.md @@ -0,0 +1,8 @@ +# pkcs7 + +[![GoDoc](https://godoc.org/github.com/fullsailor/pkcs7?status.svg)](https://godoc.org/github.com/fullsailor/pkcs7) +[![Build Status](https://travis-ci.org/fullsailor/pkcs7.svg?branch=master)](https://travis-ci.org/fullsailor/pkcs7) + +pkcs7 implements parsing and creating signed and enveloped messages. + +- Documentation on [GoDoc](http://godoc.org/github.com/fullsailor/pkcs7) diff --git a/common/crypto/pkcs7/ber.go b/common/crypto/pkcs7/ber.go new file mode 100644 index 00000000..1a0d8c02 --- /dev/null +++ b/common/crypto/pkcs7/ber.go @@ -0,0 +1,248 @@ +package pkcs7 + +import ( + "bytes" + "errors" +) + +var encodeIndent = 0 + +type asn1Object interface { + EncodeTo(writer *bytes.Buffer) error +} + +type asn1Structured struct { + tagBytes []byte + content []asn1Object +} + +func (s asn1Structured) EncodeTo(out *bytes.Buffer) error { + //fmt.Printf("%s--> tag: % X\n", strings.Repeat("| ", encodeIndent), s.tagBytes) + encodeIndent++ + inner := new(bytes.Buffer) + for _, obj := range s.content { + err := obj.EncodeTo(inner) + if err != nil { + return err + } + } + encodeIndent-- + out.Write(s.tagBytes) + encodeLength(out, inner.Len()) + out.Write(inner.Bytes()) + return nil +} + +type asn1Primitive struct { + tagBytes []byte + length int + content []byte +} + +func (p asn1Primitive) EncodeTo(out *bytes.Buffer) error { + _, err := out.Write(p.tagBytes) + if err != nil { + return err + } + if err = encodeLength(out, p.length); err != nil { + return err + } + //fmt.Printf("%s--> tag: % X length: %d\n", strings.Repeat("| ", encodeIndent), p.tagBytes, p.length) + //fmt.Printf("%s--> content length: %d\n", strings.Repeat("| ", encodeIndent), len(p.content)) + out.Write(p.content) + + return nil +} + +func ber2der(ber []byte) ([]byte, error) { + if len(ber) == 0 { + return nil, errors.New("ber2der: input ber is empty") + } + //fmt.Printf("--> ber2der: Transcoding %d bytes\n", len(ber)) + out := new(bytes.Buffer) + + obj, _, err := readObject(ber, 0) + if err != nil { + return nil, err + } + obj.EncodeTo(out) + + // if offset < len(ber) { + // return nil, fmt.Errorf("ber2der: Content longer than expected. Got %d, expected %d", offset, len(ber)) + //} + + return out.Bytes(), nil +} + +// encodes lengths that are longer than 127 into string of bytes +func marshalLongLength(out *bytes.Buffer, i int) (err error) { + n := lengthLength(i) + + for ; n > 0; n-- { + err = out.WriteByte(byte(i >> uint((n-1)*8))) + if err != nil { + return + } + } + + return nil +} + +// computes the byte length of an encoded length value +func lengthLength(i int) (numBytes int) { + numBytes = 1 + for i > 255 { + numBytes++ + i >>= 8 + } + return +} + +// encodes the length in DER format +// If the length fits in 7 bits, the value is encoded directly. +// +// Otherwise, the number of bytes to encode the length is first determined. +// This number is likely to be 4 or less for a 32bit length. This number is +// added to 0x80. The length is encoded in big endian encoding follow after +// +// Examples: +// length | byte 1 | bytes n +// 0 | 0x00 | - +// 120 | 0x78 | - +// 200 | 0x81 | 0xC8 +// 500 | 0x82 | 0x01 0xF4 +// +func encodeLength(out *bytes.Buffer, length int) (err error) { + if length >= 128 { + l := lengthLength(length) + err = out.WriteByte(0x80 | byte(l)) + if err != nil { + return + } + err = marshalLongLength(out, length) + if err != nil { + return + } + } else { + err = out.WriteByte(byte(length)) + if err != nil { + return + } + } + return +} + +func readObject(ber []byte, offset int) (asn1Object, int, error) { + //fmt.Printf("\n====> Starting readObject at offset: %d\n\n", offset) + tagStart := offset + b := ber[offset] + offset++ + tag := b & 0x1F // last 5 bits + if tag == 0x1F { + tag = 0 + for ber[offset] >= 0x80 { + tag = tag*128 + ber[offset] - 0x80 + offset++ + } + tag = tag*128 + ber[offset] - 0x80 + offset++ + } + tagEnd := offset + + kind := b & 0x20 + /* + if kind == 0 { + fmt.Print("--> Primitive\n") + } else { + fmt.Print("--> Constructed\n") + } + */ + // read length + var length int + l := ber[offset] + offset++ + indefinite := false + if l > 0x80 { + numberOfBytes := (int)(l & 0x7F) + if numberOfBytes > 4 { // int is only guaranteed to be 32bit + return nil, 0, errors.New("ber2der: BER tag length too long") + } + if numberOfBytes == 4 && (int)(ber[offset]) > 0x7F { + return nil, 0, errors.New("ber2der: BER tag length is negative") + } + if 0x0 == (int)(ber[offset]) { + return nil, 0, errors.New("ber2der: BER tag length has leading zero") + } + //fmt.Printf("--> (compute length) indicator byte: %x\n", l) + //fmt.Printf("--> (compute length) length bytes: % X\n", ber[offset:offset+numberOfBytes]) + for i := 0; i < numberOfBytes; i++ { + length = length*256 + (int)(ber[offset]) + offset++ + } + } else if l == 0x80 { + indefinite = true + } else { + length = (int)(l) + } + + //fmt.Printf("--> length : %d\n", length) + contentEnd := offset + length + if contentEnd > len(ber) { + return nil, 0, errors.New("ber2der: BER tag length is more than available data") + } + //fmt.Printf("--> content start : %d\n", offset) + //fmt.Printf("--> content end : %d\n", contentEnd) + //fmt.Printf("--> content : % X\n", ber[offset:contentEnd]) + var obj asn1Object + if indefinite && kind == 0 { + return nil, 0, errors.New("ber2der: Indefinite form tag must have constructed encoding") + } + if kind == 0 { + obj = asn1Primitive{ + tagBytes: ber[tagStart:tagEnd], + length: length, + content: ber[offset:contentEnd], + } + } else { + var subObjects []asn1Object + for (offset < contentEnd) || indefinite { + var subObj asn1Object + var err error + subObj, offset, err = readObject(ber, offset) + if err != nil { + return nil, 0, err + } + subObjects = append(subObjects, subObj) + + if indefinite { + terminated, err := isIndefiniteTermination(ber, offset) + if err != nil { + return nil, 0, err + } + + if terminated { + break + } + } + } + obj = asn1Structured{ + tagBytes: ber[tagStart:tagEnd], + content: subObjects, + } + } + + // Apply indefinite form length with 0x0000 terminator. + if indefinite { + contentEnd = offset + 2 + } + + return obj, contentEnd, nil +} + +func isIndefiniteTermination(ber []byte, offset int) (bool, error) { + if len(ber)-offset < 2 { + return false, errors.New("ber2der: Invalid BER format") + } + + return bytes.Index(ber[offset:], []byte{0x0, 0x0}) == 0, nil +} diff --git a/common/crypto/pkcs7/ber_test.go b/common/crypto/pkcs7/ber_test.go new file mode 100644 index 00000000..72288c80 --- /dev/null +++ b/common/crypto/pkcs7/ber_test.go @@ -0,0 +1,98 @@ +package pkcs7 + +import ( + "bytes" + "strings" + "testing" + + "github.com/gunnsth/crypto/asn1" +) + +func TestBer2Der(t *testing.T) { + // indefinite length fixture + ber := []byte{0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00} + expected := []byte{0x30, 0x03, 0x02, 0x01, 0x01} + der, err := ber2der(ber) + if err != nil { + t.Fatalf("ber2der failed with error: %v", err) + } + if bytes.Compare(der, expected) != 0 { + t.Errorf("ber2der result did not match.\n\tExpected: % X\n\tActual: % X", expected, der) + } + + if der2, err := ber2der(der); err != nil { + t.Errorf("ber2der on DER bytes failed with error: %v", err) + } else { + if !bytes.Equal(der, der2) { + t.Error("ber2der is not idempotent") + } + } + var thing struct { + Number int + } + rest, err := asn1.Unmarshal(der, &thing) + if err != nil { + t.Errorf("Cannot parse resulting DER because: %v", err) + } else if len(rest) > 0 { + t.Errorf("Resulting DER has trailing data: % X", rest) + } +} + +func TestBer2Der_Negatives(t *testing.T) { + fixtures := []struct { + Input []byte + ErrorContains string + }{ + {[]byte{0x30, 0x85}, "length too long"}, + {[]byte{0x30, 0x84, 0x80, 0x0, 0x0, 0x0}, "length is negative"}, + {[]byte{0x30, 0x82, 0x0, 0x1}, "length has leading zero"}, + {[]byte{0x30, 0x80, 0x1, 0x2, 0x1, 0x2}, "Invalid BER format"}, + {[]byte{0x30, 0x03, 0x01, 0x02}, "length is more than available data"}, + } + + for _, fixture := range fixtures { + _, err := ber2der(fixture.Input) + if err == nil { + t.Errorf("No error thrown. Expected: %s", fixture.ErrorContains) + } + if !strings.Contains(err.Error(), fixture.ErrorContains) { + t.Errorf("Unexpected error thrown.\n\tExpected: /%s/\n\tActual: %s", fixture.ErrorContains, err.Error()) + } + } +} + +func TestBer2Der_NestedMultipleIndefinite(t *testing.T) { + // indefinite length fixture + ber := []byte{0x30, 0x80, 0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00, 0x30, 0x80, 0x02, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00} + expected := []byte{0x30, 0x0A, 0x30, 0x03, 0x02, 0x01, 0x01, 0x30, 0x03, 0x02, 0x01, 0x02} + + der, err := ber2der(ber) + if err != nil { + t.Fatalf("ber2der failed with error: %v", err) + } + if bytes.Compare(der, expected) != 0 { + t.Errorf("ber2der result did not match.\n\tExpected: % X\n\tActual: % X", expected, der) + } + + if der2, err := ber2der(der); err != nil { + t.Errorf("ber2der on DER bytes failed with error: %v", err) + } else { + if !bytes.Equal(der, der2) { + t.Error("ber2der is not idempotent") + } + } + var thing struct { + Nest1 struct { + Number int + } + Nest2 struct { + Number int + } + } + rest, err := asn1.Unmarshal(der, &thing) + if err != nil { + t.Errorf("Cannot parse resulting DER because: %v", err) + } else if len(rest) > 0 { + t.Errorf("Resulting DER has trailing data: % X", rest) + } +} diff --git a/common/crypto/pkcs7/pkcs7.go b/common/crypto/pkcs7/pkcs7.go new file mode 100644 index 00000000..b401c761 --- /dev/null +++ b/common/crypto/pkcs7/pkcs7.go @@ -0,0 +1,979 @@ +// Package pkcs7 implements parsing and generation of some PKCS#7 structures. +package pkcs7 + +import ( + "bytes" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/des" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/hex" + "errors" + "fmt" + "log" + "math/big" + "sort" + "time" + + _ "crypto/sha1" // for crypto.SHA1 +) + +// PKCS7 Represents a PKCS7 structure +type PKCS7 struct { + Content []byte + Certificates []*x509.Certificate + CRLs []pkix.CertificateList + Signers []signerInfo + raw interface{} +} + +type contentInfo struct { + ContentType asn1.ObjectIdentifier + Content asn1.RawValue `asn1:"explicit,optional,tag:0"` +} + +// ErrUnsupportedContentType is returned when a PKCS7 content is not supported. +// Currently only Data (1.2.840.113549.1.7.1), Signed Data (1.2.840.113549.1.7.2), +// and Enveloped Data are supported (1.2.840.113549.1.7.3) +var ErrUnsupportedContentType = errors.New("pkcs7: cannot parse data: unimplemented content type") + +type unsignedData []byte + +var ( + oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1} + oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2} + oidEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 3} + oidSignedAndEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 4} + oidDigestedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 5} + oidEncryptedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 6} + oidAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} + oidAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} + oidAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5} +) + +type signedData struct { + Version int `asn1:"default:1"` + DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"` + ContentInfo contentInfo + Certificates rawCertificates `asn1:"optional,tag:0"` + CRLs []pkix.CertificateList `asn1:"optional,tag:1"` + SignerInfos []signerInfo `asn1:"set"` +} + +type rawCertificates struct { + Raw asn1.RawContent +} + +type envelopedData struct { + Version int + RecipientInfos []recipientInfo `asn1:"set"` + EncryptedContentInfo encryptedContentInfo +} + +type recipientInfo struct { + Version int + IssuerAndSerialNumber issuerAndSerial + KeyEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedKey []byte +} + +type encryptedContentInfo struct { + ContentType asn1.ObjectIdentifier + ContentEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedContent asn1.RawValue `asn1:"tag:0,optional,explicit"` +} + +type attribute struct { + Type asn1.ObjectIdentifier + Value asn1.RawValue `asn1:"set"` +} + +type issuerAndSerial struct { + IssuerName asn1.RawValue + SerialNumber *big.Int +} + +// MessageDigestMismatchError is returned when the signer data digest does not +// match the computed digest for the contained content +type MessageDigestMismatchError struct { + ExpectedDigest []byte + ActualDigest []byte +} + +func (err *MessageDigestMismatchError) Error() string { + return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest) +} + +type signerInfo struct { + Version int `asn1:"default:1"` + IssuerAndSerialNumber issuerAndSerial + DigestAlgorithm pkix.AlgorithmIdentifier + AuthenticatedAttributes []attribute `asn1:"optional,tag:0"` + DigestEncryptionAlgorithm pkix.AlgorithmIdentifier + EncryptedDigest []byte + UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"` +} + +// Parse decodes a DER encoded PKCS7 package +func Parse(data []byte) (p7 *PKCS7, err error) { + if len(data) == 0 { + return nil, errors.New("pkcs7: input data is empty") + } + var info contentInfo + der, err := ber2der(data) + if err != nil { + return nil, err + } + rest, err := asn1.Unmarshal(der, &info) + if len(rest) > 0 { + err = asn1.SyntaxError{Msg: "trailing data"} + return + } + if err != nil { + return + } + + // For debugging: + // fmt.Printf("--> Content Type: %s", info.ContentType) + switch { + case info.ContentType.Equal(oidSignedData): + return parseSignedData(info.Content.Bytes) + case info.ContentType.Equal(oidEnvelopedData): + return parseEnvelopedData(info.Content.Bytes) + } + return nil, ErrUnsupportedContentType +} + +func parseSignedData(data []byte) (*PKCS7, error) { + var sd signedData + asn1.Unmarshal(data, &sd) + certs, err := sd.Certificates.Parse() + if err != nil { + return nil, err + } + // For debugging: + // fmt.Printf("--> Signed Data Version %d\n", sd.Version) + + var compound asn1.RawValue + var content unsignedData + + // The Content.Bytes maybe empty on PKI responses. + if len(sd.ContentInfo.Content.Bytes) > 0 { + if _, err := asn1.Unmarshal(sd.ContentInfo.Content.Bytes, &compound); err != nil { + return nil, err + } + } + // Compound octet string + if compound.IsCompound { + if _, err = asn1.Unmarshal(compound.Bytes, &content); err != nil { + return nil, err + } + } else { + // assuming this is tag 04 + content = compound.Bytes + } + return &PKCS7{ + Content: content, + Certificates: certs, + CRLs: sd.CRLs, + Signers: sd.SignerInfos, + raw: sd}, nil +} + +func (raw rawCertificates) Parse() ([]*x509.Certificate, error) { + if len(raw.Raw) == 0 { + return nil, nil + } + + var val asn1.RawValue + if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil { + return nil, err + } + + return x509.ParseCertificates(val.Bytes) +} + +func parseEnvelopedData(data []byte) (*PKCS7, error) { + var ed envelopedData + if _, err := asn1.Unmarshal(data, &ed); err != nil { + return nil, err + } + return &PKCS7{ + raw: ed, + }, nil +} + +// Verify checks the signatures of a PKCS7 object +// WARNING: Verify does not check signing time or verify certificate chains at +// this time. +func (p7 *PKCS7) Verify() (err error) { + if len(p7.Signers) == 0 { + return errors.New("pkcs7: Message has no signers") + } + for _, signer := range p7.Signers { + if err := verifySignature(p7, signer); err != nil { + return err + } + } + return nil +} + +func verifySignature(p7 *PKCS7, signer signerInfo) error { + signedData := p7.Content + hash, err := getHashForOID(signer.DigestAlgorithm.Algorithm) + if err != nil { + return err + } + // For debugging: + //fmt.Printf("Hash type: %T %#v\n", hash, hash) + if len(signer.AuthenticatedAttributes) > 0 { + // TODO(fullsailor): First check the content type match + var digest []byte + err := unmarshalAttribute(signer.AuthenticatedAttributes, oidAttributeMessageDigest, &digest) + if err != nil { + return err + } + h := hash.New() + // For debugging: + //fmt.Printf("Hash: %T %#v\n", h, h) + //fmt.Printf("Expected digest: % X\n", digest) + //fmt.Printf("Content (%d): %X\n", len(p7.Content), p7.Content) + + h.Write(p7.Content) + computed := h.Sum(nil) + log.Println("Computed", hex.EncodeToString(computed)) + if !hmac.Equal(digest, computed) { + return &MessageDigestMismatchError{ + ExpectedDigest: digest, + ActualDigest: computed, + } + } + + // TODO(fullsailor): Optionally verify certificate chain + // TODO(fullsailor): Optionally verify signingTime against certificate NotAfter/NotBefore + signedData, err = marshalAttributes(signer.AuthenticatedAttributes) + if err != nil { + return err + } + } + cert := getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) + if cert == nil { + return errors.New("pkcs7: No certificate for signer") + } + + algo := getSignatureAlgorithmFromAI(signer.DigestEncryptionAlgorithm) + if algo == x509.UnknownSignatureAlgorithm { + // I'm not sure what the spec here is, and the openssl sources were not + // helpful. But, this is what App Store receipts appear to do. + // The DigestEncryptionAlgorithm is just "rsaEncryption (PKCS #1)" + // But we're expecting a digest + encryption algorithm. So... we're going + // to determine an algorithm based on the DigestAlgorithm and this + // encryption algorithm. + if signer.DigestEncryptionAlgorithm.Algorithm.Equal(oidEncryptionAlgorithmRSA) { + algo = getRSASignatureAlgorithmForDigestAlgorithm(hash) + } + } + return cert.CheckSignature(algo, signedData, signer.EncryptedDigest) +} + +func marshalAttributes(attrs []attribute) ([]byte, error) { + encodedAttributes, err := asn1.Marshal(struct { + A []attribute `asn1:"set"` + }{A: attrs}) + if err != nil { + return nil, err + } + + // Remove the leading sequence octets + var raw asn1.RawValue + asn1.Unmarshal(encodedAttributes, &raw) + return raw.Bytes, nil +} + +var ( + oidDigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} + oidDigestAlgorithmSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} + oidEncryptionAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} +) + +func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndSerial) *x509.Certificate { + for _, cert := range certs { + if isCertMatchForIssuerAndSerial(cert, ias) { + return cert + } + } + return nil +} + +func getHashForOID(oid asn1.ObjectIdentifier) (crypto.Hash, error) { + switch { + case oid.Equal(oidDigestAlgorithmSHA1): + return crypto.SHA1, nil + case oid.Equal(oidDigestAlgorithmSHA256): + return crypto.SHA256, nil + } + + // For debugging: + // fmt.Printf("Unsupported algo: %s\n", oid.String()) + return crypto.Hash(0), ErrUnsupportedAlgorithm +} + +func getRSASignatureAlgorithmForDigestAlgorithm(hash crypto.Hash) x509.SignatureAlgorithm { + for _, details := range signatureAlgorithmDetails { + if details.pubKeyAlgo == x509.RSA && details.hash == hash { + return details.algo + } + } + return x509.UnknownSignatureAlgorithm +} + +// GetOnlySigner returns an x509.Certificate for the first signer of the signed +// data payload. If there are more or less than one signer, nil is returned +func (p7 *PKCS7) GetOnlySigner() *x509.Certificate { + if len(p7.Signers) != 1 { + return nil + } + signer := p7.Signers[0] + return getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) +} + +// ErrUnsupportedAlgorithm tells you when our quick dev assumptions have failed +var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3, AES-256-CBC and AES-128-GCM supported") + +// ErrNotEncryptedContent is returned when attempting to Decrypt data that is not encrypted data +var ErrNotEncryptedContent = errors.New("pkcs7: content data is a decryptable data type") + +// Decrypt decrypts encrypted content info for recipient cert and private key +func (p7 *PKCS7) Decrypt(cert *x509.Certificate, pk crypto.PrivateKey) ([]byte, error) { + data, ok := p7.raw.(envelopedData) + if !ok { + return nil, ErrNotEncryptedContent + } + recipient := selectRecipientForCertificate(data.RecipientInfos, cert) + if recipient.EncryptedKey == nil { + return nil, errors.New("pkcs7: no enveloped recipient for provided certificate") + } + if priv := pk.(*rsa.PrivateKey); priv != nil { + var contentKey []byte + contentKey, err := rsa.DecryptPKCS1v15(rand.Reader, priv, recipient.EncryptedKey) + if err != nil { + return nil, err + } + return data.EncryptedContentInfo.decrypt(contentKey) + } + fmt.Printf("Unsupported Private Key: %v\n", pk) + return nil, ErrUnsupportedAlgorithm +} + +var oidEncryptionAlgorithmDESCBC = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 7} +var oidEncryptionAlgorithmDESEDE3CBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 3, 7} +var oidEncryptionAlgorithmAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} +var oidEncryptionAlgorithmAES128GCM = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 6} +var oidEncryptionAlgorithmAES128CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 2} + +func (eci encryptedContentInfo) decrypt(key []byte) ([]byte, error) { + alg := eci.ContentEncryptionAlgorithm.Algorithm + if !alg.Equal(oidEncryptionAlgorithmDESCBC) && + !alg.Equal(oidEncryptionAlgorithmDESEDE3CBC) && + !alg.Equal(oidEncryptionAlgorithmAES256CBC) && + !alg.Equal(oidEncryptionAlgorithmAES128CBC) && + !alg.Equal(oidEncryptionAlgorithmAES128GCM) { + fmt.Printf("Unsupported Content Encryption Algorithm: %s\n", alg) + return nil, ErrUnsupportedAlgorithm + } + + // EncryptedContent can either be constructed of multple OCTET STRINGs + // or _be_ a tagged OCTET STRING + var cyphertext []byte + if eci.EncryptedContent.IsCompound { + // Complex case to concat all of the children OCTET STRINGs + var buf bytes.Buffer + cypherbytes := eci.EncryptedContent.Bytes + for { + var part []byte + cypherbytes, _ = asn1.Unmarshal(cypherbytes, &part) + buf.Write(part) + if cypherbytes == nil { + break + } + } + cyphertext = buf.Bytes() + } else { + // Simple case, the bytes _are_ the cyphertext + cyphertext = eci.EncryptedContent.Bytes + } + + var block cipher.Block + var err error + + switch { + case alg.Equal(oidEncryptionAlgorithmDESCBC): + block, err = des.NewCipher(key) + case alg.Equal(oidEncryptionAlgorithmDESEDE3CBC): + block, err = des.NewTripleDESCipher(key) + case alg.Equal(oidEncryptionAlgorithmAES256CBC): + fallthrough + case alg.Equal(oidEncryptionAlgorithmAES128GCM), alg.Equal(oidEncryptionAlgorithmAES128CBC): + block, err = aes.NewCipher(key) + } + + if err != nil { + return nil, err + } + + if alg.Equal(oidEncryptionAlgorithmAES128GCM) { + params := aesGCMParameters{} + paramBytes := eci.ContentEncryptionAlgorithm.Parameters.Bytes + + _, err := asn1.Unmarshal(paramBytes, ¶ms) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(params.Nonce) != gcm.NonceSize() { + return nil, errors.New("pkcs7: encryption algorithm parameters are incorrect") + } + if params.ICVLen != gcm.Overhead() { + return nil, errors.New("pkcs7: encryption algorithm parameters are incorrect") + } + + plaintext, err := gcm.Open(nil, params.Nonce, cyphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil + } + + iv := eci.ContentEncryptionAlgorithm.Parameters.Bytes + if len(iv) != block.BlockSize() { + return nil, errors.New("pkcs7: encryption algorithm parameters are malformed") + } + mode := cipher.NewCBCDecrypter(block, iv) + plaintext := make([]byte, len(cyphertext)) + mode.CryptBlocks(plaintext, cyphertext) + if plaintext, err = unpad(plaintext, mode.BlockSize()); err != nil { + return nil, err + } + return plaintext, nil +} + +func selectRecipientForCertificate(recipients []recipientInfo, cert *x509.Certificate) recipientInfo { + for _, recp := range recipients { + if isCertMatchForIssuerAndSerial(cert, recp.IssuerAndSerialNumber) { + return recp + } + } + return recipientInfo{} +} + +func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool { + return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Compare(cert.RawIssuer, ias.IssuerName.FullBytes) == 0 +} + +func pad(data []byte, blocklen int) ([]byte, error) { + if blocklen < 1 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + padlen := blocklen - (len(data) % blocklen) + if padlen == 0 { + padlen = blocklen + } + pad := bytes.Repeat([]byte{byte(padlen)}, padlen) + return append(data, pad...), nil +} + +func unpad(data []byte, blocklen int) ([]byte, error) { + if blocklen < 1 { + return nil, fmt.Errorf("invalid blocklen %d", blocklen) + } + if len(data)%blocklen != 0 || len(data) == 0 { + return nil, fmt.Errorf("invalid data len %d", len(data)) + } + + // the last byte is the length of padding + padlen := int(data[len(data)-1]) + + // check padding integrity, all bytes should be the same + pad := data[len(data)-padlen:] + for _, padbyte := range pad { + if padbyte != byte(padlen) { + return nil, errors.New("invalid padding") + } + } + + return data[:len(data)-padlen], nil +} + +func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error { + for _, attr := range attrs { + if attr.Type.Equal(attributeType) { + _, err := asn1.Unmarshal(attr.Value.Bytes, out) + return err + } + } + return errors.New("pkcs7: attribute type not in attributes") +} + +// UnmarshalSignedAttribute decodes a single attribute from the signer info +func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, out interface{}) error { + sd, ok := p7.raw.(signedData) + if !ok { + return errors.New("pkcs7: payload is not signedData content") + } + if len(sd.SignerInfos) < 1 { + return errors.New("pkcs7: payload has no signers") + } + attributes := sd.SignerInfos[0].AuthenticatedAttributes + return unmarshalAttribute(attributes, attributeType, out) +} + +// SignedData is an opaque data structure for creating signed data payloads +type SignedData struct { + sd signedData + certs []*x509.Certificate + messageDigest []byte +} + +// Attribute represents a key value pair attribute. Value must be marshalable byte +// `encoding/asn1` +type Attribute struct { + Type asn1.ObjectIdentifier + Value interface{} +} + +// SignerInfoConfig are optional values to include when adding a signer +type SignerInfoConfig struct { + ExtraSignedAttributes []Attribute +} + +// NewSignedData initializes a SignedData with content +func NewSignedData(data []byte) (*SignedData, error) { + content, err := asn1.Marshal(data) + if err != nil { + return nil, err + } + ci := contentInfo{ + ContentType: oidData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, + } + digAlg := pkix.AlgorithmIdentifier{ + Algorithm: oidDigestAlgorithmSHA1, + } + h := crypto.SHA1.New() + h.Write(data) + md := h.Sum(nil) + sd := signedData{ + ContentInfo: ci, + Version: 1, + DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{digAlg}, + } + return &SignedData{sd: sd, messageDigest: md}, nil +} + +type attributes struct { + types []asn1.ObjectIdentifier + values []interface{} +} + +// Add adds the attribute, maintaining insertion order +func (attrs *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) { + attrs.types = append(attrs.types, attrType) + attrs.values = append(attrs.values, value) +} + +type sortableAttribute struct { + SortKey []byte + Attribute attribute +} + +type attributeSet []sortableAttribute + +func (sa attributeSet) Len() int { + return len(sa) +} + +func (sa attributeSet) Less(i, j int) bool { + return bytes.Compare(sa[i].SortKey, sa[j].SortKey) < 0 +} + +func (sa attributeSet) Swap(i, j int) { + sa[i], sa[j] = sa[j], sa[i] +} + +func (sa attributeSet) Attributes() []attribute { + attrs := make([]attribute, len(sa)) + for i, attr := range sa { + attrs[i] = attr.Attribute + } + return attrs +} + +func (attrs *attributes) ForMarshaling() ([]attribute, error) { + sortables := make(attributeSet, len(attrs.types)) + for i := range sortables { + attrType := attrs.types[i] + attrValue := attrs.values[i] + asn1Value, err := asn1.Marshal(attrValue) + if err != nil { + return nil, err + } + attr := attribute{ + Type: attrType, + Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag + } + encoded, err := asn1.Marshal(attr) + if err != nil { + return nil, err + } + sortables[i] = sortableAttribute{ + SortKey: encoded, + Attribute: attr, + } + } + sort.Sort(sortables) + return sortables.Attributes(), nil +} + +// AddSigner signs attributes about the content and adds certificate to payload +func (sd *SignedData) AddSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error { + attrs := &attributes{} + attrs.Add(oidAttributeContentType, sd.sd.ContentInfo.ContentType) + attrs.Add(oidAttributeMessageDigest, sd.messageDigest) + attrs.Add(oidAttributeSigningTime, time.Now()) + for _, attr := range config.ExtraSignedAttributes { + attrs.Add(attr.Type, attr.Value) + } + finalAttrs, err := attrs.ForMarshaling() + if err != nil { + return err + } + signature, err := signAttributes(finalAttrs, pkey, crypto.SHA1) + if err != nil { + return err + } + + ias, err := cert2issuerAndSerial(cert) + if err != nil { + return err + } + + signer := signerInfo{ + AuthenticatedAttributes: finalAttrs, + DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidDigestAlgorithmSHA1}, + DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidSignatureSHA1WithRSA}, + IssuerAndSerialNumber: ias, + EncryptedDigest: signature, + Version: 1, + } + // create signature of signed attributes + sd.certs = append(sd.certs, cert) + sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer) + return nil +} + +// AddCertificate adds the certificate to the payload. Useful for parent certificates +func (sd *SignedData) AddCertificate(cert *x509.Certificate) { + sd.certs = append(sd.certs, cert) +} + +// Detach removes content from the signed data struct to make it a detached signature. +// This must be called right before Finish() +func (sd *SignedData) Detach() { + sd.sd.ContentInfo = contentInfo{ContentType: oidSignedData} +} + +// Finish marshals the content and its signers +func (sd *SignedData) Finish() ([]byte, error) { + sd.sd.Certificates = marshalCertificates(sd.certs) + inner, err := asn1.Marshal(sd.sd) + if err != nil { + return nil, err + } + outer := contentInfo{ + ContentType: oidSignedData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true}, + } + return asn1.Marshal(outer) +} + +func cert2issuerAndSerial(cert *x509.Certificate) (issuerAndSerial, error) { + var ias issuerAndSerial + // The issuer RDNSequence has to match exactly the sequence in the certificate + // We cannot use cert.Issuer.ToRDNSequence() here since it mangles the sequence + ias.IssuerName = asn1.RawValue{FullBytes: cert.RawIssuer} + ias.SerialNumber = cert.SerialNumber + + return ias, nil +} + +// signs the DER encoded form of the attributes with the private key +func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) { + attrBytes, err := marshalAttributes(attrs) + if err != nil { + return nil, err + } + h := hash.New() + h.Write(attrBytes) + hashed := h.Sum(nil) + switch priv := pkey.(type) { + case *rsa.PrivateKey: + return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed) + } + return nil, ErrUnsupportedAlgorithm +} + +// concats and wraps the certificates in the RawValue structure +func marshalCertificates(certs []*x509.Certificate) rawCertificates { + var buf bytes.Buffer + for _, cert := range certs { + buf.Write(cert.Raw) + } + rawCerts, _ := marshalCertificateBytes(buf.Bytes()) + return rawCerts +} + +// Even though, the tag & length are stripped out during marshalling the +// RawContent, we have to encode it into the RawContent. If its missing, +// then `asn1.Marshal()` will strip out the certificate wrapper instead. +func marshalCertificateBytes(certs []byte) (rawCertificates, error) { + var val = asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true} + b, err := asn1.Marshal(val) + if err != nil { + return rawCertificates{}, err + } + return rawCertificates{Raw: b}, nil +} + +// DegenerateCertificate creates a signed data structure containing only the +// provided certificate or certificate chain. +func DegenerateCertificate(cert []byte) ([]byte, error) { + rawCert, err := marshalCertificateBytes(cert) + if err != nil { + return nil, err + } + emptyContent := contentInfo{ContentType: oidData} + sd := signedData{ + Version: 1, + ContentInfo: emptyContent, + Certificates: rawCert, + CRLs: []pkix.CertificateList{}, + } + content, err := asn1.Marshal(sd) + if err != nil { + return nil, err + } + signedContent := contentInfo{ + ContentType: oidSignedData, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, + } + return asn1.Marshal(signedContent) +} + +const ( + EncryptionAlgorithmDESCBC = iota + EncryptionAlgorithmAES128GCM +) + +// ContentEncryptionAlgorithm determines the algorithm used to encrypt the +// plaintext message. Change the value of this variable to change which +// algorithm is used in the Encrypt() function. +var ContentEncryptionAlgorithm = EncryptionAlgorithmDESCBC + +// ErrUnsupportedEncryptionAlgorithm is returned when attempting to encrypt +// content with an unsupported algorithm. +var ErrUnsupportedEncryptionAlgorithm = errors.New("pkcs7: cannot encrypt content: only DES-CBC and AES-128-GCM supported") + +const nonceSize = 12 + +type aesGCMParameters struct { + Nonce []byte `asn1:"tag:4"` + ICVLen int +} + +func encryptAES128GCM(content []byte) ([]byte, *encryptedContentInfo, error) { + // Create AES key and nonce + key := make([]byte, 16) + nonce := make([]byte, nonceSize) + + _, err := rand.Read(key) + if err != nil { + return nil, nil, err + } + + _, err = rand.Read(nonce) + if err != nil { + return nil, nil, err + } + + // Encrypt content + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, nil, err + } + + ciphertext := gcm.Seal(nil, nonce, content, nil) + + // Prepare ASN.1 Encrypted Content Info + paramSeq := aesGCMParameters{ + Nonce: nonce, + ICVLen: gcm.Overhead(), + } + + paramBytes, err := asn1.Marshal(paramSeq) + if err != nil { + return nil, nil, err + } + + eci := encryptedContentInfo{ + ContentType: oidData, + ContentEncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: oidEncryptionAlgorithmAES128GCM, + Parameters: asn1.RawValue{ + Tag: asn1.TagSequence, + Bytes: paramBytes, + }, + }, + EncryptedContent: marshalEncryptedContent(ciphertext), + } + + return key, &eci, nil +} + +func encryptDESCBC(content []byte) ([]byte, *encryptedContentInfo, error) { + // Create DES key & CBC IV + key := make([]byte, 8) + iv := make([]byte, des.BlockSize) + _, err := rand.Read(key) + if err != nil { + return nil, nil, err + } + _, err = rand.Read(iv) + if err != nil { + return nil, nil, err + } + + // Encrypt padded content + block, err := des.NewCipher(key) + if err != nil { + return nil, nil, err + } + mode := cipher.NewCBCEncrypter(block, iv) + plaintext, err := pad(content, mode.BlockSize()) + cyphertext := make([]byte, len(plaintext)) + mode.CryptBlocks(cyphertext, plaintext) + + // Prepare ASN.1 Encrypted Content Info + eci := encryptedContentInfo{ + ContentType: oidData, + ContentEncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: oidEncryptionAlgorithmDESCBC, + Parameters: asn1.RawValue{Tag: 4, Bytes: iv}, + }, + EncryptedContent: marshalEncryptedContent(cyphertext), + } + + return key, &eci, nil +} + +// Encrypt creates and returns an envelope data PKCS7 structure with encrypted +// recipient keys for each recipient public key. +// +// The algorithm used to perform encryption is determined by the current value +// of the global ContentEncryptionAlgorithm package variable. By default, the +// value is EncryptionAlgorithmDESCBC. To use a different algorithm, change the +// value before calling Encrypt(). For example: +// +// ContentEncryptionAlgorithm = EncryptionAlgorithmAES128GCM +// +// TODO(fullsailor): Add support for encrypting content with other algorithms +func Encrypt(content []byte, recipients []*x509.Certificate) ([]byte, error) { + var eci *encryptedContentInfo + var key []byte + var err error + + // Apply chosen symmetric encryption method + switch ContentEncryptionAlgorithm { + case EncryptionAlgorithmDESCBC: + key, eci, err = encryptDESCBC(content) + + case EncryptionAlgorithmAES128GCM: + key, eci, err = encryptAES128GCM(content) + + default: + return nil, ErrUnsupportedEncryptionAlgorithm + } + + if err != nil { + return nil, err + } + + // Prepare each recipient's encrypted cipher key + recipientInfos := make([]recipientInfo, len(recipients)) + for i, recipient := range recipients { + encrypted, err := encryptKey(key, recipient) + if err != nil { + return nil, err + } + ias, err := cert2issuerAndSerial(recipient) + if err != nil { + return nil, err + } + info := recipientInfo{ + Version: 0, + IssuerAndSerialNumber: ias, + KeyEncryptionAlgorithm: pkix.AlgorithmIdentifier{ + Algorithm: oidEncryptionAlgorithmRSA, + }, + EncryptedKey: encrypted, + } + recipientInfos[i] = info + } + + // Prepare envelope content + envelope := envelopedData{ + EncryptedContentInfo: *eci, + Version: 0, + RecipientInfos: recipientInfos, + } + innerContent, err := asn1.Marshal(envelope) + if err != nil { + return nil, err + } + + // Prepare outer payload structure + wrapper := contentInfo{ + ContentType: oidEnvelopedData, + Content: asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: innerContent}, + } + + return asn1.Marshal(wrapper) +} + +func marshalEncryptedContent(content []byte) asn1.RawValue { + asn1Content, _ := asn1.Marshal(content) + return asn1.RawValue{Tag: 0, Class: 2, Bytes: asn1Content, IsCompound: true} +} + +func encryptKey(key []byte, recipient *x509.Certificate) ([]byte, error) { + if pub := recipient.PublicKey.(*rsa.PublicKey); pub != nil { + return rsa.EncryptPKCS1v15(rand.Reader, pub, key) + } + return nil, ErrUnsupportedAlgorithm +} diff --git a/common/crypto/pkcs7/pkcs7_test.go b/common/crypto/pkcs7/pkcs7_test.go new file mode 100644 index 00000000..330f87e5 --- /dev/null +++ b/common/crypto/pkcs7/pkcs7_test.go @@ -0,0 +1,680 @@ +package pkcs7 + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "encoding/pem" + "fmt" + "io" + "io/ioutil" + "math/big" + "os" + "os/exec" + "testing" + "time" + + "github.com/gunnsth/crypto/asn1" + "github.com/gunnsth/crypto/x509" + "github.com/gunnsth/crypto/x509/pkix" +) + +func TestVerify(t *testing.T) { + fixture := UnmarshalTestFixture(SignedTestFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Errorf("Parse encountered unexpected error: %v", err) + } + + if err := p7.Verify(); err != nil { + t.Errorf("Verify failed with error: %v", err) + } + expected := []byte("We the People") + if bytes.Compare(p7.Content, expected) != 0 { + t.Errorf("Signed content does not match.\n\tExpected:%s\n\tActual:%s", expected, p7.Content) + + } +} + +func TestVerifyEC2(t *testing.T) { + fixture := UnmarshalTestFixture(EC2IdentityDocumentFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Errorf("Parse encountered unexpected error: %v", err) + } + p7.Certificates = []*x509.Certificate{fixture.Certificate} + if err := p7.Verify(); err != nil { + t.Errorf("Verify failed with error: %v", err) + } +} + +func TestVerifyAppStore(t *testing.T) { + fixture := UnmarshalTestFixture(AppStoreRecieptFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Errorf("Parse encountered unexpected error: %v", err) + } + if err := p7.Verify(); err != nil { + t.Errorf("Verify failed with error: %v", err) + } +} + +func TestDecrypt(t *testing.T) { + fixture := UnmarshalTestFixture(EncryptedTestFixture) + p7, err := Parse(fixture.Input) + if err != nil { + t.Fatal(err) + } + content, err := p7.Decrypt(fixture.Certificate, fixture.PrivateKey) + if err != nil { + t.Errorf("Cannot Decrypt with error: %v", err) + } + expected := []byte("This is a test") + if bytes.Compare(content, expected) != 0 { + t.Errorf("Decrypted result does not match.\n\tExpected:%s\n\tActual:%s", expected, content) + } +} + +func TestDegenerateCertificate(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + deg, err := DegenerateCertificate(cert.Certificate.Raw) + if err != nil { + t.Fatal(err) + } + testOpenSSLParse(t, deg) + pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: deg}) +} + +// writes the cert to a temporary file and tests that openssl can read it. +func testOpenSSLParse(t *testing.T, certBytes []byte) { + tmpCertFile, err := ioutil.TempFile("", "testCertificate") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpCertFile.Name()) // clean up + + if _, err := tmpCertFile.Write(certBytes); err != nil { + t.Fatal(err) + } + + opensslCMD := exec.Command("openssl", "pkcs7", "-inform", "der", "-in", tmpCertFile.Name()) + _, err = opensslCMD.Output() + if err != nil { + t.Fatal(err) + } + + if err := tmpCertFile.Close(); err != nil { + t.Fatal(err) + } + +} + +func TestSign(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + content := []byte("Hello World") + for _, testDetach := range []bool{false, true} { + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + if testDetach { + t.Log("Testing detached signature") + toBeSigned.Detach() + } else { + t.Log("Testing attached signature") + } + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: signed}) + p7, err := Parse(signed) + if err != nil { + t.Fatalf("Cannot parse our signed data: %s", err) + } + if testDetach { + p7.Content = content + } + if bytes.Compare(content, p7.Content) != 0 { + t.Errorf("Our content was not in the parsed data:\n\tExpected: %s\n\tActual: %s", content, p7.Content) + } + if err := p7.Verify(); err != nil { + t.Errorf("Cannot verify our signed data: %s", err) + } + } +} + +func ExampleSignedData() { + // generate a signing cert or load a key pair + cert, err := createTestCertificate() + if err != nil { + fmt.Printf("Cannot create test certificates: %s", err) + } + + // Initialize a SignedData struct with content to be signed + signedData, err := NewSignedData([]byte("Example data to be signed")) + if err != nil { + fmt.Printf("Cannot initialize signed data: %s", err) + } + + // Add the signing cert and private key + if err := signedData.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil { + fmt.Printf("Cannot add signer: %s", err) + } + + // Call Detach() is you want to remove content from the signature + // and generate an S/MIME detached signature + signedData.Detach() + + // Finish() to obtain the signature bytes + detachedSignature, err := signedData.Finish() + if err != nil { + fmt.Printf("Cannot finish signing data: %s", err) + } + pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: detachedSignature}) +} + +func TestOpenSSLVerifyDetachedSignature(t *testing.T) { + rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil) + if err != nil { + t.Fatalf("Cannot generate root cert: %s", err) + } + signerCert, err := createTestCertificateByIssuer("PKCS7 Test Signer Cert", rootCert) + if err != nil { + t.Fatalf("Cannot generate signer cert: %s", err) + } + content := []byte("Hello World") + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + if err := toBeSigned.AddSigner(signerCert.Certificate, signerCert.PrivateKey, SignerInfoConfig{}); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + toBeSigned.Detach() + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + + // write the root cert to a temp file + tmpRootCertFile, err := ioutil.TempFile("", "pkcs7TestRootCA") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpRootCertFile.Name()) // clean up + fd, err := os.OpenFile(tmpRootCertFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + t.Fatal(err) + } + pem.Encode(fd, &pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Certificate.Raw}) + fd.Close() + + // write the signature to a temp file + tmpSignatureFile, err := ioutil.TempFile("", "pkcs7Signature") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpSignatureFile.Name()) // clean up + ioutil.WriteFile(tmpSignatureFile.Name(), signed, 0755) + + // write the content to a temp file + tmpContentFile, err := ioutil.TempFile("", "pkcs7Content") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpContentFile.Name()) // clean up + ioutil.WriteFile(tmpContentFile.Name(), content, 0755) + + // call openssl to verify the signature on the content using the root + opensslCMD := exec.Command("openssl", "smime", "-verify", + "-in", tmpSignatureFile.Name(), "-inform", "DER", + "-content", tmpContentFile.Name(), + "-CAfile", tmpRootCertFile.Name()) + out, err := opensslCMD.Output() + t.Logf("%s", out) + if err != nil { + t.Fatalf("openssl command failed with %s", err) + } +} + +func TestEncrypt(t *testing.T) { + modes := []int{ + EncryptionAlgorithmDESCBC, + EncryptionAlgorithmAES128GCM, + } + + for _, mode := range modes { + ContentEncryptionAlgorithm = mode + + plaintext := []byte("Hello Secret World!") + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + encrypted, err := Encrypt(plaintext, []*x509.Certificate{cert.Certificate}) + if err != nil { + t.Fatal(err) + } + p7, err := Parse(encrypted) + if err != nil { + t.Fatalf("cannot Parse encrypted result: %s", err) + } + result, err := p7.Decrypt(cert.Certificate, cert.PrivateKey) + if err != nil { + t.Fatalf("cannot Decrypt encrypted result: %s", err) + } + if bytes.Compare(plaintext, result) != 0 { + t.Errorf("encrypted data does not match plaintext:\n\tExpected: %s\n\tActual: %s", plaintext, result) + } + } +} + +func TestUnmarshalSignedAttribute(t *testing.T) { + cert, err := createTestCertificate() + if err != nil { + t.Fatal(err) + } + content := []byte("Hello World") + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("Cannot initialize signed data: %s", err) + } + oidTest := asn1.ObjectIdentifier{2, 3, 4, 5, 6, 7} + testValue := "TestValue" + if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{ + ExtraSignedAttributes: []Attribute{Attribute{Type: oidTest, Value: testValue}}, + }); err != nil { + t.Fatalf("Cannot add signer: %s", err) + } + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("Cannot finish signing data: %s", err) + } + p7, err := Parse(signed) + var actual string + err = p7.UnmarshalSignedAttribute(oidTest, &actual) + if err != nil { + t.Fatalf("Cannot unmarshal test value: %s", err) + } + if testValue != actual { + t.Errorf("Attribute does not match test value\n\tExpected: %s\n\tActual: %s", testValue, actual) + } +} + +func TestPad(t *testing.T) { + tests := []struct { + Original []byte + Expected []byte + BlockSize int + }{ + {[]byte{0x1, 0x2, 0x3, 0x10}, []byte{0x1, 0x2, 0x3, 0x10, 0x4, 0x4, 0x4, 0x4}, 8}, + {[]byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0}, []byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8}, 8}, + } + for _, test := range tests { + padded, err := pad(test.Original, test.BlockSize) + if err != nil { + t.Errorf("pad encountered error: %s", err) + continue + } + if bytes.Compare(test.Expected, padded) != 0 { + t.Errorf("pad results mismatch:\n\tExpected: %X\n\tActual: %X", test.Expected, padded) + } + } +} + +type certKeyPair struct { + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey +} + +func createTestCertificate() (certKeyPair, error) { + signer, err := createTestCertificateByIssuer("Eddard Stark", nil) + if err != nil { + return certKeyPair{}, err + } + fmt.Println("Created root cert") + pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: signer.Certificate.Raw}) + pair, err := createTestCertificateByIssuer("Jon Snow", signer) + if err != nil { + return certKeyPair{}, err + } + fmt.Println("Created signer cert") + pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: pair.Certificate.Raw}) + return *pair, nil +} + +func createTestCertificateByIssuer(name string, issuer *certKeyPair) (*certKeyPair, error) { + priv, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + return nil, err + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 32) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + SignatureAlgorithm: x509.SHA256WithRSA, + Subject: pkix.Name{ + CommonName: name, + Organization: []string{"Acme Co"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}, + } + var issuerCert *x509.Certificate + var issuerKey crypto.PrivateKey + if issuer != nil { + issuerCert = issuer.Certificate + issuerKey = issuer.PrivateKey + } else { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + issuerCert = &template + issuerKey = priv + } + cert, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, priv.Public(), issuerKey) + if err != nil { + return nil, err + } + leaf, err := x509.ParseCertificate(cert) + if err != nil { + return nil, err + } + return &certKeyPair{ + Certificate: leaf, + PrivateKey: priv, + }, nil +} + +type TestFixture struct { + Input []byte + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey +} + +func UnmarshalTestFixture(testPEMBlock string) TestFixture { + var result TestFixture + var derBlock *pem.Block + var pemBlock = []byte(testPEMBlock) + for { + derBlock, pemBlock = pem.Decode(pemBlock) + if derBlock == nil { + break + } + switch derBlock.Type { + case "PKCS7": + result.Input = derBlock.Bytes + case "CERTIFICATE": + result.Certificate, _ = x509.ParseCertificate(derBlock.Bytes) + case "PRIVATE KEY": + result.PrivateKey, _ = x509.ParsePKCS1PrivateKey(derBlock.Bytes) + } + } + + return result +} + +func MarshalTestFixture(t TestFixture, w io.Writer) { + if t.Input != nil { + pem.Encode(w, &pem.Block{Type: "PKCS7", Bytes: t.Input}) + } + if t.Certificate != nil { + pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: t.Certificate.Raw}) + } + if t.PrivateKey != nil { + pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(t.PrivateKey)}) + } +} + +var SignedTestFixture = ` +-----BEGIN PKCS7----- +MIIDVgYJKoZIhvcNAQcCoIIDRzCCA0MCAQExCTAHBgUrDgMCGjAcBgkqhkiG9w0B +BwGgDwQNV2UgdGhlIFBlb3BsZaCCAdkwggHVMIIBQKADAgECAgRpuDctMAsGCSqG +SIb3DQEBCzApMRAwDgYDVQQKEwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3Rh +cmswHhcNMTUwNTA2MDQyNDQ4WhcNMTYwNTA2MDQyNDQ4WjAlMRAwDgYDVQQKEwdB +Y21lIENvMREwDwYDVQQDEwhKb24gU25vdzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEAqr+tTF4mZP5rMwlXp1y+crRtFpuLXF1zvBZiYMfIvAHwo1ta8E1IcyEP +J1jIiKMcwbzeo6kAmZzIJRCTezq9jwXUsKbQTvcfOH9HmjUmXBRWFXZYoQs/OaaF +a45deHmwEeMQkuSWEtYiVKKZXtJOtflKIT3MryJEDiiItMkdybUCAwEAAaMSMBAw +DgYDVR0PAQH/BAQDAgCgMAsGCSqGSIb3DQEBCwOBgQDK1EweZWRL+f7Z+J0kVzY8 +zXptcBaV4Lf5wGZJLJVUgp33bpLNpT3yadS++XQJ+cvtW3wADQzBSTMduyOF8Zf+ +L7TjjrQ2+F2HbNbKUhBQKudxTfv9dJHdKbD+ngCCdQJYkIy2YexsoNG0C8nQkggy +axZd/J69xDVx6pui3Sj8sDGCATYwggEyAgEBMDEwKTEQMA4GA1UEChMHQWNtZSBD +bzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrAgRpuDctMAcGBSsOAwIaoGEwGAYJKoZI +hvcNAQkDMQsGCSqGSIb3DQEHATAgBgkqhkiG9w0BCQUxExcRMTUwNTA2MDAyNDQ4 +LTA0MDAwIwYJKoZIhvcNAQkEMRYEFG9D7gcTh9zfKiYNJ1lgB0yTh4sZMAsGCSqG +SIb3DQEBAQSBgFF3sGDU9PtXty/QMtpcFa35vvIOqmWQAIZt93XAskQOnBq4OloX +iL9Ct7t1m4pzjRm0o9nDkbaSLZe7HKASHdCqijroScGlI8M+alJ8drHSFv6ZIjnM +FIwIf0B2Lko6nh9/6mUXq7tbbIHa3Gd1JUVire/QFFtmgRXMbXYk8SIS +-----END PKCS7----- +-----BEGIN CERTIFICATE----- +MIIB1TCCAUCgAwIBAgIEabg3LTALBgkqhkiG9w0BAQswKTEQMA4GA1UEChMHQWNt +ZSBDbzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrMB4XDTE1MDUwNjA0MjQ0OFoXDTE2 +MDUwNjA0MjQ0OFowJTEQMA4GA1UEChMHQWNtZSBDbzERMA8GA1UEAxMISm9uIFNu +b3cwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKq/rUxeJmT+azMJV6dcvnK0 +bRabi1xdc7wWYmDHyLwB8KNbWvBNSHMhDydYyIijHMG83qOpAJmcyCUQk3s6vY8F +1LCm0E73Hzh/R5o1JlwUVhV2WKELPzmmhWuOXXh5sBHjEJLklhLWIlSimV7STrX5 +SiE9zK8iRA4oiLTJHcm1AgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIAoDALBgkqhkiG +9w0BAQsDgYEAytRMHmVkS/n+2fidJFc2PM16bXAWleC3+cBmSSyVVIKd926SzaU9 +8mnUvvl0CfnL7Vt8AA0MwUkzHbsjhfGX/i+04460Nvhdh2zWylIQUCrncU37/XSR +3Smw/p4AgnUCWJCMtmHsbKDRtAvJ0JIIMmsWXfyevcQ1ceqbot0o/LA= +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIICXgIBAAKBgQCqv61MXiZk/mszCVenXL5ytG0Wm4tcXXO8FmJgx8i8AfCjW1rw +TUhzIQ8nWMiIoxzBvN6jqQCZnMglEJN7Or2PBdSwptBO9x84f0eaNSZcFFYVdlih +Cz85poVrjl14ebAR4xCS5JYS1iJUople0k61+UohPcyvIkQOKIi0yR3JtQIDAQAB +AoGBAIPLCR9N+IKxodq11lNXEaUFwMHXc1zqwP8no+2hpz3+nVfplqqubEJ4/PJY +5AgbJoIfnxVhyBXJXu7E+aD/OPneKZrgp58YvHKgGvvPyJg2gpC/1Fh0vQB0HNpI +1ZzIZUl8ZTUtVgtnCBUOh5JGI4bFokAqrT//Uvcfd+idgxqBAkEA1ZbP/Kseld14 +qbWmgmU5GCVxsZRxgR1j4lG3UVjH36KXMtRTm1atAam1uw3OEGa6Y3ANjpU52FaB +Hep5rkk4FQJBAMynMo1L1uiN5GP+KYLEF5kKRxK+FLjXR0ywnMh+gpGcZDcOae+J ++t1gLoWBIESH/Xt639T7smuSfrZSA9V0EyECQA8cvZiWDvLxmaEAXkipmtGPjKzQ +4PsOtkuEFqFl07aKDYKmLUg3aMROWrJidqsIabWxbvQgsNgSvs38EiH3wkUCQQCg +ndxb7piVXb9RBwm3OoU2tE1BlXMX+sVXmAkEhd2dwDsaxrI3sHf1xGXem5AimQRF +JBOFyaCnMotGNioSHY5hAkEAxyXcNixQ2RpLXJTQZtwnbk0XDcbgB+fBgXnv/4f3 +BCvcu85DqJeJyQv44Oe1qsXEX9BfcQIOVaoep35RPlKi9g== +-----END PRIVATE KEY-----` + +// echo -n "This is a test" > test.txt +// openssl cms -encrypt -in test.txt cert.pem +var EncryptedTestFixture = ` +-----BEGIN PKCS7----- +MIIBGgYJKoZIhvcNAQcDoIIBCzCCAQcCAQAxgcwwgckCAQAwMjApMRAwDgYDVQQK +EwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3RhcmsCBQDL+CvWMA0GCSqGSIb3 +DQEBAQUABIGAyFz7bfI2noUs4FpmYfztm1pVjGyB00p9x0H3gGHEYNXdqlq8VG8d +iq36poWtEkatnwsOlURWZYECSi0g5IAL0U9sj82EN0xssZNaK0S5FTGnB3DPvYgt +HJvcKq7YvNLKMh4oqd17C6GB4oXyEBDj0vZnL7SUoCAOAWELPeC8CTUwMwYJKoZI +hvcNAQcBMBQGCCqGSIb3DQMHBAhEowTkot3a7oAQFD//J/IhFnk+JbkH7HZQFA== +-----END PKCS7----- +-----BEGIN CERTIFICATE----- +MIIB1jCCAUGgAwIBAgIFAMv4K9YwCwYJKoZIhvcNAQELMCkxEDAOBgNVBAoTB0Fj +bWUgQ28xFTATBgNVBAMTDEVkZGFyZCBTdGFyazAeFw0xNTA1MDYwMzU2NDBaFw0x +NjA1MDYwMzU2NDBaMCUxEDAOBgNVBAoTB0FjbWUgQ28xETAPBgNVBAMTCEpvbiBT +bm93MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK6NU0R0eiCYVquU4RcjKc +LzGfx0aa1lMr2TnLQUSeLFZHFxsyyMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg +8+Zg2r8xnnney7abxcuv0uATWSIeKlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP ++Zxp2ni5qHNraf3wE2VPIQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAKAwCwYJKoZI +hvcNAQELA4GBAIr2F7wsqmEU/J/kLyrCgEVXgaV/sKZq4pPNnzS0tBYk8fkV3V18 +sBJyHKRLL/wFZASvzDcVGCplXyMdAOCyfd8jO3F9Ac/xdlz10RrHJT75hNu3a7/n +9KNwKhfN4A1CQv2x372oGjRhCW5bHNCWx4PIVeNzCyq/KZhyY9sxHE6f +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIICXgIBAAKBgQDK6NU0R0eiCYVquU4RcjKcLzGfx0aa1lMr2TnLQUSeLFZHFxsy +yMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg8+Zg2r8xnnney7abxcuv0uATWSIe +KlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP+Zxp2ni5qHNraf3wE2VPIQIDAQAB +AoGBALyvnSt7KUquDen7nXQtvJBudnf9KFPt//OjkdHHxNZNpoF/JCSqfQeoYkeu +MdAVYNLQGMiRifzZz4dDhA9xfUAuy7lcGQcMCxEQ1dwwuFaYkawbS0Tvy2PFlq2d +H5/HeDXU4EDJ3BZg0eYj2Bnkt1sJI35UKQSxblQ0MY2q0uFBAkEA5MMOogkgUx1C +67S1tFqMUSM8D0mZB0O5vOJZC5Gtt2Urju6vywge2ArExWRXlM2qGl8afFy2SgSv +Xk5eybcEiQJBAOMRwwbEoW5NYHuFFbSJyWll4n71CYuWuQOCzehDPyTb80WFZGLV +i91kFIjeERyq88eDE5xVB3ZuRiXqaShO/9kCQQCKOEkpInaDgZSjskZvuJ47kByD +6CYsO4GIXQMMeHML8ncFH7bb6AYq5ybJVb2NTU7QLFJmfeYuhvIm+xdOreRxAkEA +o5FC5Jg2FUfFzZSDmyZ6IONUsdF/i78KDV5nRv1R+hI6/oRlWNCtTNBv/lvBBd6b +dseUE9QoaQZsn5lpILEvmQJAZ0B+Or1rAYjnbjnUhdVZoy9kC4Zov+4UH3N/BtSy +KJRWUR0wTWfZBPZ5hAYZjTBEAFULaYCXlQKsODSp0M1aQA== +-----END PRIVATE KEY-----` + +var EC2IdentityDocumentFixture = ` +-----BEGIN PKCS7----- +MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA +JIAEggGmewogICJwcml2YXRlSXAiIDogIjE3Mi4zMC4wLjI1MiIsCiAgImRldnBh +eVByb2R1Y3RDb2RlcyIgOiBudWxsLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1 +cy1lYXN0LTFhIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3Rh +bmNlSWQiIDogImktZjc5ZmU1NmMiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVs +bCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIg +OiAiMTIxNjU5MDE0MzM0IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwK +ICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDhUMDM6MDE6MzhaIiwKICAiYXJj +aGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJy +YW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAA +AAAxggEYMIIBFAIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5n +dG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2Vi +IFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0B +CQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDgwMzAxNDRaMCMG +CSqGSIb3DQEJBDEWBBTuUc28eBXmImAautC+wOjqcFCBVjAJBgcqhkjOOAQDBC8w +LQIVAKA54NxGHWWCz5InboDmY/GHs33nAhQ6O/ZI86NwjA9Vz3RNMUJrUPU5tAAA +AAAAAA== +-----END PKCS7----- +-----BEGIN CERTIFICATE----- +MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw +FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD +VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z +ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u +IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl +cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e +ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 +VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P +hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j +k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U +hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF +lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf +MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW +MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw +vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw +7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K +-----END CERTIFICATE-----` + +var AppStoreRecieptFixture = ` +-----BEGIN PKCS7----- +MIITtgYJKoZIhvcNAQcCoIITpzCCE6MCAQExCzAJBgUrDgMCGgUAMIIDVwYJKoZI +hvcNAQcBoIIDSASCA0QxggNAMAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQEC +AQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADAL +AgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQE +AgIAjTANAgENAgEBBAUCAwFgvTANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAy +NDcwGAIBAgIBAQQQDA5jb20uemhpaHUudGVzdDAYAgEEAgECBBCS+ZODNMHwT1Nz +gWYDXyWZMBsCAQACAQEEEwwRUHJvZHVjdGlvblNhbmRib3gwHAIBBQIBAQQU4nRh +YCEZx70Flzv7hvJRjJZckYIwHgIBDAIBAQQWFhQyMDE2LTA3LTIzVDA2OjIxOjEx +WjAeAgESAgEBBBYWFDIwMTMtMDgtMDFUMDc6MDA6MDBaMD0CAQYCAQEENbR21I+a +8+byMXo3NPRoDWQmSXQF2EcCeBoD4GaL//ZCRETp9rGFPSg1KekCP7Kr9HAqw09m +MEICAQcCAQEEOlVJozYYBdugybShbiiMsejDMNeCbZq6CrzGBwW6GBy+DGWxJI91 +Y3ouXN4TZUhuVvLvN1b0m5T3ggQwggFaAgERAgEBBIIBUDGCAUwwCwICBqwCAQEE +AhYAMAsCAgatAgEBBAIMADALAgIGsAIBAQQCFgAwCwICBrICAQEEAgwAMAsCAgaz +AgEBBAIMADALAgIGtAIBAQQCDAAwCwICBrUCAQEEAgwAMAsCAga2AgEBBAIMADAM +AgIGpQIBAQQDAgEBMAwCAgarAgEBBAMCAQEwDAICBq4CAQEEAwIBADAMAgIGrwIB +AQQDAgEAMAwCAgaxAgEBBAMCAQAwGwICBqcCAQEEEgwQMTAwMDAwMDIyNTMyNTkw +MTAbAgIGqQIBAQQSDBAxMDAwMDAwMjI1MzI1OTAxMB8CAgaoAgEBBBYWFDIwMTYt +MDctMjNUMDY6MjE6MTFaMB8CAgaqAgEBBBYWFDIwMTYtMDctMjNUMDY6MjE6MTFa +MCACAgamAgEBBBcMFWNvbS56aGlodS50ZXN0LnRlc3RfMaCCDmUwggV8MIIEZKAD +AgECAggO61eH554JjTANBgkqhkiG9w0BAQUFADCBljELMAkGA1UEBhMCVVMxEzAR +BgNVBAoMCkFwcGxlIEluYy4xLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZl +bG9wZXIgUmVsYXRpb25zMUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxv +cGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNTExMTMw +MjE1MDlaFw0yMzAyMDcyMTQ4NDdaMIGJMTcwNQYDVQQDDC5NYWMgQXBwIFN0b3Jl +IGFuZCBpVHVuZXMgU3RvcmUgUmVjZWlwdCBTaWduaW5nMSwwKgYDVQQLDCNBcHBs +ZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczETMBEGA1UECgwKQXBwbGUg +SW5jLjELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQClz4H9JaKBW9aH7SPaMxyO4iPApcQmyz3Gn+xKDVWG/6QC15fKOVRtfX+yVBid +xCxScY5ke4LOibpJ1gjltIhxzz9bRi7GxB24A6lYogQ+IXjV27fQjhKNg0xbKmg3 +k8LyvR7E0qEMSlhSqxLj7d0fmBWQNS3CzBLKjUiB91h4VGvojDE2H0oGDEdU8zeQ +uLKSiX1fpIVK4cCc4Lqku4KXY/Qrk8H9Pm/KwfU8qY9SGsAlCnYO3v6Z/v/Ca/Vb +XqxzUUkIVonMQ5DMjoEC0KCXtlyxoWlph5AQaCYmObgdEHOwCl3Fc9DfdjvYLdmI +HuPsB8/ijtDT+iZVge/iA0kjAgMBAAGjggHXMIIB0zA/BggrBgEFBQcBAQQzMDEw +LwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwLmFwcGxlLmNvbS9vY3NwMDMtd3dkcjA0 +MB0GA1UdDgQWBBSRpJz8xHa3n6CK9E31jzZd7SsEhTAMBgNVHRMBAf8EAjAAMB8G +A1UdIwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MIIBHgYDVR0gBIIBFTCCAREw +ggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9u +IHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5j +ZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25k +aXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0 +aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3 +LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wDgYDVR0PAQH/BAQDAgeA +MBAGCiqGSIb3Y2QGCwEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQANphvTLj3jWysH +bkKWbNPojEMwgl/gXNGNvr0PvRr8JZLbjIXDgFnf4+LXLgUUrA3btrj+/DUufMut +F2uOfx/kd7mxZ5W0E16mGYZ2+FogledjjA9z/Ojtxh+umfhlSFyg4Cg6wBA3Lbmg +BDkfc7nIBf3y3n8aKipuKwH8oCBc2et9J6Yz+PWY4L5E27FMZ/xuCk/J4gao0pfz +p45rUaJahHVl0RYEYuPBX/UIqc9o2ZIAycGMs/iNAGS6WGDAfK+PdcppuVsq1h1o +bphC9UynNxmbzDscehlD86Ntv0hgBgw2kivs3hi1EdotI9CO/KBpnBcbnoB7OUdF +MGEvxxOoMIIEIjCCAwqgAwIBAgIIAd68xDltoBAwDQYJKoZIhvcNAQEFBQAwYjEL +MAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxl +IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENB +MB4XDTEzMDIwNzIxNDg0N1oXDTIzMDIwNzIxNDg0N1owgZYxCzAJBgNVBAYTAlVT +MRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUg +RGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERl +dmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKOFSmy1aqyCQ5SOmM7uxfuH8mkbw0 +U3rOfGOAYXdkXqUHI7Y5/lAtFVZYcC1+xG7BSoU+L/DehBqhV8mvexj/avoVEkkV +CBmsqtsqMu2WY2hSFT2Miuy/axiV4AOsAX2XBWfODoWVN2rtCbauZ81RZJ/GXNG8 +V25nNYB2NqSHgW44j9grFU57Jdhav06DwY3Sk9UacbVgnJ0zTlX5ElgMhrgWDcHl +d0WNUEi6Ky3klIXh6MSdxmilsKP8Z35wugJZS3dCkTm59c3hTO/AO0iMpuUhXf1q +arunFjVg0uat80YpyejDi+l5wGphZxWy8P3laLxiX27Pmd3vG2P+kmWrAgMBAAGj +gaYwgaMwHQYDVR0OBBYEFIgnFwmpthhgi+zruvZHWcVSVKO3MA8GA1UdEwEB/wQF +MAMBAf8wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wLgYDVR0fBCcw +JTAjoCGgH4YdaHR0cDovL2NybC5hcHBsZS5jb20vcm9vdC5jcmwwDgYDVR0PAQH/ +BAQDAgGGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQBPz+9Z +viz1smwvj+4ThzLoBTWobot9yWkMudkXvHcs1Gfi/ZptOllc34MBvbKuKmFysa/N +w0Uwj6ODDc4dR7Txk4qjdJukw5hyhzs+r0ULklS5MruQGFNrCk4QttkdUGwhgAqJ +TleMa1s8Pab93vcNIx0LSiaHP7qRkkykGRIZbVf1eliHe2iK5IaMSuviSRSqpd1V +AKmuu0swruGgsbwpgOYJd+W+NKIByn/c4grmO7i77LpilfMFY0GCzQ87HUyVpNur ++cmV6U/kTecmmYHpvPm0KdIBembhLoz2IYrF+Hjhga6/05Cdqa3zr/04GpZnMBxR +pVzscYqCtGwPDBUfMIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQsw +CQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0Ew +HhcNMDYwNDI1MjE0MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzET +MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne ++Uts9QerIjAC6Bg++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjcz +y8QPTc4UadHJGXL1XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQ +Z48ItCD3y6wsIG9wtj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCS +C7EhFi501TwN22IWq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINB +hzOKgbEwWOxaBDKMaLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIB +djAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9Bp +R5R2Cf70a40uQKb3R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/ +CF4wggERBgNVHSAEggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcC +ARYeaHR0cHM6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCB +thqBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFz +c3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJk +IHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5 +IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3 +DQEBBQUAA4IBAQBcNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizU +sZAS2L70c5vu0mQPy3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJ +fBdAVhEedNO3iyM7R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr +1KIkIxH3oayPc4FgxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltk +wGMzd/c6ByxW69oPIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIq +xw8dtk2cXmPIS4AXUKqK1drk/NAJBzewdXUhMYIByzCCAccCAQEwgaMwgZYxCzAJ +BgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBX +b3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29y +bGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3Jp +dHkCCA7rV4fnngmNMAkGBSsOAwIaBQAwDQYJKoZIhvcNAQEBBQAEggEAasPtnide +NWyfUtewW9OSgcQA8pW+5tWMR0469cBPZR84uJa0gyfmPspySvbNOAwnrwzZHYLa +ujOxZLip4DUw4F5s3QwUa3y4BXpF4J+NSn9XNvxNtnT/GcEQtCuFwgJ0o3F0ilhv +MTHrwiwyx/vr+uNDqlORK8lfK+1qNp+A/kzh8eszMrn4JSeTh9ZYxLHE56WkTQGD +VZXl0gKgxSOmDrcp1eQxdlymzrPv9U60wUJ0bkPfrU9qZj3mJrmrkQk61JTe3j6/ +QfjfFBG9JG2mUmYQP1KQ3SypGHzDW8vngvsGu//tNU0NFfOqQu4bYU4VpQl0nPtD +4B85NkrgvQsWAQ== +-----END PKCS7-----` diff --git a/common/crypto/pkcs7/x509.go b/common/crypto/pkcs7/x509.go new file mode 100644 index 00000000..5fe4389b --- /dev/null +++ b/common/crypto/pkcs7/x509.go @@ -0,0 +1,135 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the go/golang LICENSE file. + +package pkcs7 + +// These are private constants and functions from the crypto/x509 package that +// are useful when dealing with signatures verified by x509 certificates + +import ( + "bytes" + "crypto" + "encoding/asn1" + + "crypto/x509" + "crypto/x509/pkix" +) + +var ( + oidSignatureRSAEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + oidSignatureMD2WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 2} + oidSignatureMD5WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4} + oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5} + oidSignatureSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} + oidSignatureSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12} + oidSignatureSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13} + oidSignatureRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} + oidSignatureDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3} + oidSignatureDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2} + oidSignatureECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1} + oidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} + oidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3} + oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4} + + oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} + oidSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} + oidSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} + + oidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8} + + // oidISOSignatureSHA1WithRSA means the same as oidSignatureSHA1WithRSA + // but it's specified by ISO. Microsoft's makecert.exe has been known + // to produce certificates with this OID. + oidISOSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 29} +) + +var signatureAlgorithmDetails = []struct { + algo x509.SignatureAlgorithm + name string + oid asn1.ObjectIdentifier + pubKeyAlgo x509.PublicKeyAlgorithm + hash crypto.Hash +}{ + {x509.MD2WithRSA, "MD2-RSA", oidSignatureMD2WithRSA, x509.RSA, crypto.Hash(0) /* no value for MD2 */}, + {x509.MD5WithRSA, "MD5-RSA", oidSignatureMD5WithRSA, x509.RSA, crypto.MD5}, + {x509.SHA1WithRSA, "SHA1-RSA", oidSignatureSHA1WithRSA, x509.RSA, crypto.SHA1}, + {x509.SHA1WithRSA, "SHA1-RSA", oidISOSignatureSHA1WithRSA, x509.RSA, crypto.SHA1}, + {x509.SHA256WithRSA, "SHA256-RSA", oidSignatureSHA256WithRSA, x509.RSA, crypto.SHA256}, + {x509.SHA384WithRSA, "SHA384-RSA", oidSignatureSHA384WithRSA, x509.RSA, crypto.SHA384}, + {x509.SHA512WithRSA, "SHA512-RSA", oidSignatureSHA512WithRSA, x509.RSA, crypto.SHA512}, + {x509.SHA256WithRSAPSS, "SHA256-RSAPSS", oidSignatureRSAPSS, x509.RSA, crypto.SHA256}, + {x509.SHA384WithRSAPSS, "SHA384-RSAPSS", oidSignatureRSAPSS, x509.RSA, crypto.SHA384}, + {x509.SHA512WithRSAPSS, "SHA512-RSAPSS", oidSignatureRSAPSS, x509.RSA, crypto.SHA512}, + {x509.DSAWithSHA1, "DSA-SHA1", oidSignatureDSAWithSHA1, x509.DSA, crypto.SHA1}, + {x509.DSAWithSHA256, "DSA-SHA256", oidSignatureDSAWithSHA256, x509.DSA, crypto.SHA256}, + {x509.ECDSAWithSHA1, "ECDSA-SHA1", oidSignatureECDSAWithSHA1, x509.ECDSA, crypto.SHA1}, + {x509.ECDSAWithSHA256, "ECDSA-SHA256", oidSignatureECDSAWithSHA256, x509.ECDSA, crypto.SHA256}, + {x509.ECDSAWithSHA384, "ECDSA-SHA384", oidSignatureECDSAWithSHA384, x509.ECDSA, crypto.SHA384}, + {x509.ECDSAWithSHA512, "ECDSA-SHA512", oidSignatureECDSAWithSHA512, x509.ECDSA, crypto.SHA512}, +} + +// pssParameters reflects the parameters in an AlgorithmIdentifier that +// specifies RSA PSS. See https://tools.ietf.org/html/rfc3447#appendix-A.2.3 +type pssParameters struct { + // The following three fields are not marked as + // optional because the default values specify SHA-1, + // which is no longer suitable for use in signatures. + Hash pkix.AlgorithmIdentifier `asn1:"explicit,tag:0"` + MGF pkix.AlgorithmIdentifier `asn1:"explicit,tag:1"` + SaltLength int `asn1:"explicit,tag:2"` + TrailerField int `asn1:"optional,explicit,tag:3,default:1"` +} + +// asn1.NullBytes is not available prior to Go 1.9 +var nullBytes = []byte{5, 0} + +func getSignatureAlgorithmFromAI(ai pkix.AlgorithmIdentifier) x509.SignatureAlgorithm { + if !ai.Algorithm.Equal(oidSignatureRSAPSS) { + for _, details := range signatureAlgorithmDetails { + if ai.Algorithm.Equal(details.oid) { + return details.algo + } + } + return x509.UnknownSignatureAlgorithm + } + + // RSA PSS is special because it encodes important parameters + // in the Parameters. + + var params pssParameters + if _, err := asn1.Unmarshal(ai.Parameters.FullBytes, ¶ms); err != nil { + return x509.UnknownSignatureAlgorithm + } + + var mgf1HashFunc pkix.AlgorithmIdentifier + if _, err := asn1.Unmarshal(params.MGF.Parameters.FullBytes, &mgf1HashFunc); err != nil { + return x509.UnknownSignatureAlgorithm + } + + // PSS is greatly overburdened with options. This code forces + // them into three buckets by requiring that the MGF1 hash + // function always match the message hash function (as + // recommended in + // https://tools.ietf.org/html/rfc3447#section-8.1), that the + // salt length matches the hash length, and that the trailer + // field has the default value. + if !bytes.Equal(params.Hash.Parameters.FullBytes, nullBytes) || + !params.MGF.Algorithm.Equal(oidMGF1) || + !mgf1HashFunc.Algorithm.Equal(params.Hash.Algorithm) || + !bytes.Equal(mgf1HashFunc.Parameters.FullBytes, nullBytes) || + params.TrailerField != 1 { + return x509.UnknownSignatureAlgorithm + } + + switch { + case params.Hash.Algorithm.Equal(oidSHA256) && params.SaltLength == 32: + return x509.SHA256WithRSAPSS + case params.Hash.Algorithm.Equal(oidSHA384) && params.SaltLength == 48: + return x509.SHA384WithRSAPSS + case params.Hash.Algorithm.Equal(oidSHA512) && params.SaltLength == 64: + return x509.SHA512WithRSAPSS + } + + return x509.UnknownSignatureAlgorithm +} diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go new file mode 100644 index 00000000..e6e0fe3e --- /dev/null +++ b/pdf/model/appearance.go @@ -0,0 +1,47 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import "github.com/unidoc/unidoc/pdf/core" + +// PdfAppearance contains the common attributes of an appearance form field. +type PdfAppearance struct { + *PdfField + *PdfAnnotationWidget + Signature *PdfSignature +} + +// NewPdfAnnotationWidget returns an initialized annotation widget. +func NewPdfAppearance() *PdfAppearance { + app := &PdfAppearance{} + app.PdfField = NewPdfField() + app.PdfAnnotationWidget = NewPdfAnnotationWidget() + app.PdfField.SetContext(app) + app.PdfAnnotationWidget.SetContext(app) + app.PdfAnnotationWidget.container = app.PdfField.container + return app +} + +// ToPdfObject implements interface PdfModel. +func (app *PdfAppearance) ToPdfObject() core.PdfObject { + if app.Signature != nil { + app.V = app.Signature.ToPdfObject() + } + app.PdfAnnotation.ToPdfObject() + app.PdfField.ToPdfObject() + container := app.container + d := container.PdfObject.(*core.PdfObjectDictionary) + + d.SetIfNotNil("Subtype", core.MakeName("Widget")) + d.SetIfNotNil("H", app.H) + d.SetIfNotNil("MK", app.MK) + d.SetIfNotNil("A", app.A) + d.SetIfNotNil("AA", app.PdfAnnotationWidget.AA) + d.SetIfNotNil("BS", app.BS) + d.SetIfNotNil("Parent", app.PdfAnnotationWidget.Parent) + + return container +} diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 32e5e894..4110cc81 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -13,6 +13,7 @@ import ( "os" "strconv" "strings" + "time" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" @@ -370,6 +371,65 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } } +// Sign a document +func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, appearance *PdfAppearance, err error) { + acroForm = a.Reader.AcroForm + if acroForm == nil { + acroForm = NewPdfAcroForm() + } + pageIndex := pageNum - 1 + var page *PdfPage + for i, p := range a.pages { + if i == pageIndex { + page = p.Duplicate() + break + } + } + if page == nil { + return nil, nil, fmt.Errorf("page %d not found", pageNum) + } + + // TODO add more checks before set the fields + acroForm.SigFlags = core.MakeInteger(3) + acroForm.DA = core.MakeString("/F1 0 Tf 0 g") + n2ResourcesFont := core.MakeDict() + n2ResourcesFont.Set("F1", DefaultFont().ToPdfObject()) + acroForm.DR = NewPdfPageResources() + acroForm.DR.Font = n2ResourcesFont + sig := NewPdfSignature() + sig.M = core.MakeString(time.Now().Format("D:20060102150405-07'00'")) + //sig.M = core.MakeString("D:20150226112648Z") + sig.Type = core.MakeName("Sig") + sig.Reason = core.MakeString("Test1") + if err := handler.InitSignature(sig); err != nil { + return nil, nil, err + } + a.addNewObjects(sig.container) + + appearance = NewPdfAppearance() + + fields := append(acroForm.AllFields(), appearance.PdfField) + acroForm.Fields = &fields + + procPage(page) + + appearance.V = sig.ToPdfObject() + appearance.FT = core.MakeName("Sig") + appearance.V = sig.ToPdfObject() + //appearance.Ff = core.MakeInteger(0) + appearance.T = core.MakeString("Signature1") + appearance.F = core.MakeInteger(132) + appearance.P = page.ToPdfObject() + appearance.Rect = core.MakeArray(core.MakeInteger(0), core.MakeInteger(0), core.MakeInteger(0), + core.MakeInteger(0)) + appearance.Signature = sig + + a.pages[pageIndex] = page + + a.ReplaceAcroForm(acroForm) + return acroForm, appearance, nil +} + // ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which replaces the original acrobat form. func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { a.acroForm = acroForm @@ -377,13 +437,6 @@ func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { // Write writes the Appender output to io.Writer. func (a *PdfAppender) Write(w io.Writer) error { - if _, err := a.rs.Seek(0, io.SeekStart); err != nil { - return err - } - offset, err := io.Copy(w, a.rs) - if err != nil { - return err - } writer := NewPdfWriter() @@ -473,6 +526,51 @@ func (a *PdfAppender) Write(w io.Writer) error { writer.SetForms(a.acroForm) } + if _, err := a.rs.Seek(0, io.SeekStart); err != nil { + return err + } + + digestWriters := make(map[SignatureHandler]io.Writer) + byteRange := core.MakeArray() + for _, obj := range a.newObjects { + if ind, found := core.GetIndirect(obj); found { + if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { + handler := *sigDict.handler + // TODO fix it + digestWriters[handler], _ = handler.NewDigest(sigDict.signature) + byteRange.Append(core.MakeInteger(0xfffff), core.MakeInteger(0xfffff)) + } + } + } + if byteRange.Len() > 0 { + byteRange.Append(core.MakeInteger(0xfffff), core.MakeInteger(0xfffff)) + } + + for _, obj := range a.newObjects { + if ind, found := core.GetIndirect(obj); found { + if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { + sigDict.Set("ByteRange", byteRange) + } + } + } + + hasSigDict := len(digestWriters) > 0 + + var reader io.Reader = a.rs + if hasSigDict { + writers := make([]io.Writer, 0, len(digestWriters)) + for _, hash := range digestWriters { + writers = append(writers, hash) + } + //hashSha1 := sha1.New() // if needed + reader = io.TeeReader(a.rs, io.MultiWriter(writers...)) + } + + offset, err := io.Copy(w, reader) + if err != nil { + return err + } + if len(a.newObjects) == 0 { return nil } @@ -481,13 +579,97 @@ func (a *PdfAppender) Write(w io.Writer) error { writer.ObjNumOffset = a.greatestObjNum writer.appendMode = true writer.appendToXrefs = a.xrefs + writer.minorVersion = 7 for _, obj := range a.newObjects { writer.addObject(obj) } - if err := writer.Write(w); err != nil { + + writerW := w + + if hasSigDict { + writerW = bytes.NewBuffer(nil) + } + + if err := writer.Write(writerW); err != nil { return err } + + if hasSigDict { + bufferData := writerW.(*bytes.Buffer).Bytes() + byteRange := core.MakeArray() + var sigDicts []*pdfSignDictionary + var lastPosition int64 + for _, obj := range writer.objects { + if ind, found := core.GetIndirect(obj); found { + if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { + sigDicts = append(sigDicts, sigDict) + newPosition := sigDict.fileOffset + int64(sigDict.contentsOffsetStart) + byteRange.Append( + core.MakeInteger(lastPosition), + core.MakeInteger(newPosition-lastPosition), + ) + lastPosition = sigDict.fileOffset + int64(sigDict.contentsOffsetEnd) + } + } + } + byteRange.Append( + core.MakeInteger(lastPosition), + core.MakeInteger(offset+int64(len(bufferData))-lastPosition), + ) + // set the ByteRange value + byteRangeData := []byte(byteRange.WriteString()) + for _, sigDict := range sigDicts { + bufferOffset := int(sigDict.fileOffset - offset) + for i := sigDict.byteRangeOffsetStart; i < sigDict.byteRangeOffsetEnd; i++ { + bufferData[bufferOffset+i] = ' ' + } + dst := bufferData[bufferOffset+sigDict.byteRangeOffsetStart : bufferOffset+sigDict.byteRangeOffsetEnd] + copy(dst, byteRangeData) + } + var prevOffset int + for _, sigDict := range sigDicts { + bufferOffset := int(sigDict.fileOffset - offset) + data := bufferData[prevOffset : bufferOffset+sigDict.contentsOffsetStart] + handler := *sigDict.handler + digestWriters[handler].Write(data) + prevOffset = bufferOffset + sigDict.contentsOffsetEnd + } + for _, sigDict := range sigDicts { + data := bufferData[prevOffset:] + handler := *sigDict.handler + digestWriters[handler].Write(data) + } + for _, sigDict := range sigDicts { + bufferOffset := int(sigDict.fileOffset - offset) + handler := *sigDict.handler + digest := digestWriters[handler] + if err := handler.Sign(sigDict.signature, digest); err != nil { + return err + } + contents := []byte(sigDict.signature.Contents.WriteString()) + + for i := sigDict.byteRangeOffsetStart; i < sigDict.byteRangeOffsetEnd; i++ { + bufferData[bufferOffset+i] = ' ' + } + for i := sigDict.contentsOffsetStart; i < sigDict.contentsOffsetEnd; i++ { + bufferData[bufferOffset+i] = ' ' + } + + dst := bufferData[bufferOffset+sigDict.byteRangeOffsetStart : bufferOffset+sigDict.byteRangeOffsetEnd] + copy(dst, byteRangeData) + dst = bufferData[bufferOffset+sigDict.contentsOffsetStart : bufferOffset+sigDict.contentsOffsetEnd] + copy(dst, contents) + } + + writerW = bytes.NewBuffer(bufferData) + + } + + if buffer, ok := writerW.(*bytes.Buffer); ok { + _, err = io.Copy(w, buffer) + } + return err } diff --git a/pdf/model/appender_test.go b/pdf/model/appender_test.go index a28bfe05..2dbc7f8d 100644 --- a/pdf/model/appender_test.go +++ b/pdf/model/appender_test.go @@ -6,12 +6,16 @@ package model import ( + "bytes" + "crypto/rsa" + "io/ioutil" "os" "path/filepath" "testing" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" + "golang.org/x/crypto/pkcs12" ) // This test file contains multiple tests to generate PDFs from existing Pdf files. The outputs are written into /tmp as files. The files @@ -32,6 +36,11 @@ const imgPdfFile2 = "./testdata/img1-2.pdf" // source http://foersom.com/net/HowTo/data/OoPdfFormExample.pdf const testPdfAcroFormFile1 = "./testdata/OoPdfFormExample.pdf" +const testPdfSignedPDFDocument = "./testdata/SampleSignedPDFDocument.pdf" + +const testPKS12Key = "./testdata/ks12" +const testPKS12KeyPassword = "password" + func tempFile(name string) string { return filepath.Join(os.TempDir(), name) } @@ -362,3 +371,86 @@ func TestAppenderMergePage3(t *testing.T) { return } } + +func validateFile(t *testing.T, fileName string) { + data, err := ioutil.ReadFile(fileName) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + reader, err := NewPdfReader(bytes.NewReader(data)) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, _ := NewAdobeX509RSASHA1SignatureHandler(nil, nil) + handler2, _ := NewAdobePKCS7DetachedSignatureHandler(nil, nil) + handlers := []SignatureHandler{handler, handler2} + + res, err := reader.Validate(handlers) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + if len(res) == 0 { + t.Errorf("Fail: signature fields not found") + return + } + + if !res[0].IsSigned || !res[0].IsVerified { + t.Errorf("Fail: validation failed") + return + } +} + +func TestAppenderSignPage4(t *testing.T) { + // TODO move to reader_test.go + validateFile(t, testPdfSignedPDFDocument) + + f1, err := os.Open(testPdfFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f1.Close() + pdf1, err := NewPdfReader(f1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appender, err := NewPdfAppender(pdf1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + f, _ := ioutil.ReadFile(testPKS12Key) + privateKey, cert, err := pkcs12.Decode(f, testPKS12KeyPassword) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + handler, err := NewAdobePKCS7DetachedSignatureHandler(privateKey.(*rsa.PrivateKey), cert) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + _, appearance, err := appender.Sign(1, handler) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + appearance.Signature.Reason = core.MakeString("TestAppenderSignPage4") + appearance.Signature.Name = core.MakeString("Test Appender") + + err = appender.WriteToFile(tempFile("appender_sign_page_4.pdf")) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + validateFile(t, tempFile("appender_sign_page_4.pdf")) +} diff --git a/pdf/model/fields.go b/pdf/model/fields.go index 5c3bc02f..2d026a72 100644 --- a/pdf/model/fields.go +++ b/pdf/model/fields.go @@ -254,28 +254,24 @@ func (f *PdfField) ToPdfObject() core.PdfObject { if f.Parent != nil { d.Set("Parent", f.Parent.GetContainingPdfObject()) } - + kids := core.MakeArray() if f.Kids != nil { // Create an array of the kids (fields or widgets). - kids := core.MakeArray() for _, child := range f.Kids { kids.Append(child.ToPdfObject()) } - d.Set("Kids", kids) } if f.Annotations != nil { - _, hasKids := d.Get("Kids").(*core.PdfObjectArray) - if !hasKids { - d.Set("Kids", &core.PdfObjectArray{}) - } - // TODO: If only 1 widget annotation, it can be merged in. - kids := d.Get("Kids").(*core.PdfObjectArray) for _, annot := range f.Annotations { kids.Append(annot.GetContext().ToPdfObject()) } } + if f.Kids != nil || f.Annotations != nil { + d.Set("Kids", kids) + } + d.SetIfNotNil("T", f.T) d.SetIfNotNil("TU", f.TU) d.SetIfNotNil("TM", f.TM) diff --git a/pdf/model/signature.go b/pdf/model/signature.go index 55972db5..d713a262 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -10,9 +10,69 @@ import ( "github.com/unidoc/unidoc/pdf/core" ) +// pdfSignDictionary is needed because of the digital checksum calculates after a new file creation and writes as a value into PdfDictionary in the existing file. +type pdfSignDictionary struct { + *core.PdfObjectDictionary + handler *SignatureHandler + signature *PdfSignature + fileOffset int64 + contentsOffsetStart int + contentsOffsetEnd int + byteRangeOffsetStart int + byteRangeOffsetEnd int +} + +// GetSubFilter returns SubFilter value or empty string. +func (d *pdfSignDictionary) GetSubFilter() string { + obj := d.Get("SubFilter") + if obj == nil { + return "" + } + if sf, found := core.GetNameVal(obj); found { + return sf + } + return "" +} + +// WriteString outputs the object as it is to be written to file. +func (d *pdfSignDictionary) WriteString() string { + d.contentsOffsetStart = 0 + d.contentsOffsetEnd = 0 + d.byteRangeOffsetStart = 0 + d.byteRangeOffsetEnd = 0 + + outStr := "<<" + for _, k := range d.Keys() { + v := d.Get(k) + switch k { + case "ByteRange": + outStr += k.WriteString() + outStr += " " + d.byteRangeOffsetStart = len(outStr) + outStr += v.WriteString() + outStr += " " + d.byteRangeOffsetEnd = len(outStr) + case "Contents": + outStr += k.WriteString() + outStr += " " + d.contentsOffsetStart = len(outStr) + outStr += v.WriteString() + outStr += " " + d.contentsOffsetEnd = len(outStr) + default: + outStr += k.WriteString() + outStr += " " + outStr += v.WriteString() + } + } + outStr += ">>" + return outStr +} + // PdfSignature represents a PDF signature dictionary and is used for signing via form signature fields. // (Section 12.8, Table 252 - Entries in a signature dictionary p. 475 in PDF32000_2008). type PdfSignature struct { + Handler SignatureHandler container *core.PdfIndirectObject // Type: Sig/DocTimeStamp Type *core.PdfObjectName @@ -35,6 +95,14 @@ type PdfSignature struct { PropAuthType *core.PdfObjectName } +// NewPdfSignature creates a new PdfSignature object. +func NewPdfSignature() *PdfSignature { + sig := &PdfSignature{} + sigDict := &pdfSignDictionary{PdfObjectDictionary: core.MakeDict(), handler: &sig.Handler, signature: sig} + sig.container = core.MakeIndirectObject(sigDict) + return sig +} + // PdfSignatureReference represents a signature reference dictionary. // (Table 253 - p. 477 in PDF32000_2008). type PdfSignatureReference struct { @@ -53,7 +121,13 @@ func (sig *PdfSignature) GetContainingPdfObject() core.PdfObject { // ToPdfObject implements interface PdfModel. func (sig *PdfSignature) ToPdfObject() core.PdfObject { container := sig.container - dict := container.PdfObject.(*core.PdfObjectDictionary) + + var dict *core.PdfObjectDictionary + if sigDict, ok := container.PdfObject.(*pdfSignDictionary); ok { + dict = sigDict.PdfObjectDictionary + } else { + dict = container.PdfObject.(*core.PdfObjectDictionary) + } dict.Set("Type", sig.Type) @@ -90,7 +164,12 @@ func (sig *PdfSignature) ToPdfObject() core.PdfObject { if sig.ContactInfo != nil { dict.Set("ContactInfo", sig.ContactInfo) } - + if sig.ByteRange != nil { + dict.Set("ByteRange", sig.ByteRange) + } + if sig.Contents != nil { + dict.Set("Contents", sig.Contents) + } // FIXME: ByteRange and Contents need to be updated dynamically. return container } @@ -175,6 +254,7 @@ type PdfSignatureField struct { // V: *PdfSignature... Lock *core.PdfIndirectObject // Shall be an indirect reference. SV *core.PdfIndirectObject // Shall be an indirect reference. + Kids *core.PdfObjectArray } // NewPdfSignatureField prepares a PdfSignatureField from a PdfSignature. @@ -201,6 +281,9 @@ func (sf *PdfSignatureField) ToPdfObject() core.PdfObject { if sf.SV != nil { dict.Set("SV", sf.SV) } + if sf.Kids != nil { + dict.Set("Kids", sf.Kids) + } // Other standard fields... return container @@ -218,6 +301,7 @@ type PdfSignatureFieldLock struct { // PdfSignatureFieldSeed represents signature field seed value dictionary. // (Table 234 - p. 455 in PDF32000_2008). type PdfSignatureFieldSeed struct { + container *core.PdfIndirectObject // Type Ff *core.PdfObjectInteger Filter *core.PdfObjectName @@ -234,9 +318,56 @@ type PdfSignatureFieldSeed struct { AppearanceFilter *core.PdfObjectString } +func (pss *PdfSignatureFieldSeed) ToPdfObject() core.PdfObject { + container := pss.container + dict := container.PdfObject.(*core.PdfObjectDictionary) + + if pss.Ff != nil { + dict.Set("Ff", pss.Ff) + } + if pss.Filter != nil { + dict.Set("Filter", pss.Filter) + } + if pss.SubFilter != nil { + dict.Set("SubFilter", pss.SubFilter) + } + if pss.DigestMethod != nil { + dict.Set("DigestMethod", pss.DigestMethod) + } + if pss.V != nil { + dict.Set("V", pss.V) + } + if pss.Cert != nil { + dict.Set("Cert", pss.Cert) + } + if pss.Reasons != nil { + dict.Set("Reasons", pss.Reasons) + } + if pss.MDP != nil { + dict.Set("MDP", pss.MDP) + } + if pss.TimeStamp != nil { + dict.Set("TimeStamp", pss.TimeStamp) + } + if pss.LegalAttestation != nil { + dict.Set("LegalAttestation", pss.LegalAttestation) + } + if pss.AddRevInfo != nil { + dict.Set("AddRevInfo", pss.AddRevInfo) + } + if pss.LockDocument != nil { + dict.Set("LockDocument", pss.LockDocument) + } + if pss.AppearanceFilter != nil { + dict.Set("AppearanceFilter", pss.AppearanceFilter) + } + return container +} + // PdfCertificateSeed represents certificate seed value dictionary. // (Table 235 - p. 457 in PDF32000_2008). type PdfCertificateSeed struct { + container *core.PdfIndirectObject // Type Ff *core.PdfObjectInteger Subject *core.PdfObjectArray @@ -251,3 +382,45 @@ type PdfCertificateSeed struct { URL *core.PdfObjectString URLType *core.PdfObjectName } + +func (pcs *PdfCertificateSeed) ToPdfObject() core.PdfObject { + container := pcs.container + dict := container.PdfObject.(*core.PdfObjectDictionary) + if pcs.Ff != nil { + dict.Set("Ff", pcs.Ff) + } + if pcs.Subject != nil { + dict.Set("Subject", pcs.Subject) + } + if pcs.SignaturePolicyOID != nil { + dict.Set("SignaturePolicyOID", pcs.SignaturePolicyOID) + } + if pcs.SignaturePolicyHashValue != nil { + dict.Set("SignaturePolicyHashValue", pcs.SignaturePolicyHashValue) + } + if pcs.SignaturePolicyHashAlgorithm != nil { + dict.Set("SignaturePolicyHashAlgorithm", pcs.SignaturePolicyHashAlgorithm) + } + if pcs.SignaturePolicyCommitmentType != nil { + dict.Set("SignaturePolicyCommitmentType", pcs.SignaturePolicyCommitmentType) + } + if pcs.SubjectDN != nil { + dict.Set("SubjectDN", pcs.SubjectDN) + } + if pcs.KeyUsage != nil { + dict.Set("KeyUsage", pcs.KeyUsage) + } + if pcs.Issuer != nil { + dict.Set("Issuer", pcs.Issuer) + } + if pcs.OID != nil { + dict.Set("OID", pcs.OID) + } + if pcs.URL != nil { + dict.Set("URL", pcs.URL) + } + if pcs.URLType != nil { + dict.Set("URLType", pcs.URLType) + } + return container +} diff --git a/pdf/model/signature_handler.go b/pdf/model/signature_handler.go new file mode 100644 index 00000000..24a3eaaa --- /dev/null +++ b/pdf/model/signature_handler.go @@ -0,0 +1,270 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "errors" + "hash" + "io" + + "github.com/unidoc/unidoc/pdf/core" +) + +// Digest is the interface that wraps the basic Write method. +type Digest interface { + Write(p []byte) (n int, err error) +} + +// SignatureHandler interface defines the common functionality for PDF signature handlers, which +// need to be capable of validating digital signatures and signing PDF documents. +type SignatureHandler interface { + IsApplicable(sig *PdfSignature) bool + Validate(sig *PdfSignature, digest Digest) (SignatureValidationResult, error) + // InitSignature sets the PdfSignature parameters. + InitSignature(*PdfSignature) error + NewDigest(sig *PdfSignature) (Digest, error) + Sign(sig *PdfSignature, digest Digest) error +} + +// SignatureValidationResult defines the response from the signature validation handler. +type SignatureValidationResult struct { + IsSigned bool + IsVerified bool + IsTrusted bool + Fields []*PdfField + Name string + Date PdfDate + Reason string + Location string + ContactInfo string +} + +type adobeX509RSASHA1SignatureHandler struct { + privateKey *rsa.PrivateKey + certificate *x509.Certificate +} + +// NewAdobeX509RSASHA1SignatureHandler creates a new Adobe.PPKMS/Adobe.PPKLite adbe.x509.rsa_sha1 signature handler. +// The both parameters may be nil for the signature validation. +func NewAdobeX509RSASHA1SignatureHandler(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (SignatureHandler, error) { + return &adobeX509RSASHA1SignatureHandler{certificate: certificate, privateKey: privateKey}, nil +} + +// InitSignature initialises the PdfSignature. +func (a *adobeX509RSASHA1SignatureHandler) InitSignature(sig *PdfSignature) error { + if a.certificate == nil { + return errors.New("certificate must not be nil") + } + if a.privateKey == nil { + return errors.New("privateKey must not be nil") + } + + handler := *a + sig.Handler = &handler + sig.Filter = core.MakeName("Adobe.PPKLite") + //sig.Filter = core.MakeName("Adobe.PPKMS") + sig.SubFilter = core.MakeName("adbe.x509.rsa_sha1") + sig.Cert = core.MakeString(string(handler.certificate.Raw)) + + sig.Reference = nil + digest, err := handler.NewDigest(sig) + if err != nil { + return err + } + digest.Write([]byte("calculate the Contents field size")) + return handler.Sign(sig, digest) +} + +func getHashFromSignatureAlgorithm(sa x509.SignatureAlgorithm) (crypto.Hash, bool) { + return crypto.SHA1, true + /* + switch sa { + case x509.SHA1WithRSA: + return crypto.SHA1, true + case x509.SHA256WithRSA: + return crypto.SHA256, true + case x509.SHA512WithRSA: + return crypto.SHA512, true + } + return crypto.MD5, false + */ +} + +func (a *adobeX509RSASHA1SignatureHandler) getCertificate(sig *PdfSignature) (*x509.Certificate, error) { + certificate := a.certificate + if certificate == nil { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return nil, err + } + certificate = certs[0] + } + return certificate, nil +} + +// NewDigest creates a new digest. +func (a *adobeX509RSASHA1SignatureHandler) NewDigest(sig *PdfSignature) (Digest, error) { + certificate, err := a.getCertificate(sig) + if err != nil { + return nil, err + } + h, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) + return h.New(), nil +} + +// Validate validates PdfSignature. +func (a *adobeX509RSASHA1SignatureHandler) Validate(sig *PdfSignature, digest Digest) (SignatureValidationResult, error) { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return SignatureValidationResult{}, err + } + if len(certs) == 0 { + return SignatureValidationResult{}, errors.New("certificate not found") + } + cert := certs[0] + signed := sig.Contents.Bytes() + var sigHash []byte + if _, err := asn1.Unmarshal(signed, &sigHash); err != nil { + return SignatureValidationResult{}, err + } + h, ok := digest.(hash.Hash) + if !ok { + return SignatureValidationResult{}, errors.New("hash type error") + } + certificate, err := a.getCertificate(sig) + if err != nil { + return SignatureValidationResult{}, err + } + ha, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) + if err := rsa.VerifyPKCS1v15(cert.PublicKey.(*rsa.PublicKey), ha, h.Sum(nil), sigHash); err != nil { + return SignatureValidationResult{}, err + } + return SignatureValidationResult{IsSigned: true, IsVerified: true}, nil +} + +// Sign sets the Contents fields. +func (a *adobeX509RSASHA1SignatureHandler) Sign(sig *PdfSignature, digest Digest) error { + h, ok := digest.(hash.Hash) + if !ok { + return errors.New("hash type error") + } + ha, _ := getHashFromSignatureAlgorithm(a.certificate.SignatureAlgorithm) + data, err := rsa.SignPKCS1v15(rand.Reader, a.privateKey, ha, h.Sum(nil)) + if err != nil { + return err + } + data, err = asn1.Marshal(data) + if err != nil { + return err + } + sig.Contents = core.MakeHexString(string(data)) + return nil +} + +// IsApplicable returns true if the signature handler is applicable for the PdfSignature +func (a *adobeX509RSASHA1SignatureHandler) IsApplicable(sig *PdfSignature) bool { + if sig == nil || sig.Filter == nil || sig.SubFilter == nil { + return false + } + return (*sig.Filter == "Adobe.PPKMS" || *sig.Filter == "Adobe.PPKLite") && *sig.SubFilter == "adbe.x509.rsa_sha1" +} + +// Validate validates signatures. +func (r *PdfReader) Validate(handlers []SignatureHandler) ([]SignatureValidationResult, error) { + if r.AcroForm == nil { + return nil, nil + } + if r.AcroForm.Fields == nil { + return nil, nil + } + type sigFieldPair struct { + sig *PdfSignature + field *PdfField + handler SignatureHandler + } + var pairs []*sigFieldPair + + for _, f := range *r.AcroForm.Fields { + if f.V == nil { + continue + } + if d, found := core.GetDict(f.V); found { + if name, ok := core.GetNameVal(d.Get("Type")); ok && name == "Sig" { + ind, _ := core.GetIndirect(f.V) // TODO refactoring + sig, err := r.newPdfSignatureFromIndirect(ind) + if err != nil { + return nil, err + } + pairs = append(pairs, &sigFieldPair{sig: sig, field: f}) + } + } + } + + for _, pair := range pairs { + for _, handler := range handlers { + if handler.IsApplicable(pair.sig) { + pair.handler = handler + break + } + } + } + + var results []SignatureValidationResult + + for _, pair := range pairs { + defaultResult := SignatureValidationResult{IsSigned: true, Fields: []*PdfField{pair.field}} + if pair.handler == nil { + // TODO think about variants + // to throw an error + // to skip the field and add error message to the result + results = append(results, defaultResult) + continue + } + digest, err := pair.handler.NewDigest(pair.sig) + if err != nil { + // TODO think about variants + // to throw an error + // to skip the field and add error message to the result + results = append(results, defaultResult) + continue + } + byteRange := pair.sig.ByteRange + if byteRange == nil { + // TODO think about variants + // to throw an error + // to skip the field and add error message to the result + results = append(results, defaultResult) + continue + } + for i := 0; i < byteRange.Len(); i = i + 2 { + start, _ := core.GetNumberAsInt64(byteRange.Get(i)) + ln, _ := core.GetIntVal(byteRange.Get(i + 1)) + if _, err := r.rs.Seek(start, io.SeekStart); err != nil { + return nil, err + } + data := make([]byte, ln) + if _, err := r.rs.Read(data); err != nil { + return nil, err + } + digest.Write(data) + } + + result, err := pair.handler.Validate(pair.sig, digest) + if err != nil { + return nil, err + } + result.Fields = defaultResult.Fields + results = append(results, result) + } + return results, nil +} diff --git a/pdf/model/signature_handler_pkcs7.go b/pdf/model/signature_handler_pkcs7.go new file mode 100644 index 00000000..9cbcb99a --- /dev/null +++ b/pdf/model/signature_handler_pkcs7.go @@ -0,0 +1,126 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "bytes" + "crypto/rsa" + "crypto/x509" + "errors" + + "github.com/unidoc/unidoc/common/crypto/pkcs7" + "github.com/unidoc/unidoc/pdf/core" +) + +type adobePKCS7DetachedSignatureHandler struct { + privateKey *rsa.PrivateKey + certificate *x509.Certificate +} + +// NewAdobePKCS7DetachedSignatureHandler creates a new Adobe.PPKMS/Adobe.PPKLite adbe.pkcs7.detached signature handler. +// The both parameters may be nil for the signature validation. +func NewAdobePKCS7DetachedSignatureHandler(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (SignatureHandler, error) { + return &adobePKCS7DetachedSignatureHandler{certificate: certificate, privateKey: privateKey}, nil +} + +// InitSignature initialises the PdfSignature. +func (a *adobePKCS7DetachedSignatureHandler) InitSignature(sig *PdfSignature) error { + if a.certificate == nil { + return errors.New("certificate must not be nil") + } + if a.privateKey == nil { + return errors.New("privateKey must not be nil") + } + + handler := *a + sig.Handler = &handler + sig.Filter = core.MakeName("Adobe.PPKLite") + //sig.Filter = core.MakeName("Adobe.PPKMS") + sig.SubFilter = core.MakeName("adbe.pkcs7.detached") + sig.Cert = core.MakeString(string(handler.certificate.Raw)) + + sig.Reference = nil + digest, err := handler.NewDigest(sig) + if err != nil { + return err + } + digest.Write([]byte("calculate the Contents field size")) + return handler.Sign(sig, digest) +} + +func (a *adobePKCS7DetachedSignatureHandler) getCertificate(sig *PdfSignature) (*x509.Certificate, error) { + certificate := a.certificate + if certificate == nil { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return nil, err + } + certificate = certs[0] + } + return certificate, nil +} + +// NewDigest creates a new digest. +func (a *adobePKCS7DetachedSignatureHandler) NewDigest(sig *PdfSignature) (Digest, error) { + return bytes.NewBuffer(nil), nil +} + +// Validate validates PdfSignature. +func (a *adobePKCS7DetachedSignatureHandler) Validate(sig *PdfSignature, digest Digest) (SignatureValidationResult, error) { + signed := sig.Contents.Bytes() + + buffer := digest.(*bytes.Buffer) + p7, err := pkcs7.Parse(signed) + if err != nil { + return SignatureValidationResult{}, err + } + p7.Content = buffer.Bytes() + err = p7.Verify() + if err != nil { + return SignatureValidationResult{}, err + } + + return SignatureValidationResult{IsSigned: true, IsVerified: true}, nil +} + +// Sign sets the Contents fields. +func (a *adobePKCS7DetachedSignatureHandler) Sign(sig *PdfSignature, digest Digest) error { + + buffer := digest.(*bytes.Buffer) + signedData, err := pkcs7.NewSignedData(buffer.Bytes()) + if err != nil { + return err + } + + // Add the signing cert and private key + if err := signedData.AddSigner(a.certificate, a.privateKey, pkcs7.SignerInfoConfig{}); err != nil { + return err + } + + // Call Detach() is you want to remove content from the signature + // and generate an S/MIME detached signature + signedData.Detach() + // Finish() to obtain the signature bytes + detachedSignature, err := signedData.Finish() + if err != nil { + return err + } + + data := make([]byte, 8192) + copy(data, detachedSignature) + + sig.Contents = core.MakeHexString(string(data)) + return nil +} + +// IsApplicable returns true if the signature handler is applicable for the PdfSignature +func (a *adobePKCS7DetachedSignatureHandler) IsApplicable(sig *PdfSignature) bool { + if sig == nil || sig.Filter == nil || sig.SubFilter == nil { + return false + } + return (*sig.Filter == "Adobe.PPKMS" || *sig.Filter == "Adobe.PPKLite") && *sig.SubFilter == "adbe.pkcs7.detached" +} diff --git a/pdf/model/testdata/SampleSignedPDFDocument.pdf b/pdf/model/testdata/SampleSignedPDFDocument.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8858f93345ff53acea97ae84399b7e0d1e837d4e GIT binary patch literal 272318 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�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

C!s2F`4sXIA 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 literal 0 HcmV?d00001 diff --git a/pdf/model/testdata/ks12 b/pdf/model/testdata/ks12 new file mode 100644 index 0000000000000000000000000000000000000000..b8a13ce6764cde19b8ad852a8f845d69d8cccd45 GIT binary patch literal 2587 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 literal 0 HcmV?d00001 diff --git a/pdf/model/writer.go b/pdf/model/writer.go index 845cf9d3..c3075222 100644 --- a/pdf/model/writer.go +++ b/pdf/model/writer.go @@ -225,6 +225,16 @@ func copyObject(obj PdfObject, objectToObjectCopyMap map[PdfObject]PdfObject) Pd newObj := PdfObjectBool(*t) objectToObjectCopyMap[obj] = &newObj return &newObj + case *pdfSignDictionary: + newObj := &pdfSignDictionary{PdfObjectDictionary: MakeDict()} + newObj.handler = t.handler + newObj.signature = t.signature + objectToObjectCopyMap[obj] = newObj + for _, key := range t.Keys() { + val := t.Get(key) + newObj.Set(key, copyObject(val, objectToObjectCopyMap)) + } + return newObj default: common.Log.Info("TODO(a5i): implement copyObject for %+v", obj) } @@ -572,6 +582,9 @@ func (w *PdfWriter) writeObject(num int, obj PdfObject) { if pobj, isIndirect := obj.(*PdfIndirectObject); isIndirect { w.crossReferenceMap[num] = crossReference{Type: 1, Offset: w.writePos, Generation: pobj.GenerationNumber} outStr := fmt.Sprintf("%d 0 obj\n", num) + if sDict, ok := pobj.PdfObject.(*pdfSignDictionary); ok { + sDict.fileOffset = w.writePos + int64(len(outStr)) + } outStr += pobj.PdfObject.WriteString() outStr += "\nendobj\n" w.writeString(outStr) From 7b66633f8e025940174d52c1f54ced110b2a9108 Mon Sep 17 00:00:00 2001 From: Aleksei Pavliukov Date: Sun, 30 Dec 2018 01:59:32 +0300 Subject: [PATCH 02/20] add digital sign API prototype --- common/crypto/pkcs7/ber_test.go | 2 -- common/crypto/pkcs7/pkcs7_test.go | 5 +---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/common/crypto/pkcs7/ber_test.go b/common/crypto/pkcs7/ber_test.go index 72288c80..6ed7d38d 100644 --- a/common/crypto/pkcs7/ber_test.go +++ b/common/crypto/pkcs7/ber_test.go @@ -4,8 +4,6 @@ import ( "bytes" "strings" "testing" - - "github.com/gunnsth/crypto/asn1" ) func TestBer2Der(t *testing.T) { diff --git a/common/crypto/pkcs7/pkcs7_test.go b/common/crypto/pkcs7/pkcs7_test.go index 330f87e5..71c78cd5 100644 --- a/common/crypto/pkcs7/pkcs7_test.go +++ b/common/crypto/pkcs7/pkcs7_test.go @@ -5,6 +5,7 @@ import ( "crypto" "crypto/rand" "crypto/rsa" + "crypto/x509" "encoding/pem" "fmt" "io" @@ -14,10 +15,6 @@ import ( "os/exec" "testing" "time" - - "github.com/gunnsth/crypto/asn1" - "github.com/gunnsth/crypto/x509" - "github.com/gunnsth/crypto/x509/pkix" ) func TestVerify(t *testing.T) { From 8efc55b05f29c33b640efb7763060c2c5c9f16e4 Mon Sep 17 00:00:00 2001 From: Aleksei Pavliukov Date: Sun, 30 Dec 2018 02:11:55 +0300 Subject: [PATCH 03/20] add digital sign API prototype --- common/crypto/pkcs7/ber_test.go | 96 ----- common/crypto/pkcs7/pkcs7_test.go | 677 ------------------------------ 2 files changed, 773 deletions(-) delete mode 100644 common/crypto/pkcs7/ber_test.go delete mode 100644 common/crypto/pkcs7/pkcs7_test.go diff --git a/common/crypto/pkcs7/ber_test.go b/common/crypto/pkcs7/ber_test.go deleted file mode 100644 index 6ed7d38d..00000000 --- a/common/crypto/pkcs7/ber_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package pkcs7 - -import ( - "bytes" - "strings" - "testing" -) - -func TestBer2Der(t *testing.T) { - // indefinite length fixture - ber := []byte{0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00} - expected := []byte{0x30, 0x03, 0x02, 0x01, 0x01} - der, err := ber2der(ber) - if err != nil { - t.Fatalf("ber2der failed with error: %v", err) - } - if bytes.Compare(der, expected) != 0 { - t.Errorf("ber2der result did not match.\n\tExpected: % X\n\tActual: % X", expected, der) - } - - if der2, err := ber2der(der); err != nil { - t.Errorf("ber2der on DER bytes failed with error: %v", err) - } else { - if !bytes.Equal(der, der2) { - t.Error("ber2der is not idempotent") - } - } - var thing struct { - Number int - } - rest, err := asn1.Unmarshal(der, &thing) - if err != nil { - t.Errorf("Cannot parse resulting DER because: %v", err) - } else if len(rest) > 0 { - t.Errorf("Resulting DER has trailing data: % X", rest) - } -} - -func TestBer2Der_Negatives(t *testing.T) { - fixtures := []struct { - Input []byte - ErrorContains string - }{ - {[]byte{0x30, 0x85}, "length too long"}, - {[]byte{0x30, 0x84, 0x80, 0x0, 0x0, 0x0}, "length is negative"}, - {[]byte{0x30, 0x82, 0x0, 0x1}, "length has leading zero"}, - {[]byte{0x30, 0x80, 0x1, 0x2, 0x1, 0x2}, "Invalid BER format"}, - {[]byte{0x30, 0x03, 0x01, 0x02}, "length is more than available data"}, - } - - for _, fixture := range fixtures { - _, err := ber2der(fixture.Input) - if err == nil { - t.Errorf("No error thrown. Expected: %s", fixture.ErrorContains) - } - if !strings.Contains(err.Error(), fixture.ErrorContains) { - t.Errorf("Unexpected error thrown.\n\tExpected: /%s/\n\tActual: %s", fixture.ErrorContains, err.Error()) - } - } -} - -func TestBer2Der_NestedMultipleIndefinite(t *testing.T) { - // indefinite length fixture - ber := []byte{0x30, 0x80, 0x30, 0x80, 0x02, 0x01, 0x01, 0x00, 0x00, 0x30, 0x80, 0x02, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00} - expected := []byte{0x30, 0x0A, 0x30, 0x03, 0x02, 0x01, 0x01, 0x30, 0x03, 0x02, 0x01, 0x02} - - der, err := ber2der(ber) - if err != nil { - t.Fatalf("ber2der failed with error: %v", err) - } - if bytes.Compare(der, expected) != 0 { - t.Errorf("ber2der result did not match.\n\tExpected: % X\n\tActual: % X", expected, der) - } - - if der2, err := ber2der(der); err != nil { - t.Errorf("ber2der on DER bytes failed with error: %v", err) - } else { - if !bytes.Equal(der, der2) { - t.Error("ber2der is not idempotent") - } - } - var thing struct { - Nest1 struct { - Number int - } - Nest2 struct { - Number int - } - } - rest, err := asn1.Unmarshal(der, &thing) - if err != nil { - t.Errorf("Cannot parse resulting DER because: %v", err) - } else if len(rest) > 0 { - t.Errorf("Resulting DER has trailing data: % X", rest) - } -} diff --git a/common/crypto/pkcs7/pkcs7_test.go b/common/crypto/pkcs7/pkcs7_test.go deleted file mode 100644 index 71c78cd5..00000000 --- a/common/crypto/pkcs7/pkcs7_test.go +++ /dev/null @@ -1,677 +0,0 @@ -package pkcs7 - -import ( - "bytes" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" - "io" - "io/ioutil" - "math/big" - "os" - "os/exec" - "testing" - "time" -) - -func TestVerify(t *testing.T) { - fixture := UnmarshalTestFixture(SignedTestFixture) - p7, err := Parse(fixture.Input) - if err != nil { - t.Errorf("Parse encountered unexpected error: %v", err) - } - - if err := p7.Verify(); err != nil { - t.Errorf("Verify failed with error: %v", err) - } - expected := []byte("We the People") - if bytes.Compare(p7.Content, expected) != 0 { - t.Errorf("Signed content does not match.\n\tExpected:%s\n\tActual:%s", expected, p7.Content) - - } -} - -func TestVerifyEC2(t *testing.T) { - fixture := UnmarshalTestFixture(EC2IdentityDocumentFixture) - p7, err := Parse(fixture.Input) - if err != nil { - t.Errorf("Parse encountered unexpected error: %v", err) - } - p7.Certificates = []*x509.Certificate{fixture.Certificate} - if err := p7.Verify(); err != nil { - t.Errorf("Verify failed with error: %v", err) - } -} - -func TestVerifyAppStore(t *testing.T) { - fixture := UnmarshalTestFixture(AppStoreRecieptFixture) - p7, err := Parse(fixture.Input) - if err != nil { - t.Errorf("Parse encountered unexpected error: %v", err) - } - if err := p7.Verify(); err != nil { - t.Errorf("Verify failed with error: %v", err) - } -} - -func TestDecrypt(t *testing.T) { - fixture := UnmarshalTestFixture(EncryptedTestFixture) - p7, err := Parse(fixture.Input) - if err != nil { - t.Fatal(err) - } - content, err := p7.Decrypt(fixture.Certificate, fixture.PrivateKey) - if err != nil { - t.Errorf("Cannot Decrypt with error: %v", err) - } - expected := []byte("This is a test") - if bytes.Compare(content, expected) != 0 { - t.Errorf("Decrypted result does not match.\n\tExpected:%s\n\tActual:%s", expected, content) - } -} - -func TestDegenerateCertificate(t *testing.T) { - cert, err := createTestCertificate() - if err != nil { - t.Fatal(err) - } - deg, err := DegenerateCertificate(cert.Certificate.Raw) - if err != nil { - t.Fatal(err) - } - testOpenSSLParse(t, deg) - pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: deg}) -} - -// writes the cert to a temporary file and tests that openssl can read it. -func testOpenSSLParse(t *testing.T, certBytes []byte) { - tmpCertFile, err := ioutil.TempFile("", "testCertificate") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpCertFile.Name()) // clean up - - if _, err := tmpCertFile.Write(certBytes); err != nil { - t.Fatal(err) - } - - opensslCMD := exec.Command("openssl", "pkcs7", "-inform", "der", "-in", tmpCertFile.Name()) - _, err = opensslCMD.Output() - if err != nil { - t.Fatal(err) - } - - if err := tmpCertFile.Close(); err != nil { - t.Fatal(err) - } - -} - -func TestSign(t *testing.T) { - cert, err := createTestCertificate() - if err != nil { - t.Fatal(err) - } - content := []byte("Hello World") - for _, testDetach := range []bool{false, true} { - toBeSigned, err := NewSignedData(content) - if err != nil { - t.Fatalf("Cannot initialize signed data: %s", err) - } - if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil { - t.Fatalf("Cannot add signer: %s", err) - } - if testDetach { - t.Log("Testing detached signature") - toBeSigned.Detach() - } else { - t.Log("Testing attached signature") - } - signed, err := toBeSigned.Finish() - if err != nil { - t.Fatalf("Cannot finish signing data: %s", err) - } - pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: signed}) - p7, err := Parse(signed) - if err != nil { - t.Fatalf("Cannot parse our signed data: %s", err) - } - if testDetach { - p7.Content = content - } - if bytes.Compare(content, p7.Content) != 0 { - t.Errorf("Our content was not in the parsed data:\n\tExpected: %s\n\tActual: %s", content, p7.Content) - } - if err := p7.Verify(); err != nil { - t.Errorf("Cannot verify our signed data: %s", err) - } - } -} - -func ExampleSignedData() { - // generate a signing cert or load a key pair - cert, err := createTestCertificate() - if err != nil { - fmt.Printf("Cannot create test certificates: %s", err) - } - - // Initialize a SignedData struct with content to be signed - signedData, err := NewSignedData([]byte("Example data to be signed")) - if err != nil { - fmt.Printf("Cannot initialize signed data: %s", err) - } - - // Add the signing cert and private key - if err := signedData.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil { - fmt.Printf("Cannot add signer: %s", err) - } - - // Call Detach() is you want to remove content from the signature - // and generate an S/MIME detached signature - signedData.Detach() - - // Finish() to obtain the signature bytes - detachedSignature, err := signedData.Finish() - if err != nil { - fmt.Printf("Cannot finish signing data: %s", err) - } - pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: detachedSignature}) -} - -func TestOpenSSLVerifyDetachedSignature(t *testing.T) { - rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil) - if err != nil { - t.Fatalf("Cannot generate root cert: %s", err) - } - signerCert, err := createTestCertificateByIssuer("PKCS7 Test Signer Cert", rootCert) - if err != nil { - t.Fatalf("Cannot generate signer cert: %s", err) - } - content := []byte("Hello World") - toBeSigned, err := NewSignedData(content) - if err != nil { - t.Fatalf("Cannot initialize signed data: %s", err) - } - if err := toBeSigned.AddSigner(signerCert.Certificate, signerCert.PrivateKey, SignerInfoConfig{}); err != nil { - t.Fatalf("Cannot add signer: %s", err) - } - toBeSigned.Detach() - signed, err := toBeSigned.Finish() - if err != nil { - t.Fatalf("Cannot finish signing data: %s", err) - } - - // write the root cert to a temp file - tmpRootCertFile, err := ioutil.TempFile("", "pkcs7TestRootCA") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpRootCertFile.Name()) // clean up - fd, err := os.OpenFile(tmpRootCertFile.Name(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755) - if err != nil { - t.Fatal(err) - } - pem.Encode(fd, &pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Certificate.Raw}) - fd.Close() - - // write the signature to a temp file - tmpSignatureFile, err := ioutil.TempFile("", "pkcs7Signature") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpSignatureFile.Name()) // clean up - ioutil.WriteFile(tmpSignatureFile.Name(), signed, 0755) - - // write the content to a temp file - tmpContentFile, err := ioutil.TempFile("", "pkcs7Content") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpContentFile.Name()) // clean up - ioutil.WriteFile(tmpContentFile.Name(), content, 0755) - - // call openssl to verify the signature on the content using the root - opensslCMD := exec.Command("openssl", "smime", "-verify", - "-in", tmpSignatureFile.Name(), "-inform", "DER", - "-content", tmpContentFile.Name(), - "-CAfile", tmpRootCertFile.Name()) - out, err := opensslCMD.Output() - t.Logf("%s", out) - if err != nil { - t.Fatalf("openssl command failed with %s", err) - } -} - -func TestEncrypt(t *testing.T) { - modes := []int{ - EncryptionAlgorithmDESCBC, - EncryptionAlgorithmAES128GCM, - } - - for _, mode := range modes { - ContentEncryptionAlgorithm = mode - - plaintext := []byte("Hello Secret World!") - cert, err := createTestCertificate() - if err != nil { - t.Fatal(err) - } - encrypted, err := Encrypt(plaintext, []*x509.Certificate{cert.Certificate}) - if err != nil { - t.Fatal(err) - } - p7, err := Parse(encrypted) - if err != nil { - t.Fatalf("cannot Parse encrypted result: %s", err) - } - result, err := p7.Decrypt(cert.Certificate, cert.PrivateKey) - if err != nil { - t.Fatalf("cannot Decrypt encrypted result: %s", err) - } - if bytes.Compare(plaintext, result) != 0 { - t.Errorf("encrypted data does not match plaintext:\n\tExpected: %s\n\tActual: %s", plaintext, result) - } - } -} - -func TestUnmarshalSignedAttribute(t *testing.T) { - cert, err := createTestCertificate() - if err != nil { - t.Fatal(err) - } - content := []byte("Hello World") - toBeSigned, err := NewSignedData(content) - if err != nil { - t.Fatalf("Cannot initialize signed data: %s", err) - } - oidTest := asn1.ObjectIdentifier{2, 3, 4, 5, 6, 7} - testValue := "TestValue" - if err := toBeSigned.AddSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{ - ExtraSignedAttributes: []Attribute{Attribute{Type: oidTest, Value: testValue}}, - }); err != nil { - t.Fatalf("Cannot add signer: %s", err) - } - signed, err := toBeSigned.Finish() - if err != nil { - t.Fatalf("Cannot finish signing data: %s", err) - } - p7, err := Parse(signed) - var actual string - err = p7.UnmarshalSignedAttribute(oidTest, &actual) - if err != nil { - t.Fatalf("Cannot unmarshal test value: %s", err) - } - if testValue != actual { - t.Errorf("Attribute does not match test value\n\tExpected: %s\n\tActual: %s", testValue, actual) - } -} - -func TestPad(t *testing.T) { - tests := []struct { - Original []byte - Expected []byte - BlockSize int - }{ - {[]byte{0x1, 0x2, 0x3, 0x10}, []byte{0x1, 0x2, 0x3, 0x10, 0x4, 0x4, 0x4, 0x4}, 8}, - {[]byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0}, []byte{0x1, 0x2, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8}, 8}, - } - for _, test := range tests { - padded, err := pad(test.Original, test.BlockSize) - if err != nil { - t.Errorf("pad encountered error: %s", err) - continue - } - if bytes.Compare(test.Expected, padded) != 0 { - t.Errorf("pad results mismatch:\n\tExpected: %X\n\tActual: %X", test.Expected, padded) - } - } -} - -type certKeyPair struct { - Certificate *x509.Certificate - PrivateKey *rsa.PrivateKey -} - -func createTestCertificate() (certKeyPair, error) { - signer, err := createTestCertificateByIssuer("Eddard Stark", nil) - if err != nil { - return certKeyPair{}, err - } - fmt.Println("Created root cert") - pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: signer.Certificate.Raw}) - pair, err := createTestCertificateByIssuer("Jon Snow", signer) - if err != nil { - return certKeyPair{}, err - } - fmt.Println("Created signer cert") - pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: pair.Certificate.Raw}) - return *pair, nil -} - -func createTestCertificateByIssuer(name string, issuer *certKeyPair) (*certKeyPair, error) { - priv, err := rsa.GenerateKey(rand.Reader, 1024) - if err != nil { - return nil, err - } - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 32) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - return nil, err - } - - template := x509.Certificate{ - SerialNumber: serialNumber, - SignatureAlgorithm: x509.SHA256WithRSA, - Subject: pkix.Name{ - CommonName: name, - Organization: []string{"Acme Co"}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().AddDate(1, 0, 0), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}, - } - var issuerCert *x509.Certificate - var issuerKey crypto.PrivateKey - if issuer != nil { - issuerCert = issuer.Certificate - issuerKey = issuer.PrivateKey - } else { - template.IsCA = true - template.KeyUsage |= x509.KeyUsageCertSign - issuerCert = &template - issuerKey = priv - } - cert, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, priv.Public(), issuerKey) - if err != nil { - return nil, err - } - leaf, err := x509.ParseCertificate(cert) - if err != nil { - return nil, err - } - return &certKeyPair{ - Certificate: leaf, - PrivateKey: priv, - }, nil -} - -type TestFixture struct { - Input []byte - Certificate *x509.Certificate - PrivateKey *rsa.PrivateKey -} - -func UnmarshalTestFixture(testPEMBlock string) TestFixture { - var result TestFixture - var derBlock *pem.Block - var pemBlock = []byte(testPEMBlock) - for { - derBlock, pemBlock = pem.Decode(pemBlock) - if derBlock == nil { - break - } - switch derBlock.Type { - case "PKCS7": - result.Input = derBlock.Bytes - case "CERTIFICATE": - result.Certificate, _ = x509.ParseCertificate(derBlock.Bytes) - case "PRIVATE KEY": - result.PrivateKey, _ = x509.ParsePKCS1PrivateKey(derBlock.Bytes) - } - } - - return result -} - -func MarshalTestFixture(t TestFixture, w io.Writer) { - if t.Input != nil { - pem.Encode(w, &pem.Block{Type: "PKCS7", Bytes: t.Input}) - } - if t.Certificate != nil { - pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: t.Certificate.Raw}) - } - if t.PrivateKey != nil { - pem.Encode(w, &pem.Block{Type: "PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(t.PrivateKey)}) - } -} - -var SignedTestFixture = ` ------BEGIN PKCS7----- -MIIDVgYJKoZIhvcNAQcCoIIDRzCCA0MCAQExCTAHBgUrDgMCGjAcBgkqhkiG9w0B -BwGgDwQNV2UgdGhlIFBlb3BsZaCCAdkwggHVMIIBQKADAgECAgRpuDctMAsGCSqG -SIb3DQEBCzApMRAwDgYDVQQKEwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3Rh -cmswHhcNMTUwNTA2MDQyNDQ4WhcNMTYwNTA2MDQyNDQ4WjAlMRAwDgYDVQQKEwdB -Y21lIENvMREwDwYDVQQDEwhKb24gU25vdzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw -gYkCgYEAqr+tTF4mZP5rMwlXp1y+crRtFpuLXF1zvBZiYMfIvAHwo1ta8E1IcyEP -J1jIiKMcwbzeo6kAmZzIJRCTezq9jwXUsKbQTvcfOH9HmjUmXBRWFXZYoQs/OaaF -a45deHmwEeMQkuSWEtYiVKKZXtJOtflKIT3MryJEDiiItMkdybUCAwEAAaMSMBAw -DgYDVR0PAQH/BAQDAgCgMAsGCSqGSIb3DQEBCwOBgQDK1EweZWRL+f7Z+J0kVzY8 -zXptcBaV4Lf5wGZJLJVUgp33bpLNpT3yadS++XQJ+cvtW3wADQzBSTMduyOF8Zf+ -L7TjjrQ2+F2HbNbKUhBQKudxTfv9dJHdKbD+ngCCdQJYkIy2YexsoNG0C8nQkggy -axZd/J69xDVx6pui3Sj8sDGCATYwggEyAgEBMDEwKTEQMA4GA1UEChMHQWNtZSBD -bzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrAgRpuDctMAcGBSsOAwIaoGEwGAYJKoZI -hvcNAQkDMQsGCSqGSIb3DQEHATAgBgkqhkiG9w0BCQUxExcRMTUwNTA2MDAyNDQ4 -LTA0MDAwIwYJKoZIhvcNAQkEMRYEFG9D7gcTh9zfKiYNJ1lgB0yTh4sZMAsGCSqG -SIb3DQEBAQSBgFF3sGDU9PtXty/QMtpcFa35vvIOqmWQAIZt93XAskQOnBq4OloX -iL9Ct7t1m4pzjRm0o9nDkbaSLZe7HKASHdCqijroScGlI8M+alJ8drHSFv6ZIjnM -FIwIf0B2Lko6nh9/6mUXq7tbbIHa3Gd1JUVire/QFFtmgRXMbXYk8SIS ------END PKCS7----- ------BEGIN CERTIFICATE----- -MIIB1TCCAUCgAwIBAgIEabg3LTALBgkqhkiG9w0BAQswKTEQMA4GA1UEChMHQWNt -ZSBDbzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrMB4XDTE1MDUwNjA0MjQ0OFoXDTE2 -MDUwNjA0MjQ0OFowJTEQMA4GA1UEChMHQWNtZSBDbzERMA8GA1UEAxMISm9uIFNu -b3cwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKq/rUxeJmT+azMJV6dcvnK0 -bRabi1xdc7wWYmDHyLwB8KNbWvBNSHMhDydYyIijHMG83qOpAJmcyCUQk3s6vY8F -1LCm0E73Hzh/R5o1JlwUVhV2WKELPzmmhWuOXXh5sBHjEJLklhLWIlSimV7STrX5 -SiE9zK8iRA4oiLTJHcm1AgMBAAGjEjAQMA4GA1UdDwEB/wQEAwIAoDALBgkqhkiG -9w0BAQsDgYEAytRMHmVkS/n+2fidJFc2PM16bXAWleC3+cBmSSyVVIKd926SzaU9 -8mnUvvl0CfnL7Vt8AA0MwUkzHbsjhfGX/i+04460Nvhdh2zWylIQUCrncU37/XSR -3Smw/p4AgnUCWJCMtmHsbKDRtAvJ0JIIMmsWXfyevcQ1ceqbot0o/LA= ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIICXgIBAAKBgQCqv61MXiZk/mszCVenXL5ytG0Wm4tcXXO8FmJgx8i8AfCjW1rw -TUhzIQ8nWMiIoxzBvN6jqQCZnMglEJN7Or2PBdSwptBO9x84f0eaNSZcFFYVdlih -Cz85poVrjl14ebAR4xCS5JYS1iJUople0k61+UohPcyvIkQOKIi0yR3JtQIDAQAB -AoGBAIPLCR9N+IKxodq11lNXEaUFwMHXc1zqwP8no+2hpz3+nVfplqqubEJ4/PJY -5AgbJoIfnxVhyBXJXu7E+aD/OPneKZrgp58YvHKgGvvPyJg2gpC/1Fh0vQB0HNpI -1ZzIZUl8ZTUtVgtnCBUOh5JGI4bFokAqrT//Uvcfd+idgxqBAkEA1ZbP/Kseld14 -qbWmgmU5GCVxsZRxgR1j4lG3UVjH36KXMtRTm1atAam1uw3OEGa6Y3ANjpU52FaB -Hep5rkk4FQJBAMynMo1L1uiN5GP+KYLEF5kKRxK+FLjXR0ywnMh+gpGcZDcOae+J -+t1gLoWBIESH/Xt639T7smuSfrZSA9V0EyECQA8cvZiWDvLxmaEAXkipmtGPjKzQ -4PsOtkuEFqFl07aKDYKmLUg3aMROWrJidqsIabWxbvQgsNgSvs38EiH3wkUCQQCg -ndxb7piVXb9RBwm3OoU2tE1BlXMX+sVXmAkEhd2dwDsaxrI3sHf1xGXem5AimQRF -JBOFyaCnMotGNioSHY5hAkEAxyXcNixQ2RpLXJTQZtwnbk0XDcbgB+fBgXnv/4f3 -BCvcu85DqJeJyQv44Oe1qsXEX9BfcQIOVaoep35RPlKi9g== ------END PRIVATE KEY-----` - -// echo -n "This is a test" > test.txt -// openssl cms -encrypt -in test.txt cert.pem -var EncryptedTestFixture = ` ------BEGIN PKCS7----- -MIIBGgYJKoZIhvcNAQcDoIIBCzCCAQcCAQAxgcwwgckCAQAwMjApMRAwDgYDVQQK -EwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3RhcmsCBQDL+CvWMA0GCSqGSIb3 -DQEBAQUABIGAyFz7bfI2noUs4FpmYfztm1pVjGyB00p9x0H3gGHEYNXdqlq8VG8d -iq36poWtEkatnwsOlURWZYECSi0g5IAL0U9sj82EN0xssZNaK0S5FTGnB3DPvYgt -HJvcKq7YvNLKMh4oqd17C6GB4oXyEBDj0vZnL7SUoCAOAWELPeC8CTUwMwYJKoZI -hvcNAQcBMBQGCCqGSIb3DQMHBAhEowTkot3a7oAQFD//J/IhFnk+JbkH7HZQFA== ------END PKCS7----- ------BEGIN CERTIFICATE----- -MIIB1jCCAUGgAwIBAgIFAMv4K9YwCwYJKoZIhvcNAQELMCkxEDAOBgNVBAoTB0Fj -bWUgQ28xFTATBgNVBAMTDEVkZGFyZCBTdGFyazAeFw0xNTA1MDYwMzU2NDBaFw0x -NjA1MDYwMzU2NDBaMCUxEDAOBgNVBAoTB0FjbWUgQ28xETAPBgNVBAMTCEpvbiBT -bm93MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK6NU0R0eiCYVquU4RcjKc -LzGfx0aa1lMr2TnLQUSeLFZHFxsyyMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg -8+Zg2r8xnnney7abxcuv0uATWSIeKlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP -+Zxp2ni5qHNraf3wE2VPIQIDAQABoxIwEDAOBgNVHQ8BAf8EBAMCAKAwCwYJKoZI -hvcNAQELA4GBAIr2F7wsqmEU/J/kLyrCgEVXgaV/sKZq4pPNnzS0tBYk8fkV3V18 -sBJyHKRLL/wFZASvzDcVGCplXyMdAOCyfd8jO3F9Ac/xdlz10RrHJT75hNu3a7/n -9KNwKhfN4A1CQv2x372oGjRhCW5bHNCWx4PIVeNzCyq/KZhyY9sxHE6f ------END CERTIFICATE----- ------BEGIN PRIVATE KEY----- -MIICXgIBAAKBgQDK6NU0R0eiCYVquU4RcjKcLzGfx0aa1lMr2TnLQUSeLFZHFxsy -yMXXuMPig3HK4A7SGFHupO+/1H/sL4xpH5zg8+Zg2r8xnnney7abxcuv0uATWSIe -KlNnb1ZO1BAxFnESc3GtyOCr2dUwZHX5mRVP+Zxp2ni5qHNraf3wE2VPIQIDAQAB -AoGBALyvnSt7KUquDen7nXQtvJBudnf9KFPt//OjkdHHxNZNpoF/JCSqfQeoYkeu -MdAVYNLQGMiRifzZz4dDhA9xfUAuy7lcGQcMCxEQ1dwwuFaYkawbS0Tvy2PFlq2d -H5/HeDXU4EDJ3BZg0eYj2Bnkt1sJI35UKQSxblQ0MY2q0uFBAkEA5MMOogkgUx1C -67S1tFqMUSM8D0mZB0O5vOJZC5Gtt2Urju6vywge2ArExWRXlM2qGl8afFy2SgSv -Xk5eybcEiQJBAOMRwwbEoW5NYHuFFbSJyWll4n71CYuWuQOCzehDPyTb80WFZGLV -i91kFIjeERyq88eDE5xVB3ZuRiXqaShO/9kCQQCKOEkpInaDgZSjskZvuJ47kByD -6CYsO4GIXQMMeHML8ncFH7bb6AYq5ybJVb2NTU7QLFJmfeYuhvIm+xdOreRxAkEA -o5FC5Jg2FUfFzZSDmyZ6IONUsdF/i78KDV5nRv1R+hI6/oRlWNCtTNBv/lvBBd6b -dseUE9QoaQZsn5lpILEvmQJAZ0B+Or1rAYjnbjnUhdVZoy9kC4Zov+4UH3N/BtSy -KJRWUR0wTWfZBPZ5hAYZjTBEAFULaYCXlQKsODSp0M1aQA== ------END PRIVATE KEY-----` - -var EC2IdentityDocumentFixture = ` ------BEGIN PKCS7----- -MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAaCA -JIAEggGmewogICJwcml2YXRlSXAiIDogIjE3Mi4zMC4wLjI1MiIsCiAgImRldnBh -eVByb2R1Y3RDb2RlcyIgOiBudWxsLAogICJhdmFpbGFiaWxpdHlab25lIiA6ICJ1 -cy1lYXN0LTFhIiwKICAidmVyc2lvbiIgOiAiMjAxMC0wOC0zMSIsCiAgImluc3Rh -bmNlSWQiIDogImktZjc5ZmU1NmMiLAogICJiaWxsaW5nUHJvZHVjdHMiIDogbnVs -bCwKICAiaW5zdGFuY2VUeXBlIiA6ICJ0Mi5taWNybyIsCiAgImFjY291bnRJZCIg -OiAiMTIxNjU5MDE0MzM0IiwKICAiaW1hZ2VJZCIgOiAiYW1pLWZjZTNjNjk2IiwK -ICAicGVuZGluZ1RpbWUiIDogIjIwMTYtMDQtMDhUMDM6MDE6MzhaIiwKICAiYXJj -aGl0ZWN0dXJlIiA6ICJ4ODZfNjQiLAogICJrZXJuZWxJZCIgOiBudWxsLAogICJy -YW1kaXNrSWQiIDogbnVsbCwKICAicmVnaW9uIiA6ICJ1cy1lYXN0LTEiCn0AAAAA -AAAxggEYMIIBFAIBATBpMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5n -dG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2Vi -IFNlcnZpY2VzIExMQwIJAJa6SNnlXhpnMAkGBSsOAwIaBQCgXTAYBgkqhkiG9w0B -CQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNjA0MDgwMzAxNDRaMCMG -CSqGSIb3DQEJBDEWBBTuUc28eBXmImAautC+wOjqcFCBVjAJBgcqhkjOOAQDBC8w -LQIVAKA54NxGHWWCz5InboDmY/GHs33nAhQ6O/ZI86NwjA9Vz3RNMUJrUPU5tAAA -AAAAAA== ------END PKCS7----- ------BEGIN CERTIFICATE----- -MIIC7TCCAq0CCQCWukjZ5V4aZzAJBgcqhkjOOAQDMFwxCzAJBgNVBAYTAlVTMRkw -FwYDVQQIExBXYXNoaW5ndG9uIFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYD -VQQKExdBbWF6b24gV2ViIFNlcnZpY2VzIExMQzAeFw0xMjAxMDUxMjU2MTJaFw0z -ODAxMDUxMjU2MTJaMFwxCzAJBgNVBAYTAlVTMRkwFwYDVQQIExBXYXNoaW5ndG9u -IFN0YXRlMRAwDgYDVQQHEwdTZWF0dGxlMSAwHgYDVQQKExdBbWF6b24gV2ViIFNl -cnZpY2VzIExMQzCCAbcwggEsBgcqhkjOOAQBMIIBHwKBgQCjkvcS2bb1VQ4yt/5e -ih5OO6kK/n1Lzllr7D8ZwtQP8fOEpp5E2ng+D6Ud1Z1gYipr58Kj3nssSNpI6bX3 -VyIQzK7wLclnd/YozqNNmgIyZecN7EglK9ITHJLP+x8FtUpt3QbyYXJdmVMegN6P -hviYt5JH/nYl4hh3Pa1HJdskgQIVALVJ3ER11+Ko4tP6nwvHwh6+ERYRAoGBAI1j -k+tkqMVHuAFcvAGKocTgsjJem6/5qomzJuKDmbJNu9Qxw3rAotXau8Qe+MBcJl/U -hhy1KHVpCGl9fueQ2s6IL0CaO/buycU1CiYQk40KNHCcHfNiZbdlx1E9rpUp7bnF -lRa2v1ntMX3caRVDdbtPEWmdxSCYsYFDk4mZrOLBA4GEAAKBgEbmeve5f8LIE/Gf -MNmP9CM5eovQOGx5ho8WqD+aTebs+k2tn92BBPqeZqpWRa5P/+jrdKml1qx4llHW -MXrs3IgIb6+hUIB+S8dz8/mmO0bpr76RoZVCXYab2CZedFut7qc3WUH9+EUAH5mw -vSeDCOUMYQR7R9LINYwouHIziqQYMAkGByqGSM44BAMDLwAwLAIUWXBlk40xTwSw -7HX32MxXYruse9ACFBNGmdX2ZBrVNGrN9N2f6ROk0k9K ------END CERTIFICATE-----` - -var AppStoreRecieptFixture = ` ------BEGIN PKCS7----- -MIITtgYJKoZIhvcNAQcCoIITpzCCE6MCAQExCzAJBgUrDgMCGgUAMIIDVwYJKoZI -hvcNAQcBoIIDSASCA0QxggNAMAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQEC -AQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADAL -AgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQE -AgIAjTANAgENAgEBBAUCAwFgvTANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAy -NDcwGAIBAgIBAQQQDA5jb20uemhpaHUudGVzdDAYAgEEAgECBBCS+ZODNMHwT1Nz -gWYDXyWZMBsCAQACAQEEEwwRUHJvZHVjdGlvblNhbmRib3gwHAIBBQIBAQQU4nRh -YCEZx70Flzv7hvJRjJZckYIwHgIBDAIBAQQWFhQyMDE2LTA3LTIzVDA2OjIxOjEx -WjAeAgESAgEBBBYWFDIwMTMtMDgtMDFUMDc6MDA6MDBaMD0CAQYCAQEENbR21I+a -8+byMXo3NPRoDWQmSXQF2EcCeBoD4GaL//ZCRETp9rGFPSg1KekCP7Kr9HAqw09m -MEICAQcCAQEEOlVJozYYBdugybShbiiMsejDMNeCbZq6CrzGBwW6GBy+DGWxJI91 -Y3ouXN4TZUhuVvLvN1b0m5T3ggQwggFaAgERAgEBBIIBUDGCAUwwCwICBqwCAQEE -AhYAMAsCAgatAgEBBAIMADALAgIGsAIBAQQCFgAwCwICBrICAQEEAgwAMAsCAgaz -AgEBBAIMADALAgIGtAIBAQQCDAAwCwICBrUCAQEEAgwAMAsCAga2AgEBBAIMADAM -AgIGpQIBAQQDAgEBMAwCAgarAgEBBAMCAQEwDAICBq4CAQEEAwIBADAMAgIGrwIB -AQQDAgEAMAwCAgaxAgEBBAMCAQAwGwICBqcCAQEEEgwQMTAwMDAwMDIyNTMyNTkw -MTAbAgIGqQIBAQQSDBAxMDAwMDAwMjI1MzI1OTAxMB8CAgaoAgEBBBYWFDIwMTYt -MDctMjNUMDY6MjE6MTFaMB8CAgaqAgEBBBYWFDIwMTYtMDctMjNUMDY6MjE6MTFa -MCACAgamAgEBBBcMFWNvbS56aGlodS50ZXN0LnRlc3RfMaCCDmUwggV8MIIEZKAD -AgECAggO61eH554JjTANBgkqhkiG9w0BAQUFADCBljELMAkGA1UEBhMCVVMxEzAR -BgNVBAoMCkFwcGxlIEluYy4xLDAqBgNVBAsMI0FwcGxlIFdvcmxkd2lkZSBEZXZl -bG9wZXIgUmVsYXRpb25zMUQwQgYDVQQDDDtBcHBsZSBXb3JsZHdpZGUgRGV2ZWxv -cGVyIFJlbGF0aW9ucyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNTExMTMw -MjE1MDlaFw0yMzAyMDcyMTQ4NDdaMIGJMTcwNQYDVQQDDC5NYWMgQXBwIFN0b3Jl -IGFuZCBpVHVuZXMgU3RvcmUgUmVjZWlwdCBTaWduaW5nMSwwKgYDVQQLDCNBcHBs -ZSBXb3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczETMBEGA1UECgwKQXBwbGUg -SW5jLjELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -AQClz4H9JaKBW9aH7SPaMxyO4iPApcQmyz3Gn+xKDVWG/6QC15fKOVRtfX+yVBid -xCxScY5ke4LOibpJ1gjltIhxzz9bRi7GxB24A6lYogQ+IXjV27fQjhKNg0xbKmg3 -k8LyvR7E0qEMSlhSqxLj7d0fmBWQNS3CzBLKjUiB91h4VGvojDE2H0oGDEdU8zeQ -uLKSiX1fpIVK4cCc4Lqku4KXY/Qrk8H9Pm/KwfU8qY9SGsAlCnYO3v6Z/v/Ca/Vb -XqxzUUkIVonMQ5DMjoEC0KCXtlyxoWlph5AQaCYmObgdEHOwCl3Fc9DfdjvYLdmI -HuPsB8/ijtDT+iZVge/iA0kjAgMBAAGjggHXMIIB0zA/BggrBgEFBQcBAQQzMDEw -LwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwLmFwcGxlLmNvbS9vY3NwMDMtd3dkcjA0 -MB0GA1UdDgQWBBSRpJz8xHa3n6CK9E31jzZd7SsEhTAMBgNVHRMBAf8EAjAAMB8G -A1UdIwQYMBaAFIgnFwmpthhgi+zruvZHWcVSVKO3MIIBHgYDVR0gBIIBFTCCAREw -ggENBgoqhkiG92NkBQYBMIH+MIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9u -IHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5j -ZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25k -aXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0 -aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDYGCCsGAQUFBwIBFipodHRwOi8vd3d3 -LmFwcGxlLmNvbS9jZXJ0aWZpY2F0ZWF1dGhvcml0eS8wDgYDVR0PAQH/BAQDAgeA -MBAGCiqGSIb3Y2QGCwEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQANphvTLj3jWysH -bkKWbNPojEMwgl/gXNGNvr0PvRr8JZLbjIXDgFnf4+LXLgUUrA3btrj+/DUufMut -F2uOfx/kd7mxZ5W0E16mGYZ2+FogledjjA9z/Ojtxh+umfhlSFyg4Cg6wBA3Lbmg -BDkfc7nIBf3y3n8aKipuKwH8oCBc2et9J6Yz+PWY4L5E27FMZ/xuCk/J4gao0pfz -p45rUaJahHVl0RYEYuPBX/UIqc9o2ZIAycGMs/iNAGS6WGDAfK+PdcppuVsq1h1o -bphC9UynNxmbzDscehlD86Ntv0hgBgw2kivs3hi1EdotI9CO/KBpnBcbnoB7OUdF -MGEvxxOoMIIEIjCCAwqgAwIBAgIIAd68xDltoBAwDQYJKoZIhvcNAQEFBQAwYjEL -MAkGA1UEBhMCVVMxEzARBgNVBAoTCkFwcGxlIEluYy4xJjAkBgNVBAsTHUFwcGxl -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRYwFAYDVQQDEw1BcHBsZSBSb290IENB -MB4XDTEzMDIwNzIxNDg0N1oXDTIzMDIwNzIxNDg0N1owgZYxCzAJBgNVBAYTAlVT -MRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBXb3JsZHdpZGUg -RGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERl -dmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0G -CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKOFSmy1aqyCQ5SOmM7uxfuH8mkbw0 -U3rOfGOAYXdkXqUHI7Y5/lAtFVZYcC1+xG7BSoU+L/DehBqhV8mvexj/avoVEkkV -CBmsqtsqMu2WY2hSFT2Miuy/axiV4AOsAX2XBWfODoWVN2rtCbauZ81RZJ/GXNG8 -V25nNYB2NqSHgW44j9grFU57Jdhav06DwY3Sk9UacbVgnJ0zTlX5ElgMhrgWDcHl -d0WNUEi6Ky3klIXh6MSdxmilsKP8Z35wugJZS3dCkTm59c3hTO/AO0iMpuUhXf1q -arunFjVg0uat80YpyejDi+l5wGphZxWy8P3laLxiX27Pmd3vG2P+kmWrAgMBAAGj -gaYwgaMwHQYDVR0OBBYEFIgnFwmpthhgi+zruvZHWcVSVKO3MA8GA1UdEwEB/wQF -MAMBAf8wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wLgYDVR0fBCcw -JTAjoCGgH4YdaHR0cDovL2NybC5hcHBsZS5jb20vcm9vdC5jcmwwDgYDVR0PAQH/ -BAQDAgGGMBAGCiqGSIb3Y2QGAgEEAgUAMA0GCSqGSIb3DQEBBQUAA4IBAQBPz+9Z -viz1smwvj+4ThzLoBTWobot9yWkMudkXvHcs1Gfi/ZptOllc34MBvbKuKmFysa/N -w0Uwj6ODDc4dR7Txk4qjdJukw5hyhzs+r0ULklS5MruQGFNrCk4QttkdUGwhgAqJ -TleMa1s8Pab93vcNIx0LSiaHP7qRkkykGRIZbVf1eliHe2iK5IaMSuviSRSqpd1V -AKmuu0swruGgsbwpgOYJd+W+NKIByn/c4grmO7i77LpilfMFY0GCzQ87HUyVpNur -+cmV6U/kTecmmYHpvPm0KdIBembhLoz2IYrF+Hjhga6/05Cdqa3zr/04GpZnMBxR -pVzscYqCtGwPDBUfMIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQsw -CQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUg -Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0Ew -HhcNMDYwNDI1MjE0MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzET -MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv -biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne -+Uts9QerIjAC6Bg++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjcz -y8QPTc4UadHJGXL1XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQ -Z48ItCD3y6wsIG9wtj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCS -C7EhFi501TwN22IWq6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINB -hzOKgbEwWOxaBDKMaLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIB -djAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9Bp -R5R2Cf70a40uQKb3R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/ -CF4wggERBgNVHSAEggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcC -ARYeaHR0cHM6Ly93d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCB -thqBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFz -c3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJk -IHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5 -IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3 -DQEBBQUAA4IBAQBcNplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizU -sZAS2L70c5vu0mQPy3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJ -fBdAVhEedNO3iyM7R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr -1KIkIxH3oayPc4FgxhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltk -wGMzd/c6ByxW69oPIQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIq -xw8dtk2cXmPIS4AXUKqK1drk/NAJBzewdXUhMYIByzCCAccCAQEwgaMwgZYxCzAJ -BgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSwwKgYDVQQLDCNBcHBsZSBX -b3JsZHdpZGUgRGV2ZWxvcGVyIFJlbGF0aW9uczFEMEIGA1UEAww7QXBwbGUgV29y -bGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkCCA7rV4fnngmNMAkGBSsOAwIaBQAwDQYJKoZIhvcNAQEBBQAEggEAasPtnide -NWyfUtewW9OSgcQA8pW+5tWMR0469cBPZR84uJa0gyfmPspySvbNOAwnrwzZHYLa -ujOxZLip4DUw4F5s3QwUa3y4BXpF4J+NSn9XNvxNtnT/GcEQtCuFwgJ0o3F0ilhv -MTHrwiwyx/vr+uNDqlORK8lfK+1qNp+A/kzh8eszMrn4JSeTh9ZYxLHE56WkTQGD -VZXl0gKgxSOmDrcp1eQxdlymzrPv9U60wUJ0bkPfrU9qZj3mJrmrkQk61JTe3j6/ -QfjfFBG9JG2mUmYQP1KQ3SypGHzDW8vngvsGu//tNU0NFfOqQu4bYU4VpQl0nPtD -4B85NkrgvQsWAQ== ------END PKCS7-----` From 59c83c5f63e1784ddc857c3b3f8523f58c6d738e Mon Sep 17 00:00:00 2001 From: Aleksei Pavliukov Date: Mon, 14 Jan 2019 12:39:19 +0300 Subject: [PATCH 04/20] add digital sign API prototype --- pdf/model/appearance.go | 2 +- pdf/model/appender.go | 8 +++--- pdf/model/signature.go | 40 +++++++++++++++------------- pdf/model/signature_handler.go | 18 ++++++------- pdf/model/signature_handler_pkcs7.go | 6 ++--- 5 files changed, 39 insertions(+), 35 deletions(-) diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go index e6e0fe3e..05b681a5 100644 --- a/pdf/model/appearance.go +++ b/pdf/model/appearance.go @@ -14,7 +14,7 @@ type PdfAppearance struct { Signature *PdfSignature } -// NewPdfAnnotationWidget returns an initialized annotation widget. +// NewPdfAppearance returns an initialized annotation widget. func NewPdfAppearance() *PdfAppearance { app := &PdfAppearance{} app.PdfField = NewPdfField() diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 4110cc81..5a5d6001 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -536,8 +536,11 @@ func (a *PdfAppender) Write(w io.Writer) error { if ind, found := core.GetIndirect(obj); found { if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { handler := *sigDict.handler - // TODO fix it - digestWriters[handler], _ = handler.NewDigest(sigDict.signature) + var err error + digestWriters[handler], err = handler.NewDigest(sigDict.signature) + if err != nil { + return err + } byteRange.Append(core.MakeInteger(0xfffff), core.MakeInteger(0xfffff)) } } @@ -562,7 +565,6 @@ func (a *PdfAppender) Write(w io.Writer) error { for _, hash := range digestWriters { writers = append(writers, hash) } - //hashSha1 := sha1.New() // if needed reader = io.TeeReader(a.rs, io.MultiWriter(writers...)) } diff --git a/pdf/model/signature.go b/pdf/model/signature.go index d713a262..cf62748d 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -6,6 +6,8 @@ package model import ( + "bytes" + "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" ) @@ -40,33 +42,33 @@ func (d *pdfSignDictionary) WriteString() string { d.contentsOffsetEnd = 0 d.byteRangeOffsetStart = 0 d.byteRangeOffsetEnd = 0 - - outStr := "<<" + out := bytes.NewBuffer(nil) + out.WriteString("<<") for _, k := range d.Keys() { v := d.Get(k) switch k { case "ByteRange": - outStr += k.WriteString() - outStr += " " - d.byteRangeOffsetStart = len(outStr) - outStr += v.WriteString() - outStr += " " - d.byteRangeOffsetEnd = len(outStr) + out.WriteString(k.WriteString()) + out.WriteString(" ") + d.byteRangeOffsetStart = out.Len() + out.WriteString(v.WriteString()) + out.WriteString(" ") + d.byteRangeOffsetEnd = out.Len() case "Contents": - outStr += k.WriteString() - outStr += " " - d.contentsOffsetStart = len(outStr) - outStr += v.WriteString() - outStr += " " - d.contentsOffsetEnd = len(outStr) + out.WriteString(k.WriteString()) + out.WriteString(" ") + d.contentsOffsetStart = out.Len() + out.WriteString(v.WriteString()) + out.WriteString(" ") + d.contentsOffsetEnd = out.Len() default: - outStr += k.WriteString() - outStr += " " - outStr += v.WriteString() + out.WriteString(k.WriteString()) + out.WriteString(" ") + out.WriteString(v.WriteString()) } } - outStr += ">>" - return outStr + out.WriteString(">>") + return out.String() } // PdfSignature represents a PDF signature dictionary and is used for signing via form signature fields. diff --git a/pdf/model/signature_handler.go b/pdf/model/signature_handler.go index 24a3eaaa..fdda40d6 100644 --- a/pdf/model/signature_handler.go +++ b/pdf/model/signature_handler.go @@ -18,8 +18,8 @@ import ( "github.com/unidoc/unidoc/pdf/core" ) -// Digest is the interface that wraps the basic Write method. -type Digest interface { +// Hasher is the interface that wraps the basic Write method. +type Hasher interface { Write(p []byte) (n int, err error) } @@ -27,11 +27,11 @@ type Digest interface { // need to be capable of validating digital signatures and signing PDF documents. type SignatureHandler interface { IsApplicable(sig *PdfSignature) bool - Validate(sig *PdfSignature, digest Digest) (SignatureValidationResult, error) + Validate(sig *PdfSignature, digest Hasher) (SignatureValidationResult, error) // InitSignature sets the PdfSignature parameters. InitSignature(*PdfSignature) error - NewDigest(sig *PdfSignature) (Digest, error) - Sign(sig *PdfSignature, digest Digest) error + NewDigest(sig *PdfSignature) (Hasher, error) + Sign(sig *PdfSignature, digest Hasher) error } // SignatureValidationResult defines the response from the signature validation handler. @@ -112,7 +112,7 @@ func (a *adobeX509RSASHA1SignatureHandler) getCertificate(sig *PdfSignature) (*x } // NewDigest creates a new digest. -func (a *adobeX509RSASHA1SignatureHandler) NewDigest(sig *PdfSignature) (Digest, error) { +func (a *adobeX509RSASHA1SignatureHandler) NewDigest(sig *PdfSignature) (Hasher, error) { certificate, err := a.getCertificate(sig) if err != nil { return nil, err @@ -122,7 +122,7 @@ func (a *adobeX509RSASHA1SignatureHandler) NewDigest(sig *PdfSignature) (Digest, } // Validate validates PdfSignature. -func (a *adobeX509RSASHA1SignatureHandler) Validate(sig *PdfSignature, digest Digest) (SignatureValidationResult, error) { +func (a *adobeX509RSASHA1SignatureHandler) Validate(sig *PdfSignature, digest Hasher) (SignatureValidationResult, error) { certData := sig.Cert.(*core.PdfObjectString).Bytes() certs, err := x509.ParseCertificates(certData) if err != nil { @@ -153,7 +153,7 @@ func (a *adobeX509RSASHA1SignatureHandler) Validate(sig *PdfSignature, digest Di } // Sign sets the Contents fields. -func (a *adobeX509RSASHA1SignatureHandler) Sign(sig *PdfSignature, digest Digest) error { +func (a *adobeX509RSASHA1SignatureHandler) Sign(sig *PdfSignature, digest Hasher) error { h, ok := digest.(hash.Hash) if !ok { return errors.New("hash type error") @@ -194,7 +194,7 @@ func (r *PdfReader) Validate(handlers []SignatureHandler) ([]SignatureValidation } var pairs []*sigFieldPair - for _, f := range *r.AcroForm.Fields { + for _, f := range r.AcroForm.AllFields() { if f.V == nil { continue } diff --git a/pdf/model/signature_handler_pkcs7.go b/pdf/model/signature_handler_pkcs7.go index 9cbcb99a..affaf803 100644 --- a/pdf/model/signature_handler_pkcs7.go +++ b/pdf/model/signature_handler_pkcs7.go @@ -65,12 +65,12 @@ func (a *adobePKCS7DetachedSignatureHandler) getCertificate(sig *PdfSignature) ( } // NewDigest creates a new digest. -func (a *adobePKCS7DetachedSignatureHandler) NewDigest(sig *PdfSignature) (Digest, error) { +func (a *adobePKCS7DetachedSignatureHandler) NewDigest(sig *PdfSignature) (Hasher, error) { return bytes.NewBuffer(nil), nil } // Validate validates PdfSignature. -func (a *adobePKCS7DetachedSignatureHandler) Validate(sig *PdfSignature, digest Digest) (SignatureValidationResult, error) { +func (a *adobePKCS7DetachedSignatureHandler) Validate(sig *PdfSignature, digest Hasher) (SignatureValidationResult, error) { signed := sig.Contents.Bytes() buffer := digest.(*bytes.Buffer) @@ -88,7 +88,7 @@ func (a *adobePKCS7DetachedSignatureHandler) Validate(sig *PdfSignature, digest } // Sign sets the Contents fields. -func (a *adobePKCS7DetachedSignatureHandler) Sign(sig *PdfSignature, digest Digest) error { +func (a *adobePKCS7DetachedSignatureHandler) Sign(sig *PdfSignature, digest Hasher) error { buffer := digest.(*bytes.Buffer) signedData, err := pkcs7.NewSignedData(buffer.Bytes()) From 11e11f4b94826ee7ad86bd60357ad1d8e5dd8ad6 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 22:39:38 +0000 Subject: [PATCH 05/20] Moved signature handler implementation logic to subpackage sighandlers. --- pdf/model/appender_test.go | 160 ++++++++++--- .../sighandler_pkcs7.go} | 37 +-- pdf/model/sighandler/sighandler_rsa_sha1.go | 152 ++++++++++++ pdf/model/signature_handler.go | 222 ++++++------------ 4 files changed, 367 insertions(+), 204 deletions(-) rename pdf/model/{signature_handler_pkcs7.go => sighandler/sighandler_pkcs7.go} (66%) create mode 100644 pdf/model/sighandler/sighandler_rsa_sha1.go diff --git a/pdf/model/appender_test.go b/pdf/model/appender_test.go index 2dbc7f8d..04c1b6d8 100644 --- a/pdf/model/appender_test.go +++ b/pdf/model/appender_test.go @@ -3,24 +3,28 @@ * file 'LICENSE.md', which is part of this source code package. */ -package model +package model_test import ( "bytes" "crypto/rsa" + "fmt" "io/ioutil" "os" "path/filepath" "testing" + "golang.org/x/crypto/pkcs12" + "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" - "golang.org/x/crypto/pkcs12" + "github.com/unidoc/unidoc/pdf/model" + "github.com/unidoc/unidoc/pdf/model/sighandler" ) -// This test file contains multiple tests to generate PDFs from existing Pdf files. The outputs are written into /tmp as files. The files -// themselves need to be observed to check for correctness as we don't have a good way to automatically check -// if every detail is correct. +// This test file contains multiple tests to generate PDFs from existing Pdf files. The outputs are written +// into TMPDIR as files. The files themselves need to be observed to check for correctness as we don't have +// a good way to automatically check if every detail is correct. func init() { common.SetLogger(common.NewConsoleLogger(common.LogLevelDebug)) @@ -52,7 +56,7 @@ func TestAppenderAddPage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -63,13 +67,13 @@ func TestAppenderAddPage(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -94,7 +98,7 @@ func TestAppenderAddPage2(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -105,13 +109,13 @@ func TestAppenderAddPage2(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -135,13 +139,13 @@ func TestAppenderRemovePage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -164,7 +168,7 @@ func TestAppenderReplacePage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -176,13 +180,13 @@ func TestAppenderReplacePage(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -205,21 +209,21 @@ func TestAppenderAddAnnotation(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return } page := pdf1.PageList[0] - annotation := NewPdfAnnotationSquare() - rect := PdfRectangle{Ury: 250.0, Urx: 150.0, Lly: 50.0, Llx: 50.0} + annotation := model.NewPdfAnnotationSquare() + rect := model.PdfRectangle{Ury: 250.0, Urx: 150.0, Lly: 50.0, Llx: 50.0} annotation.Rect = rect.ToPdfObject() annotation.IC = core.MakeArrayFromFloats([]float64{4.0, 0.0, 0.3}) annotation.CA = core.MakeFloat(0.5) @@ -242,7 +246,7 @@ func TestAppenderMergePage(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -254,13 +258,13 @@ func TestAppenderMergePage(t *testing.T) { return } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -287,7 +291,7 @@ func TestAppenderMergePage2(t *testing.T) { } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -300,13 +304,13 @@ func TestAppenderMergePage2(t *testing.T) { } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -334,7 +338,7 @@ func TestAppenderMergePage3(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -347,13 +351,13 @@ func TestAppenderMergePage3(t *testing.T) { } defer f2.Close() - pdf2, err := NewPdfReader(f2) + pdf2, err := model.NewPdfReader(f2) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -378,17 +382,17 @@ func validateFile(t *testing.T, fileName string) { t.Errorf("Fail: %v\n", err) return } - reader, err := NewPdfReader(bytes.NewReader(data)) + reader, err := model.NewPdfReader(bytes.NewReader(data)) if err != nil { t.Errorf("Fail: %v\n", err) return } - handler, _ := NewAdobeX509RSASHA1SignatureHandler(nil, nil) - handler2, _ := NewAdobePKCS7DetachedSignatureHandler(nil, nil) - handlers := []SignatureHandler{handler, handler2} + handler, _ := sighandler.NewAdobeX509RSASHA1(nil, nil) + handler2, _ := sighandler.NewAdobePKCS7Detached(nil, nil) + handlers := []model.SignatureHandler{handler, handler2} - res, err := reader.Validate(handlers) + res, err := reader.ValidateSignatures(handlers) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -402,6 +406,11 @@ func validateFile(t *testing.T, fileName string) { t.Errorf("Fail: validation failed") return } + + for i, item := range res { + t.Logf("== Signature %d", i+1) + t.Logf("%s", item.String()) + } } func TestAppenderSignPage4(t *testing.T) { @@ -414,13 +423,13 @@ func TestAppenderSignPage4(t *testing.T) { return } defer f1.Close() - pdf1, err := NewPdfReader(f1) + pdf1, err := model.NewPdfReader(f1) if err != nil { t.Errorf("Fail: %v\n", err) return } - appender, err := NewPdfAppender(pdf1) + appender, err := model.NewPdfAppender(pdf1) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -433,7 +442,7 @@ func TestAppenderSignPage4(t *testing.T) { return } - handler, err := NewAdobePKCS7DetachedSignatureHandler(privateKey.(*rsa.PrivateKey), cert) + handler, err := sighandler.NewAdobePKCS7Detached(privateKey.(*rsa.PrivateKey), cert) if err != nil { t.Errorf("Fail: %v\n", err) return @@ -444,8 +453,8 @@ func TestAppenderSignPage4(t *testing.T) { return } - appearance.Signature.Reason = core.MakeString("TestAppenderSignPage4") appearance.Signature.Name = core.MakeString("Test Appender") + appearance.Signature.Reason = core.MakeString("TestAppenderSignPage4") err = appender.WriteToFile(tempFile("appender_sign_page_4.pdf")) if err != nil { @@ -454,3 +463,78 @@ func TestAppenderSignPage4(t *testing.T) { } validateFile(t, tempFile("appender_sign_page_4.pdf")) } + +func TestAppenderSignMultiple(t *testing.T) { + inputPath := testPdfFile1 + + for i := 0; i < 3; i++ { + f, err := os.Open(inputPath) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + + pdfReader, err := model.NewPdfReader(f) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + t.Logf("Fields: %d", len(pdfReader.AcroForm.AllFields())) + + if len(pdfReader.AcroForm.AllFields()) != i { + t.Fatalf("fields != %d (got %d)", i, len(pdfReader.AcroForm.AllFields())) + } + + t.Logf("Annotations: %d", len(pdfReader.PageList[0].Annotations)) + if len(pdfReader.PageList[0].Annotations) != i { + t.Fatalf("page annotations != %d (got %d)", i, len(pdfReader.PageList[0].Annotations)) + } + + appender, err := model.NewPdfAppender(pdfReader) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + pfxData, _ := ioutil.ReadFile(testPKS12Key) + privateKey, cert, err := pkcs12.Decode(pfxData, testPKS12KeyPassword) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + handler, err := sighandler.NewAdobePKCS7Detached(privateKey.(*rsa.PrivateKey), cert) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + _, appearance, err := appender.Sign(1, handler) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + appearance.Signature.Name = core.MakeString(fmt.Sprintf("Test Appender - Round %d", i+1)) + appearance.Signature.Reason = core.MakeString("TestAppenderSignPage4") + + outPath := tempFile(fmt.Sprintf("appender_sign_multiple_%d.pdf", i+1)) + + err = appender.WriteToFile(outPath) + if err != nil { + t.Errorf("Fail: %v\n", err) + f.Close() + return + } + + validateFile(t, outPath) + inputPath = outPath + + f.Close() + } +} diff --git a/pdf/model/signature_handler_pkcs7.go b/pdf/model/sighandler/sighandler_pkcs7.go similarity index 66% rename from pdf/model/signature_handler_pkcs7.go rename to pdf/model/sighandler/sighandler_pkcs7.go index affaf803..6f57a811 100644 --- a/pdf/model/signature_handler_pkcs7.go +++ b/pdf/model/sighandler/sighandler_pkcs7.go @@ -3,7 +3,7 @@ * file 'LICENSE.md', which is part of this source code package. */ -package model +package sighandler import ( "bytes" @@ -13,21 +13,23 @@ import ( "github.com/unidoc/unidoc/common/crypto/pkcs7" "github.com/unidoc/unidoc/pdf/core" + "github.com/unidoc/unidoc/pdf/model" ) -type adobePKCS7DetachedSignatureHandler struct { +// Adobe PKCS7 detached signature handler. +type adobePKCS7Detached struct { privateKey *rsa.PrivateKey certificate *x509.Certificate } -// NewAdobePKCS7DetachedSignatureHandler creates a new Adobe.PPKMS/Adobe.PPKLite adbe.pkcs7.detached signature handler. +// NewAdobePKCS7Detached creates a new Adobe.PPKMS/Adobe.PPKLite adbe.pkcs7.detached signature handler. // The both parameters may be nil for the signature validation. -func NewAdobePKCS7DetachedSignatureHandler(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (SignatureHandler, error) { - return &adobePKCS7DetachedSignatureHandler{certificate: certificate, privateKey: privateKey}, nil +func NewAdobePKCS7Detached(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (model.SignatureHandler, error) { + return &adobePKCS7Detached{certificate: certificate, privateKey: privateKey}, nil } // InitSignature initialises the PdfSignature. -func (a *adobePKCS7DetachedSignatureHandler) InitSignature(sig *PdfSignature) error { +func (a *adobePKCS7Detached) InitSignature(sig *model.PdfSignature) error { if a.certificate == nil { return errors.New("certificate must not be nil") } @@ -38,9 +40,7 @@ func (a *adobePKCS7DetachedSignatureHandler) InitSignature(sig *PdfSignature) er handler := *a sig.Handler = &handler sig.Filter = core.MakeName("Adobe.PPKLite") - //sig.Filter = core.MakeName("Adobe.PPKMS") sig.SubFilter = core.MakeName("adbe.pkcs7.detached") - sig.Cert = core.MakeString(string(handler.certificate.Raw)) sig.Reference = nil digest, err := handler.NewDigest(sig) @@ -51,7 +51,7 @@ func (a *adobePKCS7DetachedSignatureHandler) InitSignature(sig *PdfSignature) er return handler.Sign(sig, digest) } -func (a *adobePKCS7DetachedSignatureHandler) getCertificate(sig *PdfSignature) (*x509.Certificate, error) { +func (a *adobePKCS7Detached) getCertificate(sig *model.PdfSignature) (*x509.Certificate, error) { certificate := a.certificate if certificate == nil { certData := sig.Cert.(*core.PdfObjectString).Bytes() @@ -65,31 +65,34 @@ func (a *adobePKCS7DetachedSignatureHandler) getCertificate(sig *PdfSignature) ( } // NewDigest creates a new digest. -func (a *adobePKCS7DetachedSignatureHandler) NewDigest(sig *PdfSignature) (Hasher, error) { +func (a *adobePKCS7Detached) NewDigest(sig *model.PdfSignature) (model.Hasher, error) { return bytes.NewBuffer(nil), nil } // Validate validates PdfSignature. -func (a *adobePKCS7DetachedSignatureHandler) Validate(sig *PdfSignature, digest Hasher) (SignatureValidationResult, error) { +func (a *adobePKCS7Detached) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) { signed := sig.Contents.Bytes() buffer := digest.(*bytes.Buffer) p7, err := pkcs7.Parse(signed) if err != nil { - return SignatureValidationResult{}, err + return model.SignatureValidationResult{}, err } p7.Content = buffer.Bytes() err = p7.Verify() if err != nil { - return SignatureValidationResult{}, err + return model.SignatureValidationResult{}, err } - return SignatureValidationResult{IsSigned: true, IsVerified: true}, nil + result := model.SignatureValidationResult{ + IsSigned: true, + IsVerified: true, + } + return result, nil } // Sign sets the Contents fields. -func (a *adobePKCS7DetachedSignatureHandler) Sign(sig *PdfSignature, digest Hasher) error { - +func (a *adobePKCS7Detached) Sign(sig *model.PdfSignature, digest model.Hasher) error { buffer := digest.(*bytes.Buffer) signedData, err := pkcs7.NewSignedData(buffer.Bytes()) if err != nil { @@ -118,7 +121,7 @@ func (a *adobePKCS7DetachedSignatureHandler) Sign(sig *PdfSignature, digest Hash } // IsApplicable returns true if the signature handler is applicable for the PdfSignature -func (a *adobePKCS7DetachedSignatureHandler) IsApplicable(sig *PdfSignature) bool { +func (a *adobePKCS7Detached) IsApplicable(sig *model.PdfSignature) bool { if sig == nil || sig.Filter == nil || sig.SubFilter == nil { return false } diff --git a/pdf/model/sighandler/sighandler_rsa_sha1.go b/pdf/model/sighandler/sighandler_rsa_sha1.go new file mode 100644 index 00000000..ad7f395a --- /dev/null +++ b/pdf/model/sighandler/sighandler_rsa_sha1.go @@ -0,0 +1,152 @@ +/* + * 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 ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "errors" + "hash" + + "github.com/unidoc/unidoc/pdf/core" + "github.com/unidoc/unidoc/pdf/model" +) + +// Adobe X509 RSA SHA1 signature handler. +type adobeX509RSASHA1 struct { + privateKey *rsa.PrivateKey + certificate *x509.Certificate +} + +// NewAdobeX509RSASHA1 creates a new Adobe.PPKMS/Adobe.PPKLite adbe.x509.rsa_sha1 signature handler. +// The both parameters may be nil for the signature validation. +func NewAdobeX509RSASHA1(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (model.SignatureHandler, error) { + return &adobeX509RSASHA1{certificate: certificate, privateKey: privateKey}, nil +} + +// InitSignature initialises the PdfSignature. +func (a *adobeX509RSASHA1) InitSignature(sig *model.PdfSignature) error { + if a.certificate == nil { + return errors.New("certificate must not be nil") + } + if a.privateKey == nil { + return errors.New("privateKey must not be nil") + } + + handler := *a + sig.Handler = &handler + sig.Filter = core.MakeName("Adobe.PPKLite") + //sig.Filter = core.MakeName("Adobe.PPKMS") + sig.SubFilter = core.MakeName("adbe.x509.rsa_sha1") + sig.Cert = core.MakeString(string(handler.certificate.Raw)) + + sig.Reference = nil + digest, err := handler.NewDigest(sig) + if err != nil { + return err + } + digest.Write([]byte("calculate the Contents field size")) + return handler.Sign(sig, digest) +} + +func getHashFromSignatureAlgorithm(sa x509.SignatureAlgorithm) (crypto.Hash, bool) { + return crypto.SHA1, true + /* + switch sa { + case x509.SHA1WithRSA: + return crypto.SHA1, true + case x509.SHA256WithRSA: + return crypto.SHA256, true + case x509.SHA512WithRSA: + return crypto.SHA512, true + } + return crypto.MD5, false + */ +} + +func (a *adobeX509RSASHA1) getCertificate(sig *model.PdfSignature) (*x509.Certificate, error) { + certificate := a.certificate + if certificate == nil { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return nil, err + } + certificate = certs[0] + } + return certificate, nil +} + +// NewDigest creates a new digest. +func (a *adobeX509RSASHA1) NewDigest(sig *model.PdfSignature) (model.Hasher, error) { + certificate, err := a.getCertificate(sig) + if err != nil { + return nil, err + } + h, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) + return h.New(), nil +} + +// Validate validates PdfSignature. +func (a *adobeX509RSASHA1) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) { + certData := sig.Cert.(*core.PdfObjectString).Bytes() + certs, err := x509.ParseCertificates(certData) + if err != nil { + return model.SignatureValidationResult{}, err + } + if len(certs) == 0 { + return model.SignatureValidationResult{}, errors.New("certificate not found") + } + cert := certs[0] + signed := sig.Contents.Bytes() + var sigHash []byte + if _, err := asn1.Unmarshal(signed, &sigHash); err != nil { + return model.SignatureValidationResult{}, err + } + h, ok := digest.(hash.Hash) + if !ok { + return model.SignatureValidationResult{}, errors.New("hash type error") + } + certificate, err := a.getCertificate(sig) + if err != nil { + return model.SignatureValidationResult{}, err + } + ha, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) + if err := rsa.VerifyPKCS1v15(cert.PublicKey.(*rsa.PublicKey), ha, h.Sum(nil), sigHash); err != nil { + return model.SignatureValidationResult{}, err + } + return model.SignatureValidationResult{IsSigned: true, IsVerified: true}, nil +} + +// Sign sets the Contents fields for the PdfSignature. +func (a *adobeX509RSASHA1) Sign(sig *model.PdfSignature, digest model.Hasher) error { + h, ok := digest.(hash.Hash) + if !ok { + return errors.New("hash type error") + } + ha, _ := getHashFromSignatureAlgorithm(a.certificate.SignatureAlgorithm) + data, err := rsa.SignPKCS1v15(rand.Reader, a.privateKey, ha, h.Sum(nil)) + if err != nil { + return err + } + data, err = asn1.Marshal(data) + if err != nil { + return err + } + sig.Contents = core.MakeHexString(string(data)) + return nil +} + +// IsApplicable returns true if the signature handler is applicable for the PdfSignature +func (a *adobeX509RSASHA1) 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 == "adbe.x509.rsa_sha1" +} diff --git a/pdf/model/signature_handler.go b/pdf/model/signature_handler.go index fdda40d6..4a15b88d 100644 --- a/pdf/model/signature_handler.go +++ b/pdf/model/signature_handler.go @@ -6,13 +6,8 @@ package model import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/asn1" - "errors" - "hash" + "bytes" + "fmt" "io" "github.com/unidoc/unidoc/pdf/core" @@ -36,6 +31,8 @@ type SignatureHandler interface { // SignatureValidationResult defines the response from the signature validation handler. type SignatureValidationResult struct { + // List of errors when validating the signature. + Errors []string IsSigned bool IsVerified bool IsTrusted bool @@ -45,142 +42,57 @@ type SignatureValidationResult struct { Reason string Location string ContactInfo string + + // 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). } -type adobeX509RSASHA1SignatureHandler struct { - privateKey *rsa.PrivateKey - certificate *x509.Certificate +func (v SignatureValidationResult) String() string { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("Name: %s\n", v.Name)) + if v.Date.year > 0 { + buf.WriteString(fmt.Sprintf("Date: %s\n", v.Date.ToGoTime().String())) + } else { + buf.WriteString("Date not specified\n") + } + if len(v.Reason) > 0 { + buf.WriteString(fmt.Sprintf("Reason: %s\n", v.Reason)) + } else { + buf.WriteString("No reason specified\n") + } + if len(v.Location) > 0 { + buf.WriteString(fmt.Sprintf("Location: %s\n", v.Location)) + } else { + buf.WriteString("Location not specified\n") + } + if len(v.ContactInfo) > 0 { + buf.WriteString(fmt.Sprintf("Contact Info: %s\n", v.ContactInfo)) + } else { + buf.WriteString("Contact info not specified\n") + } + buf.WriteString(fmt.Sprintf("Fields: %d\n", len(v.Fields))) + if v.IsSigned { + buf.WriteString("Signed: Document is signed\n") + } else { + buf.WriteString("Signed: Not signed\n") + } + if v.IsVerified { + buf.WriteString("Signature validation: Is valid\n") + } else { + buf.WriteString("Signature validation: Is invalid\n") + } + if v.IsTrusted { + buf.WriteString("Trusted: Certificate is trusted\n") + } else { + buf.WriteString("Trusted: Untrusted certificate\n") + } + + return buf.String() } -// NewAdobeX509RSASHA1SignatureHandler creates a new Adobe.PPKMS/Adobe.PPKLite adbe.x509.rsa_sha1 signature handler. -// The both parameters may be nil for the signature validation. -func NewAdobeX509RSASHA1SignatureHandler(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (SignatureHandler, error) { - return &adobeX509RSASHA1SignatureHandler{certificate: certificate, privateKey: privateKey}, nil -} - -// InitSignature initialises the PdfSignature. -func (a *adobeX509RSASHA1SignatureHandler) InitSignature(sig *PdfSignature) error { - if a.certificate == nil { - return errors.New("certificate must not be nil") - } - if a.privateKey == nil { - return errors.New("privateKey must not be nil") - } - - handler := *a - sig.Handler = &handler - sig.Filter = core.MakeName("Adobe.PPKLite") - //sig.Filter = core.MakeName("Adobe.PPKMS") - sig.SubFilter = core.MakeName("adbe.x509.rsa_sha1") - sig.Cert = core.MakeString(string(handler.certificate.Raw)) - - sig.Reference = nil - digest, err := handler.NewDigest(sig) - if err != nil { - return err - } - digest.Write([]byte("calculate the Contents field size")) - return handler.Sign(sig, digest) -} - -func getHashFromSignatureAlgorithm(sa x509.SignatureAlgorithm) (crypto.Hash, bool) { - return crypto.SHA1, true - /* - switch sa { - case x509.SHA1WithRSA: - return crypto.SHA1, true - case x509.SHA256WithRSA: - return crypto.SHA256, true - case x509.SHA512WithRSA: - return crypto.SHA512, true - } - return crypto.MD5, false - */ -} - -func (a *adobeX509RSASHA1SignatureHandler) getCertificate(sig *PdfSignature) (*x509.Certificate, error) { - certificate := a.certificate - if certificate == nil { - certData := sig.Cert.(*core.PdfObjectString).Bytes() - certs, err := x509.ParseCertificates(certData) - if err != nil { - return nil, err - } - certificate = certs[0] - } - return certificate, nil -} - -// NewDigest creates a new digest. -func (a *adobeX509RSASHA1SignatureHandler) NewDigest(sig *PdfSignature) (Hasher, error) { - certificate, err := a.getCertificate(sig) - if err != nil { - return nil, err - } - h, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) - return h.New(), nil -} - -// Validate validates PdfSignature. -func (a *adobeX509RSASHA1SignatureHandler) Validate(sig *PdfSignature, digest Hasher) (SignatureValidationResult, error) { - certData := sig.Cert.(*core.PdfObjectString).Bytes() - certs, err := x509.ParseCertificates(certData) - if err != nil { - return SignatureValidationResult{}, err - } - if len(certs) == 0 { - return SignatureValidationResult{}, errors.New("certificate not found") - } - cert := certs[0] - signed := sig.Contents.Bytes() - var sigHash []byte - if _, err := asn1.Unmarshal(signed, &sigHash); err != nil { - return SignatureValidationResult{}, err - } - h, ok := digest.(hash.Hash) - if !ok { - return SignatureValidationResult{}, errors.New("hash type error") - } - certificate, err := a.getCertificate(sig) - if err != nil { - return SignatureValidationResult{}, err - } - ha, _ := getHashFromSignatureAlgorithm(certificate.SignatureAlgorithm) - if err := rsa.VerifyPKCS1v15(cert.PublicKey.(*rsa.PublicKey), ha, h.Sum(nil), sigHash); err != nil { - return SignatureValidationResult{}, err - } - return SignatureValidationResult{IsSigned: true, IsVerified: true}, nil -} - -// Sign sets the Contents fields. -func (a *adobeX509RSASHA1SignatureHandler) Sign(sig *PdfSignature, digest Hasher) error { - h, ok := digest.(hash.Hash) - if !ok { - return errors.New("hash type error") - } - ha, _ := getHashFromSignatureAlgorithm(a.certificate.SignatureAlgorithm) - data, err := rsa.SignPKCS1v15(rand.Reader, a.privateKey, ha, h.Sum(nil)) - if err != nil { - return err - } - data, err = asn1.Marshal(data) - if err != nil { - return err - } - sig.Contents = core.MakeHexString(string(data)) - return nil -} - -// IsApplicable returns true if the signature handler is applicable for the PdfSignature -func (a *adobeX509RSASHA1SignatureHandler) IsApplicable(sig *PdfSignature) bool { - if sig == nil || sig.Filter == nil || sig.SubFilter == nil { - return false - } - return (*sig.Filter == "Adobe.PPKMS" || *sig.Filter == "Adobe.PPKLite") && *sig.SubFilter == "adbe.x509.rsa_sha1" -} - -// Validate validates signatures. -func (r *PdfReader) Validate(handlers []SignatureHandler) ([]SignatureValidationResult, error) { +// ValidateSignatures validates digital signatures in the document. +func (r *PdfReader) ValidateSignatures(handlers []SignatureHandler) ([]SignatureValidationResult, error) { if r.AcroForm == nil { return nil, nil } @@ -222,30 +134,28 @@ func (r *PdfReader) Validate(handlers []SignatureHandler) ([]SignatureValidation var results []SignatureValidationResult for _, pair := range pairs { - defaultResult := SignatureValidationResult{IsSigned: true, Fields: []*PdfField{pair.field}} + defaultResult := SignatureValidationResult{ + IsSigned: true, + Fields: []*PdfField{pair.field}, + } if pair.handler == nil { - // TODO think about variants - // to throw an error - // to skip the field and add error message to the result + defaultResult.Errors = append(defaultResult.Errors, "handler not set") results = append(results, defaultResult) continue } digest, err := pair.handler.NewDigest(pair.sig) if err != nil { - // TODO think about variants - // to throw an error - // to skip the field and add error message to the result + defaultResult.Errors = append(defaultResult.Errors, "digest error", err.Error()) results = append(results, defaultResult) continue } byteRange := pair.sig.ByteRange if byteRange == nil { - // TODO think about variants - // to throw an error - // to skip the field and add error message to the result + defaultResult.Errors = append(defaultResult.Errors, "ByteRange not set") results = append(results, defaultResult) continue } + for i := 0; i < byteRange.Len(); i = i + 2 { start, _ := core.GetNumberAsInt64(byteRange.Get(i)) ln, _ := core.GetIntVal(byteRange.Get(i + 1)) @@ -263,6 +173,20 @@ func (r *PdfReader) Validate(handlers []SignatureHandler) ([]SignatureValidation if err != nil { return nil, err } + + result.Name = pair.sig.Name.Decoded() + result.Reason = pair.sig.Reason.Decoded() + if pair.sig.M != nil { + sigDate, err := NewPdfDate(pair.sig.M.String()) + if err != nil { + result.Errors = append(result.Errors, err.Error()) + continue + } + result.Date = sigDate + } + result.ContactInfo = pair.sig.ContactInfo.Decoded() + result.Location = pair.sig.Location.Decoded() + result.Fields = defaultResult.Fields results = append(results, result) } From 7bd43b16be52494ed027aaff5e2f9e467f7b7aaf Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 22:41:42 +0000 Subject: [PATCH 06/20] Add doc.go to sighandlers --- pdf/model/sighandler/doc.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 pdf/model/sighandler/doc.go diff --git a/pdf/model/sighandler/doc.go b/pdf/model/sighandler/doc.go new file mode 100644 index 00000000..8aabfc10 --- /dev/null +++ b/pdf/model/sighandler/doc.go @@ -0,0 +1,7 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +// Package sighandler implements digital signature handlers for PDF signature validation and signing. +package sighandler From 649dbf0af6854708f4e47d0eeebf2c41f681f0d3 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 22:45:10 +0000 Subject: [PATCH 07/20] Removed large whitespace in signature dictionary to get valid signatures. Whitespace was causing ByteRange to not cover the entire document. Ideally should be everything outside the < > in the signature Contents field. --- pdf/model/appender.go | 14 ++++++++++---- pdf/model/signature.go | 31 ++++++++++++++++++------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 5a5d6001..fde9eb59 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -19,7 +19,7 @@ import ( "github.com/unidoc/unidoc/pdf/core" ) -// PdfAppender appends a new Pdf content to an existing Pdf document. +// PdfAppender appends new PDF content to an existing PDF document via incremental updates. type PdfAppender struct { rs io.ReadSeeker parser *core.PdfParser @@ -416,7 +416,6 @@ func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *Pdf appearance.V = sig.ToPdfObject() appearance.FT = core.MakeName("Sig") appearance.V = sig.ToPdfObject() - //appearance.Ff = core.MakeInteger(0) appearance.T = core.MakeString("Signature1") appearance.F = core.MakeInteger(132) appearance.P = page.ToPdfObject() @@ -424,9 +423,14 @@ func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *Pdf core.MakeInteger(0)) appearance.Signature = sig + // Add the signature appearance to the page annotations. + page.Annotations = append(page.Annotations, appearance.PdfAnnotationWidget.PdfAnnotation) + a.pages[pageIndex] = page + // Update acroform. a.ReplaceAcroForm(acroForm) + return acroForm, appearance, nil } @@ -437,7 +441,6 @@ func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { // Write writes the Appender output to io.Writer. func (a *PdfAppender) Write(w io.Writer) error { - writer := NewPdfWriter() pagesDict, ok := core.GetDict(writer.pages) @@ -499,7 +502,7 @@ func (a *PdfAppender) Write(w io.Writer) error { common.Log.Trace("Page Parent: %T", parent) parentDict, ok := parent.PdfObject.(*core.PdfObjectDictionary) if !ok { - return errors.New("Invalid Parent object") + return errors.New("invalid Parent object") } for _, field := range inheritedFields { common.Log.Trace("Field %s", field) @@ -593,10 +596,13 @@ func (a *PdfAppender) Write(w io.Writer) error { writerW = bytes.NewBuffer(nil) } + // Do a mock write to get offsets. + // TODO(gunnsth): Only needed when dealing with signatures? if err := writer.Write(writerW); err != nil { return err } + // TODO(gunnsth): Consider whether the dynamic content can be handled with generic write hooks? if hasSigDict { bufferData := writerW.(*bytes.Buffer).Bytes() byteRange := core.MakeArray() diff --git a/pdf/model/signature.go b/pdf/model/signature.go index cf62748d..d8d3da5d 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -12,7 +12,8 @@ import ( "github.com/unidoc/unidoc/pdf/core" ) -// pdfSignDictionary is needed because of the digital checksum calculates after a new file creation and writes as a value into PdfDictionary in the existing file. +// pdfSignDictionary is used as a wrapper around PdfSignature for digital checksum calculation +// and population of /Contents and /ByteRange. type pdfSignDictionary struct { *core.PdfObjectDictionary handler *SignatureHandler @@ -52,15 +53,15 @@ func (d *pdfSignDictionary) WriteString() string { out.WriteString(" ") d.byteRangeOffsetStart = out.Len() out.WriteString(v.WriteString()) - out.WriteString(" ") - d.byteRangeOffsetEnd = out.Len() + out.WriteString(" ") + d.byteRangeOffsetEnd = out.Len() - 1 case "Contents": out.WriteString(k.WriteString()) out.WriteString(" ") d.contentsOffsetStart = out.Len() out.WriteString(v.WriteString()) - out.WriteString(" ") - d.contentsOffsetEnd = out.Len() + out.WriteString(" ") + d.contentsOffsetEnd = out.Len() - 1 default: out.WriteString(k.WriteString()) out.WriteString(" ") @@ -100,7 +101,11 @@ type PdfSignature struct { // NewPdfSignature creates a new PdfSignature object. func NewPdfSignature() *PdfSignature { sig := &PdfSignature{} - sigDict := &pdfSignDictionary{PdfObjectDictionary: core.MakeDict(), handler: &sig.Handler, signature: sig} + sigDict := &pdfSignDictionary{ + PdfObjectDictionary: core.MakeDict(), + handler: &sig.Handler, + signature: sig, + } sig.container = core.MakeIndirectObject(sigDict) return sig } @@ -172,7 +177,9 @@ func (sig *PdfSignature) ToPdfObject() core.PdfObject { if sig.Contents != nil { dict.Set("Contents", sig.Contents) } - // FIXME: ByteRange and Contents need to be updated dynamically. + // NOTE: ByteRange and Contents need to be updated dynamically. + // TODO: Currently dynamic update is only in the appender, need to support in the PdfWriter + // too for the initial signature on document creation. return container } @@ -251,11 +258,9 @@ func (r *PdfReader) newPdfSignatureFromIndirect(container *core.PdfIndirectObjec type PdfSignatureField struct { container *core.PdfIndirectObject - V *PdfSignature - // Type: /Sig - // V: *PdfSignature... - Lock *core.PdfIndirectObject // Shall be an indirect reference. - SV *core.PdfIndirectObject // Shall be an indirect reference. + V *PdfSignature + Lock *core.PdfIndirectObject + SV *core.PdfIndirectObject Kids *core.PdfObjectArray } @@ -304,7 +309,7 @@ type PdfSignatureFieldLock struct { // (Table 234 - p. 455 in PDF32000_2008). type PdfSignatureFieldSeed struct { container *core.PdfIndirectObject - // Type + Ff *core.PdfObjectInteger Filter *core.PdfObjectName SubFilter *core.PdfObjectArray From 37ea3663211342863bc8be6998e10d709758def3 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 22:46:01 +0000 Subject: [PATCH 08/20] Cleanup and adding comments. Added check to avoid panics on nil pointers. --- pdf/core/primitives.go | 3 +++ pdf/model/appearance.go | 1 + pdf/model/form.go | 3 +++ 3 files changed, 7 insertions(+) diff --git a/pdf/core/primitives.go b/pdf/core/primitives.go index a77334d9..2deb50d3 100644 --- a/pdf/core/primitives.go +++ b/pdf/core/primitives.go @@ -292,6 +292,9 @@ func (str *PdfObjectString) Str() string { // UTF-16BE is applied when the first two bytes are 0xFE, 0XFF, otherwise decoding of // PDFDocEncoding is performed. func (str *PdfObjectString) Decoded() string { + if str == nil { + return "" + } b := []byte(str.val) if len(b) >= 2 && b[0] == 0xFE && b[1] == 0xFF { // UTF16BE. diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go index 05b681a5..7c2411ec 100644 --- a/pdf/model/appearance.go +++ b/pdf/model/appearance.go @@ -11,6 +11,7 @@ import "github.com/unidoc/unidoc/pdf/core" type PdfAppearance struct { *PdfField *PdfAnnotationWidget + // TODO(gunnsth): Signature is not really part of an appearance. Signature *PdfSignature } diff --git a/pdf/model/form.go b/pdf/model/form.go index b965318b..818a43a4 100644 --- a/pdf/model/form.go +++ b/pdf/model/form.go @@ -53,6 +53,9 @@ func flattenFields(field *PdfField) []*PdfField { // AllFields returns a flattened list of all fields in the form. func (form *PdfAcroForm) AllFields() []*PdfField { + if form == nil { + return nil + } var fields []*PdfField if form.Fields != nil { for _, field := range *form.Fields { From cad0c0ec60f915f317309ed84918e2d7c5019f3a Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 22:46:24 +0000 Subject: [PATCH 09/20] Add function to convert PDF time to go time.Time --- pdf/model/structures.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pdf/model/structures.go b/pdf/model/structures.go index 663b33a8..a31f8380 100644 --- a/pdf/model/structures.go +++ b/pdf/model/structures.go @@ -14,6 +14,7 @@ import ( "math" "regexp" "strconv" + "time" . "github.com/unidoc/unidoc/pdf/core" ) @@ -89,6 +90,21 @@ type PdfDate struct { utOffsetMins int64 // mm (00-59) } +// ToGoTime returns the date in time.Time format. +func (d PdfDate) ToGoTime() time.Time { + utcOffset := int(d.utOffsetHours*60*60 + d.utOffsetMins*60) + switch d.utOffsetSign { + case '-': + utcOffset = -utcOffset + case 'Z': + utcOffset = 0 + } + tzName := fmt.Sprintf("UTC%c%.2d%.2d", d.utOffsetSign, d.utOffsetHours, d.utOffsetMins) + tz := time.FixedZone(tzName, utcOffset) + + return time.Date(int(d.year), time.Month(d.month), int(d.day), int(d.hour), int(d.minute), int(d.second), 0, tz) +} + var reDate = regexp.MustCompile(`\s*D\s*:\s*(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([+-Z])?(\d{2})?'?(\d{2})?`) // NewPdfDate returns a new PdfDate object from a PDF date string (see 7.9.4 Dates). From bfdab997fc2cdc43f921b70781dec88dccef3c83 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 23:55:01 +0000 Subject: [PATCH 10/20] Update pkcs7 dependency and use own fork Using updated pkcs7 package updated by mozilla --- common/crypto/pkcs7/.gitignore | 24 - common/crypto/pkcs7/.travis.yml | 7 - common/crypto/pkcs7/LICENSE | 22 - common/crypto/pkcs7/README.md | 8 - common/crypto/pkcs7/ber.go | 248 ------ common/crypto/pkcs7/pkcs7.go | 979 ----------------------- common/crypto/pkcs7/x509.go | 135 ---- pdf/model/sighandler/sighandler_pkcs7.go | 3 +- 8 files changed, 2 insertions(+), 1424 deletions(-) delete mode 100644 common/crypto/pkcs7/.gitignore delete mode 100644 common/crypto/pkcs7/.travis.yml delete mode 100644 common/crypto/pkcs7/LICENSE delete mode 100644 common/crypto/pkcs7/README.md delete mode 100644 common/crypto/pkcs7/ber.go delete mode 100644 common/crypto/pkcs7/pkcs7.go delete mode 100644 common/crypto/pkcs7/x509.go diff --git a/common/crypto/pkcs7/.gitignore b/common/crypto/pkcs7/.gitignore deleted file mode 100644 index daf913b1..00000000 --- a/common/crypto/pkcs7/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test -*.prof diff --git a/common/crypto/pkcs7/.travis.yml b/common/crypto/pkcs7/.travis.yml deleted file mode 100644 index bc120437..00000000 --- a/common/crypto/pkcs7/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: go - -go: - - 1.8 - - 1.9 - - "1.10" - - tip diff --git a/common/crypto/pkcs7/LICENSE b/common/crypto/pkcs7/LICENSE deleted file mode 100644 index 75f32090..00000000 --- a/common/crypto/pkcs7/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Andrew Smith - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/common/crypto/pkcs7/README.md b/common/crypto/pkcs7/README.md deleted file mode 100644 index bfd948f3..00000000 --- a/common/crypto/pkcs7/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# pkcs7 - -[![GoDoc](https://godoc.org/github.com/fullsailor/pkcs7?status.svg)](https://godoc.org/github.com/fullsailor/pkcs7) -[![Build Status](https://travis-ci.org/fullsailor/pkcs7.svg?branch=master)](https://travis-ci.org/fullsailor/pkcs7) - -pkcs7 implements parsing and creating signed and enveloped messages. - -- Documentation on [GoDoc](http://godoc.org/github.com/fullsailor/pkcs7) diff --git a/common/crypto/pkcs7/ber.go b/common/crypto/pkcs7/ber.go deleted file mode 100644 index 1a0d8c02..00000000 --- a/common/crypto/pkcs7/ber.go +++ /dev/null @@ -1,248 +0,0 @@ -package pkcs7 - -import ( - "bytes" - "errors" -) - -var encodeIndent = 0 - -type asn1Object interface { - EncodeTo(writer *bytes.Buffer) error -} - -type asn1Structured struct { - tagBytes []byte - content []asn1Object -} - -func (s asn1Structured) EncodeTo(out *bytes.Buffer) error { - //fmt.Printf("%s--> tag: % X\n", strings.Repeat("| ", encodeIndent), s.tagBytes) - encodeIndent++ - inner := new(bytes.Buffer) - for _, obj := range s.content { - err := obj.EncodeTo(inner) - if err != nil { - return err - } - } - encodeIndent-- - out.Write(s.tagBytes) - encodeLength(out, inner.Len()) - out.Write(inner.Bytes()) - return nil -} - -type asn1Primitive struct { - tagBytes []byte - length int - content []byte -} - -func (p asn1Primitive) EncodeTo(out *bytes.Buffer) error { - _, err := out.Write(p.tagBytes) - if err != nil { - return err - } - if err = encodeLength(out, p.length); err != nil { - return err - } - //fmt.Printf("%s--> tag: % X length: %d\n", strings.Repeat("| ", encodeIndent), p.tagBytes, p.length) - //fmt.Printf("%s--> content length: %d\n", strings.Repeat("| ", encodeIndent), len(p.content)) - out.Write(p.content) - - return nil -} - -func ber2der(ber []byte) ([]byte, error) { - if len(ber) == 0 { - return nil, errors.New("ber2der: input ber is empty") - } - //fmt.Printf("--> ber2der: Transcoding %d bytes\n", len(ber)) - out := new(bytes.Buffer) - - obj, _, err := readObject(ber, 0) - if err != nil { - return nil, err - } - obj.EncodeTo(out) - - // if offset < len(ber) { - // return nil, fmt.Errorf("ber2der: Content longer than expected. Got %d, expected %d", offset, len(ber)) - //} - - return out.Bytes(), nil -} - -// encodes lengths that are longer than 127 into string of bytes -func marshalLongLength(out *bytes.Buffer, i int) (err error) { - n := lengthLength(i) - - for ; n > 0; n-- { - err = out.WriteByte(byte(i >> uint((n-1)*8))) - if err != nil { - return - } - } - - return nil -} - -// computes the byte length of an encoded length value -func lengthLength(i int) (numBytes int) { - numBytes = 1 - for i > 255 { - numBytes++ - i >>= 8 - } - return -} - -// encodes the length in DER format -// If the length fits in 7 bits, the value is encoded directly. -// -// Otherwise, the number of bytes to encode the length is first determined. -// This number is likely to be 4 or less for a 32bit length. This number is -// added to 0x80. The length is encoded in big endian encoding follow after -// -// Examples: -// length | byte 1 | bytes n -// 0 | 0x00 | - -// 120 | 0x78 | - -// 200 | 0x81 | 0xC8 -// 500 | 0x82 | 0x01 0xF4 -// -func encodeLength(out *bytes.Buffer, length int) (err error) { - if length >= 128 { - l := lengthLength(length) - err = out.WriteByte(0x80 | byte(l)) - if err != nil { - return - } - err = marshalLongLength(out, length) - if err != nil { - return - } - } else { - err = out.WriteByte(byte(length)) - if err != nil { - return - } - } - return -} - -func readObject(ber []byte, offset int) (asn1Object, int, error) { - //fmt.Printf("\n====> Starting readObject at offset: %d\n\n", offset) - tagStart := offset - b := ber[offset] - offset++ - tag := b & 0x1F // last 5 bits - if tag == 0x1F { - tag = 0 - for ber[offset] >= 0x80 { - tag = tag*128 + ber[offset] - 0x80 - offset++ - } - tag = tag*128 + ber[offset] - 0x80 - offset++ - } - tagEnd := offset - - kind := b & 0x20 - /* - if kind == 0 { - fmt.Print("--> Primitive\n") - } else { - fmt.Print("--> Constructed\n") - } - */ - // read length - var length int - l := ber[offset] - offset++ - indefinite := false - if l > 0x80 { - numberOfBytes := (int)(l & 0x7F) - if numberOfBytes > 4 { // int is only guaranteed to be 32bit - return nil, 0, errors.New("ber2der: BER tag length too long") - } - if numberOfBytes == 4 && (int)(ber[offset]) > 0x7F { - return nil, 0, errors.New("ber2der: BER tag length is negative") - } - if 0x0 == (int)(ber[offset]) { - return nil, 0, errors.New("ber2der: BER tag length has leading zero") - } - //fmt.Printf("--> (compute length) indicator byte: %x\n", l) - //fmt.Printf("--> (compute length) length bytes: % X\n", ber[offset:offset+numberOfBytes]) - for i := 0; i < numberOfBytes; i++ { - length = length*256 + (int)(ber[offset]) - offset++ - } - } else if l == 0x80 { - indefinite = true - } else { - length = (int)(l) - } - - //fmt.Printf("--> length : %d\n", length) - contentEnd := offset + length - if contentEnd > len(ber) { - return nil, 0, errors.New("ber2der: BER tag length is more than available data") - } - //fmt.Printf("--> content start : %d\n", offset) - //fmt.Printf("--> content end : %d\n", contentEnd) - //fmt.Printf("--> content : % X\n", ber[offset:contentEnd]) - var obj asn1Object - if indefinite && kind == 0 { - return nil, 0, errors.New("ber2der: Indefinite form tag must have constructed encoding") - } - if kind == 0 { - obj = asn1Primitive{ - tagBytes: ber[tagStart:tagEnd], - length: length, - content: ber[offset:contentEnd], - } - } else { - var subObjects []asn1Object - for (offset < contentEnd) || indefinite { - var subObj asn1Object - var err error - subObj, offset, err = readObject(ber, offset) - if err != nil { - return nil, 0, err - } - subObjects = append(subObjects, subObj) - - if indefinite { - terminated, err := isIndefiniteTermination(ber, offset) - if err != nil { - return nil, 0, err - } - - if terminated { - break - } - } - } - obj = asn1Structured{ - tagBytes: ber[tagStart:tagEnd], - content: subObjects, - } - } - - // Apply indefinite form length with 0x0000 terminator. - if indefinite { - contentEnd = offset + 2 - } - - return obj, contentEnd, nil -} - -func isIndefiniteTermination(ber []byte, offset int) (bool, error) { - if len(ber)-offset < 2 { - return false, errors.New("ber2der: Invalid BER format") - } - - return bytes.Index(ber[offset:], []byte{0x0, 0x0}) == 0, nil -} diff --git a/common/crypto/pkcs7/pkcs7.go b/common/crypto/pkcs7/pkcs7.go deleted file mode 100644 index b401c761..00000000 --- a/common/crypto/pkcs7/pkcs7.go +++ /dev/null @@ -1,979 +0,0 @@ -// Package pkcs7 implements parsing and generation of some PKCS#7 structures. -package pkcs7 - -import ( - "bytes" - "crypto" - "crypto/aes" - "crypto/cipher" - "crypto/des" - "crypto/hmac" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/hex" - "errors" - "fmt" - "log" - "math/big" - "sort" - "time" - - _ "crypto/sha1" // for crypto.SHA1 -) - -// PKCS7 Represents a PKCS7 structure -type PKCS7 struct { - Content []byte - Certificates []*x509.Certificate - CRLs []pkix.CertificateList - Signers []signerInfo - raw interface{} -} - -type contentInfo struct { - ContentType asn1.ObjectIdentifier - Content asn1.RawValue `asn1:"explicit,optional,tag:0"` -} - -// ErrUnsupportedContentType is returned when a PKCS7 content is not supported. -// Currently only Data (1.2.840.113549.1.7.1), Signed Data (1.2.840.113549.1.7.2), -// and Enveloped Data are supported (1.2.840.113549.1.7.3) -var ErrUnsupportedContentType = errors.New("pkcs7: cannot parse data: unimplemented content type") - -type unsignedData []byte - -var ( - oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1} - oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2} - oidEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 3} - oidSignedAndEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 4} - oidDigestedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 5} - oidEncryptedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 6} - oidAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} - oidAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} - oidAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5} -) - -type signedData struct { - Version int `asn1:"default:1"` - DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"` - ContentInfo contentInfo - Certificates rawCertificates `asn1:"optional,tag:0"` - CRLs []pkix.CertificateList `asn1:"optional,tag:1"` - SignerInfos []signerInfo `asn1:"set"` -} - -type rawCertificates struct { - Raw asn1.RawContent -} - -type envelopedData struct { - Version int - RecipientInfos []recipientInfo `asn1:"set"` - EncryptedContentInfo encryptedContentInfo -} - -type recipientInfo struct { - Version int - IssuerAndSerialNumber issuerAndSerial - KeyEncryptionAlgorithm pkix.AlgorithmIdentifier - EncryptedKey []byte -} - -type encryptedContentInfo struct { - ContentType asn1.ObjectIdentifier - ContentEncryptionAlgorithm pkix.AlgorithmIdentifier - EncryptedContent asn1.RawValue `asn1:"tag:0,optional,explicit"` -} - -type attribute struct { - Type asn1.ObjectIdentifier - Value asn1.RawValue `asn1:"set"` -} - -type issuerAndSerial struct { - IssuerName asn1.RawValue - SerialNumber *big.Int -} - -// MessageDigestMismatchError is returned when the signer data digest does not -// match the computed digest for the contained content -type MessageDigestMismatchError struct { - ExpectedDigest []byte - ActualDigest []byte -} - -func (err *MessageDigestMismatchError) Error() string { - return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest) -} - -type signerInfo struct { - Version int `asn1:"default:1"` - IssuerAndSerialNumber issuerAndSerial - DigestAlgorithm pkix.AlgorithmIdentifier - AuthenticatedAttributes []attribute `asn1:"optional,tag:0"` - DigestEncryptionAlgorithm pkix.AlgorithmIdentifier - EncryptedDigest []byte - UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"` -} - -// Parse decodes a DER encoded PKCS7 package -func Parse(data []byte) (p7 *PKCS7, err error) { - if len(data) == 0 { - return nil, errors.New("pkcs7: input data is empty") - } - var info contentInfo - der, err := ber2der(data) - if err != nil { - return nil, err - } - rest, err := asn1.Unmarshal(der, &info) - if len(rest) > 0 { - err = asn1.SyntaxError{Msg: "trailing data"} - return - } - if err != nil { - return - } - - // For debugging: - // fmt.Printf("--> Content Type: %s", info.ContentType) - switch { - case info.ContentType.Equal(oidSignedData): - return parseSignedData(info.Content.Bytes) - case info.ContentType.Equal(oidEnvelopedData): - return parseEnvelopedData(info.Content.Bytes) - } - return nil, ErrUnsupportedContentType -} - -func parseSignedData(data []byte) (*PKCS7, error) { - var sd signedData - asn1.Unmarshal(data, &sd) - certs, err := sd.Certificates.Parse() - if err != nil { - return nil, err - } - // For debugging: - // fmt.Printf("--> Signed Data Version %d\n", sd.Version) - - var compound asn1.RawValue - var content unsignedData - - // The Content.Bytes maybe empty on PKI responses. - if len(sd.ContentInfo.Content.Bytes) > 0 { - if _, err := asn1.Unmarshal(sd.ContentInfo.Content.Bytes, &compound); err != nil { - return nil, err - } - } - // Compound octet string - if compound.IsCompound { - if _, err = asn1.Unmarshal(compound.Bytes, &content); err != nil { - return nil, err - } - } else { - // assuming this is tag 04 - content = compound.Bytes - } - return &PKCS7{ - Content: content, - Certificates: certs, - CRLs: sd.CRLs, - Signers: sd.SignerInfos, - raw: sd}, nil -} - -func (raw rawCertificates) Parse() ([]*x509.Certificate, error) { - if len(raw.Raw) == 0 { - return nil, nil - } - - var val asn1.RawValue - if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil { - return nil, err - } - - return x509.ParseCertificates(val.Bytes) -} - -func parseEnvelopedData(data []byte) (*PKCS7, error) { - var ed envelopedData - if _, err := asn1.Unmarshal(data, &ed); err != nil { - return nil, err - } - return &PKCS7{ - raw: ed, - }, nil -} - -// Verify checks the signatures of a PKCS7 object -// WARNING: Verify does not check signing time or verify certificate chains at -// this time. -func (p7 *PKCS7) Verify() (err error) { - if len(p7.Signers) == 0 { - return errors.New("pkcs7: Message has no signers") - } - for _, signer := range p7.Signers { - if err := verifySignature(p7, signer); err != nil { - return err - } - } - return nil -} - -func verifySignature(p7 *PKCS7, signer signerInfo) error { - signedData := p7.Content - hash, err := getHashForOID(signer.DigestAlgorithm.Algorithm) - if err != nil { - return err - } - // For debugging: - //fmt.Printf("Hash type: %T %#v\n", hash, hash) - if len(signer.AuthenticatedAttributes) > 0 { - // TODO(fullsailor): First check the content type match - var digest []byte - err := unmarshalAttribute(signer.AuthenticatedAttributes, oidAttributeMessageDigest, &digest) - if err != nil { - return err - } - h := hash.New() - // For debugging: - //fmt.Printf("Hash: %T %#v\n", h, h) - //fmt.Printf("Expected digest: % X\n", digest) - //fmt.Printf("Content (%d): %X\n", len(p7.Content), p7.Content) - - h.Write(p7.Content) - computed := h.Sum(nil) - log.Println("Computed", hex.EncodeToString(computed)) - if !hmac.Equal(digest, computed) { - return &MessageDigestMismatchError{ - ExpectedDigest: digest, - ActualDigest: computed, - } - } - - // TODO(fullsailor): Optionally verify certificate chain - // TODO(fullsailor): Optionally verify signingTime against certificate NotAfter/NotBefore - signedData, err = marshalAttributes(signer.AuthenticatedAttributes) - if err != nil { - return err - } - } - cert := getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) - if cert == nil { - return errors.New("pkcs7: No certificate for signer") - } - - algo := getSignatureAlgorithmFromAI(signer.DigestEncryptionAlgorithm) - if algo == x509.UnknownSignatureAlgorithm { - // I'm not sure what the spec here is, and the openssl sources were not - // helpful. But, this is what App Store receipts appear to do. - // The DigestEncryptionAlgorithm is just "rsaEncryption (PKCS #1)" - // But we're expecting a digest + encryption algorithm. So... we're going - // to determine an algorithm based on the DigestAlgorithm and this - // encryption algorithm. - if signer.DigestEncryptionAlgorithm.Algorithm.Equal(oidEncryptionAlgorithmRSA) { - algo = getRSASignatureAlgorithmForDigestAlgorithm(hash) - } - } - return cert.CheckSignature(algo, signedData, signer.EncryptedDigest) -} - -func marshalAttributes(attrs []attribute) ([]byte, error) { - encodedAttributes, err := asn1.Marshal(struct { - A []attribute `asn1:"set"` - }{A: attrs}) - if err != nil { - return nil, err - } - - // Remove the leading sequence octets - var raw asn1.RawValue - asn1.Unmarshal(encodedAttributes, &raw) - return raw.Bytes, nil -} - -var ( - oidDigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} - oidDigestAlgorithmSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} - oidEncryptionAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} -) - -func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndSerial) *x509.Certificate { - for _, cert := range certs { - if isCertMatchForIssuerAndSerial(cert, ias) { - return cert - } - } - return nil -} - -func getHashForOID(oid asn1.ObjectIdentifier) (crypto.Hash, error) { - switch { - case oid.Equal(oidDigestAlgorithmSHA1): - return crypto.SHA1, nil - case oid.Equal(oidDigestAlgorithmSHA256): - return crypto.SHA256, nil - } - - // For debugging: - // fmt.Printf("Unsupported algo: %s\n", oid.String()) - return crypto.Hash(0), ErrUnsupportedAlgorithm -} - -func getRSASignatureAlgorithmForDigestAlgorithm(hash crypto.Hash) x509.SignatureAlgorithm { - for _, details := range signatureAlgorithmDetails { - if details.pubKeyAlgo == x509.RSA && details.hash == hash { - return details.algo - } - } - return x509.UnknownSignatureAlgorithm -} - -// GetOnlySigner returns an x509.Certificate for the first signer of the signed -// data payload. If there are more or less than one signer, nil is returned -func (p7 *PKCS7) GetOnlySigner() *x509.Certificate { - if len(p7.Signers) != 1 { - return nil - } - signer := p7.Signers[0] - return getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber) -} - -// ErrUnsupportedAlgorithm tells you when our quick dev assumptions have failed -var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3, AES-256-CBC and AES-128-GCM supported") - -// ErrNotEncryptedContent is returned when attempting to Decrypt data that is not encrypted data -var ErrNotEncryptedContent = errors.New("pkcs7: content data is a decryptable data type") - -// Decrypt decrypts encrypted content info for recipient cert and private key -func (p7 *PKCS7) Decrypt(cert *x509.Certificate, pk crypto.PrivateKey) ([]byte, error) { - data, ok := p7.raw.(envelopedData) - if !ok { - return nil, ErrNotEncryptedContent - } - recipient := selectRecipientForCertificate(data.RecipientInfos, cert) - if recipient.EncryptedKey == nil { - return nil, errors.New("pkcs7: no enveloped recipient for provided certificate") - } - if priv := pk.(*rsa.PrivateKey); priv != nil { - var contentKey []byte - contentKey, err := rsa.DecryptPKCS1v15(rand.Reader, priv, recipient.EncryptedKey) - if err != nil { - return nil, err - } - return data.EncryptedContentInfo.decrypt(contentKey) - } - fmt.Printf("Unsupported Private Key: %v\n", pk) - return nil, ErrUnsupportedAlgorithm -} - -var oidEncryptionAlgorithmDESCBC = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 7} -var oidEncryptionAlgorithmDESEDE3CBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 3, 7} -var oidEncryptionAlgorithmAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42} -var oidEncryptionAlgorithmAES128GCM = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 6} -var oidEncryptionAlgorithmAES128CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 2} - -func (eci encryptedContentInfo) decrypt(key []byte) ([]byte, error) { - alg := eci.ContentEncryptionAlgorithm.Algorithm - if !alg.Equal(oidEncryptionAlgorithmDESCBC) && - !alg.Equal(oidEncryptionAlgorithmDESEDE3CBC) && - !alg.Equal(oidEncryptionAlgorithmAES256CBC) && - !alg.Equal(oidEncryptionAlgorithmAES128CBC) && - !alg.Equal(oidEncryptionAlgorithmAES128GCM) { - fmt.Printf("Unsupported Content Encryption Algorithm: %s\n", alg) - return nil, ErrUnsupportedAlgorithm - } - - // EncryptedContent can either be constructed of multple OCTET STRINGs - // or _be_ a tagged OCTET STRING - var cyphertext []byte - if eci.EncryptedContent.IsCompound { - // Complex case to concat all of the children OCTET STRINGs - var buf bytes.Buffer - cypherbytes := eci.EncryptedContent.Bytes - for { - var part []byte - cypherbytes, _ = asn1.Unmarshal(cypherbytes, &part) - buf.Write(part) - if cypherbytes == nil { - break - } - } - cyphertext = buf.Bytes() - } else { - // Simple case, the bytes _are_ the cyphertext - cyphertext = eci.EncryptedContent.Bytes - } - - var block cipher.Block - var err error - - switch { - case alg.Equal(oidEncryptionAlgorithmDESCBC): - block, err = des.NewCipher(key) - case alg.Equal(oidEncryptionAlgorithmDESEDE3CBC): - block, err = des.NewTripleDESCipher(key) - case alg.Equal(oidEncryptionAlgorithmAES256CBC): - fallthrough - case alg.Equal(oidEncryptionAlgorithmAES128GCM), alg.Equal(oidEncryptionAlgorithmAES128CBC): - block, err = aes.NewCipher(key) - } - - if err != nil { - return nil, err - } - - if alg.Equal(oidEncryptionAlgorithmAES128GCM) { - params := aesGCMParameters{} - paramBytes := eci.ContentEncryptionAlgorithm.Parameters.Bytes - - _, err := asn1.Unmarshal(paramBytes, ¶ms) - if err != nil { - return nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, err - } - - if len(params.Nonce) != gcm.NonceSize() { - return nil, errors.New("pkcs7: encryption algorithm parameters are incorrect") - } - if params.ICVLen != gcm.Overhead() { - return nil, errors.New("pkcs7: encryption algorithm parameters are incorrect") - } - - plaintext, err := gcm.Open(nil, params.Nonce, cyphertext, nil) - if err != nil { - return nil, err - } - - return plaintext, nil - } - - iv := eci.ContentEncryptionAlgorithm.Parameters.Bytes - if len(iv) != block.BlockSize() { - return nil, errors.New("pkcs7: encryption algorithm parameters are malformed") - } - mode := cipher.NewCBCDecrypter(block, iv) - plaintext := make([]byte, len(cyphertext)) - mode.CryptBlocks(plaintext, cyphertext) - if plaintext, err = unpad(plaintext, mode.BlockSize()); err != nil { - return nil, err - } - return plaintext, nil -} - -func selectRecipientForCertificate(recipients []recipientInfo, cert *x509.Certificate) recipientInfo { - for _, recp := range recipients { - if isCertMatchForIssuerAndSerial(cert, recp.IssuerAndSerialNumber) { - return recp - } - } - return recipientInfo{} -} - -func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool { - return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Compare(cert.RawIssuer, ias.IssuerName.FullBytes) == 0 -} - -func pad(data []byte, blocklen int) ([]byte, error) { - if blocklen < 1 { - return nil, fmt.Errorf("invalid blocklen %d", blocklen) - } - padlen := blocklen - (len(data) % blocklen) - if padlen == 0 { - padlen = blocklen - } - pad := bytes.Repeat([]byte{byte(padlen)}, padlen) - return append(data, pad...), nil -} - -func unpad(data []byte, blocklen int) ([]byte, error) { - if blocklen < 1 { - return nil, fmt.Errorf("invalid blocklen %d", blocklen) - } - if len(data)%blocklen != 0 || len(data) == 0 { - return nil, fmt.Errorf("invalid data len %d", len(data)) - } - - // the last byte is the length of padding - padlen := int(data[len(data)-1]) - - // check padding integrity, all bytes should be the same - pad := data[len(data)-padlen:] - for _, padbyte := range pad { - if padbyte != byte(padlen) { - return nil, errors.New("invalid padding") - } - } - - return data[:len(data)-padlen], nil -} - -func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error { - for _, attr := range attrs { - if attr.Type.Equal(attributeType) { - _, err := asn1.Unmarshal(attr.Value.Bytes, out) - return err - } - } - return errors.New("pkcs7: attribute type not in attributes") -} - -// UnmarshalSignedAttribute decodes a single attribute from the signer info -func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, out interface{}) error { - sd, ok := p7.raw.(signedData) - if !ok { - return errors.New("pkcs7: payload is not signedData content") - } - if len(sd.SignerInfos) < 1 { - return errors.New("pkcs7: payload has no signers") - } - attributes := sd.SignerInfos[0].AuthenticatedAttributes - return unmarshalAttribute(attributes, attributeType, out) -} - -// SignedData is an opaque data structure for creating signed data payloads -type SignedData struct { - sd signedData - certs []*x509.Certificate - messageDigest []byte -} - -// Attribute represents a key value pair attribute. Value must be marshalable byte -// `encoding/asn1` -type Attribute struct { - Type asn1.ObjectIdentifier - Value interface{} -} - -// SignerInfoConfig are optional values to include when adding a signer -type SignerInfoConfig struct { - ExtraSignedAttributes []Attribute -} - -// NewSignedData initializes a SignedData with content -func NewSignedData(data []byte) (*SignedData, error) { - content, err := asn1.Marshal(data) - if err != nil { - return nil, err - } - ci := contentInfo{ - ContentType: oidData, - Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, - } - digAlg := pkix.AlgorithmIdentifier{ - Algorithm: oidDigestAlgorithmSHA1, - } - h := crypto.SHA1.New() - h.Write(data) - md := h.Sum(nil) - sd := signedData{ - ContentInfo: ci, - Version: 1, - DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{digAlg}, - } - return &SignedData{sd: sd, messageDigest: md}, nil -} - -type attributes struct { - types []asn1.ObjectIdentifier - values []interface{} -} - -// Add adds the attribute, maintaining insertion order -func (attrs *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) { - attrs.types = append(attrs.types, attrType) - attrs.values = append(attrs.values, value) -} - -type sortableAttribute struct { - SortKey []byte - Attribute attribute -} - -type attributeSet []sortableAttribute - -func (sa attributeSet) Len() int { - return len(sa) -} - -func (sa attributeSet) Less(i, j int) bool { - return bytes.Compare(sa[i].SortKey, sa[j].SortKey) < 0 -} - -func (sa attributeSet) Swap(i, j int) { - sa[i], sa[j] = sa[j], sa[i] -} - -func (sa attributeSet) Attributes() []attribute { - attrs := make([]attribute, len(sa)) - for i, attr := range sa { - attrs[i] = attr.Attribute - } - return attrs -} - -func (attrs *attributes) ForMarshaling() ([]attribute, error) { - sortables := make(attributeSet, len(attrs.types)) - for i := range sortables { - attrType := attrs.types[i] - attrValue := attrs.values[i] - asn1Value, err := asn1.Marshal(attrValue) - if err != nil { - return nil, err - } - attr := attribute{ - Type: attrType, - Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag - } - encoded, err := asn1.Marshal(attr) - if err != nil { - return nil, err - } - sortables[i] = sortableAttribute{ - SortKey: encoded, - Attribute: attr, - } - } - sort.Sort(sortables) - return sortables.Attributes(), nil -} - -// AddSigner signs attributes about the content and adds certificate to payload -func (sd *SignedData) AddSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error { - attrs := &attributes{} - attrs.Add(oidAttributeContentType, sd.sd.ContentInfo.ContentType) - attrs.Add(oidAttributeMessageDigest, sd.messageDigest) - attrs.Add(oidAttributeSigningTime, time.Now()) - for _, attr := range config.ExtraSignedAttributes { - attrs.Add(attr.Type, attr.Value) - } - finalAttrs, err := attrs.ForMarshaling() - if err != nil { - return err - } - signature, err := signAttributes(finalAttrs, pkey, crypto.SHA1) - if err != nil { - return err - } - - ias, err := cert2issuerAndSerial(cert) - if err != nil { - return err - } - - signer := signerInfo{ - AuthenticatedAttributes: finalAttrs, - DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidDigestAlgorithmSHA1}, - DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidSignatureSHA1WithRSA}, - IssuerAndSerialNumber: ias, - EncryptedDigest: signature, - Version: 1, - } - // create signature of signed attributes - sd.certs = append(sd.certs, cert) - sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer) - return nil -} - -// AddCertificate adds the certificate to the payload. Useful for parent certificates -func (sd *SignedData) AddCertificate(cert *x509.Certificate) { - sd.certs = append(sd.certs, cert) -} - -// Detach removes content from the signed data struct to make it a detached signature. -// This must be called right before Finish() -func (sd *SignedData) Detach() { - sd.sd.ContentInfo = contentInfo{ContentType: oidSignedData} -} - -// Finish marshals the content and its signers -func (sd *SignedData) Finish() ([]byte, error) { - sd.sd.Certificates = marshalCertificates(sd.certs) - inner, err := asn1.Marshal(sd.sd) - if err != nil { - return nil, err - } - outer := contentInfo{ - ContentType: oidSignedData, - Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true}, - } - return asn1.Marshal(outer) -} - -func cert2issuerAndSerial(cert *x509.Certificate) (issuerAndSerial, error) { - var ias issuerAndSerial - // The issuer RDNSequence has to match exactly the sequence in the certificate - // We cannot use cert.Issuer.ToRDNSequence() here since it mangles the sequence - ias.IssuerName = asn1.RawValue{FullBytes: cert.RawIssuer} - ias.SerialNumber = cert.SerialNumber - - return ias, nil -} - -// signs the DER encoded form of the attributes with the private key -func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) { - attrBytes, err := marshalAttributes(attrs) - if err != nil { - return nil, err - } - h := hash.New() - h.Write(attrBytes) - hashed := h.Sum(nil) - switch priv := pkey.(type) { - case *rsa.PrivateKey: - return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed) - } - return nil, ErrUnsupportedAlgorithm -} - -// concats and wraps the certificates in the RawValue structure -func marshalCertificates(certs []*x509.Certificate) rawCertificates { - var buf bytes.Buffer - for _, cert := range certs { - buf.Write(cert.Raw) - } - rawCerts, _ := marshalCertificateBytes(buf.Bytes()) - return rawCerts -} - -// Even though, the tag & length are stripped out during marshalling the -// RawContent, we have to encode it into the RawContent. If its missing, -// then `asn1.Marshal()` will strip out the certificate wrapper instead. -func marshalCertificateBytes(certs []byte) (rawCertificates, error) { - var val = asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true} - b, err := asn1.Marshal(val) - if err != nil { - return rawCertificates{}, err - } - return rawCertificates{Raw: b}, nil -} - -// DegenerateCertificate creates a signed data structure containing only the -// provided certificate or certificate chain. -func DegenerateCertificate(cert []byte) ([]byte, error) { - rawCert, err := marshalCertificateBytes(cert) - if err != nil { - return nil, err - } - emptyContent := contentInfo{ContentType: oidData} - sd := signedData{ - Version: 1, - ContentInfo: emptyContent, - Certificates: rawCert, - CRLs: []pkix.CertificateList{}, - } - content, err := asn1.Marshal(sd) - if err != nil { - return nil, err - } - signedContent := contentInfo{ - ContentType: oidSignedData, - Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true}, - } - return asn1.Marshal(signedContent) -} - -const ( - EncryptionAlgorithmDESCBC = iota - EncryptionAlgorithmAES128GCM -) - -// ContentEncryptionAlgorithm determines the algorithm used to encrypt the -// plaintext message. Change the value of this variable to change which -// algorithm is used in the Encrypt() function. -var ContentEncryptionAlgorithm = EncryptionAlgorithmDESCBC - -// ErrUnsupportedEncryptionAlgorithm is returned when attempting to encrypt -// content with an unsupported algorithm. -var ErrUnsupportedEncryptionAlgorithm = errors.New("pkcs7: cannot encrypt content: only DES-CBC and AES-128-GCM supported") - -const nonceSize = 12 - -type aesGCMParameters struct { - Nonce []byte `asn1:"tag:4"` - ICVLen int -} - -func encryptAES128GCM(content []byte) ([]byte, *encryptedContentInfo, error) { - // Create AES key and nonce - key := make([]byte, 16) - nonce := make([]byte, nonceSize) - - _, err := rand.Read(key) - if err != nil { - return nil, nil, err - } - - _, err = rand.Read(nonce) - if err != nil { - return nil, nil, err - } - - // Encrypt content - block, err := aes.NewCipher(key) - if err != nil { - return nil, nil, err - } - - gcm, err := cipher.NewGCM(block) - if err != nil { - return nil, nil, err - } - - ciphertext := gcm.Seal(nil, nonce, content, nil) - - // Prepare ASN.1 Encrypted Content Info - paramSeq := aesGCMParameters{ - Nonce: nonce, - ICVLen: gcm.Overhead(), - } - - paramBytes, err := asn1.Marshal(paramSeq) - if err != nil { - return nil, nil, err - } - - eci := encryptedContentInfo{ - ContentType: oidData, - ContentEncryptionAlgorithm: pkix.AlgorithmIdentifier{ - Algorithm: oidEncryptionAlgorithmAES128GCM, - Parameters: asn1.RawValue{ - Tag: asn1.TagSequence, - Bytes: paramBytes, - }, - }, - EncryptedContent: marshalEncryptedContent(ciphertext), - } - - return key, &eci, nil -} - -func encryptDESCBC(content []byte) ([]byte, *encryptedContentInfo, error) { - // Create DES key & CBC IV - key := make([]byte, 8) - iv := make([]byte, des.BlockSize) - _, err := rand.Read(key) - if err != nil { - return nil, nil, err - } - _, err = rand.Read(iv) - if err != nil { - return nil, nil, err - } - - // Encrypt padded content - block, err := des.NewCipher(key) - if err != nil { - return nil, nil, err - } - mode := cipher.NewCBCEncrypter(block, iv) - plaintext, err := pad(content, mode.BlockSize()) - cyphertext := make([]byte, len(plaintext)) - mode.CryptBlocks(cyphertext, plaintext) - - // Prepare ASN.1 Encrypted Content Info - eci := encryptedContentInfo{ - ContentType: oidData, - ContentEncryptionAlgorithm: pkix.AlgorithmIdentifier{ - Algorithm: oidEncryptionAlgorithmDESCBC, - Parameters: asn1.RawValue{Tag: 4, Bytes: iv}, - }, - EncryptedContent: marshalEncryptedContent(cyphertext), - } - - return key, &eci, nil -} - -// Encrypt creates and returns an envelope data PKCS7 structure with encrypted -// recipient keys for each recipient public key. -// -// The algorithm used to perform encryption is determined by the current value -// of the global ContentEncryptionAlgorithm package variable. By default, the -// value is EncryptionAlgorithmDESCBC. To use a different algorithm, change the -// value before calling Encrypt(). For example: -// -// ContentEncryptionAlgorithm = EncryptionAlgorithmAES128GCM -// -// TODO(fullsailor): Add support for encrypting content with other algorithms -func Encrypt(content []byte, recipients []*x509.Certificate) ([]byte, error) { - var eci *encryptedContentInfo - var key []byte - var err error - - // Apply chosen symmetric encryption method - switch ContentEncryptionAlgorithm { - case EncryptionAlgorithmDESCBC: - key, eci, err = encryptDESCBC(content) - - case EncryptionAlgorithmAES128GCM: - key, eci, err = encryptAES128GCM(content) - - default: - return nil, ErrUnsupportedEncryptionAlgorithm - } - - if err != nil { - return nil, err - } - - // Prepare each recipient's encrypted cipher key - recipientInfos := make([]recipientInfo, len(recipients)) - for i, recipient := range recipients { - encrypted, err := encryptKey(key, recipient) - if err != nil { - return nil, err - } - ias, err := cert2issuerAndSerial(recipient) - if err != nil { - return nil, err - } - info := recipientInfo{ - Version: 0, - IssuerAndSerialNumber: ias, - KeyEncryptionAlgorithm: pkix.AlgorithmIdentifier{ - Algorithm: oidEncryptionAlgorithmRSA, - }, - EncryptedKey: encrypted, - } - recipientInfos[i] = info - } - - // Prepare envelope content - envelope := envelopedData{ - EncryptedContentInfo: *eci, - Version: 0, - RecipientInfos: recipientInfos, - } - innerContent, err := asn1.Marshal(envelope) - if err != nil { - return nil, err - } - - // Prepare outer payload structure - wrapper := contentInfo{ - ContentType: oidEnvelopedData, - Content: asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: innerContent}, - } - - return asn1.Marshal(wrapper) -} - -func marshalEncryptedContent(content []byte) asn1.RawValue { - asn1Content, _ := asn1.Marshal(content) - return asn1.RawValue{Tag: 0, Class: 2, Bytes: asn1Content, IsCompound: true} -} - -func encryptKey(key []byte, recipient *x509.Certificate) ([]byte, error) { - if pub := recipient.PublicKey.(*rsa.PublicKey); pub != nil { - return rsa.EncryptPKCS1v15(rand.Reader, pub, key) - } - return nil, ErrUnsupportedAlgorithm -} diff --git a/common/crypto/pkcs7/x509.go b/common/crypto/pkcs7/x509.go deleted file mode 100644 index 5fe4389b..00000000 --- a/common/crypto/pkcs7/x509.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the go/golang LICENSE file. - -package pkcs7 - -// These are private constants and functions from the crypto/x509 package that -// are useful when dealing with signatures verified by x509 certificates - -import ( - "bytes" - "crypto" - "encoding/asn1" - - "crypto/x509" - "crypto/x509/pkix" -) - -var ( - oidSignatureRSAEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} - oidSignatureMD2WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 2} - oidSignatureMD5WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4} - oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5} - oidSignatureSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} - oidSignatureSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12} - oidSignatureSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13} - oidSignatureRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} - oidSignatureDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3} - oidSignatureDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2} - oidSignatureECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1} - oidSignatureECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} - oidSignatureECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3} - oidSignatureECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4} - - oidSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} - oidSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} - oidSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} - - oidMGF1 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 8} - - // oidISOSignatureSHA1WithRSA means the same as oidSignatureSHA1WithRSA - // but it's specified by ISO. Microsoft's makecert.exe has been known - // to produce certificates with this OID. - oidISOSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 29} -) - -var signatureAlgorithmDetails = []struct { - algo x509.SignatureAlgorithm - name string - oid asn1.ObjectIdentifier - pubKeyAlgo x509.PublicKeyAlgorithm - hash crypto.Hash -}{ - {x509.MD2WithRSA, "MD2-RSA", oidSignatureMD2WithRSA, x509.RSA, crypto.Hash(0) /* no value for MD2 */}, - {x509.MD5WithRSA, "MD5-RSA", oidSignatureMD5WithRSA, x509.RSA, crypto.MD5}, - {x509.SHA1WithRSA, "SHA1-RSA", oidSignatureSHA1WithRSA, x509.RSA, crypto.SHA1}, - {x509.SHA1WithRSA, "SHA1-RSA", oidISOSignatureSHA1WithRSA, x509.RSA, crypto.SHA1}, - {x509.SHA256WithRSA, "SHA256-RSA", oidSignatureSHA256WithRSA, x509.RSA, crypto.SHA256}, - {x509.SHA384WithRSA, "SHA384-RSA", oidSignatureSHA384WithRSA, x509.RSA, crypto.SHA384}, - {x509.SHA512WithRSA, "SHA512-RSA", oidSignatureSHA512WithRSA, x509.RSA, crypto.SHA512}, - {x509.SHA256WithRSAPSS, "SHA256-RSAPSS", oidSignatureRSAPSS, x509.RSA, crypto.SHA256}, - {x509.SHA384WithRSAPSS, "SHA384-RSAPSS", oidSignatureRSAPSS, x509.RSA, crypto.SHA384}, - {x509.SHA512WithRSAPSS, "SHA512-RSAPSS", oidSignatureRSAPSS, x509.RSA, crypto.SHA512}, - {x509.DSAWithSHA1, "DSA-SHA1", oidSignatureDSAWithSHA1, x509.DSA, crypto.SHA1}, - {x509.DSAWithSHA256, "DSA-SHA256", oidSignatureDSAWithSHA256, x509.DSA, crypto.SHA256}, - {x509.ECDSAWithSHA1, "ECDSA-SHA1", oidSignatureECDSAWithSHA1, x509.ECDSA, crypto.SHA1}, - {x509.ECDSAWithSHA256, "ECDSA-SHA256", oidSignatureECDSAWithSHA256, x509.ECDSA, crypto.SHA256}, - {x509.ECDSAWithSHA384, "ECDSA-SHA384", oidSignatureECDSAWithSHA384, x509.ECDSA, crypto.SHA384}, - {x509.ECDSAWithSHA512, "ECDSA-SHA512", oidSignatureECDSAWithSHA512, x509.ECDSA, crypto.SHA512}, -} - -// pssParameters reflects the parameters in an AlgorithmIdentifier that -// specifies RSA PSS. See https://tools.ietf.org/html/rfc3447#appendix-A.2.3 -type pssParameters struct { - // The following three fields are not marked as - // optional because the default values specify SHA-1, - // which is no longer suitable for use in signatures. - Hash pkix.AlgorithmIdentifier `asn1:"explicit,tag:0"` - MGF pkix.AlgorithmIdentifier `asn1:"explicit,tag:1"` - SaltLength int `asn1:"explicit,tag:2"` - TrailerField int `asn1:"optional,explicit,tag:3,default:1"` -} - -// asn1.NullBytes is not available prior to Go 1.9 -var nullBytes = []byte{5, 0} - -func getSignatureAlgorithmFromAI(ai pkix.AlgorithmIdentifier) x509.SignatureAlgorithm { - if !ai.Algorithm.Equal(oidSignatureRSAPSS) { - for _, details := range signatureAlgorithmDetails { - if ai.Algorithm.Equal(details.oid) { - return details.algo - } - } - return x509.UnknownSignatureAlgorithm - } - - // RSA PSS is special because it encodes important parameters - // in the Parameters. - - var params pssParameters - if _, err := asn1.Unmarshal(ai.Parameters.FullBytes, ¶ms); err != nil { - return x509.UnknownSignatureAlgorithm - } - - var mgf1HashFunc pkix.AlgorithmIdentifier - if _, err := asn1.Unmarshal(params.MGF.Parameters.FullBytes, &mgf1HashFunc); err != nil { - return x509.UnknownSignatureAlgorithm - } - - // PSS is greatly overburdened with options. This code forces - // them into three buckets by requiring that the MGF1 hash - // function always match the message hash function (as - // recommended in - // https://tools.ietf.org/html/rfc3447#section-8.1), that the - // salt length matches the hash length, and that the trailer - // field has the default value. - if !bytes.Equal(params.Hash.Parameters.FullBytes, nullBytes) || - !params.MGF.Algorithm.Equal(oidMGF1) || - !mgf1HashFunc.Algorithm.Equal(params.Hash.Algorithm) || - !bytes.Equal(mgf1HashFunc.Parameters.FullBytes, nullBytes) || - params.TrailerField != 1 { - return x509.UnknownSignatureAlgorithm - } - - switch { - case params.Hash.Algorithm.Equal(oidSHA256) && params.SaltLength == 32: - return x509.SHA256WithRSAPSS - case params.Hash.Algorithm.Equal(oidSHA384) && params.SaltLength == 48: - return x509.SHA384WithRSAPSS - case params.Hash.Algorithm.Equal(oidSHA512) && params.SaltLength == 64: - return x509.SHA512WithRSAPSS - } - - return x509.UnknownSignatureAlgorithm -} diff --git a/pdf/model/sighandler/sighandler_pkcs7.go b/pdf/model/sighandler/sighandler_pkcs7.go index 6f57a811..3c9c6e0d 100644 --- a/pdf/model/sighandler/sighandler_pkcs7.go +++ b/pdf/model/sighandler/sighandler_pkcs7.go @@ -11,7 +11,8 @@ import ( "crypto/x509" "errors" - "github.com/unidoc/unidoc/common/crypto/pkcs7" + "github.com/gunnsth/pkcs7" + "github.com/unidoc/unidoc/pdf/core" "github.com/unidoc/unidoc/pdf/model" ) From 54a3217b25e1d39372e6dc860ab658b0fd1794f8 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Thu, 17 Jan 2019 23:55:27 +0000 Subject: [PATCH 11/20] Improve documentation in appender.go --- pdf/model/appender.go | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pdf/model/appender.go b/pdf/model/appender.go index fde9eb59..7c88b115 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -31,6 +31,7 @@ type PdfAppender struct { xrefs core.XrefTable greatestObjNum int + // List of new objects and a map for quick lookups. newObjects []core.PdfObject hasNewObject map[core.PdfObject]struct{} } @@ -95,18 +96,24 @@ func getPageResources(p *PdfPage) map[core.PdfObjectName]core.PdfObject { // NewPdfAppender creates a new Pdf appender from a Pdf reader. func NewPdfAppender(reader *PdfReader) (*PdfAppender, error) { - a := &PdfAppender{} - a.rs = reader.rs - a.Reader = reader - a.parser = a.Reader.parser + a := &PdfAppender{ + rs: reader.rs, + Reader: reader, + parser: reader.parser, + } if _, err := a.rs.Seek(0, io.SeekStart); err != nil { return nil, err } var err error - // Create a readonly (immutable) reader. It increases memory using. - // Why? We can not check the original reader objects are changed or not. - // When we merge, replace a page content. The new page will contain objects from the readonly reader and other objects. - // The readonly objects won't append to the result Pdf file. This check is not resource demanding. It checks indirect objects owners only. + + // Create a readonly (immutable) reader. It increases memory use but is necessary to be able + // to detect changes in the original reader objects. + // + // In the case where an existing page is modified, the page contents are replaced upon merging + // (appending). The new page will refer to objects from the read-only reader and new instances + // of objects that have been changes. Objects from the original reader are not appended, only + // new objects that modify the PDF. The change detection check is not resource demanding. It + // only checks owners (source) of indirect objects. a.roReader, err = NewPdfReader(a.rs) if err != nil { return nil, err @@ -133,7 +140,7 @@ func (a *PdfAppender) addNewObjects(obj core.PdfObject) { switch v := obj.(type) { case *core.PdfIndirectObject: // Check the object is changing. - // If the indirect object has not the readonly parser then the object is changed. + // If the indirect object has not the read-only parser then the object is changed. if v.GetParser() != a.roReader.parser { a.newObjects = append(a.newObjects, obj) a.hasNewObject[obj] = struct{}{} @@ -332,7 +339,7 @@ func (a *PdfAppender) MergePageWith(pageNum int, page *PdfPage) error { return nil } -// AddPages adds pages to end of the source Pdf. +// AddPages adds pages to be appended to the end of the source PDF. func (a *PdfAppender) AddPages(pages ...*PdfPage) { for _, page := range pages { page = page.Duplicate() @@ -371,7 +378,7 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } } -// Sign a document +// Sign signs a document with a digital signature. func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, appearance *PdfAppearance, err error) { acroForm = a.Reader.AcroForm if acroForm == nil { @@ -434,7 +441,8 @@ func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *Pdf return acroForm, appearance, nil } -// ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which replaces the original acrobat form. +// ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which +// replaces the original AcroForm. func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { a.acroForm = acroForm } @@ -596,7 +604,7 @@ func (a *PdfAppender) Write(w io.Writer) error { writerW = bytes.NewBuffer(nil) } - // Do a mock write to get offsets. + // Do a mock write to get offsets (needed for signing). // TODO(gunnsth): Only needed when dealing with signatures? if err := writer.Write(writerW); err != nil { return err @@ -671,7 +679,6 @@ func (a *PdfAppender) Write(w io.Writer) error { } writerW = bytes.NewBuffer(bufferData) - } if buffer, ok := writerW.(*bytes.Buffer); ok { From 7ad11847c8bd44660ad068cf12c92e3b006fbcd0 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Fri, 18 Jan 2019 01:19:24 +0000 Subject: [PATCH 12/20] Return an error if try to invoke Appender Write twice. Fix bug when writing to a buffer. --- pdf/model/appender.go | 41 ++++++++++++++++++++++++--------- pdf/model/appender_test.go | 47 ++++++++++++++++++++++++++++++++++++++ pdf/model/signature.go | 3 +++ 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 7c88b115..b61b1220 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -34,6 +34,8 @@ type PdfAppender struct { // List of new objects and a map for quick lookups. newObjects []core.PdfObject hasNewObject map[core.PdfObject]struct{} + + written bool } func getPageResources(p *PdfPage) map[core.PdfObjectName]core.PdfObject { @@ -378,7 +380,8 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } } -// Sign signs a document with a digital signature. +// Sign signs a specific page with a digital signature using a specified signature handler. +// Returns an Acroform and PdfAppearance that can be used to customize the signature appearance. func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, appearance *PdfAppearance, err error) { acroForm = a.Reader.AcroForm if acroForm == nil { @@ -448,7 +451,13 @@ func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { } // Write writes the Appender output to io.Writer. +// It can only be called once and further invokations will result in an error. func (a *PdfAppender) Write(w io.Writer) error { + if a.written { + return errors.New("appender write can only be invoked once") + } + a.written = true + writer := NewPdfWriter() pagesDict, ok := core.GetDict(writer.pages) @@ -541,6 +550,9 @@ func (a *PdfAppender) Write(w io.Writer) error { return err } + // Digital signature handling: Check if any of the new objects represent a signature dictionary. + // The byte range is later updated dynamically based on the position of the actual signature + // Contents. digestWriters := make(map[SignatureHandler]io.Writer) byteRange := core.MakeArray() for _, obj := range a.newObjects { @@ -559,7 +571,6 @@ func (a *PdfAppender) Write(w io.Writer) error { if byteRange.Len() > 0 { byteRange.Append(core.MakeInteger(0xfffff), core.MakeInteger(0xfffff)) } - for _, obj := range a.newObjects { if ind, found := core.GetIndirect(obj); found { if sigDict, found := ind.PdfObject.(*pdfSignDictionary); found { @@ -579,6 +590,7 @@ func (a *PdfAppender) Write(w io.Writer) error { reader = io.TeeReader(a.rs, io.MultiWriter(writers...)) } + // Write the original PDF. offset, err := io.Copy(w, reader) if err != nil { return err @@ -599,19 +611,22 @@ func (a *PdfAppender) Write(w io.Writer) error { } writerW := w - if hasSigDict { + // For signatures, we need to write twice. First to find the byte offset of the Contents and then + // dynamically update file with the signature and ByteRange. + // Thus we create an empty buffer to write to and then at the e writerW = bytes.NewBuffer(nil) } - // Do a mock write to get offsets (needed for signing). - // TODO(gunnsth): Only needed when dealing with signatures? + // Perform the write. For signatures will do a mock write to a buffer. if err := writer.Write(writerW); err != nil { return err } - // TODO(gunnsth): Consider whether the dynamic content can be handled with generic write hooks? + // TODO(gunnsth): Consider whether the dynamic content can be handled efficiently with generic write hooks? + // Logic is getting pretty complex here. if hasSigDict { + // Update the byteRanges based on mock write. bufferData := writerW.(*bytes.Buffer).Bytes() byteRange := core.MakeArray() var sigDicts []*pdfSignDictionary @@ -665,6 +680,9 @@ func (a *PdfAppender) Write(w io.Writer) error { } contents := []byte(sigDict.signature.Contents.WriteString()) + // Empty out the ByteRange and Content data. + // FIXME(gunnsth): Is this needed? Seems like the correct data is copied below? Prefer + // to keep the rest space? for i := sigDict.byteRangeOffsetStart; i < sigDict.byteRangeOffsetEnd; i++ { bufferData[bufferOffset+i] = ' ' } @@ -672,20 +690,21 @@ func (a *PdfAppender) Write(w io.Writer) error { bufferData[bufferOffset+i] = ' ' } + // Copy the actual ByteRange and Contents data into the buffer prepared by first write. dst := bufferData[bufferOffset+sigDict.byteRangeOffsetStart : bufferOffset+sigDict.byteRangeOffsetEnd] copy(dst, byteRangeData) dst = bufferData[bufferOffset+sigDict.contentsOffsetStart : bufferOffset+sigDict.contentsOffsetEnd] copy(dst, contents) } - writerW = bytes.NewBuffer(bufferData) - } - - if buffer, ok := writerW.(*bytes.Buffer); ok { + buffer := bytes.NewBuffer(bufferData) _, err = io.Copy(w, buffer) + if err != nil { + return err + } } - return err + return nil } // WriteToFile writes the Appender output to file specified by path. diff --git a/pdf/model/appender_test.go b/pdf/model/appender_test.go index 04c1b6d8..7ba3bc26 100644 --- a/pdf/model/appender_test.go +++ b/pdf/model/appender_test.go @@ -538,3 +538,50 @@ func TestAppenderSignMultiple(t *testing.T) { f.Close() } } + +// Each Appender can only be written out once, further invokations of Write should result in an error. +func TestAppenderAttemptMultiWrite(t *testing.T) { + f1, err := os.Open(testPdfLoremIpsumFile) + 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 + } + f2, err := os.Open(testPdfFile1) + if err != nil { + t.Errorf("Fail: %v\n", err) + return + } + defer f2.Close() + pdf2, err := model.NewPdfReader(f2) + 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 + } + + appender.AddPages(pdf1.PageList...) + appender.AddPages(pdf2.PageList...) + appender.AddPages(pdf2.PageList...) + + // Write twice to buffer and compare results. + var buf1, buf2 bytes.Buffer + err = appender.Write(&buf1) + if err != nil { + t.Fatalf("Error: %v", err) + } + err = appender.Write(&buf2) + if err == nil { + t.Fatalf("Second invokation of appender.Write should yield an error") + } +} diff --git a/pdf/model/signature.go b/pdf/model/signature.go index d8d3da5d..a11fa6fe 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -12,8 +12,11 @@ import ( "github.com/unidoc/unidoc/pdf/core" ) +var _ core.PdfObject = &pdfSignDictionary{} + // pdfSignDictionary is used as a wrapper around PdfSignature for digital checksum calculation // and population of /Contents and /ByteRange. +// Implements interface core.PdfObject. type pdfSignDictionary struct { *core.PdfObjectDictionary handler *SignatureHandler From 094d2038c803d0d02e81a4201e29908e04528c4a Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Fri, 18 Jan 2019 17:41:51 +0000 Subject: [PATCH 13/20] Fix --- pdf/model/sighandler/sighandler_rsa_sha1.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdf/model/sighandler/sighandler_rsa_sha1.go b/pdf/model/sighandler/sighandler_rsa_sha1.go index ad7f395a..543cdddf 100644 --- a/pdf/model/sighandler/sighandler_rsa_sha1.go +++ b/pdf/model/sighandler/sighandler_rsa_sha1.go @@ -143,7 +143,7 @@ func (a *adobeX509RSASHA1) Sign(sig *model.PdfSignature, digest model.Hasher) er return nil } -// IsApplicable returns true if the signature handler is applicable for the PdfSignature +// IsApplicable returns true if the signature handler is applicable for the PdfSignature. func (a *adobeX509RSASHA1) IsApplicable(sig *model.PdfSignature) bool { if sig == nil || sig.Filter == nil || sig.SubFilter == nil { return false From 1a92cec9c4f27074683cca5087e19fb22d7c230f Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 12 Feb 2019 19:18:39 +0200 Subject: [PATCH 14/20] Refactor signature code --- pdf/model/appender.go | 13 +-- pdf/model/signature.go | 176 +++++++++------------------------ pdf/model/signature_handler.go | 34 ++++--- pdf/model/writer.go | 8 +- 4 files changed, 78 insertions(+), 153 deletions(-) diff --git a/pdf/model/appender.go b/pdf/model/appender.go index b61b1220..397b7d8c 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -387,17 +387,12 @@ func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *Pdf if acroForm == nil { acroForm = NewPdfAcroForm() } + pageIndex := pageNum - 1 - var page *PdfPage - for i, p := range a.pages { - if i == pageIndex { - page = p.Duplicate() - break - } - } - if page == nil { + if pageIndex < 0 || pageIndex > len(a.pages)-1 { return nil, nil, fmt.Errorf("page %d not found", pageNum) } + page := a.pages[pageIndex].Duplicate() // TODO add more checks before set the fields acroForm.SigFlags = core.MakeInteger(3) @@ -456,7 +451,6 @@ func (a *PdfAppender) Write(w io.Writer) error { if a.written { return errors.New("appender write can only be invoked once") } - a.written = true writer := NewPdfWriter() @@ -704,6 +698,7 @@ func (a *PdfAppender) Write(w io.Writer) error { } } + a.written = true return nil } diff --git a/pdf/model/signature.go b/pdf/model/signature.go index a11fa6fe..123daad2 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -140,49 +140,23 @@ func (sig *PdfSignature) ToPdfObject() core.PdfObject { } dict.Set("Type", sig.Type) + dict.SetIfNotNil("Filter", sig.Filter) + dict.SetIfNotNil("SubFilter", sig.SubFilter) + dict.SetIfNotNil("Contents", sig.Contents) + dict.SetIfNotNil("Cert", sig.Cert) + dict.SetIfNotNil("ByteRange", sig.ByteRange) + dict.SetIfNotNil("Reference", sig.Reference) + dict.SetIfNotNil("Changes", sig.Changes) + dict.SetIfNotNil("Name", sig.Name) + dict.SetIfNotNil("M", sig.M) + dict.SetIfNotNil("Reason", sig.Reason) + dict.SetIfNotNil("ContactInfo", sig.ContactInfo) + dict.SetIfNotNil("ByteRange", sig.ByteRange) + dict.SetIfNotNil("Contents", sig.Contents) - if sig.Filter != nil { - dict.Set("Filter", sig.Filter) - } - if sig.SubFilter != nil { - dict.Set("SubFilter", sig.SubFilter) - } - if sig.Contents != nil { - dict.Set("Contents", sig.Contents) - } - if sig.Cert != nil { - dict.Set("Cert", sig.Cert) - } - if sig.ByteRange != nil { - dict.Set("ByteRange", sig.ByteRange) - } - if sig.Reference != nil { - dict.Set("Reference", sig.Reference) - } - if sig.Changes != nil { - dict.Set("Changes", sig.Changes) - } - if sig.Name != nil { - dict.Set("Name", sig.Name) - } - if sig.M != nil { - dict.Set("M", sig.M) - } - if sig.Reason != nil { - dict.Set("Reason", sig.Reason) - } - if sig.ContactInfo != nil { - dict.Set("ContactInfo", sig.ContactInfo) - } - if sig.ByteRange != nil { - dict.Set("ByteRange", sig.ByteRange) - } - if sig.Contents != nil { - dict.Set("Contents", sig.Contents) - } // NOTE: ByteRange and Contents need to be updated dynamically. - // TODO: Currently dynamic update is only in the appender, need to support in the PdfWriter - // too for the initial signature on document creation. + // TODO: Currently dynamic update is only in the appender, need to support + // in the PdfWriter too for the initial signature on document creation. return container } @@ -281,19 +255,10 @@ func (sf *PdfSignatureField) ToPdfObject() core.PdfObject { dict := container.PdfObject.(*core.PdfObjectDictionary) dict.Set("FT", core.MakeName("Sig")) - - if sf.V != nil { - dict.Set("V", sf.V.ToPdfObject()) - } - if sf.Lock != nil { - dict.Set("Lock", sf.Lock) - } - if sf.SV != nil { - dict.Set("SV", sf.SV) - } - if sf.Kids != nil { - dict.Set("Kids", sf.Kids) - } + dict.SetIfNotNil("V", sf.V.ToPdfObject()) + dict.SetIfNotNil("Lock", sf.Lock) + dict.SetIfNotNil("SV", sf.SV) + dict.SetIfNotNil("Kids", sf.Kids) // Other standard fields... return container @@ -332,45 +297,20 @@ func (pss *PdfSignatureFieldSeed) ToPdfObject() core.PdfObject { container := pss.container dict := container.PdfObject.(*core.PdfObjectDictionary) - if pss.Ff != nil { - dict.Set("Ff", pss.Ff) - } - if pss.Filter != nil { - dict.Set("Filter", pss.Filter) - } - if pss.SubFilter != nil { - dict.Set("SubFilter", pss.SubFilter) - } - if pss.DigestMethod != nil { - dict.Set("DigestMethod", pss.DigestMethod) - } - if pss.V != nil { - dict.Set("V", pss.V) - } - if pss.Cert != nil { - dict.Set("Cert", pss.Cert) - } - if pss.Reasons != nil { - dict.Set("Reasons", pss.Reasons) - } - if pss.MDP != nil { - dict.Set("MDP", pss.MDP) - } - if pss.TimeStamp != nil { - dict.Set("TimeStamp", pss.TimeStamp) - } - if pss.LegalAttestation != nil { - dict.Set("LegalAttestation", pss.LegalAttestation) - } - if pss.AddRevInfo != nil { - dict.Set("AddRevInfo", pss.AddRevInfo) - } - if pss.LockDocument != nil { - dict.Set("LockDocument", pss.LockDocument) - } - if pss.AppearanceFilter != nil { - dict.Set("AppearanceFilter", pss.AppearanceFilter) - } + dict.SetIfNotNil("Ff", pss.Ff) + dict.SetIfNotNil("Filter", pss.Filter) + dict.SetIfNotNil("SubFilter", pss.SubFilter) + dict.SetIfNotNil("DigestMethod", pss.DigestMethod) + dict.SetIfNotNil("V", pss.V) + dict.SetIfNotNil("Cert", pss.Cert) + dict.SetIfNotNil("Reasons", pss.Reasons) + dict.SetIfNotNil("MDP", pss.MDP) + dict.SetIfNotNil("TimeStamp", pss.TimeStamp) + dict.SetIfNotNil("LegalAttestation", pss.LegalAttestation) + dict.SetIfNotNil("AddRevInfo", pss.AddRevInfo) + dict.SetIfNotNil("LockDocument", pss.LockDocument) + dict.SetIfNotNil("AppearanceFilter", pss.AppearanceFilter) + return container } @@ -396,41 +336,19 @@ type PdfCertificateSeed struct { func (pcs *PdfCertificateSeed) ToPdfObject() core.PdfObject { container := pcs.container dict := container.PdfObject.(*core.PdfObjectDictionary) - if pcs.Ff != nil { - dict.Set("Ff", pcs.Ff) - } - if pcs.Subject != nil { - dict.Set("Subject", pcs.Subject) - } - if pcs.SignaturePolicyOID != nil { - dict.Set("SignaturePolicyOID", pcs.SignaturePolicyOID) - } - if pcs.SignaturePolicyHashValue != nil { - dict.Set("SignaturePolicyHashValue", pcs.SignaturePolicyHashValue) - } - if pcs.SignaturePolicyHashAlgorithm != nil { - dict.Set("SignaturePolicyHashAlgorithm", pcs.SignaturePolicyHashAlgorithm) - } - if pcs.SignaturePolicyCommitmentType != nil { - dict.Set("SignaturePolicyCommitmentType", pcs.SignaturePolicyCommitmentType) - } - if pcs.SubjectDN != nil { - dict.Set("SubjectDN", pcs.SubjectDN) - } - if pcs.KeyUsage != nil { - dict.Set("KeyUsage", pcs.KeyUsage) - } - if pcs.Issuer != nil { - dict.Set("Issuer", pcs.Issuer) - } - if pcs.OID != nil { - dict.Set("OID", pcs.OID) - } - if pcs.URL != nil { - dict.Set("URL", pcs.URL) - } - if pcs.URLType != nil { - dict.Set("URLType", pcs.URLType) - } + + dict.SetIfNotNil("Ff", pcs.Ff) + dict.SetIfNotNil("Subject", pcs.Subject) + dict.SetIfNotNil("SignaturePolicyOID", pcs.SignaturePolicyOID) + dict.SetIfNotNil("SignaturePolicyHashValue", pcs.SignaturePolicyHashValue) + dict.SetIfNotNil("SignaturePolicyHashAlgorithm", pcs.SignaturePolicyHashAlgorithm) + dict.SetIfNotNil("SignaturePolicyCommitmentType", pcs.SignaturePolicyCommitmentType) + dict.SetIfNotNil("SubjectDN", pcs.SubjectDN) + dict.SetIfNotNil("KeyUsage", pcs.KeyUsage) + dict.SetIfNotNil("Issuer", pcs.Issuer) + dict.SetIfNotNil("OID", pcs.OID) + dict.SetIfNotNil("URL", pcs.URL) + dict.SetIfNotNil("URLType", pcs.URLType) + return container } diff --git a/pdf/model/signature_handler.go b/pdf/model/signature_handler.go index 4a15b88d..a1a2f286 100644 --- a/pdf/model/signature_handler.go +++ b/pdf/model/signature_handler.go @@ -10,6 +10,7 @@ import ( "fmt" "io" + "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" ) @@ -104,35 +105,44 @@ func (r *PdfReader) ValidateSignatures(handlers []SignatureHandler) ([]Signature field *PdfField handler SignatureHandler } - var pairs []*sigFieldPair + var pairs []*sigFieldPair for _, f := range r.AcroForm.AllFields() { if f.V == nil { continue } if d, found := core.GetDict(f.V); found { if name, ok := core.GetNameVal(d.Get("Type")); ok && name == "Sig" { - ind, _ := core.GetIndirect(f.V) // TODO refactoring + ind, found := core.GetIndirect(f.V) + if !found { + common.Log.Debug("ERROR: Signature container is nil") + return nil, ErrTypeCheck + } + sig, err := r.newPdfSignatureFromIndirect(ind) if err != nil { return nil, err } - pairs = append(pairs, &sigFieldPair{sig: sig, field: f}) - } - } - } - for _, pair := range pairs { - for _, handler := range handlers { - if handler.IsApplicable(pair.sig) { - pair.handler = handler - break + // Search for an appropriate handler. + var sigHandler SignatureHandler + for _, handler := range handlers { + if handler.IsApplicable(sig) { + sigHandler = handler + break + } + } + + pairs = append(pairs, &sigFieldPair{ + sig: sig, + field: f, + handler: sigHandler, + }) } } } var results []SignatureValidationResult - for _, pair := range pairs { defaultResult := SignatureValidationResult{ IsSigned: true, diff --git a/pdf/model/writer.go b/pdf/model/writer.go index 6ee85455..7e4b653b 100644 --- a/pdf/model/writer.go +++ b/pdf/model/writer.go @@ -226,9 +226,11 @@ func copyObject(obj core.PdfObject, objectToObjectCopyMap map[core.PdfObject]cor objectToObjectCopyMap[obj] = &newObj return &newObj case *pdfSignDictionary: - newObj := &pdfSignDictionary{PdfObjectDictionary: MakeDict()} - newObj.handler = t.handler - newObj.signature = t.signature + newObj := &pdfSignDictionary{ + PdfObjectDictionary: core.MakeDict(), + handler: t.handler, + signature: t.signature, + } objectToObjectCopyMap[obj] = newObj for _, key := range t.Keys() { val := t.Get(key) From 3233ce9e9cffa4dac253acfaeb51b256f18a2f2a Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 12 Feb 2019 19:49:37 +0200 Subject: [PATCH 15/20] Further refactoring --- pdf/model/appearance.go | 1 + pdf/model/fields.go | 41 ++++++++------------- pdf/model/sighandler/sighandler_pkcs7.go | 14 +++---- pdf/model/sighandler/sighandler_rsa_sha1.go | 2 +- pdf/model/signature.go | 6 ++- 5 files changed, 29 insertions(+), 35 deletions(-) diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go index 7c2411ec..853765c9 100644 --- a/pdf/model/appearance.go +++ b/pdf/model/appearance.go @@ -33,6 +33,7 @@ func (app *PdfAppearance) ToPdfObject() core.PdfObject { } app.PdfAnnotation.ToPdfObject() app.PdfField.ToPdfObject() + container := app.container d := container.PdfObject.(*core.PdfObjectDictionary) diff --git a/pdf/model/fields.go b/pdf/model/fields.go index 2d026a72..d9489055 100644 --- a/pdf/model/fields.go +++ b/pdf/model/fields.go @@ -245,46 +245,37 @@ func (f *PdfField) String() string { } // ToPdfObject sets the common field elements. -// Note: Call the more field context's ToPdfObject to set both the generic and non-generic information. +// Note: Call the more field context's ToPdfObject to set both the generic and +// non-generic information. func (f *PdfField) ToPdfObject() core.PdfObject { container := f.container d := container.PdfObject.(*core.PdfObjectDictionary) - d.SetIfNotNil("FT", f.FT) - if f.Parent != nil { - d.Set("Parent", f.Parent.GetContainingPdfObject()) - } + // Create an array of the kids (fields or widgets). kids := core.MakeArray() - if f.Kids != nil { - // Create an array of the kids (fields or widgets). - for _, child := range f.Kids { - kids.Append(child.ToPdfObject()) - } + for _, child := range f.Kids { + kids.Append(child.ToPdfObject()) + } + for _, annot := range f.Annotations { + kids.Append(annot.GetContext().ToPdfObject()) } - if f.Annotations != nil { - for _, annot := range f.Annotations { - kids.Append(annot.GetContext().ToPdfObject()) - } + // Set fields. + if f.Parent != nil { + d.SetIfNotNil("Parent", f.Parent.GetContainingPdfObject()) } - - if f.Kids != nil || f.Annotations != nil { + if kids.Len() > 0 { d.Set("Kids", kids) } + d.SetIfNotNil("FT", f.FT) d.SetIfNotNil("T", f.T) d.SetIfNotNil("TU", f.TU) d.SetIfNotNil("TM", f.TM) d.SetIfNotNil("Ff", f.Ff) - if f.V != nil { - d.Set("V", f.V) - } - if f.DV != nil { - d.Set("DV", f.DV) - } - if f.AA != nil { - d.Set("AA", f.AA) - } + d.SetIfNotNil("V", f.V) + d.SetIfNotNil("DV", f.DV) + d.SetIfNotNil("AA", f.AA) return container } diff --git a/pdf/model/sighandler/sighandler_pkcs7.go b/pdf/model/sighandler/sighandler_pkcs7.go index 3c9c6e0d..5409e57a 100644 --- a/pdf/model/sighandler/sighandler_pkcs7.go +++ b/pdf/model/sighandler/sighandler_pkcs7.go @@ -42,8 +42,8 @@ func (a *adobePKCS7Detached) InitSignature(sig *model.PdfSignature) error { sig.Handler = &handler sig.Filter = core.MakeName("Adobe.PPKLite") sig.SubFilter = core.MakeName("adbe.pkcs7.detached") - sig.Reference = nil + digest, err := handler.NewDigest(sig) if err != nil { return err @@ -73,23 +73,21 @@ func (a *adobePKCS7Detached) NewDigest(sig *model.PdfSignature) (model.Hasher, e // Validate validates PdfSignature. func (a *adobePKCS7Detached) Validate(sig *model.PdfSignature, digest model.Hasher) (model.SignatureValidationResult, error) { signed := sig.Contents.Bytes() - - buffer := digest.(*bytes.Buffer) p7, err := pkcs7.Parse(signed) if err != nil { return model.SignatureValidationResult{}, err } + + buffer := digest.(*bytes.Buffer) p7.Content = buffer.Bytes() - err = p7.Verify() - if err != nil { + if err = p7.Verify(); err != nil { return model.SignatureValidationResult{}, err } - result := model.SignatureValidationResult{ + return model.SignatureValidationResult{ IsSigned: true, IsVerified: true, - } - return result, nil + }, nil } // Sign sets the Contents fields. diff --git a/pdf/model/sighandler/sighandler_rsa_sha1.go b/pdf/model/sighandler/sighandler_rsa_sha1.go index 543cdddf..8b924947 100644 --- a/pdf/model/sighandler/sighandler_rsa_sha1.go +++ b/pdf/model/sighandler/sighandler_rsa_sha1.go @@ -45,8 +45,8 @@ func (a *adobeX509RSASHA1) InitSignature(sig *model.PdfSignature) error { //sig.Filter = core.MakeName("Adobe.PPKMS") sig.SubFilter = core.MakeName("adbe.x509.rsa_sha1") sig.Cert = core.MakeString(string(handler.certificate.Raw)) - sig.Reference = nil + digest, err := handler.NewDigest(sig) if err != nil { return err diff --git a/pdf/model/signature.go b/pdf/model/signature.go index 123daad2..ef39c82a 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -254,8 +254,12 @@ func (sf *PdfSignatureField) ToPdfObject() core.PdfObject { container := sf.container dict := container.PdfObject.(*core.PdfObjectDictionary) + // Set fields. + if sf.V != nil { + dict.Set("V", sf.V.ToPdfObject()) + } + dict.Set("FT", core.MakeName("Sig")) - dict.SetIfNotNil("V", sf.V.ToPdfObject()) dict.SetIfNotNil("Lock", sf.Lock) dict.SetIfNotNil("SV", sf.SV) dict.SetIfNotNil("Kids", sf.Kids) From cda855354a7401299b1a73772f3b2434929ad144 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 12 Feb 2019 19:57:42 +0200 Subject: [PATCH 16/20] Rename PdfAppearance to PdfSignatureAppearance --- pdf/model/appearance.go | 12 ++++++------ pdf/model/appender.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go index 853765c9..99a66310 100644 --- a/pdf/model/appearance.go +++ b/pdf/model/appearance.go @@ -7,17 +7,17 @@ package model import "github.com/unidoc/unidoc/pdf/core" -// PdfAppearance contains the common attributes of an appearance form field. -type PdfAppearance struct { +// PdfSignatureAppearance contains the common attributes of an appearance form field. +type PdfSignatureAppearance struct { *PdfField *PdfAnnotationWidget // TODO(gunnsth): Signature is not really part of an appearance. Signature *PdfSignature } -// NewPdfAppearance returns an initialized annotation widget. -func NewPdfAppearance() *PdfAppearance { - app := &PdfAppearance{} +// NewPdfSignatureAppearance returns an initialized annotation widget. +func NewPdfSignatureAppearance() *PdfSignatureAppearance { + app := &PdfSignatureAppearance{} app.PdfField = NewPdfField() app.PdfAnnotationWidget = NewPdfAnnotationWidget() app.PdfField.SetContext(app) @@ -27,7 +27,7 @@ func NewPdfAppearance() *PdfAppearance { } // ToPdfObject implements interface PdfModel. -func (app *PdfAppearance) ToPdfObject() core.PdfObject { +func (app *PdfSignatureAppearance) ToPdfObject() core.PdfObject { if app.Signature != nil { app.V = app.Signature.ToPdfObject() } diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 397b7d8c..6ba1e2a2 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -382,7 +382,7 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { // Sign signs a specific page with a digital signature using a specified signature handler. // Returns an Acroform and PdfAppearance that can be used to customize the signature appearance. -func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, appearance *PdfAppearance, err error) { +func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, appearance *PdfSignatureAppearance, err error) { acroForm = a.Reader.AcroForm if acroForm == nil { acroForm = NewPdfAcroForm() @@ -411,7 +411,7 @@ func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *Pdf } a.addNewObjects(sig.container) - appearance = NewPdfAppearance() + appearance = NewPdfSignatureAppearance() fields := append(acroForm.AllFields(), appearance.PdfField) acroForm.Fields = &fields From 2ef5b5ab535600c50b8ad345bf38cd2783b41367 Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Tue, 12 Feb 2019 23:50:57 +0000 Subject: [PATCH 17/20] Update comments --- pdf/model/appearance.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go index 99a66310..0790c7f1 100644 --- a/pdf/model/appearance.go +++ b/pdf/model/appearance.go @@ -7,15 +7,16 @@ package model import "github.com/unidoc/unidoc/pdf/core" -// PdfSignatureAppearance contains the common attributes of an appearance form field. +// PdfSignatureAppearance defines a signature with a specified form field and +// annotation widget for appearance styling. type PdfSignatureAppearance struct { *PdfField *PdfAnnotationWidget - // TODO(gunnsth): Signature is not really part of an appearance. + Signature *PdfSignature } -// NewPdfSignatureAppearance returns an initialized annotation widget. +// NewPdfSignatureAppearance returns an initialized signature field appearance. func NewPdfSignatureAppearance() *PdfSignatureAppearance { app := &PdfSignatureAppearance{} app.PdfField = NewPdfField() From c2d1efb653119b19f83a807ba32b010bf9803215 Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Wed, 13 Feb 2019 20:28:24 +0200 Subject: [PATCH 18/20] Combine PdfFieldSignature with PdfSignatureAppearance --- pdf/model/appearance.go | 50 -------------------------------------- pdf/model/appender.go | 37 ++++++++++++++-------------- pdf/model/appender_test.go | 8 +++--- pdf/model/fields.go | 31 ++++++++++++++++------- 4 files changed, 44 insertions(+), 82 deletions(-) delete mode 100644 pdf/model/appearance.go diff --git a/pdf/model/appearance.go b/pdf/model/appearance.go deleted file mode 100644 index 0790c7f1..00000000 --- a/pdf/model/appearance.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is subject to the terms and conditions defined in - * file 'LICENSE.md', which is part of this source code package. - */ - -package model - -import "github.com/unidoc/unidoc/pdf/core" - -// PdfSignatureAppearance defines a signature with a specified form field and -// annotation widget for appearance styling. -type PdfSignatureAppearance struct { - *PdfField - *PdfAnnotationWidget - - Signature *PdfSignature -} - -// NewPdfSignatureAppearance returns an initialized signature field appearance. -func NewPdfSignatureAppearance() *PdfSignatureAppearance { - app := &PdfSignatureAppearance{} - app.PdfField = NewPdfField() - app.PdfAnnotationWidget = NewPdfAnnotationWidget() - app.PdfField.SetContext(app) - app.PdfAnnotationWidget.SetContext(app) - app.PdfAnnotationWidget.container = app.PdfField.container - return app -} - -// ToPdfObject implements interface PdfModel. -func (app *PdfSignatureAppearance) ToPdfObject() core.PdfObject { - if app.Signature != nil { - app.V = app.Signature.ToPdfObject() - } - app.PdfAnnotation.ToPdfObject() - app.PdfField.ToPdfObject() - - container := app.container - d := container.PdfObject.(*core.PdfObjectDictionary) - - d.SetIfNotNil("Subtype", core.MakeName("Widget")) - d.SetIfNotNil("H", app.H) - d.SetIfNotNil("MK", app.MK) - d.SetIfNotNil("A", app.A) - d.SetIfNotNil("AA", app.PdfAnnotationWidget.AA) - d.SetIfNotNil("BS", app.BS) - d.SetIfNotNil("Parent", app.PdfAnnotationWidget.Parent) - - return container -} diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 6ba1e2a2..97d2b098 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -381,8 +381,8 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } // Sign signs a specific page with a digital signature using a specified signature handler. -// Returns an Acroform and PdfAppearance that can be used to customize the signature appearance. -func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, appearance *PdfSignatureAppearance, err error) { +// Returns an Acroform and PdfFieldSignature that can be used to customize the signature appearance. +func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, signatureField *PdfFieldSignature, err error) { acroForm = a.Reader.AcroForm if acroForm == nil { acroForm = NewPdfAcroForm() @@ -411,32 +411,31 @@ func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *Pdf } a.addNewObjects(sig.container) - appearance = NewPdfSignatureAppearance() - - fields := append(acroForm.AllFields(), appearance.PdfField) + signatureField = NewPdfFieldSignature() + fields := append(acroForm.AllFields(), signatureField.PdfField) acroForm.Fields = &fields procPage(page) - appearance.V = sig.ToPdfObject() - appearance.FT = core.MakeName("Sig") - appearance.V = sig.ToPdfObject() - appearance.T = core.MakeString("Signature1") - appearance.F = core.MakeInteger(132) - appearance.P = page.ToPdfObject() - appearance.Rect = core.MakeArray(core.MakeInteger(0), core.MakeInteger(0), core.MakeInteger(0), - core.MakeInteger(0)) - appearance.Signature = sig - - // Add the signature appearance to the page annotations. - page.Annotations = append(page.Annotations, appearance.PdfAnnotationWidget.PdfAnnotation) + signatureField.FT = core.MakeName("Sig") + signatureField.V = sig + signatureField.T = core.MakeString("Signature1") + signatureField.F = core.MakeInteger(132) + signatureField.P = page.ToPdfObject() + signatureField.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + // Add the signature field to the page annotations. + page.Annotations = append(page.Annotations, signatureField.PdfAnnotationWidget.PdfAnnotation) a.pages[pageIndex] = page // Update acroform. a.ReplaceAcroForm(acroForm) - - return acroForm, appearance, nil + return acroForm, signatureField, nil } // ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which diff --git a/pdf/model/appender_test.go b/pdf/model/appender_test.go index 7ba3bc26..f017a78d 100644 --- a/pdf/model/appender_test.go +++ b/pdf/model/appender_test.go @@ -453,8 +453,8 @@ func TestAppenderSignPage4(t *testing.T) { return } - appearance.Signature.Name = core.MakeString("Test Appender") - appearance.Signature.Reason = core.MakeString("TestAppenderSignPage4") + appearance.V.Name = core.MakeString("Test Appender") + appearance.V.Reason = core.MakeString("TestAppenderSignPage4") err = appender.WriteToFile(tempFile("appender_sign_page_4.pdf")) if err != nil { @@ -520,8 +520,8 @@ func TestAppenderSignMultiple(t *testing.T) { return } - appearance.Signature.Name = core.MakeString(fmt.Sprintf("Test Appender - Round %d", i+1)) - appearance.Signature.Reason = core.MakeString("TestAppenderSignPage4") + appearance.V.Name = core.MakeString(fmt.Sprintf("Test Appender - Round %d", i+1)) + appearance.V.Reason = core.MakeString("TestAppenderSignPage4") outPath := tempFile(fmt.Sprintf("appender_sign_multiple_%d.pdf", i+1)) diff --git a/pdf/model/fields.go b/pdf/model/fields.go index d9489055..f5876a67 100644 --- a/pdf/model/fields.go +++ b/pdf/model/fields.go @@ -433,28 +433,41 @@ func (ch *PdfFieldChoice) ToPdfObject() core.PdfObject { // the name of the signer and verifying document contents. type PdfFieldSignature struct { *PdfField + *PdfAnnotationWidget + V *PdfSignature Lock *core.PdfIndirectObject SV *core.PdfIndirectObject } +// NewPdfFieldSignature returns an initialized signature field. +func NewPdfFieldSignature() *PdfFieldSignature { + sig := &PdfFieldSignature{} + sig.PdfField = NewPdfField() + sig.PdfAnnotationWidget = NewPdfAnnotationWidget() + sig.PdfField.SetContext(sig) + sig.PdfAnnotationWidget.SetContext(sig) + sig.PdfAnnotationWidget.container = sig.PdfField.container + return sig +} + // ToPdfObject returns an indirect object containing the signature field dictionary. func (sig *PdfFieldSignature) ToPdfObject() core.PdfObject { // Set general field attributes + if sig.PdfAnnotationWidget != nil { + sig.PdfAnnotation.ToPdfObject() + } sig.PdfField.ToPdfObject() - container := sig.container // Handle signature field specific attributes + container := sig.container + d := container.PdfObject.(*core.PdfObjectDictionary) - d.Set("FT", core.MakeName("Sig")) + d.SetIfNotNil("FT", core.MakeName("Sig")) + d.SetIfNotNil("Lock", sig.Lock) + d.SetIfNotNil("SV", sig.SV) if sig.V != nil { - d.Set("V", sig.V.ToPdfObject()) - } - if sig.Lock != nil { - d.Set("Lock", sig.Lock) - } - if sig.SV != nil { - d.Set("SV", sig.SV) + d.SetIfNotNil("V", sig.V.ToPdfObject()) } return container From 28ad01f4b9dd1ce88185ed4f375665eb12e0784d Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Fri, 15 Feb 2019 20:59:17 +0200 Subject: [PATCH 19/20] Change the prototype of the appender Sign method --- pdf/model/appender.go | 76 +++++++------- pdf/model/appender_test.go | 57 +++++++++-- pdf/model/fields.go | 21 ++-- pdf/model/signature.go | 199 ++++++++----------------------------- 4 files changed, 133 insertions(+), 220 deletions(-) diff --git a/pdf/model/appender.go b/pdf/model/appender.go index 97d2b098..c7360d3d 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -13,7 +13,6 @@ import ( "os" "strconv" "strings" - "time" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" @@ -381,61 +380,58 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } // Sign signs a specific page with a digital signature using a specified signature handler. -// Returns an Acroform and PdfFieldSignature that can be used to customize the signature appearance. -func (a *PdfAppender) Sign(pageNum int, handler SignatureHandler) (acroForm *PdfAcroForm, signatureField *PdfFieldSignature, err error) { - acroForm = a.Reader.AcroForm +// Returns a PdfFieldSignature that can be used to customize the signature appearance. +func (a *PdfAppender) Sign(pageNum int, field *PdfFieldSignature) error { + if field == nil { + return errors.New("signature field cannot be nil") + } + + signature := field.V + if signature == nil { + return errors.New("field signature cannot be nil") + } + + // Get a copy of the selected page. + pageIndex := pageNum - 1 + if pageIndex < 0 || pageIndex > len(a.pages)-1 { + return fmt.Errorf("page %d not found", pageNum) + } + page := a.pages[pageIndex].Duplicate() + + // Initialize signature. + if err := signature.Initialize(); err != nil { + return err + } + a.addNewObjects(signature.container) + + // Add signature field annotations to the page annotations. + for _, annotation := range field.Annotations { + annotation.P = page.ToPdfObject() + page.Annotations = append(page.Annotations, annotation.PdfAnnotation) + } + + // Add signature field to the form. + acroForm := a.Reader.AcroForm if acroForm == nil { acroForm = NewPdfAcroForm() } - pageIndex := pageNum - 1 - if pageIndex < 0 || pageIndex > len(a.pages)-1 { - return nil, nil, fmt.Errorf("page %d not found", pageNum) - } - page := a.pages[pageIndex].Duplicate() - - // TODO add more checks before set the fields acroForm.SigFlags = core.MakeInteger(3) acroForm.DA = core.MakeString("/F1 0 Tf 0 g") n2ResourcesFont := core.MakeDict() n2ResourcesFont.Set("F1", DefaultFont().ToPdfObject()) acroForm.DR = NewPdfPageResources() acroForm.DR.Font = n2ResourcesFont - sig := NewPdfSignature() - sig.M = core.MakeString(time.Now().Format("D:20060102150405-07'00'")) - //sig.M = core.MakeString("D:20150226112648Z") - sig.Type = core.MakeName("Sig") - sig.Reason = core.MakeString("Test1") - if err := handler.InitSignature(sig); err != nil { - return nil, nil, err - } - a.addNewObjects(sig.container) - signatureField = NewPdfFieldSignature() - fields := append(acroForm.AllFields(), signatureField.PdfField) + fields := append(acroForm.AllFields(), field.PdfField) acroForm.Fields = &fields + a.ReplaceAcroForm(acroForm) + // Replace original page. procPage(page) - - signatureField.FT = core.MakeName("Sig") - signatureField.V = sig - signatureField.T = core.MakeString("Signature1") - signatureField.F = core.MakeInteger(132) - signatureField.P = page.ToPdfObject() - signatureField.Rect = core.MakeArray( - core.MakeInteger(0), - core.MakeInteger(0), - core.MakeInteger(0), - core.MakeInteger(0), - ) - - // Add the signature field to the page annotations. - page.Annotations = append(page.Annotations, signatureField.PdfAnnotationWidget.PdfAnnotation) a.pages[pageIndex] = page - // Update acroform. - a.ReplaceAcroForm(acroForm) - return acroForm, signatureField, nil + return nil } // ReplaceAcroForm replaces the acrobat form. It appends a new form to the Pdf which diff --git a/pdf/model/appender_test.go b/pdf/model/appender_test.go index f017a78d..135070d1 100644 --- a/pdf/model/appender_test.go +++ b/pdf/model/appender_test.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "testing" + "time" "golang.org/x/crypto/pkcs12" @@ -447,15 +448,33 @@ func TestAppenderSignPage4(t *testing.T) { t.Errorf("Fail: %v\n", err) return } - _, appearance, err := appender.Sign(1, handler) - if err != nil { + + // Create signature field and appearance. + signature := model.NewPdfSignature(handler) + signature.SetName("Test Appender") + signature.SetReason("TestAppenderSignPage4") + signature.SetDate(time.Now(), "") + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Signature1") + + widget := model.NewPdfAnnotationWidget() + widget.F = core.MakeInteger(132) + widget.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + widget.Parent = sigField.GetContainingPdfObject() + + sigField.Annotations = append(sigField.Annotations, widget) + + if err = appender.Sign(1, sigField); err != nil { t.Errorf("Fail: %v\n", err) return } - appearance.V.Name = core.MakeString("Test Appender") - appearance.V.Reason = core.MakeString("TestAppenderSignPage4") - err = appender.WriteToFile(tempFile("appender_sign_page_4.pdf")) if err != nil { t.Errorf("Fail: %v\n", err) @@ -513,16 +532,34 @@ func TestAppenderSignMultiple(t *testing.T) { f.Close() return } - _, appearance, err := appender.Sign(1, handler) - if err != nil { + + // Create signature field and appearance. + signature := model.NewPdfSignature(handler) + signature.SetName(fmt.Sprintf("Test Appender - Round %d", i+1)) + signature.SetReason("TestAppenderSignPage4") + signature.SetDate(time.Now(), "") + + sigField := model.NewPdfFieldSignature(signature) + sigField.T = core.MakeString("Signature1") + + widget := model.NewPdfAnnotationWidget() + widget.F = core.MakeInteger(132) + widget.Rect = core.MakeArray( + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + core.MakeInteger(0), + ) + widget.Parent = sigField.GetContainingPdfObject() + + sigField.Annotations = append(sigField.Annotations, widget) + + if err = appender.Sign(1, sigField); err != nil { t.Errorf("Fail: %v\n", err) f.Close() return } - appearance.V.Name = core.MakeString(fmt.Sprintf("Test Appender - Round %d", i+1)) - appearance.V.Reason = core.MakeString("TestAppenderSignPage4") - outPath := tempFile(fmt.Sprintf("appender_sign_multiple_%d.pdf", i+1)) err = appender.WriteToFile(outPath) diff --git a/pdf/model/fields.go b/pdf/model/fields.go index f5876a67..70b2dac2 100644 --- a/pdf/model/fields.go +++ b/pdf/model/fields.go @@ -433,7 +433,6 @@ func (ch *PdfFieldChoice) ToPdfObject() core.PdfObject { // the name of the signer and verifying document contents. type PdfFieldSignature struct { *PdfField - *PdfAnnotationWidget V *PdfSignature Lock *core.PdfIndirectObject @@ -441,22 +440,20 @@ type PdfFieldSignature struct { } // NewPdfFieldSignature returns an initialized signature field. -func NewPdfFieldSignature() *PdfFieldSignature { - sig := &PdfFieldSignature{} - sig.PdfField = NewPdfField() - sig.PdfAnnotationWidget = NewPdfAnnotationWidget() - sig.PdfField.SetContext(sig) - sig.PdfAnnotationWidget.SetContext(sig) - sig.PdfAnnotationWidget.container = sig.PdfField.container - return sig +func NewPdfFieldSignature(signature *PdfSignature) *PdfFieldSignature { + field := &PdfFieldSignature{ + PdfField: NewPdfField(), + V: signature, + } + field.PdfField.SetContext(field) + field.FT = core.MakeName("Sig") + + return field } // ToPdfObject returns an indirect object containing the signature field dictionary. func (sig *PdfFieldSignature) ToPdfObject() core.PdfObject { // Set general field attributes - if sig.PdfAnnotationWidget != nil { - sig.PdfAnnotation.ToPdfObject() - } sig.PdfField.ToPdfObject() // Handle signature field specific attributes diff --git a/pdf/model/signature.go b/pdf/model/signature.go index ef39c82a..0e26e6b8 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -7,6 +7,8 @@ package model import ( "bytes" + "errors" + "time" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" @@ -80,6 +82,7 @@ func (d *pdfSignDictionary) WriteString() string { type PdfSignature struct { Handler SignatureHandler container *core.PdfIndirectObject + // Type: Sig/DocTimeStamp Type *core.PdfObjectName Filter *core.PdfObjectName @@ -102,25 +105,20 @@ type PdfSignature struct { } // NewPdfSignature creates a new PdfSignature object. -func NewPdfSignature() *PdfSignature { - sig := &PdfSignature{} - sigDict := &pdfSignDictionary{ +func NewPdfSignature(handler SignatureHandler) *PdfSignature { + sig := &PdfSignature{ + Type: core.MakeName("Sig"), + Handler: handler, + } + + dict := &pdfSignDictionary{ PdfObjectDictionary: core.MakeDict(), - handler: &sig.Handler, + handler: &handler, signature: sig, } - sig.container = core.MakeIndirectObject(sigDict) - return sig -} -// PdfSignatureReference represents a signature reference dictionary. -// (Table 253 - p. 477 in PDF32000_2008). -type PdfSignatureReference struct { - // Type: SigRef - TransformMethod *core.PdfObjectName - TransformParams *core.PdfObjectDictionary - Data core.PdfObject - DigestMethod *core.PdfObjectName + sig.container = core.MakeIndirectObject(dict) + return sig } // GetContainingPdfObject implements interface PdfModel. @@ -128,6 +126,34 @@ func (sig *PdfSignature) GetContainingPdfObject() core.PdfObject { return sig.container } +// SetName sets the `Name` field of the signature. +func (sig *PdfSignature) SetName(name string) { + sig.Name = core.MakeString(name) +} + +// SetDate sets the `M` field of the signature. +func (sig *PdfSignature) SetDate(date time.Time, format string) { + if format == "" { + format = "D:20060102150405-07'00'" + } + + sig.M = core.MakeString(date.Format(format)) +} + +// SetReason sets the `Reason` field of the signature. +func (sig *PdfSignature) SetReason(reason string) { + sig.Reason = core.MakeString(reason) +} + +// Initialize initializes the PdfSignature. +func (sig *PdfSignature) Initialize() error { + if sig.Handler == nil { + return errors.New("signature handler cannot be nil") + } + + return sig.Handler.InitSignature(sig) +} + // ToPdfObject implements interface PdfModel. func (sig *PdfSignature) ToPdfObject() core.PdfObject { container := sig.container @@ -213,146 +239,3 @@ func (r *PdfReader) newPdfSignatureFromIndirect(container *core.PdfIndirectObjec return sig, nil } - -// PdfSignatureField represents a form field that contains a digital signature. -// (12.7.4.5 - Signature Fields p. 454 in PDF32000_2008). -// -// The signature form field serves two primary purposes. 1. Define the form field that will provide the -// visual signing properties for display but may also hold information needed when the actual signing -// takes place such as signature method. This carries information from the author of the document to the -// software that later does signing. -// -// Filling in (signing) the signature field entails updating at least the V entry and usually the AP entry of the -// associated widget annotation. (Exporting a signature field exports the T, V, AP entries) -// -// The annotation rectangle (Rect) in such a dictionary shall give the position of the field on its page. Signature -// fields that are not intended to be visible shall have an annotation rectangle that has zero height and width. PDF -// processors shall treat such signatures as not visible. PDF processors shall also treat signatures as not -// visible if either the Hidden bit or the NoView bit of the F entry is true -// -// The location of a signature within a document can have a bearing on its legal meaning. For this reason, -// signature fields shall never refer to more than one annotation. -type PdfSignatureField struct { - container *core.PdfIndirectObject - - V *PdfSignature - Lock *core.PdfIndirectObject - SV *core.PdfIndirectObject - Kids *core.PdfObjectArray -} - -// NewPdfSignatureField prepares a PdfSignatureField from a PdfSignature. -func NewPdfSignatureField(signature *PdfSignature) *PdfSignatureField { - return &PdfSignatureField{ - V: signature, - container: core.MakeIndirectObject(core.MakeDict()), - } -} - -// ToPdfObject implements interface PdfModel. -func (sf *PdfSignatureField) ToPdfObject() core.PdfObject { - container := sf.container - dict := container.PdfObject.(*core.PdfObjectDictionary) - - // Set fields. - if sf.V != nil { - dict.Set("V", sf.V.ToPdfObject()) - } - - dict.Set("FT", core.MakeName("Sig")) - dict.SetIfNotNil("Lock", sf.Lock) - dict.SetIfNotNil("SV", sf.SV) - dict.SetIfNotNil("Kids", sf.Kids) - // Other standard fields... - - return container -} - -// PdfSignatureFieldLock represents signature field lock dictionary. -// (Table 233 - p. 455 in PDF32000_2008). -type PdfSignatureFieldLock struct { - Type core.PdfObject - Action *core.PdfObjectName - Fields *core.PdfObjectArray - P *core.PdfObjectInteger -} - -// PdfSignatureFieldSeed represents signature field seed value dictionary. -// (Table 234 - p. 455 in PDF32000_2008). -type PdfSignatureFieldSeed struct { - container *core.PdfIndirectObject - - Ff *core.PdfObjectInteger - Filter *core.PdfObjectName - SubFilter *core.PdfObjectArray - DigestMethod *core.PdfObjectArray - V *core.PdfObjectInteger - Cert core.PdfObject - Reasons *core.PdfObjectArray - MDP *core.PdfObjectDictionary - TimeStamp *core.PdfObjectDictionary - LegalAttestation *core.PdfObjectArray - AddRevInfo *core.PdfObjectBool - LockDocument *core.PdfObjectName - AppearanceFilter *core.PdfObjectString -} - -func (pss *PdfSignatureFieldSeed) ToPdfObject() core.PdfObject { - container := pss.container - dict := container.PdfObject.(*core.PdfObjectDictionary) - - dict.SetIfNotNil("Ff", pss.Ff) - dict.SetIfNotNil("Filter", pss.Filter) - dict.SetIfNotNil("SubFilter", pss.SubFilter) - dict.SetIfNotNil("DigestMethod", pss.DigestMethod) - dict.SetIfNotNil("V", pss.V) - dict.SetIfNotNil("Cert", pss.Cert) - dict.SetIfNotNil("Reasons", pss.Reasons) - dict.SetIfNotNil("MDP", pss.MDP) - dict.SetIfNotNil("TimeStamp", pss.TimeStamp) - dict.SetIfNotNil("LegalAttestation", pss.LegalAttestation) - dict.SetIfNotNil("AddRevInfo", pss.AddRevInfo) - dict.SetIfNotNil("LockDocument", pss.LockDocument) - dict.SetIfNotNil("AppearanceFilter", pss.AppearanceFilter) - - return container -} - -// PdfCertificateSeed represents certificate seed value dictionary. -// (Table 235 - p. 457 in PDF32000_2008). -type PdfCertificateSeed struct { - container *core.PdfIndirectObject - // Type - Ff *core.PdfObjectInteger - Subject *core.PdfObjectArray - SignaturePolicyOID *core.PdfObjectString - SignaturePolicyHashValue *core.PdfObjectString - SignaturePolicyHashAlgorithm *core.PdfObjectName - SignaturePolicyCommitmentType *core.PdfObjectArray - SubjectDN *core.PdfObjectArray - KeyUsage *core.PdfObjectArray - Issuer *core.PdfObjectArray - OID *core.PdfObjectArray - URL *core.PdfObjectString - URLType *core.PdfObjectName -} - -func (pcs *PdfCertificateSeed) ToPdfObject() core.PdfObject { - container := pcs.container - dict := container.PdfObject.(*core.PdfObjectDictionary) - - dict.SetIfNotNil("Ff", pcs.Ff) - dict.SetIfNotNil("Subject", pcs.Subject) - dict.SetIfNotNil("SignaturePolicyOID", pcs.SignaturePolicyOID) - dict.SetIfNotNil("SignaturePolicyHashValue", pcs.SignaturePolicyHashValue) - dict.SetIfNotNil("SignaturePolicyHashAlgorithm", pcs.SignaturePolicyHashAlgorithm) - dict.SetIfNotNil("SignaturePolicyCommitmentType", pcs.SignaturePolicyCommitmentType) - dict.SetIfNotNil("SubjectDN", pcs.SubjectDN) - dict.SetIfNotNil("KeyUsage", pcs.KeyUsage) - dict.SetIfNotNil("Issuer", pcs.Issuer) - dict.SetIfNotNil("OID", pcs.OID) - dict.SetIfNotNil("URL", pcs.URL) - dict.SetIfNotNil("URLType", pcs.URLType) - - return container -} From 38d71b46b40585273ff80e96531e78c788bac39b Mon Sep 17 00:00:00 2001 From: Adrian-George Bostan Date: Tue, 19 Feb 2019 17:59:38 +0200 Subject: [PATCH 20/20] Minor code documentation improvements --- pdf/model/appender.go | 39 ++++++++++++--------- pdf/model/sighandler/sighandler_rsa_sha1.go | 12 ------- pdf/model/signature.go | 5 +++ 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/pdf/model/appender.go b/pdf/model/appender.go index c7360d3d..dbe8e4e8 100644 --- a/pdf/model/appender.go +++ b/pdf/model/appender.go @@ -140,8 +140,8 @@ func (a *PdfAppender) addNewObjects(obj core.PdfObject) { } switch v := obj.(type) { case *core.PdfIndirectObject: - // Check the object is changing. - // If the indirect object has not the read-only parser then the object is changed. + // If the current parser is different from the read-only parser, then + // the object has changed. if v.GetParser() != a.roReader.parser { a.newObjects = append(a.newObjects, obj) a.hasNewObject[obj] = struct{}{} @@ -156,22 +156,25 @@ func (a *PdfAppender) addNewObjects(obj core.PdfObject) { a.addNewObjects(v.Get(key)) } case *core.PdfObjectStreams: - // Check the object is changing. - // If the indirect object has not the readonly parser then the object is changed. + // If the current parser is different from the read-only parser, then + // the object has changed. if v.GetParser() != a.roReader.parser { for _, o := range v.Elements() { a.addNewObjects(o) } } case *core.PdfObjectStream: - // Check the object is changing. - // If the indirect object has the readonly parser then the object is not changed. - if v.GetParser() == a.roReader.parser { + // If the current parser is different from the read-only parser, then + // the object has changed. + parser := v.GetParser() + if parser == a.roReader.parser { return } - // If the indirect object has not the origin parser then the object may be changed orr not. - if v.GetParser() == a.Reader.parser { - // Check data is not changed. + + // If the current parser is different from the parser of the reader, + // then the object may have changed. + if parser == a.Reader.parser { + // Check if data has changed. if streamObj, err := a.roReader.parser.LookupByReference(v.PdfObjectReference); err == nil { var isNotChanged bool if stream, ok := core.GetStream(streamObj); ok && bytes.Equal(stream.Stream, v.Stream) { @@ -379,8 +382,9 @@ func (a *PdfAppender) ReplacePage(pageNum int, page *PdfPage) { } } -// Sign signs a specific page with a digital signature using a specified signature handler. -// Returns a PdfFieldSignature that can be used to customize the signature appearance. +// Sign signs a specific page with a digital signature. +// The signature field parameter must have a valid signature dictionary +// specified by its V field. func (a *PdfAppender) Sign(pageNum int, field *PdfFieldSignature) error { if field == nil { return errors.New("signature field cannot be nil") @@ -388,7 +392,7 @@ func (a *PdfAppender) Sign(pageNum int, field *PdfFieldSignature) error { signature := field.V if signature == nil { - return errors.New("field signature cannot be nil") + return errors.New("signature dictionary cannot be nil") } // Get a copy of the selected page. @@ -424,6 +428,7 @@ func (a *PdfAppender) Sign(pageNum int, field *PdfFieldSignature) error { acroForm.DR.Font = n2ResourcesFont fields := append(acroForm.AllFields(), field.PdfField) + acroForm.Fields = &fields a.ReplaceAcroForm(acroForm) @@ -441,7 +446,7 @@ func (a *PdfAppender) ReplaceAcroForm(acroForm *PdfAcroForm) { } // Write writes the Appender output to io.Writer. -// It can only be called once and further invokations will result in an error. +// It can only be called once and further invocations will result in an error. func (a *PdfAppender) Write(w io.Writer) error { if a.written { return errors.New("appender write can only be invoked once") @@ -601,9 +606,9 @@ func (a *PdfAppender) Write(w io.Writer) error { writerW := w if hasSigDict { - // For signatures, we need to write twice. First to find the byte offset of the Contents and then - // dynamically update file with the signature and ByteRange. - // Thus we create an empty buffer to write to and then at the e + // For signatures, we need to write twice. First to find the byte offset + // of the Contents and then dynamically update the file with the + // signature and ByteRange. writerW = bytes.NewBuffer(nil) } diff --git a/pdf/model/sighandler/sighandler_rsa_sha1.go b/pdf/model/sighandler/sighandler_rsa_sha1.go index 8b924947..f0cc4e8c 100644 --- a/pdf/model/sighandler/sighandler_rsa_sha1.go +++ b/pdf/model/sighandler/sighandler_rsa_sha1.go @@ -42,7 +42,6 @@ func (a *adobeX509RSASHA1) InitSignature(sig *model.PdfSignature) error { handler := *a sig.Handler = &handler sig.Filter = core.MakeName("Adobe.PPKLite") - //sig.Filter = core.MakeName("Adobe.PPKMS") sig.SubFilter = core.MakeName("adbe.x509.rsa_sha1") sig.Cert = core.MakeString(string(handler.certificate.Raw)) sig.Reference = nil @@ -57,17 +56,6 @@ func (a *adobeX509RSASHA1) InitSignature(sig *model.PdfSignature) error { func getHashFromSignatureAlgorithm(sa x509.SignatureAlgorithm) (crypto.Hash, bool) { return crypto.SHA1, true - /* - switch sa { - case x509.SHA1WithRSA: - return crypto.SHA1, true - case x509.SHA256WithRSA: - return crypto.SHA256, true - case x509.SHA512WithRSA: - return crypto.SHA512, true - } - return crypto.MD5, false - */ } func (a *adobeX509RSASHA1) getCertificate(sig *model.PdfSignature) (*x509.Certificate, error) { diff --git a/pdf/model/signature.go b/pdf/model/signature.go index 0e26e6b8..601a45ff 100644 --- a/pdf/model/signature.go +++ b/pdf/model/signature.go @@ -145,6 +145,11 @@ func (sig *PdfSignature) SetReason(reason string) { sig.Reason = core.MakeString(reason) } +// SetLocation sets the `Location` field of the signature. +func (sig *PdfSignature) SetLocation(location string) { + sig.Location = core.MakeString(location) +} + // Initialize initializes the PdfSignature. func (sig *PdfSignature) Initialize() error { if sig.Handler == nil {