diff --git a/cmd/sigstore/initialize.go b/cmd/sigstore/initialize.go index e3a74aa44..cb2326fb0 100644 --- a/cmd/sigstore/initialize.go +++ b/cmd/sigstore/initialize.go @@ -47,22 +47,18 @@ func sigstoreInitializeCmd(f sigstoreInitializeFunc) *cobra.Command { Any updated TUF repository will be written to $HOME/.sigstore/root/. Trusted keys and certificate used in ec verification (e.g. verifying Fulcio issued certificates - with Fulcio root CA) are pulled form the trusted metadata. - - This command is mostly a wrapper around "cosign initialize". + with Fulcio root CA) are pulled from the trusted metadata. `), Example: hd.Doc(` - ec initialize -mirror -out - Initialize root with distributed root keys, default mirror, and default out path. - ec initialize + ec sigstore initialize Initialize with an out-of-band root key file, using the default mirror. - ec initialize -root + ec sigstore initialize --root Initialize with an out-of-band root key file and custom repository mirror. - ec initialize -mirror -root + ec sigstore initialize --mirror --root `), Args: cobra.NoArgs, diff --git a/cmd/validate/image.go b/cmd/validate/image.go index e635c23df..f249e3185 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -116,7 +116,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { ec validate image --image registry/name:tag - Return a zero status code even if there are validation failures: + Return a zero status code even if there are validation failures: ec validate image --image registry/name:tag --strict=false @@ -502,7 +502,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { "URL of the certificate OIDC issuer for keyless verification") cmd.Flags().StringVar(&data.certificateOIDCIssuerRegExp, "certificate-oidc-issuer-regexp", data.certificateOIDCIssuerRegExp, - "Regular expresssion for the URL of the certificate OIDC issuer for keyless verification") + "Regular expression for the URL of the certificate OIDC issuer for keyless verification") // Deprecated: images replaced this cmd.Flags().StringVarP(&data.filePath, "file-path", "f", data.filePath, diff --git a/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc b/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc index 81e44fa3a..44a613a1b 100644 --- a/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc +++ b/docs/modules/ROOT/pages/ec_sigstore_initialize.adoc @@ -16,9 +16,7 @@ URL reference. This will enable you to point ec to a separate TUF root. Any updated TUF repository will be written to $HOME/.sigstore/root/. Trusted keys and certificate used in ec verification (e.g. verifying Fulcio issued certificates -with Fulcio root CA) are pulled form the trusted metadata. - -This command is mostly a wrapper around "cosign initialize". +with Fulcio root CA) are pulled from the trusted metadata. [source,shell] ---- @@ -26,16 +24,14 @@ ec sigstore initialize [flags] ---- == Examples -ec initialize -mirror -out - Initialize root with distributed root keys, default mirror, and default out path. -ec initialize +ec sigstore initialize Initialize with an out-of-band root key file, using the default mirror. -ec initialize -root +ec sigstore initialize --root Initialize with an out-of-band root key file and custom repository mirror. -ec initialize -mirror -root +ec sigstore initialize --mirror --root == Options diff --git a/docs/modules/ROOT/pages/ec_validate_image.adoc b/docs/modules/ROOT/pages/ec_validate_image.adoc index b44e9446c..7a11812f4 100644 --- a/docs/modules/ROOT/pages/ec_validate_image.adoc +++ b/docs/modules/ROOT/pages/ec_validate_image.adoc @@ -56,7 +56,7 @@ Return a non-zero status code on validation failure: ec validate image --image registry/name:tag - Return a zero status code even if there are validation failures: +Return a zero status code even if there are validation failures: ec validate image --image registry/name:tag --strict=false @@ -115,7 +115,7 @@ Use a regular expression to match certificate attributes. --certificate-identity:: URL of the certificate identity for keyless verification --certificate-identity-regexp:: Regular expression for the URL of the certificate identity for keyless verification --certificate-oidc-issuer:: URL of the certificate OIDC issuer for keyless verification ---certificate-oidc-issuer-regexp:: Regular expresssion for the URL of the certificate OIDC issuer for keyless verification +--certificate-oidc-issuer-regexp:: Regular expression for the URL of the certificate OIDC issuer for keyless verification --color:: Enable color when using text output even when the current terminal does not support it (Default: false) --effective-time:: Run policy checks with the provided time. Useful for testing rules with effective dates in the future. The value can be "now" (default) - for diff --git a/internal/attestation/attestation.go b/internal/attestation/attestation.go index 00e063a8c..61fbeac6a 100644 --- a/internal/attestation/attestation.go +++ b/internal/attestation/attestation.go @@ -143,6 +143,32 @@ func ProvenanceFromSignature(sig oci.Signature) (Attestation, error) { return provenance{statement: statement, data: embedded, signatures: signatures}, nil } +// ProvenanceFromBundlePayload parses an attestation from a raw DSSE envelope +// JSON payload as returned by the Sigstore bundle verification path. +func ProvenanceFromBundlePayload(dsseJSON []byte) (Attestation, error) { + var payload cosign.AttestationPayload + if err := json.Unmarshal(dsseJSON, &payload); err != nil { + return nil, fmt.Errorf("malformed bundle attestation: %w", err) + } + + if payload.PayLoad == "" { + return nil, errors.New("no `payload` data found in bundle attestation") + } + + embedded, err := decodedPayload(payload) + if err != nil { + return nil, err + } + + //nolint:staticcheck + var statement in_toto.Statement + if err := json.Unmarshal(embedded, &statement); err != nil { + return nil, fmt.Errorf("malformed bundle attestation: %w", err) + } + + return provenance{statement: statement, data: embedded}, nil +} + type provenance struct { //nolint:staticcheck statement in_toto.Statement diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index f6d414dea..73b2e666b 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -31,6 +31,8 @@ import ( app "github.com/konflux-ci/application-api/api/v1alpha1" "github.com/santhosh-tekuri/jsonschema/v5" "github.com/sigstore/cosign/v3/pkg/cosign" + cosignOCI "github.com/sigstore/cosign/v3/pkg/oci" + ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" log "github.com/sirupsen/logrus" "github.com/spf13/afero" @@ -120,6 +122,12 @@ func (a *ApplicationSnapshotImage) SetImageURL(url string) error { return nil } +func (a *ApplicationSnapshotImage) hasBundles(ctx context.Context) bool { + regOpts := []ociremote.Option{ociremote.WithRemoteOptions(oci.CreateRemoteOptions(ctx)...)} + bundles, _, err := cosign.GetBundles(ctx, a.reference, regOpts) + return err == nil && len(bundles) > 0 +} + func (a *ApplicationSnapshotImage) FetchImageConfig(ctx context.Context) error { var err error a.configJSON, err = config.FetchImageConfig(ctx, a.reference) @@ -143,38 +151,58 @@ func (a *ApplicationSnapshotImage) FetchImageFiles(ctx context.Context) error { return err } -// ValidateImageSignature executes the cosign.VerifyImageSignature method on the ApplicationSnapshotImage image ref. +// ValidateImageSignature verifies the image signature. For images with Sigstore +// bundles (OCI referrers) the new bundle path is used; otherwise the legacy +// tag-based path is used. func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) error { - // Set the ClaimVerifier on a shallow *copy* of CheckOpts to avoid unexpected side-effects opts := a.checkOpts - opts.ClaimVerifier = cosign.SimpleClaimVerifier - signatures, _, err := oci.NewClient(ctx).VerifyImageSignatures(a.reference, &opts) + client := oci.NewClient(ctx) + + var sigs []cosignOCI.Signature + var err error + + if a.hasBundles(ctx) { + opts.NewBundleFormat = true + opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier + sigs, _, err = client.VerifyImageAttestations(a.reference, &opts) + } else { + opts.ClaimVerifier = cosign.SimpleClaimVerifier + sigs, _, err = client.VerifyImageSignatures(a.reference, &opts) + } if err != nil { return err } - for _, s := range signatures { + for _, s := range sigs { es, err := signature.NewEntitySignature(s) if err != nil { return err } a.signatures = append(a.signatures, es) } - return nil } -// ValidateAttestationSignature executes the cosign.VerifyImageAttestations method +// ValidateAttestationSignature verifies and collects in-toto attestations +// attached to the image. func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Context) error { - // Set the ClaimVerifier on a shallow *copy* of CheckOpts to avoid unexpected side-effects opts := a.checkOpts opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier + useBundles := a.hasBundles(ctx) + if useBundles { + opts.NewBundleFormat = true + } + layers, _, err := oci.NewClient(ctx).VerifyImageAttestations(a.reference, &opts) if err != nil { return err } + if useBundles { + return a.parseAttestationsFromBundles(layers) + } + // Extract the signatures from the attestations here in order to also validate that // the signatures do exist in the expected format. for _, sig := range layers { @@ -220,6 +248,40 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont return nil } +// parseAttestationsFromBundles extracts attestations from Sigstore bundles. +// Bundle-wrapped layers report an incorrect media type, so we unmarshal the +// DSSE envelope from the raw payload directly. +func (a *ApplicationSnapshotImage) parseAttestationsFromBundles(layers []cosignOCI.Signature) error { + for _, sig := range layers { + payload, err := sig.Payload() + if err != nil { + log.Debugf("Skipping bundle entry: cannot read payload: %v", err) + continue + } + var dsseEnvelope struct { + PayloadType string `json:"payloadType"` + Payload string `json:"payload"` + } + if err := json.Unmarshal(payload, &dsseEnvelope); err != nil { + log.Debugf("Skipping bundle entry: not a valid DSSE envelope: %v", err) + continue + } + if dsseEnvelope.PayloadType != "application/vnd.in-toto+json" { + log.Debugf("Skipping bundle entry with payloadType: %s", dsseEnvelope.PayloadType) + continue + } + + att, err := attestation.ProvenanceFromBundlePayload(payload) + if err != nil { + return fmt.Errorf("unable to parse bundle attestation: %w", err) + } + t := att.PredicateType() + log.Debugf("Found bundle attestation with predicateType: %s", t) + a.attestations = append(a.attestations, att) + } + return nil +} + // ValidateAttestationSyntax validates the attestations against known JSON // schemas, errors out if there are no attestations to check to prevent // successful syntax check of no inputs, must invoke diff --git a/internal/utils/oci/client.go b/internal/utils/oci/client.go index 8f72a7c1b..5e8a7b7d0 100644 --- a/internal/utils/oci/client.go +++ b/internal/utils/oci/client.go @@ -65,7 +65,7 @@ func initCache() cache.Cache { } } -func createRemoteOptions(ctx context.Context) []remote.Option { +func CreateRemoteOptions(ctx context.Context) []remote.Option { backoff := remote.Backoff{ Duration: echttp.DefaultBackoff.Duration, Factor: echttp.DefaultBackoff.Factor, @@ -123,7 +123,7 @@ func NewClient(ctx context.Context, opts ...remote.Option) Client { o := opts if len(opts) == 0 { - o = createRemoteOptions(ctx) + o = CreateRemoteOptions(ctx) } return &defaultClient{ctx, o}