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