@@ -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,86 @@ 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 and their
93+ // source registry mapping, preferring IDMS and falling back to ICSP.
94+ // The returned scopes are normalized and de-duplicated for policy matching.
95+ // The sourceByMirror map links each mirror scope back to its IDMS/ICSP source.
96+ func GetMirrorRegistryScopes (ctx context.Context , helper * helper.Helper ) ([]string , map [string ]string , error ) {
97+ idmsList := & ocpconfigv1.ImageDigestMirrorSetList {}
98+ if err := helper .GetClient ().List (ctx , idmsList ); err != nil {
99+ if ! IsNoMatchError (err ) {
100+ return nil , nil , err
101+ }
102+ } else {
103+ scopes := map [string ]struct {}{}
104+ sourceByMirror := map [string ]string {}
105+ for _ , idms := range idmsList .Items {
106+ for _ , mirrorSet := range idms .Spec .ImageDigestMirrors {
107+ source := normalizeImageScope (string (mirrorSet .Source ))
108+ for _ , mirror := range mirrorSet .Mirrors {
109+ m := normalizeImageScope (string (mirror ))
110+ if m != "" {
111+ scopes [m ] = struct {}{}
112+ if source != "" {
113+ sourceByMirror [m ] = source
114+ }
115+ }
116+ }
117+ }
118+ }
119+ if result := sortedSetKeys (scopes ); len (result ) > 0 {
120+ return result , sourceByMirror , nil
121+ }
122+ }
123+
124+ icspList := & ocpicsp.ImageContentSourcePolicyList {}
125+ if err := helper .GetClient ().List (ctx , icspList ); err != nil {
126+ if ! IsNoMatchError (err ) {
127+ return nil , nil , err
128+ }
129+ } else {
130+ scopes := map [string ]struct {}{}
131+ sourceByMirror := map [string ]string {}
132+ for _ , icsp := range icspList .Items {
133+ for _ , mirrorSet := range icsp .Spec .RepositoryDigestMirrors {
134+ source := normalizeImageScope (mirrorSet .Source )
135+ for _ , mirror := range mirrorSet .Mirrors {
136+ m := normalizeImageScope (mirror )
137+ if m != "" {
138+ scopes [m ] = struct {}{}
139+ if source != "" {
140+ sourceByMirror [m ] = source
141+ }
142+ }
143+ }
144+ }
145+ }
146+ return sortedSetKeys (scopes ), sourceByMirror , nil
147+ }
148+
149+ return nil , nil , nil
150+ }
151+
68152// IsNoMatchError checks if the error indicates that a CRD/resource type doesn't exist
69153func IsNoMatchError (err error ) bool {
70154 errStr := err .Error ()
@@ -151,6 +235,214 @@ func getMachineConfig(ctx context.Context, helper *helper.Helper) (mc.MachineCon
151235 return masterMachineConfig , nil
152236}
153237
238+ // RegistryMapping pairs a mirror registry with its upstream source from IDMS/ICSP.
239+ type RegistryMapping struct {
240+ Mirror string `json:"mirror"`
241+ Source string `json:"source"`
242+ }
243+
244+ // SigstorePolicyInfo contains the EDPM-relevant parts of a ClusterImagePolicy.
245+ // A single RemapIdentity signedPrefix covers all mirrors under the same registry
246+ // root — the container runtime replaces only the prefix, preserving namespace paths.
247+ type SigstorePolicyInfo struct {
248+ RegistryMappings []RegistryMapping
249+ CosignKeyData string
250+ SignedPrefix string
251+ }
252+
253+ const (
254+ clusterImagePolicyCRDName = "clusterimagepolicies.config.openshift.io"
255+ clusterImagePolicyGroup = "config.openshift.io"
256+ clusterImagePolicyKind = "ClusterImagePolicy"
257+ clusterImagePolicyV1 = "v1"
258+ clusterImagePolicyV1Alpha1 = "v1alpha1"
259+ publicKeyRootOfTrustPolicyType = "PublicKey"
260+ remapIdentityMatchPolicy = "RemapIdentity"
261+ )
262+
263+ func normalizeImageScope (scope string ) string {
264+ return strings .TrimSuffix (strings .TrimSpace (scope ), "/" )
265+ }
266+
267+ func clusterImagePolicyScopeMatchesMirror (policyScope string , mirrorScope string ) bool {
268+ policyScope = normalizeImageScope (policyScope )
269+ mirrorScope = normalizeImageScope (mirrorScope )
270+
271+ if policyScope == "" || mirrorScope == "" {
272+ return false
273+ }
274+
275+ if strings .HasPrefix (policyScope , "*." ) {
276+ mirrorHostPort := strings .SplitN (mirrorScope , "/" , 2 )[0 ]
277+ mirrorHost := strings .SplitN (mirrorHostPort , ":" , 2 )[0 ]
278+ suffix := strings .TrimPrefix (policyScope , "*" )
279+ return strings .HasSuffix (mirrorHost , suffix )
280+ }
281+
282+ return mirrorScope == policyScope || strings .HasPrefix (mirrorScope , policyScope + "/" )
283+ }
284+
285+ func getServedClusterImagePolicyVersion (ctx context.Context , helper * helper.Helper ) (string , error ) {
286+ crd := & apiextensionsv1.CustomResourceDefinition {}
287+ if err := helper .GetClient ().Get (ctx , types.NamespacedName {Name : clusterImagePolicyCRDName }, crd ); err != nil {
288+ if k8s_errors .IsNotFound (err ) || IsNoMatchError (err ) {
289+ return "" , nil
290+ }
291+ return "" , err
292+ }
293+
294+ for _ , preferredVersion := range []string {clusterImagePolicyV1 , clusterImagePolicyV1Alpha1 } {
295+ for _ , version := range crd .Spec .Versions {
296+ if version .Name == preferredVersion && version .Served {
297+ return preferredVersion , nil
298+ }
299+ }
300+ }
301+
302+ return "" , nil
303+ }
304+
305+ func listClusterImagePolicies (
306+ ctx context.Context ,
307+ helper * helper.Helper ,
308+ version string ,
309+ ) (* unstructured.UnstructuredList , error ) {
310+ policyList := & unstructured.UnstructuredList {}
311+ policyList .SetGroupVersionKind (schema.GroupVersionKind {
312+ Group : clusterImagePolicyGroup ,
313+ Version : version ,
314+ Kind : clusterImagePolicyKind + "List" ,
315+ })
316+
317+ if err := helper .GetClient ().List (ctx , policyList ); err != nil {
318+ if IsNoMatchError (err ) {
319+ return nil , nil
320+ }
321+ return nil , err
322+ }
323+
324+ return policyList , nil
325+ }
326+
327+ // GetSigstoreImagePolicy checks if OCP has a ClusterImagePolicy configured
328+ // with sigstore signature verification for one of the mirror registries in use.
329+ // sourceByMirror maps each mirror scope to its upstream source registry (from IDMS/ICSP).
330+ // Returns policy info if a relevant policy is found, nil if no policy exists.
331+ // Returns nil without error if the ClusterImagePolicy CRD is not installed.
332+ func GetSigstoreImagePolicy (ctx context.Context , helper * helper.Helper , mirrorScopes []string , sourceByMirror map [string ]string ) (* SigstorePolicyInfo , error ) {
333+ if len (mirrorScopes ) == 0 {
334+ return nil , nil
335+ }
336+
337+ version , err := getServedClusterImagePolicyVersion (ctx , helper )
338+ if err != nil {
339+ return nil , err
340+ }
341+ if version == "" {
342+ return nil , nil
343+ }
344+
345+ policyList , err := listClusterImagePolicies (ctx , helper , version )
346+ if err != nil {
347+ return nil , err
348+ }
349+ if policyList == nil {
350+ return nil , nil
351+ }
352+
353+ var matches []string
354+ var match * SigstorePolicyInfo
355+
356+ for _ , policy := range policyList .Items {
357+ if policy .GetName () == "openshift" {
358+ continue
359+ }
360+
361+ policyType , found , err := unstructured .NestedString (policy .Object , "spec" , "policy" , "rootOfTrust" , "policyType" )
362+ if err != nil {
363+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s policyType: %w" , policy .GetName (), err )
364+ }
365+ if ! found || policyType != publicKeyRootOfTrustPolicyType {
366+ continue
367+ }
368+
369+ keyData , found , err := unstructured .NestedString (policy .Object , "spec" , "policy" , "rootOfTrust" , "publicKey" , "keyData" )
370+ if err != nil {
371+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s keyData: %w" , policy .GetName (), err )
372+ }
373+ if ! found || len (keyData ) == 0 {
374+ continue
375+ }
376+
377+ scopes , found , err := unstructured .NestedStringSlice (policy .Object , "spec" , "scopes" )
378+ if err != nil {
379+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s scopes: %w" , policy .GetName (), err )
380+ }
381+ if ! found || len (scopes ) == 0 {
382+ continue
383+ }
384+
385+ signedPrefix := ""
386+ matchPolicy , found , err := unstructured .NestedString (policy .Object , "spec" , "policy" , "signedIdentity" , "matchPolicy" )
387+ if err != nil {
388+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s matchPolicy: %w" , policy .GetName (), err )
389+ }
390+ if found && matchPolicy == remapIdentityMatchPolicy {
391+ signedPrefix , _ , err = unstructured .NestedString (
392+ policy .Object ,
393+ "spec" , "policy" , "signedIdentity" , "remapIdentity" , "signedPrefix" ,
394+ )
395+ if err != nil {
396+ return nil , fmt .Errorf ("failed to parse ClusterImagePolicy %s signedPrefix: %w" , policy .GetName (), err )
397+ }
398+ }
399+
400+ matchedMirrorScopes := map [string ]struct {}{}
401+ for _ , scope := range scopes {
402+ policyScope := normalizeImageScope (scope )
403+ if policyScope == "" {
404+ continue
405+ }
406+
407+ for _ , mirrorScope := range mirrorScopes {
408+ if clusterImagePolicyScopeMatchesMirror (policyScope , mirrorScope ) {
409+ matchedMirrorScopes [normalizeImageScope (mirrorScope )] = struct {}{}
410+ }
411+ }
412+ }
413+ if len (matchedMirrorScopes ) == 0 {
414+ continue
415+ }
416+
417+ sortedMirrors := sortedSetKeys (matchedMirrorScopes )
418+
419+ mappings := make ([]RegistryMapping , 0 , len (sortedMirrors ))
420+ for _ , m := range sortedMirrors {
421+ mappings = append (mappings , RegistryMapping {
422+ Mirror : m ,
423+ Source : sourceByMirror [m ],
424+ })
425+ }
426+
427+ matches = append (matches , fmt .Sprintf ("%s (%s)" , policy .GetName (), strings .Join (sortedMirrors , ", " )))
428+ match = & SigstorePolicyInfo {
429+ RegistryMappings : mappings ,
430+ CosignKeyData : keyData ,
431+ SignedPrefix : signedPrefix ,
432+ }
433+ }
434+
435+ if len (matches ) > 1 {
436+ sort .Strings (matches )
437+ return nil , fmt .Errorf (
438+ "expected exactly one ClusterImagePolicy matching mirror registries, found %d: %s" ,
439+ len (matches ), strings .Join (matches , ", " ),
440+ )
441+ }
442+
443+ return match , nil
444+ }
445+
154446// GetMirrorRegistryCACerts retrieves CA certificates from image.config.openshift.io/cluster.
155447// Returns nil without error if:
156448// - not on OpenShift (Image CRD doesn't exist)
0 commit comments