implement Cosign verification for HelmCharts
If implemented, users will be able to enable chart verification for OCI based helm charts. Signed-off-by: Soule BA <soule@weave.works>
This commit is contained in:
parent
55dd799dad
commit
0e97547eeb
|
@ -86,6 +86,14 @@ type HelmChartSpec struct {
|
||||||
// NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092
|
// NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092
|
||||||
// +optional
|
// +optional
|
||||||
AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"`
|
AccessFrom *acl.AccessFrom `json:"accessFrom,omitempty"`
|
||||||
|
|
||||||
|
// Verify contains the secret name containing the trusted public keys
|
||||||
|
// used to verify the signature and specifies which provider to use to check
|
||||||
|
// whether OCI image is authentic.
|
||||||
|
// This field is only supported for OCI sources.
|
||||||
|
// Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.
|
||||||
|
// +optional
|
||||||
|
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -464,6 +464,11 @@ func (in *HelmChartSpec) DeepCopyInto(out *HelmChartSpec) {
|
||||||
*out = new(acl.AccessFrom)
|
*out = new(acl.AccessFrom)
|
||||||
(*in).DeepCopyInto(*out)
|
(*in).DeepCopyInto(*out)
|
||||||
}
|
}
|
||||||
|
if in.Verify != nil {
|
||||||
|
in, out := &in.Verify, &out.Verify
|
||||||
|
*out = new(OCIRepositoryVerification)
|
||||||
|
(*in).DeepCopyInto(*out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartSpec.
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmChartSpec.
|
||||||
|
|
|
@ -403,6 +403,33 @@ spec:
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
|
verify:
|
||||||
|
description: Verify contains the secret name containing the trusted
|
||||||
|
public keys used to verify the signature and specifies which provider
|
||||||
|
to use to check whether OCI image is authentic. This field is only
|
||||||
|
supported for OCI sources. Chart dependencies, which are not bundled
|
||||||
|
in the umbrella chart artifact, are not verified.
|
||||||
|
properties:
|
||||||
|
provider:
|
||||||
|
default: cosign
|
||||||
|
description: Provider specifies the technology used to sign the
|
||||||
|
OCI Artifact.
|
||||||
|
enum:
|
||||||
|
- cosign
|
||||||
|
type: string
|
||||||
|
secretRef:
|
||||||
|
description: SecretRef specifies the Kubernetes Secret containing
|
||||||
|
the trusted public keys.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of the referent.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- provider
|
||||||
|
type: object
|
||||||
version:
|
version:
|
||||||
default: '*'
|
default: '*'
|
||||||
description: Version is the chart version semver expression, ignored
|
description: Version is the chart version semver expression, ignored
|
||||||
|
|
|
@ -19,3 +19,17 @@ spec:
|
||||||
name: podinfo
|
name: podinfo
|
||||||
version: '6.1.*'
|
version: '6.1.*'
|
||||||
interval: 1m
|
interval: 1m
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: podinfo-keyless
|
||||||
|
spec:
|
||||||
|
chart: podinfo
|
||||||
|
sourceRef:
|
||||||
|
kind: HelmRepository
|
||||||
|
name: podinfo
|
||||||
|
version: '6.2.1'
|
||||||
|
interval: 1m
|
||||||
|
verify:
|
||||||
|
provider: cosign
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||||
helmgetter "helm.sh/helm/v3/pkg/getter"
|
helmgetter "helm.sh/helm/v3/pkg/getter"
|
||||||
helmreg "helm.sh/helm/v3/pkg/registry"
|
helmreg "helm.sh/helm/v3/pkg/registry"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
@ -57,6 +58,7 @@ import (
|
||||||
"github.com/fluxcd/pkg/runtime/predicates"
|
"github.com/fluxcd/pkg/runtime/predicates"
|
||||||
"github.com/fluxcd/pkg/untar"
|
"github.com/fluxcd/pkg/untar"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
|
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
"github.com/fluxcd/source-controller/internal/cache"
|
"github.com/fluxcd/source-controller/internal/cache"
|
||||||
|
@ -80,6 +82,7 @@ var helmChartReadyCondition = summarize.Conditions{
|
||||||
sourcev1.BuildFailedCondition,
|
sourcev1.BuildFailedCondition,
|
||||||
sourcev1.ArtifactOutdatedCondition,
|
sourcev1.ArtifactOutdatedCondition,
|
||||||
sourcev1.ArtifactInStorageCondition,
|
sourcev1.ArtifactInStorageCondition,
|
||||||
|
sourcev1.SourceVerifiedCondition,
|
||||||
meta.ReadyCondition,
|
meta.ReadyCondition,
|
||||||
meta.ReconcilingCondition,
|
meta.ReconcilingCondition,
|
||||||
meta.StalledCondition,
|
meta.StalledCondition,
|
||||||
|
@ -90,6 +93,7 @@ var helmChartReadyCondition = summarize.Conditions{
|
||||||
sourcev1.BuildFailedCondition,
|
sourcev1.BuildFailedCondition,
|
||||||
sourcev1.ArtifactOutdatedCondition,
|
sourcev1.ArtifactOutdatedCondition,
|
||||||
sourcev1.ArtifactInStorageCondition,
|
sourcev1.ArtifactInStorageCondition,
|
||||||
|
sourcev1.SourceVerifiedCondition,
|
||||||
meta.StalledCondition,
|
meta.StalledCondition,
|
||||||
meta.ReconcilingCondition,
|
meta.ReconcilingCondition,
|
||||||
},
|
},
|
||||||
|
@ -564,9 +568,30 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var verifiers []soci.Verifier
|
||||||
|
if obj.Spec.Verify != nil {
|
||||||
|
provider := obj.Spec.Verify.Provider
|
||||||
|
verifiers, err = r.makeVerifiers(ctx, obj, authenticator, keychain)
|
||||||
|
if err != nil {
|
||||||
|
if obj.Spec.Verify.SecretRef == nil {
|
||||||
|
provider = fmt.Sprintf("%s keyless", provider)
|
||||||
|
}
|
||||||
|
e := serror.NewGeneric(
|
||||||
|
fmt.Errorf("failed to verify the signature using provider '%s': %w", provider, err),
|
||||||
|
sourcev1.VerificationError,
|
||||||
|
)
|
||||||
|
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tell the chart repository to use the OCI client with the configured getter
|
// Tell the chart repository to use the OCI client with the configured getter
|
||||||
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
|
clientOpts = append(clientOpts, helmgetter.WithRegistryClient(registryClient))
|
||||||
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, repository.WithOCIGetter(r.Getters), repository.WithOCIGetterOptions(clientOpts), repository.WithOCIRegistryClient(registryClient))
|
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
|
||||||
|
repository.WithOCIGetter(r.Getters),
|
||||||
|
repository.WithOCIGetterOptions(clientOpts),
|
||||||
|
repository.WithOCIRegistryClient(registryClient),
|
||||||
|
repository.WithVerifiers(verifiers))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return chartRepoConfigErrorReturn(err, obj)
|
return chartRepoConfigErrorReturn(err, obj)
|
||||||
}
|
}
|
||||||
|
@ -574,7 +599,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
|
|
||||||
// If login options are configured, use them to login to the registry
|
// If login options are configured, use them to login to the registry
|
||||||
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
||||||
if keychain != nil {
|
if loginOpt != nil {
|
||||||
err = ociChartRepo.Login(loginOpt)
|
err = ociChartRepo.Login(loginOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
|
@ -622,6 +647,17 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
opts := chart.BuildOptions{
|
opts := chart.BuildOptions{
|
||||||
ValuesFiles: obj.GetValuesFiles(),
|
ValuesFiles: obj.GetValuesFiles(),
|
||||||
Force: obj.Generation != obj.Status.ObservedGeneration,
|
Force: obj.Generation != obj.Status.ObservedGeneration,
|
||||||
|
// The remote builder will not attempt to download the chart if
|
||||||
|
// an artifact exist with the same name and version and the force is false.
|
||||||
|
// It will try to verify the chart if:
|
||||||
|
// - we are on the first reconciliation
|
||||||
|
// - the HelmChart spec has changed (generation drift)
|
||||||
|
// - the previous reconciliation resulted in a failed artifact verification
|
||||||
|
// - there is no artifact in storage
|
||||||
|
Verify: obj.Spec.Verify != nil && (obj.Generation <= 0 ||
|
||||||
|
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
|
||||||
|
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) ||
|
||||||
|
obj.GetArtifact() == nil),
|
||||||
}
|
}
|
||||||
if artifact := obj.GetArtifact(); artifact != nil {
|
if artifact := obj.GetArtifact(); artifact != nil {
|
||||||
opts.CachedChart = r.Storage.LocalPath(*artifact)
|
opts.CachedChart = r.Storage.LocalPath(*artifact)
|
||||||
|
@ -1030,7 +1066,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
|
|
||||||
// If login options are configured, use them to login to the registry
|
// If login options are configured, use them to login to the registry
|
||||||
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
// The OCIGetter will later retrieve the stored credentials to pull the chart
|
||||||
if keychain != nil {
|
if loginOpt != nil {
|
||||||
err = ociChartRepo.Login(loginOpt)
|
err = ociChartRepo.Login(loginOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
|
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
|
||||||
|
@ -1239,6 +1275,11 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
|
||||||
if build.Complete() {
|
if build.Complete() {
|
||||||
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
||||||
conditions.Delete(obj, sourcev1.BuildFailedCondition)
|
conditions.Delete(obj, sourcev1.BuildFailedCondition)
|
||||||
|
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, fmt.Sprintf("verified signature of version %s", build.Version))
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Spec.Verify == nil {
|
||||||
|
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1251,7 +1292,7 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch buildErr.Reason {
|
switch buildErr.Reason {
|
||||||
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
|
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage, chart.ErrChartVerification:
|
||||||
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
||||||
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
|
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
|
||||||
default:
|
default:
|
||||||
|
@ -1290,3 +1331,60 @@ func chartRepoConfigErrorReturn(err error, obj *sourcev1.HelmChart) (sreconcile.
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// makeVerifiers returns a list of verifiers for the given chart.
|
||||||
|
func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *sourcev1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) ([]soci.Verifier, error) {
|
||||||
|
var verifiers []soci.Verifier
|
||||||
|
verifyOpts := []remote.Option{}
|
||||||
|
if auth != nil {
|
||||||
|
verifyOpts = append(verifyOpts, remote.WithAuth(auth))
|
||||||
|
} else {
|
||||||
|
verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch obj.Spec.Verify.Provider {
|
||||||
|
case "cosign":
|
||||||
|
defaultCosignOciOpts := []soci.Options{
|
||||||
|
soci.WithRemoteOptions(verifyOpts...),
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the public keys from the given secret
|
||||||
|
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
|
||||||
|
certSecretName := types.NamespacedName{
|
||||||
|
Namespace: obj.Namespace,
|
||||||
|
Name: secretRef.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubSecret corev1.Secret
|
||||||
|
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, data := range pubSecret.Data {
|
||||||
|
// search for public keys in the secret
|
||||||
|
if strings.HasSuffix(k, ".pub") {
|
||||||
|
verifier, err := soci.NewCosignVerifier(ctx, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
verifiers = append(verifiers, verifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(verifiers) == 0 {
|
||||||
|
return nil, fmt.Errorf("no public keys found in secret '%s'", certSecretName)
|
||||||
|
}
|
||||||
|
return verifiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no secret is provided, add a keyless verifier
|
||||||
|
verifier, err := soci.NewCosignVerifier(ctx, defaultCosignOciOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
verifiers = append(verifiers, verifier)
|
||||||
|
return verifiers, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -33,6 +34,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
coptions "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||||
|
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
||||||
|
"github.com/sigstore/cosign/pkg/cosign"
|
||||||
hchart "helm.sh/helm/v3/pkg/chart"
|
hchart "helm.sh/helm/v3/pkg/chart"
|
||||||
"helm.sh/helm/v3/pkg/chart/loader"
|
"helm.sh/helm/v3/pkg/chart/loader"
|
||||||
helmreg "helm.sh/helm/v3/pkg/registry"
|
helmreg "helm.sh/helm/v3/pkg/registry"
|
||||||
|
@ -57,6 +61,7 @@ import (
|
||||||
serror "github.com/fluxcd/source-controller/internal/error"
|
serror "github.com/fluxcd/source-controller/internal/error"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/chart"
|
"github.com/fluxcd/source-controller/internal/helm/chart"
|
||||||
"github.com/fluxcd/source-controller/internal/helm/registry"
|
"github.com/fluxcd/source-controller/internal/helm/registry"
|
||||||
|
"github.com/fluxcd/source-controller/internal/oci"
|
||||||
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
|
||||||
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
|
||||||
)
|
)
|
||||||
|
@ -2213,6 +2218,228 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
server, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
const (
|
||||||
|
chartPath = "testdata/charts/helmchart-0.1.0.tgz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load a test chart
|
||||||
|
chartData, err := ioutil.ReadFile(chartPath)
|
||||||
|
|
||||||
|
// Upload the test chart
|
||||||
|
metadata, err := loadTestChartToOCI(chartData, chartPath, server)
|
||||||
|
g.Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
cachedArtifact := &sourcev1.Artifact{
|
||||||
|
Revision: "0.1.0",
|
||||||
|
Path: metadata.Name + "-" + metadata.Version + ".tgz",
|
||||||
|
}
|
||||||
|
g.Expect(storage.CopyFromPath(cachedArtifact, "testdata/charts/helmchart-0.1.0.tgz")).To(Succeed())
|
||||||
|
|
||||||
|
pf := func(b bool) ([]byte, error) {
|
||||||
|
return []byte("cosign-password"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
keys, err := cosign.GenerateKeyPair(pf)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
err = os.WriteFile(path.Join(tmpDir, "cosign.key"), keys.PrivateBytes, 0600)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.Remove(path.Join(tmpDir, "cosign.key"))
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want sreconcile.Result
|
||||||
|
wantErr bool
|
||||||
|
wantErrMsg string
|
||||||
|
shouldSign bool
|
||||||
|
beforeFunc func(obj *sourcev1.HelmChart)
|
||||||
|
assertConditions []metav1.Condition
|
||||||
|
cleanFunc func(g *WithT, build *chart.Build)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unsigned charts should not pass verification",
|
||||||
|
beforeFunc: func(obj *sourcev1.HelmChart) {
|
||||||
|
obj.Spec.Chart = metadata.Name
|
||||||
|
obj.Spec.Version = metadata.Version
|
||||||
|
obj.Spec.Verify = &sourcev1.OCIRepositoryVerification{
|
||||||
|
Provider: "cosign",
|
||||||
|
SecretRef: &meta.LocalObjectReference{Name: "cosign-key"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "chart verification error: failed to verify <url>: no matching signatures:",
|
||||||
|
want: sreconcile.ResultEmpty,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify <url>: no matching signatures:"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unsigned charts should not pass keyless verification",
|
||||||
|
beforeFunc: func(obj *sourcev1.HelmChart) {
|
||||||
|
obj.Spec.Chart = metadata.Name
|
||||||
|
obj.Spec.Version = metadata.Version
|
||||||
|
obj.Spec.Verify = &sourcev1.OCIRepositoryVerification{
|
||||||
|
Provider: "cosign",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
want: sreconcile.ResultEmpty,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify <url>: no matching signatures:"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signed charts should pass verification",
|
||||||
|
beforeFunc: func(obj *sourcev1.HelmChart) {
|
||||||
|
obj.Spec.Chart = metadata.Name
|
||||||
|
obj.Spec.Version = metadata.Version
|
||||||
|
obj.Spec.Verify = &sourcev1.OCIRepositoryVerification{
|
||||||
|
Provider: "cosign",
|
||||||
|
SecretRef: &meta.LocalObjectReference{Name: "cosign-key"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shouldSign: true,
|
||||||
|
want: sreconcile.ResultSuccess,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '<name>' chart with version '<version>'"),
|
||||||
|
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of version <version>"),
|
||||||
|
},
|
||||||
|
cleanFunc: func(g *WithT, build *chart.Build) {
|
||||||
|
g.Expect(os.Remove(build.Path)).To(Succeed())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "verify failed before, removed from spec, remove condition",
|
||||||
|
beforeFunc: func(obj *sourcev1.HelmChart) {
|
||||||
|
obj.Spec.Chart = metadata.Name
|
||||||
|
obj.Spec.Version = metadata.Version
|
||||||
|
obj.Spec.Verify = nil
|
||||||
|
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "fail msg")
|
||||||
|
obj.Status.Artifact = &sourcev1.Artifact{Path: metadata.Name + "-" + metadata.Version + ".tgz"}
|
||||||
|
},
|
||||||
|
want: sreconcile.ResultSuccess,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewChart", "pulled '<name>' chart with version '<version>'"),
|
||||||
|
},
|
||||||
|
cleanFunc: func(g *WithT, build *chart.Build) {
|
||||||
|
g.Expect(os.Remove(build.Path)).To(Succeed())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
g := NewWithT(t)
|
||||||
|
|
||||||
|
clientBuilder := fake.NewClientBuilder()
|
||||||
|
|
||||||
|
repository := &sourcev1.HelmRepository{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: "helmrepository-",
|
||||||
|
},
|
||||||
|
Spec: sourcev1.HelmRepositorySpec{
|
||||||
|
URL: fmt.Sprintf("oci://%s/testrepo", server.registryHost),
|
||||||
|
Timeout: &metav1.Duration{Duration: timeout},
|
||||||
|
Provider: sourcev1.GenericOCIProvider,
|
||||||
|
Type: sourcev1.HelmRepositoryTypeOCI,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "cosign-key",
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"cosign.pub": keys.PublicBytes,
|
||||||
|
}}
|
||||||
|
|
||||||
|
clientBuilder.WithObjects(repository, secret)
|
||||||
|
|
||||||
|
r := &HelmChartReconciler{
|
||||||
|
Client: clientBuilder.Build(),
|
||||||
|
EventRecorder: record.NewFakeRecorder(32),
|
||||||
|
Getters: testGetters,
|
||||||
|
Storage: storage,
|
||||||
|
RegistryClientGenerator: registry.ClientGenerator,
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := &sourcev1.HelmChart{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
GenerateName: "helmchart-",
|
||||||
|
},
|
||||||
|
Spec: sourcev1.HelmChartSpec{
|
||||||
|
SourceRef: sourcev1.LocalHelmChartSourceReference{
|
||||||
|
Kind: sourcev1.HelmRepositoryKind,
|
||||||
|
Name: repository.Name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
chartUrl := fmt.Sprintf("oci://%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)
|
||||||
|
|
||||||
|
if tt.beforeFunc != nil {
|
||||||
|
tt.beforeFunc(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.shouldSign {
|
||||||
|
ko := coptions.KeyOpts{
|
||||||
|
KeyRef: path.Join(tmpDir, "cosign.key"),
|
||||||
|
PassFunc: pf,
|
||||||
|
}
|
||||||
|
|
||||||
|
ro := &coptions.RootOptions{
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sign.SignCmd(ro, ko, coptions.RegistryOptions{Keychain: oci.Anonymous{}},
|
||||||
|
nil, []string{fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)}, "",
|
||||||
|
"", true, "",
|
||||||
|
"", "", false,
|
||||||
|
false, "", false)
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
assertConditions := tt.assertConditions
|
||||||
|
for k := range assertConditions {
|
||||||
|
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<name>", metadata.Name)
|
||||||
|
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<version>", metadata.Version)
|
||||||
|
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", chartUrl)
|
||||||
|
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "cosign")
|
||||||
|
}
|
||||||
|
|
||||||
|
var b chart.Build
|
||||||
|
if tt.cleanFunc != nil {
|
||||||
|
defer tt.cleanFunc(g, &b)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := r.reconcileSource(ctx, obj, &b)
|
||||||
|
if tt.wantErr {
|
||||||
|
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", chartUrl)
|
||||||
|
g.Expect(err).ToNot(BeNil())
|
||||||
|
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
|
||||||
|
} else {
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
g.Expect(got).To(Equal(tt.want))
|
||||||
|
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extractChartMeta is used to extract a chart metadata from a byte array
|
// extractChartMeta is used to extract a chart metadata from a byte array
|
||||||
func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
|
func extractChartMeta(chartData []byte) (*hchart.Metadata, error) {
|
||||||
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
|
ch, err := loader.LoadArchive(bytes.NewReader(chartData))
|
||||||
|
|
|
@ -628,7 +628,7 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
|
||||||
for k, data := range pubSecret.Data {
|
for k, data := range pubSecret.Data {
|
||||||
// search for public keys in the secret
|
// search for public keys in the secret
|
||||||
if strings.HasSuffix(k, ".pub") {
|
if strings.HasSuffix(k, ".pub") {
|
||||||
verifier, err := soci.NewVerifier(ctxTimeout, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
|
verifier, err := soci.NewCosignVerifier(ctxTimeout, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -654,7 +654,7 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
|
||||||
|
|
||||||
// if no secret is provided, try keyless verification
|
// if no secret is provided, try keyless verification
|
||||||
ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless method")
|
ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless method")
|
||||||
verifier, err := soci.NewVerifier(ctxTimeout, defaultCosignOciOpts...)
|
verifier, err := soci.NewCosignVerifier(ctxTimeout, defaultCosignOciOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -670,6 +670,24 @@ references to this object.
|
||||||
NOTE: Not implemented, provisional as of <a href="https://github.com/fluxcd/flux2/pull/2092">https://github.com/fluxcd/flux2/pull/2092</a></p>
|
NOTE: Not implemented, provisional as of <a href="https://github.com/fluxcd/flux2/pull/2092">https://github.com/fluxcd/flux2/pull/2092</a></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>verify</code><br>
|
||||||
|
<em>
|
||||||
|
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">
|
||||||
|
OCIRepositoryVerification
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>Verify contains the secret name containing the trusted public keys
|
||||||
|
used to verify the signature and specifies which provider to use to check
|
||||||
|
whether OCI image is authentic.
|
||||||
|
This field is only supported for OCI sources.
|
||||||
|
Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -2237,6 +2255,24 @@ references to this object.
|
||||||
NOTE: Not implemented, provisional as of <a href="https://github.com/fluxcd/flux2/pull/2092">https://github.com/fluxcd/flux2/pull/2092</a></p>
|
NOTE: Not implemented, provisional as of <a href="https://github.com/fluxcd/flux2/pull/2092">https://github.com/fluxcd/flux2/pull/2092</a></p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>verify</code><br>
|
||||||
|
<em>
|
||||||
|
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">
|
||||||
|
OCIRepositoryVerification
|
||||||
|
</a>
|
||||||
|
</em>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<em>(Optional)</em>
|
||||||
|
<p>Verify contains the secret name containing the trusted public keys
|
||||||
|
used to verify the signature and specifies which provider to use to check
|
||||||
|
whether OCI image is authentic.
|
||||||
|
This field is only supported for OCI sources.
|
||||||
|
Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3123,6 +3159,7 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
(<em>Appears on:</em>
|
(<em>Appears on:</em>
|
||||||
|
<a href="#source.toolkit.fluxcd.io/v1beta2.HelmChartSpec">HelmChartSpec</a>,
|
||||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
|
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
|
||||||
</p>
|
</p>
|
||||||
<p>OCIRepositoryVerification verifies the authenticity of an OCI Artifact</p>
|
<p>OCIRepositoryVerification verifies the authenticity of an OCI Artifact</p>
|
||||||
|
|
|
@ -165,6 +165,7 @@ echo "Run HelmChart from OCI registry tests"
|
||||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/helmchart-from-oci/source.yaml"
|
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/helmchart-from-oci/source.yaml"
|
||||||
kubectl -n source-system wait helmrepository/podinfo --for=condition=ready --timeout=1m
|
kubectl -n source-system wait helmrepository/podinfo --for=condition=ready --timeout=1m
|
||||||
kubectl -n source-system wait helmchart/podinfo --for=condition=ready --timeout=1m
|
kubectl -n source-system wait helmchart/podinfo --for=condition=ready --timeout=1m
|
||||||
|
kubectl -n source-system wait helmchart/podinfo-keyless --for=condition=ready --timeout=1m
|
||||||
|
|
||||||
echo "Run OCIRepository verify tests"
|
echo "Run OCIRepository verify tests"
|
||||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-key.yaml"
|
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-key.yaml"
|
||||||
|
|
|
@ -113,6 +113,8 @@ type BuildOptions struct {
|
||||||
// Force can be set to force the build of the chart, for example
|
// Force can be set to force the build of the chart, for example
|
||||||
// because the list of ValuesFiles has changed.
|
// because the list of ValuesFiles has changed.
|
||||||
Force bool
|
Force bool
|
||||||
|
// Verifier can be set to the verification of the chart.
|
||||||
|
Verify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetValuesFiles returns BuildOptions.ValuesFiles, except if it equals
|
// GetValuesFiles returns BuildOptions.ValuesFiles, except if it equals
|
||||||
|
|
|
@ -63,7 +63,7 @@ func NewRemoteBuilder(repository repository.Downloader) Builder {
|
||||||
// After downloading the chart, it is only packaged if required due to BuildOptions
|
// 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
|
// modifying the chart, otherwise the exact data as retrieved from the repository
|
||||||
// is written to p, after validating it to be a chart.
|
// 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(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) {
|
||||||
remoteRef, ok := ref.(RemoteReference)
|
remoteRef, ok := ref.(RemoteReference)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := fmt.Errorf("expected remote chart reference")
|
err := fmt.Errorf("expected remote chart reference")
|
||||||
|
@ -74,9 +74,9 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
|
||||||
return nil, &BuildError{Reason: ErrChartReference, Err: err}
|
return nil, &BuildError{Reason: ErrChartReference, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
res, result, err := b.downloadFromRepository(b.remote, remoteRef, opts)
|
res, result, err := b.downloadFromRepository(ctx, b.remote, remoteRef, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &BuildError{Reason: ErrChartPull, Err: err}
|
return nil, err
|
||||||
}
|
}
|
||||||
if res == nil {
|
if res == nil {
|
||||||
return result, nil
|
return result, nil
|
||||||
|
@ -124,7 +124,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *remoteChartBuilder) downloadFromRepository(remote repository.Downloader, remoteRef RemoteReference, opts BuildOptions) (*bytes.Buffer, *Build, error) {
|
func (b *remoteChartBuilder) downloadFromRepository(ctx context.Context, remote repository.Downloader, remoteRef RemoteReference, opts BuildOptions) (*bytes.Buffer, *Build, error) {
|
||||||
// Get the current version for the RemoteReference
|
// Get the current version for the RemoteReference
|
||||||
cv, err := remote.GetChartVersion(remoteRef.Name, remoteRef.Version)
|
cv, err := remote.GetChartVersion(remoteRef.Name, remoteRef.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -132,6 +132,13 @@ func (b *remoteChartBuilder) downloadFromRepository(remote repository.Downloader
|
||||||
return nil, nil, &BuildError{Reason: ErrChartReference, Err: err}
|
return nil, nil, &BuildError{Reason: ErrChartReference, Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the chart if necessary
|
||||||
|
if opts.Verify {
|
||||||
|
if err := remote.VerifyChart(ctx, cv); err != nil {
|
||||||
|
return nil, nil, &BuildError{Reason: ErrChartVerification, Err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result, shouldReturn, err := generateBuildResult(cv, opts)
|
result, shouldReturn, err := generateBuildResult(cv, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
|
@ -84,5 +84,6 @@ var (
|
||||||
ErrValuesFilesMerge = BuildErrorReason{Reason: "ValuesFilesError", Summary: "values files merge error"}
|
ErrValuesFilesMerge = BuildErrorReason{Reason: "ValuesFilesError", Summary: "values files merge error"}
|
||||||
ErrDependencyBuild = BuildErrorReason{Reason: "DependencyBuildError", Summary: "dependency build error"}
|
ErrDependencyBuild = BuildErrorReason{Reason: "DependencyBuildError", Summary: "dependency build error"}
|
||||||
ErrChartPackage = BuildErrorReason{Reason: "ChartPackageError", Summary: "chart package error"}
|
ErrChartPackage = BuildErrorReason{Reason: "ChartPackageError", Summary: "chart package error"}
|
||||||
|
ErrChartVerification = BuildErrorReason{Reason: "ChartVerificationError", Summary: "chart verification error"}
|
||||||
ErrUnknown = BuildErrorReason{Reason: "Unknown", Summary: "unknown build error"}
|
ErrUnknown = BuildErrorReason{Reason: "Unknown", Summary: "unknown build error"}
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
@ -520,3 +521,12 @@ func (r *ChartRepository) RemoveCache() error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyChart verifies the chart against a signature.
|
||||||
|
// If no signature is provided, a keyless verification is performed.
|
||||||
|
// It returns an error on failure.
|
||||||
|
func (r *ChartRepository) VerifyChart(_ context.Context, _ *repo.ChartVersion) error {
|
||||||
|
// no-op
|
||||||
|
// this is a no-op because this is not implemented yet.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -32,7 +33,10 @@ import (
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/version"
|
"github.com/fluxcd/pkg/version"
|
||||||
|
"github.com/fluxcd/source-controller/internal/oci"
|
||||||
"github.com/fluxcd/source-controller/internal/transport"
|
"github.com/fluxcd/source-controller/internal/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,12 +67,23 @@ type OCIChartRepository struct {
|
||||||
RegistryClient RegistryClient
|
RegistryClient RegistryClient
|
||||||
// credentialsFile is a temporary credentials file to use while downloading tags or charts from a registry.
|
// credentialsFile is a temporary credentials file to use while downloading tags or charts from a registry.
|
||||||
credentialsFile string
|
credentialsFile string
|
||||||
|
|
||||||
|
// verifiers is a list of verifiers to use when verifying a chart.
|
||||||
|
verifiers []oci.Verifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// OCIChartRepositoryOption is a function that can be passed to NewOCIChartRepository
|
// OCIChartRepositoryOption is a function that can be passed to NewOCIChartRepository
|
||||||
// to configure an OCIChartRepository.
|
// to configure an OCIChartRepository.
|
||||||
type OCIChartRepositoryOption func(*OCIChartRepository) error
|
type OCIChartRepositoryOption func(*OCIChartRepository) error
|
||||||
|
|
||||||
|
// WithVerifiers returns a ChartRepositoryOption that will set the chart verifiers
|
||||||
|
func WithVerifiers(verifiers []oci.Verifier) OCIChartRepositoryOption {
|
||||||
|
return func(r *OCIChartRepository) error {
|
||||||
|
r.verifiers = verifiers
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WithOCIRegistryClient returns a ChartRepositoryOption that will set the registry client
|
// WithOCIRegistryClient returns a ChartRepositoryOption that will set the registry client
|
||||||
func WithOCIRegistryClient(client RegistryClient) OCIChartRepositoryOption {
|
func WithOCIRegistryClient(client RegistryClient) OCIChartRepositoryOption {
|
||||||
return func(r *OCIChartRepository) error {
|
return func(r *OCIChartRepository) error {
|
||||||
|
@ -215,7 +230,6 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf
|
||||||
// Login attempts to login to the OCI registry.
|
// Login attempts to login to the OCI registry.
|
||||||
// It returns an error on failure.
|
// It returns an error on failure.
|
||||||
func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error {
|
func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error {
|
||||||
// Get login credentials from keychain
|
|
||||||
err := r.RegistryClient.Login(r.URL.Host, opts...)
|
err := r.RegistryClient.Login(r.URL.Host, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -297,3 +311,32 @@ func getLastMatchingVersionOrConstraint(cvs []string, ver string) (string, error
|
||||||
|
|
||||||
return matchingVersions[0].Original(), nil
|
return matchingVersions[0].Original(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyChart verifies the chart against a signature.
|
||||||
|
// If no signature is provided, a keyless verification is performed.
|
||||||
|
// It returns an error on failure.
|
||||||
|
func (r *OCIChartRepository) VerifyChart(ctx context.Context, chart *repo.ChartVersion) error {
|
||||||
|
if len(r.verifiers) == 0 {
|
||||||
|
return fmt.Errorf("no verifiers available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(chart.URLs) == 0 {
|
||||||
|
return fmt.Errorf("chart '%s' has no downloadable URLs", chart.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ref, err := name.ParseReference(strings.TrimPrefix(chart.URLs[0], fmt.Sprintf("%s://", registry.OCIScheme)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid chart reference: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the chart
|
||||||
|
for _, verifier := range r.verifiers {
|
||||||
|
if verified, err := verifier.Verify(ctx, ref); err != nil {
|
||||||
|
return fmt.Errorf("failed to verify %s: %w", chart.URLs[0], err)
|
||||||
|
} else if verified {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no matching signatures were found for '%s'", ref.Name())
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
|
||||||
"helm.sh/helm/v3/pkg/repo"
|
"helm.sh/helm/v3/pkg/repo"
|
||||||
)
|
)
|
||||||
|
@ -29,6 +30,8 @@ type Downloader interface {
|
||||||
GetChartVersion(name, version string) (*repo.ChartVersion, error)
|
GetChartVersion(name, version string) (*repo.ChartVersion, error)
|
||||||
// DownloadChart downloads a chart from the remote Helm repository or OCI Helm repository.
|
// DownloadChart downloads a chart from the remote Helm repository or OCI Helm repository.
|
||||||
DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error)
|
DownloadChart(chart *repo.ChartVersion) (*bytes.Buffer, error)
|
||||||
|
// VerifyChart verifies the chart against a signature.
|
||||||
|
VerifyChart(ctx context.Context, chart *repo.ChartVersion) error
|
||||||
// Clear removes all temporary files created by the downloader, caching the files if the cache is configured,
|
// Clear removes all temporary files created by the downloader, caching the files if the cache is configured,
|
||||||
// and calling garbage collector to remove unused files.
|
// and calling garbage collector to remove unused files.
|
||||||
Clear() error
|
Clear() error
|
||||||
|
|
|
@ -34,13 +34,18 @@ import (
|
||||||
"github.com/sigstore/sigstore/pkg/signature"
|
"github.com/sigstore/sigstore/pkg/signature"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Verifier is an interface for verifying the authenticity of an OCI image.
|
||||||
|
type Verifier interface {
|
||||||
|
Verify(ctx context.Context, ref name.Reference) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
// options is a struct that holds options for verifier.
|
// options is a struct that holds options for verifier.
|
||||||
type options struct {
|
type options struct {
|
||||||
PublicKey []byte
|
PublicKey []byte
|
||||||
ROpt []remote.Option
|
ROpt []remote.Option
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options is a function that configures the options applied to a Verifier.
|
// Options is a function that configures the options applied to a CosignVerifier.
|
||||||
type Options func(opts *options)
|
type Options func(opts *options)
|
||||||
|
|
||||||
// WithPublicKey sets the public key.
|
// WithPublicKey sets the public key.
|
||||||
|
@ -58,13 +63,13 @@ func WithRemoteOptions(opts ...remote.Option) Options {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifier is a struct which is responsible for executing verification logic.
|
// CosignVerifier is a struct which is responsible for executing verification logic.
|
||||||
type Verifier struct {
|
type CosignVerifier struct {
|
||||||
opts *cosign.CheckOpts
|
opts *cosign.CheckOpts
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVerifier initializes a new Verifier.
|
// NewCosignVerifier initializes a new CosignVerifier.
|
||||||
func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) {
|
func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, error) {
|
||||||
o := options{}
|
o := options{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(&o)
|
opt(&o)
|
||||||
|
@ -117,12 +122,28 @@ func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) {
|
||||||
checkOpts.RekorClient = rc
|
checkOpts.RekorClient = rc
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Verifier{
|
return &CosignVerifier{
|
||||||
opts: checkOpts,
|
opts: checkOpts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyImageSignatures verify the authenticity of the given ref OCI image.
|
// VerifyImageSignatures verify the authenticity of the given ref OCI image.
|
||||||
func (v *Verifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) {
|
func (v *CosignVerifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) {
|
||||||
return cosign.VerifyImageSignatures(ctx, ref, v.opts)
|
return cosign.VerifyImageSignatures(ctx, ref, v.opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify verifies the authenticity of the given ref OCI image.
|
||||||
|
// It returns a boolean indicating if the verification was successful.
|
||||||
|
// It returns an error if the verification fails, nil otherwise.
|
||||||
|
func (v *CosignVerifier) Verify(ctx context.Context, ref name.Reference) (bool, error) {
|
||||||
|
signatures, _, err := v.VerifyImageSignatures(ctx, ref)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(signatures) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue