Consolidate `smimesign`-specific dependencies into a monorepo.

github.com/github/fakeca → github.com/github/smimesign/fakeca
github.com/github/certstore → github.com/github/smimesign/certstore
github.com/github/ietf-cms → github.com/github/smimesign/ietf-cms
This commit is contained in:
Lucas Garron 2021-09-29 11:21:04 -07:00
Родитель ed54d09fc4
Коммит a63c2e85a4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B6FDB8D2256D0D19
45 изменённых файлов: 8196 добавлений и 24 удалений

21
certstore/LICENSE.md Normal file
Просмотреть файл

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

85
certstore/README.md Normal file
Просмотреть файл

@ -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/workflows/Test%20macOS%20(recent%20Go%20versions)/badge.svg>)](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/workflows/Test%20Windows%20(recent%20Go%20versions)/badge.svg>)](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
}
```

49
certstore/certstore.go Normal file
Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,485 @@
package certstore
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework CoreFoundation -framework Security
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
*/
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)
}

Просмотреть файл

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

296
certstore/certstore_test.go Normal file
Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,682 @@
package certstore
/*
#cgo windows LDFLAGS: -lcrypt32 -lncrypt
#include <windows.h>
#include <wincrypt.h>
#include <ncrypt.h>
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)
}

Просмотреть файл

@ -0,0 +1,207 @@
package certstore
/*
#include <windows.h>
#include <bcrypt.h>
#include <ncrypt.h>
//
// 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()
)

130
certstore/main_test.go Normal file
Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

21
fakeca/LICENSE.md Normal file
Просмотреть файл

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

44
fakeca/README.md Normal file
Просмотреть файл

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

240
fakeca/configuration.go Normal file
Просмотреть файл

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

195
fakeca/fakeca_test.go Normal file
Просмотреть файл

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

144
fakeca/identity.go Normal file
Просмотреть файл

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

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

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

24
ietf-cms/.gitignore поставляемый Normal file
Просмотреть файл

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

21
ietf-cms/LICENSE.md Normal file
Просмотреть файл

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

61
ietf-cms/README.md Normal file
Просмотреть файл

@ -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/workflows/Test%20(recent%20Go%20versions)/badge.svg>)](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/workflows/Test%20(Go%201.10)/badge.svg>)](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.

166
ietf-cms/main_test.go Normal file
Просмотреть файл

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

142
ietf-cms/oid/oid.go Normal file
Просмотреть файл

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

22
ietf-cms/protocol/LICENSE Normal file
Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

248
ietf-cms/protocol/ber.go Normal file
Просмотреть файл

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

Просмотреть файл

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

328
ietf-cms/protocol/cms.asn1 Normal file
Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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("" +
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" +
"BwGggCSABAZoZWxsbwoAAAAAAACgggNYMIIDVDCCAjygAwIBAgIIFnTa5+xvrkgw" +
"DQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAxMJQmVuIFRvZXdzMCAXDTE3MTExNjE3" +
"NTAzMloYDzIwNjMwNDA1MTcwMDAwWjAUMRIwEAYDVQQDEwlCZW4gVG9ld3MwggEi" +
"MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdcejAkkPekPH6VuFbDcbkf5XD" +
"jCAYW3JWlc+tyVpBXoOtDdETKFUQqXxxm2ukLZlRuz/+AugtaijRmgr2boPYzL6v" +
"rHuPQVlNl327QkIqaia67HEWmy/9puil+d05gzg3Y5H2VrkIqzlZieTzIbFAfnyR" +
"1KAwvC5yF0Oa60AH6rWg67JAjxzE37j/bBAsUhvNtWPbZ+mSHrAgYE6tQYts9V5x" +
"82rlOP8d6V49CRSQ59HgMsJK7P6mrhkp1TAbAU4fIIZoyKBi3JZsCMTExz+xAM+g" +
"2dT+W5JPom9izbdzF4Zj8PH95nf2Dlvf9dtlvAXVkePVozeyAmxNMo5kJbAJAgMB" +
"AAGjgacwgaQwbgYDVR0RBGcwZYEUbWFzdGFoeWV0aUBnbWFpbC5jb22BFW1hc3Rh" +
"aHlldGlAZ2l0aHViLmNvbYERYnRvZXdzQGdpdGh1Yi5jb22BI21hc3RhaHlldGlA" +
"dXNlcnMubm9yZXBseS5naXRodWIuY29tMBEGCisGAQQB2kcCAgEEAwEB/zAPBgNV" +
"HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEA" +
"iurKpC6lhIEEsqkpN65zqUhnWijgf6jai1TlM59PYhYNduGoscoMZsvgI22ONLVu" +
"DguY0zQdGOI31TugdkCvd0728Eu1rwZVzJx4z6vM0CjCb1FluDMqGXJt7PSXz92T" +
"CeybmkkgQqiR9eoJUJPi9C+Lrwi4aOfFiwutvsGw9HB+n5EOVCj+tE0jbnraY323" +
"nj2Ibfo/ZGPzXpwSJMimma0Qa9IF5CKBGkbZWPRCi/l5vfDEcqy7od9KmIW7WKAu" +
"aNjW5c0Zgu4ZufRYpiN8IEkvnAXH5WAFWSKlQslu5zVgqSoB7T8pu211OTWBdDgu" +
"LGuzzactHfA/HTr9d5LNrzGCAeEwggHdAgEBMCAwFDESMBAGA1UEAxMJQmVuIFRv" +
"ZXdzAggWdNrn7G+uSDANBglghkgBZQMEAgEFAKCBkzAYBgkqhkiG9w0BCQMxCwYJ" +
"KoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNzExMjIxNzU3NTZaMCgGCSqGSIb3" +
"DQEJDzEbMBkwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMC8GCSqGSIb3DQEJBDEi" +
"BCBYkbW1ItXfCG0P8LEQ+9nSG7T8cWOvNNCChqLoRva+AzANBgkqhkiG9w0BAQEF" +
"AASCAQBbKSOFVXnWuRADFW1M9mZApLKjU2jtzN22aaVTlvSDoHE7yzj53EVorfm4" +
"br1JWJMeOJcfAiV5oiJiuIqiXOec5bTgR9EzkCZ8yA+R89y6M538XXp8sLMxNkO/" +
"EhoLXdQV8UhoF2mXktbbe/blTODvupTBonUXQhVAeJpWi0q8Qaz5StpzuXu6UFWK" +
"nTCTsl8gg1x/Wf0zLOUVWtLLPLeQB5usv1fQker0e+kCthv/q+QyLxw9J3e5rJ9a" +
"Dekeh5WkaS8yHCCvnOyOLI9/o2rHwUII36XjvK6VF+UHG+OcoL29BnUb01+vwxPk" +
"SDXMwnexRO3w39tu4ChUFbsX8l5CAAAAAAAA",
)
var fixtureSignatureGPGSM = mustBase64Decode("" +
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" +
"BwEAAKCCA1gwggNUMIICPKADAgECAggWdNrn7G+uSDANBgkqhkiG9w0BAQsFADAU" +
"MRIwEAYDVQQDEwlCZW4gVG9ld3MwIBcNMTcxMTE2MTc1MDMyWhgPMjA2MzA0MDUx" +
"NzAwMDBaMBQxEjAQBgNVBAMTCUJlbiBUb2V3czCCASIwDQYJKoZIhvcNAQEBBQAD" +
"ggEPADCCAQoCggEBAJ1x6MCSQ96Q8fpW4VsNxuR/lcOMIBhbclaVz63JWkFeg60N" +
"0RMoVRCpfHGba6QtmVG7P/4C6C1qKNGaCvZug9jMvq+se49BWU2XfbtCQipqJrrs" +
"cRabL/2m6KX53TmDODdjkfZWuQirOVmJ5PMhsUB+fJHUoDC8LnIXQ5rrQAfqtaDr" +
"skCPHMTfuP9sECxSG821Y9tn6ZIesCBgTq1Bi2z1XnHzauU4/x3pXj0JFJDn0eAy" +
"wkrs/qauGSnVMBsBTh8ghmjIoGLclmwIxMTHP7EAz6DZ1P5bkk+ib2LNt3MXhmPw" +
"8f3md/YOW9/122W8BdWR49WjN7ICbE0yjmQlsAkCAwEAAaOBpzCBpDBuBgNVHREE" +
"ZzBlgRRtYXN0YWh5ZXRpQGdtYWlsLmNvbYEVbWFzdGFoeWV0aUBnaXRodWIuY29t" +
"gRFidG9ld3NAZ2l0aHViLmNvbYEjbWFzdGFoeWV0aUB1c2Vycy5ub3JlcGx5Lmdp" +
"dGh1Yi5jb20wEQYKKwYBBAHaRwICAQQDAQH/MA8GA1UdEwEB/wQFMAMBAf8wDgYD" +
"VR0PAQH/BAQDAgTwMA0GCSqGSIb3DQEBCwUAA4IBAQCK6sqkLqWEgQSyqSk3rnOp" +
"SGdaKOB/qNqLVOUzn09iFg124aixygxmy+AjbY40tW4OC5jTNB0Y4jfVO6B2QK93" +
"TvbwS7WvBlXMnHjPq8zQKMJvUWW4MyoZcm3s9JfP3ZMJ7JuaSSBCqJH16glQk+L0" +
"L4uvCLho58WLC62+wbD0cH6fkQ5UKP60TSNuetpjfbeePYht+j9kY/NenBIkyKaZ" +
"rRBr0gXkIoEaRtlY9EKL+Xm98MRyrLuh30qYhbtYoC5o2NblzRmC7hm59FimI3wg" +
"SS+cBcflYAVZIqVCyW7nNWCpKgHtPym7bXU5NYF0OC4sa7PNpy0d8D8dOv13ks2v" +
"MYIB4TCCAd0CAQEwIDAUMRIwEAYDVQQDEwlCZW4gVG9ld3MCCBZ02ufsb65IMA0G" +
"CWCGSAFlAwQCAQUAoIGTMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZI" +
"hvcNAQkFMQ8XDTE3MTExNzAwNDcyNFowKAYJKoZIhvcNAQkPMRswGTALBglghkgB" +
"ZQMEAQIwCgYIKoZIhvcNAwcwLwYJKoZIhvcNAQkEMSIEIE3KD9X0JKMbA6uAfLrn" +
"frMr8tCJ7tHO4VSzr+1FjeDcMA0GCSqGSIb3DQEBAQUABIIBAGH7rQRx3IPuJbPr" +
"FjErvUWvgh8fS9s0mKI3/NPgUhx2gu1TpPdTp68La8KUDbN4jRVZ8o59WnzN9/So" +
"5mpc0AcpVlolIb4B/qQMkBALx6O5nHE/lr7orXQWUPM3iSUHAscNZbNr98k8YBdl" +
"hfarrderC+7n3dLOhNwpz3+STVr6l5czuXOqggcbwOMDbg4o/fiI2hm6eG79rDsd" +
"MJ3NoMYnEURUtsK0OffSMpnbsifEyRviKQG0LC4neqMJGylm6uYOXfzNsCbP12MM" +
"VovtxgUEskE2aU9UfPPqtm6H69QgcusUxxoECxWifydVObY/di5m5FGOCzP4b+QG" +
"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("" +
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCCD0Yw" +
"ggO3MIICn6ADAgECAhAM5+DlF9hG/o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYT" +
"AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi" +
"BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTEx" +
"MTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT" +
"EHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCC" +
"ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71" +
"IDkoWGAM+IDaqRWVMmE8tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJS" +
"Yd+fINcf4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1lhb+" +
"WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqiuhOCEe05F52ZOnKh" +
"5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplazvbKX7aqn8LfFqD+VFtD/oZbrCF8Y" +
"d08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXr" +
"oq/0ksuCMS1Ri6enIZ3zbcgPMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqG" +
"SIb3DQEBBQUAA4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS" +
"TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf6WXvh+DfwWdJ" +
"s13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFvhsb6ZGjrgS2U60K3+owe3WLx" +
"vlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76" +
"jRslbWyPpbdhAbHSoyahEHGdreLD+cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFNTCCBB2gAwIBAgIQ" +
"BaTO8JYvDXElKlIYlJMocDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM" +
"RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2Vy" +
"dCBTSEEyIEFzc3VyZWQgSUQgQ0EwHhcNMTcwMzAxMDAwMDAwWhcNMjAwMjI4MTIwMDAwWjBbMQsw" +
"CQYDVQQGEwJVUzELMAkGA1UECBMCTlkxETAPBgNVBAcTCE5ldyBZb3JrMRUwEwYDVQQKEwxPcmVu" +
"IE5vdm90bnkxFTATBgNVBAMTDE9yZW4gTm92b3RueTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC" +
"AQoCggEBAMKWbckomlzHxi8o34oOv8FxVIPI2wyhVmW0VdcgFyLlr10D50h3f4jlFOqiWI60c35A" +
"3be77ykVbX7dlijMUa1xgBAxSmMFiRYWy1OqsgciGO/VXEwTmPjcxgwYGEBCcVXBAzbmYQtlvr1U" +
"FBJc3CwSQknznLPWLPmOSntPfexwQYcHOinQ3HvdenKFnfGH+BtBsaBSYGokpjH1RQCPxKruuVOa" +
"YdHeG8g+vp96w1rsCK9r0RAJp7w1gCoMePxlFQr/1r7kJhcclcNU6hodEouF9OJOeahsD9vbM9Bt" +
"DafC1RMAo5+cYbrECHgx5M3JLh/BACh5JRaLQHg3QkWrZ9kCAwEAAaOCAekwggHlMB8GA1UdIwQY" +
"MBaAFOcCI4AAT9jXvJQL2T90OUkyPIp5MB0GA1UdDgQWBBQOAAryJTOprIAZzEnY28ajByUJ6TAM" +
"BgNVHRMBAf8EAjAAMBsGA1UdEQQUMBKBEG9yZW5Abm92b3RueS5vcmcwDgYDVR0PAQH/BAQDAgWg" +
"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()
}

49
ietf-cms/sign.go Normal file
Просмотреть файл

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

221
ietf-cms/sign_test.go Normal file
Просмотреть файл

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

83
ietf-cms/signed_data.go Normal file
Просмотреть файл

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

129
ietf-cms/timestamp.go Normal file
Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

197
ietf-cms/timestamp_test.go Normal file
Просмотреть файл

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

174
ietf-cms/verify.go Normal file
Просмотреть файл

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

812
ietf-cms/verify_test.go Normal file
Просмотреть файл

@ -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("" +
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" +
"BwGggCSABAZoZWxsbwoAAAAAAACgggNYMIIDVDCCAjygAwIBAgIIFnTa5+xvrkgw" +
"DQYJKoZIhvcNAQELBQAwFDESMBAGA1UEAxMJQmVuIFRvZXdzMCAXDTE3MTExNjE3" +
"NTAzMloYDzIwNjMwNDA1MTcwMDAwWjAUMRIwEAYDVQQDEwlCZW4gVG9ld3MwggEi" +
"MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCdcejAkkPekPH6VuFbDcbkf5XD" +
"jCAYW3JWlc+tyVpBXoOtDdETKFUQqXxxm2ukLZlRuz/+AugtaijRmgr2boPYzL6v" +
"rHuPQVlNl327QkIqaia67HEWmy/9puil+d05gzg3Y5H2VrkIqzlZieTzIbFAfnyR" +
"1KAwvC5yF0Oa60AH6rWg67JAjxzE37j/bBAsUhvNtWPbZ+mSHrAgYE6tQYts9V5x" +
"82rlOP8d6V49CRSQ59HgMsJK7P6mrhkp1TAbAU4fIIZoyKBi3JZsCMTExz+xAM+g" +
"2dT+W5JPom9izbdzF4Zj8PH95nf2Dlvf9dtlvAXVkePVozeyAmxNMo5kJbAJAgMB" +
"AAGjgacwgaQwbgYDVR0RBGcwZYEUbWFzdGFoeWV0aUBnbWFpbC5jb22BFW1hc3Rh" +
"aHlldGlAZ2l0aHViLmNvbYERYnRvZXdzQGdpdGh1Yi5jb22BI21hc3RhaHlldGlA" +
"dXNlcnMubm9yZXBseS5naXRodWIuY29tMBEGCisGAQQB2kcCAgEEAwEB/zAPBgNV" +
"HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEA" +
"iurKpC6lhIEEsqkpN65zqUhnWijgf6jai1TlM59PYhYNduGoscoMZsvgI22ONLVu" +
"DguY0zQdGOI31TugdkCvd0728Eu1rwZVzJx4z6vM0CjCb1FluDMqGXJt7PSXz92T" +
"CeybmkkgQqiR9eoJUJPi9C+Lrwi4aOfFiwutvsGw9HB+n5EOVCj+tE0jbnraY323" +
"nj2Ibfo/ZGPzXpwSJMimma0Qa9IF5CKBGkbZWPRCi/l5vfDEcqy7od9KmIW7WKAu" +
"aNjW5c0Zgu4ZufRYpiN8IEkvnAXH5WAFWSKlQslu5zVgqSoB7T8pu211OTWBdDgu" +
"LGuzzactHfA/HTr9d5LNrzGCAeEwggHdAgEBMCAwFDESMBAGA1UEAxMJQmVuIFRv" +
"ZXdzAggWdNrn7G+uSDANBglghkgBZQMEAgEFAKCBkzAYBgkqhkiG9w0BCQMxCwYJ" +
"KoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xNzExMjIxNzU3NTZaMCgGCSqGSIb3" +
"DQEJDzEbMBkwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMHMC8GCSqGSIb3DQEJBDEi" +
"BCBYkbW1ItXfCG0P8LEQ+9nSG7T8cWOvNNCChqLoRva+AzANBgkqhkiG9w0BAQEF" +
"AASCAQBbKSOFVXnWuRADFW1M9mZApLKjU2jtzN22aaVTlvSDoHE7yzj53EVorfm4" +
"br1JWJMeOJcfAiV5oiJiuIqiXOec5bTgR9EzkCZ8yA+R89y6M538XXp8sLMxNkO/" +
"EhoLXdQV8UhoF2mXktbbe/blTODvupTBonUXQhVAeJpWi0q8Qaz5StpzuXu6UFWK" +
"nTCTsl8gg1x/Wf0zLOUVWtLLPLeQB5usv1fQker0e+kCthv/q+QyLxw9J3e5rJ9a" +
"Dekeh5WkaS8yHCCvnOyOLI9/o2rHwUII36XjvK6VF+UHG+OcoL29BnUb01+vwxPk" +
"SDXMwnexRO3w39tu4ChUFbsX8l5CAAAAAAAA",
)
var fixtureSignatureGPGSM = mustBase64Decode("" +
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" +
"BwEAAKCCA1gwggNUMIICPKADAgECAggWdNrn7G+uSDANBgkqhkiG9w0BAQsFADAU" +
"MRIwEAYDVQQDEwlCZW4gVG9ld3MwIBcNMTcxMTE2MTc1MDMyWhgPMjA2MzA0MDUx" +
"NzAwMDBaMBQxEjAQBgNVBAMTCUJlbiBUb2V3czCCASIwDQYJKoZIhvcNAQEBBQAD" +
"ggEPADCCAQoCggEBAJ1x6MCSQ96Q8fpW4VsNxuR/lcOMIBhbclaVz63JWkFeg60N" +
"0RMoVRCpfHGba6QtmVG7P/4C6C1qKNGaCvZug9jMvq+se49BWU2XfbtCQipqJrrs" +
"cRabL/2m6KX53TmDODdjkfZWuQirOVmJ5PMhsUB+fJHUoDC8LnIXQ5rrQAfqtaDr" +
"skCPHMTfuP9sECxSG821Y9tn6ZIesCBgTq1Bi2z1XnHzauU4/x3pXj0JFJDn0eAy" +
"wkrs/qauGSnVMBsBTh8ghmjIoGLclmwIxMTHP7EAz6DZ1P5bkk+ib2LNt3MXhmPw" +
"8f3md/YOW9/122W8BdWR49WjN7ICbE0yjmQlsAkCAwEAAaOBpzCBpDBuBgNVHREE" +
"ZzBlgRRtYXN0YWh5ZXRpQGdtYWlsLmNvbYEVbWFzdGFoeWV0aUBnaXRodWIuY29t" +
"gRFidG9ld3NAZ2l0aHViLmNvbYEjbWFzdGFoeWV0aUB1c2Vycy5ub3JlcGx5Lmdp" +
"dGh1Yi5jb20wEQYKKwYBBAHaRwICAQQDAQH/MA8GA1UdEwEB/wQFMAMBAf8wDgYD" +
"VR0PAQH/BAQDAgTwMA0GCSqGSIb3DQEBCwUAA4IBAQCK6sqkLqWEgQSyqSk3rnOp" +
"SGdaKOB/qNqLVOUzn09iFg124aixygxmy+AjbY40tW4OC5jTNB0Y4jfVO6B2QK93" +
"TvbwS7WvBlXMnHjPq8zQKMJvUWW4MyoZcm3s9JfP3ZMJ7JuaSSBCqJH16glQk+L0" +
"L4uvCLho58WLC62+wbD0cH6fkQ5UKP60TSNuetpjfbeePYht+j9kY/NenBIkyKaZ" +
"rRBr0gXkIoEaRtlY9EKL+Xm98MRyrLuh30qYhbtYoC5o2NblzRmC7hm59FimI3wg" +
"SS+cBcflYAVZIqVCyW7nNWCpKgHtPym7bXU5NYF0OC4sa7PNpy0d8D8dOv13ks2v" +
"MYIB4TCCAd0CAQEwIDAUMRIwEAYDVQQDEwlCZW4gVG9ld3MCCBZ02ufsb65IMA0G" +
"CWCGSAFlAwQCAQUAoIGTMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZI" +
"hvcNAQkFMQ8XDTE3MTExNzAwNDcyNFowKAYJKoZIhvcNAQkPMRswGTALBglghkgB" +
"ZQMEAQIwCgYIKoZIhvcNAwcwLwYJKoZIhvcNAQkEMSIEIE3KD9X0JKMbA6uAfLrn" +
"frMr8tCJ7tHO4VSzr+1FjeDcMA0GCSqGSIb3DQEBAQUABIIBAGH7rQRx3IPuJbPr" +
"FjErvUWvgh8fS9s0mKI3/NPgUhx2gu1TpPdTp68La8KUDbN4jRVZ8o59WnzN9/So" +
"5mpc0AcpVlolIb4B/qQMkBALx6O5nHE/lr7orXQWUPM3iSUHAscNZbNr98k8YBdl" +
"hfarrderC+7n3dLOhNwpz3+STVr6l5czuXOqggcbwOMDbg4o/fiI2hm6eG79rDsd" +
"MJ3NoMYnEURUtsK0OffSMpnbsifEyRviKQG0LC4neqMJGylm6uYOXfzNsCbP12MM" +
"VovtxgUEskE2aU9UfPPqtm6H69QgcusUxxoECxWifydVObY/di5m5FGOCzP4b+QG" +
"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("" +
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCCD0Yw" +
"ggO3MIICn6ADAgECAhAM5+DlF9hG/o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYT" +
"AlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi" +
"BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTEx" +
"MTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsT" +
"EHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCC" +
"ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71" +
"IDkoWGAM+IDaqRWVMmE8tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJS" +
"Yd+fINcf4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1lhb+" +
"WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqiuhOCEe05F52ZOnKh" +
"5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplazvbKX7aqn8LfFqD+VFtD/oZbrCF8Y" +
"d08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXr" +
"oq/0ksuCMS1Ri6enIZ3zbcgPMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqG" +
"SIb3DQEBBQUAA4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS" +
"TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf6WXvh+DfwWdJ" +
"s13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFvhsb6ZGjrgS2U60K3+owe3WLx" +
"vlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76" +
"jRslbWyPpbdhAbHSoyahEHGdreLD+cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFNTCCBB2gAwIBAgIQ" +
"BaTO8JYvDXElKlIYlJMocDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM" +
"RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2Vy" +
"dCBTSEEyIEFzc3VyZWQgSUQgQ0EwHhcNMTcwMzAxMDAwMDAwWhcNMjAwMjI4MTIwMDAwWjBbMQsw" +
"CQYDVQQGEwJVUzELMAkGA1UECBMCTlkxETAPBgNVBAcTCE5ldyBZb3JrMRUwEwYDVQQKEwxPcmVu" +
"IE5vdm90bnkxFTATBgNVBAMTDE9yZW4gTm92b3RueTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC" +
"AQoCggEBAMKWbckomlzHxi8o34oOv8FxVIPI2wyhVmW0VdcgFyLlr10D50h3f4jlFOqiWI60c35A" +
"3be77ykVbX7dlijMUa1xgBAxSmMFiRYWy1OqsgciGO/VXEwTmPjcxgwYGEBCcVXBAzbmYQtlvr1U" +
"FBJc3CwSQknznLPWLPmOSntPfexwQYcHOinQ3HvdenKFnfGH+BtBsaBSYGokpjH1RQCPxKruuVOa" +
"YdHeG8g+vp96w1rsCK9r0RAJp7w1gCoMePxlFQr/1r7kJhcclcNU6hodEouF9OJOeahsD9vbM9Bt" +
"DafC1RMAo5+cYbrECHgx5M3JLh/BACh5JRaLQHg3QkWrZ9kCAwEAAaOCAekwggHlMB8GA1UdIwQY" +
"MBaAFOcCI4AAT9jXvJQL2T90OUkyPIp5MB0GA1UdDgQWBBQOAAryJTOprIAZzEnY28ajByUJ6TAM" +
"BgNVHRMBAf8EAjAAMBsGA1UdEQQUMBKBEG9yZW5Abm92b3RueS5vcmcwDgYDVR0PAQH/BAQDAgWg" +
"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()
}

Просмотреть файл

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

Просмотреть файл

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