Refactor internal OCI package

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
This commit is contained in:
Stefan Prodan 2022-09-20 12:40:05 +03:00
parent 21af88fbea
commit 082028e115
No known key found for this signature in database
GPG Key ID: 3299AEB0E4085BAF
6 changed files with 52 additions and 48 deletions

View File

@ -1,6 +1,6 @@
# Image URL to use all building/pushing image targets # Image URL to use all building/pushing image targets
IMG ?= fluxcd/source-controller IMG ?= localhost:5050/source-controller
TAG ?= latest TAG ?= test1
# Base image used to build the Go binary # Base image used to build the Go binary
LIBGIT2_IMG ?= ghcr.io/fluxcd/golang-with-libgit2-only LIBGIT2_IMG ?= ghcr.io/fluxcd/golang-with-libgit2-only
@ -14,9 +14,9 @@ GO_TEST_PREFIX ?=
# Allows for defining additional Docker buildx arguments, # Allows for defining additional Docker buildx arguments,
# e.g. '--push'. # e.g. '--push'.
BUILD_ARGS ?= BUILD_ARGS ?= --load
# Architectures to build images for # Architectures to build images for
BUILD_PLATFORMS ?= linux/amd64,linux/arm64,linux/arm/v7 BUILD_PLATFORMS ?= linux/arm64
# Go additional tag arguments, e.g. 'integration', # Go additional tag arguments, e.g. 'integration',
# this is append to the tag arguments required for static builds # this is append to the tag arguments required for static builds

View File

@ -311,7 +311,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
} }
options = append(options, crane.WithAuthFromKeychain(keychain)) options = append(options, crane.WithAuthFromKeychain(keychain))
if _, ok := keychain.(util.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok { if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
auth, authErr := oidcAuth(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 := serror.NewGeneric( e := serror.NewGeneric(
@ -409,22 +409,28 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
} }
}() }()
// Verify artifact
if obj.Spec.Verify == nil {
// Remove old observations if verification was disabled
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
} else if !obj.GetArtifact().HasRevision(revision) || conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation {
provider := obj.Spec.Verify.Provider
err := r.verifyOCISourceSignature(ctx, obj, url, keychain)
if err != nil {
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())
conditions.MarkFalse(obj, meta.ReconcilingCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest %s", revision)
}
// Extract the content of the first artifact layer // Extract the content of the first artifact layer
if !obj.GetArtifact().HasRevision(revision) { if !obj.GetArtifact().HasRevision(revision) {
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
err := r.verifyOCISourceSignature(ctx, obj, url, keychain)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to verify OCI image signature '%s' using provider '%s': %w", url, provider, err),
sourcev1.VerificationError,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image %s with digest %s verified.", url, revision)
}
layers, err := img.Layers() layers, err := img.Layers()
if err != nil { if err != nil {
e := serror.NewGeneric( e := serror.NewGeneric(
@ -512,7 +518,6 @@ func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context,
case "cosign": case "cosign":
defaultCosignOciOpts := []soci.Options{ defaultCosignOciOpts := []soci.Options{
soci.WithAuthnKeychain(keychain), soci.WithAuthnKeychain(keychain),
soci.WithContext(ctxTimeout),
} }
ref, err := name.ParseReference(url) ref, err := name.ParseReference(url)
@ -536,12 +541,12 @@ func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context,
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.New(append(defaultCosignOciOpts, soci.WithPublicKey(data))...) verifier, err := soci.NewVerifier(ctxTimeout, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
if err != nil { if err != nil {
return err return err
} }
signatures, _, err := verifier.VerifyImageSignatures(ref) signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
if err != nil { if err != nil {
continue continue
} }
@ -562,12 +567,12 @@ func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context,
// 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 approach") ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless approach")
verifier, err := soci.New(defaultCosignOciOpts...) verifier, err := soci.NewVerifier(ctxTimeout, defaultCosignOciOpts...)
if err != nil { if err != nil {
return err return err
} }
signatures, _, err := verifier.VerifyImageSignatures(ref) signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
if err != nil { if err != nil {
return err return err
} }
@ -689,7 +694,7 @@ func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OC
// if no pullsecrets available return an AnonymousKeychain // if no pullsecrets available return an AnonymousKeychain
if len(pullSecretNames) == 0 { if len(pullSecretNames) == 0 {
return util.Anonymous{}, nil return soci.Anonymous{}, nil
} }
// lookup image pull secrets // lookup image pull secrets

View File

@ -1042,22 +1042,22 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
assertConditions: []metav1.Condition{ assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"), *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"), *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "OCI image <url> with digest <digest> verified."), *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest <digest>"),
}, },
}, },
{ {
name: "not signed image should not pass verification", name: "unsigned image should not pass verification",
reference: &sourcev1.OCIRepositoryRef{ reference: &sourcev1.OCIRepositoryRef{
Tag: "6.1.5", Tag: "6.1.5",
}, },
digest: img5.digest.Hex, digest: img5.digest.Hex,
wantErr: true, wantErr: true,
wantErrMsg: "failed to verify OCI image signature '<url>' using provider 'cosign': no matching signatures were found for '<url>", wantErrMsg: "failed to verify the signature using provider 'cosign': no matching signatures were found for '<url>'",
want: sreconcile.ResultEmpty, want: sreconcile.ResultEmpty,
assertConditions: []metav1.Condition{ assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"), *conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"), *conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify OCI image signature '<url>' using provider '<provider>': no matching signatures were found for '<url>'"), *conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<provider>': no matching signatures were found for '<url>'"),
}, },
}, },
} }

