зеркало из 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"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/github/certstore"
|
"github.com/github/smimesign/certstore"
|
||||||
"github.com/github/ietf-cms"
|
cms "github.com/github/smimesign/ietf-cms"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/github/ietf-cms/protocol"
|
cms "github.com/github/smimesign/ietf-cms"
|
||||||
"github.com/github/ietf-cms"
|
"github.com/github/smimesign/ietf-cms/protocol"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/certifi/gocertifi"
|
"github.com/certifi/gocertifi"
|
||||||
"github.com/github/ietf-cms"
|
cms "github.com/github/smimesign/ietf-cms"
|
||||||
"github.com/pkg/errors"
|
"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 (
|
require (
|
||||||
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261
|
github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/github/certstore v0.1.0
|
|
||||||
github.com/github/fakeca v0.1.0
|
|
||||||
github.com/github/ietf-cms v0.1.2
|
|
||||||
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b
|
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/pmezard/go-difflib v1.0.0
|
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.3.0
|
||||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:K1wa7ads2Bu1PavI6LfBRMYSy6Zi+Rky0OhWBfrmkmY=
|
||||||
github.com/pborman/getopt v0.0.0-20180811024354-2b5b3bfb099b/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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-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 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
|
||||||
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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/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=
|
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("" +
|
||||||
|
"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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 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" +
|
||||||
|
"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" +
|
||||||
|
"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"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/github/certstore"
|
"github.com/github/smimesign/certstore"
|
||||||
"github.com/pborman/getopt/v2"
|
"github.com/pborman/getopt/v2"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/github/certstore"
|
"github.com/github/smimesign/certstore"
|
||||||
"github.com/github/fakeca"
|
"github.com/github/smimesign/fakeca"
|
||||||
"github.com/pborman/getopt/v2"
|
"github.com/pborman/getopt/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче