diff --git a/cli/internal/verify/provenance.go b/cli/internal/verify/provenance.go index 49d01ce673..ac03143f06 100644 --- a/cli/internal/verify/provenance.go +++ b/cli/internal/verify/provenance.go @@ -2,15 +2,14 @@ package verify import ( "context" - "encoding/base64" "encoding/json" "errors" "fmt" + "io" + "net/http" "strings" + "time" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sigstore/sigstore-go/pkg/verify" ) @@ -23,21 +22,42 @@ const ( // Exported so selfupdate can reuse the same constant. DSSEPayloadType = "application/vnd.in-toto+json" - // slsaReferrerArtifactType is the OCI artifactType for SLSA provenance referrers. - // actions/attest-build-provenance sets this to the same value as the DSSE payload - // type; the match is intentional but these are semantically distinct OCI fields - // that could diverge in the future. - slsaReferrerArtifactType = "application/vnd.in-toto+json" + // defaultGitHubAPIBase is the base URL for the GitHub REST API. + defaultGitHubAPIBase = "https://api.github.com" + + // githubAttestationOwnerRepo is the GitHub owner/repo for attestation + // lookups. This is the canonical GitHub repository path (case-sensitive + // for the API), not derived from the image registry prefix. + githubAttestationOwnerRepo = "Aureliolo/synthorg" + + // attestationHTTPTimeout bounds individual HTTP requests to the GitHub API. + attestationHTTPTimeout = 30 * time.Second + + // maxAttestationResponseBytes caps the GitHub attestation API response + // to prevent memory exhaustion from malicious or oversized responses. + maxAttestationResponseBytes = 5 << 20 // 5MB ) // ErrNoProvenanceAttestations indicates that no SLSA provenance attestations -// were found for an image. This is distinct from a cryptographic verification -// failure — it means the image was published before provenance was configured. +// were found for an image via the GitHub attestation API. This is distinct +// from a cryptographic verification failure. var ErrNoProvenanceAttestations = errors.New("no SLSA provenance attestations found") -// VerifyProvenance fetches SLSA provenance attestations from the OCI registry -// (pushed as referrers by actions/attest-build-provenance) and verifies the -// DSSE envelope against the Sigstore transparency log and expected identity. +// githubAPIBase is the effective base URL for the GitHub REST API. +// Defaults to the production URL; tests override via setGitHubAPIBase. +var githubAPIBase = defaultGitHubAPIBase //nolint:gochecknoglobals // test override + +// attestationHTTPClient is a dedicated client for GitHub attestation API +// requests, isolated from http.DefaultClient to avoid side effects from +// other packages modifying global state. +var attestationHTTPClient = &http.Client{} //nolint:gochecknoglobals // package-scoped client + +// setGitHubAPIBase overrides the GitHub API base URL (for tests only). +func setGitHubAPIBase(base string) { githubAPIBase = base } + +// VerifyProvenance fetches SLSA provenance attestations from the GitHub +// attestation API and verifies the Sigstore bundle against the public +// transparency log and expected identity. // // The image ref must have a resolved Digest. func VerifyProvenance(ctx context.Context, ref ImageRef, sev *verify.Verifier, certID verify.CertificateIdentity) error { @@ -45,162 +65,95 @@ func VerifyProvenance(ctx context.Context, ref ImageRef, sev *verify.Verifier, c return fmt.Errorf("image digest not resolved") } - attestationDescs, err := findAttestations(ctx, ref) + bundles, err := fetchGitHubAttestations(ctx, ref.Digest) if err != nil { return err } - // Try each attestation — verify the first one that passes. + // Try each attestation bundle -- first successful verification wins. var errs []error - for i, desc := range attestationDescs { - if err := verifyAttestation(ctx, ref, desc, sev, certID); err != nil { + for i, bundleJSON := range bundles { + if err := verifyProvenanceBundle(bundleJSON, ref.Digest, sev, certID); err != nil { errs = append(errs, fmt.Errorf("attestation[%d]: %w", i, err)) continue } - return nil // verified successfully + return nil } return fmt.Errorf("no valid SLSA provenance attestation for %s: %w", ref, errors.Join(errs...)) } -// findAttestations queries OCI referrers and returns descriptors for in-toto -// attestation artifacts associated with the given image. -func findAttestations(ctx context.Context, ref ImageRef) ([]v1.Descriptor, error) { - digestRef := fmt.Sprintf("%s/%s@%s", ref.Registry, ref.Repository, ref.Digest) - parsed, err := name.NewDigest(digestRef) - if err != nil { - return nil, fmt.Errorf("parsing digest reference %q: %w", digestRef, err) - } - - referrerIdx, err := remote.Referrers(parsed, remote.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("querying referrers for %s: %w", ref, err) - } - - manifest, err := referrerIdx.IndexManifest() - if err != nil { - return nil, fmt.Errorf("reading referrer index manifest: %w", err) - } - - var descs []v1.Descriptor - for _, desc := range manifest.Manifests { - if desc.ArtifactType == slsaReferrerArtifactType { - descs = append(descs, desc) - } - } - if len(descs) == 0 { - return nil, fmt.Errorf("%w for %s", ErrNoProvenanceAttestations, ref) - } - return descs, nil +// githubAttestation represents a single attestation entry in the GitHub API response. +type githubAttestation struct { + Bundle json.RawMessage `json:"bundle"` } -// verifyAttestation fetches and verifies a single attestation manifest. -func verifyAttestation(ctx context.Context, ref ImageRef, desc v1.Descriptor, sev *verify.Verifier, certID verify.CertificateIdentity) error { - img, err := fetchAttestationImage(ctx, ref, desc) - if err != nil { - return err - } - - envelope, err := extractDSSEEnvelope(img) - if err != nil { - return err - } - - if err := validateSLSAPredicate(envelope); err != nil { - return err - } - - return verifyAttestationBundle(img, ref.Digest, sev, certID) +// githubAttestationResponse is the structure returned by the GitHub +// attestation API (GET /repos/OWNER/REPO/attestations/SUBJECT_DIGEST). +type githubAttestationResponse struct { + Attestations []githubAttestation `json:"attestations"` } -// fetchAttestationImage fetches the attestation image by its descriptor digest. -func fetchAttestationImage(ctx context.Context, ref ImageRef, desc v1.Descriptor) (v1.Image, error) { - attestRef := fmt.Sprintf("%s/%s@%s", ref.Registry, ref.Repository, desc.Digest.String()) - parsed, err := name.NewDigest(attestRef) - if err != nil { - return nil, fmt.Errorf("parsing attestation reference: %w", err) - } +// fetchGitHubAttestations queries the GitHub attestation API for Sigstore +// bundles associated with the given image digest. +func fetchGitHubAttestations(ctx context.Context, digest string) ([]json.RawMessage, error) { + url := fmt.Sprintf("%s/repos/%s/attestations/%s", githubAPIBase, githubAttestationOwnerRepo, digest) - img, err := remote.Image(parsed, remote.WithContext(ctx)) - if err != nil { - return nil, fmt.Errorf("fetching attestation image: %w", err) - } - return img, nil -} + reqCtx, cancel := context.WithTimeout(ctx, attestationHTTPTimeout) + defer cancel() -// extractDSSEEnvelope reads and parses the DSSE envelope from the first layer -// of an attestation image. -func extractDSSEEnvelope(img v1.Image) (dsseEnvelope, error) { - layers, err := img.Layers() + req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil) if err != nil { - return dsseEnvelope{}, fmt.Errorf("reading attestation layers: %w", err) - } - if len(layers) == 0 { - return dsseEnvelope{}, fmt.Errorf("attestation has no layers") + return nil, fmt.Errorf("creating attestation request: %w", err) } + req.Header.Set("Accept", "application/json") - layerReader, err := layers[0].Uncompressed() + resp, err := attestationHTTPClient.Do(req) if err != nil { - return dsseEnvelope{}, fmt.Errorf("reading attestation layer: %w", err) + return nil, fmt.Errorf("fetching attestations from GitHub API: %w", err) } - defer func() { _ = layerReader.Close() }() + defer func() { _ = resp.Body.Close() }() - var envelope dsseEnvelope - if err := json.NewDecoder(layerReader).Decode(&envelope); err != nil { - return dsseEnvelope{}, fmt.Errorf("decoding DSSE envelope: %w", err) + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("%w via GitHub API for digest %s", ErrNoProvenanceAttestations, digest) } - return envelope, nil -} - -// validateSLSAPredicate checks that a DSSE envelope contains a SLSA provenance -// predicate type. -func validateSLSAPredicate(envelope dsseEnvelope) error { - if envelope.PayloadType != DSSEPayloadType { - return fmt.Errorf("unexpected DSSE payload type %q, want %q", envelope.PayloadType, DSSEPayloadType) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GitHub attestation API returned HTTP %d for digest %s", resp.StatusCode, digest) } - payloadBytes, err := base64.StdEncoding.DecodeString(envelope.Payload) + body, err := io.ReadAll(io.LimitReader(resp.Body, maxAttestationResponseBytes+1)) if err != nil { - return fmt.Errorf("decoding DSSE payload: %w", err) - } - - var statement inTotoStatement - if err := json.Unmarshal(payloadBytes, &statement); err != nil { - return fmt.Errorf("parsing in-toto statement: %w", err) + return nil, fmt.Errorf("reading attestation response: %w", err) } - if !strings.HasPrefix(statement.PredicateType, SLSAProvenancePredicatePrefix) { - return fmt.Errorf("unexpected predicate type %q, want prefix %q", statement.PredicateType, SLSAProvenancePredicatePrefix) + if int64(len(body)) > maxAttestationResponseBytes { + return nil, fmt.Errorf("attestation response too large (>%d bytes)", maxAttestationResponseBytes) } - return nil -} -// verifyAttestationBundle looks for a Sigstore bundle in the attestation -// manifest or layer annotations and verifies it. Returns an error if no -// bundle is found — structural validation alone is insufficient. -func verifyAttestationBundle(img v1.Image, digest string, sev *verify.Verifier, certID verify.CertificateIdentity) error { - attestManifest, err := img.Manifest() - if err != nil { - return fmt.Errorf("reading attestation manifest: %w", err) + var apiResp githubAttestationResponse + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, fmt.Errorf("parsing attestation response: %w", err) } - // Look for Sigstore bundle in manifest annotations. - if bundleJSON, ok := attestManifest.Annotations[cosignBundleAnnotation]; ok { - return verifyProvenanceBundleWith([]byte(bundleJSON), digest, sev, certID) + if len(apiResp.Attestations) == 0 { + return nil, fmt.Errorf("%w via GitHub API for digest %s", ErrNoProvenanceAttestations, digest) } - // Also check layer annotations — iterate all layers, not just the first, - // since cosign may store the bundle in any layer's annotations. - for i := range attestManifest.Layers { - if bundleJSON, ok := attestManifest.Layers[i].Annotations[cosignBundleAnnotation]; ok { - return verifyProvenanceBundleWith([]byte(bundleJSON), digest, sev, certID) + bundles := make([]json.RawMessage, 0, len(apiResp.Attestations)) + for _, a := range apiResp.Attestations { + if len(a.Bundle) > 0 { + bundles = append(bundles, a.Bundle) } } - - return fmt.Errorf("no sigstore bundle found in attestation — cannot cryptographically verify provenance") + if len(bundles) == 0 { + return nil, fmt.Errorf("%w (no bundles in response) for digest %s", ErrNoProvenanceAttestations, digest) + } + return bundles, nil } -// verifyProvenanceBundleWith verifies a Sigstore bundle from an attestation -// using the provided verifier and identity policy. -func verifyProvenanceBundleWith(bundleJSON []byte, digest string, sev *verify.Verifier, certID verify.CertificateIdentity) error { +// verifyProvenanceBundle parses and verifies a single Sigstore bundle from +// the GitHub attestation API against the expected identity and image digest. +// After cryptographic verification, it also checks that the in-toto statement +// has a SLSA provenance predicate type (not an SBOM or other attestation). +func verifyProvenanceBundle(bundleJSON json.RawMessage, digest string, sev *verify.Verifier, certID verify.CertificateIdentity) error { b, err := loadBundle(bundleJSON) if err != nil { return fmt.Errorf("parsing provenance bundle: %w", err) @@ -211,7 +164,9 @@ func verifyProvenanceBundleWith(bundleJSON []byte, digest string, sev *verify.Ve return err } - _, err = sev.Verify(b, verify.NewPolicy( + // Verify the bundle cryptographically: check the signature, certificate + // identity (must be our docker.yml workflow), and artifact digest. + result, err := sev.Verify(b, verify.NewPolicy( verify.WithArtifactDigest(digestAlgo, digestHex), verify.WithCertificateIdentity(certID), )) @@ -219,16 +174,16 @@ func verifyProvenanceBundleWith(bundleJSON []byte, digest string, sev *verify.Ve return fmt.Errorf("provenance bundle verification failed: %w", err) } - return nil -} - -// dsseEnvelope is a minimal DSSE (Dead Simple Signing Envelope) structure. -type dsseEnvelope struct { - PayloadType string `json:"payloadType"` - Payload string `json:"payload"` -} + // sigstore-go does not validate the in-toto predicate type -- we must + // check it ourselves. Without this, an SBOM or other attestation signed + // by the same workflow identity would incorrectly pass as SLSA provenance. + if result == nil || result.Statement == nil { + return fmt.Errorf("verification succeeded but returned no statement for predicate check") + } + pt := result.Statement.PredicateType + if !strings.HasPrefix(pt, SLSAProvenancePredicatePrefix) { + return fmt.Errorf("unexpected predicate type %q, want prefix %q", pt, SLSAProvenancePredicatePrefix) + } -// inTotoStatement is a minimal in-toto statement for predicate type extraction. -type inTotoStatement struct { - PredicateType string `json:"predicateType"` + return nil } diff --git a/cli/internal/verify/provenance_test.go b/cli/internal/verify/provenance_test.go index 1cb670bdbb..4ad1194933 100644 --- a/cli/internal/verify/provenance_test.go +++ b/cli/internal/verify/provenance_test.go @@ -2,8 +2,8 @@ package verify import ( "context" - "encoding/base64" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -28,184 +28,124 @@ func TestVerifyProvenanceEmptyDigest(t *testing.T) { } } -func TestVerifyProvenanceNoReferrers(t *testing.T) { - repo := "test/image" - emptyIndex := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[]}` - +func TestFetchGitHubAttestationsNotFound(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.URL.Path == "/v2/": - w.WriteHeader(http.StatusOK) - case strings.Contains(r.URL.Path, "/referrers/"): - w.Header().Set("Content-Type", "application/vnd.oci.image.index.v1+json") - _, _ = w.Write([]byte(emptyIndex)) - default: - w.WriteHeader(http.StatusNotFound) - } + w.WriteHeader(http.StatusNotFound) })) defer srv.Close() - host := strings.TrimPrefix(srv.URL, "http://") - ref := ImageRef{ - Registry: host, - Repository: repo, - Tag: "1.0.0", - Digest: testDigest, - } + // Temporarily override the API base for testing. + origBase := githubAPIBase + defer func() { setGitHubAPIBase(origBase) }() + setGitHubAPIBase(srv.URL) - err := VerifyProvenance(context.Background(), ref, nil, sigverify.CertificateIdentity{}) + _, err := fetchGitHubAttestations(context.Background(), testDigest) if err == nil { - t.Fatal("expected error when no attestations found") + t.Fatal("expected error for 404 response") } - if !strings.Contains(err.Error(), "no SLSA provenance attestations") { - t.Errorf("unexpected error: %v", err) + if !errors.Is(err, ErrNoProvenanceAttestations) { + t.Errorf("expected ErrNoProvenanceAttestations, got: %v", err) } } -func TestValidateSLSAPredicate(t *testing.T) { - tests := []struct { - name string - predicateType string - payloadType string - wantErr bool - errContains string - }{ - { - name: "valid SLSA v1", - predicateType: "https://slsa.dev/provenance/v1", - payloadType: DSSEPayloadType, - }, - { - name: "wrong predicate type", - predicateType: "https://example.com/wrong-type", - payloadType: DSSEPayloadType, - wantErr: true, - errContains: "unexpected predicate type", - }, - { - name: "wrong payload type", - predicateType: "https://slsa.dev/provenance/v1", - payloadType: "application/octet-stream", - wantErr: true, - errContains: "unexpected DSSE payload type", - }, - } +func TestFetchGitHubAttestationsEmptyResponse(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"attestations": []}`)) + })) + defer srv.Close() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - statement := inTotoStatement{PredicateType: tt.predicateType} - statementJSON, err := json.Marshal(statement) - if err != nil { - t.Fatalf("failed to marshal statement: %v", err) - } - - envelope := dsseEnvelope{ - PayloadType: tt.payloadType, - Payload: base64.StdEncoding.EncodeToString(statementJSON), - } - - err = validateSLSAPredicate(envelope) - if (err != nil) != tt.wantErr { - t.Fatalf("validateSLSAPredicate() error = %v, wantErr %v", err, tt.wantErr) - } - if tt.errContains != "" && err != nil { - if !strings.Contains(err.Error(), tt.errContains) { - t.Errorf("error %q should contain %q", err.Error(), tt.errContains) - } - } - }) + origBase := githubAPIBase + defer func() { setGitHubAPIBase(origBase) }() + setGitHubAPIBase(srv.URL) + + _, err := fetchGitHubAttestations(context.Background(), testDigest) + if err == nil { + t.Fatal("expected error for empty attestations") + } + if !errors.Is(err, ErrNoProvenanceAttestations) { + t.Errorf("expected ErrNoProvenanceAttestations, got: %v", err) } } -// TestVerifyProvenanceInvalidAttestation verifies that VerifyProvenance rejects -// an attestation with an invalid structure. The mock attestation has a non-SLSA -// predicate and no Sigstore bundle, so VerifyProvenance may fail at either -// validateSLSAPredicate or verifyAttestationBundle — the test exercises the -// general invalid-attestation error path, not a specific validation step. -func TestVerifyProvenanceInvalidAttestation(t *testing.T) { - repo := "test/image" - attestDigest := "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - - statement := inTotoStatement{ - PredicateType: "https://example.com/not-slsa", +func TestFetchGitHubAttestationsSuccess(t *testing.T) { + bundle := json.RawMessage(`{"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json"}`) + resp := githubAttestationResponse{ + Attestations: []githubAttestation{ + {Bundle: bundle}, + }, } - statementJSON, err := json.Marshal(statement) + respJSON, err := json.Marshal(resp) if err != nil { - t.Fatalf("failed to marshal statement: %v", err) - } - envelope := dsseEnvelope{ - PayloadType: DSSEPayloadType, - Payload: base64.StdEncoding.EncodeToString(statementJSON), + t.Fatalf("marshaling response: %v", err) } - envelopeJSON, err := json.Marshal(envelope) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the URL path contains the digest. + if !strings.Contains(r.URL.Path, testDigest) { + t.Errorf("expected digest in URL path, got: %s", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(respJSON) + })) + defer srv.Close() + + origBase := githubAPIBase + defer func() { setGitHubAPIBase(origBase) }() + setGitHubAPIBase(srv.URL) + + bundles, err := fetchGitHubAttestations(context.Background(), testDigest) if err != nil { - t.Fatalf("failed to marshal envelope: %v", err) + t.Fatalf("unexpected error: %v", err) + } + if len(bundles) != 1 { + t.Fatalf("expected 1 bundle, got %d", len(bundles)) } +} - attestManifest := ociManifest{ - SchemaVersion: 2, - MediaType: "application/vnd.oci.image.manifest.v1+json", - Config: ociDescriptor{ - MediaType: "application/vnd.oci.image.config.v1+json", - Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - Size: 2, - }, - Layers: []ociLayerDescriptor{ - { - MediaType: "application/vnd.in-toto+json", - Digest: "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - Size: len(envelopeJSON), - }, - }, +func TestFetchGitHubAttestationsServerError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer srv.Close() + + origBase := githubAPIBase + defer func() { setGitHubAPIBase(origBase) }() + setGitHubAPIBase(srv.URL) + + _, err := fetchGitHubAttestations(context.Background(), testDigest) + if err == nil { + t.Fatal("expected error for 500 response") } - attestManifestJSON, err := json.Marshal(attestManifest) - if err != nil { - t.Fatalf("failed to marshal attestation manifest: %v", err) + if !strings.Contains(err.Error(), fmt.Sprintf("HTTP %d", http.StatusInternalServerError)) { + t.Errorf("expected HTTP status in error, got: %v", err) } +} - referrerIndex := fmt.Sprintf(`{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.index.v1+json", - "manifests": [{ - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "digest": "%s", - "size": %d, - "artifactType": "application/vnd.in-toto+json" - }] - }`, attestDigest, len(attestManifestJSON)) +func TestVerifyProvenanceInvalidBundle(t *testing.T) { + // Mock GitHub API returning an invalid bundle. + resp := `{"attestations": [{"bundle": {"invalid": "not a sigstore bundle"}}]}` srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch { - case r.URL.Path == "/v2/": - w.WriteHeader(http.StatusOK) - case strings.Contains(r.URL.Path, "/referrers/"): - w.Header().Set("Content-Type", "application/vnd.oci.image.index.v1+json") - _, _ = w.Write([]byte(referrerIndex)) - case r.URL.Path == fmt.Sprintf("/v2/%s/manifests/%s", repo, attestDigest): - w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json") - w.Header().Set("Docker-Content-Digest", attestDigest) - _, _ = w.Write(attestManifestJSON) - case strings.Contains(r.URL.Path, "/blobs/sha256:bbbb"): - _, _ = w.Write([]byte("{}")) - case strings.Contains(r.URL.Path, "/blobs/sha256:cccc"): - _, _ = w.Write(envelopeJSON) - default: - w.WriteHeader(http.StatusNotFound) - } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(resp)) })) defer srv.Close() - host := strings.TrimPrefix(srv.URL, "http://") + origBase := githubAPIBase + defer func() { setGitHubAPIBase(origBase) }() + setGitHubAPIBase(srv.URL) + ref := ImageRef{ - Registry: host, - Repository: repo, + Registry: "ghcr.io", + Repository: "test/image", Tag: "1.0.0", Digest: testDigest, } - err = VerifyProvenance(context.Background(), ref, nil, sigverify.CertificateIdentity{}) + err := VerifyProvenance(context.Background(), ref, nil, sigverify.CertificateIdentity{}) if err == nil { - t.Fatal("expected error for invalid attestation") + t.Fatal("expected error for invalid bundle") } if !strings.Contains(err.Error(), "no valid SLSA provenance attestation") { t.Errorf("expected provenance attestation error, got: %v", err) diff --git a/cli/internal/verify/verify.go b/cli/internal/verify/verify.go index 3cc51a3531..2f7a7d967d 100644 --- a/cli/internal/verify/verify.go +++ b/cli/internal/verify/verify.go @@ -2,7 +2,6 @@ package verify import ( "context" - "errors" "fmt" "io" "regexp" @@ -162,33 +161,17 @@ func verifyOneImage(ctx context.Context, ref ImageRef, sev *verify.Verifier, cer } _, _ = fmt.Fprintf(w, " Cosign signature: verified\n") - // Step 3: Verify SLSA provenance. - // Missing attestations are warn-only (pre-SLSA images may not have them). - // Cryptographic failures (tampered attestation, wrong identity) are hard errors. - provenanceVerified := true + // Step 3: Verify SLSA provenance via GitHub attestation API. + // Both missing attestations and cryptographic failures are hard errors. if err := VerifyProvenance(ctx, ref, sev, certID); err != nil { - if isProvenanceMissing(err) { - provenanceVerified = false - _, _ = fmt.Fprintf(w, " SLSA provenance: not available (warn)\n") - } else { - return VerifyResult{}, fmt.Errorf("SLSA provenance verification: %w", err) - } - } else { - _, _ = fmt.Fprintf(w, " SLSA provenance: verified\n") + return VerifyResult{}, fmt.Errorf("SLSA provenance: %w", err) } + _, _ = fmt.Fprintf(w, " SLSA provenance: verified\n") _, _ = fmt.Fprintf(w, " %s: OK\n", ref.Name()) return VerifyResult{ Ref: ref, - ProvenanceVerified: provenanceVerified, + ProvenanceVerified: true, }, nil } - -// isProvenanceMissing returns true when the provenance error indicates that -// attestations are absent (the image was published before SLSA provenance was -// configured), as opposed to a cryptographic or structural verification -// failure that indicates tampering. -func isProvenanceMissing(err error) bool { - return errors.Is(err, ErrNoProvenanceAttestations) -}