@@ -5,15 +5,19 @@ import (
55 "encoding/base64"
66 "encoding/json"
77 "fmt"
8+ "sort"
89 "strings"
910
1011 ocpconfigv1 "github.com/openshift/api/config/v1"
1112 mc "github.com/openshift/api/machineconfiguration/v1"
1213 ocpicsp "github.com/openshift/api/operator/v1alpha1"
1314 "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
1415 corev1 "k8s.io/api/core/v1"
16+ apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1517 k8s_errors "k8s.io/apimachinery/pkg/api/errors"
1618 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
20+ "k8s.io/apimachinery/pkg/runtime/schema"
1721 "k8s.io/apimachinery/pkg/types"
1822)
1923
@@ -65,6 +69,67 @@ func HasMirrorRegistries(ctx context.Context, helper *helper.Helper) (bool, erro
6569 return false , nil
6670}
6771
72+ func addNormalizedScopes (set map [string ]struct {}, values []string ) {
73+ for _ , value := range values {
74+ if scope := normalizeImageScope (value ); scope != "" {
75+ set [scope ] = struct {}{}
76+ }
77+ }
78+ }
79+
80+ func sortedSetKeys (set map [string ]struct {}) []string {
81+ if len (set ) == 0 {
82+ return nil
83+ }
84+ result := make ([]string , 0 , len (set ))
85+ for k := range set {
86+ result = append (result , k )
87+ }
88+ sort .Strings (result )
89+ return result
90+ }
91+
92+ // GetMirrorRegistryScopes returns the configured mirror scopes, preferring IDMS
93+ // and falling back to ICSP only when no IDMS mirror scopes are present.
94+ // The returned values are normalized and de-duplicated for policy matching.
95+ func GetMirrorRegistryScopes (ctx context.Context , helper * helper.Helper ) ([]string , error ) {
96+ idmsList := & ocpconfigv1.ImageDigestMirrorSetList {}
97+ if err := helper .GetClient ().List (ctx , idmsList ); err != nil {
98+ if ! IsNoMatchError (err ) {
99+ return nil , err
100+ }
101+ } else {
102+ scopes := map [string ]struct {}{}
103+ for _ , idms := range idmsList .Items {
104+ for _ , mirrorSet := range idms .Spec .ImageDigestMirrors {
105+ for _ , mirror := range mirrorSet .Mirrors {
106+ addNormalizedScopes (scopes , []string {string (mirror )})
107+ }
108+ }
109+ }
110+ if result := sortedSetKeys (scopes ); len (result ) > 0 {
111+ return result , nil
112+ }
113+ }
114+
115+ icspList := & ocpicsp.ImageContentSourcePolicyList {}
116+ if err := helper .GetClient ().List (ctx , icspList ); err != nil {
117+ if ! IsNoMatchError (err ) {
118+ return nil , err
119+ }
120+ } else {
121+ scopes := map [string ]struct {}{}
122+ for _ , icsp := range icspList .Items {
123+ for _ , mirrorSet := range icsp .Spec .RepositoryDigestMirrors {
124+ addNormalizedScopes (scopes , mirrorSet .Mirrors )
125+ }
126+ }
127+ return sortedSetKeys (scopes ), nil
128+ }
129+
130+ return nil , nil
131+ }
132+
68133// IsNoMatchError checks if the error indicates that a CRD/resource type doesn't exist
69134func IsNoMatchError (err error ) bool {
70135 errStr := err .Error ()
@@ -151,6 +216,201 @@ func getMachineConfig(ctx context.Context, helper *helper.Helper) (mc.MachineCon
151216 return masterMachineConfig , nil
152217}
153218
219+ // SigstorePolicyInfo contains the EDPM-relevant parts of a ClusterImagePolicy.
220+ type SigstorePolicyInfo struct {
221+ MirrorRegistries []string
222+ CosignKeyData string
223+ SignedPrefix string
224+ }
225+
226+ const (
227+ clusterImagePolicyCRDName = "clusterimagepolicies.config.openshift.io"
228+ clusterImagePolicyGroup = "config.openshift.io"
229+ clusterImagePolicyKind = "ClusterImagePolicy"
230+ clusterImagePolicyV1 = "v1"
231+ clusterImagePolicyV1Alpha1 = "v1alpha1"
232+ publicKeyRootOfTrustPolicyType = "PublicKey"
233+ remapIdentityMatchPolicy = "RemapIdentity"
234+ )
235+
236+ func normalizeImageScope (scope string ) string {
237+ return strings .TrimSuffix (strings .TrimSpace (scope ), "/" )
238+ }
239+
240+ func clusterImagePolicyScopeMatchesMirror (policyScope string , mirrorScope string ) bool {
241+ policyScope = normalizeImageScope (policyScope )
242+ mirrorScope = normalizeImageScope (mirrorScope )
243+
244+ if policyScope == "" || mirrorScope == "" {
245+ return false
246+ }
247+
248+ if strings .HasPrefix (policyScope , "*." ) {
249+ mirrorHostPort := strings .SplitN (mirrorScope , "/" , 2 )[0 ]
250+ mirrorHost := strings .SplitN (mirrorHostPort , ":" , 2 )[0 ]
251+ suffix := strings .TrimPrefix (policyScope , "*" )
252+ return strings .HasSuffix (mirrorHost , suffix )
253+ }
254+
255+ return mirrorScope == policyScope || strings .HasPrefix (mirrorScope , policyScope + "/" )
256+ }
257+
258+ func getServedClusterImagePolicyVersion (ctx context.Context , helper * helper.Helper ) (string , error ) {
259+ crd := & apiextensionsv1.CustomResourceDefinition {}
260+ if err := helper .GetClient ().Get (ctx , types.NamespacedName {Name : clusterImagePolicyCRDName }, crd ); err != nil {
261+ if k8s_errors .IsNotFound (err ) || IsNoMatchError (err ) {
262+ return "" , nil
263+ }
264+ return "" , err
265+ }
266+
267+ for _ , preferredVersion := range []string {clusterImagePolicyV1 , clusterImagePolicyV1Alpha1 } {
268+ for _ , version := range crd .Spec .Versions {
269+ if version .Name == preferredVersion && version .Served {
270+ return preferredVersion , nil
271+ }
272+ }
273+ }
274+
275+ return "" , nil
276+ }
277+
278+ func listClusterImagePolicies (
279+ ctx context.Context ,
280+ helper * helper.Helper ,
281+ version string ,
282+ ) (* unstructured.UnstructuredList , error ) {
283+ policyList := & unstructured.UnstructuredList {}
284+ policyList .SetGroupVersionKind (schema.GroupVersionKind {
285+ Group : clusterImagePolicyGroup ,
286+ Version : version ,
287+ Kind : clusterImagePolicyKind + "List" ,
288+ })
289+
290+ if err := helper .GetClient ().List (ctx , policyList ); err != nil {
291+ if IsNoMatchError (err ) {
292+ return nil , nil
293+ }
294+ return nil , err
295+ }
296+
297+ return policyList , nil
298+ }
299+
300+ // GetSigstoreImagePolicy checks if OCP has a ClusterImagePolicy configured
301+ // with sigstore signature verification for one of the mirror registries in use.
302+ // Returns policy info if a relevant policy is found, nil if no policy exists.
303+ // Returns nil without error if the ClusterImagePolicy CRD is not installed.
304+ func GetSigstoreImagePolicy (ctx context.Context , helper * helper.Helper , mirrorScopes []string ) (* SigstorePolicyInfo , error ) {
305+ if len (mirrorScopes ) == 0 {
306+ return nil , nil
307+ }
308+
309+ version , err := getServedClusterImagePolicyVersion (ctx , helper )
310+ if err != nil {
311+ return nil , err
312+ }
313+ if version == "" {
314+ return nil , nil
315+ }
316+
317+ policyList , err := listClusterImagePolicies (ctx , helper , version )
318+ if err != nil {
319+ return nil , err
320+ }
321+ if policyList == nil {
322+ return nil , nil
323+ }
324+
325+ var matches []string
326+ var match * SigstorePolicyInfo
327+
328+ for _ , policy := range policyList .Items {
329+ if policy .GetName () == "openshift" {
330+ continue
331+ }
332+
333+ policyType , found , err := unstructured .NestedString (policy .Object , "spec" , "policy" , "rootOfTrust" , "policyType" )
334+ if err != nil {
335+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s policyType: %w" , policy .GetName (), err )
336+ }
337+ if ! found || policyType != publicKeyRootOfTrustPolicyType {
338+ continue
339+ }
340+
341+ keyData , found , err := unstructured .NestedString (policy .Object , "spec" , "policy" , "rootOfTrust" , "publicKey" , "keyData" )
342+ if err != nil {
343+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s keyData: %w" , policy .GetName (), err )
344+ }
345+ if ! found || len (keyData ) == 0 {
346+ continue
347+ }
348+
349+ scopes , found , err := unstructured .NestedStringSlice (policy .Object , "spec" , "scopes" )
350+ if err != nil {
351+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s scopes: %w" , policy .GetName (), err )
352+ }
353+ if ! found || len (scopes ) == 0 {
354+ continue
355+ }
356+
357+ signedPrefix := ""
358+ matchPolicy , found , err := unstructured .NestedString (policy .Object , "spec" , "policy" , "signedIdentity" , "matchPolicy" )
359+ if err != nil {
360+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s matchPolicy: %w" , policy .GetName (), err )
361+ }
362+ if found && matchPolicy == remapIdentityMatchPolicy {
363+ signedPrefix , _ , err = unstructured .NestedString (
364+ policy .Object ,
365+ "spec" , "policy" , "signedIdentity" , "remapIdentity" , "signedPrefix" ,
366+ )
367+ if err != nil {
368+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s signedPrefix: %w" , policy .GetName (), err )
369+ }
370+ }
371+
372+ matchedMirrorScopes := map [string ]struct {}{}
373+ for _ , scope := range scopes {
374+ policyScope := normalizeImageScope (scope )
375+ if policyScope == "" {
376+ continue
377+ }
378+
379+ for _ , mirrorScope := range mirrorScopes {
380+ if clusterImagePolicyScopeMatchesMirror (policyScope , mirrorScope ) {
381+ matchedMirrorScopes [normalizeImageScope (mirrorScope )] = struct {}{}
382+ }
383+ }
384+ }
385+ if len (matchedMirrorScopes ) == 0 {
386+ continue
387+ }
388+
389+ mirrorRegistries := make ([]string , 0 , len (matchedMirrorScopes ))
390+ for mirrorScope := range matchedMirrorScopes {
391+ mirrorRegistries = append (mirrorRegistries , mirrorScope )
392+ }
393+ sort .Strings (mirrorRegistries )
394+
395+ matches = append (matches , fmt .Sprintf ("%s (%s)" , policy .GetName (), strings .Join (mirrorRegistries , ", " )))
396+ match = & SigstorePolicyInfo {
397+ MirrorRegistries : mirrorRegistries ,
398+ CosignKeyData : keyData ,
399+ SignedPrefix : signedPrefix ,
400+ }
401+ }
402+
403+ if len (matches ) > 1 {
404+ sort .Strings (matches )
405+ return nil , fmt .Errorf (
406+ "expected exactly one ClusterImagePolicy matching mirror registries, found %d: %s" ,
407+ len (matches ), strings .Join (matches , ", " ),
408+ )
409+ }
410+
411+ return match , nil
412+ }
413+
154414// GetMirrorRegistryCACerts retrieves CA certificates from image.config.openshift.io/cluster.
155415// Returns nil without error if:
156416// - not on OpenShift (Image CRD doesn't exist)
0 commit comments