View File

@ -455,7 +455,7 @@ data:
key2.pub: <BASE64> key2.pub: <BASE64>
``` ```
Note that the keys must have the `.pub` extension for Flux to make user of them. Note that the keys must have the `.pub` extension for Flux to make use of them.
#### Keyless verification #### Keyless verification
@ -482,7 +482,7 @@ The controller verifies the signatures using the Fulcio root CA and the Rekor
instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/).
Note that keyless verification is an **experimental feature**, using Note that keyless verification is an **experimental feature**, using
custom root CAs or self-hosted Rekor instances are not currency supported. custom root CAs or self-hosted Rekor instances are not currently supported.
### Suspend ### Suspend
@ -839,6 +839,14 @@ and is only present on the OCIRepository while the status value is `"True"`.
There may be more arbitrary values for the `reason` field to provide accurate There may be more arbitrary values for the `reason` field to provide accurate
reason for a condition. reason for a condition.
In addition to the above Condition types, when the signature
[verification](#verification) fails. A condition with
the following attributes is added to the GitRepository's `.status.conditions`:
- `type: SourceVerified`
- `status: "False"`
- `reason: VerificationError`
While the OCIRepository has one or more of these Conditions, the controller While the OCIRepository has one or more of these Conditions, the controller
will continue to attempt to produce an Artifact for the resource with an will continue to attempt to produce an Artifact for the resource with an
exponential backoff, until it succeeds and the OCIRepository is marked as exponential backoff, until it succeeds and the OCIRepository is marked as

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package util package oci
import "github.com/google/go-containerregistry/pkg/authn" import "github.com/google/go-containerregistry/pkg/authn"

View File

@ -38,7 +38,6 @@ import (
type options struct { type options struct {
PublicKey []byte PublicKey []byte
Keychain authn.Keychain Keychain authn.Keychain
Context context.Context
} }
// Options is a function that configures the options applied to a Verifier. // Options is a function that configures the options applied to a Verifier.
@ -57,20 +56,13 @@ func WithAuthnKeychain(keychain authn.Keychain) Options {
} }
} }
func WithContext(ctx context.Context) Options {
return func(opts *options) {
opts.Context = ctx
}
}
// Verifier is a struct which is responsible for executing verification logic. // Verifier is a struct which is responsible for executing verification logic.
type Verifier struct { type Verifier struct {
opts *cosign.CheckOpts opts *cosign.CheckOpts
context context.Context
} }
// New initializes a new Verifier. // NewVerifier initializes a new Verifier.
func New(opts ...Options) (*Verifier, error) { func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) {
o := options{} o := options{}
for _, opt := range opts { for _, opt := range opts {
opt(&o) opt(&o)
@ -79,7 +71,7 @@ func New(opts ...Options) (*Verifier, error) {
checkOpts := &cosign.CheckOpts{} checkOpts := &cosign.CheckOpts{}
ro := coptions.RegistryOptions{} ro := coptions.RegistryOptions{}
co, err := ro.ClientOpts(o.Context) co, err := ro.ClientOpts(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -124,12 +116,11 @@ func New(opts ...Options) (*Verifier, error) {
} }
return &Verifier{ return &Verifier{
opts: checkOpts, opts: checkOpts,
context: o.Context,
}, 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(ref name.Reference) ([]oci.Signature, bool, error) { func (v *Verifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) {
return cosign.VerifyImageSignatures(v.context, ref, v.opts) return cosign.VerifyImageSignatures(ctx, ref, v.opts)
} }