Skip to content

Commit 18d6a7a

Browse files
stefanprodandeveloper-guy
authored andcommitted
Merge pull request fluxcd#886 from pjbgf/fuzz-update
fuzz: Fuzz optimisations Signed-off-by: Batuhan Apaydın <[email protected]>
2 parents 6d479e5 + 976f4bb commit 18d6a7a

File tree

11 files changed

+1332
-83
lines changed

11 files changed

+1332
-83
lines changed

.github/workflows/cifuzz.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ jobs:
2121
uses: actions/setup-go@v3
2222
with:
2323
go-version: 1.18.x
24+
- id: go-env
25+
run: |
26+
echo "::set-output name=go-mod-cache::$(go env GOMODCACHE)"
2427
- name: Restore Go cache
2528
uses: actions/cache@v3
2629
with:
27-
path: /home/runner/work/_temp/_github_home/go/pkg/mod
30+
path: ${{ steps.go-env.outputs.go-mod-cache }}
2831
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
2932
restore-keys: |
30-
${{ runner.os }}-go-
33+
${{ runner.os }}-go
3134
- name: Smoke test Fuzzers
3235
run: make fuzz-smoketest

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ fuzz-build: $(LIBGIT2)
234234
rm -rf $(BUILD_DIR)/fuzz/
235235
mkdir -p $(BUILD_DIR)/fuzz/out/
236236

237-
docker build . --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder
237+
docker build . --pull --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder
238238
docker run --rm \
239239
-e FUZZING_LANGUAGE=go -e SANITIZER=address \
240240
-e CIFUZZ_DEBUG='True' -e OSS_FUZZ_PROJECT_NAME=fluxcd \
@@ -244,6 +244,7 @@ fuzz-build: $(LIBGIT2)
244244
fuzz-smoketest: fuzz-build
245245
docker run --rm \
246246
-v "$(BUILD_DIR)/fuzz/out":/out \
247+
-v "$(shell go env GOMODCACHE):/root/go/pkg/mod" \
247248
-v "$(shell pwd)/tests/fuzz/oss_fuzz_run.sh":/runner.sh \
248249
local-fuzzing:latest \
249250
bash -c "/runner.sh"

