From 767ecf9e0a63f5b7699d023609bd01978ff00d75 Mon Sep 17 00:00:00 2001 From: Ramon Petgrave <32398091+ramonpetgrave64@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:31:43 +0700 Subject: [PATCH] feat: handle dssev001 tlog entry types (#799) re: https://github.com/slsa-framework/slsa-github-generator/issues/3750 Rekor TLog entries can now be of the type dsse v0.0.1, as when what's returned when using sigstore-go's `Bundle()`. This is to support eventual Sigstore Bundles produced by slsa-github-generator's "generic" generator, which will likely use sigstore-go's Bundle to produce attestations - https://github.com/slsa-framework/slsa-github-generator/compare/main...ramonpetgrave64-internal-builder-sigstore-bundlev2#diff-b186a0c5d9ae459b11b694f05455568453699670926d21cad06cafec3dbf895eR101 - https://github.com/slsa-framework/slsa-github-generator/actions/runs/10359750833 ## Tesing - Added unit tests with stub data - manual invocations to very both new and old attestations and bundles, with some modifications for testing purposes - https://github.com/slsa-framework/slsa-verifier/compare/main...verify-sigstore-go-Bundlev3#diff-94741068472ee694a12811cd704179dd478a9fa20a3bf45cf6ea2d4406214dc2R179 ## Followup Finish the work to produce bundles from the generic generators - https://github.com/slsa-framework/slsa-github-generator/compare/main...ramonpetgrave64-internal-builder-sigstore-bundlev2#diff-b186a0c5d9ae459b11b694f05455568453699670926d21cad06cafec3dbf895eR101 --------- Signed-off-by: Ramon Petgrave Signed-off-by: Ramon Petgrave <32398091+ramonpetgrave64@users.noreply.github.com> --- verifiers/internal/gha/bundle.go | 83 ++++++-- verifiers/internal/gha/bundle_test.go | 260 ++++++++++++++++++++++++++ 2 files changed, 327 insertions(+), 16 deletions(-) diff --git a/verifiers/internal/gha/bundle.go b/verifiers/internal/gha/bundle.go index 4b6ffec..ffb23b0 100644 --- a/verifiers/internal/gha/bundle.go +++ b/verifiers/internal/gha/bundle.go @@ -8,6 +8,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" bundle_v1 "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" @@ -20,7 +21,10 @@ import ( // Bundle specific errors. var ( ErrorMismatchSignature = errors.New("bundle tlog entry does not match signature") + ErrorUnequalSignatures = errors.New("bundle tlog entry and envelope have an unequal number of signatures") + ErrorNoSignatures = errors.New("envolope has no signatures") ErrorUnexpectedEntryType = errors.New("unexpected tlog entry type") + ErrorParsingEntryBody = errors.New("unexpected layout of the bundle tlog entry body") ErrorMissingCertInBundle = errors.New("missing signing certificate in bundle") ErrorUnexpectedBundleContent = errors.New("expected DSSE bundle content") ) @@ -110,29 +114,41 @@ func getLeafCertFromBundle(bundle *bundle_v1.Bundle) (*x509.Certificate, error) // DSSE envelope. It MUST verify that the signatures match to ensure that the // tlog timestamp attests to the signature creation time. func matchRekorEntryWithEnvelope(tlogEntry *v1.TransparencyLogEntry, env *dsselib.Envelope) error { - kindVersion := tlogEntry.GetKindVersion() - if kindVersion.Kind != "intoto" && - kindVersion.Version != "0.0.2" { - return fmt.Errorf("%w: expected intoto:0.0.2, got %s:%s", ErrorUnexpectedEntryType, - kindVersion.Kind, kindVersion.Version) + if len(env.Signatures) == 0 { + return ErrorNoSignatures } + kindVersion := tlogEntry.GetKindVersion() + + if kindVersion.Kind == "intoto" && kindVersion.Version == "0.0.2" { + return matchRekorEntryWithEnvelopeIntotov002(tlogEntry, env) + } + + if kindVersion.Kind == "dsse" && kindVersion.Version == "0.0.1" { + return matchRekorEntryWithEnvelopeDSSEv001(tlogEntry, env) + } + + return fmt.Errorf("%w: wanted either intoto v0.0.2 or dsse v0.0.1, got: %s %s", ErrorUnexpectedEntryType, kindVersion.Kind, kindVersion.Version) +} + +// matchRekorEntryWithEnvelopeDSSEv001 handles matchRekorEntryWithEnvelope for the intoto v0.0.1 type version. +func matchRekorEntryWithEnvelopeIntotov002(tlogEntry *v1.TransparencyLogEntry, env *dsselib.Envelope) error { canonicalBody := tlogEntry.GetCanonicalizedBody() var toto models.Intoto var intotoObj models.IntotoV002Schema if err := json.Unmarshal(canonicalBody, &toto); err != nil { - return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err) + return fmt.Errorf("%w: %s", ErrorParsingEntryBody, err) } specMarshal, err := json.Marshal(toto.Spec) if err != nil { - return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err) + return fmt.Errorf("%w: %s", ErrorParsingEntryBody, err) } if err := json.Unmarshal(specMarshal, &intotoObj); err != nil { - return fmt.Errorf("%w: %s", ErrorUnexpectedEntryType, err) + return fmt.Errorf("%w: %s", ErrorParsingEntryBody, err) } if len(env.Signatures) != len(intotoObj.Content.Envelope.Signatures) { - return fmt.Errorf("expected %d sigs in canonical body, got %d", + return fmt.Errorf("%w: wanted %d, got %d", ErrorUnequalSignatures, len(env.Signatures), len(intotoObj.Content.Envelope.Signatures)) } @@ -142,13 +158,12 @@ func matchRekorEntryWithEnvelope(tlogEntry *v1.TransparencyLogEntry, env *dsseli // The signature in the canonical body is double base64-encoded. encodedEnvSig := base64.StdEncoding.EncodeToString( []byte(sig.Sig)) - var matchCanonical bool - for _, canonicalSig := range intotoObj.Content.Envelope.Signatures { - if canonicalSig.Sig.String() == encodedEnvSig { - matchCanonical = true - } - } - if !matchCanonical { + if !slices.ContainsFunc( + intotoObj.Content.Envelope.Signatures, + func(canonicalSig *models.IntotoV002SchemaContentEnvelopeSignaturesItems0) bool { + return canonicalSig.Sig.String() == encodedEnvSig + }, + ) { return ErrorMismatchSignature } } @@ -156,6 +171,42 @@ func matchRekorEntryWithEnvelope(tlogEntry *v1.TransparencyLogEntry, env *dsseli return nil } +// matchRekorEntryWithEnvelopeDSSEv001 handles matchRekorEntryWithEnvelope for the dsse v0.0.1 type version. +func matchRekorEntryWithEnvelopeDSSEv001(tlogEntry *v1.TransparencyLogEntry, env *dsselib.Envelope) error { + canonicalBody := tlogEntry.GetCanonicalizedBody() + var dsseObj models.DSSE + if err := json.Unmarshal(canonicalBody, &dsseObj); err != nil { + return fmt.Errorf("%w: %s", ErrorParsingEntryBody, err) + } + var dsseSchemaObj models.DSSEV001Schema + + specMarshal, err := json.Marshal(dsseObj.Spec) + if err != nil { + return fmt.Errorf("%w: %s", ErrorParsingEntryBody, err) + } + if err := json.Unmarshal(specMarshal, &dsseSchemaObj); err != nil { + return fmt.Errorf("%w: %s", ErrorParsingEntryBody, err) + } + + if len(env.Signatures) != len(dsseSchemaObj.Signatures) { + return fmt.Errorf("%w: wanted %d, got %d", ErrorUnequalSignatures, + len(env.Signatures), + len(dsseSchemaObj.Signatures)) + } + // TODO(#487): verify the certs match. + for _, sig := range env.Signatures { + if !slices.ContainsFunc( + dsseSchemaObj.Signatures, + func(canonicalSig *models.DSSEV001SchemaSignaturesItems0) bool { + return *canonicalSig.Signature == sig.Sig + }, + ) { + return ErrorMismatchSignature + } + } + return nil +} + // VerifyProvenanceBundle verifies the DSSE envelope using the offline Rekor bundle and // returns the verified DSSE envelope containing the provenance // and the signing certificate given the provenance. diff --git a/verifiers/internal/gha/bundle_test.go b/verifiers/internal/gha/bundle_test.go index af610b6..af8f08a 100644 --- a/verifiers/internal/gha/bundle_test.go +++ b/verifiers/internal/gha/bundle_test.go @@ -2,12 +2,17 @@ package gha import ( "context" + "encoding/base64" "fmt" "os" "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + dsselib "github.com/secure-systems-lab/go-securesystemslib/dsse" + rekorpbv1 "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" serrors "github.com/slsa-framework/slsa-verifier/v2/errors" + "github.com/slsa-framework/slsa-verifier/v2/verifiers/utils" ) @@ -76,3 +81,258 @@ func Test_verifyBundle(t *testing.T) { }) } } + +func Test_matchRekorEntryWithEnvelope(t *testing.T) { + t.Parallel() + + goodDSSEV001Body := []byte(` + { + "apiVersion": "0.0.1", + "kind": "dsse", + "spec": { + "signatures": [ + { + "signature": "MEUCIHiah7zQLL9LK9m9/0JH3rHIaYlvcus4h84KOdaR3iAlAiEAio+tnbpkW+V+FPYxpuiJBY0MuD43RVX5QmMwk3sgnUE=" + } + ] + } + } + `) + goodDSSEV001Sig := "MEUCIHiah7zQLL9LK9m9/0JH3rHIaYlvcus4h84KOdaR3iAlAiEAio+tnbpkW+V+FPYxpuiJBY0MuD43RVX5QmMwk3sgnUE=" + goodIntotoV002Body := []byte(` + { + "apiVersion": "0.0.2", + "kind": "intoto", + "spec": { + "content": { + "envelope": { + "signatures": [ + { + "publicKey": "mypubkey", + "sig": "TUVVQ0lRRGVoVlQ0MFRLUDNxWnlVN3BDcXhwMzRyeGt1Wk1ZRTVBWGhBb0x4NTdzdmdJZ0xSa3NCV1hPamUyMCtrKzh4M3ViZkZzNlpxQ2dLNXc3eXlITFB6SC9tcGs9" + } + ] + } + } + } + } + `) + goodIntotoV001Sig := "MEUCIQDehVT40TKP3qZyU7pCqxp34rxkuZMYE5AXhAoLx57svgIgLRksBWXOje20+k+8x3ubfFs6ZqCgK5w7yyHLPzH/mpk=" + + tests := []struct { + name string + tlog *rekorpbv1.TransparencyLogEntry + env *dsselib.Envelope + err error + }{ + { + name: "failure: no signtures in envelope", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "intoto", + Version: "0.0.2", + }, + CanonicalizedBody: goodIntotoV002Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{}, + }, + err: ErrorNoSignatures, + }, + { + name: "success: dsse v0.0.1", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "dsse", + Version: "0.0.1", + }, + CanonicalizedBody: goodDSSEV001Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: goodDSSEV001Sig, + }, + }, + }, + err: nil, + }, + { + name: "success: intoto v0.0.2", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "intoto", + Version: "0.0.2", + }, + CanonicalizedBody: goodIntotoV002Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: goodIntotoV001Sig, + }, + }, + }, + err: nil, + }, + { + name: "faiulure: dsse v0.0.1: mismatch signatures", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "dsse", + Version: "0.0.1", + }, + CanonicalizedBody: goodDSSEV001Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + }, + }, + err: ErrorMismatchSignature, + }, + { + name: "faiulure: dsse v0.0.1: unequal number of signatures", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "dsse", + Version: "0.0.1", + }, + CanonicalizedBody: goodDSSEV001Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + { + Sig: base64.StdEncoding.EncodeToString([]byte("othersig")), + }, + }, + }, + err: ErrorUnequalSignatures, + }, + { + name: "faiulure: intoto v0.0.2: mismatch signatures", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "intoto", + Version: "0.0.2", + }, + CanonicalizedBody: goodIntotoV002Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + { + Sig: base64.StdEncoding.EncodeToString([]byte("othersig")), + }, + }, + }, + err: ErrorUnequalSignatures, + }, + { + name: "failure: unknown type", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "slsa", + Version: "0.0.x", + }, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + }, + }, + err: ErrorUnexpectedEntryType, + }, + { + name: "failure: unknown dsse type version", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "dsse", + Version: "0.0.x", + }, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + }, + }, + err: ErrorUnexpectedEntryType, + }, + { + name: "failure: unknown intoto type version", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "dsse", + Version: "0.0.x", + }, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + }, + }, + err: ErrorUnexpectedEntryType, + }, + { + name: "failure: parse error: dsse kind, intoto body", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "dsse", + Version: "0.0.1", + }, + CanonicalizedBody: goodIntotoV002Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + }, + }, + err: ErrorParsingEntryBody, + }, + { + name: "failure: parse error: intoto kind, dsse body", + tlog: &rekorpbv1.TransparencyLogEntry{ + KindVersion: &rekorpbv1.KindVersion{ + Kind: "intoto", + Version: "0.0.2", + }, + CanonicalizedBody: goodDSSEV001Body, + }, + env: &dsselib.Envelope{ + Signatures: []dsselib.Signature{ + { + Sig: base64.StdEncoding.EncodeToString([]byte("mysig")), + }, + }, + }, + err: ErrorParsingEntryBody, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := matchRekorEntryWithEnvelope(tt.tlog, tt.env) + + if errorDiff := cmp.Diff(tt.err, err, cmpopts.EquateErrors()); errorDiff != "" { + t.Errorf("unexpected error (-want +got):\n%s", errorDiff) + } + }) + } +}