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
 }