api/v1beta2/condition_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ const (
7171
// required fields, or the provided credentials do not match.
7272
AuthenticationFailedReason string = "AuthenticationFailed"
7373

74+
// OCISourceSignatureVerifyFailedReason signals that the Source's verification
75+
// check failed.
76+
OCISourceSignatureVerifyFailedReason string = "OCISourceSignatureVerifyFailedReason"
77+
7478
// DirCreationFailedReason signals a failure caused by a directory creation
7579
// operation.
7680
DirCreationFailedReason string = "DirectoryCreationFailed"

api/v1beta2/ocirepository_types.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ type OCIRepositorySpec struct {
7878
// +optional
7979
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
8080

81+
// Verify contains the secret name containing thetrusted public keys
82+
// used to verify the signature and specifies which provider to use to check
83+
// whether OCI image is authentic.
84+
// +optional
85+
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
86+
8187
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
8288
// the image pull if the service account has attached pull secrets. For more information:
8389
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
@@ -152,11 +158,13 @@ type OCILayerSelector struct {
152158
type OCIRepositoryVerification struct {
153159
// Provider specifies the technology used to sign the OCI Artifact.
154160
// +kubebuilder:validation:Enum=cosign
161+
// +kubebuilder:default:=cosign
155162
Provider string `json:"provider"`
156163

157164
// SecretRef specifies the Kubernetes Secret containing the
158165
// trusted public keys.
159-
SecretRef meta.LocalObjectReference `json:"secretRef"`
166+
// +optional
167+
SecretRef *meta.LocalObjectReference `json:"secretRef"`
160168
}
161169

162170
// OCIRepositoryStatus defines the observed state of OCIRepository

api/v1beta2/zz_generated.deepcopy.go

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,30 @@ spec:
146146
on a remote container registry.
147147
pattern: ^oci://.*$
148148
type: string
149+
verify:
150+
description: Verify specifies which provider to use to check whether
151+
OCI image is authentic.
152+
properties:
153+
provider:
154+
default: cosign
155+
description: Provider specifies the technology used to sign the
156+
OCI Artifact.
157+
enum:
158+
- cosign
159+
type: string
160+
secretRef:
161+
description: SecretRef specifies the Kubernetes Secret containing
162+
the trusted public keys.
163+
properties:
164+
name:
165+
description: Name of the referent.
166+
type: string
167+
required:
168+
- name
169+
type: object
170+
required:
171+
- provider
172+
type: object
149173
required:
150174
- interval
151175
- url

controllers/ocirepository_controller.go

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"strings"
2929
"time"
3030

31+
soci "github.com/fluxcd/source-controller/internal/oci"
32+
3133
"github.com/Masterminds/semver/v3"
3234
"github.com/google/go-containerregistry/pkg/authn"
3335
"github.com/google/go-containerregistry/pkg/authn/k8schain"
@@ -39,6 +41,7 @@ import (
3941
"k8s.io/apimachinery/pkg/runtime"
4042
"k8s.io/apimachinery/pkg/types"
4143
"k8s.io/apimachinery/pkg/util/sets"
44+
"k8s.io/apimachinery/pkg/util/uuid"
4245
kuberecorder "k8s.io/client-go/tools/record"
4346

4447
ctrl "sigs.k8s.io/controller-runtime"
@@ -159,7 +162,9 @@ func (r *OCIRepositoryReconciler) SetupWithManagerAndOptions(mgr ctrl.Manager, o
159162

160163
func (r *OCIRepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) {
161164
start := time.Now()
162-
log := ctrl.LoggerFrom(ctx)
165+
log := ctrl.LoggerFrom(ctx).
166+
// Sets a reconcile ID to correlate logs from all suboperations.
167+
WithValues("reconcileID", uuid.NewUUID())
163168

164169
// logger will be associated to the new context that is
165170
// returned from ctrl.LoggerInto.
@@ -298,7 +303,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
298303
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
299304
defer cancel()
300305

301-
options := r.craneOptions(ctxTimeout, obj.Spec.Insecure)
306+
options := r.craneOptions(ctxTimeout)
302307

303308
// Generate the registry credential keychain either from static credentials or using cloud OIDC
304309
keychain, err := r.keychain(ctx, obj)
@@ -412,6 +417,19 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
412417

413418
// Extract the content of the first artifact layer
414419
if !obj.GetArtifact().HasRevision(revision) {
420+
provider := obj.Spec.Verify.Provider
421+
err := r.verifyOCISourceSignature(ctx, obj, url)
422+
if err != nil {
423+
e := serror.NewGeneric(
424+
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, provider, err),
425+
sourcev1.OCISourceSignatureVerifyFailedReason,
426+
)
427+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
428+
return sreconcile.ResultEmpty, e
429+
} else {
430+
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "OCI Image %s with digest %s verified.", url, imgDigest)
431+
}
432+
415433
layers, err := img.Layers()
416434
if err != nil {
417435
e := serror.NewGeneric(
@@ -488,6 +506,82 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
488506
return sreconcile.ResultSuccess, nil
489507
}
490508

509+
// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries to keyful approach
510+
// by looking at whether the given secret exists. Then, if it does not exist, it pushes a keyless approach for verification.
511+
func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string) error {
512+
// Verify the image
513+
if obj.Spec.Verify != nil {
514+
provider := obj.Spec.Verify.Provider
515+
switch provider {
516+
case "cosign":
517+
// get the public keys from the given secret
518+
secretRef := obj.Spec.Verify.SecretRef
519+
520+
// Generate the registry credential keychain either from static credentials or using cloud OIDC
521+
keychain, err := r.keychain(ctx, obj)
522+
if err != nil {
523+
return err
524+
}
525+
authnKeychain := soci.WithAuthnKeychain(keychain)
526+
527+
ref, err := name.ParseReference(url)
528+
if err != nil {
529+
return err
530+
}
531+
532+
if secretRef != nil {
533+
certSecretName := types.NamespacedName{
534+
Namespace: obj.Namespace,
535+
Name: secretRef.Name,
536+
}
537+
538+
var pubSecret corev1.Secret
539+
if err := r.Get(ctx, certSecretName, &pubSecret); err != nil {
540+
return err
541+
}
542+
543+
// traverse all public keys and try to verify the signature
544+
// this is brute-force approach, but it is ok for now
545+
for k, data := range pubSecret.Data {
546+
// search for public keys in the secret
547+
if strings.HasSuffix(k, ".pub") {
548+
verifier, err := soci.New(soci.WithPublicKey(data), authnKeychain)
549+
if err != nil {
550+
return err
551+
}
552+
553+
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
554+
if err != nil {
555+
ctrl.LoggerFrom(ctx).Error(err, "failed to verify image %s signature with key %s", url, k)
556+
continue
557+
}
558+
559+
if signatures != nil {
560+
return nil
561+
}
562+
}
563+
}
564+
} else {
565+
verifier, err := soci.New(authnKeychain)
566+
if err != nil {
567+
return err
568+
}
569+
570+
signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
571+
if err != nil {
572+
return err
573+
}
574+
575+
if len(signatures) > 0 {
576+
return nil
577+
}
578+
}
579+
return nil
580+
}
581+
}
582+
return nil
583+
}
584+
491585
// parseRepositoryURL validates and extracts the repository URL.
492586
func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) {
493587
if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) {
@@ -655,7 +749,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
655749
tlsConfig.RootCAs = syscerts
656750
}
657751
return transport, nil
658-
659752
}
660753

661754
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
@@ -681,16 +774,11 @@ func (r *OCIRepositoryReconciler) oidcAuth(ctx context.Context, obj *sourcev1.OC
681774

682775
// craneOptions sets the auth headers, timeout and user agent
683776
// for all operations against remote container registries.
684-
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context, insecure bool) []crane.Option {
777+
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Option {
685778
options := []crane.Option{
686779
crane.WithContext(ctx),
687780
crane.WithUserAgent(oci.UserAgent),
688781
}
689-
690-
if insecure {
691-
options = append(options, crane.Insecure)
692-
}
693-
694782
return options
695783
}
696784

@@ -887,7 +975,8 @@ func (r *OCIRepositoryReconciler) garbageCollect(ctx context.Context, obj *sourc
887975
// that this is a simple log. While the debug log contains complete details
888976
// about the event.
889977
func (r *OCIRepositoryReconciler) eventLogf(ctx context.Context,
890-
obj runtime.Object, eventType string, reason string, messageFmt string, args ...interface{}) {
978+
obj runtime.Object, eventType, reason, messageFmt string, args ...interface{},
979+
) {
891980
msg := fmt.Sprintf(messageFmt, args...)
892981
// Log and emit event.
893982
if eventType == corev1.EventTypeWarning {

0 commit comments

Comments
 (0)