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