From 5d7d5b14387fcd2b199af5d843f7ce9b9a5279f1 Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Fri, 22 Apr 2022 16:21:27 -0400 Subject: [PATCH] Refactor Sign/Verify functions into their own library. This commit should have no change in existing behavior, but does the following: 1. Pulls the sign/verify commands into its own package that can be invoked directly instead of needing to go through main. 2. Refactors the certstore library (a dependency of the sign library) to separate out the OS-dependent libraries so that any platform can safely pull in the certstore Identity interface. Adds a register func so that this can be set dynamically in main. My hope is to use this to allow similar tools to reuse this to provide additional identities and optional verification behavior. Signed-off-by: Billy Lynch --- certstore/certstore.go | 8 ++ certstore/{ => providers}/certstore_darwin.go | 20 ++- certstore/{ => providers}/certstore_linux.go | 2 +- certstore/{ => providers}/certstore_test.go | 25 ++-- .../{ => providers}/certstore_windows.go | 8 +- .../{ => providers}/crypt_strings_windows.go | 0 certstore/{ => providers}/main_test.go | 18 +-- .../{ => providers}/main_windows_test.go | 2 +- command_sign.go | 109 ++------------- command_verify.go | 44 +----- main.go | 3 + signature/sign.go | 132 ++++++++++++++++++ signature/signature_test.go | 58 ++++++++ signature/verify.go | 34 +++++ 14 files changed, 294 insertions(+), 169 deletions(-) rename certstore/{ => providers}/certstore_darwin.go (96%) rename certstore/{ => providers}/certstore_linux.go (95%) rename certstore/{ => providers}/certstore_test.go (91%) rename certstore/{ => providers}/certstore_windows.go (99%) rename certstore/{ => providers}/crypt_strings_windows.go (100%) rename certstore/{ => providers}/main_test.go (84%) rename certstore/{ => providers}/main_windows_test.go (95%) create mode 100644 signature/sign.go create mode 100644 signature/signature_test.go create mode 100644 signature/verify.go diff --git a/certstore/certstore.go b/certstore/certstore.go index 780aa7c..eb1c726 100644 --- a/certstore/certstore.go +++ b/certstore/certstore.go @@ -10,8 +10,16 @@ var ( // ErrUnsupportedHash is returned by Signer.Sign() when the provided hash // algorithm isn't supported. ErrUnsupportedHash = errors.New("unsupported hash algorithm") + + openStore func() (Store, error) ) +// RegisterStore registers a func to initialize a new certificate store. +// This should be invoked by providers during init(). +func RegisterStore(f func() (Store, error)) { + openStore = f +} + // Open opens the system's certificate store. func Open() (Store, error) { return openStore() diff --git a/certstore/certstore_darwin.go b/certstore/providers/certstore_darwin.go similarity index 96% rename from certstore/certstore_darwin.go rename to certstore/providers/certstore_darwin.go index f4797c2..4ae5677 100644 --- a/certstore/certstore_darwin.go +++ b/certstore/providers/certstore_darwin.go @@ -1,4 +1,4 @@ -package certstore +package providers /* #cgo CFLAGS: -x objective-c @@ -16,8 +16,14 @@ import ( "fmt" "io" "unsafe" + + "github.com/github/smimesign/certstore" ) +func init() { + certstore.RegisterStore(openStore) +} + // work around https://golang.org/doc/go1.10#cgo // in go>=1.10 CFTypeRefs are translated to uintptrs instead of pointers. var ( @@ -37,12 +43,12 @@ var ( type macStore int // openStore is a function for opening a macStore. -func openStore() (macStore, error) { +func openStore() (certstore.Store, error) { return macStore(0), nil } // Identities implements the Store interface. -func (s macStore) Identities() ([]Identity, error) { +func (s macStore) Identities() ([]certstore.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), @@ -56,7 +62,7 @@ func (s macStore) Identities() ([]Identity, error) { var absResult C.CFTypeRef if err := osStatusError(C.SecItemCopyMatching(query, &absResult)); err != nil { if err == errSecItemNotFound { - return []Identity{}, nil + return []certstore.Identity{}, nil } return nil, err @@ -71,7 +77,7 @@ func (s macStore) Identities() ([]Identity, error) { identRefs := make([]C.CFTypeRef, n) C.CFArrayGetValues(aryResult, C.CFRange{0, n}, (*unsafe.Pointer)(unsafe.Pointer(&identRefs[0]))) - idents := make([]Identity, 0, n) + idents := make([]certstore.Identity, 0, n) for _, identRef := range identRefs { idents = append(idents, newMacIdentity(C.SecIdentityRef(identRef))) } @@ -319,7 +325,7 @@ func (i *macIdentity) getAlgo(hash crypto.Hash) (algo C.SecKeyAlgorithm, err err case crypto.SHA512: algo = C.kSecKeyAlgorithmECDSASignatureDigestX962SHA512 default: - err = ErrUnsupportedHash + err = certstore.ErrUnsupportedHash } case *rsa.PublicKey: switch hash { @@ -332,7 +338,7 @@ func (i *macIdentity) getAlgo(hash crypto.Hash) (algo C.SecKeyAlgorithm, err err case crypto.SHA512: algo = C.kSecKeyAlgorithmRSASignatureDigestPKCS1v15SHA512 default: - err = ErrUnsupportedHash + err = certstore.ErrUnsupportedHash } default: err = errors.New("unsupported key type") diff --git a/certstore/certstore_linux.go b/certstore/providers/certstore_linux.go similarity index 95% rename from certstore/certstore_linux.go rename to certstore/providers/certstore_linux.go index 9da0b14..0a6f36a 100644 --- a/certstore/certstore_linux.go +++ b/certstore/providers/certstore_linux.go @@ -1,4 +1,4 @@ -package certstore +package providers import "errors" diff --git a/certstore/certstore_test.go b/certstore/providers/certstore_test.go similarity index 91% rename from certstore/certstore_test.go rename to certstore/providers/certstore_test.go index 84b22b6..21e9ac8 100644 --- a/certstore/certstore_test.go +++ b/certstore/providers/certstore_test.go @@ -1,4 +1,4 @@ -package certstore +package providers import ( "crypto" @@ -11,6 +11,7 @@ import ( "crypto/x509" "testing" + "github.com/github/smimesign/certstore" "github.com/github/smimesign/fakeca" ) @@ -24,7 +25,7 @@ func TestImportDeleteECDSA(t *testing.T) { // ImportDeleteHelper is an abstraction for testing identity Import()/Delete(). func ImportDeleteHelper(t *testing.T, i *fakeca.Identity) { - withStore(t, func(store Store) { + withStore(t, func(store certstore.Store) { // Import an identity if err := store.Import(i.PFX("asdf"), "asdf"); err != nil { t.Fatal(err) @@ -39,7 +40,7 @@ func ImportDeleteHelper(t *testing.T, i *fakeca.Identity) { defer ident.Close() } - var found Identity + var found certstore.Identity for _, ident := range idents { crt, errr := ident.Certificate() if errr != nil { @@ -95,7 +96,7 @@ func TestSignerRSA(t *testing.T) { t.Fatal("expected priv to be an RSA private key") } - withIdentity(t, leafRSA, func(ident Identity) { + withIdentity(t, leafRSA, func(ident certstore.Identity) { signer, err := ident.Signer() if err != nil { t.Fatal(err) @@ -129,7 +130,7 @@ func TestSignerRSA(t *testing.T) { // SHA256WithRSA sha256Digest := sha256.Sum256([]byte("hello")) sig, err = signer.Sign(rand.Reader, sha256Digest[:], crypto.SHA256) - if err == ErrUnsupportedHash { + if err == certstore.ErrUnsupportedHash { // Some Windows CSPs may not support this algorithm. Pass... } else if err != nil { t.Fatal(err) @@ -142,7 +143,7 @@ func TestSignerRSA(t *testing.T) { // SHA384WithRSA sha384Digest := sha512.Sum384([]byte("hello")) sig, err = signer.Sign(rand.Reader, sha384Digest[:], crypto.SHA384) - if err == ErrUnsupportedHash { + if err == certstore.ErrUnsupportedHash { // Some Windows CSPs may not support this algorithm. Pass... } else if err != nil { t.Fatal(err) @@ -155,7 +156,7 @@ func TestSignerRSA(t *testing.T) { // SHA512WithRSA sha512Digest := sha512.Sum512([]byte("hello")) sig, err = signer.Sign(rand.Reader, sha512Digest[:], crypto.SHA512) - if err == ErrUnsupportedHash { + if err == certstore.ErrUnsupportedHash { // Some Windows CSPs may not support this algorithm. Pass... } else if err != nil { t.Fatal(err) @@ -174,7 +175,7 @@ func TestSignerRSA(t *testing.T) { // Unsupported hash sha224Digest := sha256.Sum224([]byte("hello")) _, err = signer.Sign(rand.Reader, sha224Digest[:], crypto.SHA224) - if err != ErrUnsupportedHash { + if err != certstore.ErrUnsupportedHash { t.Fatal("expected ErrUnsupportedHash, got ", err) } }) @@ -186,7 +187,7 @@ func TestSignerECDSA(t *testing.T) { t.Fatal("expected priv to be an ECDSA private key") } - withIdentity(t, leafEC, func(ident Identity) { + withIdentity(t, leafEC, func(ident certstore.Identity) { signer, err := ident.Signer() if err != nil { t.Fatal(err) @@ -263,9 +264,9 @@ func TestCertificateEC(t *testing.T) { } 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) { + withIdentity(t, root, func(caIdent certstore.Identity) { + withIdentity(t, intermediate, func(interIdent certstore.Identity) { + withIdentity(t, leaf, func(leafIdent certstore.Identity) { crtActual, err := leafIdent.Certificate() if err != nil { t.Fatal(err) diff --git a/certstore/certstore_windows.go b/certstore/providers/certstore_windows.go similarity index 99% rename from certstore/certstore_windows.go rename to certstore/providers/certstore_windows.go index 86c0b1c..5eec541 100644 --- a/certstore/certstore_windows.go +++ b/certstore/providers/certstore_windows.go @@ -1,4 +1,4 @@ -package certstore +package providers /* #cgo windows LDFLAGS: -lcrypt32 -lncrypt @@ -75,8 +75,12 @@ type winStore struct { store C.HCERTSTORE } +func init() { + certstore.RegisterStore(openStore) +} + // openStore opens the current user's personal cert store. -func openStore() (*winStore, error) { +func openStore() (certstore.Store, error) { storeName := unsafe.Pointer(stringToUTF16("MY")) defer C.free(storeName) diff --git a/certstore/crypt_strings_windows.go b/certstore/providers/crypt_strings_windows.go similarity index 100% rename from certstore/crypt_strings_windows.go rename to certstore/providers/crypt_strings_windows.go diff --git a/certstore/main_test.go b/certstore/providers/main_test.go similarity index 84% rename from certstore/main_test.go rename to certstore/providers/main_test.go index 3787361..55452de 100644 --- a/certstore/main_test.go +++ b/certstore/providers/main_test.go @@ -1,4 +1,4 @@ -package certstore +package providers import ( "crypto/ecdsa" @@ -9,6 +9,7 @@ import ( "crypto/x509/pkix" "testing" + "github.com/github/smimesign/certstore" "github.com/github/smimesign/fakeca" ) @@ -38,11 +39,12 @@ var ( func init() { // delete any fixtures from a previous test run. + certstore.RegisterStore(openStore) clearFixtures() } -func withStore(t *testing.T, cb func(Store)) { - store, err := Open() +func withStore(t *testing.T, cb func(certstore.Store)) { + store, err := certstore.Open() if err != nil { t.Fatal(err) } @@ -51,8 +53,8 @@ func withStore(t *testing.T, cb func(Store)) { cb(store) } -func withIdentity(t *testing.T, i *fakeca.Identity, cb func(Identity)) { - withStore(t, func(store Store) { +func withIdentity(t *testing.T, i *fakeca.Identity, cb func(certstore.Identity)) { + withStore(t, func(store certstore.Store) { // Import an identity if err := store.Import(i.PFX("asdf"), "asdf"); err != nil { t.Fatal(err) @@ -67,7 +69,7 @@ func withIdentity(t *testing.T, i *fakeca.Identity, cb func(Identity)) { defer ident.Close() } - var found Identity + var found certstore.Identity for _, ident := range idents { crt, err := ident.Certificate() if err != nil { @@ -86,7 +88,7 @@ func withIdentity(t *testing.T, i *fakeca.Identity, cb func(Identity)) { } // Clean up after ourselves. - defer func(f Identity) { + defer func(f certstore.Identity) { if err := f.Delete(); err != nil { t.Fatal(err) } @@ -97,7 +99,7 @@ func withIdentity(t *testing.T, i *fakeca.Identity, cb func(Identity)) { } func clearFixtures() { - store, err := Open() + store, err := certstore.Open() if err != nil { panic(err) } diff --git a/certstore/main_windows_test.go b/certstore/providers/main_windows_test.go similarity index 95% rename from certstore/main_windows_test.go rename to certstore/providers/main_windows_test.go index 94a2cd7..a1bbd5d 100644 --- a/certstore/main_windows_test.go +++ b/certstore/providers/main_windows_test.go @@ -1,4 +1,4 @@ -package certstore +package providers import ( "fmt" diff --git a/command_sign.go b/command_sign.go index 43feb83..28e123d 100644 --- a/command_sign.go +++ b/command_sign.go @@ -2,15 +2,13 @@ package main import ( "bytes" - "crypto/x509" - "encoding/pem" "fmt" "io" "os" "strings" "github.com/github/smimesign/certstore" - cms "github.com/github/smimesign/ietf-cms" + "github.com/github/smimesign/signature" "github.com/pkg/errors" ) @@ -28,16 +26,6 @@ func commandSign() error { // though GPGSM does not. sBeginSigning.emit() - cert, err := userIdent.Certificate() - if err != nil { - return errors.Wrap(err, "failed to get idenity certificate") - } - - signer, err := userIdent.Signer() - if err != nil { - return errors.Wrap(err, "failed to get idenity signer") - } - var f io.ReadCloser if len(fileArgs) == 1 { if f, err = os.Open(fileArgs[0]); err != nil { @@ -53,50 +41,19 @@ func commandSign() error { return errors.Wrap(err, "failed to read message from stdin") } - sd, err := cms.NewSignedData(dataBuf.Bytes()) + sig, cert, err := signature.Sign(userIdent, dataBuf.Bytes(), signature.SignOptions{ + Detached: *detachSignFlag, + TimestampAuthority: *tsaOpt, + Armor: *armorFlag, + IncludeCerts: *includeCertsOpt, + }) if err != nil { - return errors.Wrap(err, "failed to create signed data") - } - if err = sd.Sign([]*x509.Certificate{cert}, signer); err != nil { return errors.Wrap(err, "failed to sign message") } - if *detachSignFlag { - sd.Detached() - } - - if len(*tsaOpt) > 0 { - if err = sd.AddTimestamps(*tsaOpt); err != nil { - return errors.Wrap(err, "failed to add timestamp") - } - } - - chain, err := userIdent.CertificateChain() - if err != nil { - return errors.Wrap(err, "failed to get idenity certificate chain") - } - if chain, err = certsForSignature(chain); err != nil { - return err - } - if err = sd.SetCertificates(chain); err != nil { - return errors.Wrap(err, "failed to set certificates") - } - - der, err := sd.ToDER() - if err != nil { - return errors.Wrap(err, "failed to serialize signature") - } emitSigCreated(cert, *detachSignFlag) - if *armorFlag { - err = pem.Encode(stdout, &pem.Block{ - Type: "SIGNED MESSAGE", - Bytes: der, - }) - } else { - _, err = stdout.Write(der) - } - if err != nil { + if _, err := stdout.Write(sig); err != nil { return errors.New("failed to write signature") } @@ -129,53 +86,3 @@ func findUserIdentity() (certstore.Identity, error) { return nil, nil } - -// certsForSignature determines which certificates to include in the signature -// based on the --include-certs option specified by the user. -func certsForSignature(chain []*x509.Certificate) ([]*x509.Certificate, error) { - include := *includeCertsOpt - - if include < -3 { - include = -2 // default - } - if include > len(chain) { - include = len(chain) - } - - switch include { - case -3: - for i := len(chain) - 1; i > 0; i-- { - issuer, cert := chain[i], chain[i-1] - - // remove issuer when cert has AIA extension - if bytes.Equal(issuer.RawSubject, cert.RawIssuer) && len(cert.IssuingCertificateURL) > 0 { - chain = chain[0:i] - } - } - return chainWithoutRoot(chain), nil - case -2: - return chainWithoutRoot(chain), nil - case -1: - return chain, nil - default: - return chain[0:include], nil - } -} - -// Returns the provided chain, having removed the root certificate, if present. -// This includes removing the cert itself if the chain is a single self-signed -// cert. -func chainWithoutRoot(chain []*x509.Certificate) []*x509.Certificate { - if len(chain) == 0 { - return chain - } - - lastIdx := len(chain) - 1 - last := chain[lastIdx] - - if bytes.Equal(last.RawIssuer, last.RawSubject) { - return chain[0:lastIdx] - } - - return chain -} diff --git a/command_verify.go b/command_verify.go index 25a1881..afda933 100644 --- a/command_verify.go +++ b/command_verify.go @@ -3,13 +3,12 @@ package main import ( "bytes" "crypto/x509" - "encoding/pem" "fmt" "io" "os" "github.com/certifi/gocertifi" - cms "github.com/github/smimesign/ietf-cms" + "github.com/github/smimesign/signature" "github.com/pkg/errors" ) @@ -44,22 +43,7 @@ func verifyAttached() error { return errors.Wrap(err, "failed to read signature") } - // Try decoding as PEM - var der []byte - if blk, _ := pem.Decode(buf.Bytes()); blk != nil { - der = blk.Bytes - } else { - der = buf.Bytes() - } - - // Parse signature - sd, err := cms.ParseSignedData(der) - if err != nil { - return errors.Wrap(err, "failed to parse signature") - } - - // Verify signature - chains, err := sd.Verify(verifyOpts()) + chains, err := signature.Verify(nil, buf.Bytes(), false, verifyOpts()) if err != nil { if len(chains) > 0 { emitBadSig(chains) @@ -100,25 +84,11 @@ func verifyDetached() error { } defer f.Close() - buf := new(bytes.Buffer) - if _, err = io.Copy(buf, f); err != nil { + sig := new(bytes.Buffer) + if _, err = io.Copy(sig, f); err != nil { return errors.Wrap(err, "failed to read signature file") } - // Try decoding as PEM - var der []byte - if blk, _ := pem.Decode(buf.Bytes()); blk != nil { - der = blk.Bytes - } else { - der = buf.Bytes() - } - - // Parse signature - sd, err := cms.ParseSignedData(der) - if err != nil { - return errors.Wrap(err, "failed to parse signature") - } - // Read in signed data if fileArgs[1] == "-" { f = stdin @@ -130,12 +100,12 @@ func verifyDetached() error { } // Verify signature - buf.Reset() - if _, err = io.Copy(buf, f); err != nil { + data := new(bytes.Buffer) + if _, err = io.Copy(data, f); err != nil { return errors.Wrap(err, "failed to read message file") } - chains, err := sd.VerifyDetached(buf.Bytes(), verifyOpts()) + chains, err := signature.Verify(data.Bytes(), sig.Bytes(), true, verifyOpts()) if err != nil { if len(chains) > 0 { emitBadSig(chains) diff --git a/main.go b/main.go index cc24cce..d1b0cf0 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,9 @@ import ( "github.com/github/smimesign/certstore" "github.com/pborman/getopt/v2" "github.com/pkg/errors" + + // Registers OS-specific certstore providers. + _ "github.com/github/smimesign/certstore/providers" ) var ( diff --git a/signature/sign.go b/signature/sign.go new file mode 100644 index 0000000..807924c --- /dev/null +++ b/signature/sign.go @@ -0,0 +1,132 @@ +package signature + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + + "github.com/github/smimesign/certstore" + cms "github.com/github/smimesign/ietf-cms" + "github.com/pkg/errors" +) + +type SignOptions struct { + // Make a detached signature + Detached bool + // URL of RFC3161 timestamp authority to use for timestamping + TimestampAuthority string + // Create ascii armored output + Armor bool + // IncludeCerts specifies what certs to include in the resulting signature. + // -3 is the same as -2, but ommits issuer when cert has Authority Information Access extension. + // -2 includes all certs except root. + // -1 includes all certs. + // 0 includes no certs. + // 1 includes leaf cert. + // >1 includes n from the leaf. + IncludeCerts int +} + +// Sign signs a given payload for the given identity. +// The resulting signature and cert used is returned. +func Sign(ident certstore.Identity, body []byte, opts SignOptions) ([]byte, *x509.Certificate, error) { + cert, err := ident.Certificate() + if err != nil { + return nil, nil, errors.Wrap(err, "failed to get idenity certificate") + } + signer, err := ident.Signer() + if err != nil { + return nil, nil, errors.Wrap(err, "failed to get idenity signer") + } + + sd, err := cms.NewSignedData(body) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to create signed data") + } + + if err := sd.Sign([]*x509.Certificate{cert}, signer); err != nil { + return nil, nil, errors.Wrap(err, "failed to sign message") + } + if opts.Detached { + sd.Detached() + } + + if len(opts.TimestampAuthority) > 0 { + if err = sd.AddTimestamps(opts.TimestampAuthority); err != nil { + return nil, nil, errors.Wrap(err, "failed to add timestamp") + } + } + + chain, err := ident.CertificateChain() + if err != nil { + return nil, nil, errors.Wrap(err, "failed to get idenity certificate chain") + } + if chain, err = certsForSignature(chain, opts.IncludeCerts); err != nil { + return nil, nil, err + } + if err := sd.SetCertificates(chain); err != nil { + return nil, nil, errors.Wrap(err, "failed to set certificates") + } + + der, err := sd.ToDER() + if err != nil { + return nil, nil, errors.Wrap(err, "failed to serialize signature") + } + + if opts.Armor { + return pem.EncodeToMemory(&pem.Block{ + Type: "SIGNED MESSAGE", + Bytes: der, + }), cert, nil + } else { + return der, cert, nil + } +} + +// certsForSignature determines which certificates to include in the signature +// based on the --include-certs option specified by the user. +func certsForSignature(chain []*x509.Certificate, include int) ([]*x509.Certificate, error) { + if include < -3 { + include = -2 // default + } + if include > len(chain) { + include = len(chain) + } + + switch include { + case -3: + for i := len(chain) - 1; i > 0; i-- { + issuer, cert := chain[i], chain[i-1] + + // remove issuer when cert has AIA extension + if bytes.Equal(issuer.RawSubject, cert.RawIssuer) && len(cert.IssuingCertificateURL) > 0 { + chain = chain[0:i] + } + } + return chainWithoutRoot(chain), nil + case -2: + return chainWithoutRoot(chain), nil + case -1: + return chain, nil + default: + return chain[0:include], nil + } +} + +// Returns the provided chain, having removed the root certificate, if present. +// This includes removing the cert itself if the chain is a single self-signed +// cert. +func chainWithoutRoot(chain []*x509.Certificate) []*x509.Certificate { + if len(chain) == 0 { + return chain + } + + lastIdx := len(chain) - 1 + last := chain[lastIdx] + + if bytes.Equal(last.RawIssuer, last.RawSubject) { + return chain[0:lastIdx] + } + + return chain +} diff --git a/signature/signature_test.go b/signature/signature_test.go new file mode 100644 index 0000000..fa8e36e --- /dev/null +++ b/signature/signature_test.go @@ -0,0 +1,58 @@ +package signature + +import ( + "crypto" + "crypto/x509" + "fmt" + "testing" + + "github.com/github/smimesign/certstore" + "github.com/github/smimesign/fakeca" +) + +type identity struct { + certstore.Identity + base *fakeca.Identity +} + +func (i *identity) Certificate() (*x509.Certificate, error) { + return i.base.Certificate, nil +} + +func (i *identity) CertificateChain() ([]*x509.Certificate, error) { + return i.base.Chain(), nil +} + +func (i *identity) Signer() (crypto.Signer, error) { + return i.base.PrivateKey, nil +} + +// TestSignVerify is a basic test to ensure that the Sign/Verify funcs can be +// used with each other. We're assuming that the actual signature format has +// been more thoroghly vetted in other packages (i.e. ietf-cms). +func TestSignVerify(t *testing.T) { + id := &identity{ + base: fakeca.New(), + } + data := []byte("tacocat") + + sig, _, err := Sign(id, data, SignOptions{ + Detached: true, + Armor: true, + // Fake CA outputs self-signed certs, so we need to use -1 to make sure + // the self-signed cert itself is included in the chain, otherwise + // Verify cannot find a cert to use for verification. + IncludeCerts: -1, + }) + if err != nil { + t.Fatalf("Sign() = %v", err) + } + + fmt.Println(id.base.Chain()) + if _, err := Verify(data, sig, true, x509.VerifyOptions{ + // Trust the fake CA + Roots: id.base.ChainPool(), + }); err != nil { + t.Fatalf("Verify() = %v", err) + } +} diff --git a/signature/verify.go b/signature/verify.go new file mode 100644 index 0000000..85b453b --- /dev/null +++ b/signature/verify.go @@ -0,0 +1,34 @@ +package signature + +import ( + "crypto/x509" + "encoding/pem" + + cms "github.com/github/smimesign/ietf-cms" + "github.com/pkg/errors" +) + +// Verify verifies a signature for a given identity. +// +// WARNING: this function doesn't do any revocation checking. +func Verify(body, sig []byte, detached bool, opts x509.VerifyOptions) ([][][]*x509.Certificate, error) { + // Try decoding as PEM + var der []byte + if blk, _ := pem.Decode(sig); blk != nil { + der = blk.Bytes + } else { + der = sig + } + + // Parse signature + sd, err := cms.ParseSignedData(der) + if err != nil { + return nil, errors.Wrap(err, "failed to parse signature") + } + + if detached { + return sd.VerifyDetached(body, opts) + } else { + return sd.Verify(opts) + } +}