From cb8f03b209bba7549bf03fa2712a4cb4188e8e3e Mon Sep 17 00:00:00 2001 From: asraa <asraa@google.com> Date: Fri, 17 Jun 2022 13:13:08 -0500 Subject: [PATCH] feat: add signing certificate to envelope (#330) * add signing certificate to envelope Signed-off-by: Asra Ali <asraa@google.com> --- go.mod | 3 + go.sum | 9 ++ signing/envelope/envelope.go | 86 ++++++++++++++ signing/envelope/envelope_test.go | 184 ++++++++++++++++++++++++++++++ signing/sigstore/fulcio.go | 10 +- 5 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 signing/envelope/envelope.go create mode 100644 signing/envelope/envelope_test.go diff --git a/go.mod b/go.mod index 501f6cf8f6..0d0e25fc8c 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect github.com/PaesslerAG/gval v1.0.0 // indirect github.com/PaesslerAG/jsonpath v0.1.1 // indirect github.com/ThalesIgnite/crypto11 v1.2.5 // indirect @@ -169,6 +170,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.12.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.1.0 // indirect github.com/subosito/gotenv v1.3.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect @@ -181,6 +183,7 @@ require ( github.com/vbatts/tar-split v0.11.2 // indirect github.com/xanzy/go-gitlab v0.68.0 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect + github.com/zeebo/errs v1.2.2 // indirect go.etcd.io/bbolt v1.3.6 // indirect go.etcd.io/etcd/api/v3 v3.5.4 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect diff --git a/go.sum b/go.sum index da09c90c33..b05b097095 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,8 @@ github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JP github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -1697,6 +1699,8 @@ github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spiffe/go-spiffe/v2 v2.1.0 h1:IZRlWhyFpPbJOiK8K+MwEFPU/QCdaW4Zf5bmIKBd3XM= +github.com/spiffe/go-spiffe/v2 v2.1.0/go.mod h1:5qg6rpqlwIub0JAiF1UK9IMD6BpPTmvG6yfSgDBs5lg= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= @@ -1837,6 +1841,8 @@ github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0= +github.com/zeebo/errs v1.2.2 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g= +github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -2568,6 +2574,7 @@ google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -2687,6 +2694,7 @@ google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= +google.golang.org/grpc/examples v0.0.0-20201130180447-c456688b1860/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2736,6 +2744,7 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24 gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.4.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/signing/envelope/envelope.go b/signing/envelope/envelope.go new file mode 100644 index 0000000000..c365cc0671 --- /dev/null +++ b/signing/envelope/envelope.go @@ -0,0 +1,86 @@ +package envelope + +import ( + "encoding/json" + "fmt" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +/* +Envelope captures an envelope as described by the Secure Systems Lab +Signing Specification. See here: +https://github.com/secure-systems-lab/signing-spec/blob/master/envelope.md +*/ +type Envelope struct { + PayloadType string `json:"payloadType"` + Payload string `json:"payload"` + Signatures []Signature `json:"signatures"` +} + +/* +Signature represents a generic in-toto signature that contains the identifier +of the key which was used to create the signature. +The used signature scheme has to be agreed upon by the signer and verifer +out of band. +The signature is a base64 encoding of the raw bytes from the signature +algorithm. +The cert is a PEM encoded string of the signing certificate +*/ +type Signature struct { + KeyID string `json:"keyid"` + Sig string `json:"sig"` + Cert string `json:"cert"` +} + +// AddCertToEnvelope takes a signed DSSE Envelope and a PEM-encoded certificate, and +// returns an Envelope with the certificate inside the Signature of the Envelope. +// This assumes there is only one signature present in the envelope. +func AddCertToEnvelope(signedAtt []byte, cert []byte) ([]byte, error) { + // Unmarshal into a DSSE envelope. + env := &dsse.Envelope{} + if err := json.Unmarshal(signedAtt, env); err != nil { + return nil, err + } + + // Create an envelope.Envelope. + envWithCert := &Envelope{ + PayloadType: env.PayloadType, + Payload: env.Payload, + Signatures: []Signature{}, + } + + if len(env.Signatures) != 1 { + return nil, fmt.Errorf("expected exactly one signature in the envelope") + } + + if certs, err := cryptoutils.UnmarshalCertificatesFromPEM(cert); err != nil || len(certs) != 1 { + return nil, fmt.Errorf("invalid certificate, expected PEM encoded certificate") + } + + for _, sig := range env.Signatures { + envWithCert.Signatures = append(envWithCert.Signatures, + Signature{Sig: sig.Sig, KeyID: sig.KeyID, Cert: string(cert)}) + } + + // Return marshalled result + return json.Marshal(envWithCert) +} + +// GetCertFromEnvelope takes a signed Envelope and extracts the PEM-encoded +// certificate from the signature. +// This assumes there is only one signature present in the envelope. +func GetCertFromEnvelope(signedAtt []byte) ([]byte, error) { + // Unmarshal into an envelope. + env := &Envelope{} + if err := json.Unmarshal(signedAtt, env); err != nil { + return nil, err + } + + if len(env.Signatures) != 1 { + return nil, fmt.Errorf("expected exactly one signature in the envelope") + } + + return []byte(env.Signatures[0].Cert), nil +} diff --git a/signing/envelope/envelope_test.go b/signing/envelope/envelope_test.go new file mode 100644 index 0000000000..45949a1c61 --- /dev/null +++ b/signing/envelope/envelope_test.go @@ -0,0 +1,184 @@ +package envelope + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "encoding/pem" + "math/big" + "testing" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + sdsse "github.com/sigstore/sigstore/pkg/signature/dsse" + + intoto "github.com/in-toto/in-toto-golang/in_toto" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/types" + intotod "github.com/sigstore/rekor/pkg/types/intoto/v0.0.1" + "github.com/sigstore/sigstore/pkg/signature" +) + +func intotoEntry(certPem []byte, provenance []byte) (*intotod.V001Entry, error) { + cert := strfmt.Base64(certPem) + return &intotod.V001Entry{ + IntotoObj: models.IntotoV001Schema{ + Content: &models.IntotoV001SchemaContent{ + Envelope: string(provenance), + }, + PublicKey: &cert, + }, + }, nil +} + +// marshals a dsse envelope for testing +func marshalEnvelope(t *testing.T, env *dsse.Envelope) string { + b, err := json.Marshal(env) + if err != nil { + t.Fatalf("marshalling envelope: %s", err) + } + return string(b) +} + +// test utility to sign a payload with a given signer +func envelope(t *testing.T, k *ecdsa.PrivateKey, payload []byte) string { + s, err := signature.LoadECDSASigner(k, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + wrappedSigner := sdsse.WrapSigner(s, intoto.PayloadType) + if err != nil { + t.Fatal(err) + } + dsseEnv, err := wrappedSigner.SignMessage(bytes.NewReader(payload)) + if err != nil { + t.Fatal(err) + } + return string(dsseEnv) +} + +func TestAddCert(t *testing.T) { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1), + } + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) + if err != nil { + t.Fatal(err) + } + certPemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + validPayload := "hellothispayloadisvalid" + + tests := []struct { + name string + env string + cert []byte + addErr bool + }{ + { + name: "invalid empty envelope with no signatures", + env: marshalEnvelope(t, &dsse.Envelope{}), + cert: nil, + addErr: true, + }, + { + name: "invalid envelope with two signatures", + env: marshalEnvelope(t, &dsse.Envelope{ + Payload: "", + PayloadType: in_toto.PayloadType, + Signatures: []dsse.Signature{ + { + Sig: "abc", + }, + { + Sig: "xyz", + }, + }, + }), + cert: nil, + addErr: true, + }, + { + name: "invalid cert with valid envelope", + env: marshalEnvelope(t, &dsse.Envelope{ + Payload: "", + PayloadType: in_toto.PayloadType, + Signatures: []dsse.Signature{ + { + Sig: "abc", + }, + }, + }), + cert: nil, + addErr: true, + }, + { + name: "valid envelope", + env: envelope(t, priv, []byte(validPayload)), + cert: certPemBytes, + addErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Add certificate to envelope. + envWithCert, err := AddCertToEnvelope([]byte(tt.env), tt.cert) + if (err != nil) != tt.addErr { + t.Errorf("AddCertToEnvelope() error = %v, wanted %v", err, tt.addErr) + } + if err != nil { + return + } + + // Now get cert from envelope and compare. + gotCert, err := GetCertFromEnvelope(envWithCert) + if err != nil { + t.Fatalf("GetCertFromEnvelope() error = %v", err) + } + + if !bytes.EqualFold(gotCert, tt.cert) { + t.Errorf("expected cert equality") + } + + // Now test compatibility with Rekor intoto entry type. + testRekorSupport(t, tt.cert, envWithCert) + }) + } +} + +// This servers as a regression test to make sure that the Rekor intoto +// type can successfully unmarshal our "Envelope" with included cert. +func testRekorSupport(t *testing.T, certPem []byte, envWithCert []byte) { + ctx := context.Background() + intotoEntry, err := intotoEntry(certPem, envWithCert) + if err != nil { + t.Fatalf("error creating intoto entry: %s", err) + } + e := models.Intoto{ + APIVersion: swag.String(intotoEntry.APIVersion()), + Spec: intotoEntry.IntotoObj, + } + pe := models.ProposedEntry(&e) + entry, err := types.NewEntry(pe) + if err != nil { + t.Fatalf("error creating valid intoto entry") + } + _, err = types.CanonicalizeEntry(ctx, entry) + if err != nil { + t.Fatalf("error creating valid intoto entry") + } +} diff --git a/signing/sigstore/fulcio.go b/signing/sigstore/fulcio.go index 7427dbadc4..d0cf5ffd41 100644 --- a/signing/sigstore/fulcio.go +++ b/signing/sigstore/fulcio.go @@ -25,6 +25,7 @@ import ( "github.com/sigstore/cosign/pkg/providers" "github.com/sigstore/sigstore/pkg/signature/dsse" "github.com/slsa-framework/slsa-github-generator/signing" + "github.com/slsa-framework/slsa-github-generator/signing/envelope" intoto "github.com/in-toto/in-toto-golang/in_toto" ) @@ -101,8 +102,15 @@ func (s *Fulcio) Sign(ctx context.Context, p *intoto.Statement) (signing.Attesta return nil, fmt.Errorf("signing message: %v", err) } + // Add certificate to envelope. + // TODO: Remove when DSSE spec includes a cert field inside the signatures. + signedAttWithCert, err := envelope.AddCertToEnvelope(signedAtt, k.Cert) + if err != nil { + return nil, fmt.Errorf("adding certificate to DSSE: %v", err) + } + return &attestation{ - att: signedAtt, + att: signedAttWithCert, cert: k.Cert, }, nil }