From d2579211d5b96a40d43801f257f2e0b65448653c Mon Sep 17 00:00:00 2001 From: Md Tanvir Alam Date: Mon, 24 Aug 2026 20:45:26 +0000 Subject: [PATCH] fix(provenance): accept signed SHA when archive name differs from Chart.yaml helm package records Chart.yaml's name in the provenance Files map. helm pull --verify looked that entry up from the repository path, so a chart published under a different name failed with "provenance does not contain a SHA for a file named ...". The signature already authenticates the Files map; a matching hash is enough. Fixes #32546 Signed-off-by: Md Tanvir Alam --- pkg/provenance/sign.go | 32 +++++++++++++++++++++++++++----- pkg/provenance/sign_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index b4d5f7541..0ea00b113 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -238,6 +238,11 @@ func (s *Signatory) ClearSign(archiveData []byte, filename string, metadataBytes // Verify checks a signature and verifies that it is legit for package data. // This is the core verification method that works with data in memory. +// +// filename is the local archive name. If that name is not in the signed Files +// map, a Files entry whose SHA matches the archive is accepted. helm package +// records Chart.yaml's name, which can differ from the repository path helm +// pull uses to name the downloaded file. func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Verification, error) { ver := &Verification{} @@ -264,19 +269,36 @@ func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Veri } sum = "sha256:" + sum - if sha, ok := sums.Files[filename]; !ok { - return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", filename) - } else if sha != sum { - return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", filename, sha, sum) + signedName, err := matchSignedFile(sums.Files, filename, sum) + if err != nil { + return ver, err } ver.FileHash = sum - ver.FileName = filename + ver.FileName = signedName // TODO: when image signing is added, verify that here. return ver, nil } +// matchSignedFile returns the Files key that covers archiveSum. +// If filename is present it must match; otherwise any signed name with the +// same hash is accepted (repository path vs Chart.yaml name). +func matchSignedFile(files map[string]string, filename, archiveSum string) (string, error) { + if sha, ok := files[filename]; ok { + if sha != archiveSum { + return "", fmt.Errorf("sha256 sum does not match for %s: %q != %q", filename, sha, archiveSum) + } + return filename, nil + } + for signedName, sha := range files { + if sha == archiveSum { + return signedName, nil + } + } + return "", fmt.Errorf("provenance does not contain a SHA for a file named %q", filename) +} + // verifySignature verifies that the given block is validly signed, and returns the signer. func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { return openpgp.CheckDetachedSignature( diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index f3e049999..775de6673 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -386,6 +386,32 @@ func TestVerify(t *testing.T) { } } +func TestVerifyFilenameDiffersFromProvenance(t *testing.T) { + signer, err := NewFromFiles(testKeyfile, testPubfile) + require.NoError(t, err) + + archiveData, err := os.ReadFile(testChartfile) + require.NoError(t, err) + + sigData, err := os.ReadFile(testSigBlock) + require.NoError(t, err) + + // helm pull --verify names the archive from the repository path, which can + // differ from the Chart.yaml name recorded in the provenance Files map. + ver, err := signer.Verify(archiveData, sigData, "otherrepo-1.2.3.tgz") + require.NoError(t, err) + assert.Equal(t, "hashtest-1.2.3.tgz", ver.FileName) + assert.Equal(t, "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888", ver.FileHash) + + _, err = signer.Verify([]byte("not the signed archive"), sigData, "otherrepo-1.2.3.tgz") + require.Error(t, err) + assert.ErrorContains(t, err, `provenance does not contain a SHA for a file named "otherrepo-1.2.3.tgz"`) + + _, err = signer.Verify([]byte("not the signed archive"), sigData, filepath.Base(testChartfile)) + require.Error(t, err) + assert.ErrorContains(t, err, "sha256 sum does not match") +} + // TestVerifyKeyboxKeyring mirrors TestVerify with the keyring loaded from a // GnuPG keybox instead of the legacy binary format. func TestVerifyKeyboxKeyring(t *testing.T) {