diff --git a/api/v1beta2/helmchart_types.go b/api/v1beta2/helmchart_types.go
index 5b12f1f56..0ab4f3ca6 100644
--- a/api/v1beta2/helmchart_types.go
+++ b/api/v1beta2/helmchart_types.go
@@ -86,6 +86,14 @@ type HelmChartSpec struct {
// NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092
// +optional
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.
+ // Optional dependencies e.g. umbrella chart are not verified.
+ // +optional
+ Verify *OCIRepositoryVerification `json:"verify,omitempty"`
}
const (
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index b759c3791..a05e1f33d 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -449,6 +449,11 @@ func (in *HelmChartSpec) DeepCopyInto(out *HelmChartSpec) {
*out = new(acl.AccessFrom)
(*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.
diff --git a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml
index 7ef36829d..7e231efbd 100644
--- a/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml
+++ b/config/crd/bases/source.toolkit.fluxcd.io_helmcharts.yaml
@@ -403,6 +403,31 @@ spec:
items:
type: string
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.
+ 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:
default: '*'
description: Version is the chart version semver expression, ignored
diff --git a/controllers/helmchart_controller.go b/controllers/helmchart_controller.go
index 3cc7cc66a..544262347 100644
--- a/controllers/helmchart_controller.go
+++ b/controllers/helmchart_controller.go
@@ -28,6 +28,7 @@ import (
"strings"
"time"
+ soci "github.com/fluxcd/source-controller/internal/oci"
helmgetter "helm.sh/helm/v3/pkg/getter"
helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1"
@@ -57,6 +58,7 @@ import (
"github.com/fluxcd/pkg/runtime/predicates"
"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"
"github.com/fluxcd/source-controller/internal/cache"
@@ -70,6 +72,8 @@ import (
"github.com/fluxcd/source-controller/internal/util"
)
+const cosignSignatureKey = "cosign.pub"
+
// helmChartReadyCondition contains all the conditions information
// needed for HelmChart Ready status conditions summary calculation.
var helmChartReadyCondition = summarize.Conditions{
@@ -80,6 +84,7 @@ var helmChartReadyCondition = summarize.Conditions{
sourcev1.BuildFailedCondition,
sourcev1.ArtifactOutdatedCondition,
sourcev1.ArtifactInStorageCondition,
+ sourcev1.SourceVerifiedCondition,
meta.ReadyCondition,
meta.ReconcilingCondition,
meta.StalledCondition,
@@ -90,6 +95,7 @@ var helmChartReadyCondition = summarize.Conditions{
sourcev1.BuildFailedCondition,
sourcev1.ArtifactOutdatedCondition,
sourcev1.ArtifactInStorageCondition,
+ sourcev1.SourceVerifiedCondition,
meta.StalledCondition,
meta.ReconcilingCondition,
},
@@ -563,9 +569,30 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
}()
}
+ var verifier soci.Verifier
+ if obj.Spec.Verify != nil {
+ provider := obj.Spec.Verify.Provider
+ verifier, err = r.makeVerifier(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
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.WithVerifier(verifier))
if err != nil {
return chartRepoConfigErrorReturn(err, obj)
}
@@ -573,7 +600,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
// If login options are configured, use them to login to the registry
// The OCIGetter will later retrieve the stored credentials to pull the chart
- if keychain != nil {
+ if loginOpt != nil {
err = ociChartRepo.Login(loginOpt)
if err != nil {
e := &serror.Event{
@@ -621,6 +648,17 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
opts := chart.BuildOptions{
ValuesFiles: obj.GetValuesFiles(),
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 {
opts.CachedChart = r.Storage.LocalPath(*artifact)
@@ -1029,7 +1067,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont
// If login options are configured, use them to login to the registry
// The OCIGetter will later retrieve the stored credentials to pull the chart
- if keychain != nil {
+ if loginOpt != nil {
err = ociChartRepo.Login(loginOpt)
if err != nil {
errs = append(errs, fmt.Errorf("failed to login to OCI chart repository for HelmRepository '%s': %w", repo.Name, err))
@@ -1238,6 +1276,11 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
if build.Complete() {
conditions.Delete(obj, sourcev1.FetchFailedCondition)
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 {
@@ -1253,6 +1296,10 @@ func observeChartBuild(obj *sourcev1.HelmChart, build *chart.Build, err error) {
case chart.ErrChartMetadataPatch, chart.ErrValuesFilesMerge, chart.ErrDependencyBuild, chart.ErrChartPackage:
conditions.Delete(obj, sourcev1.FetchFailedCondition)
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, buildErr.Reason.Reason, buildErr.Error())
default:
conditions.Delete(obj, sourcev1.BuildFailedCondition)
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, buildErr.Reason.Reason, buildErr.Error())
@@ -1289,3 +1336,51 @@ func chartRepoConfigErrorReturn(err error, obj *sourcev1.HelmChart) (sreconcile.
return sreconcile.ResultEmpty, e
}
}
+
+// getVerifyOptions returns the verify options for the given chart.
+func (r *HelmChartReconciler) makeVerifier(ctx context.Context, obj *sourcev1.HelmChart, auth authn.Authenticator, keychain authn.Keychain) (soci.Verifier, error) {
+ var publicKey []byte
+ verifyOpts := []remote.Option{}
+ if auth != nil {
+ verifyOpts = append(verifyOpts, remote.WithAuth(auth))
+ } else {
+ verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain))
+ }
+
+ // 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
+ }
+
+ switch obj.Spec.Verify.Provider {
+ case "cosign":
+ // we expect to find this key in the secret for cosign verification. We want to avoid
+ // having to look for a random public key.
+ if key, ok := pubSecret.Data[cosignSignatureKey]; ok {
+ publicKey = key
+ }
+ default:
+ }
+ }
+
+ switch obj.Spec.Verify.Provider {
+ case "cosign":
+ defaultCosignOciOpts := []soci.Options{
+ soci.WithRemoteOptions(verifyOpts...),
+ }
+ verifier, err := soci.NewCosignVerifier(ctx, append(defaultCosignOciOpts, soci.WithPublicKey(publicKey))...)
+ if err != nil {
+ return nil, err
+ }
+ return verifier, nil
+ default:
+ return nil, fmt.Errorf("unsupported verification provider: %s", obj.Spec.Verify.Provider)
+ }
+}
diff --git a/controllers/helmchart_controller_test.go b/controllers/helmchart_controller_test.go
index 631286bc1..d2f73f057 100644
--- a/controllers/helmchart_controller_test.go
+++ b/controllers/helmchart_controller_test.go
@@ -26,6 +26,7 @@ import (
"io/ioutil"
"net/http"
"os"
+ "path"
"path/filepath"
"reflect"
"strings"
@@ -34,6 +35,9 @@ import (
"github.com/darkowlzz/controller-check/status"
. "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"
"helm.sh/helm/v3/pkg/chart/loader"
helmreg "helm.sh/helm/v3/pkg/registry"
@@ -56,6 +60,7 @@ import (
serror "github.com/fluxcd/source-controller/internal/error"
"github.com/fluxcd/source-controller/internal/helm/chart"
"github.com/fluxcd/source-controller/internal/helm/registry"
+ "github.com/fluxcd/source-controller/internal/oci"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/internal/reconcile/summarize"
)
@@ -2217,6 +2222,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
+ 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
verify
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.
+verify
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.
+(Appears on: +HelmChartSpec, OCIRepositorySpec)
OCIRepositoryVerification verifies the authenticity of an OCI Artifact
diff --git a/internal/helm/chart/builder.go b/internal/helm/chart/builder.go index 76dc517c7..5be208d8c 100644 --- a/internal/helm/chart/builder.go +++ b/internal/helm/chart/builder.go @@ -113,6 +113,8 @@ type BuildOptions struct { // Force can be set to force the build of the chart, for example // because the list of ValuesFiles has changed. Force bool + // Verifier can be set to the verification of the chart. + Verify bool } // GetValuesFiles returns BuildOptions.ValuesFiles, except if it equals diff --git a/internal/helm/chart/builder_remote.go b/internal/helm/chart/builder_remote.go index d15e24299..20589472b 100644 --- a/internal/helm/chart/builder_remote.go +++ b/internal/helm/chart/builder_remote.go @@ -63,7 +63,7 @@ func NewRemoteBuilder(repository repository.Downloader) Builder { // After downloading the chart, it is only packaged if required due to BuildOptions // modifying the chart, otherwise the exact data as retrieved from the repository // is written to p, after validating it to be a chart. -func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) { +func (b *remoteChartBuilder) Build(ctx context.Context, ref Reference, p string, opts BuildOptions) (*Build, error) { remoteRef, ok := ref.(RemoteReference) if !ok { 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} } - res, result, err := b.downloadFromRepository(b.remote, remoteRef, opts) + res, result, err := b.downloadFromRepository(ctx, b.remote, remoteRef, opts) if err != nil { - return nil, &BuildError{Reason: ErrChartPull, Err: err} + return nil, err } if res == nil { return result, nil @@ -124,7 +124,7 @@ func (b *remoteChartBuilder) Build(_ context.Context, ref Reference, p string, o 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 cv, err := remote.GetChartVersion(remoteRef.Name, remoteRef.Version) if err != nil { @@ -132,6 +132,13 @@ func (b *remoteChartBuilder) downloadFromRepository(remote repository.Downloader 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) if err != nil { return nil, nil, err diff --git a/internal/helm/chart/errors.go b/internal/helm/chart/errors.go index dedff9e37..7b1b7f3b0 100644 --- a/internal/helm/chart/errors.go +++ b/internal/helm/chart/errors.go @@ -84,5 +84,6 @@ var ( ErrValuesFilesMerge = BuildErrorReason{Reason: "ValuesFilesError", Summary: "values files merge error"} ErrDependencyBuild = BuildErrorReason{Reason: "DependencyBuildError", Summary: "dependency build error"} ErrChartPackage = BuildErrorReason{Reason: "ChartPackageError", Summary: "chart package error"} + ErrChartVerification = BuildErrorReason{Reason: "ChartVerificationError", Summary: "chart verification error"} ErrUnknown = BuildErrorReason{Reason: "Unknown", Summary: "unknown build error"} ) diff --git a/internal/helm/repository/chart_repository.go b/internal/helm/repository/chart_repository.go index 282d49a5d..3997d5f3f 100644 --- a/internal/helm/repository/chart_repository.go +++ b/internal/helm/repository/chart_repository.go @@ -18,6 +18,7 @@ package repository import ( "bytes" + "context" "crypto/sha256" "crypto/tls" "encoding/hex" @@ -520,3 +521,12 @@ func (r *ChartRepository) RemoveCache() error { } 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 +} diff --git a/internal/helm/repository/oci_chart_repository.go b/internal/helm/repository/oci_chart_repository.go index a037e6b40..72af1bf88 100644 --- a/internal/helm/repository/oci_chart_repository.go +++ b/internal/helm/repository/oci_chart_repository.go @@ -18,6 +18,7 @@ package repository import ( "bytes" + "context" "crypto/tls" "fmt" "net/url" @@ -32,7 +33,10 @@ import ( "helm.sh/helm/v3/pkg/repo" "github.com/Masterminds/semver/v3" + "github.com/google/go-containerregistry/pkg/name" + "github.com/fluxcd/pkg/version" + "github.com/fluxcd/source-controller/internal/oci" "github.com/fluxcd/source-controller/internal/transport" ) @@ -63,12 +67,24 @@ type OCIChartRepository struct { RegistryClient RegistryClient // credentialsFile is a temporary credentials file to use while downloading tags or charts from a registry. credentialsFile string + + // verifier is a chart verifier + // If attempts to verify a chart, and if the verifier is nil, an error is returned. + verifier oci.Verifier } // OCIChartRepositoryOption is a function that can be passed to NewOCIChartRepository // to configure an OCIChartRepository. type OCIChartRepositoryOption func(*OCIChartRepository) error +// WithVerifier returns a ChartRepositoryOption that will set the chart verifier +func WithVerifier(verifier oci.Verifier) OCIChartRepositoryOption { + return func(r *OCIChartRepository) error { + r.verifier = verifier + return nil + } +} + // WithOCIRegistryClient returns a ChartRepositoryOption that will set the registry client func WithOCIRegistryClient(client RegistryClient) OCIChartRepositoryOption { return func(r *OCIChartRepository) error { @@ -215,7 +231,6 @@ func (r *OCIChartRepository) DownloadChart(chart *repo.ChartVersion) (*bytes.Buf // Login attempts to login to the OCI registry. // It returns an error on failure. func (r *OCIChartRepository) Login(opts ...registry.LoginOption) error { - // Get login credentials from keychain err := r.RegistryClient.Login(r.URL.Host, opts...) if err != nil { return err @@ -297,3 +312,28 @@ func getLastMatchingVersionOrConstraint(cvs []string, ver string) (string, error 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 r.verifier == nil { + return fmt.Errorf("no verifier 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 + if err := r.verifier.Verify(ctx, ref); err != nil { + return fmt.Errorf("failed to verify %s: %w", chart.URLs[0], err) + } + + return nil +} diff --git a/internal/helm/repository/repository.go b/internal/helm/repository/repository.go index 4c8cb7ff8..5fdf62bfa 100644 --- a/internal/helm/repository/repository.go +++ b/internal/helm/repository/repository.go @@ -18,6 +18,7 @@ package repository import ( "bytes" + "context" "helm.sh/helm/v3/pkg/repo" ) @@ -29,6 +30,8 @@ type Downloader interface { GetChartVersion(name, version string) (*repo.ChartVersion, error) // DownloadChart downloads a chart from the remote Helm repository or OCI Helm repository. 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, // and calling garbage collector to remove unused files. Clear() error diff --git a/internal/oci/verifier.go b/internal/oci/verifier.go index b8d9c5d49..3d7a0df9b 100644 --- a/internal/oci/verifier.go +++ b/internal/oci/verifier.go @@ -34,13 +34,18 @@ import ( "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) error +} + // options is a struct that holds options for verifier. type options struct { PublicKey []byte 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) // 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. -type Verifier struct { +// CosignVerifier is a struct which is responsible for executing verification logic. +type CosignVerifier struct { opts *cosign.CheckOpts } -// NewVerifier initializes a new Verifier. -func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) { +// NewCosignVerifier initializes a new CosignVerifier. +func NewCosignVerifier(ctx context.Context, opts ...Options) (*CosignVerifier, error) { o := options{} for _, opt := range opts { opt(&o) @@ -117,12 +122,26 @@ func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) { checkOpts.RekorClient = rc } - return &Verifier{ + return &CosignVerifier{ opts: checkOpts, }, nil } // 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) } + +// Verify verifies the authenticity of the given ref OCI image. +func (v *CosignVerifier) Verify(ctx context.Context, ref name.Reference) error { + signatures, _, err := v.VerifyImageSignatures(ctx, ref) + if err != nil { + return err + } + + if len(signatures) == 0 { + return fmt.Errorf("no matching signatures were found for '%s'", ref.Name()) + } + + return nil +}