Skip to content

Commit

Permalink
Merge pull request fluxcd#886 from pjbgf/fuzz-update
Browse files Browse the repository at this point in the history
fuzz: Fuzz optimisations
Signed-off-by: Batuhan Apaydın <[email protected]>
  • Loading branch information
stefanprodan authored and developer-guy committed Sep 4, 2022
2 parents 6d479e5 + 976f4bb commit 18d6a7a
Show file tree
Hide file tree
Showing 11 changed files with 1,332 additions and 83 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/cifuzz.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ jobs:
uses: actions/setup-go@v3
with:
go-version: 1.18.x
- id: go-env
run: |
echo "::set-output name=go-mod-cache::$(go env GOMODCACHE)"
- name: Restore Go cache
uses: actions/cache@v3
with:
path: /home/runner/work/_temp/_github_home/go/pkg/mod
path: ${{ steps.go-env.outputs.go-mod-cache }}
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
${{ runner.os }}-go
- name: Smoke test Fuzzers
run: make fuzz-smoketest
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ fuzz-build: $(LIBGIT2)
rm -rf $(BUILD_DIR)/fuzz/
mkdir -p $(BUILD_DIR)/fuzz/out/

docker build . --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder
docker build . --pull --tag local-fuzzing:latest -f tests/fuzz/Dockerfile.builder
docker run --rm \
-e FUZZING_LANGUAGE=go -e SANITIZER=address \
-e CIFUZZ_DEBUG='True' -e OSS_FUZZ_PROJECT_NAME=fluxcd \
Expand All @@ -244,6 +244,7 @@ fuzz-build: $(LIBGIT2)
fuzz-smoketest: fuzz-build
docker run --rm \
-v "$(BUILD_DIR)/fuzz/out":/out \
-v "$(shell go env GOMODCACHE):/root/go/pkg/mod" \
-v "$(shell pwd)/tests/fuzz/oss_fuzz_run.sh":/runner.sh \
local-fuzzing:latest \
bash -c "/runner.sh"
Expand Down
4 changes: 4 additions & 0 deletions api/v1beta2/condition_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ const (
// required fields, or the provided credentials do not match.
AuthenticationFailedReason string = "AuthenticationFailed"

// OCISourceSignatureVerifyFailedReason signals that the Source's verification
// check failed.
OCISourceSignatureVerifyFailedReason string = "OCISourceSignatureVerifyFailedReason"

// DirCreationFailedReason signals a failure caused by a directory creation
// operation.
DirCreationFailedReason string = "DirectoryCreationFailed"
Expand Down
10 changes: 9 additions & 1 deletion api/v1beta2/ocirepository_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ type OCIRepositorySpec struct {
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`

// Verify contains the secret name containing thetrusted public keys
// used to verify the signature and specifies which provider to use to check
// whether OCI image is authentic.
// +optional
Verify *OCIRepositoryVerification `json:"verify,omitempty"`

// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
// the image pull if the service account has attached pull secrets. For more information:
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
Expand Down Expand Up @@ -152,11 +158,13 @@ type OCILayerSelector struct {
type OCIRepositoryVerification struct {
// Provider specifies the technology used to sign the OCI Artifact.
// +kubebuilder:validation:Enum=cosign
// +kubebuilder:default:=cosign
Provider string `json:"provider"`

// SecretRef specifies the Kubernetes Secret containing the
// trusted public keys.
SecretRef meta.LocalObjectReference `json:"secretRef"`
// +optional
SecretRef *meta.LocalObjectReference `json:"secretRef"`
}

// OCIRepositoryStatus defines the observed state of OCIRepository
Expand Down
11 changes: 10 additions & 1 deletion api/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions config/crd/bases/source.toolkit.fluxcd.io_ocirepositories.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,30 @@ spec:
on a remote container registry.
pattern: ^oci://.*$
type: string
verify:
description: Verify 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
required:
- interval
- url
Expand Down
109 changes: 99 additions & 10 deletions controllers/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import (
"strings"
"time"

soci "github.com/fluxcd/source-controller/internal/oci"

"github.com/Masterminds/semver/v3"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/authn/k8schain"
Expand All @@ -39,6 +41,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/uuid"
kuberecorder "k8s.io/client-go/tools/record"

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

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

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

options := r.craneOptions(ctxTimeout, obj.Spec.Insecure)
options := r.craneOptions(ctxTimeout)

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

// Extract the content of the first artifact layer
if !obj.GetArtifact().HasRevision(revision) {
provider := obj.Spec.Verify.Provider
err := r.verifyOCISourceSignature(ctx, obj, url)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to verify '%s' using provider '%s': %w", url, provider, err),
sourcev1.OCISourceSignatureVerifyFailedReason,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
} else {
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "OCI Image %s with digest %s verified.", url, imgDigest)
}

layers, err := img.Layers()
if err != nil {
e := serror.NewGeneric(
Expand Down Expand Up @@ -488,6 +506,82 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
return sreconcile.ResultSuccess, nil
}

// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries to keyful approach
// by looking at whether the given secret exists. Then, if it does not exist, it pushes a keyless approach for verification.
func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string) error {
// Verify the image
if obj.Spec.Verify != nil {
provider := obj.Spec.Verify.Provider
switch provider {
case "cosign":
// get the public keys from the given secret
secretRef := obj.Spec.Verify.SecretRef

// Generate the registry credential keychain either from static credentials or using cloud OIDC
keychain, err := r.keychain(ctx, obj)
if err != nil {
return err
}
authnKeychain := soci.WithAuthnKeychain(keychain)

ref, err := name.ParseReference(url)
if err != nil {
return err
}

if 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 err
}

// traverse all public keys and try to verify the signature
// this is brute-force approach, but it is ok for now
for k, data := range pubSecret.Data {
// search for public keys in the secret
if strings.HasSuffix(k, ".pub") {
verifier, err := soci.New(soci.WithPublicKey(data), authnKeychain)
if err != nil {
return err
}

signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
if err != nil {
ctrl.LoggerFrom(ctx).Error(err, "failed to verify image %s signature with key %s", url, k)
continue
}

if signatures != nil {
return nil
}
}
}
} else {
verifier, err := soci.New(authnKeychain)
if err != nil {
return err
}

signatures, _, err := verifier.VerifyImageSignatures(ctx, ref)
if err != nil {
return err
}

if len(signatures) > 0 {
return nil
}
}
return nil
}
}
return nil
}

// parseRepositoryURL validates and extracts the repository URL.
func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) {
if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) {
Expand Down Expand Up @@ -655,7 +749,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
tlsConfig.RootCAs = syscerts
}
return transport, nil

}

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

// craneOptions sets the auth headers, timeout and user agent
// for all operations against remote container registries.
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context, insecure bool) []crane.Option {
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context) []crane.Option {
options := []crane.Option{
crane.WithContext(ctx),
crane.WithUserAgent(oci.UserAgent),
}

if insecure {
options = append(options, crane.Insecure)
}

return options
}

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

0 comments on commit 18d6a7a

Please sign in to comment.