diff --git a/command_sign.go b/command_sign.go index 5b09434a..efcf0bd5 100644 --- a/command_sign.go +++ b/command_sign.go @@ -6,10 +6,10 @@ import ( "io" "os" - "github.com/github/smimesign/signature" "github.com/pkg/errors" "github.com/wlynch/smimecosign/fulcio" "github.com/wlynch/smimecosign/git" + "github.com/wlynch/smimecosign/signature" ) func commandSign() error { diff --git a/command_verify.go b/command_verify.go index ef4b7992..d5926c7b 100644 --- a/command_verify.go +++ b/command_verify.go @@ -8,10 +8,10 @@ import ( "io" "os" - "github.com/github/smimesign/signature" "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio/fulcioroots" "github.com/wlynch/smimecosign/git" + "github.com/wlynch/smimecosign/signature" ) func commandVerify() error { diff --git a/git/git.go b/git/git.go index 2a164101..8adaf833 100644 --- a/git/git.go +++ b/git/git.go @@ -7,7 +7,6 @@ import ( "encoding/pem" cms "github.com/github/smimesign/ietf-cms" - "github.com/github/smimesign/signature" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/pkg/errors" @@ -15,6 +14,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/models" "github.com/wlynch/smimecosign/fulcio" "github.com/wlynch/smimecosign/rekor" + "github.com/wlynch/smimecosign/signature" ) func Sign(ctx context.Context, ident *fulcio.Identity, data []byte, opts signature.SignOptions) ([]byte, *x509.Certificate, error) { diff --git a/go.mod b/go.mod index 3c316a2e..3593e8f3 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,17 @@ go 1.18 require ( github.com/github/smimesign v0.2.0 github.com/go-git/go-git/v5 v5.4.2 + github.com/go-openapi/runtime v0.24.0 github.com/go-openapi/strfmt v0.21.2 github.com/go-openapi/swag v0.21.1 github.com/pborman/getopt/v2 v2.1.0 github.com/pkg/errors v0.9.1 github.com/sigstore/cosign v1.8.1-0.20220502185546-8efb042c0427 github.com/sigstore/rekor v0.4.1-0.20220114213500-23f583409af3 + github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 ) -// Remove once https://github.com/github/smimesign/pull/108 is merged. -replace github.com/github/smimesign => github.com/wlynch/smimesign v0.2.1-0.20220502200102-5d7d5b14387f - require ( bitbucket.org/creachadair/shell v0.0.6 // indirect cloud.google.com/go v0.100.2 // indirect @@ -94,7 +93,6 @@ require ( github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/loads v0.21.1 // indirect - github.com/go-openapi/runtime v0.24.0 // indirect github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/validate v0.21.0 // indirect github.com/go-piv/piv-go v1.9.0 // indirect @@ -169,7 +167,6 @@ require ( github.com/sergi/go-diff v1.2.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sigstore/fulcio v0.1.2-0.20220114150912-86a2036f9bc7 // indirect - github.com/sigstore/sigstore v1.2.1-0.20220424143412-3d41663116d5 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/soheilhy/cmux v0.1.5 // indirect diff --git a/go.sum b/go.sum index 5e6bf096..2a85617c 100644 --- a/go.sum +++ b/go.sum @@ -710,6 +710,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.3/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/github/smimesign v0.2.0 h1:Hho4YcX5N1I9XNqhq0fNx0Sts8MhLonHd+HRXVGNjvk= +github.com/github/smimesign v0.2.0/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -1965,8 +1967,6 @@ github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvC github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= -github.com/wlynch/smimesign v0.2.1-0.20220502200102-5d7d5b14387f h1:IKLDd3pc2H3fTawZlEqnCyBiFs9c/nmTfOfD7Byo2aU= -github.com/wlynch/smimesign v0.2.1-0.20220502200102-5d7d5b14387f/go.mod h1:iZiiwNT4HbtGRVqCQu7uJPEZCuEE5sfSSttcnePkDl4= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= github.com/xanzy/go-gitlab v0.64.0 h1:rMgQdW9S1w3qvNAH2LYpFd2xh7KNLk+JWJd7sorNuTc= github.com/xanzy/go-gitlab v0.64.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= diff --git a/signature/sign.go b/signature/sign.go new file mode 100644 index 00000000..79c294d1 --- /dev/null +++ b/signature/sign.go @@ -0,0 +1,148 @@ +package signature + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/pem" + + 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 +} + +// Identity is a copy of smimesign.Identity to allow for compatibility without +// needing a dependency on the whole package. This can be removed once +// https://github.com/github/smimesign/pull/108 is merged. +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() +} + +// Sign signs a given payload for the given identity. +// The resulting signature and cert used is returned. +func Sign(ident 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 00000000..fe32c2bb --- /dev/null +++ b/signature/signature_test.go @@ -0,0 +1,57 @@ +package signature + +import ( + "crypto" + "crypto/x509" + "fmt" + "testing" + + "github.com/github/smimesign/fakeca" +) + +type identity struct { + 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 00000000..85b453b5 --- /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) + } +}