зеркало из https://github.com/github/smimesign.git
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:
Родитель
ed54d09fc4
Коммит
a63c2e85a4
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
|
||||
```
|
|
@ -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")
|
||||
}
|
|
@ -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()
|
||||
)
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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()
|
||||
}
|
||||
|
||||
```
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
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
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=
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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("" +
|
||||
"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" +
|
||||
"SX+du6QAAAAAAAA=",
|
||||
)
|
||||
|
||||
var fixtureSignatureNoCertsGPGSM = mustBase64Decode("" +
|
||||
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" +
|
||||
"BwEAADGCAeEwggHdAgEBMCAwFDESMBAGA1UEAxMJQmVuIFRvZXdzAggWdNrn7G+u" +
|
||||
"SDANBglghkgBZQMEAgEFAKCBkzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG" +
|
||||
"CSqGSIb3DQEJBTEPFw0xNzExMTcwMDQxNDhaMCgGCSqGSIb3DQEJDzEbMBkwCwYJ" +
|
||||
"YIZIAWUDBAECMAoGCCqGSIb3DQMHMC8GCSqGSIb3DQEJBDEiBCBNyg/V9CSjGwOr" +
|
||||
"gHy6536zK/LQie7RzuFUs6/tRY3g3DANBgkqhkiG9w0BAQEFAASCAQAvGAGPMaH3" +
|
||||
"oRiNDU0AGIVyjXUrZ8g2VRazGCTuuO0CPGWBDbBuuvCePuWTddcv5KHHyrYO0yUD" +
|
||||
"xergVhh1EXIsOItHbJ6QeMstmY8Ub7HGm4Srdtm3MMSEe24zRmKK5yvPfeaaXeb6" +
|
||||
"MASKXvViU/j9VDwUZ2CFPUzPq8DlS6j4w6dapfphFGN1wJV3ADLUzUkTXfXQ57HE" +
|
||||
"WUKdbxgcuyBH7eLhZpKAXP31iRKm2b7dV50SruRCqNYZOp8bUQ57bC2jels0dzQd" +
|
||||
"EQS76O/DH6eQ3/OgvpmR8BjlujA82tgjqP7fj0S7Cw2VlPqcey0iqRmAmiO2qzOI" +
|
||||
"KAYzMkxWr7iUAAAAAAAA",
|
||||
)
|
||||
|
||||
var fixtureSignatureOpenSSLAttached = mustBase64Decode("" +
|
||||
"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" +
|
||||
"ZdMtHnR5z07z9wxvwiw=",
|
||||
)
|
||||
|
||||
var fixtureSignatureOpenSSLDetached = mustBase64Decode("" +
|
||||
"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",
|
||||
)
|
||||
|
||||
var fixtureSignatureOutlookDetached = mustBase64Decode("" +
|
||||
"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" +
|
||||
"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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,812 @@
|
|||
package cms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/github/smimesign/ietf-cms/protocol"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
func ExampleSignedData() {
|
||||
data := []byte("hello, world!")
|
||||
|
||||
// Wrap the data in a CMS SignedData structure and sign it with our key.
|
||||
signedDataDER, err := Sign(data, exampleChain, examplePrivateKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Re-parse the encoded SignedData structure.
|
||||
signedData, err := ParseSignedData(signedDataDER)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Verify the SignedData's signature.
|
||||
if _, err = signedData.Verify(x509.VerifyOptions{Roots: root.ChainPool()}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyOptionsForSignedData(sd *SignedData) (opts x509.VerifyOptions) {
|
||||
// add self-signed cert as trusted root
|
||||
certs, err := sd.psd.X509Certificates()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if len(certs) == 1 {
|
||||
opts.Roots = x509.NewCertPool()
|
||||
opts.Roots.AddCert(certs[0])
|
||||
}
|
||||
|
||||
// trust signing time
|
||||
signingTime, err := sd.psd.SignerInfos[0].GetSigningTimeAttribute()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
opts.CurrentTime = signingTime
|
||||
|
||||
// Any key usage
|
||||
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageAny}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func TestVerify(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureOne)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := sd.Verify(verifyOptionsForSignedData(sd)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyGPGSMAttached(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureGPGSMAttached)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err = sd.Verify(verifyOptionsForSignedData(sd)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data, err := sd.GetData()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(data, []byte("hello\n")) {
|
||||
t.Fatal("bad msg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyGPGSMDetached(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureGPGSM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := sd.VerifyDetached([]byte("hello, world!\n"), verifyOptionsForSignedData(sd)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyGPGSMNoCerts(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureNoCertsGPGSM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := sd.VerifyDetached([]byte("hello, world!\n"), x509.VerifyOptions{}); err != protocol.ErrNoCertificate {
|
||||
t.Fatalf("expected %v, got %v", protocol.ErrNoCertificate, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyOpenSSLAttached(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureOpenSSLAttached)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := sd.Verify(verifyOptionsForSignedData(sd)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyOpenSSLDetached(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureOpenSSLDetached)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := sd.VerifyDetached([]byte("hello, world!"), verifyOptionsForSignedData(sd)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyOutlookDetached(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSignatureOutlookDetached)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := sd.VerifyDetached(fixtureMessageOutlookDetached, verifyOptionsForSignedData(sd)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySmimesignAttachedWithTimestamp(t *testing.T) {
|
||||
sd, err := ParseSignedData(fixtureSmimesignAttachedWithTimestamp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opts := verifyOptionsForSignedData(sd)
|
||||
|
||||
// specify key usage for signing cert to verify that this isn't also used
|
||||
// when checking timestamp.
|
||||
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection}
|
||||
|
||||
if _, err := sd.Verify(opts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyChain(t *testing.T) {
|
||||
signerChain := leaf.Chain()
|
||||
ber, _ := Sign([]byte("hi"), leaf.Chain(), leaf.PrivateKey)
|
||||
sd, _ := ParseSignedData(ber)
|
||||
|
||||
// good root
|
||||
chains, err := sd.Verify(rootOpts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(chains) != 1 || len(chains[0]) != 1 || len(chains[0][0]) != len(signerChain) {
|
||||
t.Fatal("bad chain")
|
||||
}
|
||||
|
||||
for i, c := range signerChain {
|
||||
if !chains[0][0][i].Equal(c) {
|
||||
t.Fatalf("bad cert: %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
// bad root
|
||||
if _, err = sd.Verify(otherRootOpts); err != nil {
|
||||
if _, isX509Err := err.(x509.UnknownAuthorityError); !isX509Err {
|
||||
t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// system root
|
||||
if _, err = sd.Verify(x509.VerifyOptions{}); err != nil {
|
||||
if _, isX509Err := err.(x509.UnknownAuthorityError); !isX509Err {
|
||||
t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// no root
|
||||
if _, err = sd.Verify(x509.VerifyOptions{Roots: x509.NewCertPool()}); err != nil {
|
||||
if _, isX509Err := err.(x509.UnknownAuthorityError); !isX509Err {
|
||||
t.Fatalf("expected x509.UnknownAuthorityError, got %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyDSAWithSHA1(t *testing.T) {
|
||||
// Created with the following openssl commands:
|
||||
// openssl dsaparam -out dsakey.pem -genkey 1024
|
||||
// openssl req -key dsakey.pem -new -x509 -days 365000 -out dsa.crt -sha1 -subj "/CN=foo.com"
|
||||
// This creates a cert which is valid for 1000 years
|
||||
const publicCert string = `
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICWjCCAhoCCQCNGgCUlB6gszAJBgcqhkjOOAQDMBIxEDAOBgNVBAMMB2Zvby5j
|
||||
b20wIBcNMTkwNTI5MDMzNzUyWhgPMzAxODA5MjkwMzM3NTJaMBIxEDAOBgNVBAMM
|
||||
B2Zvby5jb20wggG2MIIBKwYHKoZIzjgEATCCAR4CgYEAyoyaU2206Zuu9MDfQ1gM
|
||||
Uba4Iu3j9EBWSSYiFjHS93Y2RVGqkNGHqNtLJ1nXANINqjnTP8RxnsccRejhX7C5
|
||||
xVAlfsKSvJpRO1idp0SA8tVItpyHNjY175SYFYcg6elr1KQxfd41o/brruo915fs
|
||||
BXxl0S3261INjJJ64Ybn+CkCFQDa9pKFl6/S1OObPF3XeemwQVSW8QKBgByeV3hw
|
||||
YGzpdIu+/6iMYAvkNAYVBTfwuYd5Oa1Le2m9detLgcHg/0/q8kD5YafNPKYVAg1N
|
||||
aD+lLYEkbFuOJo00Pk1zrTQtKkrfbU9EVxd/6/XCrsFAVLl+39Q3vDEEX3tLjf+m
|
||||
r860lPYoC0+HRj+RGJiYmMqeydsV4N8gtRZbA4GEAAKBgGp8JeErPlZ7l56NG+mL
|
||||
XJxpVB7Vb0rqM/B0r5kMX/a0Nw7oa0Nehy2BJyvI3zREz2BYJd4RGsIq8cCts2yO
|
||||
zh8PgBUSNAnEEivfxRV+LivovAjVXqsr53WolzvkCOlxeX15a9SINSDNkphqsZrW
|
||||
zYTE+BOvUEbM0lM0273nAa4KMAkGByqGSM44BAMDLwAwLAIUURvdNGP0kzOJh79x
|
||||
ZFtQRP+6EQoCFAmd0+Tig4/yNQ0eSnQEFwEMQiD1
|
||||
-----END CERTIFICATE-----
|
||||
`
|
||||
|
||||
// Signed pkcs7 doc created using above cert and the following commands:
|
||||
// printf 'Hello, World!' > hello.txt
|
||||
// openssl smime -sign -in hello.txt -nodetach -nocerts -outform pem -md sha1 -signer dsa.crt -inkey dsakey.pem
|
||||
const pkcs7Doc string = `
|
||||
-----BEGIN PKCS7-----
|
||||
MIIBvgYJKoZIhvcNAQcCoIIBrzCCAasCAQExCzAJBgUrDgMCGgUAMBwGCSqGSIb3
|
||||
DQEHAaAPBA1IZWxsbywgV29ybGQhMYIBeTCCAXUCAQEwHzASMRAwDgYDVQQDDAdm
|
||||
b28uY29tAgkAjRoAlJQeoLMwCQYFKw4DAhoFAKCCAQcwGAYJKoZIhvcNAQkDMQsG
|
||||
CSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMTkwNTI5MDM1OTU4WjAjBgkqhkiG
|
||||
9w0BCQQxFgQUCgqfKmdylCVXq1NV12r0Qvj2XgEwgacGCSqGSIb3DQEJDzGBmTCB
|
||||
ljALBglghkgBZQMEASowCAYGKoUDAgIJMAoGCCqFAwcBAQICMAoGCCqFAwcBAQID
|
||||
MAgGBiqFAwICFTALBglghkgBZQMEARYwCwYJYIZIAWUDBAECMAoGCCqGSIb3DQMH
|
||||
MA4GCCqGSIb3DQMCAgIAgDANBggqhkiG9w0DAgIBQDAHBgUrDgMCBzANBggqhkiG
|
||||
9w0DAgIBKDAJBgcqhkjOOAQDBC4wLAIUXZ2aaGVyPDzpb1svc0ruE3qCUzsCFCNw
|
||||
F1Al5pA+giJh15T7Uu+p5O0J
|
||||
-----END PKCS7-----
|
||||
`
|
||||
|
||||
pkcs7CertPEM, _ := pem.Decode([]byte(publicCert))
|
||||
if pkcs7CertPEM == nil {
|
||||
t.Fatal("failed to parse certificate PEM")
|
||||
}
|
||||
pkcs7Cert, err := x509.ParseCertificate(pkcs7CertPEM.Bytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse certificate: " + err.Error())
|
||||
}
|
||||
|
||||
pkcs7Certs := []*x509.Certificate{pkcs7Cert}
|
||||
pkcs7VerifyOptions := x509.VerifyOptions{
|
||||
Roots: x509.NewCertPool(),
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
}
|
||||
pkcs7VerifyOptions.Roots.AddCert(pkcs7Cert)
|
||||
|
||||
derRequest, _ := pem.Decode([]byte(pkcs7Doc))
|
||||
if derRequest == nil {
|
||||
t.Fatalf("failed to parse id doc PEM: %s", pkcs7Doc)
|
||||
}
|
||||
sd, err := ParseSignedData([]byte(derRequest.Bytes))
|
||||
if err != nil {
|
||||
t.Fatalf("Error parsing pkcs7 document: %s, err: %v", pkcs7Doc, err)
|
||||
}
|
||||
|
||||
sd.SetCertificates(pkcs7Certs)
|
||||
_, err = sd.Verify(pkcs7VerifyOptions)
|
||||
if err != nil {
|
||||
if xerrors.Is(err, x509.ErrUnsupportedAlgorithm) {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Error verifying signing request: %v, err %v", *sd, err)
|
||||
}
|
||||
data, err := sd.GetData()
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting data from pkcs7 document %s, err %v", pkcs7Doc, err)
|
||||
}
|
||||
|
||||
expectedData := "Hello, World!"
|
||||
if string(data) != expectedData {
|
||||
t.Fatalf("Expected data: %s Actual Data: %s", expectedData, data)
|
||||
}
|
||||
}
|
||||
|
||||
var fixtureSignatureOne = mustBase64Decode("" +
|
||||
"MIIDVgYJKoZIhvcNAQcCoIIDRzCCA0MCAQExCTAHBgUrDgMCGjAcBgkqhkiG9w0B" +
|
||||
"BwGgDwQNV2UgdGhlIFBlb3BsZaCCAdkwggHVMIIBQKADAgECAgRpuDctMAsGCSqG" +
|
||||
"SIb3DQEBCzApMRAwDgYDVQQKEwdBY21lIENvMRUwEwYDVQQDEwxFZGRhcmQgU3Rh" +
|
||||
"cmswHhcNMTUwNTA2MDQyNDQ4WhcNMTYwNTA2MDQyNDQ4WjAlMRAwDgYDVQQKEwdB" +
|
||||
"Y21lIENvMREwDwYDVQQDEwhKb24gU25vdzCBnzANBgkqhkiG9w0BAQEFAAOBjQAw" +
|
||||
"gYkCgYEAqr+tTF4mZP5rMwlXp1y+crRtFpuLXF1zvBZiYMfIvAHwo1ta8E1IcyEP" +
|
||||
"J1jIiKMcwbzeo6kAmZzIJRCTezq9jwXUsKbQTvcfOH9HmjUmXBRWFXZYoQs/OaaF" +
|
||||
"a45deHmwEeMQkuSWEtYiVKKZXtJOtflKIT3MryJEDiiItMkdybUCAwEAAaMSMBAw" +
|
||||
"DgYDVR0PAQH/BAQDAgCgMAsGCSqGSIb3DQEBCwOBgQDK1EweZWRL+f7Z+J0kVzY8" +
|
||||
"zXptcBaV4Lf5wGZJLJVUgp33bpLNpT3yadS++XQJ+cvtW3wADQzBSTMduyOF8Zf+" +
|
||||
"L7TjjrQ2+F2HbNbKUhBQKudxTfv9dJHdKbD+ngCCdQJYkIy2YexsoNG0C8nQkggy" +
|
||||
"axZd/J69xDVx6pui3Sj8sDGCATYwggEyAgEBMDEwKTEQMA4GA1UEChMHQWNtZSBD" +
|
||||
"bzEVMBMGA1UEAxMMRWRkYXJkIFN0YXJrAgRpuDctMAcGBSsOAwIaoGEwGAYJKoZI" +
|
||||
"hvcNAQkDMQsGCSqGSIb3DQEHATAgBgkqhkiG9w0BCQUxExcRMTUwNTA2MDAyNDQ4" +
|
||||
"LTA0MDAwIwYJKoZIhvcNAQkEMRYEFG9D7gcTh9zfKiYNJ1lgB0yTh4sZMAsGCSqG" +
|
||||
"SIb3DQEBAQSBgFF3sGDU9PtXty/QMtpcFa35vvIOqmWQAIZt93XAskQOnBq4OloX" +
|
||||
"iL9Ct7t1m4pzjRm0o9nDkbaSLZe7HKASHdCqijroScGlI8M+alJ8drHSFv6ZIjnM" +
|
||||
"FIwIf0B2Lko6nh9/6mUXq7tbbIHa3Gd1JUVire/QFFtmgRXMbXYk8SIS",
|
||||
)
|
||||
|
||||
var fixtureSignatureGPGSMAttached = mustBase64Decode("" +
|
||||
"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" +
|
||||
"SDXMwnexRO3w39tu4ChUFbsX8l5CAAAAAAAA",
|
||||
)
|
||||
|
||||
var fixtureSignatureGPGSM = mustBase64Decode("" +
|
||||
"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" +
|
||||
"SX+du6QAAAAAAAA=",
|
||||
)
|
||||
|
||||
var fixtureSignatureNoCertsGPGSM = mustBase64Decode("" +
|
||||
"MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0B" +
|
||||
"BwEAADGCAeEwggHdAgEBMCAwFDESMBAGA1UEAxMJQmVuIFRvZXdzAggWdNrn7G+u" +
|
||||
"SDANBglghkgBZQMEAgEFAKCBkzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG" +
|
||||
"CSqGSIb3DQEJBTEPFw0xNzExMTcwMDQxNDhaMCgGCSqGSIb3DQEJDzEbMBkwCwYJ" +
|
||||
"YIZIAWUDBAECMAoGCCqGSIb3DQMHMC8GCSqGSIb3DQEJBDEiBCBNyg/V9CSjGwOr" +
|
||||
"gHy6536zK/LQie7RzuFUs6/tRY3g3DANBgkqhkiG9w0BAQEFAASCAQAvGAGPMaH3" +
|
||||
"oRiNDU0AGIVyjXUrZ8g2VRazGCTuuO0CPGWBDbBuuvCePuWTddcv5KHHyrYO0yUD" +
|
||||
"xergVhh1EXIsOItHbJ6QeMstmY8Ub7HGm4Srdtm3MMSEe24zRmKK5yvPfeaaXeb6" +
|
||||
"MASKXvViU/j9VDwUZ2CFPUzPq8DlS6j4w6dapfphFGN1wJV3ADLUzUkTXfXQ57HE" +
|
||||
"WUKdbxgcuyBH7eLhZpKAXP31iRKm2b7dV50SruRCqNYZOp8bUQ57bC2jels0dzQd" +
|
||||
"EQS76O/DH6eQ3/OgvpmR8BjlujA82tgjqP7fj0S7Cw2VlPqcey0iqRmAmiO2qzOI" +
|
||||
"KAYzMkxWr7iUAAAAAAAA",
|
||||
)
|
||||
|
||||
var fixtureSignatureOpenSSLAttached = mustBase64Decode("" +
|
||||
"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" +
|
||||
"ZdMtHnR5z07z9wxvwiw=",
|
||||
)
|
||||
|
||||
var fixtureSignatureOpenSSLDetached = mustBase64Decode("" +
|
||||
"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",
|
||||
)
|
||||
|
||||
var fixtureSignatureOutlookDetached = mustBase64Decode("" +
|
||||
"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" +
|
||||
"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()
|
||||
}
|
2
main.go
2
main.go
|
@ -7,7 +7,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/github/certstore"
|
||||
"github.com/github/smimesign/certstore"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче