diff --git a/README.md b/README.md index bbc5c5c0..3605b955 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Available Commands: Flags: -h, --help help for dependabot --proxy-image string container image to use for the proxy (default "ghcr.io/dependabot/proxy:latest") - --skip-image-verification skip updater and proxy image verification for trusted development images + --skip-image-verification skip updater, proxy, and configured OpenTelemetry collector image verification for trusted development images --updater-image string container image to use for the updater -v, --version version for dependabot @@ -61,6 +61,7 @@ Use "dependabot [command] --help" for more information about a command. ### Image verification The CLI requires valid GitHub build attestations for updater and proxy images before it starts any containers. +When Collector configuration is supplied, it also requires a valid OpenTelemetry Collector image signature. An attestation is a signed claim about an artifact. Provenance records an image's build origin. Bundles contain the signed claims and verification evidence. @@ -70,13 +71,24 @@ An image digest identifies its exact content. The CLI requires a valid signature and matching digest, issuer, repository, workflow identity, and source ref. It uses the verified digest for image pulls and container creation. -The accepted issuer is `https://token.actions.githubusercontent.com`, and the required source ref is `refs/heads/main`. +The accepted issuer is `https://token.actions.githubusercontent.com`. +Updater and proxy images require SLSA build provenance from `refs/heads/main`. +Collector images require Cosign image signatures from stable `refs/tags/vMAJOR.MINOR.PATCH` releases. +These signatures identify the image and publisher without describing the build. The publisher workflows are: | Image | Repository | Workflow | | --- | --- | --- | | Updater | `dependabot/dependabot-core` | `.github/workflows/images-latest.yml` | | Proxy | `dependabot/proxy` | `.github/workflows/ghcr.yml` | +| Collector | `open-telemetry/opentelemetry-collector-releases` | `.github/workflows/base-release.yaml` | + +For a Collector version such as `:0.161.0`, both the workflow identity and certificate source ref must name `refs/tags/v0.161.0`. +The version is still enforced when the reference also includes a digest. +For `latest`, other non-version tags, and digest-only references, any stable release from that same workflow is accepted. +The certificate source ref must still match the release in the workflow identity. +Prerelease and manually signed Collector images are not accepted. +Collector verification, pull, or startup failures stop the job. Image overrides must satisfy the same producer policy. Registry mirrors must preserve the image digest and attestation bundles. @@ -87,12 +99,12 @@ It does not switch to a newer registry tag. It still needs network access for attestations and Sigstore trust metadata. The CLI uses existing registry credentials, not updater-job credentials, to retrieve bundles. -Storage and OpenTelemetry collector images are not verified. +Storage images are not verified. The CLI records the signed producer commit, but does not enforce an independently approved commit or image freshness. Branch builds and images from other producers require the development bypass below. Only use `--skip-image-verification` with trusted development images. -This flag disables both updater and proxy verification and prints a warning. +This flag disables updater, proxy, and configured Collector verification and prints a warning. For example: ```sh @@ -102,7 +114,7 @@ dependabot update go_modules dependabot/cli \ ``` The Docker Action exposes the same bypass through its `skip-image-verification` input, which defaults to `'false'`. -Do not use this bypass to ignore unexpected provenance in production. +Do not use this bypass to ignore unexpected signatures or provenance in production. ### `dependabot update` diff --git a/action.yml b/action.yml index 4c0e02cf..f15ca690 100644 --- a/action.yml +++ b/action.yml @@ -24,7 +24,7 @@ inputs: description: Pull images before running; false uses images already on the runner default: 'true' skip-image-verification: - description: Skip updater and proxy image verification for trusted development images + description: Skip updater, proxy, and configured OpenTelemetry collector image verification for trusted development images default: 'false' timeout: description: Maximum execution duration, excluding API bootstrap (Go duration) diff --git a/cmd/dependabot/internal/cmd/root.go b/cmd/dependabot/internal/cmd/root.go index ec2accdc..1e811ad9 100644 --- a/cmd/dependabot/internal/cmd/root.go +++ b/cmd/dependabot/internal/cmd/root.go @@ -63,5 +63,5 @@ func init() { rootCmd.PersistentFlags().StringVar(&proxyImage, "proxy-image", infra.ProxyImageName, "container image to use for the proxy") rootCmd.PersistentFlags().StringVar(&collectorImage, "collector-image", infra.CollectorImageName, "container image to use for the OpenTelemetry collector") rootCmd.PersistentFlags().StringVar(&storageImage, "storage-image", infra.StorageImageName, "container image to use for the storage service") - rootCmd.PersistentFlags().BoolVar(&skipImageVerification, "skip-image-verification", false, "skip updater and proxy image verification for trusted development images") + rootCmd.PersistentFlags().BoolVar(&skipImageVerification, "skip-image-verification", false, "skip updater, proxy, and configured OpenTelemetry collector image verification for trusted development images") } diff --git a/docs/debugging.md b/docs/debugging.md index 138769ed..d80c88e6 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -10,7 +10,7 @@ Next, clone https://github.com/dependabot/dependabot-core. This project contains Use `--skip-image-verification` only with trusted development images. Locally built images do not carry the required publisher attestations. -This flag disables updater and proxy verification and prints a warning. +This flag disables updater, proxy, and configured OpenTelemetry Collector verification and prints a warning. Verification remains enabled in debug mode unless you set this flag. In the `dependabot-core` project directory, run: diff --git a/internal/infra/attestation.go b/internal/infra/attestation.go index 8854b345..e01272d9 100644 --- a/internal/infra/attestation.go +++ b/internal/infra/attestation.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "regexp" "strings" "github.com/google/go-containerregistry/pkg/name" @@ -21,18 +22,23 @@ import ( ) const ( - actionsIssuer = "https://token.actions.githubusercontent.com" - provenancePredicate = "https://slsa.dev/provenance/v1" - sigstoreBundleType = "application/vnd.dev.sigstore.bundle" - maxBundleBytes = 16 << 20 + actionsIssuer = "https://token.actions.githubusercontent.com" + provenancePredicate = "https://slsa.dev/provenance/v1" + imageSignaturePredicate = "https://sigstore.dev/cosign/sign/v1" + stableReleaseVersionPattern = `(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)` + stableReleaseRefPattern = `refs/tags/v` + stableReleaseVersionPattern + sigstoreBundleType = "application/vnd.dev.sigstore.bundle" + maxBundleBytes = 16 << 20 ) type imageRole struct { - name string - repository string - workflow string - issuer string - sourceRef string + name string + repository string + workflow string + issuer string + sourceRef string + predicate string + releaseWorkflow bool } var ( @@ -42,6 +48,7 @@ var ( workflow: "https://github.com/dependabot/dependabot-core/.github/workflows/images-latest.yml@refs/heads/main", issuer: actionsIssuer, sourceRef: "refs/heads/main", + predicate: provenancePredicate, } proxyImageRole = imageRole{ name: "proxy", @@ -49,9 +56,46 @@ var ( workflow: "https://github.com/dependabot/proxy/.github/workflows/ghcr.yml@refs/heads/main", issuer: actionsIssuer, sourceRef: "refs/heads/main", + predicate: provenancePredicate, } + collectorImageRole = imageRole{ + name: "collector", + repository: "https://github.com/open-telemetry/opentelemetry-collector-releases", + workflow: "https://github.com/open-telemetry/opentelemetry-collector-releases/.github/workflows/base-release.yaml", + issuer: actionsIssuer, + predicate: imageSignaturePredicate, + releaseWorkflow: true, + } + stableReleaseRefRegex = regexp.MustCompile("^" + stableReleaseRefPattern + "$") + stableImageVersionRegex = regexp.MustCompile("^v?" + stableReleaseVersionPattern + "$") + versionedImageTagRegex = regexp.MustCompile(`^v?[0-9]+\.[0-9]+\.[0-9]+($|-)`) ) +func collectorRoleForImage(imageName string) (imageRole, error) { + ref, err := name.ParseReference(imageName) + if err != nil { + return imageRole{}, err + } + if _, ok := ref.(name.Digest); ok { + // Digest parsing drops an optional tag that still constrains the signing release. + tagName, _, _ := strings.Cut(imageName, "@") + ref, err = name.NewTag(tagName) + if err != nil { + return imageRole{}, err + } + } + role := collectorImageRole + if tag, ok := ref.(name.Tag); ok { + version := tag.TagStr() + if stableImageVersionRegex.MatchString(version) { + role.sourceRef = "refs/tags/v" + strings.TrimPrefix(version, "v") + } else if versionedImageTagRegex.MatchString(version) { + return imageRole{}, fmt.Errorf("image tag %q must be a stable release version", version) + } + } + return role, nil +} + type verifiedImage struct { reference name.Digest producerCommit string @@ -85,15 +129,24 @@ func (c contextHTTPClient) Do(req *http.Request) (*http.Response, error) { } func (v *imageVerifier) Verify(ctx context.Context, ref name.Digest, role imageRole) (verified verifiedImage, err error) { + workflow := role.workflow + if role.releaseWorkflow && role.sourceRef != "" { + workflow += "@" + role.sourceRef + } defer func() { if err != nil { - err = fmt.Errorf("%s image %s: attestation verification failed (expected %s): %w", role.name, ref, role.workflow, err) + err = fmt.Errorf("%s image %s: attestation verification failed (expected %s): %w", role.name, ref, workflow, err) } }() if err := ctx.Err(); err != nil { return verifiedImage{}, err } - san, err := verify.NewSANMatcher(role.workflow, "") + sanValue, sanPattern := workflow, "" + if role.releaseWorkflow && role.sourceRef == "" { + sanValue = "" + sanPattern = "^" + regexp.QuoteMeta(workflow) + "@" + stableReleaseRefPattern + "$" + } + san, err := verify.NewSANMatcher(sanValue, sanPattern) if err != nil { return verifiedImage{}, err } @@ -175,7 +228,7 @@ func (v *imageVerifier) Verify(ctx context.Context, ref name.Digest, role imageR failures = append(failures, fmt.Errorf("bundle %s: %w", bundleRef, err)) continue } - commit, err := verifiedProducerCommit(result) + commit, err := verifiedProducerCommit(result, role) if err != nil { failures = append(failures, fmt.Errorf("bundle %s: %w", bundleRef, err)) continue @@ -208,12 +261,24 @@ func readImageBundle(reader io.ReadCloser) (*bundle.Bundle, error) { return b, nil } -func verifiedProducerCommit(result *verify.VerificationResult) (string, error) { - if result.Statement == nil || result.Statement.PredicateType != provenancePredicate { - return "", fmt.Errorf("attestation must contain %s provenance", provenancePredicate) +func verifiedProducerCommit(result *verify.VerificationResult, role imageRole) (string, error) { + if result.Statement == nil || result.Statement.PredicateType != role.predicate { + return "", fmt.Errorf("attestation must contain a %s statement", role.predicate) } if result.Signature == nil || result.Signature.Certificate == nil || result.Signature.Certificate.SourceRepositoryDigest == "" { return "", errors.New("attestation certificate has no producer commit") } - return result.Signature.Certificate.SourceRepositoryDigest, nil + cert := result.Signature.Certificate + if role.releaseWorkflow { + if result.Statement.Type != "https://in-toto.io/Statement/v1" { + return "", errors.New("image signature must contain an in-toto v1 statement") + } + if !stableReleaseRefRegex.MatchString(cert.SourceRepositoryRef) { + return "", fmt.Errorf("image signature certificate has no stable release ref: %q", cert.SourceRepositoryRef) + } + if cert.SubjectAlternativeName != role.workflow+"@"+cert.SourceRepositoryRef { + return "", errors.New("image signature workflow identity does not match its source ref") + } + } + return cert.SourceRepositoryDigest, nil } diff --git a/internal/infra/attestation_test.go b/internal/infra/attestation_test.go index 78d078ea..24333dbe 100644 --- a/internal/infra/attestation_test.go +++ b/internal/infra/attestation_test.go @@ -28,6 +28,7 @@ import ( "github.com/google/go-containerregistry/pkg/v1/static" "github.com/google/go-containerregistry/pkg/v1/types" "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" sigroot "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore-go/pkg/verify" ) @@ -104,7 +105,11 @@ func newAttestationTestRegistry(t *testing.T, bundles map[string][][]byte, fallb backing.ServeHTTP(w, req) })) t.Cleanup(r.server.Close) - for _, role := range []string{"updater", "proxy"} { + roles := []string{"updater", "proxy"} + if _, ok := bundles["collector"]; ok { + roles = append(roles, "collector") + } + for _, role := range roles { repo, err := name.NewRepository(strings.TrimPrefix(r.server.URL, "http://") + "/" + role) if err != nil { t.Fatal(err) @@ -190,6 +195,44 @@ func TestImageVerifier(t *testing.T) { } } +func TestImageVerifierCollectorRelease(t *testing.T) { + for _, fallback := range []bool{false, true} { + t.Run(fmt.Sprintf("fallback=%v", fallback), func(t *testing.T) { + reg := newAttestationTestRegistry(t, map[string][][]byte{ + "collector": {attestationFixture(t, "collector.json")}, + }, fallback) + ref := reg.references["collector"] + got, err := testImageVerifier(t).Verify(t.Context(), ref, collectorImageRole) + if err != nil { + t.Fatal(err) + } + want := ref.Context().Digest("sha256:fd328de2552466ad78385e1b1289c3f2402b1c45f265b252aab1955b42845ac1") + if got.reference != want || got.producerCommit != "619d1d0d06e9488a4a38e993f09605bbc13db592" { + t.Fatalf("Verify() = %+v, want %s at the signed release commit", got, want) + } + }) + } +} + +func TestImageVerifierCollectorPinnedRelease(t *testing.T) { + reg := newAttestationTestRegistry(t, map[string][][]byte{ + "collector": {attestationFixture(t, "collector.json")}, + }, false) + for _, release := range []string{"v0.161.0", "v0.160.0"} { + t.Run(release, func(t *testing.T) { + role := collectorImageRole + role.sourceRef = "refs/tags/" + release + _, err := testImageVerifier(t).Verify(t.Context(), reg.references["collector"], role) + if release == "v0.161.0" && err != nil { + t.Fatal(err) + } + if release == "v0.160.0" && err == nil { + t.Fatal("accepted a signature from a different release") + } + }) + } +} + func TestImageVerifierRejectsUnexpectedProvenance(t *testing.T) { reg := newAttestationTestRegistry(t, officialImageBundles(t), false) verifier := testImageVerifier(t) @@ -285,18 +328,66 @@ func TestVerifiedProducerCommit(t *testing.T) { {"missing commit", provenancePredicate, ""}, } { t.Run(tt.name, func(t *testing.T) { - data := fmt.Sprintf(`{"statement":{"predicateType":%q},"signature":{"certificate":{"sourceRepositoryDigest":%q}}}`, tt.predicate, tt.commit) + data := fmt.Sprintf(`{"statement":{"predicateType":%q}}`, tt.predicate) var result verify.VerificationResult if err := json.Unmarshal([]byte(data), &result); err != nil { t.Fatal(err) } - if _, err := verifiedProducerCommit(&result); err == nil { + result.Signature = &verify.SignatureVerificationResult{ + Certificate: &certificate.Summary{Extensions: certificate.Extensions{SourceRepositoryDigest: tt.commit}}, + } + if _, err := verifiedProducerCommit(&result, updaterImageRole); err == nil { t.Fatalf("accepted %s", tt.name) } }) } } +func TestCollectorClaimPolicy(t *testing.T) { + for _, tt := range []struct { + name string + modify func(*verify.VerificationResult) + wantErr string + }{ + {"wrong predicate", func(r *verify.VerificationResult) { r.Statement.PredicateType = provenancePredicate }, imageSignaturePredicate}, + {"wrong statement type", func(r *verify.VerificationResult) { r.Statement.Type = "https://in-toto.io/Statement/v0.1" }, "in-toto v1"}, + {"missing ref", func(r *verify.VerificationResult) { r.Signature.Certificate.SourceRepositoryRef = "" }, "stable release ref"}, + {"branch ref", func(r *verify.VerificationResult) { r.Signature.Certificate.SourceRepositoryRef = "refs/heads/main" }, "stable release ref"}, + {"prerelease ref", func(r *verify.VerificationResult) { + r.Signature.Certificate.SourceRepositoryRef = "refs/tags/v0.161.0-rc.1" + }, "stable release ref"}, + {"different release", func(r *verify.VerificationResult) { r.Signature.Certificate.SourceRepositoryRef = "refs/tags/v0.160.0" }, "does not match its source ref"}, + {"workflow prefix", func(r *verify.VerificationResult) { + r.Signature.Certificate.SubjectAlternativeName = "https://unexpected.example/" + r.Signature.Certificate.SubjectAlternativeName + }, "does not match its source ref"}, + {"workflow suffix", func(r *verify.VerificationResult) { r.Signature.Certificate.SubjectAlternativeName += "/extra" }, "does not match its source ref"}, + {"email identity", func(r *verify.VerificationResult) { + r.Signature.Certificate.SubjectAlternativeName = "operator@example.com" + }, "does not match its source ref"}, + } { + t.Run(tt.name, func(t *testing.T) { + data := fmt.Sprintf(`{"statement":{"_type":"https://in-toto.io/Statement/v1","predicateType":%q}}`, imageSignaturePredicate) + var result verify.VerificationResult + if err := json.Unmarshal([]byte(data), &result); err != nil { + t.Fatal(err) + } + result.Signature = &verify.SignatureVerificationResult{ + Certificate: &certificate.Summary{ + SubjectAlternativeName: "https://github.com/open-telemetry/opentelemetry-collector-releases/.github/workflows/base-release.yaml@refs/tags/v0.161.0", + Extensions: certificate.Extensions{ + SourceRepositoryDigest: "producer", + SourceRepositoryRef: "refs/tags/v0.161.0", + }, + }, + } + tt.modify(&result) + if _, err := verifiedProducerCommit(&result, collectorImageRole); err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("accepted invalid Collector claim or returned the wrong error: %v", err) + } + }) + } +} + func TestImageVerifierCancellation(t *testing.T) { t.Setenv("HOME", t.TempDir()) t.Setenv("DOCKER_CONFIG", t.TempDir()) diff --git a/internal/infra/image.go b/internal/infra/image.go index c38c5069..d9e1eb61 100644 --- a/internal/infra/image.go +++ b/internal/infra/image.go @@ -23,15 +23,23 @@ type imageSelection struct { } func verifyImages(ctx context.Context, cli *client.Client, params *RunParams, newVerifier func(context.Context) (*imageVerifier, error)) error { - images := []struct { + type protectedImage struct { role imageRole target *string selection imageSelection verified verifiedImage - }{ + } + images := []protectedImage{ {role: proxyImageRole, target: ¶ms.ProxyImage}, {role: updaterImageRole, target: ¶ms.UpdaterImage}, } + if params.CollectorConfigPath != "" { + role, err := collectorRoleForImage(params.CollectorImage) + if err != nil { + return fmt.Errorf("collector image %s: failed to select release policy: %w", params.CollectorImage, err) + } + images = append(images, protectedImage{role: role, target: ¶ms.CollectorImage}) + } for i := range images { selected, err := selectImage(ctx, cli, *images[i].target, params.PullImages) if err != nil { @@ -41,7 +49,7 @@ func verifyImages(ctx context.Context, cli *client.Client, params *RunParams, ne } verifier, err := newVerifier(ctx) if err != nil { - return fmt.Errorf("failed to initialize updater/proxy image verification: %w", err) + return fmt.Errorf("failed to initialize image verification: %w", err) } for i := range images { verified, err := verifier.Verify(ctx, images[i].selection.reference, images[i].role) diff --git a/internal/infra/image_test.go b/internal/infra/image_test.go index c2cc61e7..9af96bf4 100644 --- a/internal/infra/image_test.go +++ b/internal/infra/image_test.go @@ -7,6 +7,7 @@ import ( "fmt" "maps" "net/http" + "path/filepath" "slices" "strings" "sync" @@ -118,7 +119,9 @@ func newImageTestDaemon(t *testing.T, cached, available map[string]image.Inspect case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/v1.44/containers/") && strings.HasSuffix(r.URL.Path, "/json"): writeJSON(t, w, map[string]any{ "Id": "container-1", "State": map[string]any{"Running": true, "ExitCode": 0}, - "NetworkSettings": map[string]any{"Networks": map[string]any{}}, + "NetworkSettings": map[string]any{"Networks": map[string]any{ + "no-internet": map[string]string{"IPAddress": "192.0.2.2"}, + }}, }) default: t.Errorf("unexpected Docker request: %s %s", r.Method, r.URL.Path) @@ -134,7 +137,11 @@ func fixtureImageInspect(t *testing.T, reg *attestationTestRegistry, role string if err := json.Unmarshal(attestationFixture(t, role+"-manifest.json"), &manifest); err != nil { t.Fatal(err) } - return image.InspectResponse{ID: manifest.Config.Digest.String(), RepoDigests: []string{reg.references[role].Name()}} + id := manifest.Config.Digest.String() + if manifest.MediaType == types.DockerManifestList || manifest.MediaType == types.OCIImageIndex { + id = "local-" + role + } + return image.InspectResponse{ID: id, RepoDigests: []string{reg.references[role].Name()}} } func verifiedImageTestParams(reg *attestationTestRegistry, pull bool) RunParams { @@ -212,11 +219,136 @@ func TestVerifyImagesPinsPullAndContainerReferences(t *testing.T) { } } +func TestVerifyImagesCollector(t *testing.T) { + for _, configured := range []bool{false, true} { + t.Run(fmt.Sprintf("configured=%v", configured), func(t *testing.T) { + bundles := officialImageBundles(t) + bundles["collector"] = [][]byte{attestationFixture(t, "collector.json")} + reg := newAttestationTestRegistry(t, bundles, false) + cached := make(map[string]image.InspectResponse) + for _, role := range []string{"proxy", "updater"} { + cached[reg.references[role].Name()] = fixtureImageInspect(t, reg, role) + } + ref := reg.references["collector"] + d := newImageTestDaemon(t, cached, map[string]image.InspectResponse{ref.Name(): fixtureImageInspect(t, reg, "collector")}) + params := verifiedImageTestParams(reg, true) + params.CollectorImage = ref.Context().Tag("latest").Name() + if configured { + params.CollectorConfigPath = filepath.Join(t.TempDir(), "collector.yaml") + } + requested := params.CollectorImage + if err := verifyImages(t.Context(), d.cli, ¶ms, func(context.Context) (*imageVerifier, error) { + return testImageVerifier(t), nil + }); err != nil { + t.Fatal(err) + } + if !configured { + if params.CollectorImage != requested { + t.Fatal("replaced the disabled Collector image") + } + reg.mu.Lock() + requests := slices.Clone(reg.requests) + reg.mu.Unlock() + for _, request := range requests { + if strings.Contains(request, "/collector/") { + t.Fatalf("requested disabled Collector metadata: %s", request) + } + } + return + } + if params.CollectorImage != ref.Name() { + t.Fatalf("Collector image = %s, want verified digest %s", params.CollectorImage, ref) + } + reg.mu.Lock() + reg.manifests["/v2/collector/manifests/latest"] = attestationFixture(t, "updater-manifest.json") + reg.mu.Unlock() + if err := pullVerifiedImage(t.Context(), d.cli, params.CollectorImage, requested); err != nil { + t.Fatal(err) + } + proxy, err := NewProxy(t.Context(), d.cli, ¶ms, nil) + if err != nil { + t.Fatal(err) + } + nets := &Networks{NoInternet: network.CreateResponse{ID: "no-internet"}, noInternetName: "no-internet"} + if _, err := NewCollector(t.Context(), d.cli, nets, ¶ms, proxy); err != nil { + t.Fatal(err) + } + d.mu.Lock() + pulls, created := slices.Clone(d.pulls), slices.Clone(d.created) + d.mu.Unlock() + if !slices.Equal(pulls, []string{ref.Name()}) || !slices.Equal(created, []string{params.ProxyImage, ref.Name()}) { + t.Fatalf("pulls = %v, creates = %v, want the verified Collector index", pulls, created) + } + }) + } +} + +func TestVerifyImagesCollectorVersion(t *testing.T) { + for _, tt := range []struct { + tag string + wantErr string + digest bool + }{ + {tag: "latest"}, + {tag: "stable"}, + {tag: "0.161.0"}, + {tag: "v0.161.0"}, + {tag: "digest"}, + {tag: "0.160.0", wantErr: "refs/tags/v0.160.0"}, + {tag: "0.161.0-rc.1", wantErr: "stable release version"}, + {tag: "00.161.0", wantErr: "stable release version"}, + {tag: "0.161.0", digest: true}, + {tag: "0.160.0", wantErr: "refs/tags/v0.160.0", digest: true}, + {tag: "0.161.0-rc.1", wantErr: "stable release version", digest: true}, + } { + testName := tt.tag + if tt.digest { + testName += "@digest" + } + t.Run(testName, func(t *testing.T) { + bundles := officialImageBundles(t) + bundles["collector"] = [][]byte{attestationFixture(t, "collector.json")} + reg := newAttestationTestRegistry(t, bundles, false) + ref := reg.references["collector"] + params := verifiedImageTestParams(reg, true) + params.CollectorConfigPath = filepath.Join(t.TempDir(), "collector.yaml") + params.CollectorImage = ref.Context().Tag(tt.tag).Name() + if tt.tag == "digest" { + params.CollectorImage = ref.Name() + } else if tt.digest { + params.CollectorImage += "@" + ref.DigestStr() + } + reg.mu.Lock() + reg.manifests["/v2/collector/manifests/"+tt.tag] = attestationFixture(t, "collector-manifest.json") + reg.mu.Unlock() + original := params + d := newImageTestDaemon(t, nil, nil) + err := verifyImages(t.Context(), d.cli, ¶ms, func(context.Context) (*imageVerifier, error) { + return testImageVerifier(t), nil + }) + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("verifyImages() = %v, want %q", err, tt.wantErr) + } + if params.ProxyImage != original.ProxyImage || params.UpdaterImage != original.UpdaterImage || params.CollectorImage != original.CollectorImage { + t.Fatal("failed Collector verification replaced protected image references") + } + } else if err != nil || params.CollectorImage != ref.Name() { + t.Fatalf("Collector selection = %s, %v, want %s", params.CollectorImage, err, ref) + } + }) + } +} + func TestVerifyImagesUsesCachedDigestWithoutPull(t *testing.T) { - reg := newAttestationTestRegistry(t, officialImageBundles(t), false) + bundles := officialImageBundles(t) + bundles["collector"] = [][]byte{attestationFixture(t, "collector.json")} + reg := newAttestationTestRegistry(t, bundles, false) params := verifiedImageTestParams(reg, false) + params.CollectorConfigPath = filepath.Join(t.TempDir(), "collector.yaml") + params.CollectorImage = reg.references["collector"].Context().Tag("latest").Name() cached := make(map[string]image.InspectResponse) - for _, role := range []string{"proxy", "updater"} { + for _, role := range []string{"proxy", "updater", "collector"} { inspect := fixtureImageInspect(t, reg, role) cached[reg.references[role].Name()] = inspect cached[reg.references[role].Context().Tag("latest").Name()] = inspect @@ -224,6 +356,7 @@ func TestVerifyImagesUsesCachedDigestWithoutPull(t *testing.T) { reg.mu.Lock() reg.manifests["/v2/proxy/manifests/latest"] = attestationFixture(t, "updater-manifest.json") reg.manifests["/v2/updater/manifests/latest"] = attestationFixture(t, "proxy-manifest.json") + reg.manifests["/v2/collector/manifests/latest"] = attestationFixture(t, "proxy-manifest.json") reg.mu.Unlock() d := newImageTestDaemon(t, cached, nil) if err := verifyImages(t.Context(), d.cli, ¶ms, func(context.Context) (*imageVerifier, error) { @@ -231,7 +364,8 @@ func TestVerifyImagesUsesCachedDigestWithoutPull(t *testing.T) { }); err != nil { t.Fatal(err) } - if params.ProxyImage != reg.references["proxy"].Name() || params.UpdaterImage != reg.references["updater"].Name() { + if params.ProxyImage != reg.references["proxy"].Name() || params.UpdaterImage != reg.references["updater"].Name() || + params.CollectorImage != reg.references["collector"].Name() { t.Fatalf("cached image selections changed: %+v", params) } d.mu.Lock() @@ -251,27 +385,35 @@ func TestVerifyImagesUsesCachedDigestWithoutPull(t *testing.T) { } func TestVerifyImagesRejectsInconsistentCachedImage(t *testing.T) { - reg := newAttestationTestRegistry(t, officialImageBundles(t), false) - params := verifiedImageTestParams(reg, false) - original := params - cached := make(map[string]image.InspectResponse) - for _, role := range []string{"proxy", "updater"} { - inspect := fixtureImageInspect(t, reg, role) - cached[reg.references[role].Context().Tag("latest").Name()] = inspect - if role == "updater" { - inspect.ID = "different-image" - } - cached[reg.references[role].Name()] = inspect - } - d := newImageTestDaemon(t, cached, nil) - err := verifyImages(t.Context(), d.cli, ¶ms, func(context.Context) (*imageVerifier, error) { - return testImageVerifier(t), nil - }) - if err == nil || !strings.Contains(err.Error(), "does not match the selected cached image") { - t.Fatalf("verifyImages() = %v, want cached identity error", err) - } - if params.ProxyImage != original.ProxyImage || params.UpdaterImage != original.UpdaterImage { - t.Fatal("failed verification replaced image references") + for _, mismatchedRole := range []string{"updater", "collector"} { + t.Run(mismatchedRole, func(t *testing.T) { + bundles := officialImageBundles(t) + bundles["collector"] = [][]byte{attestationFixture(t, "collector.json")} + reg := newAttestationTestRegistry(t, bundles, false) + params := verifiedImageTestParams(reg, false) + params.CollectorConfigPath = filepath.Join(t.TempDir(), "collector.yaml") + params.CollectorImage = reg.references["collector"].Context().Tag("latest").Name() + original := params + cached := make(map[string]image.InspectResponse) + for _, role := range []string{"proxy", "updater", "collector"} { + inspect := fixtureImageInspect(t, reg, role) + cached[reg.references[role].Context().Tag("latest").Name()] = inspect + if role == mismatchedRole { + inspect.ID = "different-image" + } + cached[reg.references[role].Name()] = inspect + } + d := newImageTestDaemon(t, cached, nil) + err := verifyImages(t.Context(), d.cli, ¶ms, func(context.Context) (*imageVerifier, error) { + return testImageVerifier(t), nil + }) + if err == nil || !strings.Contains(err.Error(), mismatchedRole+" image") || !strings.Contains(err.Error(), "does not match the selected cached image") { + t.Fatalf("verifyImages() = %v, want cached identity error", err) + } + if params.ProxyImage != original.ProxyImage || params.UpdaterImage != original.UpdaterImage || params.CollectorImage != original.CollectorImage { + t.Fatal("failed verification replaced image references") + } + }) } } diff --git a/internal/infra/run.go b/internal/infra/run.go index cbd8fa23..765bcd0b 100644 --- a/internal/infra/run.go +++ b/internal/infra/run.go @@ -69,7 +69,7 @@ type RunParams struct { ProxyCertPath string // attempt to pull images if they aren't local? PullImages bool - // SkipImageVerification permits unsigned development updater and proxy images. + // SkipImageVerification permits unsigned development updater, proxy, and collector images. SkipImageVerification bool // run an interactive shell? Debug bool @@ -406,11 +406,12 @@ func runContainers(ctx context.Context, params RunParams) (err error) { } requestedProxyImage, requestedUpdaterImage := params.ProxyImage, params.UpdaterImage + requestedCollectorImage := params.CollectorImage pullProtectedImage := func(ctx context.Context, cli *client.Client, imageName, _ string) error { return pullImage(ctx, cli, imageName) } if params.SkipImageVerification { - log.Println("WARNING: updater and proxy image verification is disabled. Only run trusted development images.") + log.Println("WARNING: updater, proxy, and OpenTelemetry collector image verification is disabled. Only run trusted development images.") } else { if err := verifyImages(ctx, cli, ¶ms, newImageVerifier); err != nil { return err @@ -425,9 +426,9 @@ func runContainers(ctx context.Context, params RunParams) (err error) { } if params.CollectorConfigPath != "" { - err = pullImage(ctx, cli, params.CollectorImage) + err = pullProtectedImage(ctx, cli, params.CollectorImage, requestedCollectorImage) if err != nil { - fmt.Println("Failed to pull OpenTelemetry collector image:", err) + return fmt.Errorf("failed to pull OpenTelemetry collector image: %w", err) } } @@ -469,7 +470,7 @@ func runContainers(ctx context.Context, params RunParams) (err error) { if params.CollectorConfigPath != "" { collector, err = NewCollector(ctx, cli, networks, ¶ms, prox) if err != nil { - fmt.Println("Failed to create OpenTelemetry collector:", err) + return fmt.Errorf("failed to create OpenTelemetry collector: %w", err) } if !params.Debug { go collector.TailLogs(ctx, cli) diff --git a/internal/infra/run_test.go b/internal/infra/run_test.go index a2917d3c..a4c0560b 100644 --- a/internal/infra/run_test.go +++ b/internal/infra/run_test.go @@ -4,6 +4,7 @@ import ( "archive/tar" "bytes" "context" + "encoding/json" "errors" "fmt" "io" @@ -95,6 +96,178 @@ func TestRunContainersRejectsUnverifiedImages(t *testing.T) { } } +func TestRunContainersRejectsUnverifiedCollector(t *testing.T) { + var mu sync.Mutex + writes := 0 + docker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodHead { + mu.Lock() + writes++ + mu.Unlock() + t.Errorf("Docker write before Collector verification: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + return + } + if strings.HasPrefix(r.URL.Path, "/v1.44/images/") && strings.HasSuffix(r.URL.Path, "/json") { + imageName := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/v1.44/images/"), "/json") + var digests []string + if imageName != "collector" { + digests = []string{imageName + "@sha256:" + strings.Repeat("a", 64)} + } + writeJSON(t, w, map[string]any{"Id": "local-" + imageName, "RepoDigests": digests}) + return + } + w.Header().Set("API-Version", testDockerAPIVersion) + fmt.Fprint(w, "OK") + })) + defer docker.Close() + t.Setenv("DOCKER_HOST", docker.URL) + t.Setenv("DOCKER_API_VERSION", testDockerAPIVersion) + t.Setenv("DOCKER_TLS_VERIFY", "") + t.Setenv("DOCKER_CERT_PATH", "") + err := runContainers(t.Context(), RunParams{ + Job: &model.Job{}, ProxyImage: "proxy", UpdaterImage: "updater", + CollectorImage: "collector", CollectorConfigPath: filepath.Join(t.TempDir(), "collector.yaml"), + }) + if err == nil || !strings.Contains(err.Error(), "collector image") || !strings.Contains(err.Error(), "no matching repository digest") { + t.Fatalf("runContainers() = %v, want unsigned Collector rejection", err) + } + mu.Lock() + defer mu.Unlock() + if writes != 0 { + t.Fatalf("performed %d Docker writes before Collector verification", writes) + } +} + +func TestRunContainersCollectorPullFailure(t *testing.T) { + docker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case r.URL.Path == "/_ping": + w.Header().Set("API-Version", testDockerAPIVersion) + fmt.Fprint(w, "OK") + case r.Method == http.MethodGet && r.URL.Path == "/v1.44/images/cached-proxy/json": + writeJSON(t, w, map[string]any{"Id": "local-proxy", "RepoDigests": []string{}}) + case r.Method == http.MethodGet && r.URL.Path == "/v1.44/images/missing-collector/json": + w.WriteHeader(http.StatusNotFound) + writeJSON(t, w, map[string]string{"message": "image not found"}) + case r.Method == http.MethodPost && r.URL.Path == "/v1.44/images/create": + w.WriteHeader(http.StatusInternalServerError) + writeJSON(t, w, map[string]string{"message": "collector pull denied"}) + default: + t.Errorf("Docker operation after Collector pull failure: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + writeJSON(t, w, map[string]string{"message": "unexpected Docker operation"}) + } + })) + defer docker.Close() + t.Setenv("DOCKER_HOST", docker.URL) + t.Setenv("DOCKER_API_VERSION", testDockerAPIVersion) + t.Setenv("DOCKER_TLS_VERIFY", "") + t.Setenv("DOCKER_CERT_PATH", "") + err := runContainers(t.Context(), RunParams{ + Job: &model.Job{}, ProxyImage: "cached-proxy", UpdaterImage: "unused-updater", + CollectorImage: "missing-collector", CollectorConfigPath: filepath.Join(t.TempDir(), "collector.yaml"), + PullImages: true, SkipImageVerification: true, + }) + if err == nil || !strings.Contains(err.Error(), "OpenTelemetry collector") || !strings.Contains(err.Error(), "collector pull denied") { + t.Fatalf("runContainers() = %v, want Collector pull failure", err) + } +} + +func TestRunContainersCollectorStartupFailure(t *testing.T) { + for _, stage := range []string{"create", "start"} { + t.Run(stage, func(t *testing.T) { + var mu sync.Mutex + var noInternetName string + var created []string + removedProxy, removedNetworks := false, 0 + docker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + defer mu.Unlock() + switch { + case r.URL.Path == "/_ping": + w.Header().Set("API-Version", testDockerAPIVersion) + fmt.Fprint(w, "OK") + case r.Method == http.MethodPost && r.URL.Path == "/v1.44/networks/create": + var cfg struct { + Name string + Internal bool + } + if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { + t.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + id := "internet" + if cfg.Internal { + id, noInternetName = "no-internet", cfg.Name + } + writeJSON(t, w, map[string]string{"Id": id}) + case r.Method == http.MethodPost && r.URL.Path == "/v1.44/containers/create": + var cfg struct{ Image string } + if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { + t.Error(err) + w.WriteHeader(http.StatusBadRequest) + return + } + created = append(created, cfg.Image) + if cfg.Image == "collector" && stage == "create" { + w.WriteHeader(http.StatusInternalServerError) + writeJSON(t, w, map[string]string{"message": "collector create denied"}) + } else if cfg.Image == "proxy" || cfg.Image == "collector" { + writeJSON(t, w, map[string]string{"Id": cfg.Image}) + } else { + t.Errorf("created updater after Collector failure: %s", cfg.Image) + w.WriteHeader(http.StatusInternalServerError) + } + case r.Method == http.MethodGet && r.URL.Path == "/v1.44/containers/proxy/json": + writeJSON(t, w, map[string]any{ + "Id": "proxy", "State": map[string]any{"Running": true, "ExitCode": 0}, + "NetworkSettings": map[string]any{"Networks": map[string]any{ + noInternetName: map[string]string{"IPAddress": "192.0.2.2"}, + }}, + }) + case r.Method == http.MethodPost && r.URL.Path == "/v1.44/containers/collector/start": + w.WriteHeader(http.StatusInternalServerError) + writeJSON(t, w, map[string]string{"message": "collector start denied"}) + case r.Method == http.MethodPut && strings.HasSuffix(r.URL.Path, "/archive"), + r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/connect"), + r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/stop"), + r.Method == http.MethodPost && r.URL.Path == "/v1.44/containers/proxy/start": + w.WriteHeader(http.StatusNoContent) + case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, "/v1.44/containers/"): + removedProxy = removedProxy || r.URL.Path == "/v1.44/containers/proxy" + w.WriteHeader(http.StatusNoContent) + case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, "/v1.44/networks/"): + removedNetworks++ + w.WriteHeader(http.StatusNoContent) + default: + t.Errorf("unexpected Docker request: %s %s", r.Method, r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer docker.Close() + t.Setenv("DOCKER_HOST", docker.URL) + t.Setenv("DOCKER_API_VERSION", testDockerAPIVersion) + t.Setenv("DOCKER_TLS_VERIFY", "") + t.Setenv("DOCKER_CERT_PATH", "") + err := runContainers(t.Context(), RunParams{ + Job: &model.Job{}, ProxyImage: "proxy", UpdaterImage: "unused-updater", + CollectorImage: "collector", CollectorConfigPath: filepath.Join(t.TempDir(), "collector.yaml"), + SkipImageVerification: true, Debug: true, + }) + if err == nil || !strings.Contains(err.Error(), "collector "+stage+" denied") { + t.Fatalf("runContainers() = %v, want Collector %s failure", err, stage) + } + mu.Lock() + defer mu.Unlock() + if strings.Join(created, ",") != "proxy,collector" || !removedProxy || removedNetworks != 2 { + t.Fatalf("created = %v, proxy removed = %v, networks removed = %d", created, removedProxy, removedNetworks) + } + }) + } +} + func TestRunContainersExplicitVerificationBypass(t *testing.T) { var output bytes.Buffer previousOutput := log.Writer() @@ -121,12 +294,13 @@ func TestRunContainersExplicitVerificationBypass(t *testing.T) { t.Setenv("DOCKER_CERT_PATH", "") err := runContainers(t.Context(), RunParams{ Job: &model.Job{}, ProxyImage: "unsigned-proxy", UpdaterImage: "unsigned-updater", + CollectorImage: "unsigned-collector", CollectorConfigPath: filepath.Join(t.TempDir(), "collector.yaml"), SkipImageVerification: true, }) if err == nil || !strings.Contains(err.Error(), "stopped after explicit bypass") { t.Fatalf("runContainers() = %v, want legacy execution after explicit bypass", err) } - if !strings.Contains(output.String(), "WARNING: updater and proxy image verification is disabled") { + if !strings.Contains(output.String(), "WARNING: updater, proxy, and OpenTelemetry collector image verification is disabled") { t.Fatalf("missing bypass warning: %s", output.String()) } } diff --git a/internal/infra/testdata/attestations/collector-manifest.json b/internal/infra/testdata/attestations/collector-manifest.json new file mode 100644 index 00000000..5fc0058a --- /dev/null +++ b/internal/infra/testdata/attestations/collector-manifest.json @@ -0,0 +1,70 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", + "manifests": [ + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 946, + "digest": "sha256:76fd04f2d75922e93e447381bafcd0b78d24f1c5cdcc13f424d9a16f0342c087", + "platform": { + "architecture": "386", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 947, + "digest": "sha256:b5cf983651c32c3ca13f936deb51742015a54d121f388cac248923ddeb8cc9fc", + "platform": { + "architecture": "amd64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 947, + "digest": "sha256:d497a11a3088097e4054ffbfd4b4f5244a8c036dc77c96cf2f14f86113ee9b86", + "platform": { + "architecture": "arm64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 946, + "digest": "sha256:b3eb06494c8eb16510a4588cd086373d4c8ac16ccefa979eabdf58a33288c9c4", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v7" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 946, + "digest": "sha256:c48fd65ad15b1c8b2113de06dca3f1a79cc1c43a705a77dd76664d13186b1fef", + "platform": { + "architecture": "ppc64le", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 946, + "digest": "sha256:f8490cdd5c3841519ece19c32a4cd4786b126b3794c0c150278236f2dc8652c0", + "platform": { + "architecture": "riscv64", + "os": "linux" + } + }, + { + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "size": 947, + "digest": "sha256:5639ab2f99e1b578664ba1a0a0070f94aa818068e53a08c72bff759b6a8f76b5", + "platform": { + "architecture": "s390x", + "os": "linux" + } + } + ] +} \ No newline at end of file diff --git a/internal/infra/testdata/attestations/collector.json b/internal/infra/testdata/attestations/collector.json new file mode 100644 index 00000000..60855954 --- /dev/null +++ b/internal/infra/testdata/attestations/collector.json @@ -0,0 +1,67 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIIH6jCCB3GgAwIBAgIUOXiQahO4HAFKaIyvBuBRAgI1DO4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjYwOTE2MDMwMjUyWhcNMjYwOTE2MDMxMjUyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPx5liUaMXGI6bNMNyPR+xuD2GQdtletEs2JFpydg/ronreKnjEc+7JUWrBEWBaLBX5UEzPOvS8YPIojHfJ76xKOCBpAwggaMMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUukeMvdQ32dXi1UgeB4/q7SDBKEgwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wgYcGA1UdEQEB/wR9MHuGeWh0dHBzOi8vZ2l0aHViLmNvbS9vcGVuLXRlbGVtZXRyeS9vcGVudGVsZW1ldHJ5LWNvbGxlY3Rvci1yZWxlYXNlcy8uZ2l0aHViL3dvcmtmbG93cy9iYXNlLXJlbGVhc2UueWFtbEByZWZzL3RhZ3MvdjAuMTYxLjAwOQYKKwYBBAGDvzABAQQraHR0cHM6Ly90b2tlbi5hY3Rpb25zLmdpdGh1YnVzZXJjb250ZW50LmNvbTASBgorBgEEAYO/MAECBARwdXNoMDYGCisGAQQBg78wAQMEKDYxOWQxZDBkMDZlOTQ4OGE0YTM4ZTk5M2YwOTYwNWJiYzEzZGI1OTIwHQYKKwYBBAGDvzABBAQPUmVsZWFzZSBDb250cmliMD0GCisGAQQBg78wAQUEL29wZW4tdGVsZW1ldHJ5L29wZW50ZWxlbWV0cnktY29sbGVjdG9yLXJlbGVhc2VzMCAGCisGAQQBg78wAQYEEnJlZnMvdGFncy92MC4xNjEuMDA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wgYkGCisGAQQBg78wAQkEewx5aHR0cHM6Ly9naXRodWIuY29tL29wZW4tdGVsZW1ldHJ5L29wZW50ZWxlbWV0cnktY29sbGVjdG9yLXJlbGVhc2VzLy5naXRodWIvd29ya2Zsb3dzL2Jhc2UtcmVsZWFzZS55YW1sQHJlZnMvdGFncy92MC4xNjEuMDA4BgorBgEEAYO/MAEKBCoMKDYxOWQxZDBkMDZlOTQ4OGE0YTM4ZTk5M2YwOTYwNWJiYzEzZGI1OTIwHQYKKwYBBAGDvzABCwQPDA1naXRodWItaG9zdGVkMFIGCisGAQQBg78wAQwERAxCaHR0cHM6Ly9naXRodWIuY29tL29wZW4tdGVsZW1ldHJ5L29wZW50ZWxlbWV0cnktY29sbGVjdG9yLXJlbGVhc2VzMDgGCisGAQQBg78wAQ0EKgwoNjE5ZDFkMGQwNmU5NDg4YTRhMzhlOTkzZjA5NjA1YmJjMTNkYjU5MjAiBgorBgEEAYO/MAEOBBQMEnJlZnMvdGFncy92MC4xNjEuMDAZBgorBgEEAYO/MAEPBAsMCTQwMDI1MzY3NjAxBgorBgEEAYO/MAEQBCMMIWh0dHBzOi8vZ2l0aHViLmNvbS9vcGVuLXRlbGVtZXRyeTAYBgorBgEEAYO/MAERBAoMCDQ5OTk4MDAyMIGMBgorBgEEAYO/MAESBH4MfGh0dHBzOi8vZ2l0aHViLmNvbS9vcGVuLXRlbGVtZXRyeS9vcGVudGVsZW1ldHJ5LWNvbGxlY3Rvci1yZWxlYXNlcy8uZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLWNvbnRyaWIueWFtbEByZWZzL3RhZ3MvdjAuMTYxLjAwOAYKKwYBBAGDvzABEwQqDCg2MTlkMWQwZDA2ZTk0ODhhNGEzOGU5OTNmMDk2MDViYmMxM2RiNTkyMBQGCisGAQQBg78wARQEBgwEcHVzaDB2BgorBgEEAYO/MAEVBGgMZmh0dHBzOi8vZ2l0aHViLmNvbS9vcGVuLXRlbGVtZXRyeS9vcGVudGVsZW1ldHJ5LWNvbGxlY3Rvci1yZWxlYXNlcy9hY3Rpb25zL3J1bnMvMzUwNDg1Nzg3MDkvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzBbBgorBgEEAYO/MAEYBE0MS3JlcG86b3Blbi10ZWxlbWV0cnkvb3BlbnRlbGVtZXRyeS1jb2xsZWN0b3ItcmVsZWFzZXM6cmVmOnJlZnMvdGFncy92MC4xNjEuMDCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABoKgqzV8AAAQDAEgwRgIhAOwQov0eaccaCrLKA569XSrsxSWFVmroy0yDJvFVUwxWAiEAqfOFVFiBehDO7ObpjfPTDWiktirkii8pvYC0ukOBvWMwCgYIKoZIzj0EAwMDZwAwZAIwaWSLGtL7qRgTrrhHlKdF55eBk3sQhh1RDKYY13EJQeKvYxC4r4pmCQ5DAI2TEBj9AjBuRUEkmLjVnxvQTdIuHPmWlBBVw2pdccScAdup/nkCtAQQJMqlDkh8CKKHTilUc8I=" + }, + "tlogEntries": [ + { + "logIndex": "2854532435", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "integratedTime": "1789527772", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIBB9R38T06oBvyiXD5b9zVgaIlM7TG7J6czbnGiAboamAiBvmpDe2yAN8AQgJc80nexkDz4xFudANsGQIQ8CEhH1PQ==" + }, + "inclusionProof": { + "logIndex": "2732628173", + "rootHash": "f9J8gQ0+3jmaFUANZFTbJzS1jY/RG/GPLE1K2d5gsdk=", + "treeSize": "2732628182", + "hashes": [ + "4465OhqWwlRar9d+FXzN1OlmlokD87i5KJALbn87gtQ=", + "50lfQh9bOOx84SYLKVPtFpiFyI0AWnStO1031GgJ8Us=", + "TCJgekT8jm//OohtC62s4ZcZXPp3wixKgsToWPxDqvI=", + "Vt3lQIeK4gj5F+FtMpfMcKwbREB9Dty+nXxkGp69u9M=", + "EfeCX94QK9UMQmC39m7O3bFfXPhGJFZLSlOXrcMBdxY=", + "DWvWdrdCwbHclVScENr8BHoVC0F67lON/x0RBmknKuc=", + "0+lwFJqUjcx3k4FjwsaEElHtLIUBlUbThp3UhCREr8I=", + "UisRG4UhIPSFTVwUcXfjAni+ExFcGmbd+HgMeW6R3t0=", + "yj1oBnRdUCQM60lzhCdzZmol217E+e+kmHAs+CpLEzE=", + "5LLmq3hQu4xaATHLzSlwlI6swaF7bdHfAfkbquTsMnM=", + "NqyGZjSUwKsxWj/Jbt/nngCQd0afI3t1FaXTPNTeiYo=", + "GuxfoQnlNUIMOXXWiG1fXQ5ctSy5Gr+J2SOBczEuQMs=", + "UHTD4gppDpUbvjVt5JlrR1znuqkF/Be+5C53hqcVmDA=", + "jhevWFOJ2cWU6SvzMnAe08zHGcVquRlP7gAed39+wI4=", + "qxzHanAzz57SDdmJe0B7bJK72NTIBbwEMGBBKvDOROw=", + "xH/DCseLHr9eKoYT8qsORZK7zVdEGYWHuVtsVrD95wY=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n2732628182\nf9J8gQ0+3jmaFUANZFTbJzS1jY/RG/GPLE1K2d5gsdk=\n\n— rekor.sigstore.dev wNI9ajBEAiA3eVbQ4jBDGVO9Zm+Tv/nXoGVnX69295hDqpwrtBODygIgTKcEFJtlgVTKcCknjkyxsG8rFrJ4pQB9jQ8BpedclkU=\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiY2U2OGVmOGQ1YTBjNzlmZDVhM2IzMDA0MTQ1ZmZkNjk1OTUxZGE4Mzg1MmFhMTFiZmI0Y2NkYjZhZTU0NzZhMiJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6ImIxZmQxZThkNTQyMDI3N2MyZWZhYjE2OTBiMWMwYjE4MmRiNGYzMGIzYWY5NjQ1MDQwODNlYmViNzFjZTY1MzcifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRRGlpd1RsOTBOVjZBK2tydHVmSTVPek1RN1p3cjk2cWR0K2RORkFIZWNzcUFJZ0pHQ1psU2srWDFJR095YlNRa1lzSzFqWG9teWw0cnZLMkQ3RzRiT1p2d1k9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VnMmFrTkRRak5IWjBGM1NVSkJaMGxWVDFocFVXRm9UelJJUVVaTFlVbDVka0oxUWxKQlowa3hSRTgwZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwWmQwOVVSVEpOUkUxM1RXcFZlVmRvWTA1TmFsbDNUMVJGTWsxRVRYaE5hbFY1VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVlFlRFZzYVZWaFRWaEhTVFppVGsxT2VWQlNLM2gxUkRKSFVXUjBiR1YwUlhNeVNrWUtjSGxrWnk5eWIyNXlaVXR1YWtWakt6ZEtWVmR5UWtWWFFtRk1RbGcxVlVWNlVFOTJVemhaVUVsdmFraG1TamMyZUV0UFEwSndRWGRuWjJGTlRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVjFhMlZOQ25aa1VUTXlaRmhwTVZWblpVSTBMM0UzVTBSQ1MwVm5kMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMmRaWTBkQk1WVmtSVkZGUWk5M1VqbE5TSFZIWlZkb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01ZG1OSFZuVk1XRkpzWWtkV2RBcGFXRko1WlZNNWRtTkhWblZrUjFaeldsY3hiR1JJU2pWTVYwNTJZa2Q0YkZrelVuWmphVEY1V2xkNGJGbFlUbXhqZVRoMVdqSnNNR0ZJVm1sTU0yUjJDbU50ZEcxaVJ6a3pZM2s1YVZsWVRteE1XRXBzWWtkV2FHTXlWWFZsVjBaMFlrVkNlVnBYV25wTU0xSm9Xak5OZG1ScVFYVk5WRmw0VEdwQmQwOVJXVXNLUzNkWlFrSkJSMFIyZWtGQ1FWRlJjbUZJVWpCalNFMDJUSGs1TUdJeWRHeGlhVFZvV1ROU2NHSXlOWHBNYldSd1pFZG9NVmx1Vm5wYVdFcHFZakkxTUFwYVZ6VXdURzFPZG1KVVFWTkNaMjl5UW1kRlJVRlpUeTlOUVVWRFFrRlNkMlJZVG05TlJGbEhRMmx6UjBGUlVVSm5OemgzUVZGTlJVdEVXWGhQVjFGNENscEVRbXROUkZwc1QxUlJORTlIUlRCWlZFMDBXbFJyTlUweVdYZFBWRmwzVGxkS2FWbDZSWHBhUjBreFQxUkpkMGhSV1V0TGQxbENRa0ZIUkhaNlFVSUtRa0ZSVUZWdFZuTmFWMFo2V2xOQ1JHSXlOVEJqYld4cFRVUXdSME5wYzBkQlVWRkNaemM0ZDBGUlZVVk1NamwzV2xjMGRHUkhWbk5hVnpGc1pFaEtOUXBNTWpsM1dsYzFNRnBYZUd4aVYxWXdZMjVyZEZreU9YTmlSMVpxWkVjNWVVeFlTbXhpUjFab1l6SldlazFEUVVkRGFYTkhRVkZSUW1jM09IZEJVVmxGQ2tWdVNteGFiazEyWkVkR2JtTjVPVEpOUXpSNFRtcEZkVTFFUVRkQ1oyOXlRbWRGUlVGWlR5OU5RVVZKUWtNd1RVc3lhREJrU0VKNlQyazRkbVJIT1hJS1dsYzBkVmxYVGpCaFZ6bDFZM2sxYm1GWVVtOWtWMG94WXpKV2VWa3lPWFZrUjFaMVpFTTFhbUl5TUhkbldXdEhRMmx6UjBGUlVVSm5OemgzUVZGclJRcGxkM2cxWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREk1ZDFwWE5IUmtSMVp6V2xjeGJHUklTalZNTWpsM1dsYzFNRnBYZUd4aVYxWXdDbU51YTNSWk1qbHpZa2RXYW1SSE9YbE1XRXBzWWtkV2FHTXlWbnBNZVRWdVlWaFNiMlJYU1haa01qbDVZVEphYzJJelpIcE1Na3BvWXpKVmRHTnRWbk1LV2xkR2VscFROVFZaVnpGelVVaEtiRnB1VFhaa1IwWnVZM2s1TWsxRE5IaE9ha1YxVFVSQk5FSm5iM0pDWjBWRlFWbFBMMDFCUlV0Q1EyOU5TMFJaZUFwUFYxRjRXa1JDYTAxRVdteFBWRkUwVDBkRk1GbFVUVFJhVkdzMVRUSlpkMDlVV1hkT1YwcHBXWHBGZWxwSFNURlBWRWwzU0ZGWlMwdDNXVUpDUVVkRUNuWjZRVUpEZDFGUVJFRXhibUZZVW05a1YwbDBZVWM1ZW1SSFZtdE5Sa2xIUTJselIwRlJVVUpuTnpoM1FWRjNSVkpCZUVOaFNGSXdZMGhOTmt4NU9XNEtZVmhTYjJSWFNYVlpNamwwVERJNWQxcFhOSFJrUjFaeldsY3hiR1JJU2pWTU1qbDNXbGMxTUZwWGVHeGlWMVl3WTI1cmRGa3lPWE5pUjFacVpFYzVlUXBNV0Vwc1lrZFdhR015Vm5wTlJHZEhRMmx6UjBGUlVVSm5OemgzUVZFd1JVdG5kMjlPYWtVMVdrUkdhMDFIVVhkT2JWVTFUa1JuTkZsVVVtaE5lbWhzQ2s5VWEzcGFha0UxVG1wQk1WbHRTbXBOVkU1cldXcFZOVTFxUVdsQ1oyOXlRbWRGUlVGWlR5OU5RVVZQUWtKUlRVVnVTbXhhYmsxMlpFZEdibU41T1RJS1RVTTBlRTVxUlhWTlJFRmFRbWR2Y2tKblJVVkJXVTh2VFVGRlVFSkJjMDFEVkZGM1RVUkpNVTE2V1ROT2FrRjRRbWR2Y2tKblJVVkJXVTh2VFVGRlVRcENRMDFOU1Zkb01HUklRbnBQYVRoMldqSnNNR0ZJVm1sTWJVNTJZbE01ZG1OSFZuVk1XRkpzWWtkV2RGcFlVbmxsVkVGWlFtZHZja0puUlVWQldVOHZDazFCUlZKQ1FXOU5RMFJSTlU5VWF6Uk5SRUY1VFVsSFRVSm5iM0pDWjBWRlFWbFBMMDFCUlZOQ1NEUk5aa2RvTUdSSVFucFBhVGgyV2pKc01HRklWbWtLVEcxT2RtSlRPWFpqUjFaMVRGaFNiR0pIVm5SYVdGSjVaVk01ZG1OSFZuVmtSMVp6V2xjeGJHUklTalZNVjA1MllrZDRiRmt6VW5aamFURjVXbGQ0YkFwWldFNXNZM2s0ZFZveWJEQmhTRlpwVEROa2RtTnRkRzFpUnprelkzazVlVnBYZUd4WldFNXNURmRPZG1KdVVubGhWMGwxWlZkR2RHSkZRbmxhVjFwNkNrd3pVbWhhTTAxMlpHcEJkVTFVV1hoTWFrRjNUMEZaUzB0M1dVSkNRVWRFZG5wQlFrVjNVWEZFUTJjeVRWUnNhMDFYVVhkYVJFRXlXbFJyTUU5RWFHZ0tUa2RGZWs5SFZUVlBWRTV0VFVSck1rMUVWbWxaYlUxNFRUSlNhVTVVYTNsTlFsRkhRMmx6UjBGUlVVSm5OemgzUVZKUlJVSm5kMFZqU0ZaNllVUkNNZ3BDWjI5eVFtZEZSVUZaVHk5TlFVVldRa2RuVFZwdGFEQmtTRUo2VDJrNGRsb3liREJoU0ZacFRHMU9kbUpUT1haalIxWjFURmhTYkdKSFZuUmFXRko1Q21WVE9YWmpSMVoxWkVkV2MxcFhNV3hrU0VvMVRGZE9kbUpIZUd4Wk0xSjJZMmt4ZVZwWGVHeFpXRTVzWTNrNWFGa3pVbkJpTWpWNlRETktNV0p1VFhZS1RYcFZkMDVFWnpGT2VtY3pUVVJyZGxsWVVqQmFWekYzWkVoTmRrMVVRVmRDWjI5eVFtZEZSVUZaVHk5TlFVVlhRa0ZuVFVKdVFqRlpiWGh3V1hwQ1lncENaMjl5UW1kRlJVRlpUeTlOUVVWWlFrVXdUVk16U214alJ6ZzJZak5DYkdKcE1UQmFWM2hzWWxkV01HTnVhM1ppTTBKc1ltNVNiR0pIVm5SYVdGSjVDbVZUTVdwaU1uaHpXbGRPTUdJelNYUmpiVlp6V2xkR2VscFlUVFpqYlZadFQyNUtiRnB1VFhaa1IwWnVZM2s1TWsxRE5IaE9ha1YxVFVSRFFtbDNXVXNLUzNkWlFrSkJTRmRsVVVsRlFXZFNPVUpJYzBGbFVVSXpRVTR3T1UxSGNrZDRlRVY1V1hoclpVaEtiRzVPZDB0cFUydzJORE5xZVhRdk5HVkxZMjlCZGdwTFpUWlBRVUZCUW05TFozRjZWamhCUVVGUlJFRkZaM2RTWjBsb1FVOTNVVzkyTUdWaFkyTmhRM0pNUzBFMU5qbFlVM0p6ZUZOWFJsWnRjbTk1TUhsRUNrcDJSbFpWZDNoWFFXbEZRWEZtVDBaV1JtbENaV2hFVHpkUFluQnFabEJVUkZkcGEzUnBjbXRwYVRod2RsbERNSFZyVDBKMlYwMTNRMmRaU1V0dldra0tlbW93UlVGM1RVUmFkMEYzV2tGSmQyRlhVMHhIZEV3M2NWSm5WSEp5YUVoc1MyUkdOVFZsUW1zemMxRm9hREZTUkV0WldURXpSVXBSWlV0MldYaEROQXB5TkhCdFExRTFSRUZKTWxSRlFtbzVRV3BDZFZKVlJXdHRUR3BXYm5oMlVWUmtTWFZJVUcxWGJFSkNWbmN5Y0dSalkxTmpRV1IxY0M5dWEwTjBRVkZSQ2twTmNXeEVhMmc0UTB0TFNGUnBiRlZqT0VrOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9XX19" + } + ], + "timestampVerificationData": { + "rfc3161Timestamps": [ + { + "signedTimestamp": "MIICyjADAgEAMIICwQYJKoZIhvcNAQcCoIICsjCCAq4CAQMxDTALBglghkgBZQMEAgEwgbgGCyqGSIb3DQEJEAEEoIGoBIGlMIGiAgEBBgkrBgEEAYO/MAIwMTANBglghkgBZQMEAgEFAAQgvQoU8ovkZhL+7BB/uM1cGZFk0AQrq9y8TRT4vPoNaVwCFQDuv5ztsYi9EH7pjXTXPckjr/caWxgPMjAyNjA5MTYwMzAyNTJaMAMCAQGgMqQwMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhoAAxggHbMIIB1wIBATBRMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQCFDoTVC8MkGHuvMFDL8uKjosqI4sMMAsGCWCGSAFlAwQCAaCB/DAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTI2MDkxNjAzMDI1MlowLwYJKoZIhvcNAQkEMSIEIOz0sDFJLA6QV1WsQ8ucM9FSu7t//CX+lN7PbmZGdy5CMIGOBgsqhkiG9w0BCRACLzF/MH0wezB5BCCF+Se8B6tiysO0Q1bBDvyBssaIP9p6uebYcNnROs0FtzBVMD2kOzA5MRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxIDAeBgNVBAMTF3NpZ3N0b3JlLXRzYS1zZWxmc2lnbmVkAhQ6E1QvDJBh7rzBQy/Lio6LKiOLDDAKBggqhkjOPQQDAgRnMGUCMC4js+ytL8Q7UiQ5ZeU0fdCfVWhIL2dEXJ1QP509uvaIzeoHimkIU3KBz38wCTfDpQIxAIj/OYfKjzi4CY+S2IIHhij/H3UIcc8FW3Svj0Kl91W2DQYn88J3SGjrYgXJBqBoDg==" + } + ] + } + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJkaWdlc3QiOnsic2hhMjU2IjoiZmQzMjhkZTI1NTI0NjZhZDc4Mzg1ZTFiMTI4OWMzZjI0MDJiMWM0NWYyNjViMjUyYWFiMTk1NWI0Mjg0NWFjMSJ9LCJhbm5vdGF0aW9ucyI6e319XSwicHJlZGljYXRlVHlwZSI6Imh0dHBzOi8vc2lnc3RvcmUuZGV2L2Nvc2lnbi9zaWduL3YxIiwicHJlZGljYXRlIjp7fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQDiiwTl90NV6A+krtufI5OzMQ7Zwr96qdt+dNFAHecsqAIgJGCZlSk+X1IGOybSQkYsK1jXomyl4rvK2D7G4bOZvwY=" + } + ] + } +} diff --git a/testdata/scripts/basic.txt b/testdata/scripts/basic.txt index ca72fac3..8148aeb3 100644 --- a/testdata/scripts/basic.txt +++ b/testdata/scripts/basic.txt @@ -10,7 +10,7 @@ stderr 'proxy image .*verification' dependabot update go_modules dependabot/cli --updater-image dummy-updater --skip-image-verification # assert the dummy is working -stderr 'WARNING: updater and proxy image verification is disabled' +stderr 'WARNING: updater, proxy, and OpenTelemetry collector image verification is disabled' stderr 'bin/run arguments: fetch_files' stderr 'bin/run arguments: update_files'