From fae51ba3dea18c4378d32dcab7218be112ffb637 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Mon, 7 Mar 2022 23:07:45 +0530 Subject: [PATCH 01/10] update helm chart builder to support prov file verification Signed-off-by: Sanskar Jaiswal --- internal/helm/chart/builder.go | 6 +- internal/helm/chart/builder_local.go | 30 ++++++++- internal/helm/chart/builder_local_test.go | 17 +++-- internal/helm/chart/builder_remote.go | 71 +++++++++++++++----- internal/helm/chart/builder_remote_test.go | 64 ++++++++++++------ internal/helm/chart/errors.go | 15 +++-- internal/helm/chart/verify.go | 57 ++++++++++++++++ internal/helm/repository/chart_repository.go | 47 +++++++++++-- internal/util/temp.go | 26 +++++++ 9 files changed, 280 insertions(+), 53 deletions(-) create mode 100644 internal/helm/chart/verify.go diff --git a/internal/helm/chart/builder.go b/internal/helm/chart/builder.go index e3ce2207d..25bc19563 100644 --- a/internal/helm/chart/builder.go +++ b/internal/helm/chart/builder.go @@ -85,7 +85,7 @@ type Builder interface { // Reference and BuildOptions, and writes it to p. // It returns the Build result, or an error. // It may return an error for unsupported Reference implementations. - Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) + Build(ctx context.Context, ref Reference, p string, opts BuildOptions, keyring []byte) (*Build, error) } // BuildOptions provides a list of options for Builder.Build. @@ -125,6 +125,10 @@ type Build struct { // Path is the absolute path to the packaged chart. // Can be empty, in which case a failure should be assumed. Path string + // ProvFilePath is the absolute path to a provenance file. + // Can be empty, in which case it should be assumed that the packaged + // chart is not verified. + ProvFilePath string // ValuesFiles is the list of files used to compose the chart's // default "values.yaml". ValuesFiles []string diff --git a/internal/helm/chart/builder_local.go b/internal/helm/chart/builder_local.go index 2710e41a9..760280b62 100644 --- a/internal/helm/chart/builder_local.go +++ b/internal/helm/chart/builder_local.go @@ -17,6 +17,7 @@ limitations under the License. package chart import ( + "bytes" "context" "fmt" "os" @@ -62,7 +63,7 @@ func NewLocalBuilder(dm *DependencyManager) Builder { // If the LocalReference.Path refers to a chart directory, dependencies are // confirmed to be present using the DependencyManager, while attempting to // resolve any missing. -func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) { +func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions, keyring []byte) (*Build, error) { localRef, ok := ref.(LocalReference) if !ok { err := fmt.Errorf("expected local chart reference") @@ -108,16 +109,37 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, // - Chart name from cached chart matches resolved name // - Chart version from cached chart matches calculated version // - BuildOptions.Force is False + var provFilePath string + verifyProvFile := func(chart, provFile string) error { + if keyring != nil { + if _, err := os.Stat(provFile); err != nil { + err = fmt.Errorf("could not load provenance file %s: %w", provFile, err) + return &BuildError{Reason: ErrProvenanceVerification, Err: err} + } + err := VerifyProvenanceFile(bytes.NewReader(keyring), chart, provFile) + if err != nil { + err = fmt.Errorf("failed to verify helm chart using provenance file: %w", err) + return &BuildError{Reason: ErrProvenanceVerification, Err: err} + } + } + return nil + } if opts.CachedChart != "" && !opts.Force { if curMeta, err = LoadChartMetadataFromArchive(opts.CachedChart); err == nil { // If the cached metadata is corrupt, we ignore its existence // and continue the build if err = curMeta.Validate(); err == nil { if result.Name == curMeta.Name && result.Version == curMeta.Version { + if !requiresPackaging { + provFilePath = provenanceFilePath(opts.CachedChart) + if err = verifyProvFile(opts.CachedChart, provFilePath); err != nil { + return nil, err + } + result.ProvFilePath = provFilePath + } result.Path = opts.CachedChart result.ValuesFiles = opts.GetValuesFiles() result.Packaged = requiresPackaging - return result, nil } } @@ -126,10 +148,14 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, // If the chart at the path is already packaged and no custom values files // options are set, we can copy the chart without making modifications + provFilePath = provenanceFilePath(localRef.Path) if !requiresPackaging { if err = copyFileToPath(localRef.Path, p); err != nil { return result, &BuildError{Reason: ErrChartPull, Err: err} } + if err = verifyProvFile(localRef.Path, provFilePath); err != nil { + return result, err + } result.Path = p return result, nil } diff --git a/internal/helm/chart/builder_local_test.go b/internal/helm/chart/builder_local_test.go index cff5f180f..e108250c3 100644 --- a/internal/helm/chart/builder_local_test.go +++ b/internal/helm/chart/builder_local_test.go @@ -40,6 +40,11 @@ func TestLocalBuilder_Build(t *testing.T) { chartB, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz") g.Expect(err).ToNot(HaveOccurred()) g.Expect(chartB).ToNot(BeEmpty()) + + keyring, err := os.ReadFile("./../testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + mockRepo := func() *repository.ChartRepository { return &repository.ChartRepository{ Client: &mockGetter{ @@ -210,7 +215,7 @@ fullnameOverride: "full-foo-name-override"`), ) b := NewLocalBuilder(dm) - cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts) + cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts, keyring) if tt.wantErr != "" { g.Expect(err).To(HaveOccurred()) @@ -243,6 +248,10 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) { reference := LocalReference{Path: "./../testdata/charts/helmchart"} + keyring, err := os.ReadFile("./../testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + dm := NewDependencyManager() b := NewLocalBuilder(dm) @@ -253,7 +262,7 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) { // Build first time. targetPath := filepath.Join(tmpDir, "chart1.tgz") buildOpts := BuildOptions{} - cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts) + cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts, keyring) g.Expect(err).ToNot(HaveOccurred()) // Set the result as the CachedChart for second build. @@ -261,13 +270,13 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) { targetPath2 := filepath.Join(tmpDir, "chart2.tgz") defer os.RemoveAll(targetPath2) - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath)) // Rebuild with build option Force. buildOpts.Force = true - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath2)) } diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index 778efd253..a159c5f1a 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -17,9 +17,9 @@ limitations under the License. package chart import ( + "bytes" "context" "fmt" - "io" "os" "path/filepath" @@ -33,6 +33,7 @@ import ( "github.com/fluxcd/source-controller/internal/fs" "github.com/fluxcd/source-controller/internal/helm/repository" + "github.com/fluxcd/source-controller/internal/util" ) type remoteChartBuilder struct { @@ -61,7 +62,7 @@ func NewRemoteBuilder(repository *repository.ChartRepository) Builder { // After downloading the chart, it is only packaged if required due to BuildOptions // modifying the chart, otherwise the exact data as retrieved from the repository // is written to p, after validating it to be a chart. -func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) { +func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions, keyring []byte) (*Build, error) { remoteRef, ok := ref.(RemoteReference) if !ok { err := fmt.Errorf("expected remote chart reference") @@ -105,6 +106,19 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o requiresPackaging := len(opts.GetValuesFiles()) != 0 || opts.VersionMetadata != "" + verifyProvFile := func(chart, provFile string) error { + if keyring != nil { + err := VerifyProvenanceFile(bytes.NewReader(keyring), chart, provFile) + if err != nil { + err = fmt.Errorf("failed to verify helm chart using provenance file %s: %w", provFile, err) + return &BuildError{Reason: ErrProvenanceVerification, Err: err} + } + } + return nil + } + + var provFilePath string + // If all the following is true, we do not need to download and/or build the chart: // - Chart name from cached chart matches resolved name // - Chart version from cached chart matches calculated version @@ -115,6 +129,14 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // and continue the build if err = curMeta.Validate(); err == nil { if result.Name == curMeta.Name && result.Version == curMeta.Version { + // We can only verify with provenance file if we didn't package the chart ourselves. + if !requiresPackaging { + provFilePath = provenanceFilePath(opts.CachedChart) + if err = verifyProvFile(opts.CachedChart, provFilePath); err != nil { + return nil, err + } + result.ProvFilePath = provFilePath + } result.Path = opts.CachedChart result.ValuesFiles = opts.GetValuesFiles() result.Packaged = requiresPackaging @@ -126,15 +148,39 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // Download the package for the resolved version res, err := b.remote.DownloadChart(cv) + // Deal with the underlying byte slice to avoid having to read the buffer multiple times. + chartBuf := res.Bytes() + if err != nil { err = fmt.Errorf("failed to download chart for remote reference: %w", err) return result, &BuildError{Reason: ErrChartPull, Err: err} } + if keyring != nil { + provFilePath = provenanceFilePath(p) + err := b.remote.DownloadProvenanceFile(cv, provFilePath) + if err != nil { + err = fmt.Errorf("failed to download provenance file for remote reference: %w", err) + return nil, &BuildError{Reason: ErrChartPull, Err: err} + } + // Write the remote chart temporarily to verify it with provenance file. + // This is needed, since the verification will work only if the .tgz file is untampered. + // But we write the packaged chart to disk under a different name, so the provenance file + // will not be valid for this _new_ packaged chart. + chart, err := util.WriteBytesToFile(chartBuf, fmt.Sprintf("%s-%s.tgz", result.Name, result.Version), false) + defer os.Remove(chart.Name()) + if err != nil { + return nil, err + } + if err = verifyProvFile(chart.Name(), provFilePath); err != nil { + return nil, err + } + result.ProvFilePath = provFilePath + } // Use literal chart copy from remote if no custom values files options are // set or version metadata isn't set. if !requiresPackaging { - if err = validatePackageAndWriteToPath(res, p); err != nil { + if err = validatePackageAndWriteToPath(chartBuf, p); err != nil { return nil, &BuildError{Reason: ErrChartPull, Err: err} } result.Path = p @@ -143,7 +189,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // Load the chart and merge chart values var chart *helmchart.Chart - if chart, err = loader.LoadArchive(res); err != nil { + if chart, err = loader.LoadArchive(bytes.NewBuffer(chartBuf)); err != nil { err = fmt.Errorf("failed to load downloaded chart: %w", err) return result, &BuildError{Reason: ErrChartPackage, Err: err} } @@ -166,6 +212,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o if err = packageToPath(chart, p); err != nil { return nil, &BuildError{Reason: ErrChartPackage, Err: err} } + result.Path = p result.Packaged = true return result, nil @@ -202,18 +249,12 @@ func mergeChartValues(chart *helmchart.Chart, paths []string) (map[string]interf // validatePackageAndWriteToPath atomically writes the packaged chart from reader // to out while validating it by loading the chart metadata from the archive. -func validatePackageAndWriteToPath(reader io.Reader, out string) error { - tmpFile, err := os.CreateTemp("", filepath.Base(out)) - if err != nil { - return fmt.Errorf("failed to create temporary file for chart: %w", err) - } +func validatePackageAndWriteToPath(b []byte, out string) error { + tmpFile, err := util.WriteBytesToFile(b, out, true) defer os.Remove(tmpFile.Name()) - if _, err = tmpFile.ReadFrom(reader); err != nil { - _ = tmpFile.Close() - return fmt.Errorf("failed to write chart to file: %w", err) - } - if err = tmpFile.Close(); err != nil { - return err + + if err != nil { + return fmt.Errorf("failed to write packaged chart to temp file: %w", err) } meta, err := LoadChartMetadataFromArchive(tmpFile.Name()) if err != nil { diff --git a/internal/helm/chart/builder_remote_test.go b/internal/helm/chart/builder_remote_test.go index 015b1bdac..b9795b352 100644 --- a/internal/helm/chart/builder_remote_test.go +++ b/internal/helm/chart/builder_remote_test.go @@ -36,9 +36,10 @@ import ( // mockIndexChartGetter returns specific response for index and chart queries. type mockIndexChartGetter struct { - IndexResponse []byte - ChartResponse []byte - requestedURL string + IndexResponse []byte + ChartResponse []byte + ProvenanceFileResponse []byte + requestedURL string } func (g *mockIndexChartGetter) Get(u string, _ ...helmgetter.Option) (*bytes.Buffer, error) { @@ -47,6 +48,9 @@ func (g *mockIndexChartGetter) Get(u string, _ ...helmgetter.Option) (*bytes.Buf if strings.HasSuffix(u, "index.yaml") { r = g.IndexResponse } + if strings.HasSuffix(u, ".prov") { + r = g.ProvenanceFileResponse + } return bytes.NewBuffer(r), nil } @@ -68,12 +72,18 @@ entries: - urls: - https://example.com/grafana.tgz description: string - version: 6.17.4 + version: 0.1.0 + name: helmchart `) + provFile, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz.prov") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(provFile).ToNot(BeEmpty()) + mockGetter := &mockIndexChartGetter{ - IndexResponse: index, - ChartResponse: chartGrafana, + IndexResponse: index, + ChartResponse: chartGrafana, + ProvenanceFileResponse: provFile, } mockRepo := func() *repository.ChartRepository { @@ -84,6 +94,10 @@ entries: } } + keyring, err := os.ReadFile("./../testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + tests := []struct { name string reference Reference @@ -128,7 +142,7 @@ entries: reference: RemoteReference{Name: "grafana"}, repository: mockRepo(), buildOpts: BuildOptions{VersionMetadata: "foo"}, - wantVersion: "6.17.4+foo", + wantVersion: "0.1.0+foo", wantPackaged: true, }, { @@ -147,7 +161,7 @@ entries: ValuesFiles: []string{"a.yaml", "b.yaml", "c.yaml"}, }, repository: mockRepo(), - wantVersion: "6.17.4", + wantVersion: "0.1.0", wantValues: chartutil.Values{ "a": "b", "b": "d", @@ -173,7 +187,10 @@ entries: b := NewRemoteBuilder(tt.repository) - cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts) + if tt.buildOpts.VersionMetadata != "" { + keyring = nil + } + cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts, keyring) if tt.wantErr != "" { g.Expect(err).To(HaveOccurred()) @@ -204,6 +221,14 @@ func TestRemoteBuilder_Build_CachedChart(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(chartGrafana).ToNot(BeEmpty()) + provFile, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz.prov") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(provFile).ToNot(BeEmpty()) + + keyring, err := os.ReadFile("./../testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + index := []byte(` apiVersion: v1 entries: @@ -216,8 +241,9 @@ entries: `) mockGetter := &mockIndexChartGetter{ - IndexResponse: index, - ChartResponse: chartGrafana, + IndexResponse: index, + ChartResponse: chartGrafana, + ProvenanceFileResponse: provFile, } mockRepo := func() *repository.ChartRepository { return &repository.ChartRepository{ @@ -242,10 +268,12 @@ entries: defer os.RemoveAll(tmpDir) // Build first time. - targetPath := filepath.Join(tmpDir, "chart1.tgz") + // The file name should be the same as the actual chart in testdata, so that + // we can verify it's signature using the provenance file. + targetPath := filepath.Join(tmpDir, "helmchart-0.1.0.tgz") defer os.RemoveAll(targetPath) buildOpts := BuildOptions{} - cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts) + cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts, keyring) g.Expect(err).ToNot(HaveOccurred()) // Set the result as the CachedChart for second build. @@ -254,13 +282,13 @@ entries: // Rebuild with a new path. targetPath2 := filepath.Join(tmpDir, "chart2.tgz") defer os.RemoveAll(targetPath2) - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath)) // Rebuild with build option Force. buildOpts.Force = true - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath2)) } @@ -346,9 +374,8 @@ func Test_validatePackageAndWriteToPath(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) defer os.RemoveAll(tmpDir) - validF, err := os.Open("./../testdata/charts/helmchart-0.1.0.tgz") + validF, err := os.ReadFile("./../testdata/charts/helmchart-0.1.0.tgz") g.Expect(err).ToNot(HaveOccurred()) - defer validF.Close() chartPath := filepath.Join(tmpDir, "chart.tgz") defer os.Remove(chartPath) @@ -356,9 +383,8 @@ func Test_validatePackageAndWriteToPath(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(chartPath).To(BeARegularFile()) - emptyF, err := os.Open("./../testdata/charts/empty.tgz") + emptyF, err := os.ReadFile("./../testdata/charts/empty.tgz") g.Expect(err).ToNot(HaveOccurred()) - defer emptyF.Close() err = validatePackageAndWriteToPath(emptyF, filepath.Join(tmpDir, "out.tgz")) g.Expect(err).To(HaveOccurred()) } diff --git a/internal/helm/chart/errors.go b/internal/helm/chart/errors.go index 5b3a5bec0..c8c29ff0e 100644 --- a/internal/helm/chart/errors.go +++ b/internal/helm/chart/errors.go @@ -77,11 +77,12 @@ func IsPersistentBuildErrorReason(err error) bool { } var ( - ErrChartReference = BuildErrorReason{Reason: "InvalidChartReference", Summary: "invalid chart reference"} - ErrChartPull = BuildErrorReason{Reason: "ChartPullError", Summary: "chart pull error"} - ErrChartMetadataPatch = BuildErrorReason{Reason: "MetadataPatchError", Summary: "chart metadata patch error"} - ErrValuesFilesMerge = BuildErrorReason{Reason: "ValuesFilesError", Summary: "values files merge error"} - ErrDependencyBuild = BuildErrorReason{Reason: "DependencyBuildError", Summary: "dependency build error"} - ErrChartPackage = BuildErrorReason{Reason: "ChartPackageError", Summary: "chart package error"} - ErrUnknown = BuildErrorReason{Reason: "Unknown", Summary: "unknown build error"} + ErrChartReference = BuildErrorReason{Reason: "InvalidChartReference", Summary: "invalid chart reference"} + ErrChartPull = BuildErrorReason{Reason: "ChartPullError", Summary: "chart pull error"} + ErrProvenanceVerification = BuildErrorReason{Reason: "ProvenanceVerificationError", Summary: "provenance file verification error"} + ErrChartMetadataPatch = BuildErrorReason{Reason: "MetadataPatchError", Summary: "chart metadata patch error"} + ErrValuesFilesMerge = BuildErrorReason{Reason: "ValuesFilesError", Summary: "values files merge error"} + ErrDependencyBuild = BuildErrorReason{Reason: "DependencyBuildError", Summary: "dependency build error"} + ErrChartPackage = BuildErrorReason{Reason: "ChartPackageError", Summary: "chart package error"} + ErrUnknown = BuildErrorReason{Reason: "Unknown", Summary: "unknown build error"} ) diff --git a/internal/helm/chart/verify.go b/internal/helm/chart/verify.go new file mode 100644 index 000000000..0600acd7e --- /dev/null +++ b/internal/helm/chart/verify.go @@ -0,0 +1,57 @@ +package chart + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/crypto/openpgp" + "helm.sh/helm/v3/pkg/provenance" +) + +func VerifyProvenanceFile(keyring io.Reader, chartPath, provFilePath string) error { + switch fi, err := os.Stat(chartPath); { + case err != nil: + return err + case fi.IsDir(): + return fmt.Errorf("unpacked charts cannot be verified") + case !isTar(chartPath): + return fmt.Errorf("chart must be a tgz file") + } + + if provFilePath == "" { + provFilePath = chartPath + ".prov" + } + + if _, err := os.Stat(provFilePath); err != nil { + return fmt.Errorf("could not load provenance file %s: %w", provFilePath, err) + } + + ring, err := openpgp.ReadKeyRing(keyring) + if err != nil { + return err + } + + sig := &provenance.Signatory{KeyRing: ring} + _, err = sig.Verify(chartPath, provFilePath) + if err != nil { + return err + } + return nil +} + +// isTar tests whether the given file is a tar file. +// +// Currently, this simply checks extension, since a subsequent function will +// untar the file and validate its binary format. +func isTar(filename string) bool { + return strings.EqualFold(filepath.Ext(filename), ".tgz") +} + +// Returns the path of a provenance file related to a packaged chart +// Adds ".prov" at the end, as per the Helm convention. +func provenanceFilePath(path string) string { + return path + ".prov" +} diff --git a/internal/helm/repository/chart_repository.go b/internal/helm/repository/chart_repository.go index e8154dca0..3bc1fa63d 100644 --- a/internal/helm/repository/chart_repository.go +++ b/internal/helm/repository/chart_repository.go @@ -38,8 +38,10 @@ import ( "github.com/fluxcd/pkg/version" + "github.com/fluxcd/source-controller/internal/fs" "github.com/fluxcd/source-controller/internal/helm" "github.com/fluxcd/source-controller/internal/transport" + "github.com/fluxcd/source-controller/internal/util" ) var ErrNoChartIndex = errors.New("no chart index") @@ -189,6 +191,45 @@ func (r *ChartRepository) Get(name, ver string) (*repo.ChartVersion, error) { // and then attempts to download the chart using the Client and Options of the // ChartRepository. It returns a bytes.Buffer containing the chart data. func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error) { + u, err := r.resolveChartURL(chart) + if err != nil { + return nil, err + } + + t := transport.NewOrIdle(r.tlsConfig) + clientOpts := append(r.Options, getter.WithTransport(t)) + defer transport.Release(t) + + return r.Client.Get(u.String(), clientOpts...) +} + +func (r *ChartRepository) DownloadProvenanceFile(chart *repo.ChartVersion, path string) error { + u, err := r.resolveChartURL(chart) + if err != nil { + return err + } + t := transport.NewOrIdle(r.tlsConfig) + clientOpts := append(r.Options, getter.WithTransport(t)) + defer transport.Release(t) + + res, err := r.Client.Get(fmt.Sprintf("%s.prov", u.String()), clientOpts...) + if err != nil { + return err + } + tmpFile, err := util.WriteBytesToFile(res.Bytes(), path, true) + defer os.Remove(tmpFile.Name()) + + if err != nil { + return fmt.Errorf("failed to write provenance file to temp file: %w", err) + } + + if err = fs.RenameWithFallback(tmpFile.Name(), path); err != nil { + return fmt.Errorf("failed to write provenance to file %s: %w", path, err) + } + return nil +} + +func (r *ChartRepository) resolveChartURL(chart *repo.ChartVersion) (*url.URL, error) { if len(chart.URLs) == 0 { return nil, fmt.Errorf("chart '%s' has no downloadable URLs", chart.Name) } @@ -217,11 +258,7 @@ func (r *ChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer u.RawQuery = q.Encode() } - t := transport.NewOrIdle(r.tlsConfig) - clientOpts := append(r.Options, getter.WithTransport(t)) - defer transport.Release(t) - - return r.Client.Get(u.String(), clientOpts...) + return u, nil } // LoadIndexFromBytes loads Index from the given bytes. diff --git a/internal/util/temp.go b/internal/util/temp.go index 054b12801..ef07c7bd3 100644 --- a/internal/util/temp.go +++ b/internal/util/temp.go @@ -50,3 +50,29 @@ func pattern(obj client.Object) (p string) { kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind) return fmt.Sprintf("%s-%s-%s-", kind, obj.GetNamespace(), obj.GetName()) } + +// TODO: think of a better name? +func WriteBytesToFile(bytes []byte, out string, temp bool) (*os.File, error) { + var file *os.File + var err error + + if temp { + file, err = os.CreateTemp("", filepath.Base(out)) + if err != nil { + return nil, fmt.Errorf("failed to create temporary file %s: %w", filepath.Base(out), err) + } + } else { + file, err = os.Create(out) + if err != nil { + return nil, fmt.Errorf("failed to create temporary file for chart %s: %w", out, err) + } + } + if _, err := file.Write(bytes); err != nil { + _ = file.Close() + return nil, fmt.Errorf("failed to write to file %s: %w", file.Name(), err) + } + if err := file.Close(); err != nil { + return nil, err + } + return file, nil +} From 36f302af0f26f655d2ba8cfd166ff369135c9519 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Mon, 7 Mar 2022 23:12:01 +0530 Subject: [PATCH 02/10] update helm builder tests for prov file verification Signed-off-by: Sanskar Jaiswal --- .../helm/testdata/charts/helmchart-0.1.0.tgz | Bin 3354 -> 15404 bytes .../testdata/charts/helmchart-0.1.0.tgz.prov | 26 ++++++++++++++++++ internal/helm/testdata/charts/pub.gpg | Bin 0 -> 1728 bytes internal/helm/testdata/charts/sec.gpg | Bin 0 -> 3670 bytes 4 files changed, 26 insertions(+) create mode 100644 internal/helm/testdata/charts/helmchart-0.1.0.tgz.prov create mode 100644 internal/helm/testdata/charts/pub.gpg create mode 100644 internal/helm/testdata/charts/sec.gpg diff --git a/internal/helm/testdata/charts/helmchart-0.1.0.tgz b/internal/helm/testdata/charts/helmchart-0.1.0.tgz index 1ffdde531f6c9e4ee48963ba512a302970994d14..622f9563696e49ed757d3b6ba6f84ec3eb69c706 100644 GIT binary patch literal 15404 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 JpX&fp003)zj$r@* diff --git a/internal/helm/testdata/charts/helmchart-0.1.0.tgz.prov b/internal/helm/testdata/charts/helmchart-0.1.0.tgz.prov new file mode 100644 index 000000000..573d72a91 --- /dev/null +++ b/internal/helm/testdata/charts/helmchart-0.1.0.tgz.prov @@ -0,0 +1,26 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +apiVersion: v2 +appVersion: 1.16.0 +description: A Helm chart for Kubernetes +name: helmchart +type: application +version: 0.1.0 + +... +files: + helmchart-0.1.0.tgz: sha256:c6c5a41ff83f415e18d2ed8ab3a386021e3f1742ea3d1bc0ba759a09aaeb8f2a +-----BEGIN PGP SIGNATURE----- + +wsDcBAEBCgAQBQJiIwjxCRCDj6SUu5W1LwAAndgMAEDZsARwLEczJYw3lKYeftcy +lebfr81jxNS1a4J2DQvOEcltdA1MBHBoir3GoG53xjMWMYMKLUj0FQLQFoLwHTvf +zl5KkfMQnH85KL5TAbzm+Oiz/WiKYQ9cza5T+50WoXFVjdfoF6efZ6tOxV+FtS/o +toga+N8z4FtkhbuY0qQx4nxM2wRd/XZHPFO0LRx+Z8E5lghedLOD7ocV7kN/FD9p +0/MMZ5kpeLevfnp4GBYjZKxojH8eOFni7WPovHUts/QHvEnYxucNwej8OTIy699w +APJbwEV3BVwzjqgsfywQxH80JEpNGzCRUt5yfnXpF4IUxzPVM1z2tp+/leoHxkxw +yfhL8FVfUbWnWvqIMY8QK6zhkqy22jb4lFE7jPEUwVu+HQc6KTzkYhZQQ5fOHmoa +pYsCbAF/AMZ1U8yT6OljVF904yIiLohR7F6s/maEu6mCdy82sNjpCdasuThqe7k0 +Hv4m4NcrULqhvKyyAqp/XgWMPeNuFkq3wqBk1DxLTw== +=oQfE +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/internal/helm/testdata/charts/pub.gpg b/internal/helm/testdata/charts/pub.gpg new file mode 100644 index 0000000000000000000000000000000000000000..5b711ded67cd4b2b26838340856433e954392889 GIT binary patch literal 1728 zcmV;x20!_k0gVJ=BM5{63;@n2aww|z0{Kp_QRBdB8SNamkuLtrpS{I8#Ahr0WtOJ0 z-qTmHqMEcfC|Kgt(^(hs&Gn{Q6@fXI;a>>d-I1!$N02Fx!VP+qmd=t0|LQ^CEZq%R z{%Og@3`8inY*Vdvzc-7~X3g`9QBYBIYi<&Q!9prlCuhGJiJCIb&T_vXSoV+EjSoXl zwwj9bfB+ekEN%LQTRhFYtsAAqZHLzLe_Swo#EeXAf*;ywu0-!J= z=AxQcv_?dCR^YVGztF|Kw%m;;7`!x+*de6HI$%D8bjskGN9O<$0RREC2vlWrbX9X@ za)|-d1QP)W06rEW1h>+p$JaO5wpg_lT!W9Kl)II+F9iZ(BM5{78v_Li1Hxwj1q%rX z2Lc8a3JC}c0t6NU0|5da0Rk6*0162ZgO8+?yOp&sE^!P0L%{a=c)M$~Dv%4LOV5V% z=?8w{aVV{s0zVkuzfUcfH8WC@dZD~~RDcwr6j3@bH=W)yK&+~*SGy$Fj{#}tjP z>(DL+G_ftYTPnC(LP5bMnJ_+hCPE*!h@D!aK2xV~&lhDbc{mYV_v9Y2!rr2870Ekv zpodQLQrZU7Y-nF^!j6Dcx3-)Yg8AS3;?)?q`<*a=dS7Sgc+wj<5>ron)=fRcPzwf2aGLCAs6L@m` z%=#b+xdDv?Vj~EI0So}%8vc@v(?^ znMiO+Gop60nY<_KZwIlsT763058tb76_A%!dwmqCy)?<=2nu#ilJ%M?mm*@GyC&u_ zee#%_+VlC@eWZJ?pbn~E5R;$RtaB9^p4dnV(ozv zNuRpCW+%3IFKZPMksVBE&*S#9dpiXPx{*mH9%I$m=4u4|6u7-d@0zibXk?fY9| z5^w_|1r68Z-%S{p4cKhjHWL`Dq8h1Tl;?b{w_{u8?SIbz5di=Ji2=L>7y$?XCKe$C zx6-7?*EiUggO8+?yOp&s1p;Ct2!sL~3m>-HhR5ObgsRds^kZ0)JR0$(Kt<&M^4#2rNtQJ)=zZhYhuK*}X_bBF&v!8gzOI)03xuffc*Y}yx9ibDb+=q>dRpj%6?Q^APhZXW6pAdf(XRF>aWpSCf(vWgRU3 zaW#z}o|_c)yX@F`BR1)0RiN#z2ePwXx?PK*S4lARlp)*NcUSf!DRous_a>(l?*OvEoMTsSO27OLXO3 z`29)o)z|(HT|lJ62slzdq!AxX@zwTIE!N}gU5JnbS~X}yE7>^M_PN|sYUpQV4i1~) z7*JUO9_$jDLVy@bJP=d0;gK+Dp7CDcodqb#Nh_;R+27|Bnd$*{m1wZoewlSZvhKF4 WPWX$$0q&Hnc)}yAh!E^KfBh}6CO8ZL literal 0 HcmV?d00001 diff --git a/internal/helm/testdata/charts/sec.gpg b/internal/helm/testdata/charts/sec.gpg new file mode 100644 index 0000000000000000000000000000000000000000..9df8a2779b3b68c7592b190ad97dde8bedf7acad GIT binary patch literal 3670 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 literal 0 HcmV?d00001 From d435fe7b22de40e9cd773e317d70b9d4342bd058 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Tue, 8 Mar 2022 21:57:24 +0530 Subject: [PATCH 03/10] update helmchart reconciliation to verify charts using prov file Signed-off-by: Sanskar Jaiswal --- api/v1beta2/helmchart_types.go | 14 ++++ .../source.toolkit.fluxcd.io_helmcharts.yaml | 19 +++++ controllers/helmchart_controller.go | 79 ++++++++++++++++++- controllers/storage.go | 3 +- internal/helm/chart/builder_local.go | 6 +- internal/helm/chart/builder_remote.go | 2 +- internal/helm/chart/verify.go | 10 +-- 7 files changed, 122 insertions(+), 11 deletions(-) diff --git a/api/v1beta2/helmchart_types.go b/api/v1beta2/helmchart_types.go index 2ce5a942f..660356535 100644 --- a/api/v1beta2/helmchart_types.go +++ b/api/v1beta2/helmchart_types.go @@ -84,6 +84,20 @@ type HelmChartSpec struct { // NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 // +optional AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"` + + // Keyring information for verifying the packaged chart's signature using a provenance file. + // +optional + VerificationKeyring *VerificationKeyring `json:"verificationKeyring,omitempty"` +} + +type VerificationKeyring struct { + // +required + SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` + + // The key that corresponds to the keyring value. + // +kubebuilder:default:=pubring.gpg + // +optional + Key string `json:"key,omitempty"` } const ( diff --git a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml index a45d0370b..1671ac596 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml @@ -404,6 +404,25 @@ spec: items: type: string type: array + verificationKeyring: + description: Keyring information for verifying the packaged chart's + signature using a provenance file. + properties: + key: + default: pubring.gpg + description: The key that corresponds to the keyring value. + type: string + secretRef: + description: LocalObjectReference contains enough information + to locate the referenced Kubernetes resource object. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + type: object version: default: '*' description: Version is the chart version semver expression, ignored diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 3fa0c0271..3c700830b 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -95,6 +95,8 @@ var helmChartReadyCondition = summarize.Conditions{ }, } +const KeyringFileName = "pubring.gpg" + // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/status,verbs=get;update;patch // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/finalizers,verbs=get;create;update;patch;delete @@ -467,13 +469,20 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * opts.VersionMetadata = strconv.FormatInt(obj.Generation, 10) } + var keyring []byte + keyring, err = r.getProvenanceKeyring(ctx, obj) + if err != nil { + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, err.Error()) + return sreconcile.ResultEmpty, err + } + // Build the chart ref := chart.RemoteReference{Name: obj.Spec.Chart, Version: obj.Spec.Version} - build, err := cb.Build(ctx, ref, util.TempPathForObj("", ".tgz", obj), opts) + build, err := cb.Build(ctx, ref, util.TempPathForObj("", ".tgz", obj), opts, keyring) + if err != nil { return sreconcile.ResultEmpty, err } - *b = *build return sreconcile.ResultSuccess, nil } @@ -590,13 +599,19 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj } opts.VersionMetadata += strconv.FormatInt(obj.Generation, 10) } + var keyring []byte + keyring, err = r.getProvenanceKeyring(ctx, obj) + if err != nil { + conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, err.Error()) + return sreconcile.ResultEmpty, err + } // Build chart cb := chart.NewLocalBuilder(dm) build, err := cb.Build(ctx, chart.LocalReference{ WorkDir: sourceDir, Path: chartPath, - }, util.TempPathForObj("", ".tgz", obj), opts) + }, util.TempPathForObj("", ".tgz", obj), opts, keyring) if err != nil { return sreconcile.ResultEmpty, err } @@ -670,6 +685,18 @@ func (r *HelmChartReconciler) reconcileArtifact(ctx context.Context, obj *source return sreconcile.ResultEmpty, e } + // the provenance file artifact is not recorded, but it shadows the HelmChart artifact + // under the assumption that the file is always available at "chart.tgz.prov" + if b.ProvFilePath != "" { + provArtifact := r.Storage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), b.Version, fmt.Sprintf("%s-%s.tgz.prov", b.Name, b.Version)) + if err = r.Storage.CopyFromPath(&provArtifact, b.ProvFilePath); err != nil { + return sreconcile.ResultEmpty, &serror.Event{ + Err: fmt.Errorf("unable to copy Helm chart provenance file to storage: %w", err), + Reason: sourcev1.StorageOperationFailedReason, + } + } + } + // Record it on the object obj.Status.Artifact = artifact.DeepCopy() obj.Status.ObservedChartName = b.Name @@ -861,6 +888,22 @@ func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repos return &secret, nil } +func (r *HelmChartReconciler) getVerificationKeyringSecret(ctx context.Context, chart *sourcev1.HelmChart) (*corev1.Secret, error) { + if chart.Spec.VerificationKeyring == nil { + return nil, nil + } + name := types.NamespacedName{ + Namespace: chart.GetNamespace(), + Name: chart.Spec.VerificationKeyring.SecretRef.Name, + } + var secret corev1.Secret + err := r.Client.Get(ctx, name, &secret) + if err != nil { + return nil, err + } + return &secret, nil +} + func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string { repo, ok := o.(*sourcev1.HelmRepository) if !ok { @@ -1021,3 +1064,33 @@ func reasonForBuild(build *chart.Build) string { } return sourcev1.ChartPullSucceededReason } + +func (r *HelmChartReconciler) getProvenanceKeyring(ctx context.Context, chart *sourcev1.HelmChart) ([]byte, error) { + if chart.Spec.VerificationKeyring == nil { + return nil, nil + } + name := types.NamespacedName{ + Namespace: chart.GetNamespace(), + Name: chart.Spec.VerificationKeyring.SecretRef.Name, + } + var secret corev1.Secret + err := r.Client.Get(ctx, name, &secret) + if err != nil { + e := &serror.Event{ + Err: fmt.Errorf("failed to get secret '%s': %w", chart.Spec.VerificationKeyring.SecretRef.Name, err), + Reason: sourcev1.AuthenticationFailedReason, + } + return nil, e + } + key := chart.Spec.VerificationKeyring.Key + if val, ok := secret.Data[key]; !ok { + err = fmt.Errorf("secret doesn't contain the advertised verification keyring name %s", key) + e := &serror.Event{ + Err: fmt.Errorf("invalid secret '%s': %w", secret.GetName(), err), + Reason: sourcev1.AuthenticationFailedReason, + } + return nil, e + } else { + return val, nil + } +} diff --git a/controllers/storage.go b/controllers/storage.go index 55f9a077c..f8016f5fe 100644 --- a/controllers/storage.go +++ b/controllers/storage.go @@ -120,6 +120,7 @@ func (s *Storage) RemoveAll(artifact sourcev1.Artifact) (string, error) { func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) ([]string, error) { deletedFiles := []string{} localPath := s.LocalPath(artifact) + localProvPath := localPath + ".prov" dir := filepath.Dir(localPath) var errors []string _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -128,7 +129,7 @@ func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) ([]string, err return nil } - if path != localPath && !info.IsDir() && info.Mode()&os.ModeSymlink != os.ModeSymlink { + if path != localPath && path != localProvPath && !info.IsDir() && info.Mode()&os.ModeSymlink != os.ModeSymlink { if err := os.Remove(path); err != nil { errors = append(errors, info.Name()) } else { diff --git a/internal/helm/chart/builder_local.go b/internal/helm/chart/builder_local.go index 760280b62..cbbe840b8 100644 --- a/internal/helm/chart/builder_local.go +++ b/internal/helm/chart/builder_local.go @@ -148,15 +148,19 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, // If the chart at the path is already packaged and no custom values files // options are set, we can copy the chart without making modifications - provFilePath = provenanceFilePath(localRef.Path) if !requiresPackaging { + provFilePath = provenanceFilePath(p) if err = copyFileToPath(localRef.Path, p); err != nil { return result, &BuildError{Reason: ErrChartPull, Err: err} } + if err = copyFileToPath(provenanceFilePath(localRef.Path), provFilePath); err != nil { + return result, &BuildError{Reason: ErrChartPull, Err: err} + } if err = verifyProvFile(localRef.Path, provFilePath); err != nil { return result, err } result.Path = p + result.ProvFilePath = provFilePath return result, nil } diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index a159c5f1a..54e1bed8c 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -166,7 +166,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // This is needed, since the verification will work only if the .tgz file is untampered. // But we write the packaged chart to disk under a different name, so the provenance file // will not be valid for this _new_ packaged chart. - chart, err := util.WriteBytesToFile(chartBuf, fmt.Sprintf("%s-%s.tgz", result.Name, result.Version), false) + chart, err := util.WriteBytesToFile(chartBuf, fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version), false) defer os.Remove(chart.Name()) if err != nil { return nil, err diff --git a/internal/helm/chart/verify.go b/internal/helm/chart/verify.go index 0600acd7e..26e91c31e 100644 --- a/internal/helm/chart/verify.go +++ b/internal/helm/chart/verify.go @@ -11,6 +11,9 @@ import ( "helm.sh/helm/v3/pkg/provenance" ) +// Ref: https://github.com/helm/helm/blob/v3.8.0/pkg/downloader/chart_downloader.go#L328 +// modified to accept a custom provenance file path and an actual keyring instead of a +// path to the file containing the keyring. func VerifyProvenanceFile(keyring io.Reader, chartPath, provFilePath string) error { switch fi, err := os.Stat(chartPath); { case err != nil: @@ -43,15 +46,12 @@ func VerifyProvenanceFile(keyring io.Reader, chartPath, provFilePath string) err } // isTar tests whether the given file is a tar file. -// -// Currently, this simply checks extension, since a subsequent function will -// untar the file and validate its binary format. func isTar(filename string) bool { return strings.EqualFold(filepath.Ext(filename), ".tgz") } -// Returns the path of a provenance file related to a packaged chart -// Adds ".prov" at the end, as per the Helm convention. +// Returns the path of a provenance file related to a packaged chart by +// adding ".prov" at the end, as per the Helm convention. func provenanceFilePath(path string) string { return path + ".prov" } From 4988803679c22534e928defd35572297d06fc5d9 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Tue, 8 Mar 2022 21:59:05 +0530 Subject: [PATCH 04/10] update helmchart reconciler tests for prov file verification Signed-off-by: Sanskar Jaiswal --- controllers/helmchart_controller_test.go | 392 ++++++++++++++++-- .../testdata/charts/helmchart-0.1.0.tgz | Bin 3277 -> 3280 bytes .../testdata/charts/helmchart-0.1.0.tgz.prov | 26 ++ controllers/testdata/charts/pub.gpg | Bin 0 -> 1728 bytes controllers/testdata/charts/sec.gpg | Bin 0 -> 3670 bytes 5 files changed, 390 insertions(+), 28 deletions(-) create mode 100644 controllers/testdata/charts/helmchart-0.1.0.tgz.prov create mode 100644 controllers/testdata/charts/pub.gpg create mode 100644 controllers/testdata/charts/sec.gpg diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 43d568b85..5390f57a4 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -52,6 +52,8 @@ import ( sreconcile "github.com/fluxcd/source-controller/internal/reconcile" ) +const publicKeyFileName = "pub.pgp" + func TestHelmChartReconciler_Reconcile(t *testing.T) { g := NewWithT(t) @@ -65,7 +67,8 @@ func TestHelmChartReconciler_Reconcile(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) defer os.RemoveAll(server.Root()) - g.Expect(server.PackageChartWithVersion(chartPath, chartVersion)).To(Succeed()) + publicKeyPath := fmt.Sprintf("%s/%s", server.Root(), publicKeyFileName) + g.Expect(server.PackageSignedChartWithVersion(chartPath, chartVersion, publicKeyPath)).To(Succeed()) g.Expect(server.GenerateIndex()).To(Succeed()) server.Start() @@ -86,6 +89,21 @@ func TestHelmChartReconciler_Reconcile(t *testing.T) { } g.Expect(testEnv.CreateAndWait(ctx, repository)).To(Succeed()) + keyring, err := os.ReadFile(publicKeyPath) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + + keyringSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret", + Namespace: ns.Name, + }, + Data: map[string][]byte{ + publicKeyFileName: keyring, + }, + } + g.Expect(testEnv.CreateAndWait(ctx, keyringSecret)).To(Succeed()) + obj := &sourcev1.HelmChart{ ObjectMeta: metav1.ObjectMeta{ GenerateName: "helmrepository-reconcile-", @@ -98,6 +116,12 @@ func TestHelmChartReconciler_Reconcile(t *testing.T) { Kind: sourcev1.HelmRepositoryKind, Name: repository.Name, }, + VerificationKeyring: &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: keyringSecret.Name, + }, + Key: publicKeyFileName, + }, }, } g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) @@ -183,12 +207,21 @@ func TestHelmChartReconciler_reconcileStorage(t *testing.T) { Path: fmt.Sprintf("/reconcile-storage/%s.txt", v), Revision: v, } + provArtifact := &sourcev1.Artifact{ + Path: fmt.Sprintf("/reconcile-storage/%s.txt.prov", v), + Revision: v, + } + if err := testStorage.MkdirAll(*obj.Status.Artifact); err != nil { return err } + if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(v), 0644); err != nil { return err } + if err := testStorage.AtomicWriteFile(provArtifact, strings.NewReader(v), 0644); err != nil { + return err + } } testStorage.SetArtifactURL(obj.Status.Artifact) return nil @@ -204,6 +237,9 @@ func TestHelmChartReconciler_reconcileStorage(t *testing.T) { "/reconcile-storage/c.txt", "!/reconcile-storage/b.txt", "!/reconcile-storage/a.txt", + "/reconcile-storage/c.txt.prov", + "!/reconcile-storage/b.txt.prov", + "!/reconcile-storage/a.txt.prov", }, want: sreconcile.ResultSuccess, }, @@ -311,9 +347,13 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { } g.Expect(storage.Archive(gitArtifact, "testdata/charts", nil)).To(Succeed()) + keyring, err := os.ReadFile("testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + tests := []struct { name string source sourcev1.Source + secret *corev1.Secret beforeFunc func(obj *sourcev1.HelmChart) want sreconcile.Result wantErr error @@ -331,12 +371,27 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { Artifact: gitArtifact, }, }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring, + }, + }, beforeFunc: func(obj *sourcev1.HelmChart) { obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" obj.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ Name: "gitrepository", Kind: sourcev1.GitRepositoryKind, } + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret", + }, + Key: publicKeyFileName, + } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, build chart.Build, obj sourcev1.HelmChart) { @@ -344,6 +399,7 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { g.Expect(build.Name).To(Equal("helmchart")) g.Expect(build.Version).To(Equal("0.1.0")) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).To(BeARegularFile()) g.Expect(obj.Status.ObservedSourceArtifactRevision).To(Equal(gitArtifact.Revision)) g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ @@ -352,6 +408,7 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) + g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { @@ -458,6 +515,9 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { if tt.source != nil { clientBuilder.WithRuntimeObjects(tt.source) } + if tt.secret != nil { + clientBuilder.WithRuntimeObjects(tt.secret) + } r := &HelmChartReconciler{ Client: clientBuilder.Build(), @@ -508,33 +568,58 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { ) serverFactory, err := helmtestserver.NewTempHelmServer() + publicKeyPath := fmt.Sprintf("%s/%s", serverFactory.Root(), publicKeyFileName) + g.Expect(err).NotTo(HaveOccurred()) defer os.RemoveAll(serverFactory.Root()) for _, ver := range []string{chartVersion, higherChartVersion} { - g.Expect(serverFactory.PackageChartWithVersion(chartPath, ver)).To(Succeed()) + g.Expect(serverFactory.PackageSignedChartWithVersion(chartPath, ver, publicKeyPath+ver)).To(Succeed()) } g.Expect(serverFactory.GenerateIndex()).To(Succeed()) + keyring1, err := os.ReadFile(publicKeyPath + chartVersion) + g.Expect(err).ToNot(HaveOccurred()) + defer os.Remove(publicKeyPath + chartVersion) + + keyring2, err := os.ReadFile(publicKeyPath + higherChartVersion) + g.Expect(err).ToNot(HaveOccurred()) + defer os.Remove(publicKeyPath + higherChartVersion) + type options struct { username string password string } tests := []struct { - name string - server options - secret *corev1.Secret - beforeFunc func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) - want sreconcile.Result - wantErr error - assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) - cleanFunc func(g *WithT, build *chart.Build) + name string + server options + secret *corev1.Secret + keyringSecret *corev1.Secret + beforeFunc func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) + want sreconcile.Result + wantErr error + assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) + cleanFunc func(g *WithT, build *chart.Build) }{ { name: "Reconciles chart build", beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Spec.Chart = "helmchart" + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret-0.3.0", + }, + Key: publicKeyFileName, + } + }, + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret-0.3.0", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring2, + }, }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) { @@ -542,9 +627,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(higherChartVersion)) g.Expect(build.Path).ToNot(BeEmpty()) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).ToNot(BeEmpty()) + g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) + g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { @@ -553,6 +641,14 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { username: "foo", password: "bar", }, + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret-0.2.0", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring1, + }, + }, secret: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "auth", @@ -566,6 +662,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { obj.Spec.Chart = chartName obj.Spec.Version = chartVersion repository.Spec.SecretRef = &meta.LocalObjectReference{Name: "auth"} + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret-0.2.0", + }, + Key: publicKeyFileName, + } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) { @@ -573,17 +675,34 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(chartVersion)) g.Expect(build.Path).ToNot(BeEmpty()) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).ToNot(BeEmpty()) + g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) + g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { name: "Uses artifact as build cache", + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret-0.2.0", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring1, + }, + }, beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Spec.Chart = chartName obj.Spec.Version = chartVersion obj.Status.Artifact = &sourcev1.Artifact{Path: chartName + "-" + chartVersion + ".tgz"} + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret-0.2.0", + }, + Key: publicKeyFileName, + } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) { @@ -591,14 +710,30 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(chartVersion)) g.Expect(build.Path).To(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path))) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).To(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path+".prov"))) + g.Expect(build.ProvFilePath).To(BeARegularFile()) }, }, { name: "Sets Generation as VersionMetadata with values files", + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret-0.3.0", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring2, + }, + }, beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Spec.Chart = chartName obj.Generation = 3 obj.Spec.ValuesFiles = []string{"values.yaml", "override.yaml"} + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret-0.3.0", + }, + Key: publicKeyFileName, + } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) { @@ -606,13 +741,24 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(higherChartVersion + "+3")) g.Expect(build.Path).ToNot(BeEmpty()) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).ToNot(BeEmpty()) + g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) + g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { name: "Forces build on generation change", + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret-0.2.0", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring1, + }, + }, beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Generation = 3 obj.Spec.Chart = chartName @@ -620,6 +766,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { obj.Status.ObservedGeneration = 2 obj.Status.Artifact = &sourcev1.Artifact{Path: chartName + "-" + chartVersion + ".tgz"} + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret-0.2.0", + }, + Key: publicKeyFileName, + } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) { @@ -627,9 +779,12 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(chartVersion)) g.Expect(build.Path).ToNot(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path))) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).ToNot(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path+".prov"))) + g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) + g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { @@ -713,6 +868,9 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { if tt.secret != nil { clientBuilder.WithObjects(tt.secret.DeepCopy()) } + if tt.keyringSecret != nil { + clientBuilder.WithObjects(tt.keyringSecret.DeepCopy()) + } storage, err := newTestStorage(server) g.Expect(err).ToNot(HaveOccurred()) @@ -754,6 +912,9 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { defer tt.cleanFunc(g, &b) } got, err := r.buildFromHelmRepository(context.TODO(), obj, repository, &b) + if err != nil { + t.Logf("error: %s", err) + } g.Expect(err != nil).To(Equal(tt.wantErr != nil)) if tt.wantErr != nil { @@ -791,18 +952,28 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { g.Expect(storage.CopyFromPath(yamlArtifact, "testdata/charts/helmchart/values.yaml")).To(Succeed()) cachedArtifact := &sourcev1.Artifact{ Revision: "0.1.0", - Path: "cached.tgz", + Path: "helmchart-0.1.0.tgz", } g.Expect(storage.CopyFromPath(cachedArtifact, "testdata/charts/helmchart-0.1.0.tgz")).To(Succeed()) + provArtifact := &sourcev1.Artifact{ + Revision: "1234smth", + Path: "helmchart-0.1.0.tgz.prov", + } + g.Expect(storage.CopyFromPath(provArtifact, "testdata/charts/helmchart-0.1.0.tgz.prov")).To(Succeed()) + + keyring, err := os.ReadFile("testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + tests := []struct { - name string - source sourcev1.Artifact - beforeFunc func(obj *sourcev1.HelmChart) - want sreconcile.Result - wantErr error - assertFunc func(g *WithT, build chart.Build) - cleanFunc func(g *WithT, build *chart.Build) + name string + source sourcev1.Artifact + keyringSecret *corev1.Secret + beforeFunc func(obj *sourcev1.HelmChart) + want sreconcile.Result + wantErr error + assertFunc func(g *WithT, build chart.Build) + cleanFunc func(g *WithT, build *chart.Build) }{ { name: "Resolves chart dependencies and builds", @@ -866,9 +1037,24 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { { name: "Chart from storage cache", source: *chartsArtifact.DeepCopy(), + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring, + }, + }, beforeFunc: func(obj *sourcev1.HelmChart) { obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" obj.Status.Artifact = cachedArtifact.DeepCopy() + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret", + }, + Key: publicKeyFileName, + } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, build chart.Build) { @@ -876,11 +1062,21 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { g.Expect(build.Version).To(Equal("0.1.0")) g.Expect(build.Path).To(Equal(storage.LocalPath(*cachedArtifact.DeepCopy()))) g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.ProvFilePath).To(Equal(storage.LocalPath(*provArtifact.DeepCopy()))) + g.Expect(build.ProvFilePath).To(BeARegularFile()) }, }, { name: "Generation change forces rebuild", source: *chartsArtifact.DeepCopy(), + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring, + }, + }, beforeFunc: func(obj *sourcev1.HelmChart) { obj.Generation = 2 obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" @@ -921,8 +1117,13 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) + clientBuilder := fake.NewClientBuilder() + if tt.keyringSecret != nil { + clientBuilder.WithObjects(tt.keyringSecret.DeepCopy()) + } + r := &HelmChartReconciler{ - Client: fake.NewClientBuilder().Build(), + Client: clientBuilder.Build(), EventRecorder: record.NewFakeRecorder(32), Storage: storage, Getters: testGetters, @@ -982,13 +1183,34 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, { name: "Copying artifact to storage from build makes Ready=True", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz"), + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", ""), beforeFunc: func(obj *sourcev1.HelmChart) { conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "") }, afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) - t.Expect(obj.GetArtifact().Checksum).To(Equal("bbdf96023c912c393b49d5238e227576ed0d20d1bb145d7476d817b80e20c11a")) + t.Expect(obj.GetArtifact().Checksum).To(Equal("5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3")) + t.Expect(obj.GetArtifact().Revision).To(Equal("0.1.0")) + t.Expect(obj.Status.URL).ToNot(BeEmpty()) + t.Expect(obj.Status.ObservedChartName).To(Equal("helmchart")) + }, + want: sreconcile.ResultSuccess, + assertConditions: []metav1.Condition{ + *conditions.TrueCondition(meta.ReadyCondition, sourcev1.ChartPullSucceededReason, "pulled 'helmchart' chart with version '0.1.0'"), + }, + }, + { + name: "A build with a non-nil ProvFilePath persists prov file to storage", + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", "testdata/charts/helmchart-0.1.0.tgz"), + beforeFunc: func(obj *sourcev1.HelmChart) { + conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "") + }, + afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { + provArtifact := testStorage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), "0.1.0", fmt.Sprintf("%s-%s.tgz.prov", "helmchart", "0.1.0")) + t.Expect(provArtifact.Path).ToNot(BeEmpty()) + t.Expect(obj.GetArtifact()).ToNot(BeNil()) + fmt.Printf("checksum: %s", obj.GetArtifact().Checksum) + t.Expect(obj.GetArtifact().Checksum).To(Equal("5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3")) t.Expect(obj.GetArtifact().Revision).To(Equal("0.1.0")) t.Expect(obj.Status.URL).ToNot(BeEmpty()) t.Expect(obj.Status.ObservedChartName).To(Equal("helmchart")) @@ -1043,13 +1265,13 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, { name: "Removes ArtifactOutdatedCondition after creating new artifact", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz"), + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", ""), beforeFunc: func(obj *sourcev1.HelmChart) { conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "") }, afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) - t.Expect(obj.GetArtifact().Checksum).To(Equal("bbdf96023c912c393b49d5238e227576ed0d20d1bb145d7476d817b80e20c11a")) + t.Expect(obj.GetArtifact().Checksum).To(Equal("5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3")) t.Expect(obj.GetArtifact().Revision).To(Equal("0.1.0")) t.Expect(obj.Status.URL).ToNot(BeEmpty()) t.Expect(obj.Status.ObservedChartName).To(Equal("helmchart")) @@ -1061,7 +1283,7 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, { name: "Creates latest symlink to the created artifact", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz"), + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", ""), afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) @@ -1185,6 +1407,105 @@ func TestHelmChartReconciler_getHelmRepositorySecret(t *testing.T) { } } +func TestHelmChartReconciler_getVerificationKeyring(t *testing.T) { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret", + Namespace: "foo", + }, + Data: map[string][]byte{ + publicKeyFileName: []byte("bar"), + }, + } + clientBuilder := fake.NewClientBuilder() + clientBuilder.WithObjects(secret) + + r := &HelmChartReconciler{ + Client: clientBuilder.Build(), + } + + tests := []struct { + name string + chart *sourcev1.HelmChart + want []byte + wantErr bool + }{ + { + name: "Existing secret reference", + chart: &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: secret.Namespace, + }, + Spec: sourcev1.HelmChartSpec{ + VerificationKeyring: &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret", + }, + Key: publicKeyFileName, + }, + }, + }, + want: []byte("bar"), + }, + { + name: "Empty secret reference", + chart: &sourcev1.HelmChart{ + Spec: sourcev1.HelmChartSpec{ + VerificationKeyring: nil, + }, + }, + want: nil, + }, + { + name: "Error on client error", + chart: &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "different", + }, + Spec: sourcev1.HelmChartSpec{ + VerificationKeyring: &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret", + }, + Key: publicKeyFileName, + }, + }, + }, + wantErr: true, + }, + { + name: "Error on invalid key", + chart: &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "foo", + }, + Spec: sourcev1.HelmChartSpec{ + VerificationKeyring: &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret", + }, + Key: "invalid-key", + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + got, err := r.getProvenanceKeyring(context.TODO(), tt.chart) + g.Expect(err != nil).To(Equal(tt.wantErr)) + g.Expect(got).To(Equal(tt.want)) + }) + } +} + func TestHelmChartReconciler_getSource(t *testing.T) { mocks := []client.Object{ &sourcev1.HelmRepository{ @@ -1462,8 +1783,9 @@ func TestHelmChartReconciler_reconcileSubRecs(t *testing.T) { } } -func mockChartBuild(name, version, path string) *chart.Build { +func mockChartBuild(name, version, path, provFilePath string) *chart.Build { var copyP string + var copyPP string if path != "" { f, err := os.Open(path) if err == nil { @@ -1477,9 +1799,23 @@ func mockChartBuild(name, version, path string) *chart.Build { } } } + if provFilePath != "" { + f, err := os.Open(provFilePath) + if err == nil { + defer f.Close() + ff, err := os.CreateTemp("", "chart-mock-*.tgz.prov") + if err == nil { + defer ff.Close() + if _, err = io.Copy(ff, f); err == nil { + copyPP = ff.Name() + } + } + } + } return &chart.Build{ - Name: name, - Version: version, - Path: copyP, + Name: name, + Version: version, + Path: copyP, + ProvFilePath: copyPP, } } diff --git a/controllers/testdata/charts/helmchart-0.1.0.tgz b/controllers/testdata/charts/helmchart-0.1.0.tgz index f64a32eeeb54fc24a44390478a3adf5bcb5cf754..a43147fd87321deb4d03de1d89d5e16a7d4a6baf 100644 GIT binary patch delta 3217 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 Da%W-z delta 3214 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 A6951J diff --git a/controllers/testdata/charts/helmchart-0.1.0.tgz.prov b/controllers/testdata/charts/helmchart-0.1.0.tgz.prov new file mode 100644 index 000000000..df5962f5f --- /dev/null +++ b/controllers/testdata/charts/helmchart-0.1.0.tgz.prov @@ -0,0 +1,26 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +apiVersion: v2 +appVersion: 1.16.0 +description: A Helm chart for Kubernetes +name: helmchart +type: application +version: 0.1.0 + +... +files: + helmchart-0.1.0.tgz: sha256:5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3 +-----BEGIN PGP SIGNATURE----- + +wsDcBAEBCgAQBQJiJkq8CRBwNbqX0yqHwQAAJ5sMAKTMqzOLvrunXBQ8TPXcqXGc +bzZ9MSrK3mBzQlhZHxabCZP25vemUwGsvNyS2BS8ow+dDzaug8luZBw1aC5slSyS +uAG9bkcyqHwsHnJ+Z8O5cpYsmOzhA4CDSsq1xICxCKYy2jZPj+I8KBk5INtSi6gP +lB1YV4ry42D2BsxK7IuPr5iUzecsVNsMbvupVLUSqsR3k3A2plGhBw10yCnYxEFg +MYlUdhHKFOSiUEmgif7toEQFfofxoutcPaAHe1zRYE4t1M2AozGx3njXchTdkHbe +WvsNc96wrFAGu852VXC6hyJH2keWhY91vaoELVbkDYnCiHouu/yXE4ox2MgN6Kr0 +u1gNeaEtk+IxYthDuBBRfslQ6O6lIfi3vObanC6Wl1pmC3YyYtnFOz6VQzW9k+Sv +FF9l6ysxoYxGWzAmIhGDIoohKwD1LtRBDjWHPivsOkYyEjmFb0MUsgFHuhVRMV4Z +aTckCGBebQS0bR3wE4GaGV1sLVoZDXph7v7YGa7Enw== +=7DS/ +-----END PGP SIGNATURE----- \ No newline at end of file diff --git a/controllers/testdata/charts/pub.gpg b/controllers/testdata/charts/pub.gpg new file mode 100644 index 0000000000000000000000000000000000000000..291f8abd8b4f524a544bdbe21cf0e1212ce010b1 GIT binary patch literal 1728 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 literal 0 HcmV?d00001 From cef5beed3b1b1acdfacee4d2314e393136594330 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Mon, 14 Mar 2022 20:59:56 +0530 Subject: [PATCH 05/10] refactor code to be more neat Signed-off-by: Sanskar Jaiswal --- api/go.mod | 5 +- api/go.sum | 15 +- controllers/helmchart_controller.go | 68 ++-- controllers/helmchart_controller_test.go | 352 ++++++++---------- controllers/storage.go | 31 +- .../testdata/charts/helmchart-0.1.0.tgz | Bin 3280 -> 3416 bytes .../testdata/charts/helmchart-0.1.0.tgz.prov | 22 +- internal/helm/chart/builder.go | 10 +- internal/helm/chart/builder_local.go | 39 +- internal/helm/chart/builder_local_test.go | 42 ++- internal/helm/chart/builder_remote.go | 35 +- internal/helm/chart/builder_remote_test.go | 39 +- internal/helm/chart/verify.go | 56 ++- internal/helm/repository/chart_repository.go | 2 +- internal/util/file.go | 43 +++ internal/util/temp.go | 26 -- 16 files changed, 454 insertions(+), 331 deletions(-) create mode 100644 internal/util/file.go diff --git a/api/go.mod b/api/go.mod index a5445cc68..7ceae5b8a 100644 --- a/api/go.mod +++ b/api/go.mod @@ -17,10 +17,13 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 // indirect + golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d // indirect + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/text v0.3.7 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/api v0.23.4 // indirect k8s.io/klog/v2 v2.30.0 // indirect k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect diff --git a/api/go.sum b/api/go.sum index 0526ae80d..1c0d21c48 100644 --- a/api/go.sum +++ b/api/go.sum @@ -297,6 +297,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -334,7 +336,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -569,8 +570,9 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9 h1:kmreh1vGI63l2FxOAYS3Yv6ATsi7lSTuwNSVbGfJV9I= golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE= +golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -660,8 +662,9 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk= golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -857,8 +860,9 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= @@ -893,8 +897,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.23.0 h1:WrL1gb73VSC8obi8cuYETJGXEoFNEh3LU0Pt+Sokgro= k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= +k8s.io/api v0.23.4 h1:85gnfXQOWbJa1SiWGpE9EEtHs0UVvDyIsSMpEtl2D4E= +k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= k8s.io/apimachinery v0.23.4 h1:fhnuMd/xUL3Cjfl64j5ULKZ1/J9n8NuQEgNL+WXWfdM= diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 3c700830b..ddd201d3c 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -73,6 +73,7 @@ var helmChartReadyCondition = summarize.Conditions{ sourcev1.FetchFailedCondition, sourcev1.StorageOperationFailedCondition, sourcev1.ArtifactOutdatedCondition, + sourcev1.SourceVerifiedCondition, meta.ReadyCondition, meta.ReconcilingCondition, meta.StalledCondition, @@ -82,6 +83,7 @@ var helmChartReadyCondition = summarize.Conditions{ sourcev1.FetchFailedCondition, sourcev1.StorageOperationFailedCondition, sourcev1.ArtifactOutdatedCondition, + sourcev1.SourceVerifiedCondition, meta.StalledCondition, meta.ReconcilingCondition, }, @@ -469,16 +471,20 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * opts.VersionMetadata = strconv.FormatInt(obj.Generation, 10) } - var keyring []byte - keyring, err = r.getProvenanceKeyring(ctx, obj) + keyring, err := r.getProvenanceKeyring(ctx, obj) if err != nil { - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, err.Error()) - return sreconcile.ResultEmpty, err + e := &serror.Event{ + Err: fmt.Errorf("failed to get public key for chart signature verification: %w", err), + Reason: sourcev1.SourceVerifiedCondition, + } + conditions.MarkFalse(obj, sourcev1.FetchFailedCondition, sourcev1.SourceVerifiedCondition, e.Error()) + return sreconcile.ResultEmpty, e } + opts.Keyring = keyring // Build the chart ref := chart.RemoteReference{Name: obj.Spec.Chart, Version: obj.Spec.Version} - build, err := cb.Build(ctx, ref, util.TempPathForObj("", ".tgz", obj), opts, keyring) + build, err := cb.Build(ctx, ref, util.TempPathForObj("", ".tgz", obj), opts) if err != nil { return sreconcile.ResultEmpty, err @@ -599,19 +605,23 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj } opts.VersionMetadata += strconv.FormatInt(obj.Generation, 10) } - var keyring []byte - keyring, err = r.getProvenanceKeyring(ctx, obj) + keyring, err := r.getProvenanceKeyring(ctx, obj) if err != nil { - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, sourcev1.AuthenticationFailedReason, err.Error()) - return sreconcile.ResultEmpty, err + e := &serror.Event{ + Err: fmt.Errorf("failed to get public key for chart signature verification: %w", err), + Reason: sourcev1.SourceVerifiedCondition, + } + conditions.MarkFalse(obj, sourcev1.FetchFailedCondition, sourcev1.SourceVerifiedCondition, e.Error()) + return sreconcile.ResultEmpty, e } + opts.Keyring = keyring // Build chart cb := chart.NewLocalBuilder(dm) build, err := cb.Build(ctx, chart.LocalReference{ WorkDir: sourceDir, Path: chartPath, - }, util.TempPathForObj("", ".tgz", obj), opts, keyring) + }, util.TempPathForObj("", ".tgz", obj), opts) if err != nil { return sreconcile.ResultEmpty, err } @@ -641,6 +651,14 @@ func (r *HelmChartReconciler) reconcileArtifact(ctx context.Context, obj *source conditions.Delete(obj, sourcev1.ArtifactOutdatedCondition) conditions.MarkTrue(obj, meta.ReadyCondition, reasonForBuild(b), b.Summary()) } + if b.VerificationSignature != nil && b.ProvFilePath != "" && obj.GetArtifact() != nil { + var sigVerMsg strings.Builder + sigVerMsg.WriteString(fmt.Sprintf("chart signed by: %v", strings.Join(b.VerificationSignature.Identities[:], ","))) + sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: %X", b.VerificationSignature.KeyFingerprint)) + sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: %s", b.VerificationSignature.FileHash)) + + conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, reasonForBuild(b), sigVerMsg.String()) + } }() // Create artifact from build data @@ -692,7 +710,7 @@ func (r *HelmChartReconciler) reconcileArtifact(ctx context.Context, obj *source if err = r.Storage.CopyFromPath(&provArtifact, b.ProvFilePath); err != nil { return sreconcile.ResultEmpty, &serror.Event{ Err: fmt.Errorf("unable to copy Helm chart provenance file to storage: %w", err), - Reason: sourcev1.StorageOperationFailedReason, + Reason: sourcev1.StorageOperationFailedCondition, } } } @@ -790,15 +808,23 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *sourcev1. obj.Status.Artifact = nil return nil } + if obj.GetArtifact() != nil { - if deleted, err := r.Storage.RemoveAllButCurrent(*obj.GetArtifact()); err != nil { + localPath := r.Storage.LocalPath(*obj.GetArtifact()) + provFilePath := localPath + ".prov" + dir := filepath.Dir(localPath) + callbacks := make([]func(path string, info os.FileInfo) bool, 0) + callbacks = append(callbacks, func(path string, info os.FileInfo) bool { + if path != localPath && path != provFilePath && info.Mode()&os.ModeSymlink != os.ModeSymlink { + return true + } + return false + }) + if _, err := r.Storage.RemoveConditionally(dir, callbacks); err != nil { return &serror.Event{ Err: fmt.Errorf("garbage collection of old artifacts failed: %w", err), Reason: "GarbageCollectionFailed", } - } else if len(deleted) > 0 { - r.eventLogf(ctx, obj, events.EventTypeTrace, "GarbageCollectionSucceeded", - "garbage collected old artifacts") } } return nil @@ -1076,20 +1102,12 @@ func (r *HelmChartReconciler) getProvenanceKeyring(ctx context.Context, chart *s var secret corev1.Secret err := r.Client.Get(ctx, name, &secret) if err != nil { - e := &serror.Event{ - Err: fmt.Errorf("failed to get secret '%s': %w", chart.Spec.VerificationKeyring.SecretRef.Name, err), - Reason: sourcev1.AuthenticationFailedReason, - } - return nil, e + return nil, err } key := chart.Spec.VerificationKeyring.Key if val, ok := secret.Data[key]; !ok { err = fmt.Errorf("secret doesn't contain the advertised verification keyring name %s", key) - e := &serror.Event{ - Err: fmt.Errorf("invalid secret '%s': %w", secret.GetName(), err), - Reason: sourcev1.AuthenticationFailedReason, - } - return nil, e + return nil, err } else { return val, nil } diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 5390f57a4..37b08630f 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -67,7 +67,119 @@ func TestHelmChartReconciler_Reconcile(t *testing.T) { g.Expect(err).NotTo(HaveOccurred()) defer os.RemoveAll(server.Root()) + g.Expect(server.PackageChartWithVersion(chartPath, chartVersion)).To(Succeed()) + g.Expect(server.GenerateIndex()).To(Succeed()) + + server.Start() + defer server.Stop() + + ns, err := testEnv.CreateNamespace(ctx, "helmchart") + g.Expect(err).ToNot(HaveOccurred()) + defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }() + + repository := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "helmrepository-", + Namespace: ns.Name, + }, + Spec: sourcev1.HelmRepositorySpec{ + URL: server.URL(), + }, + } + g.Expect(testEnv.CreateAndWait(ctx, repository)).To(Succeed()) + + obj := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "helmrepository-reconcile-", + Namespace: ns.Name, + }, + Spec: sourcev1.HelmChartSpec{ + Chart: chartName, + Version: chartVersion, + SourceRef: sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: repository.Name, + }, + }, + } + g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) + + key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} + + // Wait for finalizer to be set + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, obj); err != nil { + return false + } + return len(obj.Finalizers) > 0 + }, timeout).Should(BeTrue()) + + // Wait for HelmChart to be Ready + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, obj); err != nil { + return false + } + if !conditions.IsReady(obj) || obj.Status.Artifact == nil { + return false + } + readyCondition := conditions.Get(obj, meta.ReadyCondition) + return obj.Generation == readyCondition.ObservedGeneration && + obj.Generation == obj.Status.ObservedGeneration + }, timeout).Should(BeTrue()) + + // Check if the object status is valid. + condns := &status.Conditions{NegativePolarity: helmChartReadyCondition.NegativePolarity} + checker := status.NewChecker(testEnv.Client, testEnv.GetScheme(), condns) + checker.CheckErr(ctx, obj) + + // kstatus client conformance check. + u, err := patch.ToUnstructured(obj) + g.Expect(err).ToNot(HaveOccurred()) + res, err := kstatus.Compute(u) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(res.Status).To(Equal(kstatus.CurrentStatus)) + + // Patch the object with reconcile request annotation. + patchHelper, err := patch.NewHelper(obj, testEnv.Client) + g.Expect(err).ToNot(HaveOccurred()) + annotations := map[string]string{ + meta.ReconcileRequestAnnotation: "now", + } + obj.SetAnnotations(annotations) + g.Expect(patchHelper.Patch(ctx, obj)).ToNot(HaveOccurred()) + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, obj); err != nil { + return false + } + return obj.Status.LastHandledReconcileAt == "now" + }, timeout).Should(BeTrue()) + + g.Expect(testEnv.Delete(ctx, obj)).To(Succeed()) + + // Wait for HelmChart to be deleted + g.Eventually(func() bool { + if err := testEnv.Get(ctx, key, obj); err != nil { + return apierrors.IsNotFound(err) + } + return false + }, timeout).Should(BeTrue()) +} + +func TestHelmChartReconciler_ReconcileWithSigVerification(t *testing.T) { + g := NewWithT(t) + + const ( + chartName = "helmchart" + chartVersion = "0.2.0" + chartPath = "testdata/charts/helmchart" + ) + + server, err := helmtestserver.NewTempHelmServer() + g.Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(server.Root()) + publicKeyPath := fmt.Sprintf("%s/%s", server.Root(), publicKeyFileName) + g.Expect(server.PackageSignedChartWithVersion(chartPath, chartVersion, publicKeyPath)).To(Succeed()) g.Expect(server.GenerateIndex()).To(Succeed()) @@ -215,7 +327,6 @@ func TestHelmChartReconciler_reconcileStorage(t *testing.T) { if err := testStorage.MkdirAll(*obj.Status.Artifact); err != nil { return err } - if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(v), 0644); err != nil { return err } @@ -347,13 +458,9 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { } g.Expect(storage.Archive(gitArtifact, "testdata/charts", nil)).To(Succeed()) - keyring, err := os.ReadFile("testdata/charts/pub.gpg") - g.Expect(err).ToNot(HaveOccurred()) - tests := []struct { name string source sourcev1.Source - secret *corev1.Secret beforeFunc func(obj *sourcev1.HelmChart) want sreconcile.Result wantErr error @@ -371,27 +478,12 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { Artifact: gitArtifact, }, }, - secret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret", - Namespace: "default", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring, - }, - }, beforeFunc: func(obj *sourcev1.HelmChart) { obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" obj.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ Name: "gitrepository", Kind: sourcev1.GitRepositoryKind, } - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret", - }, - Key: publicKeyFileName, - } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, build chart.Build, obj sourcev1.HelmChart) { @@ -399,7 +491,6 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { g.Expect(build.Name).To(Equal("helmchart")) g.Expect(build.Version).To(Equal("0.1.0")) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).To(BeARegularFile()) g.Expect(obj.Status.ObservedSourceArtifactRevision).To(Equal(gitArtifact.Revision)) g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ @@ -408,7 +499,6 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) - g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { @@ -515,9 +605,6 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { if tt.source != nil { clientBuilder.WithRuntimeObjects(tt.source) } - if tt.secret != nil { - clientBuilder.WithRuntimeObjects(tt.secret) - } r := &HelmChartReconciler{ Client: clientBuilder.Build(), @@ -568,58 +655,33 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { ) serverFactory, err := helmtestserver.NewTempHelmServer() - publicKeyPath := fmt.Sprintf("%s/%s", serverFactory.Root(), publicKeyFileName) - g.Expect(err).NotTo(HaveOccurred()) defer os.RemoveAll(serverFactory.Root()) for _, ver := range []string{chartVersion, higherChartVersion} { - g.Expect(serverFactory.PackageSignedChartWithVersion(chartPath, ver, publicKeyPath+ver)).To(Succeed()) + g.Expect(serverFactory.PackageChartWithVersion(chartPath, ver)).To(Succeed()) } g.Expect(serverFactory.GenerateIndex()).To(Succeed()) - keyring1, err := os.ReadFile(publicKeyPath + chartVersion) - g.Expect(err).ToNot(HaveOccurred()) - defer os.Remove(publicKeyPath + chartVersion) - - keyring2, err := os.ReadFile(publicKeyPath + higherChartVersion) - g.Expect(err).ToNot(HaveOccurred()) - defer os.Remove(publicKeyPath + higherChartVersion) - type options struct { username string password string } tests := []struct { - name string - server options - secret *corev1.Secret - keyringSecret *corev1.Secret - beforeFunc func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) - want sreconcile.Result - wantErr error - assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) - cleanFunc func(g *WithT, build *chart.Build) + name string + server options + secret *corev1.Secret + beforeFunc func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) + want sreconcile.Result + wantErr error + assertFunc func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) + cleanFunc func(g *WithT, build *chart.Build) }{ { name: "Reconciles chart build", beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Spec.Chart = "helmchart" - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret-0.3.0", - }, - Key: publicKeyFileName, - } - }, - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret-0.3.0", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring2, - }, }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) { @@ -627,12 +689,9 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(higherChartVersion)) g.Expect(build.Path).ToNot(BeEmpty()) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).ToNot(BeEmpty()) - g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) - g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { @@ -641,14 +700,6 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { username: "foo", password: "bar", }, - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret-0.2.0", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring1, - }, - }, secret: &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "auth", @@ -662,12 +713,6 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { obj.Spec.Chart = chartName obj.Spec.Version = chartVersion repository.Spec.SecretRef = &meta.LocalObjectReference{Name: "auth"} - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret-0.2.0", - }, - Key: publicKeyFileName, - } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) { @@ -675,34 +720,17 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(chartVersion)) g.Expect(build.Path).ToNot(BeEmpty()) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).ToNot(BeEmpty()) - g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) - g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { name: "Uses artifact as build cache", - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret-0.2.0", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring1, - }, - }, beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Spec.Chart = chartName obj.Spec.Version = chartVersion obj.Status.Artifact = &sourcev1.Artifact{Path: chartName + "-" + chartVersion + ".tgz"} - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret-0.2.0", - }, - Key: publicKeyFileName, - } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) { @@ -710,30 +738,14 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(chartVersion)) g.Expect(build.Path).To(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path))) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).To(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path+".prov"))) - g.Expect(build.ProvFilePath).To(BeARegularFile()) }, }, { name: "Sets Generation as VersionMetadata with values files", - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret-0.3.0", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring2, - }, - }, beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Spec.Chart = chartName obj.Generation = 3 obj.Spec.ValuesFiles = []string{"values.yaml", "override.yaml"} - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret-0.3.0", - }, - Key: publicKeyFileName, - } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, _ *sourcev1.HelmChart, build chart.Build) { @@ -741,24 +753,13 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(higherChartVersion + "+3")) g.Expect(build.Path).ToNot(BeEmpty()) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).ToNot(BeEmpty()) - g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) - g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { name: "Forces build on generation change", - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret-0.2.0", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring1, - }, - }, beforeFunc: func(obj *sourcev1.HelmChart, repository *sourcev1.HelmRepository) { obj.Generation = 3 obj.Spec.Chart = chartName @@ -766,12 +767,6 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { obj.Status.ObservedGeneration = 2 obj.Status.Artifact = &sourcev1.Artifact{Path: chartName + "-" + chartVersion + ".tgz"} - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret-0.2.0", - }, - Key: publicKeyFileName, - } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, obj *sourcev1.HelmChart, build chart.Build) { @@ -779,12 +774,9 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { g.Expect(build.Version).To(Equal(chartVersion)) g.Expect(build.Path).ToNot(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path))) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).ToNot(Equal(filepath.Join(serverFactory.Root(), obj.Status.Artifact.Path+".prov"))) - g.Expect(build.ProvFilePath).To(BeARegularFile()) }, cleanFunc: func(g *WithT, build *chart.Build) { g.Expect(os.Remove(build.Path)).To(Succeed()) - g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) }, }, { @@ -868,9 +860,6 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { if tt.secret != nil { clientBuilder.WithObjects(tt.secret.DeepCopy()) } - if tt.keyringSecret != nil { - clientBuilder.WithObjects(tt.keyringSecret.DeepCopy()) - } storage, err := newTestStorage(server) g.Expect(err).ToNot(HaveOccurred()) @@ -912,9 +901,6 @@ func TestHelmChartReconciler_buildFromHelmRepository(t *testing.T) { defer tt.cleanFunc(g, &b) } got, err := r.buildFromHelmRepository(context.TODO(), obj, repository, &b) - if err != nil { - t.Logf("error: %s", err) - } g.Expect(err != nil).To(Equal(tt.wantErr != nil)) if tt.wantErr != nil { @@ -952,28 +938,18 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { g.Expect(storage.CopyFromPath(yamlArtifact, "testdata/charts/helmchart/values.yaml")).To(Succeed()) cachedArtifact := &sourcev1.Artifact{ Revision: "0.1.0", - Path: "helmchart-0.1.0.tgz", + Path: "cached.tgz", } g.Expect(storage.CopyFromPath(cachedArtifact, "testdata/charts/helmchart-0.1.0.tgz")).To(Succeed()) - provArtifact := &sourcev1.Artifact{ - Revision: "1234smth", - Path: "helmchart-0.1.0.tgz.prov", - } - g.Expect(storage.CopyFromPath(provArtifact, "testdata/charts/helmchart-0.1.0.tgz.prov")).To(Succeed()) - - keyring, err := os.ReadFile("testdata/charts/pub.gpg") - g.Expect(err).ToNot(HaveOccurred()) - tests := []struct { - name string - source sourcev1.Artifact - keyringSecret *corev1.Secret - beforeFunc func(obj *sourcev1.HelmChart) - want sreconcile.Result - wantErr error - assertFunc func(g *WithT, build chart.Build) - cleanFunc func(g *WithT, build *chart.Build) + name string + source sourcev1.Artifact + beforeFunc func(obj *sourcev1.HelmChart) + want sreconcile.Result + wantErr error + assertFunc func(g *WithT, build chart.Build) + cleanFunc func(g *WithT, build *chart.Build) }{ { name: "Resolves chart dependencies and builds", @@ -1037,24 +1013,9 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { { name: "Chart from storage cache", source: *chartsArtifact.DeepCopy(), - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret", - Namespace: "default", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring, - }, - }, beforeFunc: func(obj *sourcev1.HelmChart) { obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" obj.Status.Artifact = cachedArtifact.DeepCopy() - obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ - SecretRef: meta.LocalObjectReference{ - Name: "keyring-secret", - }, - Key: publicKeyFileName, - } }, want: sreconcile.ResultSuccess, assertFunc: func(g *WithT, build chart.Build) { @@ -1062,21 +1023,11 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { g.Expect(build.Version).To(Equal("0.1.0")) g.Expect(build.Path).To(Equal(storage.LocalPath(*cachedArtifact.DeepCopy()))) g.Expect(build.Path).To(BeARegularFile()) - g.Expect(build.ProvFilePath).To(Equal(storage.LocalPath(*provArtifact.DeepCopy()))) - g.Expect(build.ProvFilePath).To(BeARegularFile()) }, }, { name: "Generation change forces rebuild", source: *chartsArtifact.DeepCopy(), - keyringSecret: &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "keyring-secret", - }, - Data: map[string][]byte{ - publicKeyFileName: keyring, - }, - }, beforeFunc: func(obj *sourcev1.HelmChart) { obj.Generation = 2 obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" @@ -1117,13 +1068,8 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) { t.Run(tt.name, func(t *testing.T) { g := NewWithT(t) - clientBuilder := fake.NewClientBuilder() - if tt.keyringSecret != nil { - clientBuilder.WithObjects(tt.keyringSecret.DeepCopy()) - } - r := &HelmChartReconciler{ - Client: clientBuilder.Build(), + Client: fake.NewClientBuilder().Build(), EventRecorder: record.NewFakeRecorder(32), Storage: storage, Getters: testGetters, @@ -1189,7 +1135,7 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) - t.Expect(obj.GetArtifact().Checksum).To(Equal("5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3")) + t.Expect(obj.GetArtifact().Checksum).To(Equal("007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a")) t.Expect(obj.GetArtifact().Revision).To(Equal("0.1.0")) t.Expect(obj.Status.URL).ToNot(BeEmpty()) t.Expect(obj.Status.ObservedChartName).To(Equal("helmchart")) @@ -1200,24 +1146,21 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, }, { - name: "A build with a non-nil ProvFilePath persists prov file to storage", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", "testdata/charts/helmchart-0.1.0.tgz"), + name: "Build with a verified signature sets SourceVerifiedCondition=Truue", + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", "testdata/charts/helmchart-0.1.0.tgz.prov"), beforeFunc: func(obj *sourcev1.HelmChart) { - conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "") + obj.Status.Artifact = &sourcev1.Artifact{ + Path: "testdata/charts/helmchart-0.1.0.tgz", + } }, + want: sreconcile.ResultSuccess, afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { - provArtifact := testStorage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), "0.1.0", fmt.Sprintf("%s-%s.tgz.prov", "helmchart", "0.1.0")) + provArtifact := testStorage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), "0.1.0", "helmchart-0.1.0.tgz.prov") t.Expect(provArtifact.Path).ToNot(BeEmpty()) - t.Expect(obj.GetArtifact()).ToNot(BeNil()) - fmt.Printf("checksum: %s", obj.GetArtifact().Checksum) - t.Expect(obj.GetArtifact().Checksum).To(Equal("5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3")) - t.Expect(obj.GetArtifact().Revision).To(Equal("0.1.0")) - t.Expect(obj.Status.URL).ToNot(BeEmpty()) - t.Expect(obj.Status.ObservedChartName).To(Equal("helmchart")) }, - want: sreconcile.ResultSuccess, assertConditions: []metav1.Condition{ *conditions.TrueCondition(meta.ReadyCondition, sourcev1.ChartPullSucceededReason, "pulled 'helmchart' chart with version '0.1.0'"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartPullSucceededReason, "chart signed by: TestUser1,TestUser2 using key with fingeprint: 0102000000000000000000000000000000000000 and hash verified: 53gntj23r24asnf0"), }, }, { @@ -1271,7 +1214,7 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) - t.Expect(obj.GetArtifact().Checksum).To(Equal("5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3")) + t.Expect(obj.GetArtifact().Checksum).To(Equal("007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a")) t.Expect(obj.GetArtifact().Revision).To(Equal("0.1.0")) t.Expect(obj.Status.URL).ToNot(BeEmpty()) t.Expect(obj.Status.ObservedChartName).To(Equal("helmchart")) @@ -1786,6 +1729,7 @@ func TestHelmChartReconciler_reconcileSubRecs(t *testing.T) { func mockChartBuild(name, version, path, provFilePath string) *chart.Build { var copyP string var copyPP string + var verSig *chart.VerificationSignature if path != "" { f, err := os.Open(path) if err == nil { @@ -1810,12 +1754,18 @@ func mockChartBuild(name, version, path, provFilePath string) *chart.Build { copyPP = ff.Name() } } + verSig = &chart.VerificationSignature{ + FileHash: "53gntj23r24asnf0", + Identities: []string{"TestUser1", "TestUser2"}, + KeyFingerprint: [20]byte{1, 2}, + } } } return &chart.Build{ - Name: name, - Version: version, - Path: copyP, - ProvFilePath: copyPP, + Name: name, + Version: version, + Path: copyP, + ProvFilePath: copyPP, + VerificationSignature: verSig, } } diff --git a/controllers/storage.go b/controllers/storage.go index f8016f5fe..bd39b66b4 100644 --- a/controllers/storage.go +++ b/controllers/storage.go @@ -120,7 +120,6 @@ func (s *Storage) RemoveAll(artifact sourcev1.Artifact) (string, error) { func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) ([]string, error) { deletedFiles := []string{} localPath := s.LocalPath(artifact) - localProvPath := localPath + ".prov" dir := filepath.Dir(localPath) var errors []string _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { @@ -129,7 +128,7 @@ func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) ([]string, err return nil } - if path != localPath && path != localProvPath && !info.IsDir() && info.Mode()&os.ModeSymlink != os.ModeSymlink { + if path != localPath && !info.IsDir() && info.Mode()&os.ModeSymlink != os.ModeSymlink { if err := os.Remove(path); err != nil { errors = append(errors, info.Name()) } else { @@ -146,6 +145,34 @@ func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) ([]string, err return deletedFiles, nil } +func (s *Storage) RemoveConditionally(dir string, callbacks []func(path string, info os.FileInfo) bool) ([]string, error) { + deletedFiles := []string{} + var errors []string + _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + errors = append(errors, err.Error()) + return nil + } + for _, callback := range callbacks { + if callback(path, info) { + if err := os.Remove(path); err != nil { + errors = append(errors, info.Name()) + } else { + // Collect the successfully deleted file paths. + deletedFiles = append(deletedFiles, path) + } + break + } + } + return nil + }) + + if len(errors) > 0 { + return deletedFiles, fmt.Errorf("failed to remove files: %s", strings.Join(errors, " ")) + } + return deletedFiles, nil +} + // ArtifactExist returns a boolean indicating whether the v1beta1.Artifact exists in storage and is a regular file. func (s *Storage) ArtifactExist(artifact sourcev1.Artifact) bool { fi, err := os.Lstat(s.LocalPath(artifact)) diff --git a/controllers/testdata/charts/helmchart-0.1.0.tgz b/controllers/testdata/charts/helmchart-0.1.0.tgz index a43147fd87321deb4d03de1d89d5e16a7d4a6baf..186a1ddb788b83cff243f8ffa684859c1700a8b1 100644 GIT binary patch delta 3390 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 U2M+`P7XSeN|39#41^`?D02V^B2><{9 delta 3253 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 diff --git a/controllers/testdata/charts/helmchart-0.1.0.tgz.prov b/controllers/testdata/charts/helmchart-0.1.0.tgz.prov index df5962f5f..7c44d8c25 100644 --- a/controllers/testdata/charts/helmchart-0.1.0.tgz.prov +++ b/controllers/testdata/charts/helmchart-0.1.0.tgz.prov @@ -10,17 +10,17 @@ version: 0.1.0 ... files: - helmchart-0.1.0.tgz: sha256:5fabb8b212945e7187a24a5e893d06fe98c83f3c49ed0f7b6df6de633d95a8f3 + helmchart-0.1.0.tgz: sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a -----BEGIN PGP SIGNATURE----- -wsDcBAEBCgAQBQJiJkq8CRBwNbqX0yqHwQAAJ5sMAKTMqzOLvrunXBQ8TPXcqXGc -bzZ9MSrK3mBzQlhZHxabCZP25vemUwGsvNyS2BS8ow+dDzaug8luZBw1aC5slSyS -uAG9bkcyqHwsHnJ+Z8O5cpYsmOzhA4CDSsq1xICxCKYy2jZPj+I8KBk5INtSi6gP -lB1YV4ry42D2BsxK7IuPr5iUzecsVNsMbvupVLUSqsR3k3A2plGhBw10yCnYxEFg -MYlUdhHKFOSiUEmgif7toEQFfofxoutcPaAHe1zRYE4t1M2AozGx3njXchTdkHbe -WvsNc96wrFAGu852VXC6hyJH2keWhY91vaoELVbkDYnCiHouu/yXE4ox2MgN6Kr0 -u1gNeaEtk+IxYthDuBBRfslQ6O6lIfi3vObanC6Wl1pmC3YyYtnFOz6VQzW9k+Sv -FF9l6ysxoYxGWzAmIhGDIoohKwD1LtRBDjWHPivsOkYyEjmFb0MUsgFHuhVRMV4Z -aTckCGBebQS0bR3wE4GaGV1sLVoZDXph7v7YGa7Enw== -=7DS/ +wsDcBAEBCgAQBQJiKwNBCRBwNbqX0yqHwQAACj8MABCY6mVrWaJdC64PbhTTonVE +97MZZpQBT+CZIRAecfkvcTeMTBeKh/yRwsSmjwo46eKOpNFJ1eQVHqVcKWLfBn3Z +AijuXTaISl8SnQyKPF2Z8n+YrYwh9OWPUX2CpUQstx+snSLDuv5ltWIgRlzfHAUN +hwzsgjs8bpHe8wZTgnASUVbcMMYQXCcovbXB6NATDLkZLHBWWEISicOl6VYLLl2D +kZg7LDcDKPcPmKJ6WtVurkyWXhK3jdYzlaOQWjs2nLIH/CdlmAygELuWexsOZAhY +MEauKEMoVzDQF5oaNA78AzlBLGogxao5fBYtAAHGb5tQdnVRUeSci+7IR0LHsS05 +YF/UnUF69GSESfoKIBvQuzex4BRCLBwayq6CSyrpZQ2+Vg4ARPo7LFg7Wy0zvC9Z +NxGnIeh1az9hltdzPgg6ZahPZB+eMF+t9ouAz9OZ3kxYUDmoE+Z+NqRWsPi27Cxk +CSw9EfJfDsputN/wj4NAxZKfqauMtS5sgaSgtrW+zA== +=mfBq -----END PGP SIGNATURE----- \ No newline at end of file diff --git a/internal/helm/chart/builder.go b/internal/helm/chart/builder.go index 25bc19563..50fa9ffc5 100644 --- a/internal/helm/chart/builder.go +++ b/internal/helm/chart/builder.go @@ -85,7 +85,7 @@ type Builder interface { // Reference and BuildOptions, and writes it to p. // It returns the Build result, or an error. // It may return an error for unsupported Reference implementations. - Build(ctx context.Context, ref Reference, p string, opts BuildOptions, keyring []byte) (*Build, error) + Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) } // BuildOptions provides a list of options for Builder.Build. @@ -104,6 +104,10 @@ type BuildOptions struct { // Force can be set to force the build of the chart, for example // because the list of ValuesFiles has changed. Force bool + + // Keyring can be set to the data of the chart VerificationKeyring secret + // used for verifying a chart's signature using a provenance file. + Keyring []byte } // GetValuesFiles returns BuildOptions.ValuesFiles, except if it equals @@ -129,6 +133,9 @@ type Build struct { // Can be empty, in which case it should be assumed that the packaged // chart is not verified. ProvFilePath string + // VerificationSignature is populated when a chart's signature + // is susccessfully verified using it's provenance file. + VerificationSignature *VerificationSignature // ValuesFiles is the list of files used to compose the chart's // default "values.yaml". ValuesFiles []string @@ -161,7 +168,6 @@ func (b *Build) Summary() string { if len(b.ValuesFiles) > 0 { s.WriteString(fmt.Sprintf(" and merged values files %v", b.ValuesFiles)) } - return s.String() } diff --git a/internal/helm/chart/builder_local.go b/internal/helm/chart/builder_local.go index cbbe840b8..6885e1d40 100644 --- a/internal/helm/chart/builder_local.go +++ b/internal/helm/chart/builder_local.go @@ -26,6 +26,7 @@ import ( "github.com/Masterminds/semver/v3" securejoin "github.com/cyphar/filepath-securejoin" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/provenance" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/runtime/transform" @@ -63,7 +64,7 @@ func NewLocalBuilder(dm *DependencyManager) Builder { // If the LocalReference.Path refers to a chart directory, dependencies are // confirmed to be present using the DependencyManager, while attempting to // resolve any missing. -func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions, keyring []byte) (*Build, error) { +func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) { localRef, ok := ref.(LocalReference) if !ok { err := fmt.Errorf("expected local chart reference") @@ -105,37 +106,43 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, isChartDir := pathIsDir(localRef.Path) requiresPackaging := isChartDir || opts.VersionMetadata != "" || len(opts.GetValuesFiles()) != 0 - // If all the following is true, we do not need to package the chart: - // - Chart name from cached chart matches resolved name - // - Chart version from cached chart matches calculated version - // - BuildOptions.Force is False var provFilePath string - verifyProvFile := func(chart, provFile string) error { - if keyring != nil { + verifyProvFile := func(chart, provFile string) (*provenance.Verification, error) { + if opts.Keyring != nil { if _, err := os.Stat(provFile); err != nil { err = fmt.Errorf("could not load provenance file %s: %w", provFile, err) - return &BuildError{Reason: ErrProvenanceVerification, Err: err} + return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} } - err := VerifyProvenanceFile(bytes.NewReader(keyring), chart, provFile) + ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), chart, provFile) if err != nil { err = fmt.Errorf("failed to verify helm chart using provenance file: %w", err) - return &BuildError{Reason: ErrProvenanceVerification, Err: err} + return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} } + return ver, nil } - return nil + return nil, nil } + + // If all the following is true, we do not need to package the chart: + // - Chart name from cached chart matches resolved name + // - Chart version from cached chart matches calculated version + // - BuildOptions.Force is False if opts.CachedChart != "" && !opts.Force { if curMeta, err = LoadChartMetadataFromArchive(opts.CachedChart); err == nil { // If the cached metadata is corrupt, we ignore its existence // and continue the build if err = curMeta.Validate(); err == nil { if result.Name == curMeta.Name && result.Version == curMeta.Version { + // We can only verify a cached chart with provenance file if we didn't + // package the chart ourselves, and instead stored it as is. if !requiresPackaging { provFilePath = provenanceFilePath(opts.CachedChart) - if err = verifyProvFile(opts.CachedChart, provFilePath); err != nil { + if ver, err := verifyProvFile(opts.CachedChart, provFilePath); err != nil { return nil, err + } else { + result.VerificationSignature = buildVerificationSig(ver) + result.ProvFilePath = provFilePath } - result.ProvFilePath = provFilePath } result.Path = opts.CachedChart result.ValuesFiles = opts.GetValuesFiles() @@ -156,11 +163,13 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, if err = copyFileToPath(provenanceFilePath(localRef.Path), provFilePath); err != nil { return result, &BuildError{Reason: ErrChartPull, Err: err} } - if err = verifyProvFile(localRef.Path, provFilePath); err != nil { + if ver, err := verifyProvFile(localRef.Path, provFilePath); err != nil { return result, err + } else { + result.ProvFilePath = provFilePath + result.VerificationSignature = buildVerificationSig(ver) } result.Path = p - result.ProvFilePath = provFilePath return result, nil } diff --git a/internal/helm/chart/builder_local_test.go b/internal/helm/chart/builder_local_test.go index e108250c3..a3543a605 100644 --- a/internal/helm/chart/builder_local_test.go +++ b/internal/helm/chart/builder_local_test.go @@ -110,6 +110,7 @@ func TestLocalBuilder_Build(t *testing.T) { { name: "already packaged chart", reference: LocalReference{Path: "./../testdata/charts/helmchart-0.1.0.tgz"}, + buildOpts: BuildOptions{Keyring: keyring}, wantVersion: "0.1.0", wantPackaged: false, }, @@ -215,7 +216,7 @@ fullnameOverride: "full-foo-name-override"`), ) b := NewLocalBuilder(dm) - cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts, keyring) + cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts) if tt.wantErr != "" { g.Expect(err).To(HaveOccurred()) @@ -226,6 +227,10 @@ fullnameOverride: "full-foo-name-override"`), g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Packaged).To(Equal(tt.wantPackaged), "unexpected Build.Packaged value") g.Expect(cb.Path).ToNot(BeEmpty(), "empty Build.Path") + if tt.buildOpts.Keyring != nil { + g.Expect(cb.ProvFilePath).ToNot(BeEmpty(), "empty Build.ProvFilePath") + g.Expect(cb.VerificationSignature).ToNot(BeNil(), "nil Build.VerificationSignature") + } // Load the resulting chart and verify the values. resultChart, err := loader.Load(cb.Path) @@ -262,7 +267,7 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) { // Build first time. targetPath := filepath.Join(tmpDir, "chart1.tgz") buildOpts := BuildOptions{} - cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts, keyring) + cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts) g.Expect(err).ToNot(HaveOccurred()) // Set the result as the CachedChart for second build. @@ -270,17 +275,46 @@ func TestLocalBuilder_Build_CachedChart(t *testing.T) { targetPath2 := filepath.Join(tmpDir, "chart2.tgz") defer os.RemoveAll(targetPath2) - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath)) // Rebuild with build option Force. buildOpts.Force = true - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath2)) } +func TestLocalBuilder_VerifyCachedChartSig(t *testing.T) { + g := NewWithT(t) + + reference := LocalReference{Path: "./../testdata/charts/helmchart-0.1.0.tgz"} + + keyring, err := os.ReadFile("./../testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + + dm := NewDependencyManager() + b := NewLocalBuilder(dm) + + tmpDir, err := os.MkdirTemp("", "local-chart-") + g.Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmpDir) + + buildOpts := BuildOptions{} + buildOpts.Keyring = keyring + + buildOpts.CachedChart = "./../testdata/charts/helmchart-0.1.0.tgz" + targetPath2 := filepath.Join(tmpDir, "chart2.tgz") + defer os.RemoveAll(targetPath2) + + cb, err := b.Build(context.TODO(), reference, targetPath2, buildOpts) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cb.ProvFilePath).ToNot(BeEmpty(), "empty Build.ProvFilePath") + g.Expect(cb.VerificationSignature).ToNot(BeNil(), "nil Build.VerificationSignature") +} + func Test_mergeFileValues(t *testing.T) { tests := []struct { name string diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index 54e1bed8c..910ad102a 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -27,6 +27,7 @@ import ( helmchart "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/provenance" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/runtime/transform" @@ -62,7 +63,7 @@ func NewRemoteBuilder(repository *repository.ChartRepository) Builder { // After downloading the chart, it is only packaged if required due to BuildOptions // modifying the chart, otherwise the exact data as retrieved from the repository // is written to p, after validating it to be a chart. -func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions, keyring []byte) (*Build, error) { +func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) { remoteRef, ok := ref.(RemoteReference) if !ok { err := fmt.Errorf("expected remote chart reference") @@ -106,15 +107,16 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o requiresPackaging := len(opts.GetValuesFiles()) != 0 || opts.VersionMetadata != "" - verifyProvFile := func(chart, provFile string) error { - if keyring != nil { - err := VerifyProvenanceFile(bytes.NewReader(keyring), chart, provFile) + verifyProvFile := func(chart, provFile string) (*provenance.Verification, error) { + if opts.Keyring != nil { + ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), chart, provFile) if err != nil { err = fmt.Errorf("failed to verify helm chart using provenance file %s: %w", provFile, err) - return &BuildError{Reason: ErrProvenanceVerification, Err: err} + return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} } + return ver, nil } - return nil + return nil, nil } var provFilePath string @@ -129,13 +131,16 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // and continue the build if err = curMeta.Validate(); err == nil { if result.Name == curMeta.Name && result.Version == curMeta.Version { - // We can only verify with provenance file if we didn't package the chart ourselves. + // We can only verify a cached chart with provenance file if we didn't + // package the chart ourselves, and instead stored it as is. if !requiresPackaging { provFilePath = provenanceFilePath(opts.CachedChart) - if err = verifyProvFile(opts.CachedChart, provFilePath); err != nil { + if ver, err := verifyProvFile(opts.CachedChart, provFilePath); err != nil { return nil, err + } else { + result.ProvFilePath = provFilePath + result.VerificationSignature = buildVerificationSig(ver) } - result.ProvFilePath = provFilePath } result.Path = opts.CachedChart result.ValuesFiles = opts.GetValuesFiles() @@ -155,7 +160,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o err = fmt.Errorf("failed to download chart for remote reference: %w", err) return result, &BuildError{Reason: ErrChartPull, Err: err} } - if keyring != nil { + if opts.Keyring != nil { provFilePath = provenanceFilePath(p) err := b.remote.DownloadProvenanceFile(cv, provFilePath) if err != nil { @@ -166,15 +171,17 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // This is needed, since the verification will work only if the .tgz file is untampered. // But we write the packaged chart to disk under a different name, so the provenance file // will not be valid for this _new_ packaged chart. - chart, err := util.WriteBytesToFile(chartBuf, fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version), false) + chart, err := util.WriteToFile(chartBuf, fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version)) defer os.Remove(chart.Name()) if err != nil { return nil, err } - if err = verifyProvFile(chart.Name(), provFilePath); err != nil { + if ver, err := verifyProvFile(chart.Name(), provFilePath); err != nil { return nil, err + } else { + result.ProvFilePath = provFilePath + result.VerificationSignature = buildVerificationSig(ver) } - result.ProvFilePath = provFilePath } // Use literal chart copy from remote if no custom values files options are @@ -250,7 +257,7 @@ func mergeChartValues(chart *helmchart.Chart, paths []string) (map[string]interf // validatePackageAndWriteToPath atomically writes the packaged chart from reader // to out while validating it by loading the chart metadata from the archive. func validatePackageAndWriteToPath(b []byte, out string) error { - tmpFile, err := util.WriteBytesToFile(b, out, true) + tmpFile, err := util.WriteToTempFile(b, out) defer os.Remove(tmpFile.Name()) if err != nil { diff --git a/internal/helm/chart/builder_remote_test.go b/internal/helm/chart/builder_remote_test.go index b9795b352..9afd521b9 100644 --- a/internal/helm/chart/builder_remote_test.go +++ b/internal/helm/chart/builder_remote_test.go @@ -138,16 +138,22 @@ entries: wantErr: "Invalid Metadata string", }, { - name: "with version metadata", - reference: RemoteReference{Name: "grafana"}, - repository: mockRepo(), - buildOpts: BuildOptions{VersionMetadata: "foo"}, + name: "with version metadata", + reference: RemoteReference{Name: "grafana"}, + repository: mockRepo(), + buildOpts: BuildOptions{ + VersionMetadata: "foo", + Keyring: keyring, + }, wantVersion: "0.1.0+foo", wantPackaged: true, }, { - name: "default values", - reference: RemoteReference{Name: "grafana"}, + name: "default values", + reference: RemoteReference{Name: "grafana"}, + buildOpts: BuildOptions{ + Keyring: keyring, + }, repository: mockRepo(), wantVersion: "0.1.0", wantValues: chartutil.Values{ @@ -159,6 +165,7 @@ entries: reference: RemoteReference{Name: "grafana"}, buildOpts: BuildOptions{ ValuesFiles: []string{"a.yaml", "b.yaml", "c.yaml"}, + Keyring: keyring, }, repository: mockRepo(), wantVersion: "0.1.0", @@ -187,10 +194,7 @@ entries: b := NewRemoteBuilder(tt.repository) - if tt.buildOpts.VersionMetadata != "" { - keyring = nil - } - cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts, keyring) + cb, err := b.Build(context.TODO(), tt.reference, targetPath, tt.buildOpts) if tt.wantErr != "" { g.Expect(err).To(HaveOccurred()) @@ -201,6 +205,8 @@ entries: g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Packaged).To(Equal(tt.wantPackaged), "unexpected Build.Packaged value") g.Expect(cb.Path).ToNot(BeEmpty(), "empty Build.Path") + g.Expect(cb.ProvFilePath).ToNot(BeEmpty(), "empty Build.ProvFilePath") + g.Expect(cb.VerificationSignature).ToNot(BeNil(), "nil Build.VerificationSignature") // Load the resulting chart and verify the values. resultChart, err := loader.Load(cb.Path) @@ -273,8 +279,11 @@ entries: targetPath := filepath.Join(tmpDir, "helmchart-0.1.0.tgz") defer os.RemoveAll(targetPath) buildOpts := BuildOptions{} - cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts, keyring) + buildOpts.Keyring = keyring + cb, err := b.Build(context.TODO(), reference, targetPath, buildOpts) g.Expect(err).ToNot(HaveOccurred()) + g.Expect(cb.ProvFilePath).ToNot(BeEmpty(), "empty Build.ProvFilePath") + g.Expect(cb.VerificationSignature).ToNot(BeNil(), "nil Build.VerificationSignature") // Set the result as the CachedChart for second build. buildOpts.CachedChart = cb.Path @@ -282,15 +291,19 @@ entries: // Rebuild with a new path. targetPath2 := filepath.Join(tmpDir, "chart2.tgz") defer os.RemoveAll(targetPath2) - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath)) + g.Expect(cb.ProvFilePath).ToNot(BeEmpty(), "empty Build.ProvFilePath") + g.Expect(cb.VerificationSignature).ToNot(BeNil(), "nil Build.VerificationSignature") // Rebuild with build option Force. buildOpts.Force = true - cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts, keyring) + cb, err = b.Build(context.TODO(), reference, targetPath2, buildOpts) g.Expect(err).ToNot(HaveOccurred()) g.Expect(cb.Path).To(Equal(targetPath2)) + g.Expect(cb.ProvFilePath).ToNot(BeEmpty(), "empty Build.ProvFilePath") + g.Expect(cb.VerificationSignature).ToNot(BeNil(), "nil Build.VerificationSignature") } func Test_mergeChartValues(t *testing.T) { diff --git a/internal/helm/chart/verify.go b/internal/helm/chart/verify.go index 26e91c31e..9f0870b24 100644 --- a/internal/helm/chart/verify.go +++ b/internal/helm/chart/verify.go @@ -1,3 +1,19 @@ +/* +Copyright 2021 The Flux 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 chart import ( @@ -14,14 +30,14 @@ import ( // Ref: https://github.com/helm/helm/blob/v3.8.0/pkg/downloader/chart_downloader.go#L328 // modified to accept a custom provenance file path and an actual keyring instead of a // path to the file containing the keyring. -func VerifyProvenanceFile(keyring io.Reader, chartPath, provFilePath string) error { +func verifyChartWithProvFile(keyring io.Reader, chartPath, provFilePath string) (*provenance.Verification, error) { switch fi, err := os.Stat(chartPath); { case err != nil: - return err + return nil, err case fi.IsDir(): - return fmt.Errorf("unpacked charts cannot be verified") + return nil, fmt.Errorf("unpacked charts cannot be verified") case !isTar(chartPath): - return fmt.Errorf("chart must be a tgz file") + return nil, fmt.Errorf("chart must be a tgz file") } if provFilePath == "" { @@ -29,20 +45,17 @@ func VerifyProvenanceFile(keyring io.Reader, chartPath, provFilePath string) err } if _, err := os.Stat(provFilePath); err != nil { - return fmt.Errorf("could not load provenance file %s: %w", provFilePath, err) + return nil, fmt.Errorf("could not load provenance file %s: %w", provFilePath, err) } ring, err := openpgp.ReadKeyRing(keyring) if err != nil { - return err + return nil, err } sig := &provenance.Signatory{KeyRing: ring} - _, err = sig.Verify(chartPath, provFilePath) - if err != nil { - return err - } - return nil + verification, err := sig.Verify(chartPath, provFilePath) + return verification, err } // isTar tests whether the given file is a tar file. @@ -55,3 +68,24 @@ func isTar(filename string) bool { func provenanceFilePath(path string) string { return path + ".prov" } + +// ref: https://github.com/helm/helm/blob/v3.8.0/pkg/action/verify.go#L47-L51 +type VerificationSignature struct { + Identities []string + KeyFingerprint [20]byte + FileHash string +} + +func buildVerificationSig(ver *provenance.Verification) *VerificationSignature { + var verSig VerificationSignature + if ver != nil { + if ver.SignedBy != nil { + for name := range ver.SignedBy.Identities { + verSig.Identities = append(verSig.Identities, name) + } + } + verSig.FileHash = ver.FileHash + verSig.KeyFingerprint = ver.SignedBy.PrimaryKey.Fingerprint + } + return &verSig +} diff --git a/internal/helm/repository/chart_repository.go b/internal/helm/repository/chart_repository.go index 3bc1fa63d..3bd76a726 100644 --- a/internal/helm/repository/chart_repository.go +++ b/internal/helm/repository/chart_repository.go @@ -216,7 +216,7 @@ func (r *ChartRepository) DownloadProvenanceFile(chart *repo.ChartVersion, path if err != nil { return err } - tmpFile, err := util.WriteBytesToFile(res.Bytes(), path, true) + tmpFile, err := util.WriteToTempFile(res.Bytes(), path) defer os.Remove(tmpFile.Name()) if err != nil { diff --git a/internal/util/file.go b/internal/util/file.go new file mode 100644 index 000000000..dc023d318 --- /dev/null +++ b/internal/util/file.go @@ -0,0 +1,43 @@ +package util + +import ( + "fmt" + "os" + "path/filepath" +) + +func writeBytesToFile(bytes []byte, out string, temp bool) (*os.File, error) { + var file *os.File + var err error + + if temp { + file, err = os.CreateTemp("", out) + if err != nil { + return nil, fmt.Errorf("failed to create temporary file %s: %w", filepath.Base(out), err) + } + } else { + file, err = os.Create(out) + if err != nil { + return nil, fmt.Errorf("failed to create temporary file for chart %s: %w", out, err) + } + } + if _, err := file.Write(bytes); err != nil { + _ = file.Close() + return nil, fmt.Errorf("failed to write to file %s: %w", file.Name(), err) + } + if err := file.Close(); err != nil { + return nil, err + } + return file, nil +} + +// Writes the provided bytes to a file at the given path and returns the file handle. +func WriteToFile(bytes []byte, path string) (*os.File, error) { + return writeBytesToFile(bytes, path, false) +} + +// Writes the provided bytes to a temp file with the name provided in the path and +// returns the file handle. +func WriteToTempFile(bytes []byte, out string) (*os.File, error) { + return writeBytesToFile(bytes, filepath.Base(out), true) +} diff --git a/internal/util/temp.go b/internal/util/temp.go index ef07c7bd3..054b12801 100644 --- a/internal/util/temp.go +++ b/internal/util/temp.go @@ -50,29 +50,3 @@ func pattern(obj client.Object) (p string) { kind := strings.ToLower(obj.GetObjectKind().GroupVersionKind().Kind) return fmt.Sprintf("%s-%s-%s-", kind, obj.GetNamespace(), obj.GetName()) } - -// TODO: think of a better name? -func WriteBytesToFile(bytes []byte, out string, temp bool) (*os.File, error) { - var file *os.File - var err error - - if temp { - file, err = os.CreateTemp("", filepath.Base(out)) - if err != nil { - return nil, fmt.Errorf("failed to create temporary file %s: %w", filepath.Base(out), err) - } - } else { - file, err = os.Create(out) - if err != nil { - return nil, fmt.Errorf("failed to create temporary file for chart %s: %w", out, err) - } - } - if _, err := file.Write(bytes); err != nil { - _ = file.Close() - return nil, fmt.Errorf("failed to write to file %s: %w", file.Name(), err) - } - if err := file.Close(); err != nil { - return nil, err - } - return file, nil -} From f39ee529baf5ea13938500323c2daead31c869b9 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Wed, 16 Mar 2022 15:21:42 +0530 Subject: [PATCH 06/10] make doc changes and fix reconilation process Signed-off-by: Sanskar Jaiswal --- api/v1beta2/helmchart_types.go | 8 +- controllers/helmchart_controller.go | 49 +++------ controllers/helmchart_controller_test.go | 131 +++++++++++++---------- internal/helm/chart/builder.go | 6 +- internal/helm/chart/builder_local.go | 14 ++- internal/helm/chart/builder_remote.go | 18 ++-- internal/helm/chart/verify_test.go | 18 ++++ internal/util/file.go | 16 +++ 8 files changed, 155 insertions(+), 105 deletions(-) create mode 100644 internal/helm/chart/verify_test.go diff --git a/api/v1beta2/helmchart_types.go b/api/v1beta2/helmchart_types.go index 660356535..5435d42ff 100644 --- a/api/v1beta2/helmchart_types.go +++ b/api/v1beta2/helmchart_types.go @@ -85,7 +85,7 @@ type HelmChartSpec struct { // +optional AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"` - // Keyring information for verifying the packaged chart's signature using a provenance file. + // VerificationKeyring for verifying the packaged chart's signature using a provenance file. // +optional VerificationKeyring *VerificationKeyring `json:"verificationKeyring,omitempty"` } @@ -94,7 +94,7 @@ type VerificationKeyring struct { // +required SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` - // The key that corresponds to the keyring value. + // Key in the SecretRef that contains the public keyring in legacy GPG format. // +kubebuilder:default:=pubring.gpg // +optional Key string `json:"key,omitempty"` @@ -168,6 +168,10 @@ const ( // ChartPackageSucceededReason signals that the package of the Helm // chart succeeded. ChartPackageSucceededReason string = "ChartPackageSucceeded" + + // ChartVerifiedSucceededReason signals that the Helm chart's signature + // has been verified using it's provenance file. + ChartVerifiedSucceededReason string = "ChartVerifiedSucceeded" ) // GetConditions returns the status conditions of the object. diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index ddd201d3c..aeb23f6e0 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -97,8 +97,6 @@ var helmChartReadyCondition = summarize.Conditions{ }, } -const KeyringFileName = "pubring.gpg" - // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/status,verbs=get;update;patch // +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=helmcharts/finalizers,verbs=get;create;update;patch;delete @@ -475,9 +473,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * if err != nil { e := &serror.Event{ Err: fmt.Errorf("failed to get public key for chart signature verification: %w", err), - Reason: sourcev1.SourceVerifiedCondition, + Reason: sourcev1.AuthenticationFailedReason, } - conditions.MarkFalse(obj, sourcev1.FetchFailedCondition, sourcev1.SourceVerifiedCondition, e.Error()) + conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Error()) return sreconcile.ResultEmpty, e } opts.Keyring = keyring @@ -609,9 +607,9 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj if err != nil { e := &serror.Event{ Err: fmt.Errorf("failed to get public key for chart signature verification: %w", err), - Reason: sourcev1.SourceVerifiedCondition, + Reason: sourcev1.AuthenticationFailedReason, } - conditions.MarkFalse(obj, sourcev1.FetchFailedCondition, sourcev1.SourceVerifiedCondition, e.Error()) + conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Error()) return sreconcile.ResultEmpty, e } opts.Keyring = keyring @@ -651,14 +649,6 @@ func (r *HelmChartReconciler) reconcileArtifact(ctx context.Context, obj *source conditions.Delete(obj, sourcev1.ArtifactOutdatedCondition) conditions.MarkTrue(obj, meta.ReadyCondition, reasonForBuild(b), b.Summary()) } - if b.VerificationSignature != nil && b.ProvFilePath != "" && obj.GetArtifact() != nil { - var sigVerMsg strings.Builder - sigVerMsg.WriteString(fmt.Sprintf("chart signed by: %v", strings.Join(b.VerificationSignature.Identities[:], ","))) - sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: %X", b.VerificationSignature.KeyFingerprint)) - sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: %s", b.VerificationSignature.FileHash)) - - conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, reasonForBuild(b), sigVerMsg.String()) - } }() // Create artifact from build data @@ -914,22 +904,6 @@ func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repos return &secret, nil } -func (r *HelmChartReconciler) getVerificationKeyringSecret(ctx context.Context, chart *sourcev1.HelmChart) (*corev1.Secret, error) { - if chart.Spec.VerificationKeyring == nil { - return nil, nil - } - name := types.NamespacedName{ - Namespace: chart.GetNamespace(), - Name: chart.Spec.VerificationKeyring.SecretRef.Name, - } - var secret corev1.Secret - err := r.Client.Get(ctx, name, &secret) - if err != nil { - return nil, err - } - return &secret, nil -} - func (r *HelmChartReconciler) indexHelmRepositoryByURL(o client.Object) []string { repo, ok := o.(*sourcev1.HelmRepository) if !ok { @@ -1060,6 +1034,15 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) { conditions.Delete(obj, sourcev1.BuildFailedCondition) } + if build.VerificationSignature != nil && build.ProvFilePath != "" { + var sigVerMsg strings.Builder + sigVerMsg.WriteString(fmt.Sprintf("chart signed by: %v", strings.Join(build.VerificationSignature.Identities[:], ","))) + sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: %X", build.VerificationSignature.KeyFingerprint)) + sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: %s", build.VerificationSignature.FileHash)) + + conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, sigVerMsg.String()) + } + if err != nil { var buildErr *chart.BuildError if ok := errors.As(err, &buildErr); !ok { @@ -1105,10 +1088,10 @@ func (r *HelmChartReconciler) getProvenanceKeyring(ctx context.Context, chart *s return nil, err } key := chart.Spec.VerificationKeyring.Key - if val, ok := secret.Data[key]; !ok { + val, ok := secret.Data[key] + if !ok { err = fmt.Errorf("secret doesn't contain the advertised verification keyring name %s", key) return nil, err - } else { - return val, nil } + return val, nil } diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 37b08630f..55e0cdfde 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -52,7 +52,7 @@ import ( sreconcile "github.com/fluxcd/source-controller/internal/reconcile" ) -const publicKeyFileName = "pub.pgp" +const publicKeyFileName = "pub.gpg" func TestHelmChartReconciler_Reconcile(t *testing.T) { g := NewWithT(t) @@ -458,14 +458,19 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { } g.Expect(storage.Archive(gitArtifact, "testdata/charts", nil)).To(Succeed()) + keyring, err := os.ReadFile("testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(keyring).ToNot(BeEmpty()) + tests := []struct { - name string - source sourcev1.Source - beforeFunc func(obj *sourcev1.HelmChart) - want sreconcile.Result - wantErr error - assertFunc func(g *WithT, build chart.Build, obj sourcev1.HelmChart) - cleanFunc func(g *WithT, build *chart.Build) + name string + source sourcev1.Source + keyringSecret *corev1.Secret + beforeFunc func(obj *sourcev1.HelmChart) + want sreconcile.Result + wantErr error + assertFunc func(g *WithT, build chart.Build, obj sourcev1.HelmChart) + cleanFunc func(g *WithT, build *chart.Build) }{ { name: "Observes Artifact revision and build result", @@ -501,6 +506,59 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { g.Expect(os.Remove(build.Path)).To(Succeed()) }, }, + { + name: "Observes Artifact revision and build result with valid signature", + source: &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "gitrepository", + Namespace: "default", + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: gitArtifact, + }, + }, + keyringSecret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "keyring-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + publicKeyFileName: keyring, + }, + }, + beforeFunc: func(obj *sourcev1.HelmChart) { + obj.Spec.Chart = "testdata/charts/helmchart-0.1.0.tgz" + obj.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ + Name: "gitrepository", + Kind: sourcev1.GitRepositoryKind, + } + obj.Spec.VerificationKeyring = &sourcev1.VerificationKeyring{ + SecretRef: meta.LocalObjectReference{ + Name: "keyring-secret", + }, + Key: publicKeyFileName, + } + }, + want: sreconcile.ResultSuccess, + assertFunc: func(g *WithT, build chart.Build, obj sourcev1.HelmChart) { + g.Expect(build.Complete()).To(BeTrue()) + g.Expect(build.Name).To(Equal("helmchart")) + g.Expect(build.Version).To(Equal("0.1.0")) + g.Expect(build.Path).To(BeARegularFile()) + g.Expect(build.VerificationSignature).ToNot(BeNil()) + g.Expect(build.ProvFilePath).To(BeARegularFile()) + + g.Expect(obj.Status.ObservedSourceArtifactRevision).To(Equal(gitArtifact.Revision)) + g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ + *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled 'helmchart' chart with version '0.1.0'"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, "chart signed by: TestUser using key with fingeprint: 943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1 and hash verified: sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a"), + })) + }, + cleanFunc: func(g *WithT, build *chart.Build) { + g.Expect(os.Remove(build.Path)).To(Succeed()) + g.Expect(os.Remove(build.ProvFilePath)).To(Succeed()) + }, + }, { name: "Error on unavailable source", beforeFunc: func(obj *sourcev1.HelmChart) { @@ -605,6 +663,9 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { if tt.source != nil { clientBuilder.WithRuntimeObjects(tt.source) } + if tt.keyringSecret != nil { + clientBuilder.WithRuntimeObjects(tt.keyringSecret) + } r := &HelmChartReconciler{ Client: clientBuilder.Build(), @@ -1129,7 +1190,7 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, { name: "Copying artifact to storage from build makes Ready=True", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", ""), + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz"), beforeFunc: func(obj *sourcev1.HelmChart) { conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "") }, @@ -1145,24 +1206,6 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { *conditions.TrueCondition(meta.ReadyCondition, sourcev1.ChartPullSucceededReason, "pulled 'helmchart' chart with version '0.1.0'"), }, }, - { - name: "Build with a verified signature sets SourceVerifiedCondition=Truue", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", "testdata/charts/helmchart-0.1.0.tgz.prov"), - beforeFunc: func(obj *sourcev1.HelmChart) { - obj.Status.Artifact = &sourcev1.Artifact{ - Path: "testdata/charts/helmchart-0.1.0.tgz", - } - }, - want: sreconcile.ResultSuccess, - afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { - provArtifact := testStorage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), "0.1.0", "helmchart-0.1.0.tgz.prov") - t.Expect(provArtifact.Path).ToNot(BeEmpty()) - }, - assertConditions: []metav1.Condition{ - *conditions.TrueCondition(meta.ReadyCondition, sourcev1.ChartPullSucceededReason, "pulled 'helmchart' chart with version '0.1.0'"), - *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartPullSucceededReason, "chart signed by: TestUser1,TestUser2 using key with fingeprint: 0102000000000000000000000000000000000000 and hash verified: 53gntj23r24asnf0"), - }, - }, { name: "Up-to-date chart build does not persist artifact to storage", build: &chart.Build{ @@ -1208,7 +1251,7 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, { name: "Removes ArtifactOutdatedCondition after creating new artifact", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", ""), + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz"), beforeFunc: func(obj *sourcev1.HelmChart) { conditions.MarkTrue(obj, sourcev1.ArtifactOutdatedCondition, "Foo", "") }, @@ -1226,7 +1269,7 @@ func TestHelmChartReconciler_reconcileArtifact(t *testing.T) { }, { name: "Creates latest symlink to the created artifact", - build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz", ""), + build: mockChartBuild("helmchart", "0.1.0", "testdata/charts/helmchart-0.1.0.tgz"), afterFunc: func(t *WithT, obj *sourcev1.HelmChart) { t.Expect(obj.GetArtifact()).ToNot(BeNil()) @@ -1726,10 +1769,8 @@ func TestHelmChartReconciler_reconcileSubRecs(t *testing.T) { } } -func mockChartBuild(name, version, path, provFilePath string) *chart.Build { +func mockChartBuild(name, version, path string) *chart.Build { var copyP string - var copyPP string - var verSig *chart.VerificationSignature if path != "" { f, err := os.Open(path) if err == nil { @@ -1743,29 +1784,9 @@ func mockChartBuild(name, version, path, provFilePath string) *chart.Build { } } } - if provFilePath != "" { - f, err := os.Open(provFilePath) - if err == nil { - defer f.Close() - ff, err := os.CreateTemp("", "chart-mock-*.tgz.prov") - if err == nil { - defer ff.Close() - if _, err = io.Copy(ff, f); err == nil { - copyPP = ff.Name() - } - } - verSig = &chart.VerificationSignature{ - FileHash: "53gntj23r24asnf0", - Identities: []string{"TestUser1", "TestUser2"}, - KeyFingerprint: [20]byte{1, 2}, - } - } - } return &chart.Build{ - Name: name, - Version: version, - Path: copyP, - ProvFilePath: copyPP, - VerificationSignature: verSig, + Name: name, + Version: version, + Path: copyP, } } diff --git a/internal/helm/chart/builder.go b/internal/helm/chart/builder.go index 50fa9ffc5..6250acad3 100644 --- a/internal/helm/chart/builder.go +++ b/internal/helm/chart/builder.go @@ -105,8 +105,8 @@ type BuildOptions struct { // because the list of ValuesFiles has changed. Force bool - // Keyring can be set to the data of the chart VerificationKeyring secret - // used for verifying a chart's signature using a provenance file. + // Keyring can be configured with the bytes of a public kering in legacy + // PGP format used for verifying a chart's signature using a provenance file. Keyring []byte } @@ -134,7 +134,7 @@ type Build struct { // chart is not verified. ProvFilePath string // VerificationSignature is populated when a chart's signature - // is susccessfully verified using it's provenance file. + // is successfully verified using it's provenance file. VerificationSignature *VerificationSignature // ValuesFiles is the list of files used to compose the chart's // default "values.yaml". diff --git a/internal/helm/chart/builder_local.go b/internal/helm/chart/builder_local.go index 6885e1d40..7e5bdf31f 100644 --- a/internal/helm/chart/builder_local.go +++ b/internal/helm/chart/builder_local.go @@ -137,9 +137,11 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, // package the chart ourselves, and instead stored it as is. if !requiresPackaging { provFilePath = provenanceFilePath(opts.CachedChart) - if ver, err := verifyProvFile(opts.CachedChart, provFilePath); err != nil { + ver, err := verifyProvFile(opts.CachedChart, provFilePath) + if err != nil { return nil, err - } else { + } + if ver != nil { result.VerificationSignature = buildVerificationSig(ver) result.ProvFilePath = provFilePath } @@ -163,9 +165,11 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, if err = copyFileToPath(provenanceFilePath(localRef.Path), provFilePath); err != nil { return result, &BuildError{Reason: ErrChartPull, Err: err} } - if ver, err := verifyProvFile(localRef.Path, provFilePath); err != nil { - return result, err - } else { + ver, err := verifyProvFile(localRef.Path, provFilePath) + if err != nil { + return nil, err + } + if ver != nil { result.ProvFilePath = provFilePath result.VerificationSignature = buildVerificationSig(ver) } diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index 910ad102a..90c522812 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -135,9 +135,11 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // package the chart ourselves, and instead stored it as is. if !requiresPackaging { provFilePath = provenanceFilePath(opts.CachedChart) - if ver, err := verifyProvFile(opts.CachedChart, provFilePath); err != nil { + ver, err := verifyProvFile(opts.CachedChart, provFilePath) + if err != nil { return nil, err - } else { + } + if ver != nil { result.ProvFilePath = provFilePath result.VerificationSignature = buildVerificationSig(ver) } @@ -153,13 +155,13 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // Download the package for the resolved version res, err := b.remote.DownloadChart(cv) - // Deal with the underlying byte slice to avoid having to read the buffer multiple times. - chartBuf := res.Bytes() - if err != nil { err = fmt.Errorf("failed to download chart for remote reference: %w", err) return result, &BuildError{Reason: ErrChartPull, Err: err} } + // Deal with the underlying byte slice to avoid having to read the buffer multiple times. + chartBuf := res.Bytes() + if opts.Keyring != nil { provFilePath = provenanceFilePath(p) err := b.remote.DownloadProvenanceFile(cv, provFilePath) @@ -176,9 +178,11 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o if err != nil { return nil, err } - if ver, err := verifyProvFile(chart.Name(), provFilePath); err != nil { + ver, err := verifyProvFile(chart.Name(), provFilePath) + if err != nil { return nil, err - } else { + } + if ver != nil { result.ProvFilePath = provFilePath result.VerificationSignature = buildVerificationSig(ver) } diff --git a/internal/helm/chart/verify_test.go b/internal/helm/chart/verify_test.go new file mode 100644 index 000000000..86309714f --- /dev/null +++ b/internal/helm/chart/verify_test.go @@ -0,0 +1,18 @@ +package chart + +import ( + "os" + "testing" + + . "github.com/onsi/gomega" +) + +func Test_verifyChartWithProvFile(t *testing.T) { + g := NewWithT(t) + + keyring, err := os.Open("../testdata/charts/pub.gpg") + g.Expect(err).ToNot(HaveOccurred()) + ver, err := verifyChartWithProvFile(keyring, "../testdata/charts/helmchart-0.1.0.tgz", "../testdata/charts/helmchart-0.1.0.tgz.prov") + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(ver).ToNot(BeNil()) +} diff --git a/internal/util/file.go b/internal/util/file.go index dc023d318..fed8a73f2 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Flux 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 util import ( From 51a5e77af6cbbb60de67c49a61a72c7b451ee741 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Thu, 17 Mar 2022 14:51:07 +0530 Subject: [PATCH 07/10] add more docs and fix sig verification error reasons Signed-off-by: Sanskar Jaiswal --- api/v1beta2/condition_types.go | 4 + api/v1beta2/helmchart_types.go | 3 + api/v1beta2/zz_generated.deepcopy.go | 21 ++++++ .../source.toolkit.fluxcd.io_helmcharts.yaml | 9 ++- controllers/gitrepository_controller.go | 2 +- controllers/gitrepository_controller_test.go | 2 +- controllers/helmchart_controller.go | 29 +++---- controllers/helmchart_controller_test.go | 2 +- controllers/storage.go | 8 +- docs/api/source.md | 75 +++++++++++++++++++ internal/helm/chart/builder.go | 6 +- internal/util/file.go | 34 ++++----- 12 files changed, 150 insertions(+), 45 deletions(-) diff --git a/api/v1beta2/condition_types.go b/api/v1beta2/condition_types.go index 647b8aa7f..3b131bfcd 100644 --- a/api/v1beta2/condition_types.go +++ b/api/v1beta2/condition_types.go @@ -85,4 +85,8 @@ const ( // SymlinkUpdateFailedReason signals a failure in updating a symlink. SymlinkUpdateFailedReason string = "SymlinkUpdateFailed" + + // VerificationFailedReason signals a failure in verifying the signature of + // an artifact instance, such as a git commit or a helm chart. + VerificationFailedReason string = "VerificationFailed" ) diff --git a/api/v1beta2/helmchart_types.go b/api/v1beta2/helmchart_types.go index 5435d42ff..afaa5c1b4 100644 --- a/api/v1beta2/helmchart_types.go +++ b/api/v1beta2/helmchart_types.go @@ -90,7 +90,10 @@ type HelmChartSpec struct { VerificationKeyring *VerificationKeyring `json:"verificationKeyring,omitempty"` } +// VerificationKeyring contains enough info to get the public GPG key to be used for verifying +// the chart signature using a provenance file. type VerificationKeyring struct { + // SecretRef is a reference to the secret that contains the public GPG key. // +required SecretRef meta.LocalObjectReference `json:"secretRef,omitempty"` diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go index b789d81da..6cfbc3e35 100644 --- a/api/v1beta2/zz_generated.deepcopy.go +++ b/api/v1beta2/zz_generated.deepcopy.go @@ -442,6 +442,11 @@ func (in *HelmChartSpec) DeepCopyInto(out *HelmChartSpec) { *out = new(acl.AccessFrom) (*in).DeepCopyInto(*out) } + if in.VerificationKeyring != nil { + in, out := &in.VerificationKeyring, &out.VerificationKeyring + *out = new(VerificationKeyring) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartSpec. @@ -614,3 +619,19 @@ func (in *LocalHelmChartSourceReference) DeepCopy() *LocalHelmChartSourceReferen in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VerificationKeyring) DeepCopyInto(out *VerificationKeyring) { + *out = *in + out.SecretRef = in.SecretRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VerificationKeyring. +func (in *VerificationKeyring) DeepCopy() *VerificationKeyring { + if in == nil { + return nil + } + out := new(VerificationKeyring) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml index 1671ac596..2026e7efa 100644 --- a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml +++ b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml @@ -405,16 +405,17 @@ spec: type: string type: array verificationKeyring: - description: Keyring information for verifying the packaged chart's + description: VerificationKeyring for verifying the packaged chart's signature using a provenance file. properties: key: default: pubring.gpg - description: The key that corresponds to the keyring value. + description: Key in the SecretRef that contains the public keyring + in legacy GPG format. type: string secretRef: - description: LocalObjectReference contains enough information - to locate the referenced Kubernetes resource object. + description: SecretRef is a reference to the secret that contains + the public GPG key. properties: name: description: Name of the referent. diff --git a/controllers/gitrepository_controller.go b/controllers/gitrepository_controller.go index 9c9189ca8..d7df0a90a 100644 --- a/controllers/gitrepository_controller.go +++ b/controllers/gitrepository_controller.go @@ -638,7 +638,7 @@ func (r *GitRepositoryReconciler) verifyCommitSignature(ctx context.Context, obj if err := r.Client.Get(ctx, publicKeySecret, secret); err != nil { e := &serror.Event{ Err: fmt.Errorf("PGP public keys secret error: %w", err), - Reason: "VerificationError", + Reason: sourcev1.VerificationFailedReason, } conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e diff --git a/controllers/gitrepository_controller_test.go b/controllers/gitrepository_controller_test.go index 7b6aeba35..e7daac2b7 100644 --- a/controllers/gitrepository_controller_test.go +++ b/controllers/gitrepository_controller_test.go @@ -1209,7 +1209,7 @@ func TestGitRepositoryReconciler_verifyCommitSignature(t *testing.T) { }, wantErr: true, assertConditions: []metav1.Condition{ - *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, "VerificationError", "PGP public keys secret error: secrets \"none-existing\" not found"), + *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationFailedReason, "PGP public keys secret error: secrets \"none-existing\" not found"), }, }, { diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index aeb23f6e0..2b12210c1 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -473,9 +473,9 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * if err != nil { e := &serror.Event{ Err: fmt.Errorf("failed to get public key for chart signature verification: %w", err), - Reason: sourcev1.AuthenticationFailedReason, + Reason: sourcev1.VerificationFailedReason, } - conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Error()) + conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e } opts.Keyring = keyring @@ -483,7 +483,6 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj * // Build the chart ref := chart.RemoteReference{Name: obj.Spec.Chart, Version: obj.Spec.Version} build, err := cb.Build(ctx, ref, util.TempPathForObj("", ".tgz", obj), opts) - if err != nil { return sreconcile.ResultEmpty, err } @@ -607,9 +606,9 @@ func (r *HelmChartReconciler) buildFromTarballArtifact(ctx context.Context, obj if err != nil { e := &serror.Event{ Err: fmt.Errorf("failed to get public key for chart signature verification: %w", err), - Reason: sourcev1.AuthenticationFailedReason, + Reason: sourcev1.VerificationFailedReason, } - conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Error()) + conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e } opts.Keyring = keyring @@ -698,10 +697,11 @@ func (r *HelmChartReconciler) reconcileArtifact(ctx context.Context, obj *source if b.ProvFilePath != "" { provArtifact := r.Storage.NewArtifactFor(obj.Kind, obj.GetObjectMeta(), b.Version, fmt.Sprintf("%s-%s.tgz.prov", b.Name, b.Version)) if err = r.Storage.CopyFromPath(&provArtifact, b.ProvFilePath); err != nil { - return sreconcile.ResultEmpty, &serror.Event{ + e := &serror.Event{ Err: fmt.Errorf("unable to copy Helm chart provenance file to storage: %w", err), - Reason: sourcev1.StorageOperationFailedCondition, + Reason: sourcev1.ArchiveOperationFailedReason, } + conditions.MarkTrue(obj, sourcev1.StorageOperationFailedCondition, e.Reason, e.Err.Error()) } } @@ -803,14 +803,13 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *sourcev1. localPath := r.Storage.LocalPath(*obj.GetArtifact()) provFilePath := localPath + ".prov" dir := filepath.Dir(localPath) - callbacks := make([]func(path string, info os.FileInfo) bool, 0) - callbacks = append(callbacks, func(path string, info os.FileInfo) bool { + callback := func(path string, info os.FileInfo) bool { if path != localPath && path != provFilePath && info.Mode()&os.ModeSymlink != os.ModeSymlink { return true } return false - }) - if _, err := r.Storage.RemoveConditionally(dir, callbacks); err != nil { + } + if _, err := r.Storage.RemoveConditionally(dir, callback); err != nil { return &serror.Event{ Err: fmt.Errorf("garbage collection of old artifacts failed: %w", err), Reason: "GarbageCollectionFailed", @@ -1036,11 +1035,13 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) { if build.VerificationSignature != nil && build.ProvFilePath != "" { var sigVerMsg strings.Builder - sigVerMsg.WriteString(fmt.Sprintf("chart signed by: %v", strings.Join(build.VerificationSignature.Identities[:], ","))) - sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: %X", build.VerificationSignature.KeyFingerprint)) - sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: %s", build.VerificationSignature.FileHash)) + sigVerMsg.WriteString(fmt.Sprintf("chart signed by: '%v'", strings.Join(build.VerificationSignature.Identities[:], ","))) + sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: '%X'", build.VerificationSignature.KeyFingerprint)) + sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: '%s'", build.VerificationSignature.FileHash)) conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, sigVerMsg.String()) + } else { + conditions.Delete(obj, sourcev1.SourceVerifiedCondition) } if err != nil { diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 55e0cdfde..51a701431 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -551,7 +551,7 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { g.Expect(obj.Status.ObservedSourceArtifactRevision).To(Equal(gitArtifact.Revision)) g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled 'helmchart' chart with version '0.1.0'"), - *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, "chart signed by: TestUser using key with fingeprint: 943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1 and hash verified: sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, "chart signed by: 'TestUser' using key with fingeprint: '943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1' and hash verified: 'sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a'"), })) }, cleanFunc: func(g *WithT, build *chart.Build) { diff --git a/controllers/storage.go b/controllers/storage.go index bd39b66b4..54bf06409 100644 --- a/controllers/storage.go +++ b/controllers/storage.go @@ -53,6 +53,10 @@ type Storage struct { Timeout time.Duration `json:"timeout"` } +// removeFileCallback is a function which determines whether the +// provided file should be removed from the filesystem. +type removeFileCallback func(path string, info os.FileInfo) bool + // NewStorage creates the storage helper for a given path and hostname. func NewStorage(basePath string, hostname string, timeout time.Duration) (*Storage, error) { if f, err := os.Stat(basePath); os.IsNotExist(err) || !f.IsDir() { @@ -145,7 +149,9 @@ func (s *Storage) RemoveAllButCurrent(artifact sourcev1.Artifact) ([]string, err return deletedFiles, nil } -func (s *Storage) RemoveConditionally(dir string, callbacks []func(path string, info os.FileInfo) bool) ([]string, error) { +// RemoveConditionally walks through the provided dir and then deletes all files +// for which any of the callbacks return true. +func (s *Storage) RemoveConditionally(dir string, callbacks ...removeFileCallback) ([]string, error) { deletedFiles := []string{} var errors []string _ = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { diff --git a/docs/api/source.md b/docs/api/source.md index 6f0d1621b..600aa4125 100644 --- a/docs/api/source.md +++ b/docs/api/source.md @@ -668,6 +668,20 @@ references to this object. NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092

+ + +verificationKeyring
+ + +VerificationKeyring + + + + +(Optional) +

VerificationKeyring for verifying the packaged chart’s signature using a provenance file.

+ + @@ -1850,6 +1864,20 @@ references to this object. NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092

+ + +verificationKeyring
+ + +VerificationKeyring + + + + +(Optional) +

VerificationKeyring for verifying the packaged chart’s signature using a provenance file.

+ + @@ -2251,6 +2279,53 @@ string Source is the interface that provides generic access to the Artifact and interval. It must be supported by all kinds of the source.toolkit.fluxcd.io API group.

+

VerificationKeyring +

+

+(Appears on: +HelmChartSpec) +

+

VerificationKeyring contains enough info to get the public GPG key to be used for verifying +the chart signature using a provenance file.

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+secretRef
+ + +github.com/fluxcd/pkg/apis/meta.LocalObjectReference + + +
+

SecretRef is a reference to the secret that contains the public GPG key.

+
+key
+ +string + +
+(Optional) +

Key in the SecretRef that contains the public keyring in legacy GPG format.

+
+
+

This page was automatically generated with gen-crd-api-reference-docs

diff --git a/internal/helm/chart/builder.go b/internal/helm/chart/builder.go index 6250acad3..aaee045c6 100644 --- a/internal/helm/chart/builder.go +++ b/internal/helm/chart/builder.go @@ -105,7 +105,7 @@ type BuildOptions struct { // because the list of ValuesFiles has changed. Force bool - // Keyring can be configured with the bytes of a public kering in legacy + // Keyring can be set to the bytes of a public kering in legacy // PGP format used for verifying a chart's signature using a provenance file. Keyring []byte } @@ -130,11 +130,11 @@ type Build struct { // Can be empty, in which case a failure should be assumed. Path string // ProvFilePath is the absolute path to a provenance file. - // Can be empty, in which case it should be assumed that the packaged + // It can be empty, in which case it should be assumed that the packaged // chart is not verified. ProvFilePath string // VerificationSignature is populated when a chart's signature - // is successfully verified using it's provenance file. + // is successfully verified using its provenance file. VerificationSignature *VerificationSignature // ValuesFiles is the list of files used to compose the chart's // default "values.yaml". diff --git a/internal/util/file.go b/internal/util/file.go index fed8a73f2..403799b60 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -22,38 +22,32 @@ import ( "path/filepath" ) -func writeBytesToFile(bytes []byte, out string, temp bool) (*os.File, error) { - var file *os.File - var err error - - if temp { - file, err = os.CreateTemp("", out) - if err != nil { - return nil, fmt.Errorf("failed to create temporary file %s: %w", filepath.Base(out), err) - } - } else { - file, err = os.Create(out) - if err != nil { - return nil, fmt.Errorf("failed to create temporary file for chart %s: %w", out, err) - } - } +func writeBytesToFile(bytes []byte, file *os.File) error { if _, err := file.Write(bytes); err != nil { _ = file.Close() - return nil, fmt.Errorf("failed to write to file %s: %w", file.Name(), err) + return fmt.Errorf("failed to write to file %s: %w", file.Name(), err) } if err := file.Close(); err != nil { - return nil, err + return err } - return file, nil + return nil } // Writes the provided bytes to a file at the given path and returns the file handle. func WriteToFile(bytes []byte, path string) (*os.File, error) { - return writeBytesToFile(bytes, path, false) + file, err := os.Create(path) + if err != nil { + return nil, fmt.Errorf("failed to create temporary file for chart %s: %w", path, err) + } + return file, writeBytesToFile(bytes, file) } // Writes the provided bytes to a temp file with the name provided in the path and // returns the file handle. func WriteToTempFile(bytes []byte, out string) (*os.File, error) { - return writeBytesToFile(bytes, filepath.Base(out), true) + file, err := os.CreateTemp("", filepath.Base(out)) + if err != nil { + return nil, fmt.Errorf("failed to create temporary file %s: %w", filepath.Base(out), err) + } + return file, writeBytesToFile(bytes, file) } From a6bb5f0d5c9cf49f23252334859a63eabdd1e76c Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Thu, 17 Mar 2022 19:59:36 +0530 Subject: [PATCH 08/10] remove inline function to verify helm charts in builder Signed-off-by: Sanskar Jaiswal --- internal/helm/chart/builder_local.go | 46 ++++++++------------------- internal/helm/chart/builder_remote.go | 32 +++++-------------- internal/helm/chart/verify.go | 3 ++ 3 files changed, 25 insertions(+), 56 deletions(-) diff --git a/internal/helm/chart/builder_local.go b/internal/helm/chart/builder_local.go index 7e5bdf31f..c6060dec7 100644 --- a/internal/helm/chart/builder_local.go +++ b/internal/helm/chart/builder_local.go @@ -26,7 +26,6 @@ import ( "github.com/Masterminds/semver/v3" securejoin "github.com/cyphar/filepath-securejoin" "helm.sh/helm/v3/pkg/chart/loader" - "helm.sh/helm/v3/pkg/provenance" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/runtime/transform" @@ -107,21 +106,6 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, requiresPackaging := isChartDir || opts.VersionMetadata != "" || len(opts.GetValuesFiles()) != 0 var provFilePath string - verifyProvFile := func(chart, provFile string) (*provenance.Verification, error) { - if opts.Keyring != nil { - if _, err := os.Stat(provFile); err != nil { - err = fmt.Errorf("could not load provenance file %s: %w", provFile, err) - return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} - } - ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), chart, provFile) - if err != nil { - err = fmt.Errorf("failed to verify helm chart using provenance file: %w", err) - return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} - } - return ver, nil - } - return nil, nil - } // If all the following is true, we do not need to package the chart: // - Chart name from cached chart matches resolved name @@ -135,16 +119,14 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, if result.Name == curMeta.Name && result.Version == curMeta.Version { // We can only verify a cached chart with provenance file if we didn't // package the chart ourselves, and instead stored it as is. - if !requiresPackaging { + if !requiresPackaging && opts.Keyring != nil { provFilePath = provenanceFilePath(opts.CachedChart) - ver, err := verifyProvFile(opts.CachedChart, provFilePath) + ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), opts.CachedChart, provFilePath) if err != nil { - return nil, err - } - if ver != nil { - result.VerificationSignature = buildVerificationSig(ver) - result.ProvFilePath = provFilePath + return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} } + result.VerificationSignature = buildVerificationSig(ver) + result.ProvFilePath = provFilePath } result.Path = opts.CachedChart result.ValuesFiles = opts.GetValuesFiles() @@ -158,18 +140,18 @@ func (b *localChartBuilder) Build(ctx context.Context, ref Reference, p string, // If the chart at the path is already packaged and no custom values files // options are set, we can copy the chart without making modifications if !requiresPackaging { - provFilePath = provenanceFilePath(p) if err = copyFileToPath(localRef.Path, p); err != nil { return result, &BuildError{Reason: ErrChartPull, Err: err} } - if err = copyFileToPath(provenanceFilePath(localRef.Path), provFilePath); err != nil { - return result, &BuildError{Reason: ErrChartPull, Err: err} - } - ver, err := verifyProvFile(localRef.Path, provFilePath) - if err != nil { - return nil, err - } - if ver != nil { + if opts.Keyring != nil { + provFilePath = provenanceFilePath(p) + if err = copyFileToPath(provenanceFilePath(localRef.Path), provFilePath); err != nil { + return result, &BuildError{Reason: ErrChartPull, Err: err} + } + ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), localRef.Path, provFilePath) + if err != nil { + return nil, err + } result.ProvFilePath = provFilePath result.VerificationSignature = buildVerificationSig(ver) } diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index 90c522812..9c34a5f36 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -27,7 +27,6 @@ import ( helmchart "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chartutil" - "helm.sh/helm/v3/pkg/provenance" "sigs.k8s.io/yaml" "github.com/fluxcd/pkg/runtime/transform" @@ -107,18 +106,6 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o requiresPackaging := len(opts.GetValuesFiles()) != 0 || opts.VersionMetadata != "" - verifyProvFile := func(chart, provFile string) (*provenance.Verification, error) { - if opts.Keyring != nil { - ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), chart, provFile) - if err != nil { - err = fmt.Errorf("failed to verify helm chart using provenance file %s: %w", provFile, err) - return nil, &BuildError{Reason: ErrProvenanceVerification, Err: err} - } - return ver, nil - } - return nil, nil - } - var provFilePath string // If all the following is true, we do not need to download and/or build the chart: @@ -133,16 +120,14 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o if result.Name == curMeta.Name && result.Version == curMeta.Version { // We can only verify a cached chart with provenance file if we didn't // package the chart ourselves, and instead stored it as is. - if !requiresPackaging { + if !requiresPackaging && opts.Keyring != nil { provFilePath = provenanceFilePath(opts.CachedChart) - ver, err := verifyProvFile(opts.CachedChart, provFilePath) + ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), opts.CachedChart, provFilePath) if err != nil { return nil, err } - if ver != nil { - result.ProvFilePath = provFilePath - result.VerificationSignature = buildVerificationSig(ver) - } + result.ProvFilePath = provFilePath + result.VerificationSignature = buildVerificationSig(ver) } result.Path = opts.CachedChart result.ValuesFiles = opts.GetValuesFiles() @@ -178,14 +163,13 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o if err != nil { return nil, err } - ver, err := verifyProvFile(chart.Name(), provFilePath) + ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), chart.Name(), provFilePath) + if err != nil { return nil, err } - if ver != nil { - result.ProvFilePath = provFilePath - result.VerificationSignature = buildVerificationSig(ver) - } + result.ProvFilePath = provFilePath + result.VerificationSignature = buildVerificationSig(ver) } // Use literal chart copy from remote if no custom values files options are diff --git a/internal/helm/chart/verify.go b/internal/helm/chart/verify.go index 9f0870b24..bf1f675eb 100644 --- a/internal/helm/chart/verify.go +++ b/internal/helm/chart/verify.go @@ -55,6 +55,9 @@ func verifyChartWithProvFile(keyring io.Reader, chartPath, provFilePath string) sig := &provenance.Signatory{KeyRing: ring} verification, err := sig.Verify(chartPath, provFilePath) + if err != nil { + err = fmt.Errorf("failed to verify helm chart using provenance file: %w", err) + } return verification, err } From 6ee98645df2f1fb7099ee9bcfe5721bb4b101fd2 Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Tue, 22 Mar 2022 16:28:26 +0530 Subject: [PATCH 09/10] improve conditions and fix temp file storage Signed-off-by: Sanskar Jaiswal --- api/v1beta2/helmchart_types.go | 4 +-- controllers/helmchart_controller.go | 7 +++-- controllers/helmchart_controller_test.go | 2 +- internal/helm/chart/builder_remote.go | 7 ++--- internal/helm/repository/chart_repository.go | 2 +- internal/util/file.go | 29 ++++++++++++-------- 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/api/v1beta2/helmchart_types.go b/api/v1beta2/helmchart_types.go index afaa5c1b4..8c634b138 100644 --- a/api/v1beta2/helmchart_types.go +++ b/api/v1beta2/helmchart_types.go @@ -172,9 +172,9 @@ const ( // chart succeeded. ChartPackageSucceededReason string = "ChartPackageSucceeded" - // ChartVerifiedSucceededReason signals that the Helm chart's signature + // ChartVerificationSucceededReason signals that the Helm chart's signature // has been verified using it's provenance file. - ChartVerifiedSucceededReason string = "ChartVerifiedSucceeded" + ChartVerificationSucceededReason string = "ChartVerificationSucceeded" ) // GetConditions returns the status conditions of the object. diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 2b12210c1..4df324575 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -809,11 +809,14 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *sourcev1. } return false } - if _, err := r.Storage.RemoveConditionally(dir, callback); err != nil { + if deleted, err := r.Storage.RemoveConditionally(dir, callback); err != nil { return &serror.Event{ Err: fmt.Errorf("garbage collection of old artifacts failed: %w", err), Reason: "GarbageCollectionFailed", } + } else if len(deleted) > 0 { + r.eventLogf(ctx, obj, events.EventTypeTrace, "GarbageCollectionSucceeded", + "garbage collected old artifacts") } } return nil @@ -1039,7 +1042,7 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) { sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: '%X'", build.VerificationSignature.KeyFingerprint)) sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: '%s'", build.VerificationSignature.FileHash)) - conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, sigVerMsg.String()) + conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, sourcev1.ChartVerificationSucceededReason, sigVerMsg.String()) } else { conditions.Delete(obj, sourcev1.SourceVerifiedCondition) } diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 51a701431..255e42b70 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -551,7 +551,7 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { g.Expect(obj.Status.ObservedSourceArtifactRevision).To(Equal(gitArtifact.Revision)) g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled 'helmchart' chart with version '0.1.0'"), - *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerifiedSucceededReason, "chart signed by: 'TestUser' using key with fingeprint: '943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1' and hash verified: 'sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a'"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerificationSucceededReason, "chart signed by: 'TestUser' using key with fingeprint: '943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1' and hash verified: 'sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a'"), })) }, cleanFunc: func(g *WithT, build *chart.Build) { diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index 9c34a5f36..c16e56e02 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -158,13 +158,12 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o // This is needed, since the verification will work only if the .tgz file is untampered. // But we write the packaged chart to disk under a different name, so the provenance file // will not be valid for this _new_ packaged chart. - chart, err := util.WriteToFile(chartBuf, fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version)) - defer os.Remove(chart.Name()) + chart, err := util.WriteToTempFile(chartBuf, fmt.Sprintf("%s-%s.tgz", cv.Name, cv.Version), true) if err != nil { return nil, err } + defer os.Remove(chart.Name()) ver, err := verifyChartWithProvFile(bytes.NewReader(opts.Keyring), chart.Name(), provFilePath) - if err != nil { return nil, err } @@ -245,7 +244,7 @@ func mergeChartValues(chart *helmchart.Chart, paths []string) (map[string]interf // validatePackageAndWriteToPath atomically writes the packaged chart from reader // to out while validating it by loading the chart metadata from the archive. func validatePackageAndWriteToPath(b []byte, out string) error { - tmpFile, err := util.WriteToTempFile(b, out) + tmpFile, err := util.WriteToTempFile(b, out, false) defer os.Remove(tmpFile.Name()) if err != nil { diff --git a/internal/helm/repository/chart_repository.go b/internal/helm/repository/chart_repository.go index 3bd76a726..40c9d362c 100644 --- a/internal/helm/repository/chart_repository.go +++ b/internal/helm/repository/chart_repository.go @@ -216,7 +216,7 @@ func (r *ChartRepository) DownloadProvenanceFile(chart *repo.ChartVersion, path if err != nil { return err } - tmpFile, err := util.WriteToTempFile(res.Bytes(), path) + tmpFile, err := util.WriteToTempFile(res.Bytes(), path, false) defer os.Remove(tmpFile.Name()) if err != nil { diff --git a/internal/util/file.go b/internal/util/file.go index 403799b60..28636ae7d 100644 --- a/internal/util/file.go +++ b/internal/util/file.go @@ -20,6 +20,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/fluxcd/source-controller/internal/fs" ) func writeBytesToFile(bytes []byte, file *os.File) error { @@ -33,21 +35,24 @@ func writeBytesToFile(bytes []byte, file *os.File) error { return nil } -// Writes the provided bytes to a file at the given path and returns the file handle. -func WriteToFile(bytes []byte, path string) (*os.File, error) { - file, err := os.Create(path) - if err != nil { - return nil, fmt.Errorf("failed to create temporary file for chart %s: %w", path, err) - } - return file, writeBytesToFile(bytes, file) -} - // Writes the provided bytes to a temp file with the name provided in the path and -// returns the file handle. -func WriteToTempFile(bytes []byte, out string) (*os.File, error) { +// returns the file handle. If renameToOriginal is true, it renames the temp file to +// the intended file name (since temp file names have random bytes as suffix). +func WriteToTempFile(bytes []byte, out string, renameToOriginal bool) (*os.File, error) { file, err := os.CreateTemp("", filepath.Base(out)) if err != nil { return nil, fmt.Errorf("failed to create temporary file %s: %w", filepath.Base(out), err) } - return file, writeBytesToFile(bytes, file) + err = writeBytesToFile(bytes, file) + if err != nil { + return nil, err + } + if renameToOriginal { + err = fs.RenameWithFallback(file.Name(), filepath.Join("/tmp", filepath.Base(out))) + file, err = os.Open(filepath.Join("/tmp", filepath.Base(out))) + if err != nil { + return nil, fmt.Errorf("failed to rename temporary file %s: %w", filepath.Base(out), err) + } + } + return file, nil } From fcd34a8ee7046586d6ed176e174856563c7634ce Mon Sep 17 00:00:00 2001 From: Sanskar Jaiswal Date: Fri, 25 Mar 2022 12:07:07 +0530 Subject: [PATCH 10/10] improve verification message and file permissions Signed-off-by: Sanskar Jaiswal --- controllers/helmchart_controller.go | 9 ++++----- controllers/helmchart_controller_test.go | 8 ++++---- internal/helm/chart/verify.go | 5 +++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go index 4df324575..820ffe56f 100644 --- a/controllers/helmchart_controller.go +++ b/controllers/helmchart_controller.go @@ -1038,13 +1038,11 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) { if build.VerificationSignature != nil && build.ProvFilePath != "" { var sigVerMsg strings.Builder - sigVerMsg.WriteString(fmt.Sprintf("chart signed by: '%v'", strings.Join(build.VerificationSignature.Identities[:], ","))) - sigVerMsg.WriteString(fmt.Sprintf(" using key with fingeprint: '%X'", build.VerificationSignature.KeyFingerprint)) - sigVerMsg.WriteString(fmt.Sprintf(" and hash verified: '%s'", build.VerificationSignature.FileHash)) + sigVerMsg.WriteString(fmt.Sprintf("verified chart hash: '%s'", build.VerificationSignature.FileHash)) + sigVerMsg.WriteString(fmt.Sprintf(" signed by: '%s'", build.VerificationSignature.Identity)) + sigVerMsg.WriteString(fmt.Sprintf(" with key: '%X'", build.VerificationSignature.KeyFingerprint)) conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, sourcev1.ChartVerificationSucceededReason, sigVerMsg.String()) - } else { - conditions.Delete(obj, sourcev1.SourceVerifiedCondition) } if err != nil { @@ -1080,6 +1078,7 @@ func reasonForBuild(build *chart.Build) string { func (r *HelmChartReconciler) getProvenanceKeyring(ctx context.Context, chart *sourcev1.HelmChart) ([]byte, error) { if chart.Spec.VerificationKeyring == nil { + conditions.Delete(chart, sourcev1.SourceVerifiedCondition) return nil, nil } name := types.NamespacedName{ diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go index 255e42b70..832277949 100644 --- a/controllers/helmchart_controller_test.go +++ b/controllers/helmchart_controller_test.go @@ -327,10 +327,10 @@ func TestHelmChartReconciler_reconcileStorage(t *testing.T) { if err := testStorage.MkdirAll(*obj.Status.Artifact); err != nil { return err } - if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(v), 0644); err != nil { + if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader(v), 0o644); err != nil { return err } - if err := testStorage.AtomicWriteFile(provArtifact, strings.NewReader(v), 0644); err != nil { + if err := testStorage.AtomicWriteFile(provArtifact, strings.NewReader(v), 0o644); err != nil { return err } } @@ -384,7 +384,7 @@ func TestHelmChartReconciler_reconcileStorage(t *testing.T) { if err := testStorage.MkdirAll(*obj.Status.Artifact); err != nil { return err } - if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader("file"), 0644); err != nil { + if err := testStorage.AtomicWriteFile(obj.Status.Artifact, strings.NewReader("file"), 0o644); err != nil { return err } return nil @@ -551,7 +551,7 @@ func TestHelmChartReconciler_reconcileSource(t *testing.T) { g.Expect(obj.Status.ObservedSourceArtifactRevision).To(Equal(gitArtifact.Revision)) g.Expect(obj.Status.Conditions).To(conditions.MatchConditions([]metav1.Condition{ *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled 'helmchart' chart with version '0.1.0'"), - *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerificationSucceededReason, "chart signed by: 'TestUser' using key with fingeprint: '943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1' and hash verified: 'sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a'"), + *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, sourcev1.ChartVerificationSucceededReason, "verified chart hash: 'sha256:007c7b7446eebcb18caeffe9898a3356ba1795f54df40ad39cfcc7382874a10a' signed by: 'TestUser' with key: '943CB5929ECDA2B5B5EC88BC7035BA97D32A87C1'"), })) }, cleanFunc: func(g *WithT, build *chart.Build) { diff --git a/internal/helm/chart/verify.go b/internal/helm/chart/verify.go index bf1f675eb..30cf02822 100644 --- a/internal/helm/chart/verify.go +++ b/internal/helm/chart/verify.go @@ -74,7 +74,7 @@ func provenanceFilePath(path string) string { // ref: https://github.com/helm/helm/blob/v3.8.0/pkg/action/verify.go#L47-L51 type VerificationSignature struct { - Identities []string + Identity string KeyFingerprint [20]byte FileHash string } @@ -84,7 +84,8 @@ func buildVerificationSig(ver *provenance.Verification) *VerificationSignature { if ver != nil { if ver.SignedBy != nil { for name := range ver.SignedBy.Identities { - verSig.Identities = append(verSig.Identities, name) + verSig.Identity = name + break } } verSig.FileHash = ver.FileHash