Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SLSA 1.0 attestation support to cosign. Closes #2860 #3219

Merged
merged 4 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading