@@ -29,6 +29,7 @@ import (
29
29
"time"
30
30
31
31
"github.com/Masterminds/semver/v3"
32
+ soci "github.com/fluxcd/source-controller/internal/oci"
32
33
"github.com/google/go-containerregistry/pkg/authn"
33
34
"github.com/google/go-containerregistry/pkg/authn/k8schain"
34
35
"github.com/google/go-containerregistry/pkg/crane"
@@ -75,6 +76,7 @@ var ociRepositoryReadyCondition = summarize.Conditions{
75
76
sourcev1 .FetchFailedCondition ,
76
77
sourcev1 .ArtifactOutdatedCondition ,
77
78
sourcev1 .ArtifactInStorageCondition ,
79
+ sourcev1 .SourceVerifiedCondition ,
78
80
meta .ReadyCondition ,
79
81
meta .ReconcilingCondition ,
80
82
meta .StalledCondition ,
@@ -84,6 +86,7 @@ var ociRepositoryReadyCondition = summarize.Conditions{
84
86
sourcev1 .FetchFailedCondition ,
85
87
sourcev1 .ArtifactOutdatedCondition ,
86
88
sourcev1 .ArtifactInStorageCondition ,
89
+ sourcev1 .SourceVerifiedCondition ,
87
90
meta .StalledCondition ,
88
91
meta .ReconcilingCondition ,
89
92
},
@@ -308,7 +311,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
308
311
}
309
312
options = append (options , crane .WithAuthFromKeychain (keychain ))
310
313
311
- if _ , ok := keychain .(util .Anonymous ); obj .Spec .Provider != sourcev1 .GenericOCIProvider && ok {
314
+ if _ , ok := keychain .(soci .Anonymous ); obj .Spec .Provider != sourcev1 .GenericOCIProvider && ok {
312
315
auth , authErr := oidcAuth (ctxTimeout , obj .Spec .URL , obj .Spec .Provider )
313
316
if authErr != nil && ! errors .Is (authErr , oci .ErrUnconfiguredProvider ) {
314
317
e := serror .NewGeneric (
@@ -406,6 +409,33 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
406
409
}
407
410
}()
408
411
412
+ // Verify artifact if:
413
+ // - the upstream digest differs from the one in storage (revision drift)
414
+ // - the OCIRepository spec has changed (generation drift)
415
+ // - the previous reconciliation resulted in a failed artifact verification (retry with exponential backoff)
416
+ if obj .Spec .Verify == nil {
417
+ // Remove old observations if verification was disabled
418
+ conditions .Delete (obj , sourcev1 .SourceVerifiedCondition )
419
+ } else if ! obj .GetArtifact ().HasRevision (revision ) ||
420
+ conditions .GetObservedGeneration (obj , sourcev1 .SourceVerifiedCondition ) != obj .Generation ||
421
+ conditions .IsFalse (obj , sourcev1 .SourceVerifiedCondition ) {
422
+ err := r .verifyOCISourceSignature (ctx , obj , url , keychain )
423
+ if err != nil {
424
+ provider := obj .Spec .Verify .Provider
425
+ if obj .Spec .Verify .SecretRef == nil {
426
+ provider = fmt .Sprintf ("%s keyless" , provider )
427
+ }
428
+ e := serror .NewGeneric (
429
+ fmt .Errorf ("failed to verify the signature using provider '%s': %w" , provider , err ),
430
+ sourcev1 .VerificationError ,
431
+ )
432
+ conditions .MarkFalse (obj , sourcev1 .SourceVerifiedCondition , e .Reason , e .Err .Error ())
433
+ return sreconcile .ResultEmpty , e
434
+ }
435
+
436
+ conditions .MarkTrue (obj , sourcev1 .SourceVerifiedCondition , meta .SucceededReason , "verified signature of digest %s" , revision )
437
+ }
438
+
409
439
// Extract the content of the first artifact layer
410
440
if ! obj .GetArtifact ().HasRevision (revision ) {
411
441
layers , err := img .Layers ()
@@ -484,6 +514,86 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
484
514
return sreconcile .ResultSuccess , nil
485
515
}
486
516
517
+ // verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries using a key
518
+ // if a secret with a valid public key is provided. If not, it falls back to a keyless approach for verification.
519
+ func (r * OCIRepositoryReconciler ) verifyOCISourceSignature (ctx context.Context , obj * sourcev1.OCIRepository , url string , keychain authn.Keychain ) error {
520
+ ctxTimeout , cancel := context .WithTimeout (ctx , obj .Spec .Timeout .Duration )
521
+ defer cancel ()
522
+
523
+ provider := obj .Spec .Verify .Provider
524
+ switch provider {
525
+ case "cosign" :
526
+ defaultCosignOciOpts := []soci.Options {
527
+ soci .WithAuthnKeychain (keychain ),
528
+ }
529
+
530
+ ref , err := name .ParseReference (url )
531
+ if err != nil {
532
+ return err
533
+ }
534
+
535
+ // get the public keys from the given secret
536
+ if secretRef := obj .Spec .Verify .SecretRef ; secretRef != nil {
537
+ certSecretName := types.NamespacedName {
538
+ Namespace : obj .Namespace ,
539
+ Name : secretRef .Name ,
540
+ }
541
+
542
+ var pubSecret corev1.Secret
543
+ if err := r .Get (ctxTimeout , certSecretName , & pubSecret ); err != nil {
544
+ return err
545
+ }
546
+
547
+ signatureVerified := false
548
+ for k , data := range pubSecret .Data {
549
+ // search for public keys in the secret
550
+ if strings .HasSuffix (k , ".pub" ) {
551
+ verifier , err := soci .NewVerifier (ctxTimeout , append (defaultCosignOciOpts , soci .WithPublicKey (data ))... )
552
+ if err != nil {
553
+ return err
554
+ }
555
+
556
+ signatures , _ , err := verifier .VerifyImageSignatures (ctxTimeout , ref )
557
+ if err != nil {
558
+ continue
559
+ }
560
+
561
+ if signatures != nil {
562
+ signatureVerified = true
563
+ break
564
+ }
565
+ }
566
+ }
567
+
568
+ if ! signatureVerified {
569
+ return fmt .Errorf ("no matching signatures were found for '%s'" , url )
570
+ }
571
+
572
+ return nil
573
+ }
574
+
575
+ // if no secret is provided, try keyless verification
576
+ ctrl .LoggerFrom (ctx ).Info ("no secret reference is provided, trying to verify the image using keyless method" )
577
+ verifier , err := soci .NewVerifier (ctxTimeout , defaultCosignOciOpts ... )
578
+ if err != nil {
579
+ return err
580
+ }
581
+
582
+ signatures , _ , err := verifier .VerifyImageSignatures (ctxTimeout , ref )
583
+ if err != nil {
584
+ return err
585
+ }
586
+
587
+ if len (signatures ) > 0 {
588
+ return nil
589
+ }
590
+
591
+ return fmt .Errorf ("no matching signatures were found for '%s'" , url )
592
+ }
593
+
594
+ return nil
595
+ }
596
+
487
597
// parseRepositoryURL validates and extracts the repository URL.
488
598
func (r * OCIRepositoryReconciler ) parseRepositoryURL (obj * sourcev1.OCIRepository ) (string , error ) {
489
599
if ! strings .HasPrefix (obj .Spec .URL , sourcev1 .OCIRepositoryPrefix ) {
@@ -591,7 +701,7 @@ func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OC
591
701
592
702
// if no pullsecrets available return an AnonymousKeychain
593
703
if len (pullSecretNames ) == 0 {
594
- return util .Anonymous {}, nil
704
+ return soci .Anonymous {}, nil
595
705
}
596
706
597
707
// lookup image pull secrets
@@ -651,7 +761,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
651
761
tlsConfig .RootCAs = syscerts
652
762
}
653
763
return transport , nil
654
-
655
764
}
656
765
657
766
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
0 commit comments