diff --git a/certstore/LICENSE.md b/certstore/LICENSE.md new file mode 100644 index 0000000..7800c58 --- /dev/null +++ b/certstore/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Ben Toews. + +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/certstore/README.md b/certstore/README.md new file mode 100644 index 0000000..e68de37 --- /dev/null +++ b/certstore/README.md @@ -0,0 +1,85 @@ +# certstore [![PkgGoDev](https://pkg.go.dev/badge/github.com/github/certstore?tab=doc)](https://pkg.go.dev/github.com/github/certstore?tab=doc) [![Report card](https://goreportcard.com/badge/github.com/github/certstore)](https://goreportcard.com/report/github.com/github/certstore) + +[![Test macOS (recent Go versions)]()](https://github.com/github/certstore/actions?query=workflow%3A%22Test+macOS+%28recent+Go+versions%29%22) +[![Test Windows (recent Go versions)]()](https://github.com/github/certstore/actions?query=workflow%3A%22Test+Windows+%28recent+Go+versions%29%22) + +Certstore is a Go library for accessing user identities stored in platform certificate stores. On Windows and macOS, certstore can enumerate user identities and sign messages with their private keys. + +## Example + +```go +package main + +import ( + "crypto" + "encoding/hex" + "errors" + "fmt" + + "crypto/rand" + "crypto/sha256" + + "github.com/github/certstore" +) + +func main() { + sig, err := signWithMyIdentity("Ben Toews", "hello, world!") + if err != nil { + panic(err) + } + + fmt.Println(hex.EncodeToString(sig)) +} + +func signWithMyIdentity(cn, msg string) ([]byte, error) { + // Open the certificate store for use. This must be Close()'ed once you're + // finished with the store and any identities it contains. + store, err := certstore.Open() + if err != nil { + return nil, err + } + defer store.Close() + + // Get an Identity slice, containing every identity in the store. Each of + // these must be Close()'ed when you're done with them. + idents, err := store.Identities() + if err != nil { + return nil, err + } + + // Iterate through the identities, looking for the one we want. + var me certstore.Identity + for _, ident := range idents { + defer ident.Close() + + crt, errr := ident.Certificate() + if errr != nil { + return nil, errr + } + + if crt.Subject.CommonName == "Ben Toews" { + me = ident + } + } + + if me == nil { + return nil, errors.New("Couldn't find my identity") + } + + // Get a crypto.Signer for the identity. + signer, err := me.Signer() + if err != nil { + return nil, err + } + + // Digest and sign our message. + digest := sha256.Sum256([]byte(msg)) + signature, err := signer.Sign(rand.Reader, digest[:], crypto.SHA256) + if err != nil { + return nil, err + } + + return signature, nil +} + +``` diff --git a/certstore/certstore.go b/certstore/certstore.go new file mode 100644 index 0000000..780aa7c --- /dev/null +++ b/certstore/certstore.go @@ -0,0 +1,49 @@ +package certstore + +import ( + "crypto" + "crypto/x509" + "errors" +) + +var ( + // ErrUnsupportedHash is returned by Signer.Sign() when the provided hash + // algorithm isn't supported. + ErrUnsupportedHash = errors.New("unsupported hash algorithm") +) + +// Open opens the system's certificate store. +func Open() (Store, error) { + return openStore() +} + +// Store represents the system's certificate store. +type Store interface { + // Identities gets a list of identities from the store. + Identities() ([]Identity, error) + + // Import imports a PKCS#12 (PFX) blob containing a certificate and private + // key. + Import(data []byte, password string) error + + // Close closes the store. + Close() +} + +// Identity is a X.509 certificate and its corresponding private key. +type Identity interface { + // Certificate gets the identity's certificate. + Certificate() (*x509.Certificate, error) + + // CertificateChain attempts to get the identity's full certificate chain. + CertificateChain() ([]*x509.Certificate, error) + + // Signer gets a crypto.Signer that uses the identity's private key. + Signer() (crypto.Signer, error) + + // Delete deletes this identity from the system. + Delete() error + + // Close any manually managed memory held by the Identity. + Close() +} diff --git a/certstore/certstore_darwin.go b/certstore/certstore_darwin.go new file mode 100644 index 0000000..f4797c2 --- /dev/null +++ b/certstore/certstore_darwin.go @@ -0,0 +1,485 @@ +package certstore + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework CoreFoundation -framework Security +#include +#include +*/ +import "C" +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "errors" + "fmt" + "io" + "unsafe" +) + +// work around https://golang.org/doc/go1.10#cgo +// in go>=1.10 CFTypeRefs are translated to uintptrs instead of pointers. +var ( + nilCFDictionaryRef C.CFDictionaryRef + nilSecCertificateRef C.SecCertificateRef + nilCFArrayRef C.CFArrayRef + nilCFDataRef C.CFDataRef + nilCFErrorRef C.CFErrorRef + nilCFStringRef C.CFStringRef + nilSecIdentityRef C.SecIdentityRef + nilSecKeyRef C.SecKeyRef + nilCFAllocatorRef C.CFAllocatorRef +) + +// macStore is a bogus type. We have to explicitly open/close the store on +// windows, so we provide those methods here too. +type macStore int + +// openStore is a function for opening a macStore. +func openStore() (macStore, error) { + return macStore(0), nil +} + +// Identities implements the Store interface. +func (s macStore) Identities() ([]Identity, error) { + query := mapToCFDictionary(map[C.CFTypeRef]C.CFTypeRef{ + C.CFTypeRef(C.kSecClass): C.CFTypeRef(C.kSecClassIdentity), + C.CFTypeRef(C.kSecReturnRef): C.CFTypeRef(C.kCFBooleanTrue), + C.CFTypeRef(C.kSecMatchLimit): C.CFTypeRef(C.kSecMatchLimitAll), + }) + if query == nilCFDictionaryRef { + return nil, errors.New("error creating CFDictionary") + } + defer C.CFRelease(C.CFTypeRef(query)) + + var absResult C.CFTypeRef + if err := osStatusError(C.SecItemCopyMatching(query, &absResult)); err != nil { + if err == errSecItemNotFound { + return []Identity{}, nil + } + + return nil, err + } + defer C.CFRelease(C.CFTypeRef(absResult)) + + // don't need to release aryResult since the abstract result is released above. + aryResult := C.CFArrayRef(absResult) + + // identRefs aren't owned by us initially. newMacIdentity retains them. + n := C.CFArrayGetCount(aryResult) + identRefs := make([]C.CFTypeRef, n) + C.CFArrayGetValues(aryResult, C.CFRange{0, n}, (*unsafe.Pointer)(unsafe.Pointer(&identRefs[0]))) + + idents := make([]Identity, 0, n) + for _, identRef := range identRefs { + idents = append(idents, newMacIdentity(C.SecIdentityRef(identRef))) + } + + return idents, nil +} + +// Import implements the Store interface. +func (s macStore) Import(data []byte, password string) error { + cdata, err := bytesToCFData(data) + if err != nil { + return err + } + defer C.CFRelease(C.CFTypeRef(cdata)) + + cpass := stringToCFString(password) + defer C.CFRelease(C.CFTypeRef(cpass)) + + cops := mapToCFDictionary(map[C.CFTypeRef]C.CFTypeRef{ + C.CFTypeRef(C.kSecImportExportPassphrase): C.CFTypeRef(cpass), + }) + if cops == nilCFDictionaryRef { + return errors.New("error creating CFDictionary") + } + defer C.CFRelease(C.CFTypeRef(cops)) + + var cret C.CFArrayRef + if err := osStatusError(C.SecPKCS12Import(cdata, cops, &cret)); err != nil { + return err + } + defer C.CFRelease(C.CFTypeRef(cret)) + + return nil +} + +// Close implements the Store interface. +func (s macStore) Close() {} + +// macIdentity implements the Identity interface. +type macIdentity struct { + ref C.SecIdentityRef + kref C.SecKeyRef + cref C.SecCertificateRef + crt *x509.Certificate + chain []*x509.Certificate +} + +func newMacIdentity(ref C.SecIdentityRef) *macIdentity { + C.CFRetain(C.CFTypeRef(ref)) + return &macIdentity{ref: ref} +} + +// Certificate implements the Identity interface. +func (i *macIdentity) Certificate() (*x509.Certificate, error) { + certRef, err := i.getCertRef() + if err != nil { + return nil, err + } + + crt, err := exportCertRef(certRef) + if err != nil { + return nil, err + } + + i.crt = crt + + return i.crt, nil +} + +// CertificateChain implements the Identity interface. +func (i *macIdentity) CertificateChain() ([]*x509.Certificate, error) { + if i.chain != nil { + return i.chain, nil + } + + certRef, err := i.getCertRef() + if err != nil { + return nil, err + } + + policy := C.SecPolicyCreateSSL(0, nilCFStringRef) + + var trustRef C.SecTrustRef + if err := osStatusError(C.SecTrustCreateWithCertificates(C.CFTypeRef(certRef), C.CFTypeRef(policy), &trustRef)); err != nil { + return nil, err + } + defer C.CFRelease(C.CFTypeRef(trustRef)) + + var status C.SecTrustResultType + if err := osStatusError(C.SecTrustEvaluate(trustRef, &status)); err != nil { + return nil, err + } + + var ( + nchain = C.SecTrustGetCertificateCount(trustRef) + chain = make([]*x509.Certificate, 0, int(nchain)) + ) + + for i := C.CFIndex(0); i < nchain; i++ { + // TODO: do we need to release these? + chainCertref := C.SecTrustGetCertificateAtIndex(trustRef, i) + if chainCertref == nilSecCertificateRef { + return nil, errors.New("nil certificate in chain") + } + + chainCert, err := exportCertRef(chainCertref) + if err != nil { + return nil, err + } + + chain = append(chain, chainCert) + } + + i.chain = chain + + return chain, nil +} + +// Signer implements the Identity interface. +func (i *macIdentity) Signer() (crypto.Signer, error) { + // pre-load the certificate so Public() is less likely to return nil + // unexpectedly. + if _, err := i.Certificate(); err != nil { + return nil, err + } + + return i, nil +} + +// Delete implements the Identity interface. +func (i *macIdentity) Delete() error { + itemList := []C.SecIdentityRef{i.ref} + itemListPtr := (*unsafe.Pointer)(unsafe.Pointer(&itemList[0])) + citemList := C.CFArrayCreate(nilCFAllocatorRef, itemListPtr, 1, nil) + if citemList == nilCFArrayRef { + return errors.New("error creating CFArray") + } + defer C.CFRelease(C.CFTypeRef(citemList)) + + query := mapToCFDictionary(map[C.CFTypeRef]C.CFTypeRef{ + C.CFTypeRef(C.kSecClass): C.CFTypeRef(C.kSecClassIdentity), + C.CFTypeRef(C.kSecMatchItemList): C.CFTypeRef(citemList), + }) + if query == nilCFDictionaryRef { + return errors.New("error creating CFDictionary") + } + defer C.CFRelease(C.CFTypeRef(query)) + + if err := osStatusError(C.SecItemDelete(query)); err != nil { + return err + } + + return nil +} + +// Close implements the Identity interface. +func (i *macIdentity) Close() { + if i.ref != nilSecIdentityRef { + C.CFRelease(C.CFTypeRef(i.ref)) + i.ref = nilSecIdentityRef + } + + if i.kref != nilSecKeyRef { + C.CFRelease(C.CFTypeRef(i.kref)) + i.kref = nilSecKeyRef + } + + if i.cref != nilSecCertificateRef { + C.CFRelease(C.CFTypeRef(i.cref)) + i.cref = nilSecCertificateRef + } +} + +// Public implements the crypto.Signer interface. +func (i *macIdentity) Public() crypto.PublicKey { + cert, err := i.Certificate() + if err != nil { + return nil + } + + return cert.PublicKey +} + +// Sign implements the crypto.Signer interface. +func (i *macIdentity) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + hash := opts.HashFunc() + + if len(digest) != hash.Size() { + return nil, errors.New("bad digest for hash") + } + + kref, err := i.getKeyRef() + if err != nil { + return nil, err + } + + cdigest, err := bytesToCFData(digest) + if err != nil { + return nil, err + } + defer C.CFRelease(C.CFTypeRef(cdigest)) + + algo, err := i.getAlgo(hash) + if err != nil { + return nil, err + } + + // sign the digest + var cerr C.CFErrorRef + csig := C.SecKeyCreateSignature(kref, algo, cdigest, &cerr) + + if err := cfErrorError(cerr); err != nil { + defer C.CFRelease(C.CFTypeRef(cerr)) + + return nil, err + } + + if csig == nilCFDataRef { + return nil, errors.New("nil signature from SecKeyCreateSignature") + } + + defer C.CFRelease(C.CFTypeRef(csig)) + + sig := cfDataToBytes(csig) + + return sig, nil +} + +// getAlgo decides which algorithm to use with this key type for the given hash. +func (i *macIdentity) getAlgo(hash crypto.Hash) (algo C.SecKeyAlgorithm, err error) { + var crt *x509.Certificate + if crt, err = i.Certificate(); err != nil { + return + } + + switch crt.PublicKey.(type) { + case *ecdsa.PublicKey: + switch hash { + case crypto.SHA1: + algo = C.kSecKeyAlgorithmECDSASignatureDigestX962SHA1 + case crypto.SHA256: + algo = C.kSecKeyAlgorithmECDSASignatureDigestX962SHA256 + case crypto.SHA384: + algo = C.kSecKeyAlgorithmECDSASignatureDigestX962SHA384 + case crypto.SHA512: + algo = C.kSecKeyAlgorithmECDSASignatureDigestX962SHA512 + default: + err = ErrUnsupportedHash + } + case *rsa.PublicKey: + switch hash { + case crypto.SHA1: + algo = C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA1 + case crypto.SHA256: + algo = C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA256 + case crypto.SHA384: + algo = C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA384 + case crypto.SHA512: + algo = C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512 + default: + err = ErrUnsupportedHash + } + default: + err = errors.New("unsupported key type") + } + + return +} + +// getKeyRef gets the SecKeyRef for this identity's pricate key. +func (i *macIdentity) getKeyRef() (C.SecKeyRef, error) { + if i.kref != nilSecKeyRef { + return i.kref, nil + } + + var keyRef C.SecKeyRef + if err := osStatusError(C.SecIdentityCopyPrivateKey(i.ref, &keyRef)); err != nil { + return nilSecKeyRef, err + } + + i.kref = keyRef + + return i.kref, nil +} + +// getCertRef gets the SecCertificateRef for this identity's certificate. +func (i *macIdentity) getCertRef() (C.SecCertificateRef, error) { + if i.cref != nilSecCertificateRef { + return i.cref, nil + } + + var certRef C.SecCertificateRef + if err := osStatusError(C.SecIdentityCopyCertificate(i.ref, &certRef)); err != nil { + return nilSecCertificateRef, err + } + + i.cref = certRef + + return i.cref, nil +} + +// exportCertRef gets a *x509.Certificate for the given SecCertificateRef. +func exportCertRef(certRef C.SecCertificateRef) (*x509.Certificate, error) { + derRef := C.SecCertificateCopyData(certRef) + if derRef == nilCFDataRef { + return nil, errors.New("error getting certificate from identity") + } + defer C.CFRelease(C.CFTypeRef(derRef)) + + der := cfDataToBytes(derRef) + crt, err := x509.ParseCertificate(der) + if err != nil { + return nil, err + } + + return crt, nil +} + +// stringToCFString converts a Go string to a CFStringRef. +func stringToCFString(gostr string) C.CFStringRef { + cstr := C.CString(gostr) + defer C.free(unsafe.Pointer(cstr)) + + return C.CFStringCreateWithCString(nilCFAllocatorRef, cstr, C.kCFStringEncodingUTF8) +} + +// mapToCFDictionary converts a Go map[C.CFTypeRef]C.CFTypeRef to a +// CFDictionaryRef. +func mapToCFDictionary(gomap map[C.CFTypeRef]C.CFTypeRef) C.CFDictionaryRef { + var ( + n = len(gomap) + keys = make([]unsafe.Pointer, 0, n) + values = make([]unsafe.Pointer, 0, n) + ) + + for k, v := range gomap { + keys = append(keys, unsafe.Pointer(k)) + values = append(values, unsafe.Pointer(v)) + } + + return C.CFDictionaryCreate(nilCFAllocatorRef, &keys[0], &values[0], C.CFIndex(n), nil, nil) +} + +// cfDataToBytes converts a CFDataRef to a Go byte slice. +func cfDataToBytes(cfdata C.CFDataRef) []byte { + nBytes := C.CFDataGetLength(cfdata) + bytesPtr := C.CFDataGetBytePtr(cfdata) + return C.GoBytes(unsafe.Pointer(bytesPtr), C.int(nBytes)) +} + +// bytesToCFData converts a Go byte slice to a CFDataRef. +func bytesToCFData(gobytes []byte) (C.CFDataRef, error) { + var ( + cptr = (*C.UInt8)(nil) + clen = C.CFIndex(len(gobytes)) + ) + + if len(gobytes) > 0 { + cptr = (*C.UInt8)(&gobytes[0]) + } + + cdata := C.CFDataCreate(nilCFAllocatorRef, cptr, clen) + if cdata == nilCFDataRef { + return nilCFDataRef, errors.New("error creatin cfdata") + } + + return cdata, nil +} + +// osStatus wraps a C.OSStatus +type osStatus C.OSStatus + +const ( + errSecItemNotFound = osStatus(C.errSecItemNotFound) +) + +// osStatusError returns an error for an OSStatus unless it is errSecSuccess. +func osStatusError(s C.OSStatus) error { + if s == C.errSecSuccess { + return nil + } + + return osStatus(s) +} + +// Error implements the error interface. +func (s osStatus) Error() string { + return fmt.Sprintf("OSStatus %d", s) +} + +// cfErrorError returns an error for a CFErrorRef unless it is nil. +func cfErrorError(cerr C.CFErrorRef) error { + if cerr == nilCFErrorRef { + return nil + } + + code := int(C.CFErrorGetCode(cerr)) + + if cdescription := C.CFErrorCopyDescription(cerr); cdescription != nilCFStringRef { + defer C.CFRelease(C.CFTypeRef(cdescription)) + + if cstr := C.CFStringGetCStringPtr(cdescription, C.kCFStringEncodingUTF8); cstr != nil { + str := C.GoString(cstr) + + return fmt.Errorf("CFError %d (%s)", code, str) + } + + } + + return fmt.Errorf("CFError %d", code) +} diff --git a/certstore/certstore_linux.go b/certstore/certstore_linux.go new file mode 100644 index 0000000..9da0b14 --- /dev/null +++ b/certstore/certstore_linux.go @@ -0,0 +1,14 @@ +package certstore + +import "errors" + +// This will hopefully give a compiler error that will hint at the fact that +// this package isn't designed to work on Linux. +func init() { + CERTSTORE_DOESNT_WORK_ON_LINIX +} + +// Implement this function, just to silence other compiler errors. +func openStore() (Store, error) { + return nil, errors.New("certstore only works on macOS and Windows") +} diff --git a/certstore/certstore_test.go b/certstore/certstore_test.go new file mode 100644 index 0000000..84b22b6 --- /dev/null +++ b/certstore/certstore_test.go @@ -0,0 +1,296 @@ +package certstore + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "crypto/x509" + "testing" + + "github.com/github/smimesign/fakeca" +) + +func TestImportDeleteRSA(t *testing.T) { + ImportDeleteHelper(t, leafRSA) +} + +func TestImportDeleteECDSA(t *testing.T) { + ImportDeleteHelper(t, leafEC) +} + +// ImportDeleteHelper is an abstraction for testing identity Import()/Delete(). +func ImportDeleteHelper(t *testing.T, i *fakeca.Identity) { + withStore(t, func(store Store) { + // Import an identity + if err := store.Import(i.PFX("asdf"), "asdf"); err != nil { + t.Fatal(err) + } + + // Look for our imported identity + idents, err := store.Identities() + if err != nil { + t.Fatal(err) + } + for _, ident := range idents { + defer ident.Close() + } + + var found Identity + for _, ident := range idents { + crt, errr := ident.Certificate() + if errr != nil { + t.Fatal(errr) + } + + if i.Certificate.Equal(crt) { + if found != nil { + t.Fatal("duplicate identity imported") + } + + found = ident + } + } + if found == nil { + t.Fatal("imported identity not found") + } + + // Delete it + if err = found.Delete(); err != nil { + t.Fatal(err) + } + + // Look for our deleted identity + idents, err = store.Identities() + if err != nil { + t.Fatal(err) + } + for _, ident := range idents { + defer ident.Close() + } + + found = nil + for _, ident := range idents { + crt, err := ident.Certificate() + if err != nil { + t.Fatal(err) + } + + if i.Certificate.Equal(crt) { + found = ident + } + } + if found != nil { + t.Fatal("imported identity not deleted") + } + }) +} + +func TestSignerRSA(t *testing.T) { + rsaPriv, ok := leafRSA.PrivateKey.(*rsa.PrivateKey) + if !ok { + t.Fatal("expected priv to be an RSA private key") + } + + withIdentity(t, leafRSA, func(ident Identity) { + signer, err := ident.Signer() + if err != nil { + t.Fatal(err) + } + + pk := signer.Public() + rsaPub, ok := pk.(*rsa.PublicKey) + if !ok { + t.Fatal("expected pk to be an RSA public key") + } + + if rsaPub.E != rsaPriv.E { + t.Fatalf("bad E. Got %d, expected %d", rsaPub.E, rsaPriv.E) + } + + if rsaPub.N.Cmp(rsaPriv.N) != 0 { + t.Fatalf("bad N. Got %s, expected %s", rsaPub.N.Text(16), rsaPriv.N.Text(16)) + } + + // SHA1WithRSA + sha1Digest := sha1.Sum([]byte("hello")) + sig, err := signer.Sign(rand.Reader, sha1Digest[:], crypto.SHA1) + if err != nil { + // SHA1 should be supported by all platforms. + t.Fatal(err) + } + if err = leafRSA.Certificate.CheckSignature(x509.SHA1WithRSA, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + + // SHA256WithRSA + sha256Digest := sha256.Sum256([]byte("hello")) + sig, err = signer.Sign(rand.Reader, sha256Digest[:], crypto.SHA256) + if err == ErrUnsupportedHash { + // Some Windows CSPs may not support this algorithm. Pass... + } else if err != nil { + t.Fatal(err) + } else { + if err = leafRSA.Certificate.CheckSignature(x509.SHA256WithRSA, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + } + + // SHA384WithRSA + sha384Digest := sha512.Sum384([]byte("hello")) + sig, err = signer.Sign(rand.Reader, sha384Digest[:], crypto.SHA384) + if err == ErrUnsupportedHash { + // Some Windows CSPs may not support this algorithm. Pass... + } else if err != nil { + t.Fatal(err) + } else { + if err = leafRSA.Certificate.CheckSignature(x509.SHA384WithRSA, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + } + + // SHA512WithRSA + sha512Digest := sha512.Sum512([]byte("hello")) + sig, err = signer.Sign(rand.Reader, sha512Digest[:], crypto.SHA512) + if err == ErrUnsupportedHash { + // Some Windows CSPs may not support this algorithm. Pass... + } else if err != nil { + t.Fatal(err) + } else { + if err = leafRSA.Certificate.CheckSignature(x509.SHA512WithRSA, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + } + + // Bad digest size + _, err = signer.Sign(rand.Reader, sha1Digest[5:], crypto.SHA1) + if err == nil { + t.Fatal("expected error for bad digest size") + } + + // Unsupported hash + sha224Digest := sha256.Sum224([]byte("hello")) + _, err = signer.Sign(rand.Reader, sha224Digest[:], crypto.SHA224) + if err != ErrUnsupportedHash { + t.Fatal("expected ErrUnsupportedHash, got ", err) + } + }) +} + +func TestSignerECDSA(t *testing.T) { + ecPriv, ok := leafEC.PrivateKey.(*ecdsa.PrivateKey) + if !ok { + t.Fatal("expected priv to be an ECDSA private key") + } + + withIdentity(t, leafEC, func(ident Identity) { + signer, err := ident.Signer() + if err != nil { + t.Fatal(err) + } + + pk := signer.Public() + ecPub, ok := pk.(*ecdsa.PublicKey) + if !ok { + t.Fatal("expected pk to be an RSA public key") + } + + if ecPub.X.Cmp(ecPriv.X) != 0 { + t.Fatalf("bad X. Got %s, expected %s", ecPub.X.Text(16), ecPriv.X.Text(16)) + } + + if ecPub.Y.Cmp(ecPriv.Y) != 0 { + t.Fatalf("bad Y. Got %s, expected %s", ecPub.Y.Text(16), ecPriv.Y.Text(16)) + } + + // ECDSAWithSHA1 + sha1Digest := sha1.Sum([]byte("hello")) + sig, err := signer.Sign(rand.Reader, sha1Digest[:], crypto.SHA1) + if err != nil { + t.Fatal(err) + } + if err = leafEC.Certificate.CheckSignature(x509.ECDSAWithSHA1, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + + // ECDSAWithSHA256 + sha256Digest := sha256.Sum256([]byte("hello")) + sig, err = signer.Sign(rand.Reader, sha256Digest[:], crypto.SHA256) + if err != nil { + t.Fatal(err) + } + if err = leafEC.Certificate.CheckSignature(x509.ECDSAWithSHA256, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + + // ECDSAWithSHA384 + sha384Digest := sha512.Sum384([]byte("hello")) + sig, err = signer.Sign(rand.Reader, sha384Digest[:], crypto.SHA384) + if err != nil { + t.Fatal(err) + } + if err = leafEC.Certificate.CheckSignature(x509.ECDSAWithSHA384, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + + // ECDSAWithSHA512 + sha512Digest := sha512.Sum512([]byte("hello")) + sig, err = signer.Sign(rand.Reader, sha512Digest[:], crypto.SHA512) + if err != nil { + t.Fatal(err) + } + if err = leafEC.Certificate.CheckSignature(x509.ECDSAWithSHA512, []byte("hello"), sig); err != nil { + t.Fatal(err) + } + + // Bad digest size + _, err = signer.Sign(rand.Reader, sha512Digest[5:], crypto.SHA512) + if err == nil { + t.Fatal("expected error for bad digest size") + } + }) +} + +func TestCertificateRSA(t *testing.T) { + CertificateHelper(t, leafRSA) +} + +func TestCertificateEC(t *testing.T) { + CertificateHelper(t, leafEC) +} + +func CertificateHelper(t *testing.T, leaf *fakeca.Identity) { + withIdentity(t, root, func(caIdent Identity) { + withIdentity(t, intermediate, func(interIdent Identity) { + withIdentity(t, leaf, func(leafIdent Identity) { + crtActual, err := leafIdent.Certificate() + if err != nil { + t.Fatal(err) + } + if !leaf.Certificate.Equal(crtActual) { + t.Fatal("Expected cert to match pfx") + } + + chain, err := leafIdent.CertificateChain() + if err != nil { + t.Fatal(err) + } + if len(chain) != 3 { + t.Fatalf("bad chain len. expected 3, got %d", len(chain)) + } + if !leaf.Certificate.Equal(chain[0]) { + t.Fatal("first chain cert should be leaf") + } + if !intermediate.Certificate.Equal(chain[1]) { + t.Fatal("second chain cert should be intermediate") + } + if !root.Certificate.Equal(chain[2]) { + t.Fatal("second chain cert should be intermediate") + } + }) + }) + }) +} diff --git a/certstore/certstore_windows.go b/certstore/certstore_windows.go new file mode 100644 index 0000000..86c0b1c --- /dev/null +++ b/certstore/certstore_windows.go @@ -0,0 +1,682 @@ +package certstore + +/* +#cgo windows LDFLAGS: -lcrypt32 -lncrypt + +#include +#include +#include + +char* errMsg(DWORD code) { + char* lpMsgBuf; + DWORD ret = 0; + + ret = FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPTSTR) &lpMsgBuf, + 0, NULL); + + if (ret == 0) { + return NULL; + } else { + return lpMsgBuf; + } +} +*/ +import "C" + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/asn1" + "fmt" + "io" + "math/big" + "unicode/utf16" + "unsafe" + + "github.com/pkg/errors" +) + +const ( + winTrue C.WINBOOL = 1 + winFalse C.WINBOOL = 0 + + // ERROR_SUCCESS + ERROR_SUCCESS = 0x00000000 + + // CRYPT_E_NOT_FOUND — Cannot find object or property. + CRYPT_E_NOT_FOUND = 0x80092004 + + // NTE_BAD_ALGID — Invalid algorithm specified. + NTE_BAD_ALGID = 0x80090008 +) + +// winAPIFlag specifies the flags that should be passed to +// CryptAcquireCertificatePrivateKey. This impacts whether the CryptoAPI or CNG +// API will be used. +// +// Possible values are: +// 0x00000000 — — Only use CryptoAPI. +// 0x00010000 — CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG — Prefer CryptoAPI. +// 0x00020000 — CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG — Prefer CNG. +// 0x00040000 — CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG — Only uyse CNG. +var winAPIFlag C.DWORD = C.CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG + +// winStore is a wrapper around a C.HCERTSTORE. +type winStore struct { + store C.HCERTSTORE +} + +// openStore opens the current user's personal cert store. +func openStore() (*winStore, error) { + storeName := unsafe.Pointer(stringToUTF16("MY")) + defer C.free(storeName) + + store := C.CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, 0, C.CERT_SYSTEM_STORE_CURRENT_USER, storeName) + if store == nil { + return nil, lastError("failed to open system cert store") + } + + return &winStore{store}, nil +} + +// Identities implements the Store interface. +func (s *winStore) Identities() ([]Identity, error) { + var ( + err error + idents = []Identity{} + + // CertFindChainInStore parameters + encoding = C.DWORD(C.X509_ASN_ENCODING) + flags = C.DWORD(C.CERT_CHAIN_FIND_BY_ISSUER_CACHE_ONLY_FLAG | C.CERT_CHAIN_FIND_BY_ISSUER_CACHE_ONLY_URL_FLAG) + findType = C.DWORD(C.CERT_CHAIN_FIND_BY_ISSUER) + params = &C.CERT_CHAIN_FIND_BY_ISSUER_PARA{cbSize: C.DWORD(unsafe.Sizeof(C.CERT_CHAIN_FIND_BY_ISSUER_PARA{}))} + paramsPtr = unsafe.Pointer(params) + chainCtx = C.PCCERT_CHAIN_CONTEXT(nil) + ) + + for { + if chainCtx = C.CertFindChainInStore(s.store, encoding, flags, findType, paramsPtr, chainCtx); chainCtx == nil { + break + } + if chainCtx.cChain < 1 { + err = errors.New("bad chain") + goto fail + } + + // not sure why this isn't 1 << 29 + const maxPointerArray = 1 << 28 + + // rgpChain is actually an array, but we only care about the first one. + simpleChain := *chainCtx.rgpChain + if simpleChain.cElement < 1 || simpleChain.cElement > maxPointerArray { + err = errors.New("bad chain") + goto fail + } + + // Hacky way to get chain elements (c array) as a slice. + chainElts := (*[maxPointerArray]C.PCERT_CHAIN_ELEMENT)(unsafe.Pointer(simpleChain.rgpElement))[:simpleChain.cElement:simpleChain.cElement] + + // Build chain of certificates from each elt's certificate context. + chain := make([]C.PCCERT_CONTEXT, len(chainElts)) + for j := range chainElts { + chain[j] = chainElts[j].pCertContext + } + + idents = append(idents, newWinIdentity(chain)) + } + + if err = checkError("failed to iterate certs in store"); err != nil && errors.Cause(err) != errCode(CRYPT_E_NOT_FOUND) { + goto fail + } + + return idents, nil + +fail: + for _, ident := range idents { + ident.Close() + } + + return nil, err +} + +// Import implements the Store interface. +func (s *winStore) Import(data []byte, password string) error { + cdata := C.CBytes(data) + defer C.free(cdata) + + cpw := stringToUTF16(password) + defer C.free(unsafe.Pointer(cpw)) + + pfx := &C.CRYPT_DATA_BLOB{ + cbData: C.DWORD(len(data)), + pbData: (*C.BYTE)(cdata), + } + + flags := C.CRYPT_USER_KEYSET + + // import into preferred KSP + if winAPIFlag&C.CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG > 0 { + flags |= C.PKCS12_PREFER_CNG_KSP + } else if winAPIFlag&C.CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG > 0 { + flags |= C.PKCS12_ALWAYS_CNG_KSP + } + + store := C.PFXImportCertStore(pfx, cpw, C.DWORD(flags)) + if store == nil { + return lastError("failed to import PFX cert store") + } + defer C.CertCloseStore(store, C.CERT_CLOSE_STORE_FORCE_FLAG) + + var ( + ctx = C.PCCERT_CONTEXT(nil) + encoding = C.DWORD(C.X509_ASN_ENCODING | C.PKCS_7_ASN_ENCODING) + ) + + for { + // iterate through certs in temporary store + if ctx = C.CertFindCertificateInStore(store, encoding, 0, C.CERT_FIND_ANY, nil, ctx); ctx == nil { + if err := checkError("failed to iterate certs in store"); err != nil && errors.Cause(err) != errCode(CRYPT_E_NOT_FOUND) { + return err + } + + break + } + + // Copy the cert to the system store. + if ok := C.CertAddCertificateContextToStore(s.store, ctx, C.CERT_STORE_ADD_REPLACE_EXISTING, nil); ok == winFalse { + return lastError("failed to add importerd certificate to MY store") + } + } + + return nil +} + +// Close implements the Store interface. +func (s *winStore) Close() { + C.CertCloseStore(s.store, 0) + s.store = nil +} + +// winIdentity implements the Identity interface. +type winIdentity struct { + chain []C.PCCERT_CONTEXT + signer *winPrivateKey +} + +func newWinIdentity(chain []C.PCCERT_CONTEXT) *winIdentity { + for _, ctx := range chain { + C.CertDuplicateCertificateContext(ctx) + } + + return &winIdentity{chain: chain} +} + +// Certificate implements the Identity interface. +func (i *winIdentity) Certificate() (*x509.Certificate, error) { + return exportCertCtx(i.chain[0]) +} + +// CertificateChain implements the Identity interface. +func (i *winIdentity) CertificateChain() ([]*x509.Certificate, error) { + var ( + certs = make([]*x509.Certificate, len(i.chain)) + err error + ) + + for j := range i.chain { + if certs[j], err = exportCertCtx(i.chain[j]); err != nil { + return nil, err + } + } + + return certs, nil +} + +// Signer implements the Identity interface. +func (i *winIdentity) Signer() (crypto.Signer, error) { + return i.getPrivateKey() +} + +// getPrivateKey gets this identity's private *winPrivateKey. +func (i *winIdentity) getPrivateKey() (*winPrivateKey, error) { + if i.signer != nil { + return i.signer, nil + } + + cert, err := i.Certificate() + if err != nil { + return nil, errors.Wrap(err, "failed to get identity certificate") + } + + signer, err := newWinPrivateKey(i.chain[0], cert.PublicKey) + if err != nil { + return nil, errors.Wrap(err, "failed to load identity private key") + } + + i.signer = signer + + return i.signer, nil +} + +// Delete implements the Identity interface. +func (i *winIdentity) Delete() error { + // duplicate cert context, since CertDeleteCertificateFromStore will free it. + deleteCtx := C.CertDuplicateCertificateContext(i.chain[0]) + + // try deleting cert + if ok := C.CertDeleteCertificateFromStore(deleteCtx); ok == winFalse { + return lastError("failed to delete certificate from store") + } + + // try deleting private key + wpk, err := i.getPrivateKey() + if err != nil { + return errors.Wrap(err, "failed to get identity private key") + } + + if err := wpk.Delete(); err != nil { + return errors.Wrap(err, "failed to delete identity private key") + } + + return nil +} + +// Close implements the Identity interface. +func (i *winIdentity) Close() { + if i.signer != nil { + i.signer.Close() + i.signer = nil + } + + for _, ctx := range i.chain { + C.CertFreeCertificateContext(ctx) + i.chain = nil + } +} + +// winPrivateKey is a wrapper around a HCRYPTPROV_OR_NCRYPT_KEY_HANDLE. +type winPrivateKey struct { + publicKey crypto.PublicKey + + // CryptoAPI fields + capiProv C.HCRYPTPROV + + // CNG fields + cngHandle C.NCRYPT_KEY_HANDLE + keySpec C.DWORD +} + +// newWinPrivateKey gets a *winPrivateKey for the given certificate. +func newWinPrivateKey(certCtx C.PCCERT_CONTEXT, publicKey crypto.PublicKey) (*winPrivateKey, error) { + var ( + provOrKey C.HCRYPTPROV_OR_NCRYPT_KEY_HANDLE + keySpec C.DWORD + mustFree C.WINBOOL + ) + + if publicKey == nil { + return nil, errors.New("nil public key") + } + + // Get a handle for the found private key. + if ok := C.CryptAcquireCertificatePrivateKey(certCtx, winAPIFlag, nil, &provOrKey, &keySpec, &mustFree); ok == winFalse { + return nil, lastError("failed to get private key for certificate") + } + + if mustFree != winTrue { + // This shouldn't happen since we're not asking for cached keys. + return nil, errors.New("CryptAcquireCertificatePrivateKey set mustFree") + } + + if keySpec == C.CERT_NCRYPT_KEY_SPEC { + return &winPrivateKey{ + publicKey: publicKey, + cngHandle: C.NCRYPT_KEY_HANDLE(provOrKey), + }, nil + } else { + return &winPrivateKey{ + publicKey: publicKey, + capiProv: C.HCRYPTPROV(provOrKey), + keySpec: keySpec, + }, nil + } +} + +// Public implements the crypto.Signer interface. +func (wpk *winPrivateKey) Public() crypto.PublicKey { + return wpk.publicKey +} + +// Sign implements the crypto.Signer interface. +func (wpk *winPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + if wpk.capiProv != 0 { + return wpk.capiSignHash(opts.HashFunc(), digest) + } else if wpk.cngHandle != 0 { + return wpk.cngSignHash(opts.HashFunc(), digest) + } else { + return nil, errors.New("bad private key") + } +} + +// cngSignHash signs a digest using the CNG APIs. +func (wpk *winPrivateKey) cngSignHash(hash crypto.Hash, digest []byte) ([]byte, error) { + if len(digest) != hash.Size() { + return nil, errors.New("bad digest for hash") + } + + var ( + // input + padPtr = unsafe.Pointer(nil) + digestPtr = (*C.BYTE)(&digest[0]) + digestLen = C.DWORD(len(digest)) + flags = C.DWORD(0) + + // output + sigLen = C.DWORD(0) + ) + + // setup pkcs1v1.5 padding for RSA + if _, isRSA := wpk.publicKey.(*rsa.PublicKey); isRSA { + flags |= C.BCRYPT_PAD_PKCS1 + padInfo := C.BCRYPT_PKCS1_PADDING_INFO{} + padPtr = unsafe.Pointer(&padInfo) + + switch hash { + case crypto.SHA1: + padInfo.pszAlgId = BCRYPT_SHA1_ALGORITHM + case crypto.SHA256: + padInfo.pszAlgId = BCRYPT_SHA256_ALGORITHM + case crypto.SHA384: + padInfo.pszAlgId = BCRYPT_SHA384_ALGORITHM + case crypto.SHA512: + padInfo.pszAlgId = BCRYPT_SHA512_ALGORITHM + default: + return nil, ErrUnsupportedHash + } + } + + // get signature length + if err := checkStatus(C.NCryptSignHash(wpk.cngHandle, padPtr, digestPtr, digestLen, nil, 0, &sigLen, flags)); err != nil { + return nil, errors.Wrap(err, "failed to get signature length") + } + + // get signature + sig := make([]byte, sigLen) + sigPtr := (*C.BYTE)(&sig[0]) + if err := checkStatus(C.NCryptSignHash(wpk.cngHandle, padPtr, digestPtr, digestLen, sigPtr, sigLen, &sigLen, flags)); err != nil { + return nil, errors.Wrap(err, "failed to sign digest") + } + + // CNG returns a raw ECDSA signature, but we wan't ASN.1 DER encoding. + if _, isEC := wpk.publicKey.(*ecdsa.PublicKey); isEC { + if len(sig)%2 != 0 { + return nil, errors.New("bad ecdsa signature from CNG") + } + + type ecdsaSignature struct { + R, S *big.Int + } + + r := new(big.Int).SetBytes(sig[:len(sig)/2]) + s := new(big.Int).SetBytes(sig[len(sig)/2:]) + + encoded, err := asn1.Marshal(ecdsaSignature{r, s}) + if err != nil { + return nil, errors.Wrap(err, "failed to ASN.1 encode EC signature") + } + + return encoded, nil + } + + return sig, nil +} + +// capiSignHash signs a digest using the CryptoAPI APIs. +func (wpk *winPrivateKey) capiSignHash(hash crypto.Hash, digest []byte) ([]byte, error) { + if len(digest) != hash.Size() { + return nil, errors.New("bad digest for hash") + } + + // Figure out which CryptoAPI hash algorithm we're using. + var hash_alg C.ALG_ID + + switch hash { + case crypto.SHA1: + hash_alg = C.CALG_SHA1 + case crypto.SHA256: + hash_alg = C.CALG_SHA_256 + case crypto.SHA384: + hash_alg = C.CALG_SHA_384 + case crypto.SHA512: + hash_alg = C.CALG_SHA_512 + default: + return nil, ErrUnsupportedHash + } + + // Instantiate a CryptoAPI hash object. + var chash C.HCRYPTHASH + + if ok := C.CryptCreateHash(C.HCRYPTPROV(wpk.capiProv), hash_alg, 0, 0, &chash); ok == winFalse { + if err := lastError("failed to create hash"); errors.Cause(err) == errCode(NTE_BAD_ALGID) { + return nil, ErrUnsupportedHash + } else { + return nil, err + } + } + defer C.CryptDestroyHash(chash) + + // Make sure the hash size matches. + var ( + hashSize C.DWORD + hashSizePtr = (*C.BYTE)(unsafe.Pointer(&hashSize)) + hashSizeLen = C.DWORD(unsafe.Sizeof(hashSize)) + ) + + if ok := C.CryptGetHashParam(chash, C.HP_HASHSIZE, hashSizePtr, &hashSizeLen, 0); ok == winFalse { + return nil, lastError("failed to get hash size") + } + + if hash.Size() != int(hashSize) { + return nil, errors.New("invalid CryptoAPI hash") + } + + // Put our digest into the hash object. + digestPtr := (*C.BYTE)(unsafe.Pointer(&digest[0])) + if ok := C.CryptSetHashParam(chash, C.HP_HASHVAL, digestPtr, 0); ok == winFalse { + return nil, lastError("failed to set hash digest") + } + + // Get signature length. + var sigLen C.DWORD + + if ok := C.CryptSignHash(chash, wpk.keySpec, nil, 0, nil, &sigLen); ok == winFalse { + return nil, lastError("failed to get signature length") + } + + // Get signature + var ( + sig = make([]byte, int(sigLen)) + sigPtr = (*C.BYTE)(unsafe.Pointer(&sig[0])) + ) + + if ok := C.CryptSignHash(chash, wpk.keySpec, nil, 0, sigPtr, &sigLen); ok == winFalse { + return nil, lastError("failed to sign digest") + } + + // Signature is little endian, but we want big endian. Reverse it. + for i := len(sig)/2 - 1; i >= 0; i-- { + opp := len(sig) - 1 - i + sig[i], sig[opp] = sig[opp], sig[i] + } + + return sig, nil +} + +func (wpk *winPrivateKey) Delete() error { + if wpk.cngHandle != 0 { + // Delete CNG key + if err := checkStatus(C.NCryptDeleteKey(wpk.cngHandle, 0)); err != nil { + return err + } + } else if wpk.capiProv != 0 { + // Delete CryptoAPI key + var ( + param unsafe.Pointer + err error + + containerName C.LPCTSTR + providerName C.LPCTSTR + providerType *C.DWORD + ) + + if param, err = wpk.getProviderParam(C.PP_CONTAINER); err != nil { + return errors.Wrap(err, "failed to get PP_CONTAINER") + } else { + containerName = C.LPCTSTR(param) + } + + if param, err = wpk.getProviderParam(C.PP_NAME); err != nil { + return errors.Wrap(err, "failed to get PP_NAME") + } else { + providerName = C.LPCTSTR(param) + } + + if param, err = wpk.getProviderParam(C.PP_PROVTYPE); err != nil { + return errors.Wrap(err, "failed to get PP_PROVTYPE") + } else { + providerType = (*C.DWORD)(param) + } + + // use CRYPT_SILENT too? + var prov C.HCRYPTPROV + if ok := C.CryptAcquireContext(&prov, containerName, providerName, *providerType, C.CRYPT_DELETEKEYSET); ok == winFalse { + return lastError("failed to delete key set") + } + } else { + return errors.New("bad private key") + } + + return nil +} + +// getProviderParam gets a parameter about a provider. +func (wpk *winPrivateKey) getProviderParam(param C.DWORD) (unsafe.Pointer, error) { + var dataLen C.DWORD + if ok := C.CryptGetProvParam(wpk.capiProv, param, nil, &dataLen, 0); ok == winFalse { + return nil, lastError("failed to get provider parameter size") + } + + data := make([]byte, dataLen) + dataPtr := (*C.BYTE)(unsafe.Pointer(&data[0])) + if ok := C.CryptGetProvParam(wpk.capiProv, param, dataPtr, &dataLen, 0); ok == winFalse { + return nil, lastError("failed to get provider parameter") + } + + // TODO leaking memory here + return C.CBytes(data), nil +} + +// Close closes this winPrivateKey. +func (wpk *winPrivateKey) Close() { + if wpk.cngHandle != 0 { + C.NCryptFreeObject(C.NCRYPT_HANDLE(wpk.cngHandle)) + wpk.cngHandle = 0 + } + + if wpk.capiProv != 0 { + C.CryptReleaseContext(wpk.capiProv, 0) + wpk.capiProv = 0 + } +} + +// exportCertCtx exports a PCCERT_CONTEXT as an *x509.Certificate. +func exportCertCtx(ctx C.PCCERT_CONTEXT) (*x509.Certificate, error) { + der := C.GoBytes(unsafe.Pointer(ctx.pbCertEncoded), C.int(ctx.cbCertEncoded)) + + cert, err := x509.ParseCertificate(der) + if err != nil { + return nil, errors.Wrap(err, "certificate parsing failed") + } + + return cert, nil +} + +type errCode uint64 + +// lastError gets the last error from the current thread. If there isn't one, it +// returns a new error. +func lastError(msg string) error { + if err := checkError(msg); err != nil { + return err + } + + return errors.New(msg) +} + +// checkError tries to get the last error from the current thread. If there +// isn't one, it returns nil. +func checkError(msg string) error { + if code := errCode(C.GetLastError()); code != 0 { + return errors.Wrap(code, msg) + } + + return nil +} + +func (c errCode) Error() string { + cmsg := C.errMsg(C.DWORD(c)) + if cmsg == nil { + return fmt.Sprintf("Error %X", int(c)) + } + defer C.LocalFree(C.HLOCAL(cmsg)) + + gomsg := C.GoString(cmsg) + + return fmt.Sprintf("Error: %X %s", int(c), gomsg) +} + +type securityStatus uint64 + +func checkStatus(s C.SECURITY_STATUS) error { + ss := securityStatus(s) + + if ss == ERROR_SUCCESS { + return nil + } + + if ss == NTE_BAD_ALGID { + return ErrUnsupportedHash + } + + return ss +} + +func (ss securityStatus) Error() string { + return fmt.Sprintf("SECURITY_STATUS %d", int(ss)) +} + +func stringToUTF16(s string) C.LPCWSTR { + // Not sure why this isn't 1 << 30... + const maxUint16Array = 1 << 29 + + if len(s) > maxUint16Array { + panic("string too long") + } + + wstr := utf16.Encode([]rune(s)) + + p := C.calloc(C.size_t(len(wstr)+1), C.size_t(unsafe.Sizeof(uint16(0)))) + pp := (*[maxUint16Array]uint16)(p) + copy(pp[:], wstr) + + return (C.LPCWSTR)(p) +} diff --git a/certstore/crypt_strings_windows.go b/certstore/crypt_strings_windows.go new file mode 100644 index 0000000..1e8216f --- /dev/null +++ b/certstore/crypt_strings_windows.go @@ -0,0 +1,207 @@ +package certstore + +/* +#include +#include +#include + +// +// Go complains about LPCWSTR constants not being defined, so we define getter +// methods for a bunch of constants we might want. +// + +// Store name +LPCSTR GET_CERT_STORE_PROV_SYSTEM_W() { return CERT_STORE_PROV_SYSTEM_W; } + +// NCRYPT Object Property Names +LPCWSTR GET_NCRYPT_ALGORITHM_GROUP_PROPERTY() { return NCRYPT_ALGORITHM_GROUP_PROPERTY; } +LPCWSTR GET_NCRYPT_ALGORITHM_PROPERTY() { return NCRYPT_ALGORITHM_PROPERTY; } +LPCWSTR GET_NCRYPT_BLOCK_LENGTH_PROPERTY() { return NCRYPT_BLOCK_LENGTH_PROPERTY; } +LPCWSTR GET_NCRYPT_CERTIFICATE_PROPERTY() { return NCRYPT_CERTIFICATE_PROPERTY; } +LPCWSTR GET_NCRYPT_DH_PARAMETERS_PROPERTY() { return NCRYPT_DH_PARAMETERS_PROPERTY; } +LPCWSTR GET_NCRYPT_EXPORT_POLICY_PROPERTY() { return NCRYPT_EXPORT_POLICY_PROPERTY; } +LPCWSTR GET_NCRYPT_IMPL_TYPE_PROPERTY() { return NCRYPT_IMPL_TYPE_PROPERTY; } +LPCWSTR GET_NCRYPT_KEY_TYPE_PROPERTY() { return NCRYPT_KEY_TYPE_PROPERTY; } +LPCWSTR GET_NCRYPT_KEY_USAGE_PROPERTY() { return NCRYPT_KEY_USAGE_PROPERTY; } +LPCWSTR GET_NCRYPT_LAST_MODIFIED_PROPERTY() { return NCRYPT_LAST_MODIFIED_PROPERTY; } +LPCWSTR GET_NCRYPT_LENGTH_PROPERTY() { return NCRYPT_LENGTH_PROPERTY; } +LPCWSTR GET_NCRYPT_LENGTHS_PROPERTY() { return NCRYPT_LENGTHS_PROPERTY; } +LPCWSTR GET_NCRYPT_MAX_NAME_LENGTH_PROPERTY() { return NCRYPT_MAX_NAME_LENGTH_PROPERTY; } +LPCWSTR GET_NCRYPT_NAME_PROPERTY() { return NCRYPT_NAME_PROPERTY; } +LPCWSTR GET_NCRYPT_PIN_PROMPT_PROPERTY() { return NCRYPT_PIN_PROMPT_PROPERTY; } +LPCWSTR GET_NCRYPT_PIN_PROPERTY() { return NCRYPT_PIN_PROPERTY; } +LPCWSTR GET_NCRYPT_PROVIDER_HANDLE_PROPERTY() { return NCRYPT_PROVIDER_HANDLE_PROPERTY; } +LPCWSTR GET_NCRYPT_READER_PROPERTY() { return NCRYPT_READER_PROPERTY; } +LPCWSTR GET_NCRYPT_ROOT_CERTSTORE_PROPERTY() { return NCRYPT_ROOT_CERTSTORE_PROPERTY; } +LPCWSTR GET_NCRYPT_SECURE_PIN_PROPERTY() { return NCRYPT_SECURE_PIN_PROPERTY; } +LPCWSTR GET_NCRYPT_SECURITY_DESCR_PROPERTY() { return NCRYPT_SECURITY_DESCR_PROPERTY; } +LPCWSTR GET_NCRYPT_SECURITY_DESCR_SUPPORT_PROPERTY() { return NCRYPT_SECURITY_DESCR_SUPPORT_PROPERTY; } +LPCWSTR GET_NCRYPT_SMARTCARD_GUID_PROPERTY() { return NCRYPT_SMARTCARD_GUID_PROPERTY; } +LPCWSTR GET_NCRYPT_UI_POLICY_PROPERTY() { return NCRYPT_UI_POLICY_PROPERTY; } +LPCWSTR GET_NCRYPT_UNIQUE_NAME_PROPERTY() { return NCRYPT_UNIQUE_NAME_PROPERTY; } +LPCWSTR GET_NCRYPT_USE_CONTEXT_PROPERTY() { return NCRYPT_USE_CONTEXT_PROPERTY; } +LPCWSTR GET_NCRYPT_USE_COUNT_ENABLED_PROPERTY() { return NCRYPT_USE_COUNT_ENABLED_PROPERTY; } +LPCWSTR GET_NCRYPT_USE_COUNT_PROPERTY() { return NCRYPT_USE_COUNT_PROPERTY; } +LPCWSTR GET_NCRYPT_USER_CERTSTORE_PROPERTY() { return NCRYPT_USER_CERTSTORE_PROPERTY; } +LPCWSTR GET_NCRYPT_VERSION_PROPERTY() { return NCRYPT_VERSION_PROPERTY; } +LPCWSTR GET_NCRYPT_WINDOW_HANDLE_PROPERTY() { return NCRYPT_WINDOW_HANDLE_PROPERTY; } + +// BCRYPT BLOB Types +LPCWSTR GET_BCRYPT_DH_PRIVATE_BLOB() { return BCRYPT_DH_PRIVATE_BLOB; } +LPCWSTR GET_BCRYPT_DH_PUBLIC_BLOB() { return BCRYPT_DH_PUBLIC_BLOB; } +LPCWSTR GET_BCRYPT_DSA_PRIVATE_BLOB() { return BCRYPT_DSA_PRIVATE_BLOB; } +LPCWSTR GET_BCRYPT_DSA_PUBLIC_BLOB() { return BCRYPT_DSA_PUBLIC_BLOB; } +LPCWSTR GET_BCRYPT_ECCPRIVATE_BLOB() { return BCRYPT_ECCPRIVATE_BLOB; } +LPCWSTR GET_BCRYPT_ECCPUBLIC_BLOB() { return BCRYPT_ECCPUBLIC_BLOB; } +LPCWSTR GET_BCRYPT_PUBLIC_KEY_BLOB() { return BCRYPT_PUBLIC_KEY_BLOB; } +LPCWSTR GET_BCRYPT_PRIVATE_KEY_BLOB() { return BCRYPT_PRIVATE_KEY_BLOB; } +LPCWSTR GET_BCRYPT_RSAFULLPRIVATE_BLOB() { return BCRYPT_RSAFULLPRIVATE_BLOB; } +LPCWSTR GET_BCRYPT_RSAPRIVATE_BLOB() { return BCRYPT_RSAPRIVATE_BLOB; } +LPCWSTR GET_BCRYPT_RSAPUBLIC_BLOB() { return BCRYPT_RSAPUBLIC_BLOB; } + +// BCRYPT Algorithm Names +LPCWSTR GET_BCRYPT_3DES_ALGORITHM() { return BCRYPT_3DES_ALGORITHM; } +LPCWSTR GET_BCRYPT_3DES_112_ALGORITHM() { return BCRYPT_3DES_112_ALGORITHM; } +LPCWSTR GET_BCRYPT_AES_ALGORITHM() { return BCRYPT_AES_ALGORITHM; } +LPCWSTR GET_BCRYPT_AES_CMAC_ALGORITHM() { return BCRYPT_AES_CMAC_ALGORITHM; } +LPCWSTR GET_BCRYPT_AES_GMAC_ALGORITHM() { return BCRYPT_AES_GMAC_ALGORITHM; } +LPCWSTR GET_BCRYPT_CAPI_KDF_ALGORITHM() { return BCRYPT_CAPI_KDF_ALGORITHM; } +LPCWSTR GET_BCRYPT_DES_ALGORITHM() { return BCRYPT_DES_ALGORITHM; } +LPCWSTR GET_BCRYPT_DESX_ALGORITHM() { return BCRYPT_DESX_ALGORITHM; } +LPCWSTR GET_BCRYPT_DH_ALGORITHM() { return BCRYPT_DH_ALGORITHM; } +LPCWSTR GET_BCRYPT_DSA_ALGORITHM() { return BCRYPT_DSA_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDH_P256_ALGORITHM() { return BCRYPT_ECDH_P256_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDH_P384_ALGORITHM() { return BCRYPT_ECDH_P384_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDH_P521_ALGORITHM() { return BCRYPT_ECDH_P521_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDSA_P256_ALGORITHM() { return BCRYPT_ECDSA_P256_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDSA_P384_ALGORITHM() { return BCRYPT_ECDSA_P384_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDSA_P521_ALGORITHM() { return BCRYPT_ECDSA_P521_ALGORITHM; } +LPCWSTR GET_BCRYPT_MD2_ALGORITHM() { return BCRYPT_MD2_ALGORITHM; } +LPCWSTR GET_BCRYPT_MD4_ALGORITHM() { return BCRYPT_MD4_ALGORITHM; } +LPCWSTR GET_BCRYPT_MD5_ALGORITHM() { return BCRYPT_MD5_ALGORITHM; } +LPCWSTR GET_BCRYPT_RC2_ALGORITHM() { return BCRYPT_RC2_ALGORITHM; } +LPCWSTR GET_BCRYPT_RC4_ALGORITHM() { return BCRYPT_RC4_ALGORITHM; } +LPCWSTR GET_BCRYPT_RNG_ALGORITHM() { return BCRYPT_RNG_ALGORITHM; } +LPCWSTR GET_BCRYPT_RNG_DUAL_EC_ALGORITHM() { return BCRYPT_RNG_DUAL_EC_ALGORITHM; } +LPCWSTR GET_BCRYPT_RNG_FIPS186_DSA_ALGORITHM() { return BCRYPT_RNG_FIPS186_DSA_ALGORITHM; } +LPCWSTR GET_BCRYPT_RSA_ALGORITHM() { return BCRYPT_RSA_ALGORITHM; } +LPCWSTR GET_BCRYPT_RSA_SIGN_ALGORITHM() { return BCRYPT_RSA_SIGN_ALGORITHM; } +LPCWSTR GET_BCRYPT_SHA1_ALGORITHM() { return BCRYPT_SHA1_ALGORITHM; } +LPCWSTR GET_BCRYPT_SHA256_ALGORITHM() { return BCRYPT_SHA256_ALGORITHM; } +LPCWSTR GET_BCRYPT_SHA384_ALGORITHM() { return BCRYPT_SHA384_ALGORITHM; } +LPCWSTR GET_BCRYPT_SHA512_ALGORITHM() { return BCRYPT_SHA512_ALGORITHM; } +LPCWSTR GET_BCRYPT_SP800108_CTR_HMAC_ALGORITHM() { return BCRYPT_SP800108_CTR_HMAC_ALGORITHM; } +LPCWSTR GET_BCRYPT_SP80056A_CONCAT_ALGORITHM() { return BCRYPT_SP80056A_CONCAT_ALGORITHM; } +LPCWSTR GET_BCRYPT_PBKDF2_ALGORITHM() { return BCRYPT_PBKDF2_ALGORITHM; } + +// +// These may be missing from bcrypt.h if (NTDDI_VERSION >= NTDDI_WINTHRESHOLD) +// Not sure what that really means... +// + +#ifndef BCRYPT_ECDSA_ALGORITHM +#define BCRYPT_ECDSA_ALGORITHM L"ECDSA" +#endif + +#ifndef BCRYPT_ECDH_ALGORITHM +#define BCRYPT_ECDH_ALGORITHM L"ECDH" +#endif + +#ifndef BCRYPT_XTS_AES_ALGORITHM +#define BCRYPT_XTS_AES_ALGORITHM L"XTS-AES" +#endif + +LPCWSTR GET_BCRYPT_ECDSA_ALGORITHM() { return BCRYPT_ECDSA_ALGORITHM; } +LPCWSTR GET_BCRYPT_ECDH_ALGORITHM() { return BCRYPT_ECDH_ALGORITHM; } +LPCWSTR GET_BCRYPT_XTS_AES_ALGORITHM() { return BCRYPT_XTS_AES_ALGORITHM; } +*/ +import "C" + +var ( + // Store name + CERT_STORE_PROV_SYSTEM_W = C.GET_CERT_STORE_PROV_SYSTEM_W() + + // NCRYPT Object Property Names + NCRYPT_ALGORITHM_GROUP_PROPERTY = C.GET_NCRYPT_ALGORITHM_GROUP_PROPERTY() + NCRYPT_ALGORITHM_PROPERTY = C.GET_NCRYPT_ALGORITHM_PROPERTY() + NCRYPT_BLOCK_LENGTH_PROPERTY = C.GET_NCRYPT_BLOCK_LENGTH_PROPERTY() + NCRYPT_CERTIFICATE_PROPERTY = C.GET_NCRYPT_CERTIFICATE_PROPERTY() + NCRYPT_DH_PARAMETERS_PROPERTY = C.GET_NCRYPT_DH_PARAMETERS_PROPERTY() + NCRYPT_EXPORT_POLICY_PROPERTY = C.GET_NCRYPT_EXPORT_POLICY_PROPERTY() + NCRYPT_IMPL_TYPE_PROPERTY = C.GET_NCRYPT_IMPL_TYPE_PROPERTY() + NCRYPT_KEY_TYPE_PROPERTY = C.GET_NCRYPT_KEY_TYPE_PROPERTY() + NCRYPT_KEY_USAGE_PROPERTY = C.GET_NCRYPT_KEY_USAGE_PROPERTY() + NCRYPT_LAST_MODIFIED_PROPERTY = C.GET_NCRYPT_LAST_MODIFIED_PROPERTY() + NCRYPT_LENGTH_PROPERTY = C.GET_NCRYPT_LENGTH_PROPERTY() + NCRYPT_LENGTHS_PROPERTY = C.GET_NCRYPT_LENGTHS_PROPERTY() + NCRYPT_MAX_NAME_LENGTH_PROPERTY = C.GET_NCRYPT_MAX_NAME_LENGTH_PROPERTY() + NCRYPT_NAME_PROPERTY = C.GET_NCRYPT_NAME_PROPERTY() + NCRYPT_PIN_PROMPT_PROPERTY = C.GET_NCRYPT_PIN_PROMPT_PROPERTY() + NCRYPT_PIN_PROPERTY = C.GET_NCRYPT_PIN_PROPERTY() + NCRYPT_PROVIDER_HANDLE_PROPERTY = C.GET_NCRYPT_PROVIDER_HANDLE_PROPERTY() + NCRYPT_READER_PROPERTY = C.GET_NCRYPT_READER_PROPERTY() + NCRYPT_ROOT_CERTSTORE_PROPERTY = C.GET_NCRYPT_ROOT_CERTSTORE_PROPERTY() + NCRYPT_SECURE_PIN_PROPERTY = C.GET_NCRYPT_SECURE_PIN_PROPERTY() + NCRYPT_SECURITY_DESCR_PROPERTY = C.GET_NCRYPT_SECURITY_DESCR_PROPERTY() + NCRYPT_SECURITY_DESCR_SUPPORT_PROPERTY = C.GET_NCRYPT_SECURITY_DESCR_SUPPORT_PROPERTY() + NCRYPT_SMARTCARD_GUID_PROPERTY = C.GET_NCRYPT_SMARTCARD_GUID_PROPERTY() + NCRYPT_UI_POLICY_PROPERTY = C.GET_NCRYPT_UI_POLICY_PROPERTY() + NCRYPT_UNIQUE_NAME_PROPERTY = C.GET_NCRYPT_UNIQUE_NAME_PROPERTY() + NCRYPT_USE_CONTEXT_PROPERTY = C.GET_NCRYPT_USE_CONTEXT_PROPERTY() + NCRYPT_USE_COUNT_ENABLED_PROPERTY = C.GET_NCRYPT_USE_COUNT_ENABLED_PROPERTY() + NCRYPT_USE_COUNT_PROPERTY = C.GET_NCRYPT_USE_COUNT_PROPERTY() + NCRYPT_USER_CERTSTORE_PROPERTY = C.GET_NCRYPT_USER_CERTSTORE_PROPERTY() + NCRYPT_VERSION_PROPERTY = C.GET_NCRYPT_VERSION_PROPERTY() + NCRYPT_WINDOW_HANDLE_PROPERTY = C.GET_NCRYPT_WINDOW_HANDLE_PROPERTY() + + // BCRYPT BLOB Types + BCRYPT_DH_PRIVATE_BLOB = C.GET_BCRYPT_DH_PRIVATE_BLOB() + BCRYPT_DH_PUBLIC_BLOB = C.GET_BCRYPT_DH_PUBLIC_BLOB() + BCRYPT_DSA_PRIVATE_BLOB = C.GET_BCRYPT_DSA_PRIVATE_BLOB() + BCRYPT_DSA_PUBLIC_BLOB = C.GET_BCRYPT_DSA_PUBLIC_BLOB() + BCRYPT_ECCPRIVATE_BLOB = C.GET_BCRYPT_ECCPRIVATE_BLOB() + BCRYPT_ECCPUBLIC_BLOB = C.GET_BCRYPT_ECCPUBLIC_BLOB() + BCRYPT_PUBLIC_KEY_BLOB = C.GET_BCRYPT_PUBLIC_KEY_BLOB() + BCRYPT_PRIVATE_KEY_BLOB = C.GET_BCRYPT_PRIVATE_KEY_BLOB() + BCRYPT_RSAFULLPRIVATE_BLOB = C.GET_BCRYPT_RSAFULLPRIVATE_BLOB() + BCRYPT_RSAPRIVATE_BLOB = C.GET_BCRYPT_RSAPRIVATE_BLOB() + BCRYPT_RSAPUBLIC_BLOB = C.GET_BCRYPT_RSAPUBLIC_BLOB() + + // BCRYPT Algorithm Names + BCRYPT_3DES_ALGORITHM = C.GET_BCRYPT_3DES_ALGORITHM() + BCRYPT_3DES_112_ALGORITHM = C.GET_BCRYPT_3DES_112_ALGORITHM() + BCRYPT_AES_ALGORITHM = C.GET_BCRYPT_AES_ALGORITHM() + BCRYPT_AES_CMAC_ALGORITHM = C.GET_BCRYPT_AES_CMAC_ALGORITHM() + BCRYPT_AES_GMAC_ALGORITHM = C.GET_BCRYPT_AES_GMAC_ALGORITHM() + BCRYPT_CAPI_KDF_ALGORITHM = C.GET_BCRYPT_CAPI_KDF_ALGORITHM() + BCRYPT_DES_ALGORITHM = C.GET_BCRYPT_DES_ALGORITHM() + BCRYPT_DESX_ALGORITHM = C.GET_BCRYPT_DESX_ALGORITHM() + BCRYPT_DH_ALGORITHM = C.GET_BCRYPT_DH_ALGORITHM() + BCRYPT_DSA_ALGORITHM = C.GET_BCRYPT_DSA_ALGORITHM() + BCRYPT_ECDH_P256_ALGORITHM = C.GET_BCRYPT_ECDH_P256_ALGORITHM() + BCRYPT_ECDH_P384_ALGORITHM = C.GET_BCRYPT_ECDH_P384_ALGORITHM() + BCRYPT_ECDH_P521_ALGORITHM = C.GET_BCRYPT_ECDH_P521_ALGORITHM() + BCRYPT_ECDSA_P256_ALGORITHM = C.GET_BCRYPT_ECDSA_P256_ALGORITHM() + BCRYPT_ECDSA_P384_ALGORITHM = C.GET_BCRYPT_ECDSA_P384_ALGORITHM() + BCRYPT_ECDSA_P521_ALGORITHM = C.GET_BCRYPT_ECDSA_P521_ALGORITHM() + BCRYPT_MD2_ALGORITHM = C.GET_BCRYPT_MD2_ALGORITHM() + BCRYPT_MD4_ALGORITHM = C.GET_BCRYPT_MD4_ALGORITHM() + BCRYPT_MD5_ALGORITHM = C.GET_BCRYPT_MD5_ALGORITHM() + BCRYPT_RC2_ALGORITHM = C.GET_BCRYPT_RC2_ALGORITHM() + BCRYPT_RC4_ALGORITHM = C.GET_BCRYPT_RC4_ALGORITHM() + BCRYPT_RNG_ALGORITHM = C.GET_BCRYPT_RNG_ALGORITHM() + BCRYPT_RNG_DUAL_EC_ALGORITHM = C.GET_BCRYPT_RNG_DUAL_EC_ALGORITHM() + BCRYPT_RNG_FIPS186_DSA_ALGORITHM = C.GET_BCRYPT_RNG_FIPS186_DSA_ALGORITHM() + BCRYPT_RSA_ALGORITHM = C.GET_BCRYPT_RSA_ALGORITHM() + BCRYPT_RSA_SIGN_ALGORITHM = C.GET_BCRYPT_RSA_SIGN_ALGORITHM() + BCRYPT_SHA1_ALGORITHM = C.GET_BCRYPT_SHA1_ALGORITHM() + BCRYPT_SHA256_ALGORITHM = C.GET_BCRYPT_SHA256_ALGORITHM() + BCRYPT_SHA384_ALGORITHM = C.GET_BCRYPT_SHA384_ALGORITHM() + BCRYPT_SHA512_ALGORITHM = C.GET_BCRYPT_SHA512_ALGORITHM() + BCRYPT_SP800108_CTR_HMAC_ALGORITHM = C.GET_BCRYPT_SP800108_CTR_HMAC_ALGORITHM() + BCRYPT_SP80056A_CONCAT_ALGORITHM = C.GET_BCRYPT_SP80056A_CONCAT_ALGORITHM() + BCRYPT_PBKDF2_ALGORITHM = C.GET_BCRYPT_PBKDF2_ALGORITHM() + BCRYPT_ECDSA_ALGORITHM = C.GET_BCRYPT_ECDSA_ALGORITHM() + BCRYPT_ECDH_ALGORITHM = C.GET_BCRYPT_ECDH_ALGORITHM() + BCRYPT_XTS_AES_ALGORITHM = C.GET_BCRYPT_XTS_AES_ALGORITHM() +) diff --git a/certstore/main_test.go b/certstore/main_test.go new file mode 100644 index 0000000..3787361 --- /dev/null +++ b/certstore/main_test.go @@ -0,0 +1,130 @@ +package certstore + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "testing" + + "github.com/github/smimesign/fakeca" +) + +var ( + root = fakeca.New(fakeca.IsCA, fakeca.Subject(pkix.Name{ + Organization: []string{"certstore"}, + CommonName: "root", + })) + + intermediate = root.Issue(fakeca.IsCA, fakeca.Subject(pkix.Name{ + Organization: []string{"certstore"}, + CommonName: "intermediate", + })) + + leafKeyRSA, _ = rsa.GenerateKey(rand.Reader, 2048) + leafRSA = intermediate.Issue(fakeca.PrivateKey(leafKeyRSA), fakeca.Subject(pkix.Name{ + Organization: []string{"certstore"}, + CommonName: "leaf-rsa", + })) + + leafKeyEC, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + leafEC = intermediate.Issue(fakeca.PrivateKey(leafKeyEC), fakeca.Subject(pkix.Name{ + Organization: []string{"certstore"}, + CommonName: "leaf-ec", + })) +) + +func init() { + // delete any fixtures from a previous test run. + clearFixtures() +} + +func withStore(t *testing.T, cb func(Store)) { + store, err := Open() + if err != nil { + t.Fatal(err) + } + defer store.Close() + + cb(store) +} + +func withIdentity(t *testing.T, i *fakeca.Identity, cb func(Identity)) { + withStore(t, func(store Store) { + // Import an identity + if err := store.Import(i.PFX("asdf"), "asdf"); err != nil { + t.Fatal(err) + } + + // Look for our imported identity + idents, err := store.Identities() + if err != nil { + t.Fatal(err) + } + for _, ident := range idents { + defer ident.Close() + } + + var found Identity + for _, ident := range idents { + crt, err := ident.Certificate() + if err != nil { + t.Fatal(err) + } + + if i.Certificate.Equal(crt) { + if found != nil { + t.Fatal("duplicate identity imported") + } + found = ident + } + } + if found == nil { + t.Fatal("imported identity not found") + } + + // Clean up after ourselves. + defer func(f Identity) { + if err := f.Delete(); err != nil { + t.Fatal(err) + } + }(found) + + cb(found) + }) +} + +func clearFixtures() { + store, err := Open() + if err != nil { + panic(err) + } + defer store.Close() + + idents, err := store.Identities() + if err != nil { + panic(err) + } + for _, ident := range idents { + defer ident.Close() + } + + for _, ident := range idents { + crt, err := ident.Certificate() + if err != nil { + panic(err) + } + + if isFixture(crt) { + if err := ident.Delete(); err != nil { + panic(err) + } + } + } +} + +func isFixture(crt *x509.Certificate) bool { + return len(crt.Subject.Organization) == 1 && crt.Subject.Organization[0] == "certstore" +} diff --git a/certstore/main_windows_test.go b/certstore/main_windows_test.go new file mode 100644 index 0000000..94a2cd7 --- /dev/null +++ b/certstore/main_windows_test.go @@ -0,0 +1,25 @@ +package certstore + +import ( + "fmt" + "os" + "testing" +) + +func TestMain(m *testing.M) { + // Prefer CryptoAPI + fmt.Println("CRYPT_ACQUIRE_ALLOW_NCRYPT_KEY_FLAG") + winAPIFlag = 0x00010000 + if status := m.Run(); status != 0 { + os.Exit(status) + } + + // Prefer CNG + fmt.Println("CRYPT_ACQUIRE_PREFER_NCRYPT_KEY_FLAG") + winAPIFlag = 0x00020000 + if status := m.Run(); status != 0 { + os.Exit(status) + } + + os.Exit(0) +} diff --git a/command_sign.go b/command_sign.go index 35e9e27..43feb83 100644 --- a/command_sign.go +++ b/command_sign.go @@ -9,8 +9,8 @@ import ( "os" "strings" - "github.com/github/certstore" - "github.com/github/ietf-cms" + "github.com/github/smimesign/certstore" + cms "github.com/github/smimesign/ietf-cms" "github.com/pkg/errors" ) diff --git a/command_sign_test.go b/command_sign_test.go index 85c9c4d..388b4e7 100644 --- a/command_sign_test.go +++ b/command_sign_test.go @@ -4,8 +4,8 @@ import ( "crypto/x509" "testing" - "github.com/github/ietf-cms/protocol" - "github.com/github/ietf-cms" + cms "github.com/github/smimesign/ietf-cms" + "github.com/github/smimesign/ietf-cms/protocol" "github.com/stretchr/testify/require" ) diff --git a/command_verify.go b/command_verify.go index 82bfda9..25a1881 100644 --- a/command_verify.go +++ b/command_verify.go @@ -9,7 +9,7 @@ import ( "os" "github.com/certifi/gocertifi" - "github.com/github/ietf-cms" + cms "github.com/github/smimesign/ietf-cms" "github.com/pkg/errors" ) diff --git a/fakeca/LICENSE.md b/fakeca/LICENSE.md new file mode 100644 index 0000000..7800c58 --- /dev/null +++ b/fakeca/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Ben Toews. + +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/fakeca/README.md b/fakeca/README.md new file mode 100644 index 0000000..9fbe407 --- /dev/null +++ b/fakeca/README.md @@ -0,0 +1,44 @@ +# fakeca [![PkgGoDev](https://pkg.go.dev/badge/github.com/github/fakeca?tab=doc)](https://pkg.go.dev/github.com/github/fakeca?tab=doc) [![Report card](https://goreportcard.com/badge/github.com/github/fakeca)](https://goreportcard.com/report/github.com/github/fakeca) [![Actions CI](https://github.com/github/fakeca/workflows/Test/badge.svg)](https://github.com/github/fakeca/actions?query=workflow%3ATest) + +This is a package for creating fake certificate authorities for test fixtures. + +## Example + +```go +package main + +import ( + "crypto/x509/pkix" + + "github.com/github/fakeca" +) + +func main() { + // Change defaults for cert subjects. + fakeca.DefaultProvince = []string{"CO"} + fakeca.DefaultLocality = []string{"Denver"} + + // Create a root CA. + root := fakeca.New(fakeca.IsCA, fakeca.Subject(pkix.Name{ + CommonName: "root.myorg.com", + })) + + // Create an intermediate CA under the root. + intermediate := root.Issue(fakeca.IsCA, fakeca.Subject(pkix.Name{ + CommonName: "intermediate.myorg.com", + })) + + // Create a leaf certificate under the intermediate. + leaf := intermediate.Issue(fakeca.Subject(pkix.Name{ + CommonName: "leaf.myorg.com", + })) + + // Get PFX (PKCS12) blob containing certificate and encrypted private key. + leafPFX := leaf.PFX("pa55w0rd") + + // Get an *x509.CertPool containing certificate chain from CA to leaf for use + // with Go's TLS libraries. + leafPool := leaf.ChainPool() +} + +``` diff --git a/fakeca/configuration.go b/fakeca/configuration.go new file mode 100644 index 0000000..7e1be3a --- /dev/null +++ b/fakeca/configuration.go @@ -0,0 +1,240 @@ +package fakeca + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "fmt" + "math" + "math/big" + "time" +) + +type configuration struct { + subject *pkix.Name + issuer *Identity + nextSN *int64 + priv *crypto.Signer + isCA bool + notBefore *time.Time + notAfter *time.Time + issuingCertificateURL []string + ocspServer []string + keyUsage x509.KeyUsage +} + +func (c *configuration) generate() *Identity { + templ := &x509.Certificate{ + Subject: c.getSubject(), + IsCA: c.isCA, + BasicConstraintsValid: true, + NotAfter: c.getNotAfter(), + NotBefore: c.getNotBefore(), + IssuingCertificateURL: c.issuingCertificateURL, + OCSPServer: c.ocspServer, + KeyUsage: c.keyUsage, + } + + var ( + parent *x509.Certificate + thisPriv = c.getPrivateKey() + priv crypto.Signer + ) + + if c.issuer != nil { + parent = c.issuer.Certificate + templ.SerialNumber = big.NewInt(c.issuer.IncrementSN()) + priv = c.issuer.PrivateKey + } else { + parent = templ + templ.SerialNumber = randSN() + priv = thisPriv + } + + der, err := x509.CreateCertificate(rand.Reader, templ, parent, thisPriv.Public(), priv) + if err != nil { + panic(err) + } + + cert, err := x509.ParseCertificate(der) + if err != nil { + panic(err) + } + + return &Identity{ + Certificate: cert, + PrivateKey: thisPriv, + Issuer: c.issuer, + NextSN: c.getNextSN(), + } +} + +var ( + // DefaultCountry is the default subject Country. + DefaultCountry = []string{"US"} + + // DefaultProvince is the default subject Province. + DefaultProvince = []string{"CA"} + + // DefaultLocality is the default subject Locality. + DefaultLocality = []string{"San Francisco"} + + // DefaultStreetAddress is the default subject StreetAddress. + DefaultStreetAddress = []string(nil) + + // DefaultPostalCode is the default subject PostalCode. + DefaultPostalCode = []string(nil) + + // DefaultCommonName is the default subject CommonName. + DefaultCommonName = "fakeca" + + cnCounter int64 +) + +func (c *configuration) getSubject() pkix.Name { + if c.subject != nil { + return *c.subject + } + + var cn string + if cnCounter == 0 { + cn = DefaultCommonName + } else { + cn = fmt.Sprintf("%s #%d", DefaultCommonName, cnCounter) + } + cnCounter++ + + return pkix.Name{ + Country: DefaultCountry, + Province: DefaultProvince, + Locality: DefaultLocality, + StreetAddress: DefaultStreetAddress, + PostalCode: DefaultPostalCode, + CommonName: cn, + } +} + +func (c *configuration) getNextSN() int64 { + if c.nextSN == nil { + sn := randSN().Int64() + c.nextSN = &sn + } + + return *c.nextSN +} + +func randSN() *big.Int { + i, err := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64))) + if err != nil { + panic(err) + } + + return i +} + +func (c *configuration) getPrivateKey() crypto.Signer { + if c.priv == nil { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + + signer := crypto.Signer(priv) + + c.priv = &signer + } + + return *c.priv +} + +func (c *configuration) getNotBefore() time.Time { + if c.notBefore == nil { + return time.Unix(0, 0) + } + + return *c.notBefore +} + +func (c *configuration) getNotAfter() time.Time { + if c.notAfter == nil { + return time.Now().Add(time.Hour * 24 * 365 * 10) + } + + return *c.notAfter +} + +// Option is an option that can be passed to New(). +type Option option +type option func(c *configuration) + +// Subject is an Option that sets a identity's subject field. +func Subject(value pkix.Name) Option { + return func(c *configuration) { + c.subject = &value + } +} + +// NextSerialNumber is an Option that determines the SN of the next issued +// certificate. +func NextSerialNumber(value int64) Option { + return func(c *configuration) { + c.nextSN = &value + } +} + +// PrivateKey is an Option for setting the identity's private key. +func PrivateKey(value crypto.Signer) Option { + return func(c *configuration) { + c.priv = &value + } +} + +// Issuer is an Option for setting the identity's issuer. +func Issuer(value *Identity) Option { + return func(c *configuration) { + c.issuer = value + } +} + +// NotBefore is an Option for setting the identity's certificate's NotBefore. +func NotBefore(value time.Time) Option { + return func(c *configuration) { + c.notBefore = &value + } +} + +// NotAfter is an Option for setting the identity's certificate's NotAfter. +func NotAfter(value time.Time) Option { + return func(c *configuration) { + c.notAfter = &value + } +} + +// IssuingCertificateURL is an Option for setting the identity's certificate's +// IssuingCertificateURL. +func IssuingCertificateURL(value ...string) Option { + return func(c *configuration) { + c.issuingCertificateURL = append(c.issuingCertificateURL, value...) + } +} + +// OCSPServer is an Option for setting the identity's certificate's OCSPServer. +func OCSPServer(value ...string) Option { + return func(c *configuration) { + c.ocspServer = append(c.ocspServer, value...) + } +} + +// KeyUsage is an Option for setting the identity's certificate's KeyUsage. +func KeyUsage(ku x509.KeyUsage) Option { + return func(c *configuration) { + c.keyUsage = ku + } +} + +// IsCA is an Option for making an identity a certificate authority. +var IsCA Option = func(c *configuration) { + c.isCA = true +} diff --git a/fakeca/fakeca_test.go b/fakeca/fakeca_test.go new file mode 100644 index 0000000..cc2173e --- /dev/null +++ b/fakeca/fakeca_test.go @@ -0,0 +1,195 @@ +package fakeca + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "reflect" + "testing" +) + +func TestDefaults(t *testing.T) { + assertNoPanic(t, func() { + root := New(IsCA) + + if err := root.Certificate.CheckSignatureFrom(root.Certificate); err != nil { + t.Fatal(err) + } + }) +} + +func TestIntermediate(t *testing.T) { + assertNoPanic(t, func() { + New().Issue() + }) +} + +func TestSubject(t *testing.T) { + assertNoPanic(t, func() { + var ( + expected = "foobar" + root = New(Subject(pkix.Name{CommonName: expected})) + actual = root.Certificate.Subject.CommonName + ) + + if actual != expected { + t.Fatalf("bad subject. expected '%s', got '%s'", expected, actual) + } + }) +} + +func TestNextSerialNumber(t *testing.T) { + assertNoPanic(t, func() { + var ( + expected = int64(123) + ca = New(NextSerialNumber(expected)).Issue() + actual = ca.Certificate.SerialNumber.Int64() + ) + + if actual != expected { + t.Fatalf("bad sn. expected '%d', got '%d'", expected, actual) + } + }) +} + +func TestPrivateKey(t *testing.T) { + assertNoPanic(t, func() { + var ( + expected, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + ca = New(PrivateKey(expected)) + actual = ca.PrivateKey.(*ecdsa.PrivateKey) + ) + + if actual.D.Cmp(expected.D) != 0 { + t.Fatalf("bad D. expected '%s', got '%s'", expected.D.String(), actual.D.String()) + } + + if actual.X.Cmp(expected.X) != 0 { + t.Fatalf("bad X. expected '%s', got '%s'", expected.X.String(), actual.X.String()) + } + + if actual.Y.Cmp(expected.Y) != 0 { + t.Fatalf("bad Y. expected '%s', got '%s'", expected.Y.String(), actual.Y.String()) + } + }) +} + +func TestIssuer(t *testing.T) { + assertNoPanic(t, func() { + var ( + root = New(IsCA) + inter = New(Issuer(root)) + + expected = root.Certificate.RawSubject + actual = inter.Certificate.RawIssuer + ) + + if !bytes.Equal(actual, expected) { + t.Fatalf("bad issuer. expected '%s', got '%s'", string(expected), string(actual)) + } + + if err := inter.Certificate.CheckSignatureFrom(root.Certificate); err != nil { + t.Fatal(err) + } + }) +} + +func TestIsCA(t *testing.T) { + var ( + normal = New() + ca = New(IsCA) + ) + + if normal.Certificate.IsCA { + t.Fatal("expected normal cert not to be CA") + } + + if !ca.Certificate.IsCA { + t.Fatal("expected CA cert to be CA") + } +} + +func TestChain(t *testing.T) { + var ( + ca = New(IsCA) + inter = ca.Issue(IsCA) + leaf = inter.Issue() + ) + + if !leaf.Chain()[0].Equal(leaf.Certificate) { + t.Fatal() + } + + if !leaf.Chain()[1].Equal(inter.Certificate) { + t.Fatal() + } + + if !leaf.Chain()[2].Equal(ca.Certificate) { + t.Fatal() + } +} + +func TestChainPool(t *testing.T) { + var ( + ca = New(IsCA) + inter = ca.Issue(IsCA) + leaf = inter.Issue() + ) + + _, err := leaf.Certificate.Verify(x509.VerifyOptions{ + Roots: ca.ChainPool(), + Intermediates: leaf.ChainPool(), + }) + + if err != nil { + t.Fatal(err) + } +} + +func TestPFX(t *testing.T) { + assertNoPanic(t, func() { + New().PFX("asdf") + }) +} + +func TestAIA(t *testing.T) { + i := New(IssuingCertificateURL("a", "b"), OCSPServer("c", "d")) + + if !reflect.DeepEqual(i.Certificate.IssuingCertificateURL, []string{"a", "b"}) { + t.Error("bad IssuingCertificateURL: ", i.Certificate.IssuingCertificateURL) + } + + if !reflect.DeepEqual(i.Certificate.OCSPServer, []string{"c", "d"}) { + t.Error("bad OCSPServer: ", i.Certificate.OCSPServer) + } +} + +func assertNoPanic(t *testing.T, cb func()) { + // Check that t.Helper() is defined for Go<1.9 + if h, ok := interface{}(t).(interface{ Helper() }); ok { + h.Helper() + } + + defer func() { + if r := recover(); r != nil { + t.Fatal(r) + } + }() + + cb() +} + +func TestKeyUsage(t *testing.T) { + root := New(IsCA, KeyUsage(x509.KeyUsageCertSign)) + if root.Certificate.KeyUsage != x509.KeyUsageCertSign { + t.Fatalf("expected %x, got %d", x509.KeyUsageCertSign, root.Certificate.KeyUsage) + } + + leaf := root.Issue(KeyUsage(x509.KeyUsageDataEncipherment | x509.KeyUsageDigitalSignature)) + if leaf.Certificate.KeyUsage != x509.KeyUsageDataEncipherment|x509.KeyUsageDigitalSignature { + t.Fatalf("expected %x, got %d", x509.KeyUsageDataEncipherment|x509.KeyUsageDigitalSignature, leaf.Certificate.KeyUsage) + } +} diff --git a/fakeca/identity.go b/fakeca/identity.go new file mode 100644 index 0000000..19ec9a4 --- /dev/null +++ b/fakeca/identity.go @@ -0,0 +1,144 @@ +package fakeca + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os/exec" +) + +// Identity is a certificate and private key. +type Identity struct { + Issuer *Identity + PrivateKey crypto.Signer + Certificate *x509.Certificate + NextSN int64 +} + +// New creates a new CA. +func New(opts ...Option) *Identity { + c := &configuration{} + + for _, opt := range opts { + option(opt)(c) + } + + return c.generate() +} + +// Issue issues a new Identity with this one as its parent. +func (id *Identity) Issue(opts ...Option) *Identity { + opts = append(opts, Issuer(id)) + return New(opts...) +} + +// PFX wraps the certificate and private key in an encrypted PKCS#12 packet. The +// provided password must be alphanumeric. +func (id *Identity) PFX(password string) []byte { + return toPFX(id.Certificate, id.PrivateKey, password) +} + +// Chain builds a slice of *x509.Certificate from this CA and its issuers. +func (id *Identity) Chain() []*x509.Certificate { + chain := []*x509.Certificate{} + for this := id; this != nil; this = this.Issuer { + chain = append(chain, this.Certificate) + } + + return chain +} + +// ChainPool builds an *x509.CertPool from this CA and its issuers. +func (id *Identity) ChainPool() *x509.CertPool { + chain := x509.NewCertPool() + for this := id; this != nil; this = this.Issuer { + chain.AddCert(this.Certificate) + } + + return chain +} + +// IncrementSN returns the next serial number. +func (id *Identity) IncrementSN() int64 { + defer func() { + id.NextSN++ + }() + + return id.NextSN +} + +func toPFX(cert *x509.Certificate, priv interface{}, password string) []byte { + // only allow alphanumeric passwords + for _, c := range password { + switch { + case c >= 'a' && c <= 'z': + case c >= 'A' && c <= 'Z': + case c >= '0' && c <= '9': + default: + panic("password must be alphanumeric") + } + } + + passout := fmt.Sprintf("pass:%s", password) + cmd := exec.Command("openssl", "pkcs12", "-export", "-passout", passout) + + cmd.Stdin = bytes.NewReader(append(append(toPKCS8(priv), '\n'), toPEM(cert)...)) + + out := new(bytes.Buffer) + cmd.Stdout = out + + if err := cmd.Run(); err != nil { + panic(err) + } + + return out.Bytes() +} + +func toPEM(cert *x509.Certificate) []byte { + buf := new(bytes.Buffer) + if err := pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { + panic(err) + } + + return buf.Bytes() +} + +func toDER(priv interface{}) []byte { + var ( + der []byte + err error + ) + switch p := priv.(type) { + case *rsa.PrivateKey: + der = x509.MarshalPKCS1PrivateKey(p) + case *ecdsa.PrivateKey: + der, err = x509.MarshalECPrivateKey(p) + default: + err = errors.New("unknown key type") + } + if err != nil { + panic(err) + } + + return der +} + +func toPKCS8(priv interface{}) []byte { + cmd := exec.Command("openssl", "pkcs8", "-topk8", "-nocrypt", "-inform", "DER") + + cmd.Stdin = bytes.NewReader(toDER(priv)) + + out := new(bytes.Buffer) + cmd.Stdout = out + + if err := cmd.Run(); err != nil { + panic(err) + } + + return out.Bytes() +} diff --git a/go.mod b/go.mod index f69e5dc..ba48589 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,10 @@ go 1.12 require ( github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261 - github.com/davecgh/go-spew v1.1.1 - github.com/github/certstore v0.1.0 - github.com/github/fakeca v0.1.0 - github.com/github/ietf-cms v0.1.2 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b github.com/pkg/errors v0.8.1 - github.com/pmezard/go-difflib v1.0.0 github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 ) diff --git a/go.sum b/go.sum index 461a79a..0ce1304 100644 --- a/go.sum +++ b/go.sum @@ -3,18 +3,8 @@ github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEex github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/github/certstore v0.1.0 h1:oZF2PcqgBo6YNp7gCUDfF6vP9c0kTxh5VhUNrW6d2wc= -github.com/github/certstore v0.1.0/go.mod h1:Sgb3YVYOB2iCO06NJ6We5gjXe7uxxM3zPYoEXjuTKno= -github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= -github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= -github.com/github/ietf-cms v0.1.0 h1:D+O9re6xDeWTYRpAFTfM0dm5NqJUcXZKFGOQg5Iq6Ls= -github.com/github/ietf-cms v0.1.0/go.mod h1:eJEmhqWUqjpuS6OoXiqtuTmzOx4u81npQrXOzt/sPqo= -github.com/github/ietf-cms v0.1.2 h1:YR01RZde22wi0FDNjlxfrzSdftlTSqjMXVQKXTnP3k4= -github.com/github/ietf-cms v0.1.2/go.mod h1:cVKaskLRggxGmj5FRRU4OxryCkKSnYq4K32xYWOxnl0= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b h1:K1wa7ads2Bu1PavI6LfBRMYSy6Zi+Rky0OhWBfrmkmY= github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -22,7 +12,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -30,4 +19,5 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ietf-cms/.gitignore b/ietf-cms/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/ietf-cms/.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/ietf-cms/LICENSE.md b/ietf-cms/LICENSE.md new file mode 100644 index 0000000..7800c58 --- /dev/null +++ b/ietf-cms/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Ben Toews. + +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/ietf-cms/README.md b/ietf-cms/README.md new file mode 100644 index 0000000..0d7aef7 --- /dev/null +++ b/ietf-cms/README.md @@ -0,0 +1,61 @@ +# CMS [![PkgGoDev](https://pkg.go.dev/badge/github.com/github/ietf-cms?tab=doc)](https://pkg.go.dev/github.com/github/ietf-cms?tab=doc) [![Report card](https://goreportcard.com/badge/github.com/github/ietf-cms)](https://goreportcard.com/report/github.com/github/ietf-cms) + +[![Test (recent Go versions)]()](https://github.com/github/ietf-cms/actions?query=workflow%3A%22Test+%28recent+Go+versions%29%22) +[![Test (Go 1.10)]()](https://github.com/github/ietf-cms/actions?query=workflow%3A%22Test+%28Go+1.10%29%22) + +[CMS (Cryptographic Message Syntax)](https://tools.ietf.org/html/rfc5652) is a syntax for signing, digesting, and encrypting arbitrary messages. It evolved from PKCS#7 and is the basis for higher level protocols such as S/MIME. This package implements the SignedData CMS content-type, allowing users to digitally sign data as well as verify data signed by others. + +## Signing and Verifying Data + +High level APIs are provided for signing a message with a certificate and key: + +```go +msg := []byte("some data") +cert, _ := x509.ParseCertificate(someCertificateData) +key, _ := x509.ParseECPrivateKey(somePrivateKeyData) + +der, _ := cms.Sign(msg, []*x509.Certificate{cert}, key) + +//// +/// At another time, in another place... +// + +sd, _ := ParseSignedData(der) +if err, _ := sd.Verify(x509.VerifyOptions{}); err != nil { + panic(err) +} +``` + +By default, CMS SignedData includes the original message. High level APIs are also available for creating and verifying detached signatures: + +```go +msg := []byte("some data") +cert, _ := x509.ParseCertificate(someCertificateData) +key, _ := x509.ParseECPrivateKey(somePrivateKeyData) + +der, _ := cms.SignDetached(msg, cert, key) + +//// +/// At another time, in another place... +// + +sd, _ := ParseSignedData(der) +if err, _ := sd.VerifyDetached(msg, x509.VerifyOptions{}); err != nil { + panic(err) +} +``` + +## Timestamping + +Because certificates expire and can be revoked, it is may be helpful to attach certified timestamps to signatures, proving that they existed at a given time. RFC3161 timestamps can be added to signatures like so: + +```go +signedData, _ := NewSignedData([]byte("Hello, world!")) +signedData.Sign(identity.Chain(), identity.PrivateKey) +signedData.AddTimestamps("http://timestamp.digicert.com") + +derEncoded, _ := signedData.ToDER() +io.Copy(os.Stdout, bytes.NewReader(derEncoded)) +``` + +Verification functions implicitly verify timestamps as well. Without a timestamp, verification will fail if the certificate is no longer valid. diff --git a/ietf-cms/main_test.go b/ietf-cms/main_test.go new file mode 100644 index 0000000..47a9599 --- /dev/null +++ b/ietf-cms/main_test.go @@ -0,0 +1,166 @@ +package cms + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/asn1" + "io" + "io/ioutil" + "math/big" + "net/http" + "time" + + "github.com/github/smimesign/fakeca" + "github.com/github/smimesign/ietf-cms/oid" + "github.com/github/smimesign/ietf-cms/protocol" + "github.com/github/smimesign/ietf-cms/timestamp" +) + +var ( + // fake PKI setup + root = fakeca.New(fakeca.IsCA) + otherRoot = fakeca.New(fakeca.IsCA) + + intermediateKey, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + intermediate = root.Issue(fakeca.IsCA, fakeca.PrivateKey(intermediateKey)) + + leaf = intermediate.Issue( + fakeca.NotBefore(time.Now().Add(-time.Hour)), + fakeca.NotAfter(time.Now().Add(time.Hour)), + ) + + rootOpts = x509.VerifyOptions{Roots: root.ChainPool()} + otherRootOpts = x509.VerifyOptions{Roots: otherRoot.ChainPool()} + intermediateOpts = x509.VerifyOptions{Roots: intermediate.ChainPool()} + + // fake timestamp authority setup + tsa = &testTSA{ident: intermediate.Issue()} + thc = &testHTTPClient{tsa} +) + +func init() { + timestamp.DefaultHTTPClient = thc +} + +type testTSA struct { + ident *fakeca.Identity + sn int64 + hookInfo func(timestamp.Info) timestamp.Info + hookToken func(*protocol.SignedData) *protocol.SignedData + hookResponse func(timestamp.Response) timestamp.Response +} + +func (tt *testTSA) Clear() { + tt.hookInfo = nil + tt.hookToken = nil + tt.hookResponse = nil +} + +func (tt *testTSA) HookInfo(hook func(timestamp.Info) timestamp.Info) { + tt.Clear() + tt.hookInfo = hook +} + +func (tt *testTSA) HookToken(hook func(*protocol.SignedData) *protocol.SignedData) { + tt.Clear() + tt.hookToken = hook +} + +func (tt *testTSA) HookResponse(hook func(timestamp.Response) timestamp.Response) { + tt.Clear() + tt.hookResponse = hook +} + +func (tt *testTSA) nextSN() *big.Int { + defer func() { tt.sn++ }() + return big.NewInt(tt.sn) +} + +func (tt *testTSA) Do(req timestamp.Request) (timestamp.Response, error) { + info := timestamp.Info{ + Version: 1, + Policy: asn1.ObjectIdentifier{1, 2, 3}, + SerialNumber: tt.nextSN(), + GenTime: time.Now(), + MessageImprint: req.MessageImprint, + Nonce: req.Nonce, + } + + if tt.hookInfo != nil { + info = tt.hookInfo(info) + } + + eciDER, err := asn1.Marshal(info) + if err != nil { + panic(err) + } + + eci, err := protocol.NewEncapsulatedContentInfo(oid.ContentTypeTSTInfo, eciDER) + if err != nil { + panic(err) + } + + tst, err := protocol.NewSignedData(eci) + if err != nil { + panic(err) + } + + if err = tst.AddSignerInfo(tsa.ident.Chain(), tsa.ident.PrivateKey); err != nil { + panic(err) + } + + if tt.hookToken != nil { + tt.hookToken(tst) + } + + ci, err := tst.ContentInfo() + if err != nil { + panic(err) + } + + resp := timestamp.Response{ + Status: timestamp.PKIStatusInfo{Status: 0}, + TimeStampToken: ci, + } + + if tt.hookResponse != nil { + resp = tt.hookResponse(resp) + } + + return resp, nil +} + +type testHTTPClient struct { + tt *testTSA +} + +func (thc *testHTTPClient) Do(httpReq *http.Request) (*http.Response, error) { + buf := new(bytes.Buffer) + if _, err := io.Copy(buf, httpReq.Body); err != nil { + return nil, err + } + + var tsReq timestamp.Request + if _, err := asn1.Unmarshal(buf.Bytes(), &tsReq); err != nil { + return nil, err + } + + tsResp, err := thc.tt.Do(tsReq) + if err != nil { + return nil, err + } + + respDER, err := asn1.Marshal(tsResp) + if err != nil { + return nil, err + } + + return &http.Response{ + StatusCode: 200, + Header: http.Header{"Content-Type": {"application/timestamp-reply"}}, + Body: ioutil.NopCloser(bytes.NewReader(respDER)), + }, nil +} diff --git a/ietf-cms/oid/oid.go b/ietf-cms/oid/oid.go new file mode 100644 index 0000000..f9cac43 --- /dev/null +++ b/ietf-cms/oid/oid.go @@ -0,0 +1,142 @@ +// Package oid contains OIDs that are used by other packages in this repository. +package oid + +import ( + "crypto" + "crypto/x509" + "encoding/asn1" +) + +var ( + ContentTypeData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1} + ContentTypeSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2} + ContentTypeTSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4} + + AttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3} + AttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4} + AttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5} + AttributeTimeStampToken = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14} + + PublicKeyAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} + PublicKeyAlgorithmECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1} + + DigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26} + DigestAlgorithmMD5 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 5} + DigestAlgorithmSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1} + DigestAlgorithmSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2} + DigestAlgorithmSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3} + + SignatureAlgorithmMD2WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 2} + SignatureAlgorithmMD5WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 4} + SignatureAlgorithmSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5} + SignatureAlgorithmSHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11} + SignatureAlgorithmSHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12} + SignatureAlgorithmSHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13} + SignatureAlgorithmRSAPSS = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} + SignatureAlgorithmDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10040, 4, 3} + SignatureAlgorithmDSAWithSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 3, 2} + SignatureAlgorithmECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1} + SignatureAlgorithmECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2} + SignatureAlgorithmECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3} + SignatureAlgorithmECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4} + SignatureAlgorithmISOSHA1WithRSA = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 29} + + ExtensionSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14} +) + +// DigestAlgorithmToCryptoHash maps digest OIDs to crypto.Hash values. +var DigestAlgorithmToCryptoHash = map[string]crypto.Hash{ + DigestAlgorithmSHA1.String(): crypto.SHA1, + DigestAlgorithmMD5.String(): crypto.MD5, + DigestAlgorithmSHA256.String(): crypto.SHA256, + DigestAlgorithmSHA384.String(): crypto.SHA384, + DigestAlgorithmSHA512.String(): crypto.SHA512, +} + +// CryptoHashToDigestAlgorithm maps crypto.Hash values to digest OIDs. +var CryptoHashToDigestAlgorithm = map[crypto.Hash]asn1.ObjectIdentifier{ + crypto.SHA1: DigestAlgorithmSHA1, + crypto.MD5: DigestAlgorithmMD5, + crypto.SHA256: DigestAlgorithmSHA256, + crypto.SHA384: DigestAlgorithmSHA384, + crypto.SHA512: DigestAlgorithmSHA512, +} + +// X509SignatureAlgorithmToDigestAlgorithm maps x509.SignatureAlgorithm to +// digestAlgorithm OIDs. +var X509SignatureAlgorithmToDigestAlgorithm = map[x509.SignatureAlgorithm]asn1.ObjectIdentifier{ + x509.SHA1WithRSA: DigestAlgorithmSHA1, + x509.MD5WithRSA: DigestAlgorithmMD5, + x509.SHA256WithRSA: DigestAlgorithmSHA256, + x509.SHA384WithRSA: DigestAlgorithmSHA384, + x509.SHA512WithRSA: DigestAlgorithmSHA512, + x509.ECDSAWithSHA1: DigestAlgorithmSHA1, + x509.ECDSAWithSHA256: DigestAlgorithmSHA256, + x509.ECDSAWithSHA384: DigestAlgorithmSHA384, + x509.ECDSAWithSHA512: DigestAlgorithmSHA512, +} + +// X509SignatureAlgorithmToPublicKeyAlgorithm maps x509.SignatureAlgorithm to +// signatureAlgorithm OIDs. +var X509SignatureAlgorithmToPublicKeyAlgorithm = map[x509.SignatureAlgorithm]asn1.ObjectIdentifier{ + x509.SHA1WithRSA: PublicKeyAlgorithmRSA, + x509.MD5WithRSA: PublicKeyAlgorithmRSA, + x509.SHA256WithRSA: PublicKeyAlgorithmRSA, + x509.SHA384WithRSA: PublicKeyAlgorithmRSA, + x509.SHA512WithRSA: PublicKeyAlgorithmRSA, + x509.ECDSAWithSHA1: PublicKeyAlgorithmECDSA, + x509.ECDSAWithSHA256: PublicKeyAlgorithmECDSA, + x509.ECDSAWithSHA384: PublicKeyAlgorithmECDSA, + x509.ECDSAWithSHA512: PublicKeyAlgorithmECDSA, +} + +// PublicKeyAndDigestAlgorithmToX509SignatureAlgorithm maps digest and signature +// OIDs to x509.SignatureAlgorithm values. +var PublicKeyAndDigestAlgorithmToX509SignatureAlgorithm = map[string]map[string]x509.SignatureAlgorithm{ + PublicKeyAlgorithmRSA.String(): map[string]x509.SignatureAlgorithm{ + DigestAlgorithmSHA1.String(): x509.SHA1WithRSA, + DigestAlgorithmMD5.String(): x509.MD5WithRSA, + DigestAlgorithmSHA256.String(): x509.SHA256WithRSA, + DigestAlgorithmSHA384.String(): x509.SHA384WithRSA, + DigestAlgorithmSHA512.String(): x509.SHA512WithRSA, + }, + PublicKeyAlgorithmECDSA.String(): map[string]x509.SignatureAlgorithm{ + DigestAlgorithmSHA1.String(): x509.ECDSAWithSHA1, + DigestAlgorithmSHA256.String(): x509.ECDSAWithSHA256, + DigestAlgorithmSHA384.String(): x509.ECDSAWithSHA384, + DigestAlgorithmSHA512.String(): x509.ECDSAWithSHA512, + }, +} + +// SignatureAlgorithmToX509SignatureAlgorithm maps signature algorithm OIDs to +// x509.SignatureAlgorithm values. +var SignatureAlgorithmToX509SignatureAlgorithm = map[string]x509.SignatureAlgorithm{ + SignatureAlgorithmSHA1WithRSA.String(): x509.SHA1WithRSA, + SignatureAlgorithmMD5WithRSA.String(): x509.MD5WithRSA, + SignatureAlgorithmSHA256WithRSA.String(): x509.SHA256WithRSA, + SignatureAlgorithmSHA384WithRSA.String(): x509.SHA384WithRSA, + SignatureAlgorithmSHA512WithRSA.String(): x509.SHA512WithRSA, + SignatureAlgorithmECDSAWithSHA1.String(): x509.ECDSAWithSHA1, + SignatureAlgorithmECDSAWithSHA256.String(): x509.ECDSAWithSHA256, + SignatureAlgorithmECDSAWithSHA384.String(): x509.ECDSAWithSHA384, + SignatureAlgorithmECDSAWithSHA512.String(): x509.ECDSAWithSHA512, + SignatureAlgorithmDSAWithSHA1.String(): x509.DSAWithSHA1, +} + +// X509PublicKeyAndDigestAlgorithmToSignatureAlgorithm maps X509 public key and +// digest algorithms to to SignatureAlgorithm OIDs. +var X509PublicKeyAndDigestAlgorithmToSignatureAlgorithm = map[x509.PublicKeyAlgorithm]map[string]asn1.ObjectIdentifier{ + x509.RSA: map[string]asn1.ObjectIdentifier{ + DigestAlgorithmSHA1.String(): SignatureAlgorithmSHA1WithRSA, + DigestAlgorithmMD5.String(): SignatureAlgorithmMD5WithRSA, + DigestAlgorithmSHA256.String(): SignatureAlgorithmSHA256WithRSA, + DigestAlgorithmSHA384.String(): SignatureAlgorithmSHA384WithRSA, + DigestAlgorithmSHA512.String(): SignatureAlgorithmSHA512WithRSA, + }, + x509.ECDSA: map[string]asn1.ObjectIdentifier{ + DigestAlgorithmSHA1.String(): SignatureAlgorithmECDSAWithSHA1, + DigestAlgorithmSHA256.String(): SignatureAlgorithmECDSAWithSHA256, + DigestAlgorithmSHA384.String(): SignatureAlgorithmECDSAWithSHA384, + DigestAlgorithmSHA512.String(): SignatureAlgorithmECDSAWithSHA512, + }, +} diff --git a/ietf-cms/protocol/LICENSE b/ietf-cms/protocol/LICENSE new file mode 100644 index 0000000..75f3209 --- /dev/null +++ b/ietf-cms/protocol/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/ietf-cms/protocol/README.md b/ietf-cms/protocol/README.md new file mode 100644 index 0000000..05512cf --- /dev/null +++ b/ietf-cms/protocol/README.md @@ -0,0 +1,5 @@ +# cms/protocol [![GoDoc](https://godoc.org/github.com/github/ietf-cms/protocol?status.svg)](https://godoc.org/github.com/github/ietf-cms/protocol) + +cms/protocol implements low-level parsing of CMS (PKCS#7) data. + +This package is based off https://github.com/fullsailor/pkcs7 and contains the license from that repository. diff --git a/ietf-cms/protocol/any_set.go b/ietf-cms/protocol/any_set.go new file mode 100644 index 0000000..bad1ca4 --- /dev/null +++ b/ietf-cms/protocol/any_set.go @@ -0,0 +1,63 @@ +package protocol + +import ( + "encoding/asn1" + "fmt" +) + +// AnySet is a helper for dealing with SET OF ANY types. +type AnySet struct { + Elements []asn1.RawValue `asn1:"set"` +} + +// NewAnySet creates a new AnySet. +func NewAnySet(elts ...asn1.RawValue) AnySet { + return AnySet{elts} +} + +// DecodeAnySet manually decodes a SET OF ANY type, since Go's parser can't +// handle them. +func DecodeAnySet(rv asn1.RawValue) (as AnySet, err error) { + // Make sure it's really a SET. + if rv.Class != asn1.ClassUniversal { + err = ASN1Error{fmt.Sprintf("Bad class. Expecting %d, got %d", asn1.ClassUniversal, rv.Class)} + return + } + if rv.Tag != asn1.TagSet { + err = ASN1Error{fmt.Sprintf("Bad tag. Expecting %d, got %d", asn1.TagSet, rv.Tag)} + return + } + + // Decode each element. + der := rv.Bytes + for len(der) > 0 { + if der, err = asn1.Unmarshal(der, &rv); err != nil { + return + } + + as.Elements = append(as.Elements, rv) + } + + return +} + +// Encode manually encodes a SET OF ANY type, since Go's parser can't handle +// them. +func (as AnySet) Encode(dst *asn1.RawValue) (err error) { + dst.Class = asn1.ClassUniversal + dst.Tag = asn1.TagSet + dst.IsCompound = true + + var der []byte + for _, elt := range as.Elements { + if der, err = asn1.Marshal(elt); err != nil { + return + } + + dst.Bytes = append(dst.Bytes, der...) + } + + dst.FullBytes, err = asn1.Marshal(*dst) + + return +} diff --git a/ietf-cms/protocol/any_set_test.go b/ietf-cms/protocol/any_set_test.go new file mode 100644 index 0000000..2195720 --- /dev/null +++ b/ietf-cms/protocol/any_set_test.go @@ -0,0 +1,59 @@ +package protocol + +import ( + "bytes" + "encoding/asn1" + "encoding/hex" + "testing" +) + +func TestAnySet(t *testing.T) { + // OpenSSL::ASN1::Set.new([ + // OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(5)), + // OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(6)) + // ]) + der := []byte{49, 6, 2, 1, 5, 2, 1, 6} + + var rv asn1.RawValue + if rest, err := asn1.Unmarshal(der, &rv); err != nil { + t.Fatal(err) + } else if len(rest) > 0 { + t.Fatal("trailing data") + } + + as, err := DecodeAnySet(rv) + if err != nil { + t.Fatal(err) + } + + if len(as.Elements) != 2 { + t.Fatal("bad decoded values") + } + + var i int + if rest, err := asn1.Unmarshal(as.Elements[0].FullBytes, &i); err != nil { + t.Fatal(err) + } else if len(rest) > 0 { + t.Fatal("trailing data") + } + if i != 5 { + t.Fatalf("bad decoded value: %d", i) + } + + if rest, err := asn1.Unmarshal(as.Elements[1].FullBytes, &i); err != nil { + t.Fatal(err) + } else if len(rest) > 0 { + t.Fatal("trailing data") + } + if i != 6 { + t.Fatalf("bad decoded value: %d", i) + } + + var rv2 asn1.RawValue + if err := as.Encode(&rv2); err != nil { + t.Fatal(err) + } + if !bytes.Equal(rv.FullBytes, rv2.FullBytes) { + t.Fatal(hex.EncodeToString(rv2.FullBytes), " != ", hex.EncodeToString(rv.FullBytes)) + } +} diff --git a/ietf-cms/protocol/ber.go b/ietf-cms/protocol/ber.go new file mode 100644 index 0000000..f1bdb7d --- /dev/null +++ b/ietf-cms/protocol/ber.go @@ -0,0 +1,248 @@ +package protocol + +import ( + "bytes" +) + +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 +} + +// BER2DER attempts to convert BER encoded data to DER encoding. +func BER2DER(ber []byte) ([]byte, error) { + if len(ber) == 0 { + return nil, ASN1Error{"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, ASN1Error{"ber2der: BER tag length too long"} + } + if numberOfBytes == 4 && (int)(ber[offset]) > 0x7F { + return nil, 0, ASN1Error{"ber2der: BER tag length is negative"} + } + if 0x0 == (int)(ber[offset]) { + return nil, 0, ASN1Error{"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, ASN1Error{"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, ASN1Error{"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, ASN1Error{"ber2der: Invalid BER format"} + } + + return bytes.Index(ber[offset:], []byte{0x0, 0x0}) == 0, nil +} diff --git a/ietf-cms/protocol/ber_test.go b/ietf-cms/protocol/ber_test.go new file mode 100644 index 0000000..accc3b8 --- /dev/null +++ b/ietf-cms/protocol/ber_test.go @@ -0,0 +1,99 @@ +package protocol + +import ( + "bytes" + "encoding/asn1" + "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) + } + + der2, err := BER2DER(der) + if 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) + } + + der2, err := BER2DER(der) + if 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/ietf-cms/protocol/cms.asn1 b/ietf-cms/protocol/cms.asn1 new file mode 100644 index 0000000..f373440 --- /dev/null +++ b/ietf-cms/protocol/cms.asn1 @@ -0,0 +1,328 @@ +CryptographicMessageSyntax2004 + { iso(1) member-body(2) us(840) rsadsi(113549) + pkcs(1) pkcs-9(9) smime(16) modules(0) cms-2004(24) } + + DEFINITIONS IMPLICIT TAGS ::= + BEGIN + + -- EXPORTS All + -- The types and values defined in this module are exported for use + -- in the other ASN.1 modules. Other applications may use them for + -- their own purposes. + + IMPORTS + + -- Imports from RFC 5280 [PROFILE], Appendix A.1 + AlgorithmIdentifier, Certificate, CertificateList, + CertificateSerialNumber, Name + FROM PKIX1Explicit88 + { iso(1) identified-organization(3) dod(6) + internet(1) security(5) mechanisms(5) pkix(7) + mod(0) pkix1-explicit(18) } + + -- Imports from RFC 3281 [ACPROFILE], Appendix B + AttributeCertificate + FROM PKIXAttributeCertificate + { iso(1) identified-organization(3) dod(6) + internet(1) security(5) mechanisms(5) pkix(7) + mod(0) attribute-cert(12) } + + -- Imports from Appendix B of this document + AttributeCertificateV1 + FROM AttributeCertificateVersion1 + { iso(1) member-body(2) us(840) rsadsi(113549) + pkcs(1) pkcs-9(9) smime(16) modules(0) + v1AttrCert(15) } ; + + -- Cryptographic Message Syntax + + ContentInfo ::= SEQUENCE { + contentType ContentType, + content [0] EXPLICIT ANY DEFINED BY contentType } + + ContentType ::= OBJECT IDENTIFIER + + SignedData ::= SEQUENCE { + version CMSVersion, + digestAlgorithms DigestAlgorithmIdentifiers, + encapContentInfo EncapsulatedContentInfo, + certificates [0] IMPLICIT CertificateSet OPTIONAL, + crls [1] IMPLICIT RevocationInfoChoices OPTIONAL, + signerInfos SignerInfos } + + DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier + + SignerInfos ::= SET OF SignerInfo + + EncapsulatedContentInfo ::= SEQUENCE { + eContentType ContentType, + eContent [0] EXPLICIT OCTET STRING OPTIONAL } + + SignerInfo ::= SEQUENCE { + version CMSVersion, + sid SignerIdentifier, + digestAlgorithm DigestAlgorithmIdentifier, + signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL, + signatureAlgorithm SignatureAlgorithmIdentifier, + signature SignatureValue, + unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL } + + SignerIdentifier ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + subjectKeyIdentifier [0] SubjectKeyIdentifier } + + SignedAttributes ::= SET SIZE (1..MAX) OF Attribute + + UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute + + Attribute ::= SEQUENCE { + attrType OBJECT IDENTIFIER, + attrValues SET OF AttributeValue } + + AttributeValue ::= ANY + + SignatureValue ::= OCTET STRING + + EnvelopedData ::= SEQUENCE { + version CMSVersion, + originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL, + recipientInfos RecipientInfos, + encryptedContentInfo EncryptedContentInfo, + unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL } + + OriginatorInfo ::= SEQUENCE { + certs [0] IMPLICIT CertificateSet OPTIONAL, + crls [1] IMPLICIT RevocationInfoChoices OPTIONAL } + + RecipientInfos ::= SET SIZE (1..MAX) OF RecipientInfo + + EncryptedContentInfo ::= SEQUENCE { + contentType ContentType, + contentEncryptionAlgorithm ContentEncryptionAlgorithmIdentifier, + encryptedContent [0] IMPLICIT EncryptedContent OPTIONAL } + + EncryptedContent ::= OCTET STRING + + UnprotectedAttributes ::= SET SIZE (1..MAX) OF Attribute + + RecipientInfo ::= CHOICE { + ktri KeyTransRecipientInfo, + kari [1] KeyAgreeRecipientInfo, + kekri [2] KEKRecipientInfo, + pwri [3] PasswordRecipientInfo, + ori [4] OtherRecipientInfo } + + EncryptedKey ::= OCTET STRING + + KeyTransRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 0 or 2 + rid RecipientIdentifier, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + encryptedKey EncryptedKey } + + RecipientIdentifier ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + subjectKeyIdentifier [0] SubjectKeyIdentifier } + + KeyAgreeRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 3 + originator [0] EXPLICIT OriginatorIdentifierOrKey, + ukm [1] EXPLICIT UserKeyingMaterial OPTIONAL, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + recipientEncryptedKeys RecipientEncryptedKeys } + + OriginatorIdentifierOrKey ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + subjectKeyIdentifier [0] SubjectKeyIdentifier, + originatorKey [1] OriginatorPublicKey } + + OriginatorPublicKey ::= SEQUENCE { + algorithm AlgorithmIdentifier, + publicKey BIT STRING } + + RecipientEncryptedKeys ::= SEQUENCE OF RecipientEncryptedKey + + RecipientEncryptedKey ::= SEQUENCE { + rid KeyAgreeRecipientIdentifier, + encryptedKey EncryptedKey } + + KeyAgreeRecipientIdentifier ::= CHOICE { + issuerAndSerialNumber IssuerAndSerialNumber, + rKeyId [0] IMPLICIT RecipientKeyIdentifier } + + RecipientKeyIdentifier ::= SEQUENCE { + subjectKeyIdentifier SubjectKeyIdentifier, + date GeneralizedTime OPTIONAL, + other OtherKeyAttribute OPTIONAL } + + SubjectKeyIdentifier ::= OCTET STRING + + KEKRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 4 + kekid KEKIdentifier, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + encryptedKey EncryptedKey } + + KEKIdentifier ::= SEQUENCE { + keyIdentifier OCTET STRING, + date GeneralizedTime OPTIONAL, + other OtherKeyAttribute OPTIONAL } + + PasswordRecipientInfo ::= SEQUENCE { + version CMSVersion, -- always set to 0 + keyDerivationAlgorithm [0] KeyDerivationAlgorithmIdentifier + OPTIONAL, + keyEncryptionAlgorithm KeyEncryptionAlgorithmIdentifier, + encryptedKey EncryptedKey } + + OtherRecipientInfo ::= SEQUENCE { + oriType OBJECT IDENTIFIER, + oriValue ANY DEFINED BY oriType } + + DigestedData ::= SEQUENCE { + version CMSVersion, + digestAlgorithm DigestAlgorithmIdentifier, + encapContentInfo EncapsulatedContentInfo, + digest Digest } + + Digest ::= OCTET STRING + + EncryptedData ::= SEQUENCE { + version CMSVersion, + encryptedContentInfo EncryptedContentInfo, + unprotectedAttrs [1] IMPLICIT UnprotectedAttributes OPTIONAL } + + AuthenticatedData ::= SEQUENCE { + version CMSVersion, + originatorInfo [0] IMPLICIT OriginatorInfo OPTIONAL, + recipientInfos RecipientInfos, + macAlgorithm MessageAuthenticationCodeAlgorithm, + digestAlgorithm [1] DigestAlgorithmIdentifier OPTIONAL, + encapContentInfo EncapsulatedContentInfo, + authAttrs [2] IMPLICIT AuthAttributes OPTIONAL, + mac MessageAuthenticationCode, + unauthAttrs [3] IMPLICIT UnauthAttributes OPTIONAL } + + AuthAttributes ::= SET SIZE (1..MAX) OF Attribute + + UnauthAttributes ::= SET SIZE (1..MAX) OF Attribute + + MessageAuthenticationCode ::= OCTET STRING + + DigestAlgorithmIdentifier ::= AlgorithmIdentifier + + SignatureAlgorithmIdentifier ::= AlgorithmIdentifier + + KeyEncryptionAlgorithmIdentifier ::= AlgorithmIdentifier + + ContentEncryptionAlgorithmIdentifier ::= AlgorithmIdentifier + + MessageAuthenticationCodeAlgorithm ::= AlgorithmIdentifier + + KeyDerivationAlgorithmIdentifier ::= AlgorithmIdentifier + + RevocationInfoChoices ::= SET OF RevocationInfoChoice + + RevocationInfoChoice ::= CHOICE { + crl CertificateList, + other [1] IMPLICIT OtherRevocationInfoFormat } + + OtherRevocationInfoFormat ::= SEQUENCE { + otherRevInfoFormat OBJECT IDENTIFIER, + otherRevInfo ANY DEFINED BY otherRevInfoFormat } + + CertificateChoices ::= CHOICE { + certificate Certificate, + extendedCertificate [0] IMPLICIT ExtendedCertificate, -- Obsolete + v1AttrCert [1] IMPLICIT AttributeCertificateV1, -- Obsolete + v2AttrCert [2] IMPLICIT AttributeCertificateV2, + other [3] IMPLICIT OtherCertificateFormat } + + AttributeCertificateV2 ::= AttributeCertificate + + OtherCertificateFormat ::= SEQUENCE { + otherCertFormat OBJECT IDENTIFIER, + otherCert ANY DEFINED BY otherCertFormat } + + CertificateSet ::= SET OF CertificateChoices + + IssuerAndSerialNumber ::= SEQUENCE { + issuer Name, + serialNumber CertificateSerialNumber } + + CMSVersion ::= INTEGER { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) } + + UserKeyingMaterial ::= OCTET STRING + + OtherKeyAttribute ::= SEQUENCE { + keyAttrId OBJECT IDENTIFIER, + keyAttr ANY DEFINED BY keyAttrId OPTIONAL } + + -- Content Type Object Identifiers + + id-ct-contentInfo OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) smime(16) ct(1) 6 } + + id-data OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 1 } + + id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 } + + id-envelopedData OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 3 } + + id-digestedData OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 5 } + + id-encryptedData OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs7(7) 6 } + + id-ct-authData OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs-9(9) smime(16) ct(1) 2 } + + -- The CMS Attributes + + MessageDigest ::= OCTET STRING + + SigningTime ::= Time + + Time ::= CHOICE { + utcTime UTCTime, + generalTime GeneralizedTime } + + Countersignature ::= SignerInfo + + -- Attribute Object Identifiers + + id-contentType OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) 3 } + + id-messageDigest OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) 4 } + + id-signingTime OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) 5 } + + id-countersignature OBJECT IDENTIFIER ::= { iso(1) member-body(2) + us(840) rsadsi(113549) pkcs(1) pkcs9(9) 6 } + + -- Obsolete Extended Certificate syntax from PKCS #6 + + ExtendedCertificateOrCertificate ::= CHOICE { + certificate Certificate, + extendedCertificate [0] IMPLICIT ExtendedCertificate } + + ExtendedCertificate ::= SEQUENCE { + extendedCertificateInfo ExtendedCertificateInfo, + signatureAlgorithm SignatureAlgorithmIdentifier, + signature Signature } + + ExtendedCertificateInfo ::= SEQUENCE { + version CMSVersion, + certificate Certificate, + attributes UnauthAttributes } + + Signature ::= BIT STRING + + END -- of CryptographicMessageSyntax2004 diff --git a/ietf-cms/protocol/protocol.go b/ietf-cms/protocol/protocol.go new file mode 100644 index 0000000..127ff51 --- /dev/null +++ b/ietf-cms/protocol/protocol.go @@ -0,0 +1,861 @@ +// Package protocol implements low level CMS types, parsing and generation. +package protocol + +import ( + "bytes" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + _ "crypto/sha1" // for crypto.SHA1 + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + "math/big" + "sort" + "time" + + "github.com/github/smimesign/ietf-cms/oid" +) + +// ASN1Error is an error from parsing ASN.1 structures. +type ASN1Error struct { + Message string +} + +// Error implements the error interface. +func (err ASN1Error) Error() string { + return fmt.Sprintf("cms/protocol: ASN.1 Error — %s", err.Message) +} + +var ( + // ErrWrongType is returned by methods that make assumptions about types. + // Helper methods are defined for accessing CHOICE and ANY feilds. These + // helper methods get the value of the field, assuming it is of a given type. + // This error is returned if that assumption is wrong and the field has a + // different type. + ErrWrongType = errors.New("cms/protocol: wrong choice or any type") + + // ErrNoCertificate is returned when a requested certificate cannot be found. + ErrNoCertificate = errors.New("no certificate found") + + // ErrUnsupported is returned when an unsupported type or version + // is encountered. + ErrUnsupported = ASN1Error{"unsupported type or version"} + + // ErrTrailingData is returned when extra data is found after parsing an ASN.1 + // structure. + ErrTrailingData = ASN1Error{"unexpected trailing data"} +) + +// ContentInfo ::= SEQUENCE { +// contentType ContentType, +// content [0] EXPLICIT ANY DEFINED BY contentType } +// +// ContentType ::= OBJECT IDENTIFIER +type ContentInfo struct { + ContentType asn1.ObjectIdentifier + Content asn1.RawValue `asn1:"explicit,tag:0"` +} + +// ParseContentInfo parses a top-level ContentInfo type from BER encoded data. +func ParseContentInfo(ber []byte) (ci ContentInfo, err error) { + var der []byte + if der, err = BER2DER(ber); err != nil { + return + } + + var rest []byte + if rest, err = asn1.Unmarshal(der, &ci); err != nil { + return + } + if len(rest) > 0 { + err = ErrTrailingData + } + + return +} + +// SignedDataContent gets the content assuming contentType is signedData. +func (ci ContentInfo) SignedDataContent() (*SignedData, error) { + if !ci.ContentType.Equal(oid.ContentTypeSignedData) { + return nil, ErrWrongType + } + + sd := new(SignedData) + if rest, err := asn1.Unmarshal(ci.Content.Bytes, sd); err != nil { + return nil, err + } else if len(rest) > 0 { + return nil, ErrTrailingData + } + + return sd, nil +} + +// EncapsulatedContentInfo ::= SEQUENCE { +// eContentType ContentType, +// eContent [0] EXPLICIT OCTET STRING OPTIONAL } +// +// ContentType ::= OBJECT IDENTIFIER +type EncapsulatedContentInfo struct { + EContentType asn1.ObjectIdentifier + EContent asn1.RawValue `asn1:"optional,explicit,tag:0"` +} + +// NewDataEncapsulatedContentInfo creates a new EncapsulatedContentInfo of type +// id-data. +func NewDataEncapsulatedContentInfo(data []byte) (EncapsulatedContentInfo, error) { + return NewEncapsulatedContentInfo(oid.ContentTypeData, data) +} + +// NewEncapsulatedContentInfo creates a new EncapsulatedContentInfo. +func NewEncapsulatedContentInfo(contentType asn1.ObjectIdentifier, content []byte) (EncapsulatedContentInfo, error) { + octets, err := asn1.Marshal(asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagOctetString, + Bytes: content, + IsCompound: false, + }) + if err != nil { + return EncapsulatedContentInfo{}, err + } + + return EncapsulatedContentInfo{ + EContentType: contentType, + EContent: asn1.RawValue{ + Class: asn1.ClassContextSpecific, + Tag: 0, + Bytes: octets, + IsCompound: true, + }, + }, nil +} + +// EContentValue gets the OCTET STRING EContent value without tag or length. +// This is what the message digest is calculated over. A nil byte slice is +// returned if the OPTIONAL eContent field is missing. +func (eci EncapsulatedContentInfo) EContentValue() ([]byte, error) { + if eci.EContent.Bytes == nil { + return nil, nil + } + + // The EContent is an `[0] EXPLICIT OCTET STRING`. EXPLICIT means that there + // is another whole tag wrapping the OCTET STRING. When we decoded the + // EContent into a asn1.RawValue we're just getting that outer tag, so the + // EContent.Bytes is the encoded OCTET STRING, which is what we really want + // the value of. + var octets asn1.RawValue + if rest, err := asn1.Unmarshal(eci.EContent.Bytes, &octets); err != nil { + return nil, err + } else if len(rest) > 0 { + return nil, ErrTrailingData + } + if octets.Class != asn1.ClassUniversal || octets.Tag != asn1.TagOctetString { + return nil, ASN1Error{"bad tag or class"} + } + + // While we already tried converting BER to DER, we didn't take constructed + // types into account. Constructed string types, as opposed to primitive + // types, can encode indefinite length strings by including a bunch of + // sub-strings that are joined together to get the actual value. Gpgsm uses + // a constructed OCTET STRING for the EContent, so we have to manually decode + // it here. + var value []byte + if octets.IsCompound { + rest := octets.Bytes + for len(rest) > 0 { + var err error + if rest, err = asn1.Unmarshal(rest, &octets); err != nil { + return nil, err + } + + // Don't allow further constructed types. + if octets.Class != asn1.ClassUniversal || octets.Tag != asn1.TagOctetString || octets.IsCompound { + return nil, ASN1Error{"bad class or tag"} + } + + value = append(value, octets.Bytes...) + } + } else { + value = octets.Bytes + } + + return value, nil +} + +// IsTypeData checks if the EContentType is id-data. +func (eci EncapsulatedContentInfo) IsTypeData() bool { + return eci.EContentType.Equal(oid.ContentTypeData) +} + +// DataEContent gets the EContent assuming EContentType is data. +func (eci EncapsulatedContentInfo) DataEContent() ([]byte, error) { + if !eci.IsTypeData() { + return nil, ErrWrongType + } + return eci.EContentValue() +} + +// Attribute ::= SEQUENCE { +// attrType OBJECT IDENTIFIER, +// attrValues SET OF AttributeValue } +// +// AttributeValue ::= ANY +type Attribute struct { + Type asn1.ObjectIdentifier + + // This should be a SET OF ANY, but Go's asn1 parser can't handle slices of + // RawValues. Use value() to get an AnySet of the value. + RawValue asn1.RawValue +} + +// NewAttribute creates a single-value Attribute. +func NewAttribute(typ asn1.ObjectIdentifier, val interface{}) (attr Attribute, err error) { + var der []byte + if der, err = asn1.Marshal(val); err != nil { + return + } + + var rv asn1.RawValue + if _, err = asn1.Unmarshal(der, &rv); err != nil { + return + } + + if err = NewAnySet(rv).Encode(&attr.RawValue); err != nil { + return + } + + attr.Type = typ + + return +} + +// Value further decodes the attribute Value as a SET OF ANY, which Go's asn1 +// parser can't handle directly. +func (a Attribute) Value() (AnySet, error) { + return DecodeAnySet(a.RawValue) +} + +// Attributes is a common Go type for SignedAttributes and UnsignedAttributes. +// +// SignedAttributes ::= SET SIZE (1..MAX) OF Attribute +// +// UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute +type Attributes []Attribute + +// MarshaledForSigning DER encodes the Attributes as needed for signing +// SignedAttributes. RFC5652 explains this encoding: +// A separate encoding of the signedAttrs field is performed for message +// digest calculation. The IMPLICIT [0] tag in the signedAttrs is not used for +// the DER encoding, rather an EXPLICIT SET OF tag is used. That is, the DER +// encoding of the EXPLICIT SET OF tag, rather than of the IMPLICIT [0] tag, +// MUST be included in the message digest calculation along with the length +// and content octets of the SignedAttributes value. +func (attrs Attributes) MarshaledForSigning() ([]byte, error) { + seq, err := asn1.Marshal(struct { + Attributes `asn1:"set"` + }{attrs}) + + if err != nil { + return nil, err + } + + // unwrap the outer SEQUENCE + var raw asn1.RawValue + if _, err = asn1.Unmarshal(seq, &raw); err != nil { + return nil, err + } + + return raw.Bytes, nil +} + +// MarshaledForVerification DER encodes the Attributes as needed for +// verification of SignedAttributes. This is done differently than +// MarshaledForSigning because when verifying attributes, we need to +// use the received order. +func (attrs Attributes) MarshaledForVerification() ([]byte, error) { + seq, err := asn1.Marshal(struct { + Attributes `asn1:"sequence"` + }{attrs}) + + if err != nil { + return nil, err + } + + // unwrap the outer SEQUENCE + var raw asn1.RawValue + if _, err = asn1.Unmarshal(seq, &raw); err != nil { + return nil, err + } + + // Change SEQUENCE OF to SET OF. + raw.Bytes[0] = 0x31 + return raw.Bytes, nil +} + +// GetOnlyAttributeValueBytes gets an attribute value, returning an error if the +// attribute occurs multiple times or has multiple values. +func (attrs Attributes) GetOnlyAttributeValueBytes(oid asn1.ObjectIdentifier) (rv asn1.RawValue, err error) { + var vals []AnySet + if vals, err = attrs.GetValues(oid); err != nil { + return + } + if len(vals) != 1 { + err = ASN1Error{"bad attribute count"} + return + } + if len(vals[0].Elements) != 1 { + err = ASN1Error{"bad attribute element count"} + return + } + + return vals[0].Elements[0], nil +} + +// GetValues retreives the attributes with the given OID. A nil value is +// returned if the OPTIONAL SET of Attributes is missing from the SignerInfo. An +// empty slice is returned if the specified attribute isn't in the set. +func (attrs Attributes) GetValues(oid asn1.ObjectIdentifier) ([]AnySet, error) { + if attrs == nil { + return nil, nil + } + + vals := []AnySet{} + for _, attr := range attrs { + if attr.Type.Equal(oid) { + val, err := attr.Value() + if err != nil { + return nil, err + } + + vals = append(vals, val) + } + } + + return vals, nil +} + +// HasAttribute checks if an attribute is present. +func (attrs Attributes) HasAttribute(oid asn1.ObjectIdentifier) bool { + for _, attr := range attrs { + if attr.Type.Equal(oid) { + return true + } + } + + return false +} + +// IssuerAndSerialNumber ::= SEQUENCE { +// issuer Name, +// serialNumber CertificateSerialNumber } +// +// CertificateSerialNumber ::= INTEGER +type IssuerAndSerialNumber struct { + Issuer asn1.RawValue + SerialNumber *big.Int +} + +// NewIssuerAndSerialNumber creates a IssuerAndSerialNumber SID for the given +// cert. +func NewIssuerAndSerialNumber(cert *x509.Certificate) (rv asn1.RawValue, err error) { + sid := IssuerAndSerialNumber{ + SerialNumber: new(big.Int).Set(cert.SerialNumber), + } + + if _, err = asn1.Unmarshal(cert.RawIssuer, &sid.Issuer); err != nil { + return + } + + var der []byte + if der, err = asn1.Marshal(sid); err != nil { + return + } + + if _, err = asn1.Unmarshal(der, &rv); err != nil { + return + } + + return +} + +// SignerInfo ::= SEQUENCE { +// version CMSVersion, +// sid SignerIdentifier, +// digestAlgorithm DigestAlgorithmIdentifier, +// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL, +// signatureAlgorithm SignatureAlgorithmIdentifier, +// signature SignatureValue, +// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL } +// +// CMSVersion ::= INTEGER +// { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) } +// +// SignerIdentifier ::= CHOICE { +// issuerAndSerialNumber IssuerAndSerialNumber, +// subjectKeyIdentifier [0] SubjectKeyIdentifier } +// +// DigestAlgorithmIdentifier ::= AlgorithmIdentifier +// +// SignedAttributes ::= SET SIZE (1..MAX) OF Attribute +// +// SignatureAlgorithmIdentifier ::= AlgorithmIdentifier +// +// SignatureValue ::= OCTET STRING +// +// UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute +type SignerInfo struct { + Version int + SID asn1.RawValue + DigestAlgorithm pkix.AlgorithmIdentifier + SignedAttrs Attributes `asn1:"optional,tag:0"` + SignatureAlgorithm pkix.AlgorithmIdentifier + Signature []byte + UnsignedAttrs Attributes `asn1:"set,optional,tag:1"` +} + +// FindCertificate finds this SignerInfo's certificate in a slice of +// certificates. +func (si SignerInfo) FindCertificate(certs []*x509.Certificate) (*x509.Certificate, error) { + switch si.Version { + case 1: // SID is issuer and serial number + isn, err := si.issuerAndSerialNumberSID() + if err != nil { + return nil, err + } + + for _, cert := range certs { + if bytes.Equal(cert.RawIssuer, isn.Issuer.FullBytes) && isn.SerialNumber.Cmp(cert.SerialNumber) == 0 { + return cert, nil + } + } + case 3: // SID is SubjectKeyIdentifier + ski, err := si.subjectKeyIdentifierSID() + if err != nil { + return nil, err + } + + for _, cert := range certs { + for _, ext := range cert.Extensions { + if oid.ExtensionSubjectKeyIdentifier.Equal(ext.Id) { + if bytes.Equal(ski, ext.Value) { + return cert, nil + } + } + } + } + default: + return nil, ErrUnsupported + } + + return nil, ErrNoCertificate +} + +// issuerAndSerialNumberSID gets the SID, assuming it is a issuerAndSerialNumber. +func (si SignerInfo) issuerAndSerialNumberSID() (isn IssuerAndSerialNumber, err error) { + if si.SID.Class != asn1.ClassUniversal || si.SID.Tag != asn1.TagSequence { + err = ErrWrongType + return + } + + var rest []byte + if rest, err = asn1.Unmarshal(si.SID.FullBytes, &isn); err == nil && len(rest) > 0 { + err = ErrTrailingData + } + + return +} + +// subjectKeyIdentifierSID gets the SID, assuming it is a subjectKeyIdentifier. +func (si SignerInfo) subjectKeyIdentifierSID() ([]byte, error) { + if si.SID.Class != asn1.ClassContextSpecific || si.SID.Tag != 0 { + return nil, ErrWrongType + } + + return si.SID.Bytes, nil +} + +// Hash gets the crypto.Hash associated with this SignerInfo's DigestAlgorithm. +// 0 is returned for unrecognized algorithms. +func (si SignerInfo) Hash() (crypto.Hash, error) { + algo := si.DigestAlgorithm.Algorithm.String() + hash := oid.DigestAlgorithmToCryptoHash[algo] + if hash == 0 || !hash.Available() { + return 0, ErrUnsupported + } + + return hash, nil +} + +// X509SignatureAlgorithm gets the x509.SignatureAlgorithm that should be used +// for verifying this SignerInfo's signature. +func (si SignerInfo) X509SignatureAlgorithm() x509.SignatureAlgorithm { + var ( + sigOID = si.SignatureAlgorithm.Algorithm.String() + digestOID = si.DigestAlgorithm.Algorithm.String() + ) + + if sa := oid.SignatureAlgorithmToX509SignatureAlgorithm[sigOID]; sa != x509.UnknownSignatureAlgorithm { + return sa + } + + return oid.PublicKeyAndDigestAlgorithmToX509SignatureAlgorithm[sigOID][digestOID] +} + +// GetContentTypeAttribute gets the signed ContentType attribute from the +// SignerInfo. +func (si SignerInfo) GetContentTypeAttribute() (asn1.ObjectIdentifier, error) { + rv, err := si.SignedAttrs.GetOnlyAttributeValueBytes(oid.AttributeContentType) + if err != nil { + return nil, err + } + + var ct asn1.ObjectIdentifier + if rest, err := asn1.Unmarshal(rv.FullBytes, &ct); err != nil { + return nil, err + } else if len(rest) > 0 { + return nil, ErrTrailingData + } + + return ct, nil +} + +// GetMessageDigestAttribute gets the signed MessageDigest attribute from the +// SignerInfo. +func (si SignerInfo) GetMessageDigestAttribute() ([]byte, error) { + rv, err := si.SignedAttrs.GetOnlyAttributeValueBytes(oid.AttributeMessageDigest) + if err != nil { + return nil, err + } + if rv.Class != asn1.ClassUniversal || rv.Tag != asn1.TagOctetString { + return nil, ASN1Error{"bad class or tag"} + } + + return rv.Bytes, nil +} + +// GetSigningTimeAttribute gets the signed SigningTime attribute from the +// SignerInfo. +func (si SignerInfo) GetSigningTimeAttribute() (time.Time, error) { + var t time.Time + + if !si.SignedAttrs.HasAttribute(oid.AttributeSigningTime) { + return t, nil + } + rv, err := si.SignedAttrs.GetOnlyAttributeValueBytes(oid.AttributeSigningTime) + if err != nil { + return t, err + } + if rv.Class != asn1.ClassUniversal || (rv.Tag != asn1.TagUTCTime && rv.Tag != asn1.TagGeneralizedTime) { + return t, ASN1Error{"bad class or tag"} + } + + if rest, err := asn1.Unmarshal(rv.FullBytes, &t); err != nil { + return t, err + } else if len(rest) > 0 { + return t, ErrTrailingData + } + + return t, nil +} + +// SignedData ::= SEQUENCE { +// version CMSVersion, +// digestAlgorithms DigestAlgorithmIdentifiers, +// encapContentInfo EncapsulatedContentInfo, +// certificates [0] IMPLICIT CertificateSet OPTIONAL, +// crls [1] IMPLICIT RevocationInfoChoices OPTIONAL, +// signerInfos SignerInfos } +// +// CMSVersion ::= INTEGER +// { v0(0), v1(1), v2(2), v3(3), v4(4), v5(5) } +// +// DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier +// +// CertificateSet ::= SET OF CertificateChoices +// +// CertificateChoices ::= CHOICE { +// certificate Certificate, +// extendedCertificate [0] IMPLICIT ExtendedCertificate, -- Obsolete +// v1AttrCert [1] IMPLICIT AttributeCertificateV1, -- Obsolete +// v2AttrCert [2] IMPLICIT AttributeCertificateV2, +// other [3] IMPLICIT OtherCertificateFormat } +// +// OtherCertificateFormat ::= SEQUENCE { +// otherCertFormat OBJECT IDENTIFIER, +// otherCert ANY DEFINED BY otherCertFormat } +// +// RevocationInfoChoices ::= SET OF RevocationInfoChoice +// +// RevocationInfoChoice ::= CHOICE { +// crl CertificateList, +// other [1] IMPLICIT OtherRevocationInfoFormat } +// +// OtherRevocationInfoFormat ::= SEQUENCE { +// otherRevInfoFormat OBJECT IDENTIFIER, +// otherRevInfo ANY DEFINED BY otherRevInfoFormat } +// +// SignerInfos ::= SET OF SignerInfo +type SignedData struct { + Version int + DigestAlgorithms []pkix.AlgorithmIdentifier `asn1:"set"` + EncapContentInfo EncapsulatedContentInfo + Certificates []asn1.RawValue `asn1:"optional,set,tag:0"` + CRLs []asn1.RawValue `asn1:"optional,set,tag:1"` + SignerInfos []SignerInfo `asn1:"set"` +} + +// NewSignedData creates a new SignedData. +func NewSignedData(eci EncapsulatedContentInfo) (*SignedData, error) { + // The version is picked based on which CMS features are used. We only use + // version 1 features, except for supporting non-data econtent. + version := 1 + if !eci.IsTypeData() { + version = 3 + } + + return &SignedData{ + Version: version, + DigestAlgorithms: []pkix.AlgorithmIdentifier{}, + EncapContentInfo: eci, + SignerInfos: []SignerInfo{}, + }, nil +} + +// AddSignerInfo adds a SignerInfo to the SignedData. +func (sd *SignedData) AddSignerInfo(chain []*x509.Certificate, signer crypto.Signer) error { + // figure out which certificate is associated with signer. + pub, err := x509.MarshalPKIXPublicKey(signer.Public()) + if err != nil { + return err + } + + var ( + cert *x509.Certificate + certPub []byte + ) + + for _, c := range chain { + if err = sd.AddCertificate(c); err != nil { + return err + } + + if certPub, err = x509.MarshalPKIXPublicKey(c.PublicKey); err != nil { + return err + } + + if bytes.Equal(pub, certPub) { + cert = c + } + } + if cert == nil { + return ErrNoCertificate + } + + sid, err := NewIssuerAndSerialNumber(cert) + if err != nil { + return err + } + + digestAlgorithmID := digestAlgorithmForPublicKey(pub) + + signatureAlgorithmOID, ok := oid.X509PublicKeyAndDigestAlgorithmToSignatureAlgorithm[cert.PublicKeyAlgorithm][digestAlgorithmID.Algorithm.String()] + if !ok { + return errors.New("unsupported certificate public key algorithm") + } + + signatureAlgorithmID := pkix.AlgorithmIdentifier{Algorithm: signatureAlgorithmOID} + + si := SignerInfo{ + Version: 1, + SID: sid, + DigestAlgorithm: digestAlgorithmID, + SignedAttrs: nil, + SignatureAlgorithm: signatureAlgorithmID, + Signature: nil, + UnsignedAttrs: nil, + } + + // Get the message + content, err := sd.EncapContentInfo.EContentValue() + if err != nil { + return err + } + if content == nil { + return errors.New("already detached") + } + + // Digest the message. + hash, err := si.Hash() + if err != nil { + return err + } + md := hash.New() + if _, err = md.Write(content); err != nil { + return err + } + + // Build our SignedAttributes + stAttr, err := NewAttribute(oid.AttributeSigningTime, time.Now().UTC()) + if err != nil { + return err + } + mdAttr, err := NewAttribute(oid.AttributeMessageDigest, md.Sum(nil)) + if err != nil { + return err + } + ctAttr, err := NewAttribute(oid.AttributeContentType, sd.EncapContentInfo.EContentType) + if err != nil { + return err + } + + // sort attributes to match required order in marshaled form + si.SignedAttrs, err = sortAttributes(stAttr, mdAttr, ctAttr) + if err != nil { + return err + } + + // Signature is over the marshaled signed attributes + sm, err := si.SignedAttrs.MarshaledForSigning() + if err != nil { + return err + } + smd := hash.New() + if _, errr := smd.Write(sm); errr != nil { + return errr + } + if si.Signature, err = signer.Sign(rand.Reader, smd.Sum(nil), hash); err != nil { + return err + } + + sd.addDigestAlgorithm(si.DigestAlgorithm) + + sd.SignerInfos = append(sd.SignerInfos, si) + + return nil +} + +func sortAttributes(attrs ...Attribute) ([]Attribute, error) { + // Sort attrs by their encoded values (including tag and + // lengths) as specified in X690 Section 11.6 and implemented + // in go >= 1.15's asn1.Marshal(). + sort.Slice(attrs, func(i, j int) bool { + return bytes.Compare( + attrs[i].RawValue.FullBytes, + attrs[j].RawValue.FullBytes) < 0 + }) + + return attrs, nil +} + +// algorithmsForPublicKey takes an opinionated stance on what algorithms to use +// for the given public key. +func digestAlgorithmForPublicKey(pub crypto.PublicKey) pkix.AlgorithmIdentifier { + if ecPub, ok := pub.(*ecdsa.PublicKey); ok { + switch ecPub.Curve { + case elliptic.P384(): + return pkix.AlgorithmIdentifier{Algorithm: oid.DigestAlgorithmSHA384} + case elliptic.P521(): + return pkix.AlgorithmIdentifier{Algorithm: oid.DigestAlgorithmSHA512} + } + } + + return pkix.AlgorithmIdentifier{Algorithm: oid.DigestAlgorithmSHA256} +} + +// ClearCertificates removes all certificates. +func (sd *SignedData) ClearCertificates() { + sd.Certificates = []asn1.RawValue{} +} + +// AddCertificate adds a *x509.Certificate. +func (sd *SignedData) AddCertificate(cert *x509.Certificate) error { + for _, existing := range sd.Certificates { + if bytes.Equal(existing.Bytes, cert.Raw) { + return errors.New("certificate already added") + } + } + + var rv asn1.RawValue + if _, err := asn1.Unmarshal(cert.Raw, &rv); err != nil { + return err + } + + sd.Certificates = append(sd.Certificates, rv) + + return nil +} + +// addDigestAlgorithm adds a new AlgorithmIdentifier if it doesn't exist yet. +func (sd *SignedData) addDigestAlgorithm(algo pkix.AlgorithmIdentifier) { + for _, existing := range sd.DigestAlgorithms { + if existing.Algorithm.Equal(algo.Algorithm) { + return + } + } + + sd.DigestAlgorithms = append(sd.DigestAlgorithms, algo) +} + +// X509Certificates gets the certificates, assuming that they're X.509 encoded. +func (sd *SignedData) X509Certificates() ([]*x509.Certificate, error) { + // Certificates field is optional. Handle missing value. + if sd.Certificates == nil { + return nil, nil + } + + // Empty set + if len(sd.Certificates) == 0 { + return []*x509.Certificate{}, nil + } + + certs := make([]*x509.Certificate, 0, len(sd.Certificates)) + for _, raw := range sd.Certificates { + if raw.Class != asn1.ClassUniversal || raw.Tag != asn1.TagSequence { + return nil, ErrUnsupported + } + + x509, err := x509.ParseCertificate(raw.FullBytes) + if err != nil { + return nil, err + } + + certs = append(certs, x509) + } + + return certs, nil +} + +// ContentInfo returns the SignedData wrapped in a ContentInfo packet. +func (sd *SignedData) ContentInfo() (ContentInfo, error) { + var nilCI ContentInfo + + der, err := asn1.Marshal(*sd) + if err != nil { + return nilCI, err + } + + return ContentInfo{ + ContentType: oid.ContentTypeSignedData, + Content: asn1.RawValue{ + Class: asn1.ClassContextSpecific, + Tag: 0, + Bytes: der, + IsCompound: true, + }, + }, nil + +} + +// ContentInfoDER returns the SignedData wrapped in a ContentInfo packet and DER +// encoded. +func (sd *SignedData) ContentInfoDER() ([]byte, error) { + ci, err := sd.ContentInfo() + if err != nil { + return nil, err + } + + return asn1.Marshal(ci) +} diff --git a/ietf-cms/protocol/protocol_test.go b/ietf-cms/protocol/protocol_test.go new file mode 100644 index 0000000..cc3018d --- /dev/null +++ b/ietf-cms/protocol/protocol_test.go @@ -0,0 +1,680 @@ +package protocol + +import ( + "bytes" + "crypto/ecdsa" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "io" + "strings" + "testing" + "time" + + "github.com/github/smimesign/ietf-cms/oid" + "golang.org/x/crypto/pkcs12" +) + +func TestSignerInfo(t *testing.T) { + priv, cert, err := pkcs12.Decode(fixturePFX, "asdf") + if err != nil { + t.Fatal(err) + } + + msg := []byte("hello, world!") + + eci, err := NewEncapsulatedContentInfo(oid.ContentTypeData, msg) + if err != nil { + t.Fatal(err) + } + + sd, err := NewSignedData(eci) + if err != nil { + t.Fatal(err) + } + + chain := []*x509.Certificate{cert} + if err = sd.AddSignerInfo(chain, priv.(*ecdsa.PrivateKey)); err != nil { + t.Fatal(err) + } + + der, err := sd.ContentInfoDER() + if err != nil { + t.Fatal(err) + } + + ci, err := ParseContentInfo(der) + if err != nil { + t.Fatal(err) + } + + sd2, err := ci.SignedDataContent() + if err != nil { + t.Fatal(err) + } + + msg2, err := sd2.EncapContentInfo.DataEContent() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(msg, msg2) { + t.Fatal() + } + + // Make detached + sd.EncapContentInfo.EContent = asn1.RawValue{} + + der, err = sd.ContentInfoDER() + if err != nil { + t.Fatal(err) + } + + ci, err = ParseContentInfo(der) + if err != nil { + t.Fatal(err) + } + + sd2, err = ci.SignedDataContent() + if err != nil { + t.Fatal(err) + } + + msg2, err = sd2.EncapContentInfo.DataEContent() + if err != nil { + t.Fatal(err) + } + if msg2 != nil { + t.Fatal() + } +} + +func TestEncapsulatedContentInfo(t *testing.T) { + ci, _ := ParseContentInfo(fixtureSignatureOpenSSLAttached) + sd, _ := ci.SignedDataContent() + oldECI := sd.EncapContentInfo + + oldData, err := oldECI.DataEContent() + if err != nil { + t.Fatal(err) + } + + newECI, err := NewEncapsulatedContentInfo(oid.ContentTypeData, oldData) + if err != nil { + t.Fatal(err) + } + + newData, err := newECI.DataEContent() + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldData, newData) { + t.Fatal("ECI data round trip mismatch: ", oldData, " != ", newData) + } + + oldDER, err := asn1.Marshal(oldECI) + if err != nil { + t.Fatal(err) + } + + newDER, err := asn1.Marshal(newECI) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldDER, newDER) { + t.Fatal("ECI round trip mismatch: ", oldDER, " != ", newDER) + } +} + +func TestMessageDigestAttribute(t *testing.T) { + ci, _ := ParseContentInfo(fixtureSignatureOpenSSLAttached) + sd, _ := ci.SignedDataContent() + si := sd.SignerInfos[0] + + oldAttrVal, err := si.GetMessageDigestAttribute() + if err != nil { + t.Fatal(err) + } + + var oldAttr Attribute + for _, attr := range si.SignedAttrs { + if attr.Type.Equal(oid.AttributeMessageDigest) { + oldAttr = attr + break + } + } + + newAttr, err := NewAttribute(oid.AttributeMessageDigest, oldAttrVal) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldAttr.RawValue.Bytes, newAttr.RawValue.Bytes) { + t.Fatal("raw value mismatch") + } + + oldDER, err := asn1.Marshal(oldAttr) + if err != nil { + t.Fatal(err) + } + + newDER, err := asn1.Marshal(newAttr) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldDER, newDER) { + t.Fatal("der mismatch") + } +} + +func TestContentTypeAttribute(t *testing.T) { + ci, _ := ParseContentInfo(fixtureSignatureOpenSSLAttached) + sd, _ := ci.SignedDataContent() + si := sd.SignerInfos[0] + + oldAttrVal, err := si.GetContentTypeAttribute() + if err != nil { + t.Fatal(err) + } + + var oldAttr Attribute + for _, attr := range si.SignedAttrs { + if attr.Type.Equal(oid.AttributeContentType) { + oldAttr = attr + break + } + } + + newAttr, err := NewAttribute(oid.AttributeContentType, oldAttrVal) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldAttr.RawValue.Bytes, newAttr.RawValue.Bytes) { + t.Fatal("raw value mismatch") + } + + oldDER, err := asn1.Marshal(oldAttr) + if err != nil { + t.Fatal(err) + } + + newDER, err := asn1.Marshal(newAttr) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldDER, newDER) { + t.Fatal("der mismatch") + } +} + +func TestSigningTimeAttribute(t *testing.T) { + ci, _ := ParseContentInfo(fixtureSignatureOpenSSLAttached) + sd, _ := ci.SignedDataContent() + si := sd.SignerInfos[0] + + oldAttrVal, err := si.GetSigningTimeAttribute() + if err != nil { + t.Fatal(err) + } + + var oldAttr Attribute + for _, attr := range si.SignedAttrs { + if attr.Type.Equal(oid.AttributeSigningTime) { + oldAttr = attr + break + } + } + + newAttr, err := NewAttribute(oid.AttributeSigningTime, oldAttrVal) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldAttr.RawValue.Bytes, newAttr.RawValue.Bytes) { + t.Fatal("raw value mismatch") + } + + oldDER, err := asn1.Marshal(oldAttr) + if err != nil { + t.Fatal(err) + } + + newDER, err := asn1.Marshal(newAttr) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(oldDER, newDER) { + t.Fatal("der mismatch") + } +} + +func TestIssuerAndSerialNumber(t *testing.T) { + ci, _ := ParseContentInfo(fixtureSignatureOpenSSLAttached) + sd, _ := ci.SignedDataContent() + si := sd.SignerInfos[0] + certs, _ := sd.X509Certificates() + cert, _ := si.FindCertificate(certs) + + newISN, err := NewIssuerAndSerialNumber(cert) + if err != nil { + t.Fatal(err) + } + + oldDER, _ := asn1.Marshal(si.SID) + newDER, _ := asn1.Marshal(newISN) + + if !bytes.Equal(oldDER, newDER) { + t.Fatal("SID mismatch") + } +} + +func TestParseSignatureOne(t *testing.T) { + testParseContentInfo(t, fixtureSignatureOne) +} + +func TestParseSignatureGPGSMAttached(t *testing.T) { + testParseContentInfo(t, fixtureSignatureGPGSMAttached) +} + +func TestParseSignatureGPGSM(t *testing.T) { + testParseContentInfo(t, fixtureSignatureGPGSM) +} + +func TestParseSignatureNoCertsGPGSM(t *testing.T) { + testParseContentInfo(t, fixtureSignatureNoCertsGPGSM) +} + +func TestParseSignatureOpenSSLAttached(t *testing.T) { + testParseContentInfo(t, fixtureSignatureOpenSSLAttached) +} + +func TestParseSignatureOpenSSLDetached(t *testing.T) { + testParseContentInfo(t, fixtureSignatureOpenSSLDetached) +} + +func TestParseSignautreOutlookDetached(t *testing.T) { + testParseContentInfo(t, fixtureSignatureOutlookDetached) +} + +func testParseContentInfo(t *testing.T, ber []byte) { + ci, err := ParseContentInfo(ber) + if err != nil { + t.Fatal(err) + } + + sd, err := ci.SignedDataContent() + if err != nil { + t.Fatal(err) + } + + certs, err := sd.X509Certificates() + if err != nil { + t.Fatal(err) + } + + if !sd.EncapContentInfo.IsTypeData() { + t.Fatal("expected id-data econtent") + } + + if !sd.EncapContentInfo.EContentType.Equal(oid.ContentTypeData) { + t.Fatalf("expected %s content, got %s", oid.ContentTypeData.String(), sd.EncapContentInfo.EContentType.String()) + } + + data, err := sd.EncapContentInfo.DataEContent() + if err != nil { + t.Fatal(err) + } + if data != nil && len(data) == 0 { + t.Fatal("attached signature with zero length data") + } + + for _, si := range sd.SignerInfos { + if _, err = si.FindCertificate(certs); err != nil && len(certs) > 0 { + t.Fatal(err) + } + + if ct, errr := si.GetContentTypeAttribute(); errr != nil { + t.Fatal(errr) + } else { + // signerInfo contentType attribute must match signedData + // encapsulatedContentInfo content type. + if !ct.Equal(sd.EncapContentInfo.EContentType) { + t.Fatalf("expected %s content, got %s", sd.EncapContentInfo.EContentType.String(), ct.String()) + } + } + + if md, errr := si.GetMessageDigestAttribute(); errr != nil { + t.Fatal(errr) + } else if len(md) == 0 { + t.Fatal("nil/empty message digest attribute") + } + + if algo := si.X509SignatureAlgorithm(); algo == x509.UnknownSignatureAlgorithm { + t.Fatalf("unknown signature algorithm") + } + + var nilTime time.Time + if st, errr := si.GetSigningTimeAttribute(); errr != nil { + t.Fatal(errr) + } else if st == nilTime { + t.Fatal("0 value signing time") + } + } + + // round trip contentInfo + der, err := BER2DER(ber) + if err != nil { + t.Fatal(err) + } + + der2, err := asn1.Marshal(ci) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(der, der2) { + t.Fatal("re-encoded contentInfo doesn't match original") + } + + // round trip signedData + der = ci.Content.Bytes + + der2, err = asn1.Marshal(*sd) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(der, der2) { + t.Fatal("re-encoded signedData doesn't match original") + } +} + +var fixtureSignatureOne = mustBase64Decode("" + + "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", +) + +var fixtureSignatureGPGSMAttached = mustBase64Decode("" + + "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" + + "SDXMwnexRO3w39tu4ChUFbsX8l5CAAAAAAAA", +) + +var fixtureSignatureGPGSM = mustBase64Decode("" + + "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" + + "SX+du6QAAAAAAAA=", +) + +var fixtureSignatureNoCertsGPGSM = mustBase64Decode("" + + "MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" + + "BwEAADGCAeEwggHdAgEBMCAwFDESMBAGA1UEAxMJQmVuIFRvZXdzAggWdNrn7G+u" + + "SDANBglghkgBZQMEAgEFAKCBkzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG" + + "CSqGSIb3DQEJBTEPFw0xNzExMTcwMDQxNDhaMCgGCSqGSIb3DQEJDzEbMBkwCwYJ" + + "YIZIAWUDBAECMAoGCCqGSIb3DQMHMC8GCSqGSIb3DQEJBDEiBCBNyg/V9CSjGwOr" + + "gHy6536zK/LQie7RzuFUs6/tRY3g3DANBgkqhkiG9w0BAQEFAASCAQAvGAGPMaH3" + + "oRiNDU0AGIVyjXUrZ8g2VRazGCTuuO0CPGWBDbBuuvCePuWTddcv5KHHyrYO0yUD" + + "xergVhh1EXIsOItHbJ6QeMstmY8Ub7HGm4Srdtm3MMSEe24zRmKK5yvPfeaaXeb6" + + "MASKXvViU/j9VDwUZ2CFPUzPq8DlS6j4w6dapfphFGN1wJV3ADLUzUkTXfXQ57HE" + + "WUKdbxgcuyBH7eLhZpKAXP31iRKm2b7dV50SruRCqNYZOp8bUQ57bC2jels0dzQd" + + "EQS76O/DH6eQ3/OgvpmR8BjlujA82tgjqP7fj0S7Cw2VlPqcey0iqRmAmiO2qzOI" + + "KAYzMkxWr7iUAAAAAAAA", +) + +var fixtureSignatureOpenSSLAttached = mustBase64Decode("" + + "MIIFGgYJKoZIhvcNAQcCoIIFCzCCBQcCAQExDzANBglghkgBZQMEAgEFADAcBgkq" + + "hkiG9w0BBwGgDwQNaGVsbG8sIHdvcmxkIaCCAqMwggKfMIIBh6ADAgECAgEAMA0G" + + "CSqGSIb3DQEBBQUAMBMxETAPBgNVBAMMCGNtcy10ZXN0MB4XDTE3MTEyMDIwNTM0" + + "M1oXDTI3MTExODIwNTM0M1owEzERMA8GA1UEAwwIY21zLXRlc3QwggEiMA0GCSqG" + + "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWMRnJdRQxw8j8Yn3jh/rcZyeALStl+MmM" + + "TEtr6XsmMOWQhnP6nCAIOw5EIAXGpKl4Yg3F2gDKmJCVl279Q+G9nLtvmWvCzu19" + + "BJUG7jVLWzO8KSuJa83iiilZUP2adVZujdGB6dxekIBu7vkYi9XxZJm4edhj0bkd" + + "EtkxLCNUGDQKsywnKOTWzfefT9UCQJyLwt74ThJtNX7uoYrfAHNfBARk3Kx+wf4U" + + "Grd2GmSe8Lnr3FNcZ/uMJffsYvBk3fbDwYsVC6rd4BuJvvri3K1dti3rnvDEnuMI" + + "Ve7a2n7NE7yV0cietIjKeeY8bO25lwrTtBzgP5y1G9spjzAtiRLZAgMBAAEwDQYJ" + + "KoZIhvcNAQEFBQADggEBAMkYPFmsHYlyO+KZMKEWUWOdw1rwrIVhLQOKqLz8Wbe8" + + "lIQ5pdsd4S1DqvMEzYyMtpZckZ9mOBZh/SQsmdb8sZnQwiMvlPSO6IWp/MpuP+VK" + + "v8IBAr1aaLlMaelV086uIFc9coE6XAdWFrGlUT9FYM00JwoSfi51vbcqbIh6P8y9" + + "uwHqlt2vkVYujto+p0UMBnBZkfKBgzMG7ILWpJbVszmpesVzI2XUgq8BxlO0fvw5" + + "m/R4bAtHqXTK0xVrTBXUg6izFbdA3pVlFMiuv8Kq2cyBg+VkXGYmZ37BGhApe5Le" + + "Dabe4iGcXQMW4lunjRSv8gDu/ODA/20OMNVDOx92MTIxggIqMIICJgIBATAYMBMx" + + "ETAPBgNVBAMMCGNtcy10ZXN0AgEAMA0GCWCGSAFlAwQCAQUAoIHkMBgGCSqGSIb3" + + "DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE3MTEyMDIwNTM0M1ow" + + "LwYJKoZIhvcNAQkEMSIEIGjmVrJR5n6DWL74SDqw1RxmGfPnoanw51g41B/zaPco" + + "MHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglg" + + "hkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMC" + + "AgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIBAJHB" + + "kfH1hZ4Y0TI6PdW7DNFnb++KQJiu4NmzE7SyTJOCxC2W44uAKUdJw7c8cdn/lcb/" + + "y1kvwNbi2kysuZSTpywBIjHSTw3BTwdaNJFd6HUV1mX2IQRfaJIPW5fqkhLfQtZ6" + + "LZka/HWQ5fwA51g6lVNTMbStjsPlBef6qEDcCLMp/4CNEqC5+fUx8Jb7Q5mvyCHQ" + + "3IZrIEMLBYhrgrm61qh/MXKnAqlEo6XxN1fL0CXDxy9dYPSKr2G66o9+BjmYktF5" + + "3MfxrT4JDizd2S/8BVEv+H+uHmrpyRxMceREPJVrVHOdd922hyKALbAGcoyMdXpj" + + "ZdMtHnR5z07z9wxvwiw=", +) + +var fixtureSignatureOpenSSLDetached = mustBase64Decode("" + + "MIIFCQYJKoZIhvcNAQcCoIIE+jCCBPYCAQExDzANBglghkgBZQMEAgEFADALBgkq" + + "hkiG9w0BBwGgggKjMIICnzCCAYegAwIBAgIBADANBgkqhkiG9w0BAQUFADATMREw" + + "DwYDVQQDDAhjbXMtdGVzdDAeFw0xNzExMjAyMTE0NDdaFw0yNzExMTgyMTE0NDda" + + "MBMxETAPBgNVBAMMCGNtcy10ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" + + "CgKCAQEA5VQ0FRvQRA9F+6nss77yUcm3x8IOoJV/icQrtrkR/BHGgeepcLIcHkWh" + + "s/cap69xR5TCtONy0I4tqKf/vXnKXvMjsGGrecFMi8NVTbEoNg9m47nbdO7BY1+f" + + "waLfwAX5vf17BRSqA0wRIoNIzJc07mNrI84EbKfVmDtPrqzwnT0sIKqj5p2PQdWi" + + "sPwOocLYJBdAPglnLuFk6WTZalJRgV7h50nl1GBDKJVo1Yc7zqPdqWzHzFqK759g" + + "CHBZMYJdqIx/wev/l66oEcJZr6gnnKzq8lsWljpjVWD96z/W/fehWZsWlWkvmrus" + + "qizMbL0vCx8HrReo7+hszMIHR5bwTwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAD" + + "ZjPxm/JHc4KoQUaVOSAU97lO60MD21Ud0LtaebbiSJnaMH9a/rb3kuxJAKVSBhDp" + + "wyRK19KNtaSXHEAD48aJeT7J4wsDJFNfKGx/9R2iYB5xjc/POpK13A/o4fDrpLWL" + + "1doIc0KjVA63BXaYOwsEj2iKzUKNFZ2kS3bXMkEBhUDUXtSo08WFI7UkgYTuIfM2" + + "LS/wyORcwZIEIvq+ndkch/nAyQZ8U0/85dgwpOQcyZ0UDiu8Ti9z9IUlhxSq2T13" + + "JhIfiMa4m27y71JmsFy12uN3fGBckkyNkKkxVMy0H4Ukr1hq/ZkvH3HdrEnWmNEu" + + "WdU7WvIBsbe3U2idyhBSMYICKjCCAiYCAQEwGDATMREwDwYDVQQDDAhjbXMtdGVz" + + "dAIBADANBglghkgBZQMEAgEFAKCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB" + + "MBwGCSqGSIb3DQEJBTEPFw0xNzExMjAyMTE0NDdaMC8GCSqGSIb3DQEJBDEiBCBo" + + "5layUeZ+g1i++Eg6sNUcZhnz56Gp8OdYONQf82j3KDB5BgkqhkiG9w0BCQ8xbDBq" + + "MAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3" + + "DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggq" + + "hkiG9w0DAgIBKDANBgkqhkiG9w0BAQEFAASCAQAcLsBbjvlhz+HAy7m5cvh8tRav" + + "xT05fFK1hwBC287z+D/UaCrvrd2vR4bdUV8jfS5iTyUfX/BikOljxRwUMgtBLPKq" + + "gdNokoxUoQiqVOdgCER0isNLF/8+O29reI6N/9Mp+IpfE41o2xcRrggfncuPX00K" + + "MB2K4/ZF35HddfblHIgQ+9gWfHE52KMur4XeI5sc/izMNuPyR8VVB7St5JLMepHj" + + "UtbPYBJ0bRSwDX1JAoB+Ze/mPvCmo/pS5QyYfNvXg3Jw4TVoud5+oUH9r6MwSxzN" + + "BSws5SM9d0GAafR+Hj19x9s8ypUjLJmGIAjeTrlgcYUTJjnfEtZBL5Je2FuK", +) + +var fixtureSignatureOutlookDetached = mustBase64Decode("" + + "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" + + "MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDBDBgNVHSAEPDA6MDgGCmCGSAGG/WwEAQIw" + + "KjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBiAYDVR0fBIGAMH4w" + + "PaA7oDmGN2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURDQS1n" + + "Mi5jcmwwPaA7oDmGN2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVk" + + "SURDQS1nMi5jcmwweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp" + + "Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy" + + "dFNIQTJBc3N1cmVkSURDQS5jcnQwDQYJKoZIhvcNAQELBQADggEBADh2DYGfn+1eg21dTa34iZlu" + + "IyActG/S23bCLnJSThPbiCfZgGkKr9Bq6TSJ4qQfsquIB7cO46mJ+tzHL570xAsJ4pC7z3RhBdzK" + + "j9uT6ZUExdHQs2FoPjU5uT1UhqHv7T9qYp689XpZ2xPLH59SwLASIVnoQFIS0MKT8AN6ZgKxDWDY" + + "EUyRfGQxxDbfqWhncH0qxT20mv8TnvIMo2ngsCBZfpJcv9u3LijnD7uVCZ2qRIJkmJ7s1eoGc05c" + + "Z+7NeA8vC28BgGe2svMUlRInaNsMDUBmizI4x6DnS8uVlX22KAdPML9NvPOfCGCohDevZgCSMx/o" + + "nH+foA+rOCngkR8wggZOMIIFNqADAgECAhAErnlgZmaQGrnFf6ZsW9zNMA0GCSqGSIb3DQEBCwUA" + + "MGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp" + + "Y2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzExMDUx" + + "MjAwMDBaFw0yODExMDUxMjAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ" + + "bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIgQXNz" + + "dXJlZCBJRCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANz4ESM/arXvwCd5Gy0F" + + "h6IQQzHfDtQVG093pCLOPoxw8L4Hjt0nKrwBHbYsCsrdaVgfQe1qBR/aY3hZHiIsK/i6fsk1O1bx" + + "H3xCfiWwIxnGRTjXPUT5IHxgrhywWhgEvo8796nwlJqmDGNJtkEXU0AyvU/mUHpQHyVF6PGJr83/" + + "Xv9Q8/AXEf+9xYn1vWK52PuORQSFbZnNxUhN/SarAjZF6jbXX2riGoJBCtzp2fWRF47GIa04PBPm" + + "Hn9mnNVN2Uba9s9Sp307JMO0wVE1xpvr1O9+5HsD4US9egs34E/LgooNcRjkpuCJLBvzsnM8wbCS" + + "nhh9vat9xX0IoSzCn3MCAwEAAaOCAvgwggL0MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/" + + "BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu" + + "Y29tMIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRB" + + "c3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNl" + + "cnRBc3N1cmVkSURSb290Q0EuY3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCCAbMG" + + "A1UdIASCAaowggGmMIIBogYKYIZIAYb9bAACBDCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3" + + "LmRpZ2ljZXJ0LmNvbS9DUFMwggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABv" + + "AGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQA" + + "ZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0" + + "ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQA" + + "eQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBp" + + "AGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUA" + + "cgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wHQYDVR0OBBYEFOcCI4AAT9jXvJQL" + + "2T90OUkyPIp5MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUA" + + "A4IBAQBO1Iknuf0dh3d+DygFkPEKL8k7Pr2TnJDGr/qRUYcyVGvoysFxUVyZjrX64GIZmaYHmnwT" + + "J9vlAqKEEtkV9gpEV8Q0j21zHzrWoAE93uOC5EVrsusl/YBeHTmQvltC9s6RYOP5oFYMSBDOM2h7" + + "zZOr8GrLT1gPuXtdGwSBnqci4ldJJ+6Skwi+aQhTAjouXcgZ9FCATgLZsF2RtJOH+ZaWgVVAjmbt" + + "gti7KF/tTGHtBlgoGVMRRLxHICmyBGzYiVSZO3XbZ3gsHpJ4xlU9WBIRMm69QwxNNNt7xkLb7L6r" + + "m2FMBpLjjt8hKlBXBMBgojXVJJ5mNwlJz9X4ZbPg4m7CMYIDvzCCA7sCAQEweTBlMQswCQYDVQQG" + + "EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw" + + "IgYDVQQDExtEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ0ECEAWkzvCWLw1xJSpSGJSTKHAwDQYJ" + + "YIZIAWUDBAIBBQCgggIXMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X" + + "DTE3MTEyOTE0NDMxOVowLwYJKoZIhvcNAQkEMSIEIEgBjCiMhZLBevfHienSec11YNE+P7PSd4JD" + + "wfCQCrwWMIGIBgkrBgEEAYI3EAQxezB5MGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy" + + "dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIg" + + "QXNzdXJlZCBJRCBDQQIQBaTO8JYvDXElKlIYlJMocDCBigYLKoZIhvcNAQkQAgsxe6B5MGUxCzAJ" + + "BgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j" + + "b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDQQIQBaTO8JYvDXElKlIYlJMo" + + "cDCBkwYJKoZIhvcNAQkPMYGFMIGCMAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCgYIKoZIhvcN" + + "AwcwCwYJYIZIAWUDBAECMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDALBglghkgBZQME" + + "AgEwCwYJYIZIAWUDBAIDMAsGCWCGSAFlAwQCAjAHBgUrDgMCGjANBgkqhkiG9w0BAQEFAASCAQBh" + + "AjkUd98Si7LKxELWdwY8yrqrfK61JxVSxSY/BkF3xS/0QbQMU9Y+0V23nJX5ymamgCd9yNTdNapV" + + "D4OzoVXfmTqd1/AD30M1a1CdBVoNGV8X4Uv8Z1fAl5MN+6Yt1CeIun39gvkutAgUmvCVrjFN+gD6" + + "GH+VTQNGHr3wxdmtL9F8WeNECvpVgYEMqnYRrYHw4B6euJRsy4UnB4Sy/ogV1elkipxCbqRovPU1" + + "pVeKhkfYuRlsLwbBwQPKvzcfUU3ZJua4I3AKKPxlqdY8uP72A5iObDTL8kHhSRMtVVHoruVzgJPZ" + + "+9Mfsz41eM4pJSPDKZPYD9rH6cUKJI8xEnmCAAAAAAAA", +) + +var fixturePFX = mustBase64Decode("" + + "MIIDIgIBAzCCAugGCSqGSIb3DQEHAaCCAtkEggLVMIIC0TCCAccGCSqGSIb3" + + "DQEHBqCCAbgwggG0AgEAMIIBrQYJKoZIhvcNAQcBMBwGCiqGSIb3DQEMAQYw" + + "DgQIhJhqIE0wYvgCAggAgIIBgFfQz7+5T0RBGtlNHUjM+WmjJFPPhljOcl5v" + + "SEFWi2mNpSuUIcaNQlhUTxBX7hUJRq6eW3J5T20hY3WBomC6cy4sRpAZlOSD" + + "o/UYrQG6YIFc+X97t8E1M8bihsmp9GEBEdLCDCwhrIpFX7xuxfudYH9MLRKA" + + "dKwJ8xqrpFjgFFbosvKHoqi0gH2RLS7+G8V5wReWTOVKvzy3zD8XlMgtdSUn" + + "G+MiP0aaa8jFGfprFoeMMJJr5cO89UjjC+qYkcqA9HP7mf2VmenEJSJt7E06" + + "51CE3/eaEONgoIDudTXZt8CB4vvbOnL8QfmVp2kzKKl1hsN43jPVvRqbM6+4" + + "OR1Yp3T1UVKLcGwpZCh3t/fYgpyjBqrQqEWQzhKs+bTWlCeDpXdxhHJIquHh" + + "zZ8Sm2s/r1GDv7kVLw9d8APyWep5WrFVE/r7kN9Ac8tbiqTM54sFMTQLkzhP" + + "TIhNdjIQkn8i0H2673cGYkFYWLIO+I8jFhMl3ZBwQt54Wnb35zInpchoQjCC" + + "AQIGCSqGSIb3DQEHAaCB9ASB8TCB7jCB6wYLKoZIhvcNAQwKAQKggbQwgbEw" + + "HAYKKoZIhvcNAQwBAzAOBAhlMkjWb0xXBAICCAAEgZALV1NzLJa6MAAaYkIs" + + "eJRapR+h9Emzew5dstSbB23kMt3PLyafv4M0AvUi3Mk+VEowmL62WhC+PcQf" + + "dE4YaW6PvepWjS+gk42RA6hT8zdG2PiP2rhS4wuxs/I/rPQIgY8i3M2RGmrR" + + "9CcOFCE7hnpJp/0tm7Trc11SfCNB3MXYSvttL5ZJ29ewYZ9kg+lv0XoxJTAj" + + "BgkqhkiG9w0BCRUxFgQU7q/jH1Mc5Ctiwkdl0Hx9xKSYy90wMTAhMAkGBSsO" + + "AwIaBQAEFDPX7JM9l8ZnTwGGaDQQvlp7RiBKBAg2WsoFwawSzwICCAA=", +) + +func mustBase64Decode(b64 string) []byte { + decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64)) + buf := new(bytes.Buffer) + + if _, err := io.Copy(buf, decoder); err != nil { + panic(err) + } + + return buf.Bytes() +} diff --git a/ietf-cms/sign.go b/ietf-cms/sign.go new file mode 100644 index 0000000..dd2de9e --- /dev/null +++ b/ietf-cms/sign.go @@ -0,0 +1,49 @@ +package cms + +import ( + "crypto" + "crypto/x509" +) + +// Sign creates a CMS SignedData from the content and signs it with signer. At +// minimum, chain must contain the leaf certificate associated with the signer. +// Any additional intermediates will also be added to the SignedData. The DER +// encoded CMS message is returned. +func Sign(data []byte, chain []*x509.Certificate, signer crypto.Signer) ([]byte, error) { + sd, err := NewSignedData(data) + if err != nil { + return nil, err + } + + if err = sd.Sign(chain, signer); err != nil { + return nil, err + } + + return sd.ToDER() +} + +// SignDetached creates a detached CMS SignedData from the content and signs it +// with signer. At minimum, chain must contain the leaf certificate associated +// with the signer. Any additional intermediates will also be added to the +// SignedData. The DER encoded CMS message is returned. +func SignDetached(data []byte, chain []*x509.Certificate, signer crypto.Signer) ([]byte, error) { + sd, err := NewSignedData(data) + if err != nil { + return nil, err + } + + if err = sd.Sign(chain, signer); err != nil { + return nil, err + } + + sd.Detached() + + return sd.ToDER() +} + +// Sign adds a signature to the SignedData.At minimum, chain must contain the +// leaf certificate associated with the signer. Any additional intermediates +// will also be added to the SignedData. +func (sd *SignedData) Sign(chain []*x509.Certificate, signer crypto.Signer) error { + return sd.psd.AddSignerInfo(chain, signer) +} diff --git a/ietf-cms/sign_test.go b/ietf-cms/sign_test.go new file mode 100644 index 0000000..21d210f --- /dev/null +++ b/ietf-cms/sign_test.go @@ -0,0 +1,221 @@ +package cms + +import ( + "crypto/x509" + "encoding/pem" + "io/ioutil" + "os" + "os/exec" + "testing" + "time" +) + +var ( + examplePrivateKey = leaf.PrivateKey + exampleChain = leaf.Chain() +) + +func TestSign(t *testing.T) { + data := []byte("hello, world!") + + ci, err := Sign(data, leaf.Chain(), leaf.PrivateKey) + if err != nil { + t.Fatal(err) + } + + sd2, err := ParseSignedData(ci) + if err != nil { + t.Fatal(err) + } + + if _, err = sd2.Verify(rootOpts); err != nil { + t.Fatal(err) + } + + // test that we're including whole chain in sd + sdCerts, err := sd2.psd.X509Certificates() + if err != nil { + t.Fatal(err) + } + for _, chainCert := range leaf.Chain() { + var found bool + for _, sdCert := range sdCerts { + if sdCert.Equal(chainCert) { + if found == true { + t.Fatal("duplicate cert in sd") + } + found = true + } + } + if !found { + t.Fatal("missing cert in sd") + } + } + + // check that we're including signing time attribute + st, err := sd2.psd.SignerInfos[0].GetSigningTimeAttribute() + if st.After(time.Now().Add(time.Second)) || st.Before(time.Now().Add(-time.Second)) { + t.Fatal("expected SigningTime to be now. Difference was", st.Sub(time.Now())) + } +} + +func TestSignDetached(t *testing.T) { + data := []byte("hello, world!") + + ci, err := SignDetached(data, leaf.Chain(), leaf.PrivateKey) + if err != nil { + t.Fatal(err) + } + + sd2, err := ParseSignedData(ci) + if err != nil { + t.Fatal(err) + } + + if _, err = sd2.VerifyDetached(data, rootOpts); err != nil { + t.Fatal(err) + } + + // test that we're including whole chain in sd + sdCerts, err := sd2.psd.X509Certificates() + if err != nil { + t.Fatal(err) + } + for _, chainCert := range leaf.Chain() { + var found bool + for _, sdCert := range sdCerts { + if sdCert.Equal(chainCert) { + if found == true { + t.Fatal("duplicate cert in sd") + } + found = true + } + } + if !found { + t.Fatal("missing cert in sd") + } + } + + // check that we're including signing time attribute + st, err := sd2.psd.SignerInfos[0].GetSigningTimeAttribute() + if st.After(time.Now().Add(time.Second)) || st.Before(time.Now().Add(-time.Second)) { + t.Fatal("expected SigningTime to be now. Difference was", st.Sub(time.Now())) + } +} + +func TestSignDetachedWithOpenSSL(t *testing.T) { + // Do not require this test to pass if openssl is not in the path + opensslPath, err := exec.LookPath("openssl") + if err != nil { + t.Skip("could not find openssl in path") + } + + content := []byte("hello, world!") + + signatureDER, err := SignDetached(content, leaf.Chain(), leaf.PrivateKey) + if err != nil { + t.Fatal(err) + } + + signatureFile, err := ioutil.TempFile("", "TestSignatureOpenSSL_signatureFile_*") + if err != nil { + t.Fatal(err) + } + + _, err = signatureFile.Write(signatureDER) + if err != nil { + t.Fatal(err) + } + + signatureFile.Close() + + // write content to a temp file + contentFile, err := ioutil.TempFile("", "TestSignatureOpenSSL_contentFile_*") + if err != nil { + t.Fatal(err) + } + + _, err = contentFile.Write(content) + if err != nil { + t.Fatal(err) + } + + contentFile.Close() + + // write CA cert to a temp file + certsFile, err := ioutil.TempFile("", "TestSignatureOpenSSL_certsFile_*") + if err != nil { + t.Fatal(err) + } + + for _, cert := range leaf.Chain() { + // write leaf as PEM + certBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + } + + certPEM := pem.EncodeToMemory(certBlock) + + _, err = certsFile.Write(certPEM) + if err != nil { + t.Fatal(err) + } + } + + certsFile.Close() + + cmd := exec.Command(opensslPath, "cms", "-verify", + "-content", contentFile.Name(), "-binary", + "-in", signatureFile.Name(), "-inform", "DER", + "-CAfile", certsFile.Name()) + + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + // + // Remove temporary files if test was successful. + // Intentionally leave the temp files if test fails. + // + os.Remove(contentFile.Name()) + os.Remove(signatureFile.Name()) + os.Remove(certsFile.Name()) +} + +func TestSignRemoveHeaders(t *testing.T) { + sd, err := NewSignedData([]byte("hello, world")) + if err != nil { + t.Fatal(err) + } + if err = sd.Sign(leaf.Chain(), leaf.PrivateKey); err != nil { + t.Fatal(err) + } + if err = sd.SetCertificates([]*x509.Certificate{}); err != nil { + t.Fatal(err) + } + if certs, err := sd.GetCertificates(); err != nil { + t.Fatal(err) + } else if len(certs) != 0 { + t.Fatal("expected 0 certs") + } + + der, err := sd.ToDER() + if err != nil { + t.Fatal(err) + } + if sd, err = ParseSignedData(der); err != nil { + t.Fatal(err) + } + sd.SetCertificates([]*x509.Certificate{leaf.Certificate}) + + opts := x509.VerifyOptions{ + Roots: root.ChainPool(), + Intermediates: leaf.ChainPool(), + } + + if _, err := sd.Verify(opts); err != nil { + t.Fatal(err) + } +} diff --git a/ietf-cms/signed_data.go b/ietf-cms/signed_data.go new file mode 100644 index 0000000..edfcf41 --- /dev/null +++ b/ietf-cms/signed_data.go @@ -0,0 +1,83 @@ +package cms + +import ( + "crypto/x509" + "encoding/asn1" + + "github.com/github/smimesign/ietf-cms/protocol" +) + +// SignedData represents a signed message or detached signature. +type SignedData struct { + psd *protocol.SignedData +} + +// NewSignedData creates a new SignedData from the given data. +func NewSignedData(data []byte) (*SignedData, error) { + eci, err := protocol.NewDataEncapsulatedContentInfo(data) + if err != nil { + return nil, err + } + + psd, err := protocol.NewSignedData(eci) + if err != nil { + return nil, err + } + + return &SignedData{psd}, nil +} + +// ParseSignedData parses a SignedData from BER encoded data. +func ParseSignedData(ber []byte) (*SignedData, error) { + ci, err := protocol.ParseContentInfo(ber) + if err != nil { + return nil, err + } + + psd, err := ci.SignedDataContent() + if err != nil { + return nil, err + } + + return &SignedData{psd}, nil +} + +// GetData gets the encapsulated data from the SignedData. Nil will be returned +// if this is a detached signature. A protocol.ErrWrongType will be returned if +// the SignedData encapsulates something other than data (1.2.840.113549.1.7.1). +func (sd *SignedData) GetData() ([]byte, error) { + return sd.psd.EncapContentInfo.DataEContent() +} + +// GetCertificates gets all the certificates stored in the SignedData. +func (sd *SignedData) GetCertificates() ([]*x509.Certificate, error) { + return sd.psd.X509Certificates() +} + +// SetCertificates replaces the certificates stored in the SignedData with new +// ones. +func (sd *SignedData) SetCertificates(certs []*x509.Certificate) error { + sd.psd.ClearCertificates() + for _, cert := range certs { + if err := sd.psd.AddCertificate(cert); err != nil { + return err + } + } + return nil +} + +// Detached removes the data content from this SignedData. No more signatures +// can be added after this method has been called. +func (sd *SignedData) Detached() { + sd.psd.EncapContentInfo.EContent = asn1.RawValue{} +} + +// IsDetached checks if this SignedData has data content. +func (sd *SignedData) IsDetached() bool { + return sd.psd.EncapContentInfo.EContent.Bytes == nil +} + +// ToDER encodes this SignedData message using DER. +func (sd *SignedData) ToDER() ([]byte, error) { + return sd.psd.ContentInfoDER() +} diff --git a/ietf-cms/timestamp.go b/ietf-cms/timestamp.go new file mode 100644 index 0000000..6b561a4 --- /dev/null +++ b/ietf-cms/timestamp.go @@ -0,0 +1,129 @@ +package cms + +import ( + "bytes" + "crypto/x509" + "errors" + + "github.com/github/smimesign/ietf-cms/oid" + "github.com/github/smimesign/ietf-cms/protocol" + "github.com/github/smimesign/ietf-cms/timestamp" +) + +// AddTimestamps adds a timestamp to the SignedData using the RFC3161 +// timestamping service at the given URL. This timestamp proves that the signed +// message existed the time of generation, allowing verifiers to have more trust +// in old messages signed with revoked keys. +func (sd *SignedData) AddTimestamps(url string) error { + var ( + attrs = make([]protocol.Attribute, len(sd.psd.SignerInfos)) + err error + ) + + // Fetch all timestamp tokens before adding any to sd. This avoids a partial + // failure. + for i := range attrs { + if attrs[i], err = fetchTS(url, sd.psd.SignerInfos[i]); err != nil { + return err + } + } + + for i := range attrs { + sd.psd.SignerInfos[i].UnsignedAttrs = append(sd.psd.SignerInfos[i].UnsignedAttrs, attrs[i]) + } + + return nil +} + +func fetchTS(url string, si protocol.SignerInfo) (protocol.Attribute, error) { + nilAttr := protocol.Attribute{} + + req, err := tsRequest(si) + if err != nil { + return nilAttr, err + } + + resp, err := req.Do(url) + if err != nil { + return nilAttr, err + } + + if tsti, err := resp.Info(); err != nil { + return nilAttr, err + } else if !req.Matches(tsti) { + return nilAttr, errors.New("invalid message imprint") + } + + return protocol.NewAttribute(oid.AttributeTimeStampToken, resp.TimeStampToken) +} + +func tsRequest(si protocol.SignerInfo) (timestamp.Request, error) { + hash, err := si.Hash() + if err != nil { + return timestamp.Request{}, err + } + + mi, err := timestamp.NewMessageImprint(hash, bytes.NewReader(si.Signature)) + if err != nil { + return timestamp.Request{}, err + } + + return timestamp.Request{ + Version: 1, + CertReq: true, + Nonce: timestamp.GenerateNonce(), + MessageImprint: mi, + }, nil +} + +// getTimestamp verifies and returns the timestamp.Info from the SignerInfo. +func getTimestamp(si protocol.SignerInfo, opts x509.VerifyOptions) (timestamp.Info, error) { + rawValue, err := si.UnsignedAttrs.GetOnlyAttributeValueBytes(oid.AttributeTimeStampToken) + if err != nil { + return timestamp.Info{}, err + } + + tst, err := ParseSignedData(rawValue.FullBytes) + if err != nil { + return timestamp.Info{}, err + } + + tsti, err := timestamp.ParseInfo(tst.psd.EncapContentInfo) + if err != nil { + return timestamp.Info{}, err + } + + if tsti.Version != 1 { + return timestamp.Info{}, protocol.ErrUnsupported + } + + // verify timestamp signature and certificate chain.. + if _, err = tst.Verify(opts); err != nil { + return timestamp.Info{}, err + } + + // verify timestamp token matches SignerInfo. + hash, err := tsti.MessageImprint.Hash() + if err != nil { + return timestamp.Info{}, err + } + mi, err := timestamp.NewMessageImprint(hash, bytes.NewReader(si.Signature)) + if err != nil { + return timestamp.Info{}, err + } + if !mi.Equal(tsti.MessageImprint) { + return timestamp.Info{}, errors.New("invalid message imprint") + } + + return tsti, nil +} + +// hasTimestamp checks if si has a timestamp. +func hasTimestamp(si protocol.SignerInfo) (bool, error) { + vals, err := si.UnsignedAttrs.GetValues(oid.AttributeTimeStampToken) + if err != nil { + return false, err + } + + return len(vals) > 0, nil +} diff --git a/ietf-cms/timestamp/timestamp.go b/ietf-cms/timestamp/timestamp.go new file mode 100644 index 0000000..f4f3604 --- /dev/null +++ b/ietf-cms/timestamp/timestamp.go @@ -0,0 +1,427 @@ +package timestamp + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "io" + "math/big" + "net/http" + "strings" + "time" + + "github.com/github/smimesign/ietf-cms/oid" + "github.com/github/smimesign/ietf-cms/protocol" +) + +// HTTPClient is an interface for *http.Client, allowing callers to customize +// HTTP behavior. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// DefaultHTTPClient is the HTTP client used for fetching timestamps. This +// variable may be changed to modify HTTP behavior (eg. add timeouts). +var DefaultHTTPClient = HTTPClient(http.DefaultClient) + +const ( + contentTypeTSQuery = "application/timestamp-query" + contentTypeTSReply = "application/timestamp-reply" + nonceBytes = 16 +) + +// GenerateNonce generates a new nonce for this TSR. +func GenerateNonce() *big.Int { + buf := make([]byte, nonceBytes) + if _, err := rand.Read(buf); err != nil { + panic(err) + } + + return new(big.Int).SetBytes(buf[:]) +} + +// Request is a TimeStampReq +// TimeStampReq ::= SEQUENCE { +// version INTEGER { v1(1) }, +// messageImprint MessageImprint, +// --a hash algorithm OID and the hash value of the data to be +// --time-stamped +// reqPolicy TSAPolicyId OPTIONAL, +// nonce INTEGER OPTIONAL, +// certReq BOOLEAN DEFAULT FALSE, +// extensions [0] IMPLICIT Extensions OPTIONAL } +type Request struct { + Version int + MessageImprint MessageImprint + ReqPolicy asn1.ObjectIdentifier `asn1:"optional"` + Nonce *big.Int `asn1:"optional"` + CertReq bool `asn1:"optional,default:false"` + Extensions []pkix.Extension `asn1:"tag:1,optional"` +} + +// Matches checks if the MessageImprint and Nonce from a responsee match those +// of the request. +func (req Request) Matches(tsti Info) bool { + if !req.MessageImprint.Equal(tsti.MessageImprint) { + return false + } + + if req.Nonce != nil && tsti.Nonce == nil || req.Nonce.Cmp(tsti.Nonce) != 0 { + return false + } + + return true +} + +// Do sends this timestamp request to the specified timestamp service, returning +// the parsed response. The timestamp.HTTPClient is used to make the request and +// HTTP behavior can be modified by changing that variable. +func (req Request) Do(url string) (Response, error) { + var nilResp Response + + reqDER, err := asn1.Marshal(req) + if err != nil { + return nilResp, err + } + + httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(reqDER)) + if err != nil { + return nilResp, err + } + httpReq.Header.Add("Content-Type", contentTypeTSQuery) + + httpResp, err := DefaultHTTPClient.Do(httpReq) + if err != nil { + return nilResp, err + } + if ct := httpResp.Header.Get("Content-Type"); ct != contentTypeTSReply { + return nilResp, fmt.Errorf("Bad content-type: %s", ct) + } + + buf := bytes.NewBuffer(make([]byte, 0, httpResp.ContentLength)) + if _, err = io.Copy(buf, httpResp.Body); err != nil { + return nilResp, err + } + + return ParseResponse(buf.Bytes()) +} + +// Response is a TimeStampResp +// TimeStampResp ::= SEQUENCE { +// status PKIStatusInfo, +// timeStampToken TimeStampToken OPTIONAL } +// +// TimeStampToken ::= ContentInfo +type Response struct { + Status PKIStatusInfo + TimeStampToken protocol.ContentInfo `asn1:"optional"` +} + +// ParseResponse parses a BER encoded TimeStampResp. +func ParseResponse(ber []byte) (Response, error) { + var resp Response + + der, err := protocol.BER2DER(ber) + if err != nil { + return resp, err + } + + rest, err := asn1.Unmarshal(der, &resp) + if err != nil { + return resp, err + } + if len(rest) > 0 { + return resp, protocol.ErrTrailingData + } + + return resp, nil +} + +// Info gets an Info from the response, doing no validation of the SignedData. +func (r Response) Info() (Info, error) { + var nilInfo Info + + if err := r.Status.GetError(); err != nil { + return nilInfo, err + } + + sd, err := r.TimeStampToken.SignedDataContent() + if err != nil { + return nilInfo, err + } + + return ParseInfo(sd.EncapContentInfo) +} + +// PKIStatusInfo ::= SEQUENCE { +// status PKIStatus, +// statusString PKIFreeText OPTIONAL, +// failInfo PKIFailureInfo OPTIONAL } +// +// PKIStatus ::= INTEGER { +// granted (0), +// -- when the PKIStatus contains the value zero a TimeStampToken, as +// requested, is present. +// grantedWithMods (1), +// -- when the PKIStatus contains the value one a TimeStampToken, +// with modifications, is present. +// rejection (2), +// waiting (3), +// revocationWarning (4), +// -- this message contains a warning that a revocation is +// -- imminent +// revocationNotification (5) +// -- notification that a revocation has occurred } +// +// -- When the TimeStampToken is not present +// -- failInfo indicates the reason why the +// -- time-stamp request was rejected and +// -- may be one of the following values. +// +// PKIFailureInfo ::= BIT STRING { +// badAlg (0), +// -- unrecognized or unsupported Algorithm Identifier +// badRequest (2), +// -- transaction not permitted or supported +// badDataFormat (5), +// -- the data submitted has the wrong format +// timeNotAvailable (14), +// -- the TSA's time source is not available +// unacceptedPolicy (15), +// -- the requested TSA policy is not supported by the TSA. +// unacceptedExtension (16), +// -- the requested extension is not supported by the TSA. +// addInfoNotAvailable (17) +// -- the additional information requested could not be understood +// -- or is not available +// systemFailure (25) +// -- the request cannot be handled due to system failure } +type PKIStatusInfo struct { + Status int + StatusString PKIFreeText `asn1:"optional"` + FailInfo asn1.BitString `asn1:"optional"` +} + +// Error represents an unsuccessful PKIStatusInfo as an error. +func (si PKIStatusInfo) GetError() error { + if si.Status == 0 { + return nil + } + return si +} + +// Error implements the error interface. +func (si PKIStatusInfo) Error() string { + fiStr := "" + if si.FailInfo.BitLength > 0 { + fibin := make([]byte, si.FailInfo.BitLength) + for i := range fibin { + if si.FailInfo.At(i) == 1 { + fibin[i] = byte('1') + } else { + fibin[i] = byte('0') + } + } + fiStr = fmt.Sprintf(" FailInfo(0b%s)", string(fibin)) + } + + statusStr := "" + if len(si.StatusString) > 0 { + if strs, err := si.StatusString.Strings(); err == nil { + statusStr = fmt.Sprintf(" StatusString(%s)", strings.Join(strs, ",")) + } + } + + return fmt.Sprintf("Bad TimeStampResp: Status(%d)%s%s", si.Status, statusStr, fiStr) +} + +// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String +type PKIFreeText []asn1.RawValue + +// Append returns a new copy of the PKIFreeText with the provided string +// appended. +func (ft PKIFreeText) Append(t string) PKIFreeText { + return append(ft, asn1.RawValue{ + Class: asn1.ClassUniversal, + Tag: asn1.TagUTF8String, + Bytes: []byte(t), + }) +} + +// Strings decodes the PKIFreeText into a []string. +func (ft PKIFreeText) Strings() ([]string, error) { + strs := make([]string, len(ft)) + + for i := range ft { + if rest, err := asn1.Unmarshal(ft[i].FullBytes, &strs[i]); err != nil { + return nil, err + } else if len(rest) != 0 { + return nil, protocol.ErrTrailingData + } + } + + return strs, nil +} + +// Info is a TSTInfo +// TSTInfo ::= SEQUENCE { +// version INTEGER { v1(1) }, +// policy TSAPolicyId, +// messageImprint MessageImprint, +// -- MUST have the same value as the similar field in +// -- TimeStampReq +// serialNumber INTEGER, +// -- Time-Stamping users MUST be ready to accommodate integers +// -- up to 160 bits. +// genTime GeneralizedTime, +// accuracy Accuracy OPTIONAL, +// ordering BOOLEAN DEFAULT FALSE, +// nonce INTEGER OPTIONAL, +// -- MUST be present if the similar field was present +// -- in TimeStampReq. In that case it MUST have the same value. +// tsa [0] GeneralName OPTIONAL, +// extensions [1] IMPLICIT Extensions OPTIONAL } +// +// TSAPolicyId ::= OBJECT IDENTIFIER +type Info struct { + Version int + Policy asn1.ObjectIdentifier + MessageImprint MessageImprint + SerialNumber *big.Int + GenTime time.Time `asn1:"generalized"` + Accuracy Accuracy `asn1:"optional"` + Ordering bool `asn1:"optional,default:false"` + Nonce *big.Int `asn1:"optional"` + TSA asn1.RawValue `asn1:"tag:0,optional"` + Extensions []pkix.Extension `asn1:"tag:1,optional"` +} + +// ParseInfo parses an Info out of a CMS EncapsulatedContentInfo. +func ParseInfo(eci protocol.EncapsulatedContentInfo) (Info, error) { + i := Info{} + + if !eci.EContentType.Equal(oid.ContentTypeTSTInfo) { + return i, protocol.ErrWrongType + } + + ecval, err := eci.EContentValue() + if err != nil { + return i, err + } + if ecval == nil { + return i, protocol.ASN1Error{Message: "missing EContent for non data type"} + } + + if rest, err := asn1.Unmarshal(ecval, &i); err != nil { + return i, err + } else if len(rest) > 0 { + return i, protocol.ErrTrailingData + } + + return i, nil +} + +// Before checks if the latest time the signature could have been generated at +// is before the specified time. For example, you might check that a signature +// was made *before* a certificate's not-after date. +func (i *Info) Before(t time.Time) bool { + return i.genTimeMax().Before(t) +} + +// After checks if the earlier time the signature could have been generated at +// is before the specified time. For example, you might check that a signature +// was made *after* a certificate's not-before date. +func (i *Info) After(t time.Time) bool { + return i.genTimeMin().After(t) +} + +// genTimeMax is the latest time at which the token could have been generated +// based on the included GenTime and Accuracy attributes. +func (i *Info) genTimeMax() time.Time { + return i.GenTime.Add(i.Accuracy.Duration()) +} + +// genTimeMin is the earliest time at which the token could have been generated +// based on the included GenTime and Accuracy attributes. +func (i *Info) genTimeMin() time.Time { + return i.GenTime.Add(-i.Accuracy.Duration()) +} + +// MessageImprint ::= SEQUENCE { +// hashAlgorithm AlgorithmIdentifier, +// hashedMessage OCTET STRING } +type MessageImprint struct { + HashAlgorithm pkix.AlgorithmIdentifier + HashedMessage []byte +} + +// NewMessageImprint creates a new MessageImprint, digesting all bytes from the +// provided reader using the specified hash. +func NewMessageImprint(hash crypto.Hash, r io.Reader) (MessageImprint, error) { + digestAlgorithm := oid.CryptoHashToDigestAlgorithm[hash] + if len(digestAlgorithm) == 0 { + return MessageImprint{}, protocol.ErrUnsupported + } + + if !hash.Available() { + return MessageImprint{}, protocol.ErrUnsupported + } + h := hash.New() + if _, err := io.Copy(h, r); err != nil { + return MessageImprint{}, err + } + + return MessageImprint{ + HashAlgorithm: pkix.AlgorithmIdentifier{Algorithm: digestAlgorithm}, + HashedMessage: h.Sum(nil), + }, nil +} + +// Hash gets the crypto.Hash associated with this SignerInfo's DigestAlgorithm. +// 0 is returned for unrecognized algorithms. +func (mi MessageImprint) Hash() (crypto.Hash, error) { + algo := mi.HashAlgorithm.Algorithm.String() + hash := oid.DigestAlgorithmToCryptoHash[algo] + if hash == 0 || !hash.Available() { + return 0, protocol.ErrUnsupported + } + + return hash, nil +} + +// Equal checks if this MessageImprint is identical to another MessageImprint. +func (mi MessageImprint) Equal(other MessageImprint) bool { + if !mi.HashAlgorithm.Algorithm.Equal(other.HashAlgorithm.Algorithm) { + return false + } + if len(mi.HashAlgorithm.Parameters.Bytes) > 0 || len(other.HashAlgorithm.Parameters.Bytes) > 0 { + if !bytes.Equal(mi.HashAlgorithm.Parameters.FullBytes, other.HashAlgorithm.Parameters.FullBytes) { + return false + } + } + if !bytes.Equal(mi.HashedMessage, other.HashedMessage) { + return false + } + return true +} + +// Accuracy ::= SEQUENCE { +// seconds INTEGER OPTIONAL, +// millis [0] INTEGER (1..999) OPTIONAL, +// micros [1] INTEGER (1..999) OPTIONAL } +type Accuracy struct { + Seconds int `asn1:"optional"` + Millis int `asn1:"tag:0,optional"` + Micros int `asn1:"tag:1,optional"` +} + +// Duration returns this Accuracy as a time.Duration. +func (a Accuracy) Duration() time.Duration { + return 0 + + time.Duration(a.Seconds)*time.Second + + time.Duration(a.Millis)*time.Millisecond + + time.Duration(a.Micros)*time.Microsecond +} diff --git a/ietf-cms/timestamp/timestamp_test.go b/ietf-cms/timestamp/timestamp_test.go new file mode 100644 index 0000000..f7a8456 --- /dev/null +++ b/ietf-cms/timestamp/timestamp_test.go @@ -0,0 +1,676 @@ +package timestamp + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "errors" + "io" + "math/big" + "net/http" + "strings" + "testing" + "time" + + "github.com/github/smimesign/ietf-cms/protocol" +) + +var ( + errFakeClient = errors.New("fake client") + lastRequest *http.Request +) + +type testHTTPClient struct{} + +func (c testHTTPClient) Do(req *http.Request) (*http.Response, error) { + lastRequest = req + return nil, errFakeClient +} + +func TestRequestDo(t *testing.T) { + DefaultHTTPClient = testHTTPClient{} + + var ( + req = Request{Version: 1} + err error + ) + + req.CertReq = true + req.Nonce = GenerateNonce() + if req.MessageImprint, err = NewMessageImprint(crypto.SHA256, bytes.NewReader([]byte("hello"))); err != nil { + t.Fatal(err) + } + + if _, err = req.Do("https://google.com"); err != errFakeClient { + t.Fatalf("expected errFakeClient, got %v", err) + } + + if lastRequest == nil { + t.Fatal("expected lastRequest") + } + + if ct := lastRequest.Header.Get("Content-Type"); ct != contentTypeTSQuery { + t.Fatalf("expected ts content-type, got %s", ct) + } + + body, err := lastRequest.GetBody() + if err != nil { + t.Fatal(err) + } + buf := bytes.NewBuffer(nil) + if _, err := io.Copy(buf, body); err != nil { + t.Fatal(err) + } + + var req2 Request + if rest, err := asn1.Unmarshal(buf.Bytes(), &req2); err != nil { + t.Fatal(err) + } else if len(rest) > 0 { + t.Fatal("unexpected trailing data") + } +} + +func TestRequestMatches(t *testing.T) { + var err error + + req := Request{Version: 1} + req.Nonce = GenerateNonce() + if req.MessageImprint, err = NewMessageImprint(crypto.SHA256, bytes.NewReader([]byte("hello"))); err != nil { + t.Fatal(err) + } + + tsti := Info{ + MessageImprint: req.MessageImprint, + Nonce: new(big.Int).Set(req.Nonce), + } + + if !req.Matches(tsti) { + t.Fatal("req doesn't match tsti") + } + + tsti.Nonce.SetInt64(123) + if req.Matches(tsti) { + t.Fatal("req matches tsti") + } + tsti.Nonce.Set(req.Nonce) + + tsti.MessageImprint, _ = NewMessageImprint(crypto.SHA256, bytes.NewReader([]byte("asdf"))) + if req.Matches(tsti) { + t.Fatal("req matches tsti") + } +} + +func TestGenerateNonce(t *testing.T) { + nonce := GenerateNonce() + if nonce == nil { + t.Fatal("expected non-nil nonce") + } + + // don't check for exact bitlength match, since leading 0's don't count + // towards length. + if nonce.BitLen() < nonceBytes*8/2 { + t.Fatalf("expected %d bit nonce, got %d", nonceBytes*8, nonce.BitLen()) + } + if nonce.Cmp(new(big.Int)) == 0 { + t.Fatal("expected non-zero nonce") + } +} + +func TestMessageImprint(t *testing.T) { + m := []byte("hello, world!") + mi1, err := NewMessageImprint(crypto.SHA256, bytes.NewReader(m)) + if err != nil { + panic(err) + } + + // same + mi2, err := NewMessageImprint(crypto.SHA256, bytes.NewReader(m)) + if err != nil { + panic(err) + } + if !mi1.Equal(mi2) { + t.Fatal("expected m1==m2") + } + + // round trip + der, err := asn1.Marshal(mi1) + if err != nil { + t.Fatal(err) + } + if _, err = asn1.Unmarshal(der, &mi2); err != nil { + t.Fatal(err) + } + if !mi1.Equal(mi2) { + t.Fatal("expected m1==m2") + } + + // null value for hash alrogithm parameters (as opposed to being absent entirely) + mi2, _ = NewMessageImprint(crypto.SHA256, bytes.NewReader(m)) + mi2.HashAlgorithm.Parameters = asn1.NullRawValue + if !mi1.Equal(mi2) { + t.Fatal("expected m1==m2") + } + + // different digest + mi2, err = NewMessageImprint(crypto.SHA1, bytes.NewReader(m)) + if err != nil { + panic(err) + } + if mi1.Equal(mi2) { + t.Fatal("expected m1!=m2") + } + + // different message + mi2, err = NewMessageImprint(crypto.SHA256, bytes.NewReader([]byte("wrong"))) + if err != nil { + panic(err) + } + if mi1.Equal(mi2) { + t.Fatal("expected m1!=m2") + } + + // bad digest + mi2, _ = NewMessageImprint(crypto.SHA256, bytes.NewReader(m)) + mi2.HashedMessage = mi2.HashedMessage[0 : len(mi2.HashedMessage)-1] + if mi1.Equal(mi2) { + t.Fatal("expected m1!=m2") + } +} + +func TestErrorResponse(t *testing.T) { + // Error response from request with missing message digest. + respDER, _ := protocol.BER2DER(mustBase64Decode("MDQwMgIBAjApDCd0aGUgZGF0YSBzdWJtaXR0ZWQgaGFzIHRoZSB3cm9uZyBmb3JtYXQDAgIE")) + resp, err := ParseResponse(respDER) + if err != nil { + t.Fatal(err) + } + + rt, err := asn1.Marshal(resp) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(respDER, rt) { + t.Fatal("expected round-tripping error TimeStampResp to equal") + } + + expectedStatus := 2 + if resp.Status.Status != expectedStatus { + t.Fatalf("expected status %d, got %d", expectedStatus, resp.Status.Status) + } + + if numStrings := len(resp.Status.StatusString); numStrings != 1 { + t.Fatalf("expected single status string, got %d", numStrings) + } + + expectedString := "the data submitted has the wrong format" + actualStrings, err := resp.Status.StatusString.Strings() + if err != nil { + t.Fatal(err) + } + if actualStrings[0] != expectedString { + t.Fatalf("expected status string %s, got %s", expectedString, actualStrings[0]) + } + + expectedFailInfoLen := 6 + if resp.Status.FailInfo.BitLength != expectedFailInfoLen { + t.Fatalf("expected len(failinfo) %d, got %d", expectedFailInfoLen, resp.Status.FailInfo.BitLength) + } + + expectedFailInfo := []int{0, 0, 0, 0, 0, 1} + for i, v := range expectedFailInfo { + if actual := resp.Status.FailInfo.At(i); actual != v { + t.Fatalf("expected failinfo[%d] to be %d, got %d", i, v, actual) + } + } +} + +func TestPKIFreeText(t *testing.T) { + der := mustBase64Decode("MBUME0JhZCBtZXNzYWdlIGRpZ2VzdC4=") + var ft PKIFreeText + if _, err := asn1.Unmarshal(der, &ft); err != nil { + t.Fatal(err) + } + + rt, err := asn1.Marshal(ft) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(der, rt) { + t.Fatal("expected round-tripped PKIFreeText to match") + } + + ft = PKIFreeText{}.Append("Bad message digest.") + if err != nil { + t.Fatal(err) + } + rt, err = asn1.Marshal(ft) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(der, rt) { + t.Fatal("expected newly made PKIFreeText to match original DER") + } +} + +func TestTSTInfo(t *testing.T) { + resp, err := ParseResponse(fixtureTimestampSymantecWithCerts) + if err != nil { + t.Fatal(err) + } + + sd, err := resp.TimeStampToken.SignedDataContent() + if err != nil { + t.Fatal(err) + } + + inf, err := ParseInfo(sd.EncapContentInfo) + if err != nil { + t.Fatal(err) + } + + expectedVersion := 1 + if inf.Version != expectedVersion { + t.Fatalf("expected version %d, got %d", expectedVersion, inf.Version) + } + + expectedPolicy := asn1.ObjectIdentifier{2, 16, 840, 1, 113733, 1, 7, 23, 3} + if !inf.Policy.Equal(expectedPolicy) { + t.Fatalf("expected policy %s, got %s", expectedPolicy.String(), inf.Policy.String()) + } + + expectedHash := crypto.SHA256 + if hash, err := inf.MessageImprint.Hash(); err != nil { + t.Fatal(err) + } else if hash != expectedHash { + t.Fatalf("expected hash %d, got %d", expectedHash, hash) + } + + expectedMI, _ := NewMessageImprint(crypto.SHA256, bytes.NewReader([]byte("hello\n"))) + if !inf.MessageImprint.Equal(expectedMI) { + t.Fatalf("expected hash %s, got %s", + hex.EncodeToString(expectedMI.HashedMessage), + hex.EncodeToString(inf.MessageImprint.HashedMessage)) + } + + expectedSN := new(big.Int).SetBytes([]byte{0x34, 0x99, 0xB7, 0x2E, 0xCE, 0x6F, 0xB6, 0x6B, 0x68, 0x2D, 0x35, 0x25, 0xC6, 0xE5, 0x6A, 0x07, 0x77, 0x3D, 0xC9, 0xD8}) + if inf.SerialNumber.Cmp(expectedSN) != 0 { + t.Fatalf("expected SN %s, got %s", expectedSN.String(), inf.SerialNumber.String()) + } + + timeFmt := "2006-01-02 15:04:05 MST" + expectedGenTime, _ := time.Parse(timeFmt, "2018-05-09 18:25:22 UTC") + if !inf.GenTime.Equal(expectedGenTime) { + t.Fatalf("expected gentime %s, got %s", expectedGenTime.String(), inf.GenTime.String()) + } + + expectedAccuracy := 30 * time.Second + if accuracy := inf.Accuracy.Duration(); accuracy != expectedAccuracy { + t.Fatalf("expected accurracy %s, got %s", expectedAccuracy.String(), accuracy.String()) + } + + expectedGenTimeMax := expectedGenTime.Add(expectedAccuracy) + if inf.genTimeMax() != expectedGenTimeMax { + t.Fatalf("expected gentimemax %s, got %s", expectedGenTimeMax.String(), inf.genTimeMax().String()) + } + + expectedGenTimeMin := expectedGenTime.Add(-expectedAccuracy) + if inf.genTimeMin() != expectedGenTimeMin { + t.Fatalf("expected gentimemax %s, got %s", expectedGenTimeMin.String(), inf.genTimeMin().String()) + } + + expectedOrdering := false + if inf.Ordering != expectedOrdering { + t.Fatalf("expected ordering %t, got %t", expectedOrdering, inf.Ordering) + } + + if inf.Nonce != nil { + t.Fatal("expected nil nonce") + } + + // don't bother with TSA, since we don't want to mess with parsing GeneralNames. + + if inf.Extensions != nil { + t.Fatal("expected nil extensions") + } +} + +func TestParseTimestampSymantec(t *testing.T) { + testParseInfo(t, fixtureTimestampSymantec) +} + +func TestParseTimestampSymantecWithCerts(t *testing.T) { + testParseInfo(t, fixtureTimestampSymantecWithCerts) +} + +func TestParseTimestampDigicert(t *testing.T) { + testParseInfo(t, fixtureTimestampDigicert) +} + +func TestParseTimestampComodo(t *testing.T) { + testParseInfo(t, fixtureTimestampComodo) +} + +func TestParseTimestampGlobalSign(t *testing.T) { + testParseInfo(t, fixtureTimestampGlobalSign) +} + +func testParseInfo(t *testing.T, ber []byte) { + resp, err := ParseResponse(ber) + if err != nil { + t.Fatal(err) + } + if err = resp.Status.GetError(); err != nil { + t.Fatal(err) + } + + sd, err := resp.TimeStampToken.SignedDataContent() + if err != nil { + t.Fatal(err) + } + + certs, err := sd.X509Certificates() + if err != nil { + t.Fatal(err) + } + + inf, err := ParseInfo(sd.EncapContentInfo) + if err != nil { + t.Fatal(err) + } + + hash, err := inf.MessageImprint.Hash() + if err != nil { + t.Fatal(err) + } + if hash != crypto.SHA256 { + t.Fatalf("expected SHA256 hash, found %s", inf.MessageImprint.HashAlgorithm.Algorithm.String()) + } + + if inf.SerialNumber == nil { + t.Fatal("expected non-nill SN") + } + if inf.SerialNumber.Cmp(big.NewInt(0)) <= 0 { + t.Fatal("expected SN>0") + } + + if inf.Version != 1 { + t.Fatalf("expected tst v1, found %d", inf.Version) + } + + for _, si := range sd.SignerInfos { + if _, err = si.FindCertificate(certs); err != nil && len(certs) > 0 { + t.Fatal(err) + } + + if ct, errr := si.GetContentTypeAttribute(); errr != nil { + t.Fatal(errr) + } else { + // signerInfo contentType attribute must match signedData + // encapsulatedContentInfo content type. + if !ct.Equal(sd.EncapContentInfo.EContentType) { + t.Fatalf("expected %s content, got %s", sd.EncapContentInfo.EContentType.String(), ct.String()) + } + } + + if md, errr := si.GetMessageDigestAttribute(); errr != nil { + t.Fatal(errr) + } else if len(md) == 0 { + t.Fatal("nil/empty message digest attribute") + } + + if algo := si.X509SignatureAlgorithm(); algo == x509.UnknownSignatureAlgorithm { + t.Fatalf("unknown signature algorithm") + } + + var nilTime time.Time + if st, errr := si.GetSigningTimeAttribute(); errr != nil { + t.Fatal(errr) + } else if st == nilTime { + t.Fatal("0 value signing time") + } + } + + // round trip resp + der, err := protocol.BER2DER(ber) + if err != nil { + t.Fatal(err) + } + + der2, err := asn1.Marshal(resp) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(der, der2) { + t.Fatal("re-encoded contentInfo doesn't match original") + } + + // round trip signedData + der = resp.TimeStampToken.Content.Bytes + + der2, err = asn1.Marshal(*sd) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(der, der2) { + t.Fatal("re-encoded signedData doesn't match original") + } +} + +var fixtureTimestampSymantec = mustBase64Decode("" + + "MIIDnjADAgEAMIIDlQYJKoZIhvcNAQcCoIIDhjCCA4ICAQMxDTALBglghkgBZQMEAgEwggEOBgsqhkiG" + + "9w0BCRABBKCB/gSB+zCB+AIBAQYLYIZIAYb4RQEHFwMwMTANBglghkgBZQMEAgEFAAQgWJG1tSLV3wht" + + "D/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgMCFHERZNISITpb8tPCqDQtcNGcWhhSGA8yMDE4MDUwOTE0NTQy" + + "MlowAwIBHqCBhqSBgzCBgDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9u" + + "MR8wHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTEwLwYDVQQDEyhTeW1hbnRlYyBTSEEyNTYg" + + "VGltZVN0YW1waW5nIFNpZ25lciAtIEcyMYICWjCCAlYCAQEwgYswdzELMAkGA1UEBhMCVVMxHTAbBgNV" + + "BAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMSgw" + + "JgYDVQQDEx9TeW1hbnRlYyBTSEEyNTYgVGltZVN0YW1waW5nIENBAhBUWPKq10HWRLyEqXugllLmMAsG" + + "CWCGSAFlAwQCAaCBpDAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTE4" + + "MDUwOTE0NTQyMlowLwYJKoZIhvcNAQkEMSIEIF/3JTU7CB+pzL3Mf+8BKgIRZQlDbovL5WzNhyeTSCn6" + + "MDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIM96wXrQR+zV/cNoIgMbEtTvB4tvK0xea6Qfj/LPS61nMAsG" + + "CSqGSIb3DQEBAQSCAQCRxSB9MLAzK4YnNoFqIK9i71b011Q4pcyF6FEffC3ihOHjdmaHf/rFCeuv4roh" + + "yGxW9cRTshE8UohcghMEuSbkSyaFtVt37o31NC1IvW0vouJVQ0j0rg6nQjlsO9rMGW7cJOS2lVnREqk5" + + "+WfBMKJVnuYSXrnUdxcjSG++4eBCEF5L1fdCVjm4s1hagEORimvUoKuStibW0lwE8rdOEBjusZjRPDV6" + + "hudDhI+2SJPCAFhnNaDDT73y+Ux4x5cVdxHV+tME8kUrr6Hm/l6EyPxu/jwrV/EdJFVsJfkemdJz/ACa" + + "EbbTXfP8KuOwEyUwbFbRCXqO+Z6Gg0RqpiAZWCSM", +) + +var fixtureTimestampSymantecWithCerts = mustBase64Decode("" + + "MIIOLTADAgEAMIIOJAYJKoZIhvcNAQcCoIIOFTCCDhECAQMxDTALBglghkgBZQMEAgEwggEOBgsqhkiG" + + "9w0BCRABBKCB/gSB+zCB+AIBAQYLYIZIAYb4RQEHFwMwMTANBglghkgBZQMEAgEFAAQgWJG1tSLV3wht" + + "D/CxEPvZ0hu0/HFjrzTQgoai6Eb2vgMCFDSZty7Ob7ZraC01Jcblagd3PcnYGA8yMDE4MDUwOTE4MjUy" + + "MlowAwIBHqCBhqSBgzCBgDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9u" + + "MR8wHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTEwLwYDVQQDEyhTeW1hbnRlYyBTSEEyNTYg" + + "VGltZVN0YW1waW5nIFNpZ25lciAtIEczoIIKizCCBTgwggQgoAMCAQICEHsFsdRJaFFE98mJ0pwZnRIw" + + "DQYJKoZIhvcNAQELBQAwgb0xCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjEfMB0G" + + "A1UECxMWVmVyaVNpZ24gVHJ1c3QgTmV0d29yazE6MDgGA1UECxMxKGMpIDIwMDggVmVyaVNpZ24sIElu" + + "Yy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25seTE4MDYGA1UEAxMvVmVyaVNpZ24gVW5pdmVyc2FsIFJv" + + "b3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTYwMTEyMDAwMDAwWhcNMzEwMTExMjM1OTU5WjB3" + + "MQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNVBAsTFlN5bWFu" + + "dGVjIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMTH1N5bWFudGVjIFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0Ew" + + "ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7WZ1ZVU+djHJdGoGi61XzsAGtPHGsMo8Fa4aa" + + "JwAyl2pNyWQUSym7wtkpuS7sY7Phzz8LVpD4Yht+66YH4t5/Xm1AONSRBudBfHkcy8utG7/YlZHz8O5s" + + "+K2WOS5/wSe4eDnFhKXt7a+Hjs6Nx23q0pi1Oh8eOZ3D9Jqo9IThxNF8ccYGKbQ/5IMNJsN7CD5N+Qq3" + + "M0n/yjvU9bKbS+GImRr1wOkzFNbfx4Dbke7+vJJXcnf0zajM/gn1kze+lYhqxdz0sUvUzugJkV+1hHk1" + + "inisGTKPI8EyQRtZDqk+scz51ivvt9jk1R1tETqS9pPJnONI7rtTDtQ2l4Z4xaE3AgMBAAGjggF3MIIB" + + "czAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADBmBgNVHSAEXzBdMFsGC2CGSAGG+EUB" + + "BxcDMEwwIwYIKwYBBQUHAgEWF2h0dHBzOi8vZC5zeW1jYi5jb20vY3BzMCUGCCsGAQUFBwICMBkaF2h0" + + "dHBzOi8vZC5zeW1jYi5jb20vcnBhMC4GCCsGAQUFBwEBBCIwIDAeBggrBgEFBQcwAYYSaHR0cDovL3Mu" + + "c3ltY2QuY29tMDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9zLnN5bWNiLmNvbS91bml2ZXJzYWwtcm9v" + + "dC5jcmwwEwYDVR0lBAwwCgYIKwYBBQUHAwgwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFt" + + "cC0yMDQ4LTMwHQYDVR0OBBYEFK9j1sqjToVy4Ke8QfMpojh/gHViMB8GA1UdIwQYMBaAFLZ3+mlIR59T" + + "EtXC6gcydgfRlwcZMA0GCSqGSIb3DQEBCwUAA4IBAQB16rAt1TQZXDJF/g7h1E+meMFv1+rd3E/zociB" + + "iPenjxXmQCmt5l30otlWZIRxMCrdHmEXZiBWBpgZjV1x8viXvAn9HJFHyeLojQP7zJAv1gpsTjPs1rST" + + "yEyQY0g5QCHE3dZuiZg8tZiX6KkGtwnJj1NXQZAv4R5NTtzKEHhsQm7wtsX4YVxS9U72a433Snq+8839" + + "A9fZ9gOoD+NT9wp17MZ1LqpmhQSZt/gGV+HGDvbor9rsmxgfqrnjOgC/zoqUywHbnsc4uw9Sq9HjlANg" + + "Ck2g/idtFDL8P5dA4b+ZidvkORS92uTTw+orWrOVWFUEfcea7CMDjYUq0v+uqWGBMIIFSzCCBDOgAwIB" + + "AgIQe9Tlr7rMBz+hASMEIkFNEjANBgkqhkiG9w0BAQsFADB3MQswCQYDVQQGEwJVUzEdMBsGA1UEChMU" + + "U3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxKDAmBgNV" + + "BAMTH1N5bWFudGVjIFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMTcxMjIzMDAwMDAwWhcNMjkwMzIy" + + "MjM1OTU5WjCBgDELMAkGA1UEBhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYD" + + "VQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3JrMTEwLwYDVQQDEyhTeW1hbnRlYyBTSEEyNTYgVGltZVN0" + + "YW1waW5nIFNpZ25lciAtIEczMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArw6Kqvjcv2l7" + + "VBdxRwm9jTyB+HQVd2eQnP3eTgKeS3b25TY+ZdUkIG0w+d0dg+k/J0ozTm0WiuSNQI0iqr6nCxvSB7Y8" + + "tRokKPgbclE9yAmIJgg6+fpDI3VHcAyzX1uPCB1ySFdlTa8CPED39N0yOJM/5Sym81kjy4DeE035EMmq" + + "ChhsVWFX0fECLMS1q/JsI9KfDQ8ZbK2FYmn9ToXBilIxq1vYyXRS41dsIr9Vf2/KBqs/SrcidmXs7Dby" + + "lpWBJiz9u5iqATjTryVAmwlT8ClXhVhe6oVIQSGH5d600yaye0BTWHmOUjEGTZQDRcTOPAPstwDyOiLF" + + "tG/l77CKmwIDAQABo4IBxzCCAcMwDAYDVR0TAQH/BAIwADBmBgNVHSAEXzBdMFsGC2CGSAGG+EUBBxcD" + + "MEwwIwYIKwYBBQUHAgEWF2h0dHBzOi8vZC5zeW1jYi5jb20vY3BzMCUGCCsGAQUFBwICMBkaF2h0dHBz" + + "Oi8vZC5zeW1jYi5jb20vcnBhMEAGA1UdHwQ5MDcwNaAzoDGGL2h0dHA6Ly90cy1jcmwud3Muc3ltYW50" + + "ZWMuY29tL3NoYTI1Ni10c3MtY2EuY3JsMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQE" + + "AwIHgDB3BggrBgEFBQcBAQRrMGkwKgYIKwYBBQUHMAGGHmh0dHA6Ly90cy1vY3NwLndzLnN5bWFudGVj" + + "LmNvbTA7BggrBgEFBQcwAoYvaHR0cDovL3RzLWFpYS53cy5zeW1hbnRlYy5jb20vc2hhMjU2LXRzcy1j" + + "YS5jZXIwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0yMDQ4LTYwHQYDVR0OBBYEFKUT" + + "AamfhcwbbhYeXzsxqnk2AHsdMB8GA1UdIwQYMBaAFK9j1sqjToVy4Ke8QfMpojh/gHViMA0GCSqGSIb3" + + "DQEBCwUAA4IBAQBGnq/wuKJfoplIz6gnSyHNsrmmcnBjL+NVKXs5Rk7nfmUGWIu8V4qSDQjYELo2JPoK" + + "e/s702K/SpQV5oLbilRt/yj+Z89xP+YzCdmiWRD0Hkr+Zcze1GvjUil1AEorpczLm+ipTfe0F1mSQcO3" + + "P4bm9sB/RDxGXBda46Q71Wkm1SF94YBnfmKst04uFZrlnCOvWxHqcalB+Q15OKmhDc+0sdo+mnrHIsV0" + + "zd9HCYbE/JElshuW6YUI6N3qdGBuYKVWeg3IRFjc5vlIFJ7lv94AvXexmBRyFCTfxxEsHwA/w0sUxmcc" + + "zB4Go5BfXFSLPuMzW4IPxbeGAk5xn+lmRT92MYICWjCCAlYCAQEwgYswdzELMAkGA1UEBhMCVVMxHTAb" + + "BgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMR8wHQYDVQQLExZTeW1hbnRlYyBUcnVzdCBOZXR3b3Jr" + + "MSgwJgYDVQQDEx9TeW1hbnRlYyBTSEEyNTYgVGltZVN0YW1waW5nIENBAhB71OWvuswHP6EBIwQiQU0S" + + "MAsGCWCGSAFlAwQCAaCBpDAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8X" + + "DTE4MDUwOTE4MjUyMlowLwYJKoZIhvcNAQkEMSIEIF5EOTCml8PvDOxSGeQnbCv+jXprtZlEut7wcOx/" + + "xjfvMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIEIMR0znYAfQI5Tg2l5N58FMaA+eKCATz+9lPvXbcf32H4" + + "MAsGCSqGSIb3DQEBAQSCAQBD1SGuMSSNtmwg38x/1d8v+uvX/2aPIJQS//p5Q54Y8moIEeezRhG0tK3N" + + "81tfKdLeYTVE6VL8D7ZaCpbKzNJeD6DQM4S87bzH88H5RQOb2JTCvBPF3C/ytcl7ylezx6xsFNtftbW3" + + "IOXETaWLgIBpeL7jUZQDhgQ4Xb9HeFl4vA6Wk2kR88h+8Tv2ci0AI9hZgHhH9c/OwPvd8TKbhSjK9qXK" + + "DjaJr0BeVuYHPSWxfsxWVCOjNIOg7moWpPLSYQpqM2gdg5ppjQWffWYC4rywmM6XsBKs+EKFb++4GSOc" + + "wc6JJCugxm8Ba1a6nDAAAQYf/pQyBRRlh/qCHZ0rIoFq", +) + +var fixtureTimestampDigicert = mustBase64Decode("" + + "MIIOuTADAgEAMIIOsAYJKoZIhvcNAQcCoIIOoTCCDp0CAQMxDzANBglghkgBZQMEAgEFADB3BgsqhkiG" + + "9w0BCRABBKBoBGYwZAIBAQYJYIZIAYb9bAcBMDEwDQYJYIZIAWUDBAIBBQAEIFiRtbUi1d8IbQ/wsRD7" + + "2dIbtPxxY6800IKGouhG9r4DAhAvZIfDsFuq0GRqVn9Wu2I8GA8yMDE4MDUwOTE4NDgxOFqgggu7MIIF" + + "MTCCBBmgAwIBAgIQCqEl1tYyG35B5AXaNpfCFTANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEV" + + "MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE" + + "aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTYwMTA3MTIwMDAwWhcNMzEwMTA3MTIwMDAwWjBy" + + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu" + + "Y29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5nIENBMIIBIjAN" + + "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvdAy7kvNj3/dqbqCmcU5VChXtiNKxA4HRTNREH3Q+X1N" + + "aH7ntqD0jbOI5Je/YyGQmL8TvFfTw+F+CNZqFAA49y4eO+7MpvYyWf5fZT/gm+vjRkcGGlV+Cyd+wKL1" + + "oODeIj8O/36V+/OjuiI+GKwR5PCZA207hXwJ0+5dyJoLVOOoCXFr4M8iEA91z3FyTgqt30A6XLdR4aF5" + + "FMZNJCMwXbzsPGBqrC8HzP3w6kfZiFBe/WZuVmEnKYmEUeaC50ZQ/ZQqLKfkdT66mA+Ef58xFNat1fJk" + + "y3seBdCEGXIX8RcG7z3N1k3vBkL9olMqT4UdxB08r8/arBD13ays6Vb/kwIDAQABo4IBzjCCAcowHQYD" + + "VR0OBBYEFPS24SAd/imu0uRhpbKiJbLIFzVuMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgP" + + "MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHkG" + + "CCsGAQUFBwEBBG0wazAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUF" + + "BzAChjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3J0" + + "MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk" + + "SURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVk" + + "SURSb290Q0EuY3JsMFAGA1UdIARJMEcwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczov" + + "L3d3dy5kaWdpY2VydC5jb20vQ1BTMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAQEAcZUS6VGH" + + "VmnN793afKpjerN4zwY3QITvS4S/ys8DAv3Fp8MOIEIsr3fzKx8MIVoqtwU0HWqumfgnoma/Capg33ak" + + "OpMP+LLR2HwZYuhegiUexLoceywh4tZbLBQ1QwRostt1AuByx5jWPGTlH0gQGF+JOGFNYkYkh2OMkVIs" + + "rymJ5Xgf1gsUpYDXEkdws3XVk4WTfraSZ/tTYYmo9WuWwPRYaQ18yAGxuSh1t5ljhSKMYcp5lH5Z/IwP" + + "42+1ASa2bKXuh1Eh5Fhgm7oMLSttosR+u8QlK0cCCHxJrhO24XxCQijGGFbPQTS2Zl22dHv1VjMiLyI2" + + "skuiSpXY9aaOUjCCBoIwggVqoAMCAQICEAnA/EbIBEITtVmLryhPTkEwDQYJKoZIhvcNAQELBQAwcjEL" + + "MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv" + + "bTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQTAeFw0xNzAx" + + "MDQwMDAwMDBaFw0yODAxMTgwMDAwMDBaMEwxCzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdpQ2VydDEq" + + "MCgGA1UEAxMhRGlnaUNlcnQgU0hBMiBUaW1lc3RhbXAgUmVzcG9uZGVyMIIBIjANBgkqhkiG9w0BAQEF" + + "AAOCAQ8AMIIBCgKCAQEAnpWYajQ7cxuofvzHvilpicdoJkZfPY1ic4eBo6Gc8LdbJDdaktT0Wdd2ieTc" + + "1Sfw1Wa8Cu60KzFnrFjFSpFZK0UeCQHWZLNZ7o1mTfsjXswQDQuKZ+9SrqAIkMJS9/WotW6bLHud57U+" + + "+3jNMlAYv0C1TIy7V/SgTxFFbEJCueWv1t/0p3wKaJYP0l8pV877HTL/9BGhEyL7Esvv11PS65fLoqwb" + + "HZ1YIVGCwsLe6is/LCKE0EPsOzs/R8T2VtxFN5i0a3S1Wa94V2nIDwkCeN3YU8GZ22DEnequr+B+hkpc" + + "qVhhqF50igEoaHJOp4adtQJSh3BmSNOO74EkzNzYZQIDAQABo4IDODCCAzQwDgYDVR0PAQH/BAQDAgeA" + + "MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwggG/BgNVHSAEggG2MIIBsjCCAaEG" + + "CWCGSAGG/WwHATCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwggFk" + + "BggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABp" + + "AGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAg" + + "AG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABl" + + "ACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBo" + + "ACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwBy" + + "AHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJ" + + "YIZIAYb9bAMVMB8GA1UdIwQYMBaAFPS24SAd/imu0uRhpbKiJbLIFzVuMB0GA1UdDgQWBBThpzJK7gEh" + + "KH1U1fIHkm60Bw89hzBxBgNVHR8EajBoMDKgMKAuhixodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hh" + + "Mi1hc3N1cmVkLXRzLmNybDAyoDCgLoYsaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJl" + + "ZC10cy5jcmwwgYUGCCsGAQUFBwEBBHkwdzAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu" + + "Y29tME8GCCsGAQUFBzAChkNodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNz" + + "dXJlZElEVGltZXN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4IBAQAe8EGCMq7t8bQ1E9xQwtWX" + + "riIinQ4OrzPTTP18v28BEaeUZSJcxiKhyIlSa5qMc1zZXj8y3hZgTIs2/TGZCr3BhLeNHe+JJhMFVvNH" + + "zUdbrYSyOK9qI7VF4x6IMkaA0remmSL9wXjP9YvYDIwFCe5E5oDVbXDMn1MeJ90qSN7ak2WtbmWjmafC" + + "QA5zzFhPj0Uo5byciOYozmBdLSVdi3MupQ1bUdqaTv9QBYko2vJ4u9JYeI1Ep6w6AJF4aYlkBNNdlt8q" + + "v/mlTCyT/+aK3YKs8dKzooaawVWJVmpHP/rWM5VDNYkFeFo6adoiuARD029oNTZ6FD5F6Zhkhg8TDCZK" + + "MYICTTCCAkkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE" + + "CxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVz" + + "dGFtcGluZyBDQQIQCcD8RsgEQhO1WYuvKE9OQTANBglghkgBZQMEAgEFAKCBmDAaBgkqhkiG9w0BCQMx" + + "DQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTE4MDUwOTE4NDgxOFowLwYJKoZIhvcNAQkEMSIE" + + "IDpdtczqob9pSfKx5ZEHQZSSHM3P+8uGHy1rXmrK9iUjMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFEAB" + + "kUdcmIkd66EEr0cJG1621MvLMA0GCSqGSIb3DQEBAQUABIIBAIlFY+12XT6zvj4/0LVL5//VunTmYTKg" + + "Z6eSrafFT9zOvGbDzm/8XnDLrUQq9Y4kQpE+eKfHWJOBQQZ0ze0wftUml+iRsvqEVlax7G03SzHyPIYH" + + "HzEH/IKRlryHR5LgzzeFqS6IdVg18FBLvrs2fvPJlsj0ZGmAbwn6ntHDromtnkwZV6Cir5gH+wSKuA+Z" + + "3Qj5odgrTQ9gmbmNlFgwp4BwH/vFbBB1eIt7EUD1KfZzThfdFYHnyl8eRcE5p5+MxvyAC78fPzlSlJJP" + + "OES5LDDTx/Qvhet0PjJv70Z7kKgMmAA0BMTRuTnGfiVfEoFm2bzoKmwprU38EPz+PVnrbUA=", +) + +var fixtureTimestampComodo = mustBase64Decode("" + + "MIIDuDADAgEAMIIDrwYJKoZIhvcNAQcCoIIDoDCCA5wCAQMxDzANBglghkgBZQMEAgEFADCCAQ8GCyqG" + + "SIb3DQEJEAEEoIH/BIH8MIH5AgEBBgorBgEEAbIxAgEBMDEwDQYJYIZIAWUDBAIBBQAEIFiRtbUi1d8I" + + "bQ/wsRD72dIbtPxxY6800IKGouhG9r4DAhUA4Fc3zQPRFgrg3c8/sksclhBco7QYDzIwMTgwNTA5MTg0" + + "NzQyWqCBjKSBiTCBhjELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G" + + "A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxLDAqBgNVBAMTI0NPTU9ETyBT" + + "SEEtMjU2IFRpbWUgU3RhbXBpbmcgU2lnbmVyMYICcTCCAm0CAQEwgaowgZUxCzAJBgNVBAYTAlVTMQsw" + + "CQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtlIENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1Qg" + + "TmV0d29yazEhMB8GA1UECxMYaHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNF" + + "UkZpcnN0LU9iamVjdAIQTrCHj8wkNTay2Mn3vzlVdzANBglghkgBZQMEAgEFAKCBmDAaBgkqhkiG9w0B" + + "CQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTE4MDUwOTE4NDc0MlowKwYLKoZIhvcNAQkQ" + + "AgwxHDAaMBgwFgQUNlJ9T6JqaPnrRZbx2Zq7LA6nbfowLwYJKoZIhvcNAQkEMSIEIJeVWgArDRySkAZc" + + "F6na8PZrsUBoQs2jUzy94iOFYfM6MA0GCSqGSIb3DQEBAQUABIIBAKKV56NeTuFn4VdoNv15X0bUWG3p" + + "JSMRVbp1CWktnraj7E5m3BUmFlb4Dwrf3IMmE4QJrGrzDUWtUmpnHR4VuGAUmyajEcmDICc2gpBBG+aV" + + "0Ng/lXQ1xAotKkU7/4wNQY1nOBsquZykYRHWbzJaVxaq8VEc0nVZY2o1TVDgWtLF7BHAd96vw4iVuG3O" + + "Pb8izdFMwQ0t/TMNq0FD0hEFQDSTvVkayeaficblGbhf/p1xuCxSMoFBmnfO56aRX01E3SDNAgo3/hFl" + + "na2g8ESpdWHRMqG3+8ehvgMwljUnhj5+iYT1YF7Rm6KcV2TCIh6QyokN42ji4BMqTlBA7vzSx5A=", +) + +var fixtureTimestampGlobalSign = mustBase64Decode("" + + "MIIDoTADAgEAMIIDmAYJKoZIhvcNAQcCoIIDiTCCA4UCAQMxCzAJBgUrDgMCGgUAMIHdBgsqhkiG9w0B" + + "CRABBKCBzQSByjCBxwIBAQYJKwYBBAGgMgICMDEwDQYJYIZIAWUDBAIBBQAEIFiRtbUi1d8IbQ/wsRD7" + + "2dIbtPxxY6800IKGouhG9r4DAhRYZmxGjSg8ojY0mWZG3dUdVW0mAxgPMjAxODA1MDkxODQ2MjRaoF2k" + + "WzBZMQswCQYDVQQGEwJTRzEfMB0GA1UEChMWR01PIEdsb2JhbFNpZ24gUHRlIEx0ZDEpMCcGA1UEAxMg" + + "R2xvYmFsU2lnbiBUU0EgZm9yIFN0YW5kYXJkIC0gRzIxggKRMIICjQIBATBoMFIxCzAJBgNVBAYTAkJF" + + "MRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMSgwJgYDVQQDEx9HbG9iYWxTaWduIFRpbWVzdGFtcGlu" + + "ZyBDQSAtIEcyAhIRIbRVNR67GrJPl+8H/iqzC4owCQYFKw4DAhoFAKCB/zAaBgkqhkiG9w0BCQMxDQYL" + + "KoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTE4MDUwOTE4NDYyNFowIwYJKoZIhvcNAQkEMRYEFOmL" + + "BqSyLEaL7tN+hDwnk6fha6wfMIGdBgsqhkiG9w0BCRACDDGBjTCBijCBhzCBhAQUg/3hunb+9VKRtQ1o" + + "YZBtqkW1jLUwbDBWpFQwUjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExKDAm" + + "BgNVBAMTH0dsb2JhbFNpZ24gVGltZXN0YW1waW5nIENBIC0gRzICEhEhtFU1Hrsask+X7wf+KrMLijAN" + + "BgkqhkiG9w0BAQEFAASCAQBhWhjTagaTyATim1IHw0tF0wb22rlj6qXki86lclB/2uxBC8/3uLVd259z" + + "iz7aaTmxSj3ksMBq9A75beQW5Be9vK00B/mj/p1dLrtgCcYZtV4uhoBkBx0YbriumEnvQoQL1bI1EiXh" + + "TDbdTrGs2wXn3Xzw/qwqc7w+IjW1BjqzLf6BB9jw2raxMuWBA3EGMwGTumRx5x6a7j2Jx/9Uhs+3ce+9" + + "ZRDtiWAFCkTQVvNLrAuHLTFK6lLOqfucrru76adpJMlTJ+VRut0adpwviS1Cb2ifIX1iUHjtGssihk6v" + + "/tt7Yo4J341G5pC4JDXXhJvxHImNew3l0BWM0LROEgLM", +) + +func mustBase64Decode(b64 string) []byte { + decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64)) + buf := new(bytes.Buffer) + + if _, err := io.Copy(buf, decoder); err != nil { + panic(err) + } + + return buf.Bytes() +} + +// The fixtures above are complete TimeStampResp's, but most of our tests only +// care about the TimeStampToken (CMS ContentInfo) part of it. +func mustExtractTimeStampToken(ber []byte) []byte { + resp, err := ParseResponse(ber) + if err != nil { + panic(err) + } + + tstDER, err := asn1.Marshal(resp.TimeStampToken) + if err != nil { + panic(err) + } + + return tstDER +} diff --git a/ietf-cms/timestamp_test.go b/ietf-cms/timestamp_test.go new file mode 100644 index 0000000..4c377e0 --- /dev/null +++ b/ietf-cms/timestamp_test.go @@ -0,0 +1,197 @@ +package cms + +import ( + "crypto/rsa" + "crypto/x509" + "strings" + "testing" + "time" + + "github.com/github/smimesign/fakeca" + "github.com/github/smimesign/ietf-cms/oid" + "github.com/github/smimesign/ietf-cms/protocol" + "github.com/github/smimesign/ietf-cms/timestamp" +) + +func TestAddTimestamps(t *testing.T) { + // Good response + tsa.Clear() + sd, _ := NewSignedData([]byte("hi")) + sd.Sign(leaf.Chain(), leaf.PrivateKey) + if err := sd.AddTimestamps("https://google.com"); err != nil { + t.Fatal(err) + } + if _, err := sd.Verify(intermediateOpts); err != nil { + t.Fatal(err) + } + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != nil { + t.Fatal(err) + } + + // Error status in response + tsa.HookResponse(func(resp timestamp.Response) timestamp.Response { + resp.Status.Status = 1 + return resp + }) + sd, _ = NewSignedData([]byte("hi")) + sd.Sign(leaf.Chain(), leaf.PrivateKey) + if err := sd.AddTimestamps("https://google.com"); err != nil { + if _, isStatusErr := err.(timestamp.PKIStatusInfo); !isStatusErr { + t.Fatalf("expected timestamp.PKIStatusInfo error, got %v", err) + } + } + + // Bad nonce + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.Nonce.SetInt64(123123) + return info + }) + sd, _ = NewSignedData([]byte("hi")) + sd.Sign(leaf.Chain(), leaf.PrivateKey) + if err := sd.AddTimestamps("https://google.com"); err == nil || err.Error() != "invalid message imprint" { + t.Fatalf("expected 'invalid message imprint', got %v", err) + } + + // Bad message imprint + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.MessageImprint.HashedMessage[0] ^= 0xFF + return info + }) + sd, _ = NewSignedData([]byte("hi")) + sd.Sign(leaf.Chain(), leaf.PrivateKey) + if err := sd.AddTimestamps("https://google.com"); err == nil || err.Error() != "invalid message imprint" { + t.Fatalf("expected 'invalid message imprint', got %v", err) + } +} + +func TestTimestampsVerifications(t *testing.T) { + getTimestampedSignedData := func() *SignedData { + sd, _ := NewSignedData([]byte("hi")) + sd.Sign(leaf.Chain(), leaf.PrivateKey) + tsReq, _ := tsRequest(sd.psd.SignerInfos[0]) + tsResp, _ := tsa.Do(tsReq) + tsAttr, _ := protocol.NewAttribute(oid.AttributeTimeStampToken, tsResp.TimeStampToken) + sd.psd.SignerInfos[0].UnsignedAttrs = append(sd.psd.SignerInfos[0].UnsignedAttrs, tsAttr) + return sd + } + + // Good timestamp + tsa.Clear() + sd := getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != nil { + t.Fatal(err) + } + if _, err := sd.Verify(intermediateOpts); err != nil { + t.Fatal(err) + } + + // Timestamped maybe before not-before + // + // Not-Before Not-After + // |--------------------------------| + // |--------| + // sig-min sig-max + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.Accuracy.Seconds = 30 + info.GenTime = leaf.Certificate.NotBefore + return info + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != nil { + t.Fatal(err) + } + if _, err := sd.Verify(intermediateOpts); err == nil || !strings.HasPrefix(err.Error(), "x509: certificate has expired") { + t.Fatalf("expected expired error, got %v", err) + } + + // Timestamped after not-before + // + // Not-Before Not-After + // |--------------------------------| + // |--------| + // sig-min sig-max + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.Accuracy.Seconds = 30 + info.GenTime = leaf.Certificate.NotBefore.Add(31 * time.Second) + return info + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != nil { + t.Fatal(err) + } + if _, err := sd.Verify(intermediateOpts); err != nil { + t.Fatal(err) + } + + // Timestamped maybe after not-after + // + // Not-Before Not-After + // |--------------------------------| + // |--------| + // sig-min sig-max + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.Accuracy.Seconds = 30 + info.GenTime = leaf.Certificate.NotAfter + return info + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != nil { + t.Fatal(err) + } + if _, err := sd.Verify(intermediateOpts); err == nil || !strings.HasPrefix(err.Error(), "x509: certificate has expired") { + t.Fatalf("expected expired error, got %v", err) + } + + // Timestamped before not-after + // + // Not-Before Not-After + // |--------------------------------| + // |--------| + // sig-min sig-max + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.Accuracy.Seconds = 30 + info.GenTime = leaf.Certificate.NotAfter.Add(-31 * time.Second) + return info + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != nil { + t.Fatal(err) + } + if _, err := sd.Verify(intermediateOpts); err != nil { + t.Fatal(err) + } + + // Bad message imprint + tsa.HookInfo(func(info timestamp.Info) timestamp.Info { + info.MessageImprint.HashedMessage[0] ^= 0xFF + return info + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err == nil || err.Error() != "invalid message imprint" { + t.Fatalf("expected 'invalid message imprint', got %v", err) + } + + // Untrusted signature + tsa.HookToken(func(tst *protocol.SignedData) *protocol.SignedData { + badIdent := fakeca.New() + tst.SignerInfos = nil + tst.AddSignerInfo(badIdent.Chain(), badIdent.PrivateKey) + return tst + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err == nil { + t.Fatal("expected error") + } else if _, ok := err.(x509.UnknownAuthorityError); !ok { + t.Fatalf("expected x509.UnknownAuthorityError, got %v", err) + } + + // Bad signature + tsa.HookToken(func(tst *protocol.SignedData) *protocol.SignedData { + tst.SignerInfos[0].Signature[0] ^= 0xFF + return tst + }) + sd = getTimestampedSignedData() + if _, err := getTimestamp(sd.psd.SignerInfos[0], intermediateOpts); err != rsa.ErrVerification { + t.Fatalf("expected %v, got %v", rsa.ErrVerification, err) + } +} diff --git a/ietf-cms/verify.go b/ietf-cms/verify.go new file mode 100644 index 0000000..70a956c --- /dev/null +++ b/ietf-cms/verify.go @@ -0,0 +1,174 @@ +package cms + +import ( + "bytes" + "crypto/x509" + "errors" + + "github.com/github/smimesign/ietf-cms/protocol" +) + +// Verify verifies the SingerInfos' signatures. Each signature's associated +// certificate is verified using the provided roots. UnsafeNoVerify may be +// specified to skip this verification. Nil may be provided to use system roots. +// The full chains for the certificates whose keys made the signatures are +// returned. +// +// WARNING: this function doesn't do any revocation checking. +func (sd *SignedData) Verify(opts x509.VerifyOptions) ([][][]*x509.Certificate, error) { + econtent, err := sd.psd.EncapContentInfo.EContentValue() + if err != nil { + return nil, err + } + if econtent == nil { + return nil, errors.New("detached signature") + } + + return sd.verify(econtent, opts) +} + +// VerifyDetached verifies the SingerInfos' detached signatures over the +// provided data message. Each signature's associated certificate is verified +// using the provided roots. UnsafeNoVerify may be specified to skip this +// verification. Nil may be provided to use system roots. The full chains for +// the certificates whose keys made the signatures are returned. +// +// WARNING: this function doesn't do any revocation checking. +func (sd *SignedData) VerifyDetached(message []byte, opts x509.VerifyOptions) ([][][]*x509.Certificate, error) { + if sd.psd.EncapContentInfo.EContent.Bytes != nil { + return nil, errors.New("signature not detached") + } + return sd.verify(message, opts) +} + +func (sd *SignedData) verify(econtent []byte, opts x509.VerifyOptions) ([][][]*x509.Certificate, error) { + if len(sd.psd.SignerInfos) == 0 { + return nil, protocol.ASN1Error{Message: "no signatures found"} + } + + certs, err := sd.psd.X509Certificates() + if err != nil { + return nil, err + } + + if opts.Intermediates == nil { + opts.Intermediates = x509.NewCertPool() + } + + for _, cert := range certs { + opts.Intermediates.AddCert(cert) + } + + // Use provided verification options for timestamp verification also, but + // explicitly ask for key-usage=timestamping. + tsOpts := opts + tsOpts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping} + + chains := make([][][]*x509.Certificate, 0, len(sd.psd.SignerInfos)) + + for _, si := range sd.psd.SignerInfos { + var signedMessage []byte + + // SignedAttrs is optional if EncapContentInfo eContentType isn't id-data. + if si.SignedAttrs == nil { + // SignedAttrs may only be absent if EncapContentInfo eContentType is + // id-data. + if !sd.psd.EncapContentInfo.IsTypeData() { + return nil, protocol.ASN1Error{Message: "missing SignedAttrs"} + } + + // If SignedAttrs is absent, the signature is over the original + // encapsulated content itself. + signedMessage = econtent + } else { + // If SignedAttrs is present, we validate the mandatory ContentType and + // MessageDigest attributes. + siContentType, err := si.GetContentTypeAttribute() + if err != nil { + return nil, err + } + if !siContentType.Equal(sd.psd.EncapContentInfo.EContentType) { + return nil, protocol.ASN1Error{Message: "invalid SignerInfo ContentType attribute"} + } + + // Calculate the digest over the actual message. + hash, err := si.Hash() + if err != nil { + return nil, err + } + actualMessageDigest := hash.New() + if _, err = actualMessageDigest.Write(econtent); err != nil { + return nil, err + } + + // Get the digest from the SignerInfo. + messageDigestAttr, err := si.GetMessageDigestAttribute() + if err != nil { + return nil, err + } + + // Make sure message digests match. + if !bytes.Equal(messageDigestAttr, actualMessageDigest.Sum(nil)) { + return nil, errors.New("invalid message digest") + } + + // The signature is over the DER encoded signed attributes, minus the + // leading class/tag/length bytes. This includes the digest of the + // original message, so it is implicitly signed too. + if signedMessage, err = si.SignedAttrs.MarshaledForVerification(); err != nil { + return nil, err + } + } + + cert, err := si.FindCertificate(certs) + if err != nil { + return nil, err + } + + algo := si.X509SignatureAlgorithm() + if algo == x509.UnknownSignatureAlgorithm { + return nil, protocol.ErrUnsupported + } + + if err := cert.CheckSignature(algo, signedMessage, si.Signature); err != nil { + return nil, err + } + + // If the caller didn't specify the signature time, we'll use the verified + // timestamp. If there's no timestamp we use the current time when checking + // the cert validity window. This isn't perfect because the signature may + // have been created before the cert's not-before date, but this is the best + // we can do. We update a copy of opts because we are verifying multiple + // signatures in a loop and only want the timestamp to affect this one. + optsCopy := opts + + if hasTS, err := hasTimestamp(si); err != nil { + return nil, err + } else if hasTS { + tsti, err := getTimestamp(si, tsOpts) + if err != nil { + return nil, err + } + + // This check is slightly redundant, given that the cert validity times + // are checked by cert.Verify. We take the timestamp accuracy into account + // here though, whereas cert.Verify will not. + if !tsti.Before(cert.NotAfter) || !tsti.After(cert.NotBefore) { + return nil, x509.CertificateInvalidError{Cert: cert, Reason: x509.Expired, Detail: ""} + } + + if optsCopy.CurrentTime.IsZero() { + optsCopy.CurrentTime = tsti.GenTime + } + } + + if chain, err := cert.Verify(optsCopy); err != nil { + return nil, err + } else { + chains = append(chains, chain) + } + } + + // OK + return chains, nil +} diff --git a/ietf-cms/verify_test.go b/ietf-cms/verify_test.go new file mode 100644 index 0000000..2adc35a --- /dev/null +++ b/ietf-cms/verify_test.go @@ -0,0 +1,812 @@ +package cms + +import ( + "bytes" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "io" + "strings" + "testing" + + "github.com/github/smimesign/ietf-cms/protocol" + "golang.org/x/xerrors" +) + +func ExampleSignedData() { + data := []byte("hello, world!") + + // Wrap the data in a CMS SignedData structure and sign it with our key. + signedDataDER, err := Sign(data, exampleChain, examplePrivateKey) + if err != nil { + panic(err) + } + + // Re-parse the encoded SignedData structure. + signedData, err := ParseSignedData(signedDataDER) + if err != nil { + panic(err) + } + + // Verify the SignedData's signature. + if _, err = signedData.Verify(x509.VerifyOptions{Roots: root.ChainPool()}); err != nil { + panic(err) + } +} + +func verifyOptionsForSignedData(sd *SignedData) (opts x509.VerifyOptions) { + // add self-signed cert as trusted root + certs, err := sd.psd.X509Certificates() + if err != nil { + panic(err) + } + if len(certs) == 1 { + opts.Roots = x509.NewCertPool() + opts.Roots.AddCert(certs[0]) + } + + // trust signing time + signingTime, err := sd.psd.SignerInfos[0].GetSigningTimeAttribute() + if err != nil { + panic(err) + } + opts.CurrentTime = signingTime + + // Any key usage + opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} + + return +} + +func TestVerify(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureOne) + if err != nil { + t.Fatal(err) + } + + if _, err := sd.Verify(verifyOptionsForSignedData(sd)); err != nil { + t.Fatal(err) + } +} + +func TestVerifyGPGSMAttached(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureGPGSMAttached) + if err != nil { + t.Fatal(err) + } + + if _, err = sd.Verify(verifyOptionsForSignedData(sd)); err != nil { + t.Fatal(err) + } + + data, err := sd.GetData() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(data, []byte("hello\n")) { + t.Fatal("bad msg") + } +} + +func TestVerifyGPGSMDetached(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureGPGSM) + if err != nil { + t.Fatal(err) + } + + if _, err := sd.VerifyDetached([]byte("hello, world!\n"), verifyOptionsForSignedData(sd)); err != nil { + t.Fatal(err) + } +} + +func TestVerifyGPGSMNoCerts(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureNoCertsGPGSM) + if err != nil { + t.Fatal(err) + } + + if _, err := sd.VerifyDetached([]byte("hello, world!\n"), x509.VerifyOptions{}); err != protocol.ErrNoCertificate { + t.Fatalf("expected %v, got %v", protocol.ErrNoCertificate, err) + } +} + +func TestVerifyOpenSSLAttached(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureOpenSSLAttached) + if err != nil { + t.Fatal(err) + } + + if _, err := sd.Verify(verifyOptionsForSignedData(sd)); err != nil { + t.Fatal(err) + } +} + +func TestVerifyOpenSSLDetached(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureOpenSSLDetached) + if err != nil { + t.Fatal(err) + } + + if _, err := sd.VerifyDetached([]byte("hello, world!"), verifyOptionsForSignedData(sd)); err != nil { + t.Fatal(err) + } +} + +func TestVerifyOutlookDetached(t *testing.T) { + sd, err := ParseSignedData(fixtureSignatureOutlookDetached) + if err != nil { + t.Fatal(err) + } + + if _, err := sd.VerifyDetached(fixtureMessageOutlookDetached, verifyOptionsForSignedData(sd)); err != nil { + t.Fatal(err) + } +} + +func TestVerifySmimesignAttachedWithTimestamp(t *testing.T) { + sd, err := ParseSignedData(fixtureSmimesignAttachedWithTimestamp) + if err != nil { + t.Fatal(err) + } + + opts := verifyOptionsForSignedData(sd) + + // specify key usage for signing cert to verify that this isn't also used + // when checking timestamp. + opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection} + + if _, err := sd.Verify(opts); err != nil { + t.Fatal(err) + } +} + +func TestVerifyChain(t *testing.T) { + signerChain := leaf.Chain() + ber, _ := Sign([]byte("hi"), leaf.Chain(), leaf.PrivateKey) + sd, _ := ParseSignedData(ber) + + // good root + chains, err := sd.Verify(rootOpts) + if err != nil { + t.Fatal(err) + } + + if len(chains) != 1 || len(chains[0]) != 1 || len(chains[0][0]) != len(signerChain) { + t.Fatal("bad chain") + } + + for i, c := range signerChain { + if !chains[0][0][i].Equal(c) { + t.Fatalf("bad cert: %d", i) + } + } + + // bad root + if _, err = sd.Verify(otherRootOpts); err != nil { + if _, isX509Err := err.(x509.UnknownAuthorityError); !isX509Err { + t.Fatalf("expected x509.UnknownAuthorityError, got %v", err) + } + } + + // system root + if _, err = sd.Verify(x509.VerifyOptions{}); err != nil { + if _, isX509Err := err.(x509.UnknownAuthorityError); !isX509Err { + t.Fatalf("expected x509.UnknownAuthorityError, got %v", err) + } + } + + // no root + if _, err = sd.Verify(x509.VerifyOptions{Roots: x509.NewCertPool()}); err != nil { + if _, isX509Err := err.(x509.UnknownAuthorityError); !isX509Err { + t.Fatalf("expected x509.UnknownAuthorityError, got %v", err) + } + } +} + +func TestVerifyDSAWithSHA1(t *testing.T) { + // Created with the following openssl commands: + // openssl dsaparam -out dsakey.pem -genkey 1024 + // openssl req -key dsakey.pem -new -x509 -days 365000 -out dsa.crt -sha1 -subj "/CN=foo.com" + // This creates a cert which is valid for 1000 years + const publicCert string = ` +-----BEGIN CERTIFICATE----- +MIICWjCCAhoCCQCNGgCUlB6gszAJBgcqhkjOOAQDMBIxEDAOBgNVBAMMB2Zvby5j +b20wIBcNMTkwNTI5MDMzNzUyWhgPMzAxODA5MjkwMzM3NTJaMBIxEDAOBgNVBAMM +B2Zvby5jb20wggG2MIIBKwYHKoZIzjgEATCCAR4CgYEAyoyaU2206Zuu9MDfQ1gM +Uba4Iu3j9EBWSSYiFjHS93Y2RVGqkNGHqNtLJ1nXANINqjnTP8RxnsccRejhX7C5 +xVAlfsKSvJpRO1idp0SA8tVItpyHNjY175SYFYcg6elr1KQxfd41o/brruo915fs +BXxl0S3261INjJJ64Ybn+CkCFQDa9pKFl6/S1OObPF3XeemwQVSW8QKBgByeV3hw +YGzpdIu+/6iMYAvkNAYVBTfwuYd5Oa1Le2m9detLgcHg/0/q8kD5YafNPKYVAg1N +aD+lLYEkbFuOJo00Pk1zrTQtKkrfbU9EVxd/6/XCrsFAVLl+39Q3vDEEX3tLjf+m +r860lPYoC0+HRj+RGJiYmMqeydsV4N8gtRZbA4GEAAKBgGp8JeErPlZ7l56NG+mL +XJxpVB7Vb0rqM/B0r5kMX/a0Nw7oa0Nehy2BJyvI3zREz2BYJd4RGsIq8cCts2yO +zh8PgBUSNAnEEivfxRV+LivovAjVXqsr53WolzvkCOlxeX15a9SINSDNkphqsZrW +zYTE+BOvUEbM0lM0273nAa4KMAkGByqGSM44BAMDLwAwLAIUURvdNGP0kzOJh79x +ZFtQRP+6EQoCFAmd0+Tig4/yNQ0eSnQEFwEMQiD1 +-----END CERTIFICATE----- +` + + // Signed pkcs7 doc created using above cert and the following commands: + // printf 'Hello, World!' > hello.txt + // openssl smime -sign -in hello.txt -nodetach -nocerts -outform pem -md sha1 -signer dsa.crt -inkey dsakey.pem + const pkcs7Doc string = ` +-----BEGIN PKCS7----- +MIIBvgYJKoZIhvcNAQcCoIIBrzCCAasCAQExCzAJBgUrDgMCGgUAMBwGCSqGSIb3 +DQEHAaAPBA1IZWxsbywgV29ybGQhMYIBeTCCAXUCAQEwHzASMRAwDgYDVQQDDAdm +b28uY29tAgkAjRoAlJQeoLMwCQYFKw4DAhoFAKCCAQcwGAYJKoZIhvcNAQkDMQsG +CSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTkwNTI5MDM1OTU4WjAjBgkqhkiG +9w0BCQQxFgQUCgqfKmdylCVXq1NV12r0Qvj2XgEwgacGCSqGSIb3DQEJDzGBmTCB +ljALBglghkgBZQMEASowCAYGKoUDAgIJMAoGCCqFAwcBAQICMAoGCCqFAwcBAQID +MAgGBiqFAwICFTALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMH +MA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG +9w0DAgIBKDAJBgcqhkjOOAQDBC4wLAIUXZ2aaGVyPDzpb1svc0ruE3qCUzsCFCNw +F1Al5pA+giJh15T7Uu+p5O0J +-----END PKCS7----- +` + + pkcs7CertPEM, _ := pem.Decode([]byte(publicCert)) + if pkcs7CertPEM == nil { + t.Fatal("failed to parse certificate PEM") + } + pkcs7Cert, err := x509.ParseCertificate(pkcs7CertPEM.Bytes) + if err != nil { + t.Fatalf("failed to parse certificate: " + err.Error()) + } + + pkcs7Certs := []*x509.Certificate{pkcs7Cert} + pkcs7VerifyOptions := x509.VerifyOptions{ + Roots: x509.NewCertPool(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + } + pkcs7VerifyOptions.Roots.AddCert(pkcs7Cert) + + derRequest, _ := pem.Decode([]byte(pkcs7Doc)) + if derRequest == nil { + t.Fatalf("failed to parse id doc PEM: %s", pkcs7Doc) + } + sd, err := ParseSignedData([]byte(derRequest.Bytes)) + if err != nil { + t.Fatalf("Error parsing pkcs7 document: %s, err: %v", pkcs7Doc, err) + } + + sd.SetCertificates(pkcs7Certs) + _, err = sd.Verify(pkcs7VerifyOptions) + if err != nil { + if xerrors.Is(err, x509.ErrUnsupportedAlgorithm) { + return + } + t.Fatalf("Error verifying signing request: %v, err %v", *sd, err) + } + data, err := sd.GetData() + if err != nil { + t.Fatalf("Error getting data from pkcs7 document %s, err %v", pkcs7Doc, err) + } + + expectedData := "Hello, World!" + if string(data) != expectedData { + t.Fatalf("Expected data: %s Actual Data: %s", expectedData, data) + } +} + +var fixtureSignatureOne = mustBase64Decode("" + + "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", +) + +var fixtureSignatureGPGSMAttached = mustBase64Decode("" + + "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" + + "SDXMwnexRO3w39tu4ChUFbsX8l5CAAAAAAAA", +) + +var fixtureSignatureGPGSM = mustBase64Decode("" + + "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" + + "SX+du6QAAAAAAAA=", +) + +var fixtureSignatureNoCertsGPGSM = mustBase64Decode("" + + "MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" + + "BwEAADGCAeEwggHdAgEBMCAwFDESMBAGA1UEAxMJQmVuIFRvZXdzAggWdNrn7G+u" + + "SDANBglghkgBZQMEAgEFAKCBkzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG" + + "CSqGSIb3DQEJBTEPFw0xNzExMTcwMDQxNDhaMCgGCSqGSIb3DQEJDzEbMBkwCwYJ" + + "YIZIAWUDBAECMAoGCCqGSIb3DQMHMC8GCSqGSIb3DQEJBDEiBCBNyg/V9CSjGwOr" + + "gHy6536zK/LQie7RzuFUs6/tRY3g3DANBgkqhkiG9w0BAQEFAASCAQAvGAGPMaH3" + + "oRiNDU0AGIVyjXUrZ8g2VRazGCTuuO0CPGWBDbBuuvCePuWTddcv5KHHyrYO0yUD" + + "xergVhh1EXIsOItHbJ6QeMstmY8Ub7HGm4Srdtm3MMSEe24zRmKK5yvPfeaaXeb6" + + "MASKXvViU/j9VDwUZ2CFPUzPq8DlS6j4w6dapfphFGN1wJV3ADLUzUkTXfXQ57HE" + + "WUKdbxgcuyBH7eLhZpKAXP31iRKm2b7dV50SruRCqNYZOp8bUQ57bC2jels0dzQd" + + "EQS76O/DH6eQ3/OgvpmR8BjlujA82tgjqP7fj0S7Cw2VlPqcey0iqRmAmiO2qzOI" + + "KAYzMkxWr7iUAAAAAAAA", +) + +var fixtureSignatureOpenSSLAttached = mustBase64Decode("" + + "MIIFGgYJKoZIhvcNAQcCoIIFCzCCBQcCAQExDzANBglghkgBZQMEAgEFADAcBgkq" + + "hkiG9w0BBwGgDwQNaGVsbG8sIHdvcmxkIaCCAqMwggKfMIIBh6ADAgECAgEAMA0G" + + "CSqGSIb3DQEBBQUAMBMxETAPBgNVBAMMCGNtcy10ZXN0MB4XDTE3MTEyMDIwNTM0" + + "M1oXDTI3MTExODIwNTM0M1owEzERMA8GA1UEAwwIY21zLXRlc3QwggEiMA0GCSqG" + + "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDWMRnJdRQxw8j8Yn3jh/rcZyeALStl+MmM" + + "TEtr6XsmMOWQhnP6nCAIOw5EIAXGpKl4Yg3F2gDKmJCVl279Q+G9nLtvmWvCzu19" + + "BJUG7jVLWzO8KSuJa83iiilZUP2adVZujdGB6dxekIBu7vkYi9XxZJm4edhj0bkd" + + "EtkxLCNUGDQKsywnKOTWzfefT9UCQJyLwt74ThJtNX7uoYrfAHNfBARk3Kx+wf4U" + + "Grd2GmSe8Lnr3FNcZ/uMJffsYvBk3fbDwYsVC6rd4BuJvvri3K1dti3rnvDEnuMI" + + "Ve7a2n7NE7yV0cietIjKeeY8bO25lwrTtBzgP5y1G9spjzAtiRLZAgMBAAEwDQYJ" + + "KoZIhvcNAQEFBQADggEBAMkYPFmsHYlyO+KZMKEWUWOdw1rwrIVhLQOKqLz8Wbe8" + + "lIQ5pdsd4S1DqvMEzYyMtpZckZ9mOBZh/SQsmdb8sZnQwiMvlPSO6IWp/MpuP+VK" + + "v8IBAr1aaLlMaelV086uIFc9coE6XAdWFrGlUT9FYM00JwoSfi51vbcqbIh6P8y9" + + "uwHqlt2vkVYujto+p0UMBnBZkfKBgzMG7ILWpJbVszmpesVzI2XUgq8BxlO0fvw5" + + "m/R4bAtHqXTK0xVrTBXUg6izFbdA3pVlFMiuv8Kq2cyBg+VkXGYmZ37BGhApe5Le" + + "Dabe4iGcXQMW4lunjRSv8gDu/ODA/20OMNVDOx92MTIxggIqMIICJgIBATAYMBMx" + + "ETAPBgNVBAMMCGNtcy10ZXN0AgEAMA0GCWCGSAFlAwQCAQUAoIHkMBgGCSqGSIb3" + + "DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTE3MTEyMDIwNTM0M1ow" + + "LwYJKoZIhvcNAQkEMSIEIGjmVrJR5n6DWL74SDqw1RxmGfPnoanw51g41B/zaPco" + + "MHkGCSqGSIb3DQEJDzFsMGowCwYJYIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglg" + + "hkgBZQMEAQIwCgYIKoZIhvcNAwcwDgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMC" + + "AgFAMAcGBSsOAwIHMA0GCCqGSIb3DQMCAgEoMA0GCSqGSIb3DQEBAQUABIIBAJHB" + + "kfH1hZ4Y0TI6PdW7DNFnb++KQJiu4NmzE7SyTJOCxC2W44uAKUdJw7c8cdn/lcb/" + + "y1kvwNbi2kysuZSTpywBIjHSTw3BTwdaNJFd6HUV1mX2IQRfaJIPW5fqkhLfQtZ6" + + "LZka/HWQ5fwA51g6lVNTMbStjsPlBef6qEDcCLMp/4CNEqC5+fUx8Jb7Q5mvyCHQ" + + "3IZrIEMLBYhrgrm61qh/MXKnAqlEo6XxN1fL0CXDxy9dYPSKr2G66o9+BjmYktF5" + + "3MfxrT4JDizd2S/8BVEv+H+uHmrpyRxMceREPJVrVHOdd922hyKALbAGcoyMdXpj" + + "ZdMtHnR5z07z9wxvwiw=", +) + +var fixtureSignatureOpenSSLDetached = mustBase64Decode("" + + "MIIFCQYJKoZIhvcNAQcCoIIE+jCCBPYCAQExDzANBglghkgBZQMEAgEFADALBgkq" + + "hkiG9w0BBwGgggKjMIICnzCCAYegAwIBAgIBADANBgkqhkiG9w0BAQUFADATMREw" + + "DwYDVQQDDAhjbXMtdGVzdDAeFw0xNzExMjAyMTE0NDdaFw0yNzExMTgyMTE0NDda" + + "MBMxETAPBgNVBAMMCGNtcy10ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB" + + "CgKCAQEA5VQ0FRvQRA9F+6nss77yUcm3x8IOoJV/icQrtrkR/BHGgeepcLIcHkWh" + + "s/cap69xR5TCtONy0I4tqKf/vXnKXvMjsGGrecFMi8NVTbEoNg9m47nbdO7BY1+f" + + "waLfwAX5vf17BRSqA0wRIoNIzJc07mNrI84EbKfVmDtPrqzwnT0sIKqj5p2PQdWi" + + "sPwOocLYJBdAPglnLuFk6WTZalJRgV7h50nl1GBDKJVo1Yc7zqPdqWzHzFqK759g" + + "CHBZMYJdqIx/wev/l66oEcJZr6gnnKzq8lsWljpjVWD96z/W/fehWZsWlWkvmrus" + + "qizMbL0vCx8HrReo7+hszMIHR5bwTwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQAD" + + "ZjPxm/JHc4KoQUaVOSAU97lO60MD21Ud0LtaebbiSJnaMH9a/rb3kuxJAKVSBhDp" + + "wyRK19KNtaSXHEAD48aJeT7J4wsDJFNfKGx/9R2iYB5xjc/POpK13A/o4fDrpLWL" + + "1doIc0KjVA63BXaYOwsEj2iKzUKNFZ2kS3bXMkEBhUDUXtSo08WFI7UkgYTuIfM2" + + "LS/wyORcwZIEIvq+ndkch/nAyQZ8U0/85dgwpOQcyZ0UDiu8Ti9z9IUlhxSq2T13" + + "JhIfiMa4m27y71JmsFy12uN3fGBckkyNkKkxVMy0H4Ukr1hq/ZkvH3HdrEnWmNEu" + + "WdU7WvIBsbe3U2idyhBSMYICKjCCAiYCAQEwGDATMREwDwYDVQQDDAhjbXMtdGVz" + + "dAIBADANBglghkgBZQMEAgEFAKCB5DAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB" + + "MBwGCSqGSIb3DQEJBTEPFw0xNzExMjAyMTE0NDdaMC8GCSqGSIb3DQEJBDEiBCBo" + + "5layUeZ+g1i++Eg6sNUcZhnz56Gp8OdYONQf82j3KDB5BgkqhkiG9w0BCQ8xbDBq" + + "MAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3" + + "DQMHMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggq" + + "hkiG9w0DAgIBKDANBgkqhkiG9w0BAQEFAASCAQAcLsBbjvlhz+HAy7m5cvh8tRav" + + "xT05fFK1hwBC287z+D/UaCrvrd2vR4bdUV8jfS5iTyUfX/BikOljxRwUMgtBLPKq" + + "gdNokoxUoQiqVOdgCER0isNLF/8+O29reI6N/9Mp+IpfE41o2xcRrggfncuPX00K" + + "MB2K4/ZF35HddfblHIgQ+9gWfHE52KMur4XeI5sc/izMNuPyR8VVB7St5JLMepHj" + + "UtbPYBJ0bRSwDX1JAoB+Ze/mPvCmo/pS5QyYfNvXg3Jw4TVoud5+oUH9r6MwSxzN" + + "BSws5SM9d0GAafR+Hj19x9s8ypUjLJmGIAjeTrlgcYUTJjnfEtZBL5Je2FuK", +) + +var fixtureSignatureOutlookDetached = mustBase64Decode("" + + "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" + + "MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDBDBgNVHSAEPDA6MDgGCmCGSAGG/WwEAQIw" + + "KjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBiAYDVR0fBIGAMH4w" + + "PaA7oDmGN2h0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURDQS1n" + + "Mi5jcmwwPaA7oDmGN2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVk" + + "SURDQS1nMi5jcmwweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp" + + "Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy" + + "dFNIQTJBc3N1cmVkSURDQS5jcnQwDQYJKoZIhvcNAQELBQADggEBADh2DYGfn+1eg21dTa34iZlu" + + "IyActG/S23bCLnJSThPbiCfZgGkKr9Bq6TSJ4qQfsquIB7cO46mJ+tzHL570xAsJ4pC7z3RhBdzK" + + "j9uT6ZUExdHQs2FoPjU5uT1UhqHv7T9qYp689XpZ2xPLH59SwLASIVnoQFIS0MKT8AN6ZgKxDWDY" + + "EUyRfGQxxDbfqWhncH0qxT20mv8TnvIMo2ngsCBZfpJcv9u3LijnD7uVCZ2qRIJkmJ7s1eoGc05c" + + "Z+7NeA8vC28BgGe2svMUlRInaNsMDUBmizI4x6DnS8uVlX22KAdPML9NvPOfCGCohDevZgCSMx/o" + + "nH+foA+rOCngkR8wggZOMIIFNqADAgECAhAErnlgZmaQGrnFf6ZsW9zNMA0GCSqGSIb3DQEBCwUA" + + "MGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp" + + "Y2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzExMDUx" + + "MjAwMDBaFw0yODExMDUxMjAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ" + + "bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIgQXNz" + + "dXJlZCBJRCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANz4ESM/arXvwCd5Gy0F" + + "h6IQQzHfDtQVG093pCLOPoxw8L4Hjt0nKrwBHbYsCsrdaVgfQe1qBR/aY3hZHiIsK/i6fsk1O1bx" + + "H3xCfiWwIxnGRTjXPUT5IHxgrhywWhgEvo8796nwlJqmDGNJtkEXU0AyvU/mUHpQHyVF6PGJr83/" + + "Xv9Q8/AXEf+9xYn1vWK52PuORQSFbZnNxUhN/SarAjZF6jbXX2riGoJBCtzp2fWRF47GIa04PBPm" + + "Hn9mnNVN2Uba9s9Sp307JMO0wVE1xpvr1O9+5HsD4US9egs34E/LgooNcRjkpuCJLBvzsnM8wbCS" + + "nhh9vat9xX0IoSzCn3MCAwEAAaOCAvgwggL0MBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/" + + "BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu" + + "Y29tMIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRB" + + "c3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNl" + + "cnRBc3N1cmVkSURSb290Q0EuY3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDBDCCAbMG" + + "A1UdIASCAaowggGmMIIBogYKYIZIAYb9bAACBDCCAZIwKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3" + + "LmRpZ2ljZXJ0LmNvbS9DUFMwggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABv" + + "AGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQA" + + "ZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0" + + "ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQA" + + "eQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBp" + + "AGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUA" + + "cgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wHQYDVR0OBBYEFOcCI4AAT9jXvJQL" + + "2T90OUkyPIp5MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUA" + + "A4IBAQBO1Iknuf0dh3d+DygFkPEKL8k7Pr2TnJDGr/qRUYcyVGvoysFxUVyZjrX64GIZmaYHmnwT" + + "J9vlAqKEEtkV9gpEV8Q0j21zHzrWoAE93uOC5EVrsusl/YBeHTmQvltC9s6RYOP5oFYMSBDOM2h7" + + "zZOr8GrLT1gPuXtdGwSBnqci4ldJJ+6Skwi+aQhTAjouXcgZ9FCATgLZsF2RtJOH+ZaWgVVAjmbt" + + "gti7KF/tTGHtBlgoGVMRRLxHICmyBGzYiVSZO3XbZ3gsHpJ4xlU9WBIRMm69QwxNNNt7xkLb7L6r" + + "m2FMBpLjjt8hKlBXBMBgojXVJJ5mNwlJz9X4ZbPg4m7CMYIDvzCCA7sCAQEweTBlMQswCQYDVQQG" + + "EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw" + + "IgYDVQQDExtEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ0ECEAWkzvCWLw1xJSpSGJSTKHAwDQYJ" + + "YIZIAWUDBAIBBQCgggIXMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X" + + "DTE3MTEyOTE0NDMxOVowLwYJKoZIhvcNAQkEMSIEIEgBjCiMhZLBevfHienSec11YNE+P7PSd4JD" + + "wfCQCrwWMIGIBgkrBgEEAYI3EAQxezB5MGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy" + + "dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIg" + + "QXNzdXJlZCBJRCBDQQIQBaTO8JYvDXElKlIYlJMocDCBigYLKoZIhvcNAQkQAgsxe6B5MGUxCzAJ" + + "BgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j" + + "b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDQQIQBaTO8JYvDXElKlIYlJMo" + + "cDCBkwYJKoZIhvcNAQkPMYGFMIGCMAsGCWCGSAFlAwQBKjALBglghkgBZQMEARYwCgYIKoZIhvcN" + + "AwcwCwYJYIZIAWUDBAECMA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDALBglghkgBZQME" + + "AgEwCwYJYIZIAWUDBAIDMAsGCWCGSAFlAwQCAjAHBgUrDgMCGjANBgkqhkiG9w0BAQEFAASCAQBh" + + "AjkUd98Si7LKxELWdwY8yrqrfK61JxVSxSY/BkF3xS/0QbQMU9Y+0V23nJX5ymamgCd9yNTdNapV" + + "D4OzoVXfmTqd1/AD30M1a1CdBVoNGV8X4Uv8Z1fAl5MN+6Yt1CeIun39gvkutAgUmvCVrjFN+gD6" + + "GH+VTQNGHr3wxdmtL9F8WeNECvpVgYEMqnYRrYHw4B6euJRsy4UnB4Sy/ogV1elkipxCbqRovPU1" + + "pVeKhkfYuRlsLwbBwQPKvzcfUU3ZJua4I3AKKPxlqdY8uP72A5iObDTL8kHhSRMtVVHoruVzgJPZ" + + "+9Mfsz41eM4pJSPDKZPYD9rH6cUKJI8xEnmCAAAAAAAA", +) + +var fixtureMessageOutlookDetached = mustBase64Decode("" + + "Q29udGVudC1UeXBlOiBtdWx0aXBhcnQvbWl4ZWQ7DQoJYm91bmRhcnk9Ii0t" + + "LS09X05leHRQYXJ0XzAwMV8wMDFEXzAxRDM2OEY2LjdDRTk2MzcwIg0KDQoN" + + "Ci0tLS0tLT1fTmV4dFBhcnRfMDAxXzAwMURfMDFEMzY4RjYuN0NFOTYzNzAN" + + "CkNvbnRlbnQtVHlwZTogbXVsdGlwYXJ0L2FsdGVybmF0aXZlOw0KCWJvdW5k" + + "YXJ5PSItLS0tPV9OZXh0UGFydF8wMDJfMDAxRV8wMUQzNjhGNi43Q0U5NjM3" + + "MCINCg0KDQotLS0tLS09X05leHRQYXJ0XzAwMl8wMDFFXzAxRDM2OEY2LjdD" + + "RTk2MzcwDQpDb250ZW50LVR5cGU6IHRleHQvcGxhaW47DQoJY2hhcnNldD0i" + + "dXMtYXNjaWkiDQpDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA3Yml0DQoN" + + "CkhlcmUncyBhIG1lc3NhZ2Ugd2l0aCBhbiBTL01JTUUgc2lnbmF0dXJlIGFu" + + "ZCBhbiBhdHRhY2htZW50Lg0KDQogDQoNCkp1c3QgY3VyaW91cyB3aGF0IHlv" + + "dSdyZSBsb29raW5nIGF0IHNpZ25hdHVyZXMgZm9yPw0KDQogDQoNCkhvcGUg" + + "dGhpcyBoZWxwcyENCg0KT3Jlbg0KDQoNCi0tLS0tLT1fTmV4dFBhcnRfMDAy" + + "XzAwMUVfMDFEMzY4RjYuN0NFOTYzNzANCkNvbnRlbnQtVHlwZTogdGV4dC9o" + + "dG1sOw0KCWNoYXJzZXQ9InVzLWFzY2lpIg0KQ29udGVudC1UcmFuc2Zlci1F" + + "bmNvZGluZzogcXVvdGVkLXByaW50YWJsZQ0KDQo8aHRtbCB4bWxuczp2PTNE" + + "InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206dm1sIiA9DQp4bWxuczpvPTNE" + + "InVybjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSIgPQ0K" + + "eG1sbnM6dz0zRCJ1cm46c2NoZW1hcy1taWNyb3NvZnQtY29tOm9mZmljZTp3" + + "b3JkIiA9DQp4bWxuczptPTNEImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5j" + + "b20vb2ZmaWNlLzIwMDQvMTIvb21tbCIgPQ0KeG1sbnM9M0QiaHR0cDovL3d3" + + "dy53My5vcmcvVFIvUkVDLWh0bWw0MCI+PGhlYWQ+PE1FVEEgPQ0KSFRUUC1F" + + "UVVJVj0zRCJDb250ZW50LVR5cGUiIENPTlRFTlQ9M0QidGV4dC9odG1sOyA9" + + "DQpjaGFyc2V0PTNEdXMtYXNjaWkiPjxtZXRhIG5hbWU9M0RHZW5lcmF0b3Ig" + + "Y29udGVudD0zRCJNaWNyb3NvZnQgV29yZCAxNSA9DQooZmlsdGVyZWQgbWVk" + + "aXVtKSI+PHN0eWxlPjwhLS0NCi8qIEZvbnQgRGVmaW5pdGlvbnMgKi8NCkBm" + + "b250LWZhY2UNCgl7Zm9udC1mYW1pbHk6IkNhbWJyaWEgTWF0aCI7DQoJcGFu" + + "b3NlLTE6MiA0IDUgMyA1IDQgNiAzIDIgNDt9DQpAZm9udC1mYWNlDQoJe2Zv" + + "bnQtZmFtaWx5OkNhbGlicmk7DQoJcGFub3NlLTE6MiAxNSA1IDIgMiAyIDQg" + + "MyAyIDQ7fQ0KLyogU3R5bGUgRGVmaW5pdGlvbnMgKi8NCnAuTXNvTm9ybWFs" + + "LCBsaS5Nc29Ob3JtYWwsIGRpdi5Nc29Ob3JtYWwNCgl7bWFyZ2luOjBpbjsN" + + "CgltYXJnaW4tYm90dG9tOi4wMDAxcHQ7DQoJZm9udC1zaXplOjExLjBwdDsN" + + "Cglmb250LWZhbWlseToiQ2FsaWJyaSIsc2Fucy1zZXJpZjt9DQphOmxpbmss" + + "IHNwYW4uTXNvSHlwZXJsaW5rDQoJe21zby1zdHlsZS1wcmlvcml0eTo5OTsN" + + "Cgljb2xvcjojMDU2M0MxOw0KCXRleHQtZGVjb3JhdGlvbjp1bmRlcmxpbmU7" + + "fQ0KYTp2aXNpdGVkLCBzcGFuLk1zb0h5cGVybGlua0ZvbGxvd2VkDQoJe21z" + + "by1zdHlsZS1wcmlvcml0eTo5OTsNCgljb2xvcjojOTU0RjcyOw0KCXRleHQt" + + "ZGVjb3JhdGlvbjp1bmRlcmxpbmU7fQ0Kc3Bhbi5FbWFpbFN0eWxlMTcNCgl7" + + "bXNvLXN0eWxlLXR5cGU6cGVyc29uYWwtY29tcG9zZTsNCglmb250LWZhbWls" + + "eToiQ2FsaWJyaSIsc2Fucy1zZXJpZjsNCgljb2xvcjp3aW5kb3d0ZXh0O30N" + + "Ci5Nc29DaHBEZWZhdWx0DQoJe21zby1zdHlsZS10eXBlOmV4cG9ydC1vbmx5" + + "Ow0KCWZvbnQtZmFtaWx5OiJDYWxpYnJpIixzYW5zLXNlcmlmO30NCkBwYWdl" + + "IFdvcmRTZWN0aW9uMQ0KCXtzaXplOjguNWluIDExLjBpbjsNCgltYXJnaW46" + + "MS4waW4gMS4waW4gMS4waW4gMS4waW47fQ0KZGl2LldvcmRTZWN0aW9uMQ0K" + + "CXtwYWdlOldvcmRTZWN0aW9uMTt9DQotLT48L3N0eWxlPjwhLS1baWYgZ3Rl" + + "IG1zbyA5XT48eG1sPg0KPG86c2hhcGVkZWZhdWx0cyB2OmV4dD0zRCJlZGl0" + + "IiBzcGlkbWF4PTNEIjEwMjYiIC8+DQo8L3htbD48IVtlbmRpZl0tLT48IS0t" + + "W2lmIGd0ZSBtc28gOV0+PHhtbD4NCjxvOnNoYXBlbGF5b3V0IHY6ZXh0PTNE" + + "ImVkaXQiPg0KPG86aWRtYXAgdjpleHQ9M0QiZWRpdCIgZGF0YT0zRCIxIiAv" + + "Pg0KPC9vOnNoYXBlbGF5b3V0PjwveG1sPjwhW2VuZGlmXS0tPjwvaGVhZD48" + + "Ym9keSBsYW5nPTNERU4tVVMgPQ0KbGluaz0zRCIjMDU2M0MxIiB2bGluaz0z" + + "RCIjOTU0RjcyIj48ZGl2IGNsYXNzPTNEV29yZFNlY3Rpb24xPjxwID0NCmNs" + + "YXNzPTNETXNvTm9ybWFsPkhlcmUmIzgyMTc7cyBhIG1lc3NhZ2Ugd2l0aCBh" + + "biBTL01JTUUgc2lnbmF0dXJlIGFuZCBhbiA9DQphdHRhY2htZW50LjxvOnA+" + + "PC9vOnA+PC9wPjxwIGNsYXNzPTNETXNvTm9ybWFsPjxvOnA+Jm5ic3A7PC9v" + + "OnA+PC9wPjxwID0NCmNsYXNzPTNETXNvTm9ybWFsPkp1c3QgY3VyaW91cyB3" + + "aGF0IHlvdSYjODIxNztyZSBsb29raW5nIGF0IHNpZ25hdHVyZXMgPQ0KZm9y" + + "PzxvOnA+PC9vOnA+PC9wPjxwIGNsYXNzPTNETXNvTm9ybWFsPjxvOnA+Jm5i" + + "c3A7PC9vOnA+PC9wPjxwID0NCmNsYXNzPTNETXNvTm9ybWFsPkhvcGUgdGhp" + + "cyBoZWxwcyE8bzpwPjwvbzpwPjwvcD48cCA9DQpjbGFzcz0zRE1zb05vcm1h" + + "bD5PcmVuPG86cD48L286cD48L3A+PHAgY2xhc3M9M0RNc29Ob3JtYWw+ID0N" + + "CjxvOnA+PC9vOnA+PC9wPjwvZGl2PjwvYm9keT48L2h0bWw+DQotLS0tLS09" + + "X05leHRQYXJ0XzAwMl8wMDFFXzAxRDM2OEY2LjdDRTk2MzcwLS0NCg0KLS0t" + + "LS0tPV9OZXh0UGFydF8wMDFfMDAxRF8wMUQzNjhGNi43Q0U5NjM3MA0KQ29u" + + "dGVudC1UeXBlOiB0ZXh0L3BsYWluOw0KCW5hbWU9InRlc3QudHh0Ig0KQ29u" + + "dGVudC1UcmFuc2Zlci1FbmNvZGluZzogN2JpdA0KQ29udGVudC1EaXNwb3Np" + + "dGlvbjogYXR0YWNobWVudDsNCglmaWxlbmFtZT0idGVzdC50eHQiDQoNCnRl" + + "c3QNCi0tLS0tLT1fTmV4dFBhcnRfMDAxXzAwMURfMDFEMzY4RjYuN0NFOTYz" + + "NzAtLQ0K", +) + +var fixtureSmimesignAttachedWithTimestamp = mustBase64Decode("" + + "MIIgZQYJKoZIhvcNAQcCoIIgVjCCIFICAQExDTALBglghkgBZQMEAgEwFQYJ" + + "KoZIhvcNAQcBoAgEBmhlbGxvCqCCD1UwggVEMIIELKADAgECAhAMLfp+jIxN" + + "FhbxAJyYi7cNMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD" + + "VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x" + + "JDAiBgNVBAMTG0RpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDQTAeFw0xNzEx" + + "MjIwMDAwMDBaFw0yMDExMjIxMjAwMDBaMGUxCzAJBgNVBAYTAlVTMRMwEQYD" + + "VQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRUwEwYD" + + "VQQKEwxHaXRIdWIsIEluYy4xEjAQBgNVBAMTCUJlbiBUb2V3czCCASIwDQYJ" + + "KoZIhvcNAQEBBQADggEPADCCAQoCggEBAO9ZN/PY8mMHV/fGVQvEJqlNqQKY" + + "keUjZljjsOodUq1g6CjelFgqVjfib+B6yDfUESpi4gD0PK9jODyQ7vC221sS" + + "scihYl5BMsBn93bQwy2zFIfyW0lOFuhtpPT6DZHCrqSI+NWbQ4+Wf+braXRv" + + "re7nYB7LkbC9Y9n2wq8n3hMxggAI1GcgWi6OqV8FrJKLBgmkYvlBkKOROHSq" + + "UsHKx/FPZ9U3B4KvVSIwPR5fcR1M+zvWQ6vpY3iGWbZlklqAjCFX+s6gdwwO" + + "Xh5PcW+kRpM2oNTRtohR6xh+pQ631KzS4d3RKKMiJaBVpasVUH206+mtaSxa" + + "2Mw9Sm0UBZTnTG0CAwEAAaOCAe4wggHqMB8GA1UdIwQYMBaAFOcCI4AAT9jX" + + "vJQL2T90OUkyPIp5MB0GA1UdDgQWBBSRnqSdQlLKpx5J3ytAbMevm7xdbjAM" + + "BgNVHRMBAf8EAjAAMCAGA1UdEQQZMBeBFW1hc3RhaHlldGlAZ2l0aHViLmNv" + + "bTAOBgNVHQ8BAf8EBAMCBsAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUF" + + "BwMEMEMGA1UdIAQ8MDowOAYKYIZIAYb9bAQBAjAqMCgGCCsGAQUFBwIBFhxo" + + "dHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGIBgNVHR8EgYAwfjA9oDug" + + "OYY3aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3Vy" + + "ZWRJRENBLWcyLmNybDA9oDugOYY3aHR0cDovL2NybDQuZGlnaWNlcnQuY29t" + + "L0RpZ2lDZXJ0U0hBMkFzc3VyZWRJRENBLWcyLmNybDB5BggrBgEFBQcBAQRt" + + "MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggr" + + "BgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0" + + "U0hBMkFzc3VyZWRJRENBLmNydDANBgkqhkiG9w0BAQsFAAOCAQEAVouNNiSm" + + "K9tUIk6pN5MbwGVTqNcLYzPJAXk1ufZynmlP0mJsyLrdlwLRrhWQUkRiAAAp" + + "EWBycg8hAF4h29ZuaLzp4zPL4L/nSjN7wGRwCzQZhkrazfRf24wLpNDWQuYh" + + "rot/AsfN56/aUXUZDrLIkTQID+u9qlWVAH/+sb096oTjDULRDlahEzGnNYna" + + "gi9X+o1r3zn4drbksjYL1Jb4XBNx3pFXcb3/sFCDYLYgP0k1VdZ7SVWqam7x" + + "LD3XCR6hcCACVCIvH1fa/LjNgCCy2M1xa92DTh1SBBzeiMoAGSvcEvA0DPVu" + + "Eco2fr8+PANEg55NvpBoacqyIhnsvn9qJTCCBk4wggU2oAMCAQICEASueWBm" + + "ZpAaucV/pmxb3M0wDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCVVMxFTAT" + + "BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv" + + "bTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTEz" + + "MTEwNTEyMDAwMFoXDTI4MTEwNTEyMDAwMFowZTELMAkGA1UEBhMCVVMxFTAT" + + "BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNv" + + "bTEkMCIGA1UEAxMbRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENBMIIBIjAN" + + "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3PgRIz9qte/AJ3kbLQWHohBD" + + "Md8O1BUbT3ekIs4+jHDwvgeO3ScqvAEdtiwKyt1pWB9B7WoFH9pjeFkeIiwr" + + "+Lp+yTU7VvEffEJ+JbAjGcZFONc9RPkgfGCuHLBaGAS+jzv3qfCUmqYMY0m2" + + "QRdTQDK9T+ZQelAfJUXo8Ymvzf9e/1Dz8BcR/73FifW9YrnY+45FBIVtmc3F" + + "SE39JqsCNkXqNtdfauIagkEK3OnZ9ZEXjsYhrTg8E+Yef2ac1U3ZRtr2z1Kn" + + "fTskw7TBUTXGm+vU737kewPhRL16CzfgT8uCig1xGOSm4IksG/OyczzBsJKe" + + "GH29q33FfQihLMKfcwIDAQABo4IC+DCCAvQwEgYDVR0TAQH/BAgwBgEB/wIB" + + "ADAOBgNVHQ8BAf8EBAMCAYYwNAYIKwYBBQUHAQEEKDAmMCQGCCsGAQUFBzAB" + + "hhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wgYEGA1UdHwR6MHgwOqA4oDaG" + + "NGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv" + + "b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdp" + + "Q2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0lBBYwFAYIKwYBBQUHAwIG" + + "CCsGAQUFBwMEMIIBswYDVR0gBIIBqjCCAaYwggGiBgpghkgBhv1sAAIEMIIB" + + "kjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCC" + + "AWQGCCsGAQUFBwICMIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAgAG8AZgAgAHQA" + + "aABpAHMAIABDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIABjAG8AbgBzAHQAaQB0" + + "AHUAdABlAHMAIABhAGMAYwBlAHAAdABhAG4AYwBlACAAbwBmACAAdABoAGUA" + + "IABEAGkAZwBpAEMAZQByAHQAIABDAFAALwBDAFAAUwAgAGEAbgBkACAAdABo" + + "AGUAIABSAGUAbAB5AGkAbgBnACAAUABhAHIAdAB5ACAAQQBnAHIAZQBlAG0A" + + "ZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkAbQBpAHQAIABsAGkAYQBiAGkAbABp" + + "AHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4AYwBvAHIAcABvAHIAYQB0AGUA" + + "ZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYAZQByAGUAbgBjAGUALjAd" + + "BgNVHQ4EFgQU5wIjgABP2Ne8lAvZP3Q5STI8inkwHwYDVR0jBBgwFoAUReui" + + "r/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAE7UiSe5/R2H" + + "d34PKAWQ8QovyTs+vZOckMav+pFRhzJUa+jKwXFRXJmOtfrgYhmZpgeafBMn" + + "2+UCooQS2RX2CkRXxDSPbXMfOtagAT3e44LkRWuy6yX9gF4dOZC+W0L2zpFg" + + "4/mgVgxIEM4zaHvNk6vwastPWA+5e10bBIGepyLiV0kn7pKTCL5pCFMCOi5d" + + "yBn0UIBOAtmwXZG0k4f5lpaBVUCOZu2C2LsoX+1MYe0GWCgZUxFEvEcgKbIE" + + "bNiJVJk7ddtneCweknjGVT1YEhEybr1DDE0023vGQtvsvqubYUwGkuOO3yEq" + + "UFcEwGCiNdUknmY3CUnP1fhls+DibsIwggO3MIICn6ADAgECAhAM5+DlF9hG" + + "/o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD" + + "VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x" + + "JDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjEx" + + "MTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYD" + + "VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x" + + "JDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJ" + + "KoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwm" + + "lIiq9M71IDkoWGAM+IDaqRWVMmE8tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq" + + "9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf4rHZ/hhk0hJbX/lYGDW8R82hNvlr" + + "f9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1lhb+WZyLdm3X8aJLDSv/C3La" + + "nmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqiuhOCEe05F52ZOnKh5vqk" + + "2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplazvbKX7aqn8LfFqD+V" + + "FtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB" + + "/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgPMB8GA1Ud" + + "IwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUAA4IB" + + "AQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS" + + "TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLi" + + "l4Qf6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXB" + + "zLZ/wvFvhsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW216" + + "8RJGYIPXJwS+S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdh" + + "AbHSoyahEHGdreLD+cOZUbcrBwjOLuZQsqf6CkUvovDyMYIQzDCCEMgCAQEw" + + "eTBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD" + + "VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBTSEEy" + + "IEFzc3VyZWQgSUQgQ0ECEAwt+n6MjE0WFvEAnJiLtw0wCwYJYIZIAWUDBAIB" + + "oEswLwYJKoZIhvcNAQkEMSIEIFiRtbUi1d8IbQ/wsRD72dIbtPxxY6800IKG" + + "ouhG9r4DMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwCwYJKoZIhvcNAQEB" + + "BIIBAMQdkVhH1gK8cI2x6BzGW0Vu6IhQcvNqgUacjuzebIBDozpkOKVGYv1/" + + "qpmuDLyXEyweI307U5tArvRiWAaZnvjOpmQbdNipbCkjzzUu4tfHtLwTVjLV" + + "c6qW9THJWszaqU9rvWAgritkX3mN5AOR5X2Up/hsjMC8SMdwZRHHniRaGB7M" + + "IUwVrnnHgxfWUn2FaO85XN6vqsBz0ykI3NIDQGFAIMISX1lKYSBKHOwODvX+" + + "Z6aWr2JFVbdcc/hcLi/rbC5x0dlWVdDhREW0HTkZxW0Y37HEyz56d1qpj5II" + + "3wJLeoKdGckI7NTwGhY4lcY0lTCd1stsaoGDe9miVAlQfFahgg7bMIIO1wYL" + + "KoZIhvcNAQkQAg4xgg7GMIIOwgYJKoZIhvcNAQcCoIIOszCCDq8CAQMxDzAN" + + "BglghkgBZQMEAgEFADCBiAYLKoZIhvcNAQkQAQSgeQR3MHUCAQEGCWCGSAGG" + + "/WwHATAvMAsGCWCGSAFlAwQCAQQg3oBfclnOJ5THSQ6G1dMX7mo8WnWhN27z" + + "2w8+uXPmAHsCEHG548OQys1qPcVJD4482vQYDzIwMTgwOTEzMTQ1OTAyWgIR" + + "AN8rYymxrG3grObgUvx8VVygggu7MIIGgjCCBWqgAwIBAgIQCcD8RsgEQhO1" + + "WYuvKE9OQTANBgkqhkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UE" + + "ChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEw" + + "LwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgVGltZXN0YW1waW5n" + + "IENBMB4XDTE3MDEwNDAwMDAwMFoXDTI4MDExODAwMDAwMFowTDELMAkGA1UE" + + "BhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSowKAYDVQQDEyFEaWdpQ2VydCBT" + + "SEEyIFRpbWVzdGFtcCBSZXNwb25kZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IB" + + "DwAwggEKAoIBAQCelZhqNDtzG6h+/Me+KWmJx2gmRl89jWJzh4GjoZzwt1sk" + + "N1qS1PRZ13aJ5NzVJ/DVZrwK7rQrMWesWMVKkVkrRR4JAdZks1nujWZN+yNe" + + "zBANC4pn71KuoAiQwlL39ai1bpsse53ntT77eM0yUBi/QLVMjLtX9KBPEUVs" + + "QkK55a/W3/SnfApolg/SXylXzvsdMv/0EaETIvsSy+/XU9Lrl8uirBsdnVgh" + + "UYLCwt7qKz8sIoTQQ+w7Oz9HxPZW3EU3mLRrdLVZr3hXacgPCQJ43dhTwZnb" + + "YMSd6q6v4H6GSlypWGGoXnSKAShock6nhp21AlKHcGZI047vgSTM3NhlAgMB" + + "AAGjggM4MIIDNDAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNV" + + "HSUBAf8EDDAKBggrBgEFBQcDCDCCAb8GA1UdIASCAbYwggGyMIIBoQYJYIZI" + + "AYb9bAcBMIIBkjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu" + + "Y29tL0NQUzCCAWQGCCsGAQUFBwICMIIBVh6CAVIAQQBuAHkAIAB1AHMAZQAg" + + "AG8AZgAgAHQAaABpAHMAIABDAGUAcgB0AGkAZgBpAGMAYQB0AGUAIABjAG8A" + + "bgBzAHQAaQB0AHUAdABlAHMAIABhAGMAYwBlAHAAdABhAG4AYwBlACAAbwBm" + + "ACAAdABoAGUAIABEAGkAZwBpAEMAZQByAHQAIABDAFAALwBDAFAAUwAgAGEA" + + "bgBkACAAdABoAGUAIABSAGUAbAB5AGkAbgBnACAAUABhAHIAdAB5ACAAQQBn" + + "AHIAZQBlAG0AZQBuAHQAIAB3AGgAaQBjAGgAIABsAGkAbQBpAHQAIABsAGkA" + + "YQBiAGkAbABpAHQAeQAgAGEAbgBkACAAYQByAGUAIABpAG4AYwBvAHIAcABv" + + "AHIAYQB0AGUAZAAgAGgAZQByAGUAaQBuACAAYgB5ACAAcgBlAGYAZQByAGUA" + + "bgBjAGUALjALBglghkgBhv1sAxUwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGl" + + "sqIlssgXNW4wHQYDVR0OBBYEFOGnMkruASEofVTV8geSbrQHDz2HMHEGA1Ud" + + "HwRqMGgwMqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFz" + + "c3VyZWQtdHMuY3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20v" + + "c2hhMi1hc3N1cmVkLXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUF" + + "BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0" + + "dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVk" + + "SURUaW1lc3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggEBAB7wQYIy" + + "ru3xtDUT3FDC1ZeuIiKdDg6vM9NM/Xy/bwERp5RlIlzGIqHIiVJrmoxzXNle" + + "PzLeFmBMizb9MZkKvcGEt40d74kmEwVW80fNR1uthLI4r2ojtUXjHogyRoDS" + + "t6aZIv3BeM/1i9gMjAUJ7kTmgNVtcMyfUx4n3SpI3tqTZa1uZaOZp8JADnPM" + + "WE+PRSjlvJyI5ijOYF0tJV2Lcy6lDVtR2ppO/1AFiSja8ni70lh4jUSnrDoA" + + "kXhpiWQE012W3yq/+aVMLJP/5ordgqzx0rOihprBVYlWakc/+tYzlUM1iQV4" + + "Wjpp2iK4BEPTb2g1NnoUPkXpmGSGDxMMJkowggUxMIIEGaADAgECAhAKoSXW" + + "1jIbfkHkBdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUw" + + "EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j" + + "b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0x" + + "NjAxMDcxMjAwMDBaFw0zMTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUw" + + "EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j" + + "b20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3Rh" + + "bXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC90DLu" + + "S82Pf92puoKZxTlUKFe2I0rEDgdFM1EQfdD5fU1ofue2oPSNs4jkl79jIZCY" + + "vxO8V9PD4X4I1moUADj3Lh477sym9jJZ/l9lP+Cb6+NGRwYaVX4LJ37AovWg" + + "4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT7l3ImgtU46gJcWvgzyIQD3XP" + + "cXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM/fDqR9mIUF79Zm5WYScp" + + "iYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F0IQZchfxFwbvPc3W" + + "Te8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHOMIIByjAdBgNV" + + "HQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAUReuir/SS" + + "y4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E" + + "BAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQG" + + "CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUH" + + "MAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy" + + "ZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0" + + "LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4" + + "oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJ" + + "RFJvb3RDQS5jcmwwUAYDVR0gBEkwRzA4BgpghkgBhv1sAAIEMCowKAYIKwYB" + + "BQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCwYJYIZIAYb9" + + "bAcBMA0GCSqGSIb3DQEBCwUAA4IBAQBxlRLpUYdWac3v3dp8qmN6s3jPBjdA" + + "hO9LhL/KzwMC/cWnww4gQiyvd/MrHwwhWiq3BTQdaq6Z+CeiZr8JqmDfdqQ6" + + "kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVDBGiy23UC4HLHmNY8ZOUfSBAY" + + "X4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3CzddWThZN+tpJn+1Nhiaj1" + + "a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UBJrZspe6HUSHkWGCb" + + "ugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0e/VWMyIvIjay" + + "S6JKldj1po5SMYICTTCCAkkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV" + + "BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx" + + "MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGlu" + + "ZyBDQQIQCcD8RsgEQhO1WYuvKE9OQTANBglghkgBZQMEAgEFAKCBmDAaBgkq" + + "hkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTE4MDkx" + + "MzE0NTkwMlowLwYJKoZIhvcNAQkEMSIEIFyJ+5SjrJKFITSeJofXvLgWWxPb" + + "6ggwDfbdg+klERNxMCsGCyqGSIb3DQEJEAIMMRwwGjAYMBYEFEABkUdcmIkd" + + "66EEr0cJG1621MvLMA0GCSqGSIb3DQEBAQUABIIBAJwA/28RMum8YR9Cvx1O" + + "N5pk7SlyC1OAA4f+RECrRzrV4TBLkqOeFU+LgCZ4sl9KdyrG+qvEmuy13iAP" + + "IAiJC5VY8+WYnmaWLvuO5lt147X5psNAx7xS8ehBywOW3otMqMuy1DaqSCQe" + + "oLkUAO/kkVB+X5k2HEUudno3w7pHiNkYWxJ9idgvTPo1E9120fI/pptuvtiK" + + "yV7MXWgWWTdZFdyQ9Ig6Ntwt1YvWLNLIw52AmiZp7xPqxj08+8MIruHaUN0u" + + "9nEUK+2UxorVSK1IrZkUEObFHVp7lmeINW6tN37esXU8BQzVF+zHd9hbBPIT" + + "PWw6BOUQ5LQYHqlrGwfbnlk=", +) + +func mustBase64Decode(b64 string) []byte { + decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64)) + buf := new(bytes.Buffer) + + if _, err := io.Copy(buf, decoder); err != nil { + panic(err) + } + + return buf.Bytes() +} diff --git a/main.go b/main.go index 8d57535..cc24cce 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( "io" "os" - "github.com/github/certstore" + "github.com/github/smimesign/certstore" "github.com/pborman/getopt/v2" "github.com/pkg/errors" ) diff --git a/main_test.go b/main_test.go index 0a46190..7a1939b 100644 --- a/main_test.go +++ b/main_test.go @@ -8,8 +8,8 @@ import ( "os" "testing" - "github.com/github/certstore" - "github.com/github/fakeca" + "github.com/github/smimesign/certstore" + "github.com/github/smimesign/fakeca" "github.com/pborman/getopt/v2" )