Skip to content

Commit

Permalink
Add SLSA 1.0 attestation support to cosign. Closes sigstore#2860 (sig…
Browse files Browse the repository at this point in the history
…store#3219)

* Add SLSA 1.0 attestation support to cosign

Signed-off-by: Canaan Silberberg <csilberberg@etsy.com>

* fix leading whitspace

Signed-off-by: Canaan Silberberg <csilberberg@etsy.com>

* fix 1.0 typo

Signed-off-by: Canaan Silberberg <csilberberg@etsy.com>

* add slsaprovenance02 type

Signed-off-by: Canaan Silberberg <csilberberg@etsy.com>

---------

Signed-off-by: Canaan Silberberg <csilberberg@etsy.com>
  • Loading branch information
ziel authored and lance committed Sep 25, 2023
1 parent 49194e0 commit 7dfffd2
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 129 deletions.
252 changes: 136 additions & 116 deletions cmd/cosign/cli/attest/attest_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ func writeFile(t *testing.T, td string, blob string, name string) string {
return blobPath
}

func makeSLSA02PredicateFile(t *testing.T, td string) string {
predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`
return writeFile(t, td, predicate, "predicate02.json")
}

func makeSLSA1PredicateFile(t *testing.T, td string) string {
predicate := `{ "buildDefinition": {}, "runDetails": {} }`
return writeFile(t, td, predicate, "predicate1.json")
}

// TestAttestBlobCmdWithCert verifies the AttestBlobCmd checks
// that the cmd correctly matches the signing key with the cert
// provided.
Expand Down Expand Up @@ -99,73 +109,78 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
chainRef := writeFile(t, td, string(pemChain), "chain.pem")

blob := writeFile(t, td, "foo", "foo.txt")
predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`
predicateType := "slsaprovenance"
predicatePath := writeFile(t, td, predicate, "predicate.json")

ctx := context.Background()
for _, tc := range []struct {
name string
keyref string
certref string
certchainref string
errString string
}{
{
name: "no cert",
keyref: keyRef,
},
{
name: "cert matches key",
keyref: keyRef,
certref: certRef,
},
{
name: "fail: cert no match key",
keyref: keyRef,
certref: subCertPem,
errString: "public key in certificate does not match the provided public key",
},
{
name: "cert chain matches key",
keyref: keyRef,
certref: certRef,
certchainref: chainRef,
},
{
name: "cert chain partial",
keyref: keyRef,
certref: certRef,
certchainref: subCertPem,
},
{
name: "fail: cert chain bad",
keyref: keyRef,
certref: certRef,
certchainref: otherRootPem,
errString: "unable to validate certificate chain",
},
} {
t.Run(tc.name, func(t *testing.T) {
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: tc.keyref},
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
PredicateType: predicateType,
}
err := at.Exec(ctx, blob)
if err != nil {
if tc.errString == "" {
t.Fatalf("unexpected error %v", err)
}
if !strings.Contains(err.Error(), tc.errString) {
t.Fatalf("expected error %v got %v", tc.errString, err)
}
return
}
if tc.errString != "" {
t.Fatalf("expected error %v", tc.errString)
predicates := map[string]string{}
predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td)
predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td)

for predicateType, predicatePath := range predicates {
t.Run(predicateType, func(t *testing.T) {
ctx := context.Background()
for _, tc := range []struct {
name string
keyref string
certref string
certchainref string
errString string
}{
{
name: "no cert",
keyref: keyRef,
},
{
name: "cert matches key",
keyref: keyRef,
certref: certRef,
},
{
name: "fail: cert no match key",
keyref: keyRef,
certref: subCertPem,
errString: "public key in certificate does not match the provided public key",
},
{
name: "cert chain matches key",
keyref: keyRef,
certref: certRef,
certchainref: chainRef,
},
{
name: "cert chain partial",
keyref: keyRef,
certref: certRef,
certchainref: subCertPem,
},
{
name: "fail: cert chain bad",
keyref: keyRef,
certref: certRef,
certchainref: otherRootPem,
errString: "unable to validate certificate chain",
},
} {
t.Run(tc.name, func(t *testing.T) {
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: tc.keyref},
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
PredicateType: predicateType,
}
err := at.Exec(ctx, blob)
if err != nil {
if tc.errString == "" {
t.Fatalf("unexpected error %v", err)
}
if !strings.Contains(err.Error(), tc.errString) {
t.Fatalf("expected error %v got %v", tc.errString, err)
}
return
}
if tc.errString != "" {
t.Fatalf("expected error %v", tc.errString)
}
})
}
})
}
Expand All @@ -185,59 +200,64 @@ func TestAttestBlob(t *testing.T) {
blobPath := writeFile(t, td, string(blob), "foo.txt")
digest, _, _ := signature.ComputeDigestForSigning(bytes.NewReader(blob), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384})
blobDigest := strings.ToLower(hex.EncodeToString(digest))
predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`
predicateType := "slsaprovenance"
predicatePath := writeFile(t, td, predicate, "predicate.json")

dssePath := filepath.Join(td, "dsse.intoto.jsonl")
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
PredicatePath: predicatePath,
PredicateType: predicateType,
OutputSignature: dssePath,
}
err := at.Exec(ctx, blobPath)
if err != nil {
t.Fatal(err)
}

// Load the attestation.
dsseBytes, _ := os.ReadFile(dssePath)
env := &ssldsse.Envelope{}
if err := json.Unmarshal(dsseBytes, env); err != nil {
t.Fatal(err)
}
predicates := map[string]string{}
predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td)
predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td)

if len(env.Signatures) != 1 {
t.Fatalf("expected 1 signature, got %d", len(env.Signatures))
}
for predicateType, predicatePath := range predicates {
t.Run(predicateType, func(t *testing.T) {
dssePath := filepath.Join(td, "dsse.intoto.jsonl")
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
PredicatePath: predicatePath,
PredicateType: predicateType,
OutputSignature: dssePath,
}
err := at.Exec(ctx, blobPath)
if err != nil {
t.Fatal(err)
}

// Verify the subject digest
decodedPredicate, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
t.Fatalf("decoding dsse payload: %v", err)
}
var statement in_toto.Statement
if err := json.Unmarshal(decodedPredicate, &statement); err != nil {
t.Fatalf("decoding predicate: %v", err)
}
if statement.Subject == nil || len(statement.Subject) != 1 {
t.Fatalf("expected one subject in intoto statement")
}
if statement.Subject[0].Digest["sha256"] != blobDigest {
t.Fatalf("expected matching digest")
}
if statement.PredicateType != options.PredicateTypeMap[predicateType] {
t.Fatalf("expected matching predicate type")
}
// Load the attestation.
dsseBytes, _ := os.ReadFile(dssePath)
env := &ssldsse.Envelope{}
if err := json.Unmarshal(dsseBytes, env); err != nil {
t.Fatal(err)
}

// Load a verifier and DSSE verify
verifier, _ := signature.LoadVerifierFromPEMFile(pubKeyRef, crypto.SHA256)
dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: verifier})
if err != nil {
t.Fatalf("new envelope verifier: %v", err)
}
if _, err := dssev.Verify(ctx, env); err != nil {
t.Fatalf("dsse verify: %v", err)
if len(env.Signatures) != 1 {
t.Fatalf("expected 1 signature, got %d", len(env.Signatures))
}

// Verify the subject digest
decodedPredicate, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
t.Fatalf("decoding dsse payload: %v", err)
}
var statement in_toto.Statement
if err := json.Unmarshal(decodedPredicate, &statement); err != nil {
t.Fatalf("decoding predicate: %v", err)
}
if statement.Subject == nil || len(statement.Subject) != 1 {
t.Fatalf("expected one subject in intoto statement")
}
if statement.Subject[0].Digest["sha256"] != blobDigest {
t.Fatalf("expected matching digest")
}
if statement.PredicateType != options.PredicateTypeMap[predicateType] {
t.Fatalf("expected matching predicate type")
}

// Load a verifier and DSSE verify
verifier, _ := signature.LoadVerifierFromPEMFile(pubKeyRef, crypto.SHA256)
dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: verifier})
if err != nil {
t.Fatalf("new envelope verifier: %v", err)
}
if _, err := dssev.Verify(ctx, env); err != nil {
t.Fatalf("dsse verify: %v", err)
}
})
}
}
11 changes: 8 additions & 3 deletions cmd/cosign/cli/options/predicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import (
"fmt"
"net/url"

slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsa02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"

"github.com/in-toto/in-toto-golang/in_toto"
"github.com/spf13/cobra"
Expand All @@ -30,6 +31,8 @@ import (
const (
PredicateCustom = "custom"
PredicateSLSA = "slsaprovenance"
PredicateSLSA02 = "slsaprovenance02"
PredicateSLSA1 = "slsaprovenance1"
PredicateSPDX = "spdx"
PredicateSPDXJSON = "spdxjson"
PredicateCycloneDX = "cyclonedx"
Expand All @@ -40,7 +43,9 @@ const (
// PredicateTypeMap is the mapping between the predicate `type` option to predicate URI.
var PredicateTypeMap = map[string]string{
PredicateCustom: attestation.CosignCustomProvenanceV01,
PredicateSLSA: slsa.PredicateSLSAProvenance,
PredicateSLSA: slsa02.PredicateSLSAProvenance,
PredicateSLSA02: slsa02.PredicateSLSAProvenance,
PredicateSLSA1: slsa1.PredicateSLSAProvenance,
PredicateSPDX: in_toto.PredicateSPDX,
PredicateSPDXJSON: in_toto.PredicateSPDX,
PredicateCycloneDX: in_toto.PredicateCycloneDX,
Expand All @@ -58,7 +63,7 @@ var _ Interface = (*PredicateOptions)(nil)
// AddFlags implements Interface
func (o *PredicateOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVar(&o.Type, "type", "custom",
"specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI")
"specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI")
}

// ParsePredicateType parses the predicate `type` flag passed into a predicate URI, or validates `type` is a valid URI.
Expand Down
2 changes: 1 addition & 1 deletion doc/cosign_attest-blob.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion doc/cosign_attest.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion doc/cosign_verify-attestation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion doc/cosign_verify-blob-attestation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 7dfffd2

Please sign in to comment.