diff --git a/cmd/cosign/cli/attest/attest_blob_test.go b/cmd/cosign/cli/attest/attest_blob_test.go index 1f40a9e5d5d..a62b0eaea2d 100644 --- a/cmd/cosign/cli/attest/attest_blob_test.go +++ b/cmd/cosign/cli/attest/attest_blob_test.go @@ -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. @@ -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) + } + }) } }) } @@ -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) + } + }) } } diff --git a/cmd/cosign/cli/options/predicate.go b/cmd/cosign/cli/options/predicate.go index 707a38cae4a..0c68e279b7a 100644 --- a/cmd/cosign/cli/options/predicate.go +++ b/cmd/cosign/cli/options/predicate.go @@ -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" @@ -30,6 +31,8 @@ import ( const ( PredicateCustom = "custom" PredicateSLSA = "slsaprovenance" + PredicateSLSA02 = "slsaprovenance02" + PredicateSLSA1 = "slsaprovenance1" PredicateSPDX = "spdx" PredicateSPDXJSON = "spdxjson" PredicateCycloneDX = "cyclonedx" @@ -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, @@ -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. diff --git a/doc/cosign_attest-blob.md b/doc/cosign_attest-blob.md index e620f4e3a4f..e6d95e11202 100644 --- a/doc/cosign_attest-blob.md +++ b/doc/cosign_attest-blob.md @@ -58,7 +58,7 @@ cosign attest-blob [flags] --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-server-url string url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr --tlog-upload whether or not to upload to the tlog (default true) - --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") -y, --yes skip confirmation prompts for non-destructive operations ``` diff --git a/doc/cosign_attest.md b/doc/cosign_attest.md index bf7c25e468a..a613bcf5401 100644 --- a/doc/cosign_attest.md +++ b/doc/cosign_attest.md @@ -68,7 +68,7 @@ cosign attest [flags] --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-server-url string url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr --tlog-upload whether or not to upload to the tlog (default true) - --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") -y, --yes skip confirmation prompts for non-destructive operations ``` diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 61725949d53..f30a071f1b0 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -86,7 +86,7 @@ cosign verify-attestation [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index fc2b8cf809d..526e61890c7 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -54,7 +54,7 @@ cosign verify-blob-attestation [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --type string specify a predicate type (slsaprovenance|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") + --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|custom) or an URI (default "custom") ``` ### Options inherited from parent commands diff --git a/pkg/cosign/attestation/attestation.go b/pkg/cosign/attestation/attestation.go index 52a2008043f..5c454c1b84e 100644 --- a/pkg/cosign/attestation/attestation.go +++ b/pkg/cosign/attestation/attestation.go @@ -23,7 +23,8 @@ import ( "strings" "time" - 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" ) @@ -99,7 +100,7 @@ type GenerateOpts struct { } // GenerateStatement returns an in-toto statement based on the provided -// predicate type (custom|slsaprovenance|spdx|spdxjson|cyclonedx|link). +// predicate type (custom|slsaprovenance|slsaprovenance02|slsaprovenance1|spdx|spdxjson|cyclonedx|link). func GenerateStatement(opts GenerateOpts) (interface{}, error) { predicate, err := io.ReadAll(opts.Predicate) if err != nil { @@ -108,7 +109,11 @@ func GenerateStatement(opts GenerateOpts) (interface{}, error) { switch opts.Type { case "slsaprovenance": - return generateSLSAProvenanceStatement(predicate, opts.Digest, opts.Repo) + return generateSLSAProvenanceStatementSLSA02(predicate, opts.Digest, opts.Repo) + case "slsaprovenance02": + return generateSLSAProvenanceStatementSLSA02(predicate, opts.Digest, opts.Repo) + case "slsaprovenance1": + return generateSLSAProvenanceStatementSLSA1(predicate, opts.Digest, opts.Repo) case "spdx": return generateSPDXStatement(predicate, opts.Digest, opts.Repo, false) case "spdxjson": @@ -198,8 +203,8 @@ func generateCustomPredicate(rawPayload []byte, customType, timestamp string) (i return result, nil } -func generateSLSAProvenanceStatement(rawPayload []byte, digest string, repo string) (interface{}, error) { - var predicate slsa.ProvenancePredicate +func generateSLSAProvenanceStatementSLSA02(rawPayload []byte, digest string, repo string) (interface{}, error) { + var predicate slsa02.ProvenancePredicate err := checkRequiredJSONFields(rawPayload, reflect.TypeOf(predicate)) if err != nil { return nil, fmt.Errorf("provenance predicate: %w", err) @@ -209,7 +214,23 @@ func generateSLSAProvenanceStatement(rawPayload []byte, digest string, repo stri return "", fmt.Errorf("unmarshal Provenance predicate: %w", err) } return in_toto.ProvenanceStatement{ - StatementHeader: generateStatementHeader(digest, repo, slsa.PredicateSLSAProvenance), + StatementHeader: generateStatementHeader(digest, repo, slsa02.PredicateSLSAProvenance), + Predicate: predicate, + }, nil +} + +func generateSLSAProvenanceStatementSLSA1(rawPayload []byte, digest string, repo string) (interface{}, error) { + var predicate slsa1.ProvenancePredicate + err := checkRequiredJSONFields(rawPayload, reflect.TypeOf(predicate)) + if err != nil { + return nil, fmt.Errorf("provenance predicate: %w", err) + } + err = json.Unmarshal(rawPayload, &predicate) + if err != nil { + return "", fmt.Errorf("unmarshal Provenance predicate: %w", err) + } + return in_toto.ProvenanceStatementSLSA1{ + StatementHeader: generateStatementHeader(digest, repo, slsa1.PredicateSLSAProvenance), Predicate: predicate, }, nil }