@@ -732,10 +732,163 @@ def validate_key_create(cmd, ns):
732732 validate_tags (ns )
733733 set_vault_base_url (ns )
734734 validate_keyvault_resource_id ('key' )(ns )
735- validate_key_type (ns )
735+ validate_external_key_id (ns )
736+
737+ if getattr (ns , 'external_key_id' , None ):
738+ # External keys are backed by an External Key Manager (EKM); the service controls the
739+ # key material, so client-specified key-shape arguments are not supported. Fail fast with
740+ # a clear error instead of silently ignoring them.
741+ incompatible = [opt for opt , val in (
742+ ('--kty' , getattr (ns , 'kty' , None )),
743+ ('--size' , getattr (ns , 'key_size' , None )),
744+ ('--curve' , getattr (ns , 'curve' , None )),
745+ ('--ops' , getattr (ns , 'key_ops' , None )),
746+ ('--protection' , getattr (ns , 'protection' , None )),
747+ ('--exportable' , getattr (ns , 'exportable' , None )),
748+ ) if val is not None ]
749+ if incompatible :
750+ raise CLIError (
751+ '{} cannot be used with --external-key-id. External keys are backed by an External '
752+ 'Key Manager and the service controls the key material.' .format (', ' .join (incompatible )))
753+ else :
754+ validate_key_type (ns )
755+
736756 process_key_release_policy (cmd , ns )
737757
738758
759+ def validate_external_key_id (ns ):
760+ external_key_id = getattr (ns , 'external_key_id' , None )
761+ if not external_key_id :
762+ return
763+ if len (external_key_id ) > 64 :
764+ raise CLIError ('--external-key-id must be at most 64 characters.' )
765+ if not re .match (r'^[0-9A-Za-z-]+$' , external_key_id ):
766+ raise CLIError ('--external-key-id may contain only letters, digits, and hyphens.' )
767+
768+
769+ def _validate_ekm_path_prefix (path_prefix = None ):
770+ if path_prefix is None :
771+ return
772+ if not path_prefix .startswith ('/' ):
773+ raise CLIError ('--path-prefix must start with "/".' )
774+ if path_prefix .endswith ('/' ):
775+ raise CLIError ('--path-prefix must not end with "/".' )
776+ if len (path_prefix ) > 64 :
777+ raise CLIError ('--path-prefix must be at most 64 characters.' )
778+ if not re .match (r'^[A-Za-z0-9/-]+$' , path_prefix ):
779+ raise CLIError ('--path-prefix may contain only letters, digits, "/" and "-".' )
780+
781+
782+ def _normalize_ekm_host (host : str ):
783+ host = (host or '' ).strip ()
784+ if not host :
785+ raise CLIError ('--host cannot be empty.' )
786+ if '://' in host :
787+ raise CLIError ('--host must not include a URL scheme (use FQDN or FQDN:port).' )
788+ if '/' in host :
789+ raise CLIError ('--host must not include a path (use FQDN or FQDN:port).' )
790+
791+ if ':' not in host :
792+ return f'{ host } :443'
793+
794+ # Avoid ambiguous parsing for IPv6 literals.
795+ if host .count (':' ) != 1 :
796+ raise CLIError ('--host must be in the form FQDN or FQDN:port.' )
797+
798+ hostname , port_str = host .split (':' , 1 )
799+ if not hostname :
800+ raise CLIError ('--host must be in the form FQDN or FQDN:port.' )
801+ try :
802+ port = int (port_str )
803+ except ValueError as ex :
804+ raise CLIError ('--host port must be an integer.' ) from ex
805+ if port < 1 or port > 65535 :
806+ raise CLIError ('--host port must be between 1 and 65535.' )
807+ return f'{ hostname } :{ port } '
808+
809+
810+ def _flatten_list (value ):
811+ if value is None :
812+ return None
813+ if isinstance (value , list ) and value and isinstance (value [0 ], list ):
814+ flattened = []
815+ for item in value :
816+ flattened .extend (item )
817+ return flattened
818+ return value
819+
820+
821+ def _load_certificates_as_der_bytes (cert_paths ):
822+ import os
823+ import ssl
824+
825+ cert_paths = _flatten_list (cert_paths )
826+ if not cert_paths :
827+ return []
828+
829+ der_certs = []
830+ for cert_path in cert_paths :
831+ if not cert_path :
832+ continue
833+ expanded = os .path .expanduser (cert_path )
834+ try :
835+ with open (expanded , 'rb' ) as f :
836+ raw = f .read ()
837+ except OSError as ex :
838+ raise CLIError ("Unable to load certificate file '{}': {}." .format (cert_path , ex .strerror )) from ex
839+
840+ # PEM may contain multiple cert blocks.
841+ if b'-----BEGIN CERTIFICATE-----' in raw :
842+ text = raw .decode ('utf-8' , errors = 'ignore' )
843+ begin = '-----BEGIN CERTIFICATE-----'
844+ end = '-----END CERTIFICATE-----'
845+ start = 0
846+ found_any = False
847+ while True :
848+ b_idx = text .find (begin , start )
849+ if b_idx == - 1 :
850+ break
851+ e_idx = text .find (end , b_idx )
852+ if e_idx == - 1 :
853+ raise CLIError (f'Invalid PEM certificate in { cert_path } .' )
854+ block = text [b_idx :e_idx + len (end )]
855+ der_certs .append (ssl .PEM_cert_to_DER_cert (block ))
856+ found_any = True
857+ start = e_idx + len (end )
858+ if not found_any :
859+ raise CLIError (f'Invalid PEM certificate in { cert_path } .' )
860+ else :
861+ # Assume DER.
862+ der_certs .append (raw )
863+
864+ return der_certs
865+
866+
867+ def validate_ekm_connection_base (cmd , ns ): # pylint: disable=unused-argument
868+ set_vault_base_url (ns )
869+ if not getattr (ns , 'hsm_name' , None ) and not getattr (ns , 'identifier' , None ):
870+ raise CLIError ('Please specify --hsm-name or --id.' )
871+
872+
873+ def validate_ekm_connection_create (cmd , ns ):
874+ validate_ekm_connection_base (cmd , ns )
875+ ns .host = _normalize_ekm_host (ns .host )
876+ _validate_ekm_path_prefix (getattr (ns , 'path_prefix' , None ))
877+ server_ca_certificates = _load_certificates_as_der_bytes (getattr (ns , 'server_ca_certificates' , None ))
878+ if not server_ca_certificates :
879+ raise CLIError ('Please specify at least one --server-ca-certificate for EKM connection creation.' )
880+ ns .server_ca_certificates = server_ca_certificates
881+
882+
883+ def validate_ekm_connection_update (cmd , ns ):
884+ validate_ekm_connection_base (cmd , ns )
885+ if getattr (ns , 'host' , None ):
886+ ns .host = _normalize_ekm_host (ns .host )
887+ _validate_ekm_path_prefix (getattr (ns , 'path_prefix' , None ))
888+ if getattr (ns , 'server_ca_certificates' , None ):
889+ ns .server_ca_certificates = _load_certificates_as_der_bytes (ns .server_ca_certificates )
890+
891+
739892# pylint: disable=line-too-long, too-many-locals
740893def process_certificate_policy (cmd , ns ):
741894 policy = getattr (ns , 'policy' , None )
0 commit comments