Merge pull request #925 from souleb/cosign-verify-helm
implement Cosign verification for HelmCharts
This commit is contained in:
commit
09cae78713
|
@ -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 when using HelmRepository source with spec.type 'oci'.
|
||||||
|
// 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,34 @@ 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 when using HelmRepository source with spec.type 'oci'.
|
||||||
|
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"
|
||||||
|
@ -56,6 +57,8 @@ import (
|
||||||
"github.com/fluxcd/pkg/runtime/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
"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/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"
|
||||||
|
@ -79,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,
|
||||||
|
@ -89,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,
|
||||||
},
|
},
|
||||||
|
@ -209,6 +214,7 @@ func (r *HelmChartReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
|
||||||
summarizeHelper := summarize.NewHelper(r.EventRecorder, patchHelper)
|
summarizeHelper := summarize.NewHelper(r.EventRecorder, patchHelper)
|
||||||
summarizeOpts := []summarize.Option{
|
summarizeOpts := []summarize.Option{
|
||||||
summarize.WithConditions(helmChartReadyCondition),
|
summarize.WithConditions(helmChartReadyCondition),
|
||||||
|
summarize.WithBiPolarityConditionTypes(sourcev1.SourceVerifiedCondition),
|
||||||
summarize.WithReconcileResult(recResult),
|
summarize.WithReconcileResult(recResult),
|
||||||
summarize.WithReconcileError(retErr),
|
summarize.WithReconcileError(retErr),
|
||||||
summarize.WithIgnoreNotFound(),
|
summarize.WithIgnoreNotFound(),
|
||||||
|
@ -366,6 +372,12 @@ func (r *HelmChartReconciler) reconcileStorage(ctx context.Context, obj *sourcev
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1.HelmChart, build *chart.Build) (_ sreconcile.Result, retErr error) {
|
func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1.HelmChart, build *chart.Build) (_ sreconcile.Result, retErr error) {
|
||||||
|
// Remove any failed verification condition.
|
||||||
|
// The reason is that a failing verification should be recalculated.
|
||||||
|
if conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
|
||||||
|
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the source
|
// Retrieve the source
|
||||||
s, err := r.getSource(ctx, obj)
|
s, err := r.getSource(ctx, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -455,8 +467,9 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1
|
||||||
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *sourcev1.HelmChart,
|
func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *sourcev1.HelmChart,
|
||||||
repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
|
repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
|
||||||
var (
|
var (
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
loginOpts []helmreg.LoginOption
|
authenticator authn.Authenticator
|
||||||
|
keychain authn.Keychain
|
||||||
)
|
)
|
||||||
// Used to login with the repository declared provider
|
// Used to login with the repository declared provider
|
||||||
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
|
ctxTimeout, cancel := context.WithTimeout(ctx, repo.Spec.Timeout.Duration)
|
||||||
|
@ -481,10 +494,10 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build client options from secret
|
// Build client options from secret
|
||||||
opts, err := getter.ClientOptionsFromSecret(*secret)
|
opts, tls, err := r.clientOptionsFromSecret(secret, normalizedURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
Err: err,
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
Reason: sourcev1.AuthenticationFailedReason,
|
||||||
}
|
}
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
@ -492,20 +505,10 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
clientOpts = append(clientOpts, opts...)
|
clientOpts = append(clientOpts, opts...)
|
||||||
|
tlsConfig = tls
|
||||||
tlsConfig, err = getter.TLSClientConfigFromSecret(*secret, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
e := &serror.Event{
|
|
||||||
Err: fmt.Errorf("failed to create TLS client config with secret data: %w", err),
|
|
||||||
Reason: sourcev1.AuthenticationFailedReason,
|
|
||||||
}
|
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
|
||||||
// Requeue as content of secret might change
|
|
||||||
return sreconcile.ResultEmpty, e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build registryClient options from secret
|
// Build registryClient options from secret
|
||||||
loginOpt, err := registry.LoginOptionFromSecret(normalizedURL, *secret)
|
keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
|
||||||
|
@ -515,10 +518,8 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
// Requeue as content of secret might change
|
// Requeue as content of secret might change
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
|
|
||||||
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
|
||||||
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
auth, authErr := oidcAuth(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
|
Err: fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr),
|
||||||
|
@ -528,10 +529,20 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
return sreconcile.ResultEmpty, e
|
return sreconcile.ResultEmpty, e
|
||||||
}
|
}
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
loginOpts = append([]helmreg.LoginOption{}, auth)
|
authenticator = auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
|
||||||
|
if err != nil {
|
||||||
|
e := &serror.Event{
|
||||||
|
Err: err,
|
||||||
|
Reason: sourcev1.AuthenticationFailedReason,
|
||||||
|
}
|
||||||
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
|
||||||
|
return sreconcile.ResultEmpty, e
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the chart repository
|
// Initialize the chart repository
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
switch repo.Spec.Type {
|
switch repo.Spec.Type {
|
||||||
|
@ -545,7 +556,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
|
||||||
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
|
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
|
||||||
// TODO@souleb: remove this once the registry move to Oras v2
|
// TODO@souleb: remove this once the registry move to Oras v2
|
||||||
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
|
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
|
||||||
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to construct Helm client: %w", err),
|
Err: fmt.Errorf("failed to construct Helm client: %w", err),
|
||||||
|
@ -564,9 +575,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.Event{
|
||||||
|
Err: fmt.Errorf("failed to verify the signature using provider '%s': %w", provider, err),
|
||||||
|
Reason: 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,8 +606,8 @@ 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 loginOpts != nil {
|
if loginOpt != nil {
|
||||||
err = ociChartRepo.Login(loginOpts...)
|
err = ociChartRepo.Login(loginOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := &serror.Event{
|
e := &serror.Event{
|
||||||
Err: fmt.Errorf("failed to login to OCI registry: %w", err),
|
Err: fmt.Errorf("failed to login to OCI registry: %w", err),
|
||||||
|
@ -622,6 +654,10 @@ 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 exists with the same name and version and `Force` is false.
|
||||||
|
// It will however try to verify the chart if `obj.Spec.Verify` is set, at every reconciliation.
|
||||||
|
Verify: obj.Spec.Verify != nil && obj.Spec.Verify.Provider != "",
|
||||||
}
|
}
|
||||||
if artifact := obj.GetArtifact(); artifact != nil {
|
if artifact := obj.GetArtifact(); artifact != nil {
|
||||||
opts.CachedChart = r.Storage.LocalPath(*artifact)
|
opts.CachedChart = r.Storage.LocalPath(*artifact)
|
||||||
|
@ -941,8 +977,9 @@ func (r *HelmChartReconciler) garbageCollect(ctx context.Context, obj *sourcev1.
|
||||||
func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback {
|
func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Context, name, namespace string) chart.GetChartDownloaderCallback {
|
||||||
return func(url string) (repository.Downloader, error) {
|
return func(url string) (repository.Downloader, error) {
|
||||||
var (
|
var (
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
loginOpts []helmreg.LoginOption
|
authenticator authn.Authenticator
|
||||||
|
keychain authn.Keychain
|
||||||
)
|
)
|
||||||
normalizedURL := repository.NormalizeURL(url)
|
normalizedURL := repository.NormalizeURL(url)
|
||||||
repo, err := r.resolveDependencyRepository(ctx, url, namespace)
|
repo, err := r.resolveDependencyRepository(ctx, url, namespace)
|
||||||
|
@ -972,37 +1009,39 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
opts, err := getter.ClientOptionsFromSecret(*secret)
|
|
||||||
|
// Build client options from secret
|
||||||
|
opts, tls, err := r.clientOptionsFromSecret(secret, normalizedURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
clientOpts = append(clientOpts, opts...)
|
clientOpts = append(clientOpts, opts...)
|
||||||
|
tlsConfig = tls
|
||||||
tlsConfig, err = getter.TLSClientConfigFromSecret(*secret, normalizedURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create TLS client config for HelmRepository '%s': %w", repo.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build registryClient options from secret
|
// Build registryClient options from secret
|
||||||
loginOpt, err := registry.LoginOptionFromSecret(normalizedURL, *secret)
|
keychain, err = registry.LoginOptionFromSecret(normalizedURL, *secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create login options for HelmRepository '%s': %w", repo.Name, err)
|
return nil, fmt.Errorf("failed to create login options for HelmRepository '%s': %w", repo.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
loginOpts = append([]helmreg.LoginOption{}, loginOpt)
|
|
||||||
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
} else if repo.Spec.Provider != sourcev1.GenericOCIProvider && repo.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
auth, authErr := oidcAuthFromAdapter(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
auth, authErr := oidcAuth(ctxTimeout, repo.Spec.URL, repo.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
|
return nil, fmt.Errorf("failed to get credential from %s: %w", repo.Spec.Provider, authErr)
|
||||||
}
|
}
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
loginOpts = append([]helmreg.LoginOption{}, auth)
|
authenticator = auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginOpt, err := makeLoginOption(authenticator, keychain, normalizedURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var chartRepo repository.Downloader
|
var chartRepo repository.Downloader
|
||||||
if helmreg.IsOCI(normalizedURL) {
|
if helmreg.IsOCI(normalizedURL) {
|
||||||
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpts != nil)
|
registryClient, credentialsFile, err := r.RegistryClientGenerator(loginOpt != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create registry client for HelmRepository '%s': %w", repo.Name, err)
|
return nil, fmt.Errorf("failed to create registry client for HelmRepository '%s': %w", repo.Name, err)
|
||||||
}
|
}
|
||||||
|
@ -1027,8 +1066,8 @@ 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 loginOpts != nil {
|
if loginOpt != nil {
|
||||||
err = ociChartRepo.Login(loginOpts...)
|
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))
|
||||||
// clean up the credentialsFile
|
// clean up the credentialsFile
|
||||||
|
@ -1078,6 +1117,20 @@ func (r *HelmChartReconciler) resolveDependencyRepository(ctx context.Context, u
|
||||||
return nil, fmt.Errorf("no HelmRepository found for '%s' in '%s' namespace", url, namespace)
|
return nil, fmt.Errorf("no HelmRepository found for '%s' in '%s' namespace", url, namespace)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HelmChartReconciler) clientOptionsFromSecret(secret *corev1.Secret, normalizedURL string) ([]helmgetter.Option, *tls.Config, error) {
|
||||||
|
opts, err := getter.ClientOptionsFromSecret(*secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to configure Helm client with secret data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig, err := getter.TLSClientConfigFromSecret(*secret, normalizedURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to create TLS client config with secret data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, tlsConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repository *sourcev1.HelmRepository) (*corev1.Secret, error) {
|
func (r *HelmChartReconciler) getHelmRepositorySecret(ctx context.Context, repository *sourcev1.HelmRepository) (*corev1.Secret, error) {
|
||||||
if repository.Spec.SecretRef == nil {
|
if repository.Spec.SecretRef == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -1222,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 {
|
||||||
|
@ -1237,6 +1295,10 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
|
||||||
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
|
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
|
||||||
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())
|
||||||
|
case chart.ErrChartVerification:
|
||||||
|
conditions.Delete(obj, sourcev1.FetchFailedCondition)
|
||||||
|
conditions.MarkTrue(obj, sourcev1.BuildFailedCondition, buildErr.Reason.Reason, buildErr.Error())
|
||||||
|
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, buildErr.Error())
|
||||||
default:
|
default:
|
||||||
conditions.Delete(obj, sourcev1.BuildFailedCondition)
|
conditions.Delete(obj, sourcev1.BuildFailedCondition)
|
||||||
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, buildErr.Reason.Reason, buildErr.Error())
|
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, buildErr.Reason.Reason, buildErr.Error())
|
||||||
|
@ -1273,3 +1335,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,230 @@ 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
|
||||||
|
shouldSign bool
|
||||||
|
beforeFunc func(obj *sourcev1.HelmChart)
|
||||||
|
want sreconcile.Result
|
||||||
|
wantErr bool
|
||||||
|
wantErrMsg string
|
||||||
|
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"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
want: sreconcile.ResultEmpty,
|
||||||
|
wantErr: true,
|
||||||
|
wantErrMsg: "chart verification error: failed to verify <url>: no matching signatures:",
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify <url>: no matching signatures:"),
|
||||||
|
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "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",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
want: sreconcile.ResultEmpty,
|
||||||
|
wantErr: true,
|
||||||
|
assertConditions: []metav1.Condition{
|
||||||
|
*conditions.TrueCondition(sourcev1.BuildFailedCondition, "ChartVerificationError", "chart verification error: failed to verify <url>: no matching signatures:"),
|
||||||
|
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "chart verification error: failed to verify <url>: no matching signatures:"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "signed charts should pass verification",
|
||||||
|
shouldSign: true,
|
||||||
|
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"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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))
|
||||||
|
|
|
@ -45,6 +45,7 @@ import (
|
||||||
helper "github.com/fluxcd/pkg/runtime/controller"
|
helper "github.com/fluxcd/pkg/runtime/controller"
|
||||||
"github.com/fluxcd/pkg/runtime/patch"
|
"github.com/fluxcd/pkg/runtime/patch"
|
||||||
"github.com/fluxcd/pkg/runtime/predicates"
|
"github.com/fluxcd/pkg/runtime/predicates"
|
||||||
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
|
||||||
"github.com/fluxcd/source-controller/api/v1beta2"
|
"github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
|
||||||
|
@ -263,36 +264,21 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
|
||||||
}
|
}
|
||||||
conditions.Delete(obj, meta.StalledCondition)
|
conditions.Delete(obj, meta.StalledCondition)
|
||||||
|
|
||||||
var loginOpts []helmreg.LoginOption
|
var (
|
||||||
|
authenticator authn.Authenticator
|
||||||
|
keychain authn.Keychain
|
||||||
|
err error
|
||||||
|
)
|
||||||
// Configure any authentication related options.
|
// Configure any authentication related options.
|
||||||
if obj.Spec.SecretRef != nil {
|
if obj.Spec.SecretRef != nil {
|
||||||
// Attempt to retrieve secret.
|
keychain, err = authFromSecret(ctx, r.Client, obj)
|
||||||
name := types.NamespacedName{
|
|
||||||
Namespace: obj.GetNamespace(),
|
|
||||||
Name: obj.Spec.SecretRef.Name,
|
|
||||||
}
|
|
||||||
var secret corev1.Secret
|
|
||||||
if err := r.Client.Get(ctx, name, &secret); err != nil {
|
|
||||||
e := fmt.Errorf("failed to get secret '%s': %w", name.String(), err)
|
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
|
||||||
result, retErr = ctrl.Result{}, e
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct login options.
|
|
||||||
loginOpt, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to configure Helm client with secret data: %w", err)
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
result, retErr = ctrl.Result{}, err
|
||||||
result, retErr = ctrl.Result{}, e
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if loginOpt != nil {
|
|
||||||
loginOpts = append(loginOpts, loginOpt)
|
|
||||||
}
|
|
||||||
} else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
} else if obj.Spec.Provider != sourcev1.GenericOCIProvider && obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
|
||||||
auth, authErr := oidcAuthFromAdapter(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
||||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||||
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
e := fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
||||||
|
@ -300,12 +286,19 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
loginOpts = append(loginOpts, auth)
|
authenticator = auth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginOpt, err := makeLoginOption(authenticator, keychain, obj.Spec.URL)
|
||||||
|
if err != nil {
|
||||||
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, err.Error())
|
||||||
|
result, retErr = ctrl.Result{}, err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Create registry client and login if needed.
|
// Create registry client and login if needed.
|
||||||
registryClient, file, err := r.RegistryClientGenerator(loginOpts != nil)
|
registryClient, file, err := r.RegistryClientGenerator(loginOpt != nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to create registry client: %w", err)
|
e := fmt.Errorf("failed to create registry client: %w", err)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
|
||||||
|
@ -332,8 +325,8 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *v1beta
|
||||||
conditions.Delete(obj, meta.StalledCondition)
|
conditions.Delete(obj, meta.StalledCondition)
|
||||||
|
|
||||||
// Attempt to login to the registry if credentials are provided.
|
// Attempt to login to the registry if credentials are provided.
|
||||||
if loginOpts != nil {
|
if loginOpt != nil {
|
||||||
err = chartRepo.Login(loginOpts...)
|
err = chartRepo.Login(loginOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e := fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
|
e := fmt.Errorf("failed to login to registry '%s': %w", obj.Spec.URL, err)
|
||||||
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
conditions.MarkFalse(obj, meta.ReadyCondition, sourcev1.AuthenticationFailedReason, e.Error())
|
||||||
|
@ -375,16 +368,37 @@ func (r *HelmRepositoryOCIReconciler) eventLogf(ctx context.Context, obj runtime
|
||||||
r.Eventf(obj, eventType, reason, msg)
|
r.Eventf(obj, eventType, reason, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// oidcAuthFromAdapter generates the OIDC credential authenticator based on the specified cloud provider.
|
// authFromSecret returns an authn.Keychain for the given HelmRepository.
|
||||||
func oidcAuthFromAdapter(ctx context.Context, url, provider string) (helmreg.LoginOption, error) {
|
// If the HelmRepository does not specify a secretRef, an anonymous keychain is returned.
|
||||||
auth, err := oidcAuth(ctx, url, provider)
|
func authFromSecret(ctx context.Context, client client.Client, obj *sourcev1.HelmRepository) (authn.Keychain, error) {
|
||||||
|
// Attempt to retrieve secret.
|
||||||
|
name := types.NamespacedName{
|
||||||
|
Namespace: obj.GetNamespace(),
|
||||||
|
Name: obj.Spec.SecretRef.Name,
|
||||||
|
}
|
||||||
|
var secret corev1.Secret
|
||||||
|
if err := client.Get(ctx, name, &secret); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get secret '%s': %w", name.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct login options.
|
||||||
|
keychain, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to configure Helm client with secret data: %w", err)
|
||||||
}
|
}
|
||||||
|
return keychain, nil
|
||||||
if auth == nil {
|
}
|
||||||
return nil, fmt.Errorf("could not validate OCI provider %s with URL %s", provider, url)
|
|
||||||
}
|
// makeLoginOption returns a registry login option for the given HelmRepository.
|
||||||
|
// If the HelmRepository does not specify a secretRef, a nil login option is returned.
|
||||||
return registry.OIDCAdaptHelper(auth)
|
func makeLoginOption(auth authn.Authenticator, keychain authn.Keychain, registryURL string) (helmreg.LoginOption, error) {
|
||||||
|
if auth != nil {
|
||||||
|
return registry.AuthAdaptHelper(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if keychain != nil {
|
||||||
|
return registry.KeychainAdaptHelper(keychain)(registryURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -220,15 +220,6 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
|
||||||
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"),
|
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "with contextual login provider and invalid repository URL",
|
|
||||||
wantErr: true,
|
|
||||||
provider: "aws",
|
|
||||||
providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com",
|
|
||||||
assertConditions: []metav1.Condition{
|
|
||||||
*conditions.FalseCondition(meta.ReadyCondition, sourcev1.AuthenticationFailedReason, "failed to get credential from"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "with contextual login provider and secretRef",
|
name: "with contextual login provider and secretRef",
|
||||||
want: ctrl.Result{RequeueAfter: interval},
|
want: ctrl.Result{RequeueAfter: interval},
|
||||||
|
|
|
@ -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 when using HelmRepository source with spec.type ‘oci’.
|
||||||
|
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 when using HelmRepository source with spec.type ‘oci’.
|
||||||
|
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>
|
||||||
|
|
|
@ -240,6 +240,103 @@ in a new Artifact. When the field is set to `false` or removed, it will resume.
|
||||||
For practical information, see
|
For practical information, see
|
||||||
[suspending and resuming](#suspending-and-resuming).
|
[suspending and resuming](#suspending-and-resuming).
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
**Note:** This feature is available only for Helm charts fetched from an OCI Registry.
|
||||||
|
|
||||||
|
`.spec.verify` is an optional field to enable the verification of [Cosign](https://github.com/sigstore/cosign)
|
||||||
|
signatures. The field offers two subfields:
|
||||||
|
|
||||||
|
- `.provider`, to specify the verification provider. Only supports `cosign` at present.
|
||||||
|
- `.secretRef.name`, to specify a reference to a Secret in the same namespace as
|
||||||
|
the HelmChart, containing the Cosign public keys of trusted authors.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
spec:
|
||||||
|
verify:
|
||||||
|
provider: cosign
|
||||||
|
secretRef:
|
||||||
|
name: cosign-public-keys
|
||||||
|
```
|
||||||
|
|
||||||
|
When the verification succeeds, the controller adds a Condition with the
|
||||||
|
following attributes to the HelmChart's `.status.conditions`:
|
||||||
|
|
||||||
|
- `type: SourceVerified`
|
||||||
|
- `status: "True"`
|
||||||
|
- `reason: Succeeded`
|
||||||
|
|
||||||
|
#### Public keys verification
|
||||||
|
|
||||||
|
To verify the authenticity of HelmChart hosted in an OCI Registry, create a Kubernetes
|
||||||
|
secret with the Cosign public keys:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: cosign-public-keys
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
key1.pub: <BASE64>
|
||||||
|
key2.pub: <BASE64>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the keys must have the `.pub` extension for Flux to make use of them.
|
||||||
|
|
||||||
|
Flux will loop over the public keys and use them to verify a HelmChart's signature.
|
||||||
|
This allows for older HelmCharts to be valid as long as the right key is in the secret.
|
||||||
|
|
||||||
|
#### Keyless verification
|
||||||
|
|
||||||
|
For publicly available HelmCharts, which are signed using the
|
||||||
|
[Cosign Keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) procedure,
|
||||||
|
you can enable the verification by omitting the `.verify.secretRef` field.
|
||||||
|
|
||||||
|
Example of verifying HelmCharts signed by the
|
||||||
|
[Cosign GitHub Action](https://github.com/sigstore/cosign-installer) with GitHub OIDC Token:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: HelmChart
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
spec:
|
||||||
|
interval: 5m
|
||||||
|
chart: podinfo
|
||||||
|
reconcileStrategy: ChartVersion
|
||||||
|
sourceRef:
|
||||||
|
kind: HelmRepository
|
||||||
|
name: podinfo
|
||||||
|
version: ">=6.1.6"
|
||||||
|
verify:
|
||||||
|
provider: cosign
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||||
|
kind: HelmRepository
|
||||||
|
metadata:
|
||||||
|
name: podinfo
|
||||||
|
spec:
|
||||||
|
interval: 1m0s
|
||||||
|
url: oci://ghcr.io/stefanprodan/charts
|
||||||
|
type: "oci"
|
||||||
|
```
|
||||||
|
|
||||||
|
The controller verifies the signatures using the Fulcio root CA and the Rekor
|
||||||
|
instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/).
|
||||||
|
|
||||||
|
Note that keyless verification is an **experimental feature**, using
|
||||||
|
custom root CAs or self-hosted Rekor instances are not currently supported.
|
||||||
|
|
||||||
## Working with HelmCharts
|
## Working with HelmCharts
|
||||||
|
|
||||||
### Triggering a reconcile
|
### Triggering a reconcile
|
||||||
|
|
|
@ -462,6 +462,9 @@ data:
|
||||||
|
|
||||||
Note that the keys must have the `.pub` extension for Flux to make use of them.
|
Note that the keys must have the `.pub` extension for Flux to make use of them.
|
||||||
|
|
||||||
|
Flux will loop over the public keys and use them to verify an artifact's signature.
|
||||||
|
This allows for older artifacts to be valid as long as the right key is in the secret.
|
||||||
|
|
||||||
#### Keyless verification
|
#### Keyless verification
|
||||||
|
|
||||||
For publicly available OCI artifacts, which are signed using the
|
For publicly available OCI artifacts, which are signed using the
|
||||||
|
|
|
@ -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"}
|
||||||
)
|
)
|
||||||
|
|
|
@ -23,27 +23,42 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config"
|
"github.com/docker/cli/cli/config"
|
||||||
"github.com/docker/cli/cli/config/credentials"
|
"github.com/docker/cli/cli/config/credentials"
|
||||||
|
"github.com/fluxcd/source-controller/internal/oci"
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
"helm.sh/helm/v3/pkg/registry"
|
"helm.sh/helm/v3/pkg/registry"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// helper is a subset of the Docker credential helper credentials.Helper interface used by NewKeychainFromHelper.
|
||||||
|
type helper struct {
|
||||||
|
registry string
|
||||||
|
username, password string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h helper) Get(serverURL string) (string, string, error) {
|
||||||
|
if serverURL != h.registry {
|
||||||
|
return "", "", fmt.Errorf("unexpected serverURL: %s", serverURL)
|
||||||
|
}
|
||||||
|
return h.username, h.password, h.err
|
||||||
|
}
|
||||||
|
|
||||||
// LoginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
|
// LoginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
|
||||||
// may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold
|
// may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold
|
||||||
// a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are
|
// a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are
|
||||||
// empty, a nil LoginOption and a nil error will be returned.
|
// empty, a nil LoginOption and a nil error will be returned.
|
||||||
func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.LoginOption, error) {
|
func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (authn.Keychain, error) {
|
||||||
var username, password string
|
var username, password string
|
||||||
|
parsedURL, err := url.Parse(registryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse registry URL '%s' while reconciling Secret '%s': %w",
|
||||||
|
registryURL, secret.Name, err)
|
||||||
|
}
|
||||||
if secret.Type == corev1.SecretTypeDockerConfigJson {
|
if secret.Type == corev1.SecretTypeDockerConfigJson {
|
||||||
dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey]))
|
dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to load Docker config from Secret '%s': %w", secret.Name, err)
|
return nil, fmt.Errorf("unable to load Docker config from Secret '%s': %w", secret.Name, err)
|
||||||
}
|
}
|
||||||
parsedURL, err := url.Parse(registryURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to parse registry URL '%s' while reconciling Secret '%s': %w",
|
|
||||||
registryURL, secret.Name, err)
|
|
||||||
}
|
|
||||||
authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host)
|
authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to get authentication data from Secret '%s': %w", secret.Name, err)
|
return nil, fmt.Errorf("unable to get authentication data from Secret '%s': %w", secret.Name, err)
|
||||||
|
@ -63,19 +78,38 @@ func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.L
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case username == "" && password == "":
|
case username == "" && password == "":
|
||||||
return nil, nil
|
return oci.Anonymous{}, nil
|
||||||
case username == "" || password == "":
|
case username == "" || password == "":
|
||||||
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
|
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
|
||||||
}
|
}
|
||||||
return registry.LoginOptBasicAuth(username, password), nil
|
return authn.NewKeychainFromHelper(helper{registry: parsedURL.Host, username: username, password: password}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCAdaptHelper returns an ORAS credentials callback configured with the authorization data
|
// KeyChainAdaptHelper returns an ORAS credentials callback configured with the authorization data
|
||||||
|
// from the given authn keychain. This allows for example to make use of credential helpers from
|
||||||
|
// cloud providers.
|
||||||
|
// Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn
|
||||||
|
func KeychainAdaptHelper(keyChain authn.Keychain) func(string) (registry.LoginOption, error) {
|
||||||
|
return func(registryURL string) (registry.LoginOption, error) {
|
||||||
|
parsedURL, err := url.Parse(registryURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse registry URL '%s'", registryURL)
|
||||||
|
}
|
||||||
|
authenticator, err := keyChain.Resolve(stringResource{parsedURL.Host})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to resolve credentials for registry '%s': %w", registryURL, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthAdaptHelper(authenticator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthAdaptHelper returns an ORAS credentials callback configured with the authorization data
|
||||||
// from the given authn authenticator. This allows for example to make use of credential helpers from
|
// from the given authn authenticator. This allows for example to make use of credential helpers from
|
||||||
// cloud providers.
|
// cloud providers.
|
||||||
// Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn
|
// Ref: https://github.com/google/go-containerregistry/tree/main/pkg/authn
|
||||||
func OIDCAdaptHelper(authenticator authn.Authenticator) (registry.LoginOption, error) {
|
func AuthAdaptHelper(auth authn.Authenticator) (registry.LoginOption, error) {
|
||||||
authConfig, err := authenticator.Authorization()
|
authConfig, err := auth.Authorization()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to get authentication data from OIDC: %w", err)
|
return nil, fmt.Errorf("unable to get authentication data from OIDC: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -91,3 +125,17 @@ func OIDCAdaptHelper(authenticator authn.Authenticator) (registry.LoginOption, e
|
||||||
}
|
}
|
||||||
return registry.LoginOptBasicAuth(username, password), nil
|
return registry.LoginOptBasicAuth(username, password), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stringResource is there to satisfy the github.com/google/go-containerregistry/pkg/authn.Resource interface.
|
||||||
|
// It merely wraps a given string and returns it for all of the interface's methods.
|
||||||
|
type stringResource struct {
|
||||||
|
registry string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r stringResource) String() string {
|
||||||
|
return r.registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r stringResource) RegistryStr() string {
|
||||||
|
return r.registry
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
"github.com/google/go-containerregistry/pkg/authn"
|
||||||
|
@ -24,6 +25,8 @@ import (
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const repoURL = "https://example.com"
|
||||||
|
|
||||||
func TestLoginOptionFromSecret(t *testing.T) {
|
func TestLoginOptionFromSecret(t *testing.T) {
|
||||||
testURL := "oci://registry.example.com/foo/bar"
|
testURL := "oci://registry.example.com/foo/bar"
|
||||||
testUser := "flux"
|
testUser := "flux"
|
||||||
|
@ -131,33 +134,40 @@ func TestLoginOptionFromSecret(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOIDCAdaptHelper(t *testing.T) {
|
func TestKeychainAdaptHelper(t *testing.T) {
|
||||||
auth := &authn.Basic{
|
g := NewWithT(t)
|
||||||
Username: "flux",
|
reg, err := url.Parse(repoURL)
|
||||||
Password: "flux_password",
|
if err != nil {
|
||||||
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := helper{
|
||||||
|
username: "flux",
|
||||||
|
password: "flux_password",
|
||||||
|
registry: reg.Host,
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
auth authn.Authenticator
|
auth authn.Keychain
|
||||||
expectedLogin bool
|
expectedLogin bool
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Login from basic auth with empty auth",
|
name: "Login from basic auth with empty auth",
|
||||||
auth: &authn.Basic{},
|
auth: authn.NewKeychainFromHelper(helper{}),
|
||||||
expectedLogin: false,
|
expectedLogin: false,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Login from basic auth",
|
name: "Login from basic auth",
|
||||||
auth: auth,
|
auth: authn.NewKeychainFromHelper(auth),
|
||||||
expectedLogin: true,
|
expectedLogin: true,
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Login with missing password",
|
name: "Login with missing password",
|
||||||
auth: &authn.Basic{Username: "flux"},
|
auth: authn.NewKeychainFromHelper(helper{username: "flux", registry: reg.Host}),
|
||||||
expectedLogin: false,
|
expectedLogin: false,
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
@ -166,7 +176,7 @@ func TestOIDCAdaptHelper(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
g := NewWithT(t)
|
g := NewWithT(t)
|
||||||
loginOpt, err := OIDCAdaptHelper(tt.auth)
|
loginOpt, err := KeychainAdaptHelper(tt.auth)(repoURL)
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
g.Expect(err).To(HaveOccurred())
|
g.Expect(err).To(HaveOccurred())
|
||||||
return
|
return
|
||||||
|
|
|
@ -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,10 @@ func (r *ChartRepository) RemoveCache() error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyChart verifies the chart against a signature.
|
||||||
|
// It returns an error on failure.
|
||||||
|
func (r *ChartRepository) VerifyChart(_ context.Context, _ *repo.ChartVersion) error {
|
||||||
|
// this is a no-op because this is not implemented yet.
|
||||||
|
return fmt.Errorf("not implemented")
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -296,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,6 +34,11 @@ 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
|
||||||
|
@ -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