From 3c3be926aaea573cf94931877e8072aefb231b60 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Tue, 28 Jul 2026 18:45:24 +0300 Subject: [PATCH 01/10] fix(chart): normalize StampModTimes timestamp + Chart.lock test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #32396. StampModTimes now normalizes the supplied time to UTC and truncates to whole seconds before stamping. Without this, an SDK caller passing a local-timezone or sub-second time.Time would produce a Chart.lock generated: field with a timezone offset or fractional seconds, making the lock file content non-reproducible across machines even when the same SOURCE_DATE_EPOCH value is used — defeating the purpose of the feature. Also add: - testdata/charts/chart-with-lock fixture: a minimal chart with a Chart.lock whose generated: timestamp predates SOURCE_DATE_EPOCH. - TestRunWithSourceDateEpochStampsLockGenerated: packages the fixture with SourceDateEpoch set to a non-UTC, sub-second time.Time and asserts that the resulting archive's Chart.lock entry has both the correct tar modtime and the correct (normalized) generated: value in the marshaled YAML. Signed-off-by: Ilya Kiselev --- pkg/action/package_test.go | 59 +++++++++++++++++++ .../charts/chart-with-lock/Chart.lock | 3 + .../charts/chart-with-lock/Chart.yaml | 4 ++ pkg/chart/v2/chart.go | 27 +++++---- 4 files changed, 83 insertions(+), 10 deletions(-) create mode 100644 pkg/action/testdata/charts/chart-with-lock/Chart.lock create mode 100644 pkg/action/testdata/charts/chart-with-lock/Chart.yaml diff --git a/pkg/action/package_test.go b/pkg/action/package_test.go index 421f34b33..d216d8341 100644 --- a/pkg/action/package_test.go +++ b/pkg/action/package_test.go @@ -17,9 +17,13 @@ limitations under the License. package action import ( + "archive/tar" + "compress/gzip" + "io" "os" "path" "testing" + "time" "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" @@ -144,3 +148,58 @@ func TestRun(t *testing.T) { require.Equal(t, "empty-0.1.0.tgz", filename) require.NoError(t, os.Remove(filename)) } + +// TestRunWithSourceDateEpochStampsLockGenerated verifies that packaging a chart +// that has a Chart.lock stamps both the tar entry modtime and the marshaled +// generated: field in Chart.lock to the given epoch. +// +// This guards against the normalization regression where a caller supplying a +// local-timezone or sub-second time.Time would produce a non-reproducible +// generated: value even when the same SOURCE_DATE_EPOCH is used on different +// machines. +func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) { + // Use a non-local, non-UTC timezone and sub-second precision to confirm + // normalization: without UTC().Truncate(time.Second) the generated: field + // would contain a timezone offset or fractional seconds. + loc := time.FixedZone("UTC+3", 3*60*60) + rawEpoch := time.Unix(1700000000, 123456789).In(loc) + epoch := rawEpoch.UTC().Truncate(time.Second) + + client := NewPackage() + client.SourceDateEpoch = &rawEpoch + + filename, err := client.Run("testdata/charts/chart-with-lock", nil) + require.NoError(t, err) + t.Cleanup(func() { os.Remove(filename) }) + + f, err := os.Open(filename) + require.NoError(t, err) + defer f.Close() + + gr, err := gzip.NewReader(f) + require.NoError(t, err) + defer gr.Close() + + found := false + tr := tar.NewReader(gr) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + require.NoError(t, err) + if path.Base(hdr.Name) != "Chart.lock" { + continue + } + found = true + require.True(t, epoch.Equal(hdr.ModTime), + "Chart.lock tar modtime: got %v, want %v", hdr.ModTime, epoch) + + raw, err := io.ReadAll(tr) + require.NoError(t, err) + wantGenerated := epoch.Format(time.RFC3339) + require.Contains(t, string(raw), wantGenerated, + "Chart.lock generated: field should contain normalized UTC timestamp") + } + require.True(t, found, "expected archive to contain a Chart.lock entry") +} diff --git a/pkg/action/testdata/charts/chart-with-lock/Chart.lock b/pkg/action/testdata/charts/chart-with-lock/Chart.lock new file mode 100644 index 000000000..824df0a40 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-lock/Chart.lock @@ -0,0 +1,3 @@ +dependencies: [] +digest: sha256:0000000000000000000000000000000000000000000000000000000000000 +generated: "2016-01-01T00:00:00Z" diff --git a/pkg/action/testdata/charts/chart-with-lock/Chart.yaml b/pkg/action/testdata/charts/chart-with-lock/Chart.yaml new file mode 100644 index 000000000..2cb1b3557 --- /dev/null +++ b/pkg/action/testdata/charts/chart-with-lock/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: chart-with-lock +version: 0.1.0 +description: Test chart with a Chart.lock, used to verify SourceDateEpoch stamping diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index 9772754ce..ccfe84f51 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -177,34 +177,41 @@ func (ch *Chart) CRDObjects() []CRD { return crds } -// StampModTimes sets timestamps on the chart (and dependencies) to epoch. -// This is used for reproducible builds via SOURCE_DATE_EPOCH. -func (ch *Chart) StampModTimes(epoch time.Time) { - ch.ModTime = epoch +// StampModTimes sets timestamps on the chart (and dependencies) to t, +// normalized to UTC and truncated to whole seconds. +// +// Normalization is required because Chart.lock's generated: field is written +// by yaml.Marshal from Lock.Generated. Without UTC/truncate, a caller +// supplying a local-zone or sub-second time.Time produces a generated: value +// with a timezone offset or fractional seconds, making the lock file content +// non-reproducible across machines even when the same SOURCE_DATE_EPOCH is used. +func (ch *Chart) StampModTimes(t time.Time) { + t = t.UTC().Truncate(time.Second) + ch.ModTime = t if len(ch.Schema) > 0 { - ch.SchemaModTime = epoch + ch.SchemaModTime = t } if ch.Lock != nil { - ch.Lock.Generated = epoch + ch.Lock.Generated = t } for _, f := range ch.Raw { if f != nil { - f.ModTime = epoch + f.ModTime = t } } for _, f := range ch.Templates { if f != nil { - f.ModTime = epoch + f.ModTime = t } } for _, f := range ch.Files { if f != nil { - f.ModTime = epoch + f.ModTime = t } } for _, dep := range ch.Dependencies() { - dep.StampModTimes(epoch) + dep.StampModTimes(t) } } From e92316751375bafd4621232bb39a1238a22e91c4 Mon Sep 17 00:00:00 2001 From: Ilya Kiselev Date: Tue, 28 Jul 2026 19:05:56 +0300 Subject: [PATCH 02/10] test(package): fix errorlint + exact path match in lock test - Replace `err == io.EOF` with `errors.Is(err, io.EOF)` (errorlint). - Match Chart.lock by full tar path ("chart-with-lock/Chart.lock") instead of path.Base to avoid false positives from subchart lock files. - Break out of the loop after finding the entry (early exit). Signed-off-by: Ilya Kiselev --- pkg/action/package_test.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/action/package_test.go b/pkg/action/package_test.go index d216d8341..2b359fa52 100644 --- a/pkg/action/package_test.go +++ b/pkg/action/package_test.go @@ -19,6 +19,7 @@ package action import ( "archive/tar" "compress/gzip" + "errors" "io" "os" "path" @@ -180,15 +181,16 @@ func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) { require.NoError(t, err) defer gr.Close() + const wantPath = "chart-with-lock/Chart.lock" found := false tr := tar.NewReader(gr) for { hdr, err := tr.Next() - if err == io.EOF { + if errors.Is(err, io.EOF) { break } require.NoError(t, err) - if path.Base(hdr.Name) != "Chart.lock" { + if hdr.Name != wantPath { continue } found = true @@ -200,6 +202,7 @@ func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) { wantGenerated := epoch.Format(time.RFC3339) require.Contains(t, string(raw), wantGenerated, "Chart.lock generated: field should contain normalized UTC timestamp") + break } - require.True(t, found, "expected archive to contain a Chart.lock entry") + require.True(t, found, "expected archive to contain %q entry", wantPath) } From 601445e88cf1fe66b2885c793d564030aad1d707 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Sat, 8 Aug 2026 11:46:06 +0500 Subject: [PATCH 03/10] fix(loader): do not drop values files ending at a 4096-byte boundary YAMLReader can return EOF without yielding the last line when that line has no trailing newline and its length is a multiple of bufio's default buffer. Read the file fully and ensure a trailing newline before parsing so compact JSON values files are not silently ignored. Fixes #32506 Signed-off-by: Dean Chen <862469039@qq.com> --- internal/chart/v3/loader/load.go | 14 +++++++++++++- internal/chart/v3/loader/load_test.go | 23 +++++++++++++++++++++++ pkg/chart/v2/loader/load.go | 14 +++++++++++++- pkg/chart/v2/loader/load_test.go | 23 +++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 48f346ccf..0ff040eae 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -183,8 +183,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // The reader is expected to contain one or more YAML documents, the values of which are merged. // And the values can be either a chart's default values or user-supplied values. func LoadValues(data io.Reader) (map[string]any, error) { + // Read fully first. YAMLReader/LineReader can drop a final unterminated + // line when its length is an exact multiple of bufio.Reader's default + // buffer (4096). Appending a trailing newline avoids that case. + // See https://github.com/helm/helm/issues/32506 + b, err := io.ReadAll(data) + if err != nil { + return nil, err + } + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + values := map[string]any{} - reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) for { currentMap := map[string]any{} raw, err := reader.Read() diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index dd5fdc8db..f105b248f 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -419,6 +419,29 @@ foo: } } + +func TestLoadValuesEOFBoundary(t *testing.T) { + // Reproduces #32506: a single logical line whose length is a multiple of + // bufio's default buffer (4096) and has no trailing newline used to be + // dropped entirely by YAMLReader, yielding empty values. + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := 4096 - len(prefix) - len(suffix) + data := make([]byte, 0, 4096) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != 4096 { + t.Fatalf("test setup: want data length 4096, got %d", len(data)) + } + + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) +} + func TestMergeValuesV3(t *testing.T) { nestedMap := map[string]any{ "foo": "bar", diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 28115d062..d7b125b9b 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -210,8 +210,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // The reader is expected to contain one or more YAML documents, the values of which are merged. // And the values can be either a chart's default values or user-supplied values. func LoadValues(data io.Reader) (map[string]any, error) { + // Read fully first. YAMLReader/LineReader can drop a final unterminated + // line when its length is an exact multiple of bufio.Reader's default + // buffer (4096). Appending a trailing newline avoids that case. + // See https://github.com/helm/helm/issues/32506 + b, err := io.ReadAll(data) + if err != nil { + return nil, err + } + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + values := map[string]any{} - reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) for { currentMap := map[string]any{} raw, err := reader.Read() diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 8bf254321..b4836ee95 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -463,6 +463,29 @@ foo: } } + +func TestLoadValuesEOFBoundary(t *testing.T) { + // Reproduces #32506: a single logical line whose length is a multiple of + // bufio's default buffer (4096) and has no trailing newline used to be + // dropped entirely by YAMLReader, yielding empty values. + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := 4096 - len(prefix) - len(suffix) + data := make([]byte, 0, 4096) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != 4096 { + t.Fatalf("test setup: want data length 4096, got %d", len(data)) + } + + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) +} + func TestMergeValuesV2(t *testing.T) { nestedMap := map[string]any{ "foo": "bar", From fd4ed4952b5f9f54344a0c014f7acd7885bd60c6 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Sat, 8 Aug 2026 19:45:24 +0500 Subject: [PATCH 04/10] test(loader): cover 8192-byte EOF boundary for LoadValues Also drop the extra blank line before the test. Addresses review on #32525. Signed-off-by: Dean Chen <862469039@qq.com> --- internal/chart/v3/loader/load_test.go | 37 +++++++++++++++------------ pkg/chart/v2/loader/load_test.go | 37 +++++++++++++++------------ 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index f105b248f..e34364f39 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "log" "os" @@ -419,27 +420,31 @@ foo: } } - func TestLoadValuesEOFBoundary(t *testing.T) { // Reproduces #32506: a single logical line whose length is a multiple of // bufio's default buffer (4096) and has no trailing newline used to be // dropped entirely by YAMLReader, yielding empty values. - prefix := []byte(`{"foo":"`) - suffix := []byte(`"}`) - pad := 4096 - len(prefix) - len(suffix) - data := make([]byte, 0, 4096) - data = append(data, prefix...) - data = append(data, bytes.Repeat([]byte("x"), pad)...) - data = append(data, suffix...) - if len(data) != 4096 { - t.Fatalf("test setup: want data length 4096, got %d", len(data)) - } + // Also cover 8192 (2x buffer) so we do not only hit the single-buffer case. + for _, size := range []int{4096, 8192} { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := size - len(prefix) - len(suffix) + data := make([]byte, 0, size) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != size { + t.Fatalf("test setup: want data length %d, got %d", size, len(data)) + } - values, err := LoadValues(bytes.NewReader(data)) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": string(bytes.Repeat([]byte("x"), pad)), - }, values) + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) + }) + } } func TestMergeValuesV3(t *testing.T) { diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index b4836ee95..ae1da1130 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "log" "os" @@ -463,27 +464,31 @@ foo: } } - func TestLoadValuesEOFBoundary(t *testing.T) { // Reproduces #32506: a single logical line whose length is a multiple of // bufio's default buffer (4096) and has no trailing newline used to be // dropped entirely by YAMLReader, yielding empty values. - prefix := []byte(`{"foo":"`) - suffix := []byte(`"}`) - pad := 4096 - len(prefix) - len(suffix) - data := make([]byte, 0, 4096) - data = append(data, prefix...) - data = append(data, bytes.Repeat([]byte("x"), pad)...) - data = append(data, suffix...) - if len(data) != 4096 { - t.Fatalf("test setup: want data length 4096, got %d", len(data)) - } + // Also cover 8192 (2x buffer) so we do not only hit the single-buffer case. + for _, size := range []int{4096, 8192} { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := size - len(prefix) - len(suffix) + data := make([]byte, 0, size) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != size { + t.Fatalf("test setup: want data length %d, got %d", size, len(data)) + } - values, err := LoadValues(bytes.NewReader(data)) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": string(bytes.Repeat([]byte("x"), pad)), - }, values) + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) + }) + } } func TestMergeValuesV2(t *testing.T) { From edb94f897d1e0ec3335fb7268f07e0fd2666fbb8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:43:12 -0600 Subject: [PATCH 05/10] chore(deps): bump golang.org/x/text from 0.40.0 to 0.41.0 (#32543) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.40.0 to 0.41.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.40.0...v0.41.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.41.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 6203377b7..a2f06e39f 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.54.0 golang.org/x/term v0.45.0 - golang.org/x/text v0.40.0 + golang.org/x/text v0.41.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.36.3 k8s.io/apiextensions-apiserver v0.36.3 @@ -158,13 +158,13 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/tools v0.48.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc v1.82.1 // indirect diff --git a/go.sum b/go.sum index 1b7a58218..75ac7bb0e 100644 --- a/go.sum +++ b/go.sum @@ -393,8 +393,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -405,8 +405,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -453,8 +453,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -463,8 +463,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= From 2a29f1770b62844b27197d2507377361d45ad7c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:50:55 -0600 Subject: [PATCH 06/10] chore(deps): bump golang.org/x/crypto from 0.54.0 to 0.55.0 (#32545) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.54.0 to 0.55.0. - [Commits](https://github.com/golang/crypto/compare/v0.54.0...v0.55.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.55.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a2f06e39f..4c0468708 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.12.0 go.yaml.in/yaml/v3 v3.0.5 - golang.org/x/crypto v0.54.0 + golang.org/x/crypto v0.55.0 golang.org/x/term v0.45.0 golang.org/x/text v0.41.0 gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 75ac7bb0e..35d9e4465 100644 --- a/go.sum +++ b/go.sum @@ -387,8 +387,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= From bafdcde8ecd83119afd7f98363b364138d3b35dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:44:55 -0600 Subject: [PATCH 07/10] chore(deps): bump github.com/stretchr/testify from 1.11.1 to 1.12.0 (#32555) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.11.1...v1.12.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-version: 1.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 4c0468708..6a691efac 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.0 github.com/tetratelabs/wazero v1.12.0 go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.55.0 diff --git a/go.sum b/go.sum index 35d9e4465..54d14492a 100644 --- a/go.sum +++ b/go.sum @@ -301,14 +301,14 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= +github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= From c000a408b81266d328db64aea2ff9e2e70844790 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:58:11 -0600 Subject: [PATCH 08/10] chore(deps): bump the github-actions group across 1 directory with 4 updates (#32556) Bumps the github-actions group with 4 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/autobuild](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [github/codeql-action/upload-sarif](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.37.6 to 4.37.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd) Updates `github/codeql-action/autobuild` from 4.37.6 to 4.37.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd) Updates `github/codeql-action/analyze` from 4.37.6 to 4.37.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd) Updates `github/codeql-action/upload-sarif` from 4.37.6 to 4.37.7 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/5595ccaf912efad79be6eef63a5619ff05969be3...ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd) --- updated-dependencies: - dependency-name: github/codeql-action/analyze dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/autobuild dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/init dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.37.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 5e392cc2c..6bfec7108 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # pinv4.37.7 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6 + uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # pinv4.37.7 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -73,4 +73,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # pinv4.37.7 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 553a6b3da..0a12abf49 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -64,6 +64,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6 + uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: results.sarif From e9b85e4de16ad45805843fb121748018cf89094b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:58:20 -0600 Subject: [PATCH 09/10] chore(deps): bump github.com/stretchr/testify from 1.12.0 to 1.12.1 (#32562) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.12.0 to 1.12.1. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.12.0...v1.12.1) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-version: 1.12.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6a691efac..c34a80649 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 - github.com/stretchr/testify v1.12.0 + github.com/stretchr/testify v1.12.1 github.com/tetratelabs/wazero v1.12.0 go.yaml.in/yaml/v3 v3.0.5 golang.org/x/crypto v0.55.0 diff --git a/go.sum b/go.sum index 54d14492a..bfac195f1 100644 --- a/go.sum +++ b/go.sum @@ -307,8 +307,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= -github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= From 67d54fd8808ab47e93dad40f4238d5fe28d1efd0 Mon Sep 17 00:00:00 2001 From: Ruslan Shaydullin Date: Sun, 23 Aug 2026 17:24:34 +0500 Subject: [PATCH 10/10] fix(provenance): support GnuPG keybox (pubring.kbx) keyrings (#32281) * fix(provenance): support GnuPG keybox (pubring.kbx) keyrings Starting with GnuPG 2.1, file-backed public keyrings can use ~/.gnupg/pubring.kbx instead of the legacy pubring.gpg. Helm only read the legacy format, so chart and plugin verification failed on installations using the file-backed keybox. Make the keyring loader format-aware: - GnuPG keybox (pubring.kbx): extract OpenPGP keyblocks from the keybox container without adding a dependency. Skip ephemeral blobs, matching GnuPG's own read behavior. - ASCII-armored keyrings: load single or concatenated exports. - Legacy binary packet streams (pubring.gpg): retain the existing path. defaultKeyring() falls back to pubring.kbx when pubring.gpg is absent. pubring.gpg keeps precedence when both files exist. This change covers file-backed public keyrings. It does not read the SQLite database used by keyboxd, which needs a separate design. Related to #31836 Signed-off-by: Ruslan Shaydullin * fix(provenance): treat only not-exist as keyring absence in defaultKeyring A stat error other than 'not exist' (e.g. a permission problem) meant the file may well be present, but defaultKeyring() skipped past it: an unreadable pubring.gpg silently lost precedence to pubring.kbx, and the surfaced error could point at the wrong file. Treat only fs.ErrNotExist as absence. For any other stat error, return that path unchanged so the real error surfaces when the keyring is opened. Signed-off-by: Ruslan Shaydullin --------- Signed-off-by: Ruslan Shaydullin Co-authored-by: George Jenkins --- pkg/cmd/dependency_build.go | 21 +- pkg/cmd/dependency_build_test.go | 66 ++++++ pkg/provenance/keybox.go | 113 +++++++++++ pkg/provenance/keybox_test.go | 188 ++++++++++++++++++ pkg/provenance/sign.go | 58 +++++- pkg/provenance/sign_test.go | 93 +++++++++ .../testdata/helm-mixed-keyring.asc | 40 ++++ .../testdata/helm-mixed-keyring.kbx | Bin 0 -> 1865 bytes pkg/provenance/testdata/helm-test-key.asc | 30 +++ pkg/provenance/testdata/helm-test-key.kbx | Bin 0 -> 1465 bytes .../testdata/regen-keyring-formats.sh | 26 +++ 11 files changed, 630 insertions(+), 5 deletions(-) create mode 100644 pkg/provenance/keybox.go create mode 100644 pkg/provenance/keybox_test.go create mode 100644 pkg/provenance/testdata/helm-mixed-keyring.asc create mode 100644 pkg/provenance/testdata/helm-mixed-keyring.kbx create mode 100644 pkg/provenance/testdata/helm-test-key.asc create mode 100644 pkg/provenance/testdata/helm-test-key.kbx create mode 100755 pkg/provenance/testdata/regen-keyring-formats.sh diff --git a/pkg/cmd/dependency_build.go b/pkg/cmd/dependency_build.go index 85c385670..2fb997e4c 100644 --- a/pkg/cmd/dependency_build.go +++ b/pkg/cmd/dependency_build.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "io" + "io/fs" "os" "path/filepath" @@ -97,9 +98,25 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { } // defaultKeyring returns the expanded path to the default keyring. +// +// The legacy pubring.gpg file is preferred and treated as absent only when +// stat fails with "not exist" — any other stat error (e.g. permissions) +// keeps the legacy path so the real error surfaces when the file is opened. +// If the legacy file is absent, the file-backed pubring.kbx path takes over +// under the same rule. When neither exists, the legacy path is returned so +// error messages keep pointing at the traditional default. func defaultKeyring() string { + gnupgHome := filepath.Join(homedir.HomeDir(), ".gnupg") if v, ok := os.LookupEnv("GNUPGHOME"); ok { - return filepath.Join(v, "pubring.gpg") + gnupgHome = v } - return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg") + legacy := filepath.Join(gnupgHome, "pubring.gpg") + if _, err := os.Stat(legacy); !errors.Is(err, fs.ErrNotExist) { + return legacy + } + keybox := filepath.Join(gnupgHome, "pubring.kbx") + if _, err := os.Stat(keybox); !errors.Is(err, fs.ErrNotExist) { + return keybox + } + return legacy } diff --git a/pkg/cmd/dependency_build_test.go b/pkg/cmd/dependency_build_test.go index 1680c23a1..05e27076f 100644 --- a/pkg/cmd/dependency_build_test.go +++ b/pkg/cmd/dependency_build_test.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -140,3 +141,68 @@ func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) { t.Fatal(err) } } + +func TestDefaultKeyring(t *testing.T) { + touch := func(t *testing.T, path string) { + t.Helper() + require.NoError(t, os.WriteFile(path, []byte("test"), 0o644)) + } + + tests := []struct { + name string + files []string + want string + }{ + {"legacy keyring only", []string{"pubring.gpg"}, "pubring.gpg"}, + {"keybox only", []string{"pubring.kbx"}, "pubring.kbx"}, + {"legacy keyring preferred over keybox", []string{"pubring.gpg", "pubring.kbx"}, "pubring.gpg"}, + {"neither present falls back to legacy path", nil, "pubring.gpg"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + t.Setenv("GNUPGHOME", dir) + for _, f := range tt.files { + touch(t, filepath.Join(dir, f)) + } + assert.Equal(t, filepath.Join(dir, tt.want), defaultKeyring()) + }) + } + + t.Run("stat error other than not-exist keeps the legacy path", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("directory permissions are not enforced on Windows") + } + if os.Geteuid() == 0 { + t.Skip("root bypasses directory permissions") + } + + parent := t.TempDir() + dir := filepath.Join(parent, ".gnupg") + require.NoError(t, os.MkdirAll(dir, 0o700)) + touch(t, filepath.Join(dir, "pubring.kbx")) + t.Setenv("GNUPGHOME", dir) + + // Make the directory unsearchable so stat on both keyrings fails + // with a permission error rather than "not exist". + require.NoError(t, os.Chmod(dir, 0o000)) + t.Cleanup(func() { require.NoError(t, os.Chmod(dir, 0o700)) }) + + assert.Equal(t, filepath.Join(dir, "pubring.gpg"), defaultKeyring()) + }) + + t.Run("no GNUPGHOME falls back to the home directory", func(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("GNUPGHOME", home) // register restoration before unsetting + require.NoError(t, os.Unsetenv("GNUPGHOME")) + + gnupgDir := filepath.Join(home, ".gnupg") + require.NoError(t, os.MkdirAll(gnupgDir, 0o700)) + + assert.Equal(t, filepath.Join(gnupgDir, "pubring.gpg"), defaultKeyring()) + + touch(t, filepath.Join(gnupgDir, "pubring.kbx")) + assert.Equal(t, filepath.Join(gnupgDir, "pubring.kbx"), defaultKeyring()) + }) +} diff --git a/pkg/provenance/keybox.go b/pkg/provenance/keybox.go new file mode 100644 index 000000000..99f220b99 --- /dev/null +++ b/pkg/provenance/keybox.go @@ -0,0 +1,113 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provenance + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" +) + +// GnuPG 2.1+ can store file-backed public keys in a "keybox" (pubring.kbx), +// a container that interleaves OpenPGP keyblocks with GnuPG-specific +// metadata. It is not an OpenPGP packet stream, so it cannot be handed to +// openpgp.ReadKeyRing directly. A keybox is a sequence of blobs, each starting +// with: +// +// byte 0..3 u32 blob length, big endian, including this header +// byte 4 u8 blob type (0 empty, 1 header, 2 OpenPGP, 3 X.509) +// byte 5 u8 blob version +// +// The first blob is a header carrying the "KBXf" magic at offset 8. OpenPGP +// blobs (type 2) record where the raw keyblock lives inside the blob: +// +// byte 8..11 u32 keyblock offset, relative to the blob start +// byte 12..15 u32 keyblock length +// +// Reference: kbx/keybox-blob.c in the GnuPG source tree. +const ( + kbxBlobTypeHeader = 1 + kbxBlobTypeOpenPGP = 2 + + // kbxBlobFlagEphemeral marks a blob GnuPG considers not (yet) part of + // the keyring, e.g. written during an interrupted keyserver operation + // (KEYBOX_FLAG_BLOB_EPHEMERAL in kbx/keybox.h). GnuPG skips such blobs + // on every normal read (kbx/keybox-search.c), and so do we. + kbxBlobFlagEphemeral = 0x0002 + + // kbxMinBlobLen covers the length and type fields present in every blob. + kbxMinBlobLen = 5 + + // kbxOpenPGPHeaderLen is how much of an OpenPGP blob header must be + // present for the flags, keyblock offset and keyblock length fields to + // be readable. + kbxOpenPGPHeaderLen = 16 +) + +// isKeybox reports whether data looks like a GnuPG keybox (pubring.kbx) +// image, identified by the "KBXf" magic in the mandatory first header blob. +func isKeybox(data []byte) bool { + return len(data) >= 12 && data[4] == kbxBlobTypeHeader && string(data[8:12]) == "KBXf" +} + +// isArmored reports whether data looks like an ASCII-armored keyring, as +// produced by `gpg --export --armor`. +func isArmored(data []byte) bool { + return bytes.HasPrefix(bytes.TrimSpace(data), []byte("-----BEGIN PGP")) +} + +// keyboxPublicKeys extracts the OpenPGP keyblocks embedded in a keybox image +// and returns them concatenated, ready for openpgp.ReadKeyRing. Blobs of any +// other type (header, X.509, empty) are skipped, as are blobs flagged +// ephemeral, which GnuPG itself ignores when reading the keyring. Malformed +// input yields an error, never a panic. +func keyboxPublicKeys(data []byte) ([]byte, error) { + var keyblocks bytes.Buffer + for offset := 0; offset < len(data); { + rest := data[offset:] + if len(rest) < kbxMinBlobLen { + return nil, fmt.Errorf("truncated blob header at offset %d", offset) + } + blobLen := binary.BigEndian.Uint32(rest) + if blobLen < kbxMinBlobLen { + return nil, fmt.Errorf("invalid blob length %d at offset %d", blobLen, offset) + } + if uint64(blobLen) > uint64(len(rest)) { + return nil, fmt.Errorf("blob at offset %d has length %d exceeding the %d remaining bytes", offset, blobLen, len(rest)) + } + blob := rest[:blobLen] + if blob[4] == kbxBlobTypeOpenPGP { + if len(blob) < kbxOpenPGPHeaderLen { + return nil, fmt.Errorf("OpenPGP blob at offset %d is too short", offset) + } + flags := binary.BigEndian.Uint16(blob[6:]) + keyblockOffset := binary.BigEndian.Uint32(blob[8:]) + keyblockLen := binary.BigEndian.Uint32(blob[12:]) + if uint64(keyblockOffset)+uint64(keyblockLen) > uint64(len(blob)) { + return nil, fmt.Errorf("OpenPGP blob at offset %d has an out-of-range keyblock", offset) + } + if flags&kbxBlobFlagEphemeral == 0 { + keyblocks.Write(blob[keyblockOffset : keyblockOffset+keyblockLen]) + } + } + offset += int(blobLen) + } + if keyblocks.Len() == 0 { + return nil, errors.New("keybox contains no OpenPGP keys") + } + return keyblocks.Bytes(), nil +} diff --git a/pkg/provenance/keybox_test.go b/pkg/provenance/keybox_test.go new file mode 100644 index 000000000..e2133d45a --- /dev/null +++ b/pkg/provenance/keybox_test.go @@ -0,0 +1,188 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provenance + +import ( + "bytes" + "encoding/binary" + "os" + "testing" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + // testKeybox is a GnuPG keybox (pubring.kbx) containing the helm-test + // public key. Regenerate with testdata/regen-keyring-formats.sh. + testKeybox = "testdata/helm-test-key.kbx" + + // testMixedKeybox is a keybox containing the RSA and Ed25519 test keys. + testMixedKeybox = "testdata/helm-mixed-keyring.kbx" + + // testArmoredPubfile is the ASCII-armored export of the helm-test key. + testArmoredPubfile = "testdata/helm-test-key.asc" + + // testMultiBlockArmored is two concatenated single-key armored exports + // (cat key1.asc key2.asc), covering the RSA and Ed25519 test keys. + testMultiBlockArmored = "testdata/helm-mixed-keyring.asc" +) + +func TestIsKeybox(t *testing.T) { + tests := []struct { + name string + file string + want bool + }{ + {"keybox", testKeybox, true}, + {"mixed keybox", testMixedKeybox, true}, + {"legacy binary keyring", testPubfile, false}, + {"armored keyring", testArmoredPubfile, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := os.ReadFile(tt.file) + require.NoError(t, err) + assert.Equal(t, tt.want, isKeybox(data)) + }) + } + + t.Run("degenerate inputs", func(t *testing.T) { + assert.False(t, isKeybox(nil)) + assert.False(t, isKeybox([]byte{})) + assert.False(t, isKeybox([]byte("KBXf"))) + assert.False(t, isKeybox([]byte("garbage that is longer than twelve bytes"))) + }) +} + +func TestIsArmored(t *testing.T) { + tests := []struct { + name string + file string + want bool + }{ + {"armored keyring", testArmoredPubfile, true}, + {"legacy binary keyring", testPubfile, false}, + {"keybox", testKeybox, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := os.ReadFile(tt.file) + require.NoError(t, err) + assert.Equal(t, tt.want, isArmored(data)) + }) + } + + t.Run("degenerate inputs", func(t *testing.T) { + assert.False(t, isArmored(nil)) + assert.False(t, isArmored([]byte("not a key"))) + assert.True(t, isArmored([]byte("\n\t -----BEGIN PGP PUBLIC KEY BLOCK-----"))) + }) +} + +func TestKeyboxPublicKeys(t *testing.T) { + data, err := os.ReadFile(testKeybox) + require.NoError(t, err) + + keys, err := keyboxPublicKeys(data) + require.NoError(t, err) + + ring, err := openpgp.ReadKeyRing(bytes.NewReader(keys)) + require.NoError(t, err) + + require.Len(t, ring, 1) + _, ok := ring[0].Identities[testKeyName] + assert.True(t, ok, "expected keybox to contain %q", testKeyName) +} + +func TestKeyboxPublicKeysEphemeral(t *testing.T) { + // GnuPG flags in-progress key material as ephemeral (bit 0x0002 of the + // blob flags at blob offset 6) and hides it from every normal read; the + // parser must do the same. + setFlags := func(t *testing.T, data []byte, blobStart int, flags uint16) []byte { + t.Helper() + require.Equal(t, byte(kbxBlobTypeOpenPGP), data[blobStart+4]) + mutated := bytes.Clone(data) + binary.BigEndian.PutUint16(mutated[blobStart+6:], flags) + return mutated + } + + t.Run("all blobs ephemeral means no keys", func(t *testing.T) { + valid, err := os.ReadFile(testKeybox) + require.NoError(t, err) + + _, err = keyboxPublicKeys(setFlags(t, valid, 32, kbxBlobFlagEphemeral)) + assert.ErrorContains(t, err, "no OpenPGP keys") + }) + + t.Run("ephemeral blob is skipped, others kept", func(t *testing.T) { + valid, err := os.ReadFile(testMixedKeybox) + require.NoError(t, err) + + // Flag only the first OpenPGP blob (the RSA helm-test key). + keys, err := keyboxPublicKeys(setFlags(t, valid, 32, kbxBlobFlagEphemeral)) + require.NoError(t, err) + + ring, err := openpgp.ReadKeyRing(bytes.NewReader(keys)) + require.NoError(t, err) + + require.Len(t, ring, 1) + _, ok := ring[0].Identities[testKeyName] + assert.False(t, ok, "expected the ephemeral-flagged %q blob to be skipped", testKeyName) + }) +} + +func TestKeyboxPublicKeysMalformed(t *testing.T) { + valid, err := os.ReadFile(testKeybox) + require.NoError(t, err) + + // The mutations below rely on the fixture layout: a 32-byte header blob + // followed by an OpenPGP blob. + const blobStart = 32 + require.Greater(t, len(valid), blobStart+16) + require.Equal(t, byte(kbxBlobTypeOpenPGP), valid[blobStart+4]) + + mutate := func(offset int, value uint32) []byte { + data := bytes.Clone(valid) + binary.BigEndian.PutUint32(data[offset:], value) + return data + } + + tests := []struct { + name string + data []byte + }{ + {"header only, no keys", valid[:blobStart]}, + {"truncated inside blob header", valid[:blobStart+2]}, + {"truncated inside blob body", valid[:blobStart+16]}, + {"zero blob length", mutate(blobStart, 0)}, + {"blob length below minimum", mutate(blobStart, 4)}, + {"blob length past end of data", mutate(blobStart, uint32(len(valid))+1)}, + {"keyblock offset out of range", mutate(blobStart+8, uint32(len(valid)))}, + {"keyblock length out of range", mutate(blobStart+12, uint32(len(valid)))}, + {"keyblock offset overflow", mutate(blobStart+8, ^uint32(0))}, + {"keyblock length overflow", mutate(blobStart+12, ^uint32(0))}, + {"openpgp blob shorter than its header", append(bytes.Clone(valid[:blobStart]), 0, 0, 0, 8, kbxBlobTypeOpenPGP, 1, 0, 0)}, + {"empty input", nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := keyboxPublicKeys(tt.data) + assert.Error(t, err) + }) + } +} diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 90b7559e5..b4d5f7541 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/clearsign" "github.com/ProtonMail/go-crypto/openpgp/packet" "sigs.k8s.io/yaml" @@ -357,13 +358,64 @@ func loadKey(keypath string) (*openpgp.Entity, error) { return openpgp.ReadEntity(pr) } +// loadKeyRing loads a keyring from ringpath, auto-detecting its storage +// format. Three formats are supported: +// +// - the legacy binary OpenPGP packet stream (GnuPG's pubring.gpg) +// - a GnuPG file-backed keybox (pubring.kbx), introduced in GnuPG 2.1 +// - an ASCII-armored keyring, as produced by `gpg --export --armor` func loadKeyRing(ringpath string) (openpgp.EntityList, error) { - f, err := os.Open(ringpath) + data, err := os.ReadFile(ringpath) if err != nil { return nil, err } - defer f.Close() - return openpgp.ReadKeyRing(f) + switch { + case isKeybox(data): + keys, err := keyboxPublicKeys(data) + if err != nil { + return nil, fmt.Errorf("reading keybox %s: %w", ringpath, err) + } + return openpgp.ReadKeyRing(bytes.NewReader(keys)) + case isArmored(data): + ring, err := loadArmoredKeyRing(data) + if err != nil { + return nil, fmt.Errorf("reading armored keyring %s: %w", ringpath, err) + } + return ring, nil + default: + return openpgp.ReadKeyRing(bytes.NewReader(data)) + } +} + +// loadArmoredKeyRing reads every armored block in data and merges the keys +// into a single keyring. Unlike openpgp.ReadArmoredKeyRing, which silently +// ignores everything after the first block, this handles keyrings assembled +// by concatenating exports (cat key1.asc key2.asc > keyring.asc), the same +// way GnuPG imports them. +func loadArmoredKeyRing(data []byte) (openpgp.EntityList, error) { + var ring openpgp.EntityList + r := bytes.NewReader(data) + for { + block, err := armor.Decode(r) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + if block.Type != openpgp.PublicKeyType && block.Type != openpgp.PrivateKeyType { + return nil, fmt.Errorf("expected a public or private key block, got %q", block.Type) + } + entities, err := openpgp.ReadKeyRing(block.Body) + if err != nil { + return nil, err + } + ring = append(ring, entities...) + } + if len(ring) == 0 { + return nil, errors.New("no keys found") + } + return ring, nil } // DigestFile calculates a SHA256 hash (like Docker) for a given file. diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index ec6e6194b..f3e049999 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -16,6 +16,7 @@ limitations under the License. package provenance import ( + "bytes" "crypto" "errors" "io" @@ -24,6 +25,7 @@ import ( "strings" "testing" + "github.com/ProtonMail/go-crypto/openpgp/armor" pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/stretchr/testify/assert" @@ -143,6 +145,72 @@ func TestLoadKeyRing(t *testing.T) { } } +func TestLoadKeyRingKeybox(t *testing.T) { + k, err := loadKeyRing(testKeybox) + require.NoError(t, err) + + require.Len(t, k, 1) + _, ok := k[0].Identities[testKeyName] + assert.True(t, ok, "expected %q in keybox keyring", testKeyName) +} + +func TestLoadKeyRingMixedKeybox(t *testing.T) { + k, err := loadKeyRing(testMixedKeybox) + require.NoError(t, err) + + require.Len(t, k, 2) + + names := make([]string, 0, len(k)) + hasEdDSA := false + for _, e := range k { + for n := range e.Identities { + names = append(names, n) + } + if e.PrimaryKey != nil && e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA { + hasEdDSA = true + } + } + assert.Contains(t, names, testKeyName) + assert.True(t, hasEdDSA, "expected an Ed25519 key in %s", testMixedKeybox) +} + +func TestLoadKeyRingArmored(t *testing.T) { + k, err := loadKeyRing(testArmoredPubfile) + require.NoError(t, err) + + require.Len(t, k, 1) + _, ok := k[0].Identities[testKeyName] + assert.True(t, ok, "expected %q in armored keyring", testKeyName) +} + +func TestLoadKeyRingArmoredMultiBlock(t *testing.T) { + // A keyring assembled by concatenating exports (cat a.asc b.asc) must + // load every block, the same way gpg --import does. + k, err := loadKeyRing(testMultiBlockArmored) + require.NoError(t, err) + + require.Len(t, k, 2) + names := make([]string, 0, len(k)) + for _, e := range k { + for n := range e.Identities { + names = append(names, n) + } + } + assert.Contains(t, names, testKeyName) +} + +func TestLoadArmoredKeyRingRejectsNonKeyBlocks(t *testing.T) { + var buf bytes.Buffer + w, err := armor.Encode(&buf, "PGP MESSAGE", nil) + require.NoError(t, err) + _, err = w.Write([]byte("not a key")) + require.NoError(t, err) + require.NoError(t, w.Close()) + + _, err = loadArmoredKeyRing(buf.Bytes()) + assert.ErrorContains(t, err, "expected a public or private key block") +} + func TestDigest(t *testing.T) { f, err := os.Open(testChartfile) require.NoError(t, err) @@ -318,6 +386,31 @@ func TestVerify(t *testing.T) { } } +// TestVerifyKeyboxKeyring mirrors TestVerify with the keyring loaded from a +// GnuPG keybox instead of the legacy binary format. +func TestVerifyKeyboxKeyring(t *testing.T) { + signer, err := NewFromKeyring(testKeybox, "") + require.NoError(t, err) + + archiveData, err := os.ReadFile(testChartfile) + require.NoError(t, err) + + sigData, err := os.ReadFile(testSigBlock) + require.NoError(t, err) + + ver, err := signer.Verify(archiveData, sigData, filepath.Base(testChartfile)) + require.NoError(t, err) + require.NotNil(t, ver.SignedBy) + _, ok := ver.SignedBy.Identities[testKeyName] + assert.True(t, ok, "expected chart to verify as signed by %q", testKeyName) + + tamperedSigData, err := os.ReadFile(testTamperedSigBlock) + require.NoError(t, err) + + _, err = signer.Verify(archiveData, tamperedSigData, filepath.Base(testChartfile)) + assert.Error(t, err, "expected tampered signature to fail against keybox keyring") +} + // readSumFile reads a file containing a sum generated by the UNIX shasum tool. func readSumFile(sumfile string) (string, error) { data, err := os.ReadFile(sumfile) diff --git a/pkg/provenance/testdata/helm-mixed-keyring.asc b/pkg/provenance/testdata/helm-mixed-keyring.asc new file mode 100644 index 000000000..acde8ef18 --- /dev/null +++ b/pkg/provenance/testdata/helm-mixed-keyring.asc @@ -0,0 +1,40 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFeWbZ4BCADIsgNRyVBSWJkbH6h3UWWXsA9ce1u+YKvaCYFSjIujKycTAqCC +P7qUV2Oj/4E2zUcOD1/8/meDwuTnNTLzdSw4ujtFlKRSw7zelQE6sxvID0KM0pQK +7AxDTXsm/7Afd/fg4WNW0/hcbeiNz4TVmSWAnbqeLg8o8eljR5QhTk47H6Glo4hV +raeLCKG77qm2qOQ/m38ec+L5n9iUpoZZu1S5RXPUIanV5pLlx2rQsooIQEdJbTRF +Iv3+2Nj/56PFVdrw7E5ARqCD4PpzzYV7uj8vKumOp+VivFj95Ze6DFENh1WWy336 +jQEj0uTolgOYeQ6AdJobwSPUeYXGV6Sf2vwXABEBAAG0XUhlbG0gVGVzdGluZyAo +VGhpcyBrZXkgc2hvdWxkIG9ubHkgYmUgdXNlZCBmb3IgdGVzdGluZy4gRE8gTk9U +IFRSVVNULikgPGhlbG0tdGVzdGluZ0BoZWxtLnNoPokBNwQTAQoAIQUCV5ZtngIb +AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCEO7+YH8GHYolFCAC0+ejT5dIX +0juRl5AdG6XAlEf9IrehkVlo0s5bG1Ucea9jjNRafmmHyMravrxZVI9zGFhaNpxA ++McnnWAO8lx5yTgnGW269qvFBsj5n0ItPewILQZjTEgwLYf1oc2qCFpk+8ovBnxZ +Kaz3J5QKqU09zLO2yALuI5FTCCUupJ3OGIrKvLJ4H2jsRk/5jgqIgqJotW/4Baza +4zemFdQHj9FPvq9sVeW5lyxM48i638GtslIgc0y2Yd/bfiy0tfIkskWz/kS1U6c9 +Utr3UPwpBqHjXnakmpK7GMGlp5eDXgr63BEdLKxJJWWxIys35fR19XZXcAELrwMh +zkTzB/dvI375uQENBFeWbZ4BCADB6a8oiF2royWbvwmujBxSmD68QWziNZbSCuRs +M4WM1iy69kspJHRwJEVIE5pkoVfV6tIcwQQ0VkJmq6Pf/pyKeMAXp0u8bp5K0mVE +Yio1/adiLM3kX1yleoXLNUb1gNvsk6Kp6BZCUNRmjKlbKxapJKHmL+opX8UoeLeE +KSgJNL8jvo3nRgg+wIPnVDoizz7y03F0k4SbgNGdoA/FtZs/vcSrEfzOLgI2RxQS +dj/ePkxg9TvQygUSYgAVbYlASw6XMmd/zTjkfD9qa0l3WSejTisn85b611WryDpA +iYkYC3GzK8g7S9VwkAbtr79cbG+1djlA4aHASpil+zR12yEhABEBAAGJAR8EGAEK +AAkFAleWbZ4CGwwACgkQhDu/mB/Bh2LbNQgAiOOMNzmKzY4A7/ra8ppaH9oL8XIa +WvInivOsx22K09PPuPVqi/ooBiRGyRqhdVS4ShPOoOTR5tOsdfPpEMTHnGQ1+jW+ +Tw5MXv3oMckWw3YEYptnuvon3wT4bOXLr+eYlY4Z1ONs+pAXwMiQ2zXNuKHpA2fR +HsF8Wyw57rCVn7K5nQgZCbVxasYlzvFGnmt/itSC5w/AnIvICDOWcoGFKTieqhME +IkCyvu+DNrMAumnD2fqF2olsM4IzCEPSMEQqJIGzZTtQTseS8NSDioRGnG1AJQ94 +BssQVmTh0/hlpoTqXY803lR5wb1fr3RRnkOu+lbEI6AAUj51j5TAAo/1dA== +=bvcV +-----END PGP PUBLIC KEY BLOCK----- +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaMzP2xYJKwYBBAHaRw8BAQdAXkL0KKTOZoMaM+6HsrkWv93zmLmnMiIoQvR2 +F+bjx7G0KEhlbG0gRWQyNTUxOSBUZXN0IDxoZWxtLWVkMjU1MTlAaGVsbS5zaD6I +mQQTFgoAQRYhBEy+dUwlWAxsh43nEfuFaBLOL8tkBQJozM/bAhsDBQkFo5qABQsJ +CAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEPuFaBLOL8tktBYA/2cclE0N+BZV +Wymk6XfwFMjErM6olWAfwA8DfAGQ55+lAQCqSa+Tkjd4dmToVHcb0QAm+zwklAY4 +e6qWgGiivn77AA== +=wJ7K +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pkg/provenance/testdata/helm-mixed-keyring.kbx b/pkg/provenance/testdata/helm-mixed-keyring.kbx new file mode 100644 index 0000000000000000000000000000000000000000..300711cdb504797ac7462b06a4a8e692b6ee1793 GIT binary patch literal 1865 zcmZ{k2~-nz9>-@U2T4Gpq5*|MxJnSjRtTUdqzFL-K_HP+ppZboAZQ3jaS;Uyi72R% z`-%Y(p$!e95|Lxo5~aiv;;tKZJ<9E4t%6htqD=z4-Phgz-kbmYt~c|a|L;2=008hH z2tYpWA#s4p63M%x)R)>{F+>FfYP@U!KxhFFU=;av5T|30;FRu!_Hdp{Pq|57L5!*c zukPAlW=3bQT3yvoWxUo1jJTB9hxm+kK z`OL7fm*=%?c8h-@lp&Lb8&Zujg|S8QumRgTE#>D-9jdHytB;K zcT^&N-uIm-5YO_x`StUWY}*?hvlb%H_KzMNK}~xD74HvyutxGnBhwlxFWxceYiugZ zjl?Mbx#2V0R&PuC59T&bX~8L=wh(42^_bIopKPAJ*oFjPs-Xd${}FTFw2)AE zq(xv^)r0=N$SAPfwd*V9FT_$AW{hc{U-UOy@ozrX7O5!~p8NGGY5d?PuVhAjaEHV0 zaV>)9AFH_;z4}c)UB_w<$mkw1+lb3eF}8zaQ4x*bA>HN2HMu;e&aJ0y27kBVH@T*cWTN!4e(0+SjYuKz6cdoTRi90;}`1X9_StS8! zL4LTE&!*fypeL;#8=4qyWzS8ozu#X?BPxmA{<^-A%Tr%I#NA7S$5h3Ol~&K;3(WBW z;bi&c^IPPjOl66FucTyzICz^s4T~Q#?mHQ7yX$q^jaKa}|j4%7aOJ!+rq3y*CUr`zc+ zhy404zLDp0^T^dnBui`ta$x;o+RNbu`qjKwM~WSuQ8N4PMhQ~`YWIDwJbd509td=0 z7xQ|d;`!7y07EIH&)2m#p4! literal 0 HcmV?d00001 diff --git a/pkg/provenance/testdata/helm-test-key.asc b/pkg/provenance/testdata/helm-test-key.asc new file mode 100644 index 000000000..2d069d9b7 --- /dev/null +++ b/pkg/provenance/testdata/helm-test-key.asc @@ -0,0 +1,30 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFeWbZ4BCADIsgNRyVBSWJkbH6h3UWWXsA9ce1u+YKvaCYFSjIujKycTAqCC +P7qUV2Oj/4E2zUcOD1/8/meDwuTnNTLzdSw4ujtFlKRSw7zelQE6sxvID0KM0pQK +7AxDTXsm/7Afd/fg4WNW0/hcbeiNz4TVmSWAnbqeLg8o8eljR5QhTk47H6Glo4hV +raeLCKG77qm2qOQ/m38ec+L5n9iUpoZZu1S5RXPUIanV5pLlx2rQsooIQEdJbTRF +Iv3+2Nj/56PFVdrw7E5ARqCD4PpzzYV7uj8vKumOp+VivFj95Ze6DFENh1WWy336 +jQEj0uTolgOYeQ6AdJobwSPUeYXGV6Sf2vwXABEBAAG0XUhlbG0gVGVzdGluZyAo +VGhpcyBrZXkgc2hvdWxkIG9ubHkgYmUgdXNlZCBmb3IgdGVzdGluZy4gRE8gTk9U +IFRSVVNULikgPGhlbG0tdGVzdGluZ0BoZWxtLnNoPokBNwQTAQoAIQUCV5ZtngIb +AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCEO7+YH8GHYolFCAC0+ejT5dIX +0juRl5AdG6XAlEf9IrehkVlo0s5bG1Ucea9jjNRafmmHyMravrxZVI9zGFhaNpxA ++McnnWAO8lx5yTgnGW269qvFBsj5n0ItPewILQZjTEgwLYf1oc2qCFpk+8ovBnxZ +Kaz3J5QKqU09zLO2yALuI5FTCCUupJ3OGIrKvLJ4H2jsRk/5jgqIgqJotW/4Baza +4zemFdQHj9FPvq9sVeW5lyxM48i638GtslIgc0y2Yd/bfiy0tfIkskWz/kS1U6c9 +Utr3UPwpBqHjXnakmpK7GMGlp5eDXgr63BEdLKxJJWWxIys35fR19XZXcAELrwMh +zkTzB/dvI375uQENBFeWbZ4BCADB6a8oiF2royWbvwmujBxSmD68QWziNZbSCuRs +M4WM1iy69kspJHRwJEVIE5pkoVfV6tIcwQQ0VkJmq6Pf/pyKeMAXp0u8bp5K0mVE +Yio1/adiLM3kX1yleoXLNUb1gNvsk6Kp6BZCUNRmjKlbKxapJKHmL+opX8UoeLeE +KSgJNL8jvo3nRgg+wIPnVDoizz7y03F0k4SbgNGdoA/FtZs/vcSrEfzOLgI2RxQS +dj/ePkxg9TvQygUSYgAVbYlASw6XMmd/zTjkfD9qa0l3WSejTisn85b611WryDpA +iYkYC3GzK8g7S9VwkAbtr79cbG+1djlA4aHASpil+zR12yEhABEBAAGJAR8EGAEK +AAkFAleWbZ4CGwwACgkQhDu/mB/Bh2LbNQgAiOOMNzmKzY4A7/ra8ppaH9oL8XIa +WvInivOsx22K09PPuPVqi/ooBiRGyRqhdVS4ShPOoOTR5tOsdfPpEMTHnGQ1+jW+ +Tw5MXv3oMckWw3YEYptnuvon3wT4bOXLr+eYlY4Z1ONs+pAXwMiQ2zXNuKHpA2fR +HsF8Wyw57rCVn7K5nQgZCbVxasYlzvFGnmt/itSC5w/AnIvICDOWcoGFKTieqhME +IkCyvu+DNrMAumnD2fqF2olsM4IzCEPSMEQqJIGzZTtQTseS8NSDioRGnG1AJQ94 +BssQVmTh0/hlpoTqXY803lR5wb1fr3RRnkOu+lbEI6AAUj51j5TAAo/1dA== +=bvcV +-----END PGP PUBLIC KEY BLOCK----- diff --git a/pkg/provenance/testdata/helm-test-key.kbx b/pkg/provenance/testdata/helm-test-key.kbx new file mode 100644 index 0000000000000000000000000000000000000000..f2bf73fa5d6400c692de896244824df499a7f568 GIT binary patch literal 1465 zcmZQzU{GLWWMJ}kib!Jsf-Ea$H2hK&cMVV6PFm=xz%QIJ>N6# z%PrRXXUHFHPXek?0I8DNp3`8TT+PjXaXLHahJ>Drs!!d3!Zsi|AYf$R0TT0otXMFg ziGc-7qY*F{%w*(c37?ibkCB7n#3tszlL0{yGo|HMln16x-@qSJ9lbAM^)1fEpq}o< z+Umkg3!3bAO$kq4{J+uctUDin{GWg6&4->mH#PcPs$;Rs+I7m3pu>CaO=Yy&EPaCC zsprxZt~Wf+zSU~~H^`TNfABCl?DCJ8+!wv)TdvMjZJ4`jo*uu($Ct_OQxyIDtmPLj zUEC46c6m3)!rkvyZd>uhes;ZF@uQ#fZ%kR%7P&iQr)%*Q#g$i|O?rAf>%yik4hMJ7 zToYHNzyEIB`2T$I(a>8T-uOAVEogr5tN3hd^)7pTt(SevpC;{z`1^GFE}lT%_Rwjk zYk&1JDqni?VjA;|O1_4YS<(lUuT-`k3tuw-)*o>OK}H6~4ZzS#FGz>P2g8pm z{}6?cpwQqDJxv9h44^r>P+12MN3S@;4r&<_ByKtx%~^yQxfm2#nLshZB+bmq&B?*e z%qq&o$-&Jk#>5OvMRJTx;tdR3oC2_9(do*;u;u5A%TF(fU$UM!eS)m?(gRc6|0-=? zI59Hg(z$5qP?^g0$vs!1>N49;oVvAdPh?1cu|!0a*&K%-$JOU1@O_G@JZYgWnY-)T z>Z5EYe$IE&wSB{(%a-ipVW8Xob>Z1n98oF1PwBJOL~5@2u0DlprLXOo&D%~ey;GhT z%%Q5cWbQeMu2Xw9Rmf+&ar6J#$JNoaC}V5>57sre9-A){y~5sq(SP6ioY1E`r|bAU zKC$cm!L^%$6pDSeCEmYXr?X}2CzVaEoBz3N4PI^=bnAP-A5FG}kK@Xg%$l@Y;^5Nd z)0^YCe%%q2)mh`Inz~V0+x+R5(ywLV1&rM5nHA5ud}jZiuUz+Y0~-S~Fwz)yA`_)& zBy*6(B+Myo_2T>g=5$pY5MS=SCvTqDrBs(BEz`ftlXT8LiH})Y)q2|0?Q6sBH zWp=~GxeNG@Zk=tv_sD9&Kj-wA%-ltU%Ixpi`6PU`zHo|FD2YKdx6{F!Z@N)>{aK4A zHTGHAp5>A1i~Y3KKTrF0J#_U6D~HZb3GTwp+9#~NuNF*Td%J#rOiupRGE0Yt3lDhB zSo+(f^tPfRI5BrJ%CkrSQxqpGMe!h~oZF@x3>}Yq%q_dl_A$Kwb?eitDEV95AB&`- zKB;$oUUNLR>+{@dGC&+%`SCW8q8Y^ow!_ zYoc{5-))#Wf78yn9Fm+{3$u=?p8M!FFT1|$O4D=x19Q4ha2QW3YHZcCn72xpMaf~) zzW2>$n;CXx9=`dj^;Tz&ag#BJ^CbfpEtSU2sn!90$0vQb(%jYJHYe9XmA``Rv_M$O z!^=NXm$kf#?Kim>Qh9K1{Q8o>dCu#8g&k2|zz}3t+CSw0Q~%czSju4&c@tW2kz2dq Q!NQf1U03#IY|5+y07Bqtg8%>k literal 0 HcmV?d00001 diff --git a/pkg/provenance/testdata/regen-keyring-formats.sh b/pkg/provenance/testdata/regen-keyring-formats.sh new file mode 100755 index 000000000..c9cc95fc6 --- /dev/null +++ b/pkg/provenance/testdata/regen-keyring-formats.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Regenerate the file-backed keyring fixtures from the committed binary +# keyrings. Requires GnuPG 2.1+ (keybox support). +# +# helm-test-key.kbx keybox (pubring.kbx) containing the helm-test key +# helm-mixed-keyring.kbx keybox containing the RSA and Ed25519 test keys +# helm-test-key.asc ASCII-armored export of the helm-test key +# helm-mixed-keyring.asc two concatenated single-key armored exports +set -e + +GNUPGHOME=$(mktemp -d) +export GNUPGHOME +chmod 700 "$GNUPGHOME" +gpg --batch --no-tty --quiet --import helm-test-key.pub +cp "$GNUPGHOME/pubring.kbx" helm-test-key.kbx +gpg --batch --no-tty --export --armor helm-testing@helm.sh > helm-test-key.asc +rm -rf "$GNUPGHOME" + +GNUPGHOME=$(mktemp -d) +export GNUPGHOME +chmod 700 "$GNUPGHOME" +gpg --batch --no-tty --quiet --import helm-mixed-keyring.pub +cp "$GNUPGHOME/pubring.kbx" helm-mixed-keyring.kbx +gpg --batch --no-tty --export --armor helm-testing@helm.sh > helm-mixed-keyring.asc +gpg --batch --no-tty --export --armor helm-ed25519@helm.sh >> helm-mixed-keyring.asc +rm -rf "$GNUPGHOME"