# -*- coding: utf-8 -*- # # Copyright 2023 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Helpers for create commands.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.privateca import base as privateca_base from googlecloudsdk.api_lib.privateca import certificate_utils from googlecloudsdk.api_lib.privateca import request_utils from googlecloudsdk.calliope import exceptions from googlecloudsdk.command_lib.privateca import flags from googlecloudsdk.command_lib.privateca import resource_args from googlecloudsdk.command_lib.util.args import labels_util def _ParseCAResourceArgs(args): """Parses, validates and returns the resource args from the CLI. Args: args: The parsed arguments from the command-line. Returns: Tuple containing the Resource objects for (CA, source CA, issuer). """ resource_args.ValidateResourceIsCompleteIfSpecified(args, 'kms_key_version') resource_args.ValidateResourceIsCompleteIfSpecified(args, 'issuer_pool') resource_args.ValidateResourceIsCompleteIfSpecified(args, 'from_ca') ca_ref = args.CONCEPTS.certificate_authority.Parse() resource_args.ValidateResourceLocation( ca_ref, 'CERTIFICATE_AUTHORITY', version='v1' ) kms_key_version_ref = args.CONCEPTS.kms_key_version.Parse() if ( kms_key_version_ref and ca_ref.locationsId != kms_key_version_ref.locationsId ): raise exceptions.InvalidArgumentException( '--kms-key-version', 'KMS key must be in the same location as the Certificate Authority ' '({}).'.format(ca_ref.locationsId), ) issuer_ref = ( args.CONCEPTS.issuer_pool.Parse() if hasattr(args, 'issuer_pool') else None ) source_ca_ref = args.CONCEPTS.from_ca.Parse() if ( source_ca_ref and source_ca_ref.Parent().RelativeName() != ca_ref.Parent().RelativeName() ): raise exceptions.InvalidArgumentException( '--from-ca', 'The provided source CA must be a part of the same pool as the' ' specified CA to be created.', ) return (ca_ref, source_ca_ref, issuer_ref) def CreateCAFromArgs(args, is_subordinate): """Creates a GA CA object from CA create flags. Args: args: The parser that contains the flag values. is_subordinate: If True, a subordinate CA is returned, otherwise a root CA. Returns: A tuple for the CA to create with (CA object, CA ref, issuer). """ client = privateca_base.GetClientInstance(api_version='v1') messages = privateca_base.GetMessagesModule(api_version='v1') ca_ref, source_ca_ref, issuer_ref = _ParseCAResourceArgs(args) pool_ref = ca_ref.Parent() source_ca = None if source_ca_ref: source_ca = client.projects_locations_caPools_certificateAuthorities.Get( messages.PrivatecaProjectsLocationsCaPoolsCertificateAuthoritiesGetRequest( name=source_ca_ref.RelativeName() ) ) if not source_ca: raise exceptions.InvalidArgumentException( '--from-ca', 'The provided source CA could not be retrieved.' ) ca_pool = client.projects_locations_caPools.Get( messages.PrivatecaProjectsLocationsCaPoolsGetRequest( name=pool_ref.RelativeName() ) ) keyspec = flags.ParseKeySpec(args) if ( ca_pool.tier == messages.CaPool.TierValueValuesEnum.DEVOPS and keyspec.cloudKmsKeyVersion ): raise exceptions.InvalidArgumentException( '--kms-key-version', 'The DevOps tier does not support user-specified KMS keys.', ) subject_config = messages.SubjectConfig( subject=messages.Subject(), subjectAltName=messages.SubjectAltNames() ) if args.IsSpecified('subject'): subject_config.subject = flags.ParseSubject(args) elif args.IsKnownAndSpecified('subject_file'): subject_config.subject = flags.ParseSubjectFile(args) elif source_ca: subject_config.subject = source_ca.config.subjectConfig.subject if flags.SanFlagsAreSpecified(args): subject_config.subjectAltName = flags.ParseSanFlags(args) elif source_ca: subject_config.subjectAltName = ( source_ca.config.subjectConfig.subjectAltName ) flags.ValidateSubjectConfig(subject_config) # Populate x509 params to default. x509_parameters = flags.ParseX509Parameters(args, is_ca_command=True) if source_ca and not flags.X509ConfigFlagsAreSpecified(args): x509_parameters = source_ca.config.x509Config # Args.validity will be populated to default if not specified. lifetime = flags.ParseValidityFlag(args) if source_ca and not args.IsSpecified('validity'): lifetime = source_ca.lifetime labels = labels_util.ParseCreateArgs( args, messages.CertificateAuthority.LabelsValue ) ski = flags.ParseSubjectKeyId(args, messages) # Parse user defined access URLs user_defined_access_urls = flags.ParseUserDefinedAccessUrls(args, messages) new_ca = messages.CertificateAuthority( type=messages.CertificateAuthority.TypeValueValuesEnum.SUBORDINATE if is_subordinate else messages.CertificateAuthority.TypeValueValuesEnum.SELF_SIGNED, lifetime=lifetime, config=messages.CertificateConfig( subjectConfig=subject_config, x509Config=x509_parameters, subjectKeyId=ski, ), keySpec=keyspec, gcsBucket=None, userDefinedAccessUrls=user_defined_access_urls, labels=labels, ) return (new_ca, ca_ref, issuer_ref) def HasEnabledCa(ca_list, messages): """Checks if there are any enabled CAs in the CA list.""" for ca in ca_list: if ca.state == messages.CertificateAuthority.StateValueValuesEnum.ENABLED: return True return False def _ValidateIssuingCa(ca_pool_name, issuing_ca_id, ca_list): """Checks that an issuing CA is in the CA Pool and has a valid state. Args: ca_pool_name: The resource name of the containing CA Pool. issuing_ca_id: The CA ID of the CA to verify. ca_list: The list of JSON CA objects in the CA pool to check from Raises: InvalidArgumentException on validation errors """ messages = privateca_base.GetMessagesModule(api_version='v1') allowd_issuing_states = [ messages.CertificateAuthority.StateValueValuesEnum.ENABLED, messages.CertificateAuthority.StateValueValuesEnum.STAGED, ] issuing_ca = None for ca in ca_list: if 'certificateAuthorities/{}'.format(issuing_ca_id) in ca.name: issuing_ca = ca if not issuing_ca: raise exceptions.InvalidArgumentException( '--issuer-ca', 'The specified CA with ID [{}] was not found in CA Pool [{}]'.format( issuing_ca_id, ca_pool_name ), ) if issuing_ca.state not in allowd_issuing_states: raise exceptions.InvalidArgumentException( '--issuer-pool', 'The specified CA with ID [{}] in CA Pool [{}] is not ENABLED or' ' STAGED. Please choose a CA that has one of these states to issue the' ' CA certificate from.'.format(issuing_ca_id, ca_pool_name), ) def ValidateIssuingPool(ca_pool_name, issuing_ca_id): """Checks that a CA Pool is valid to be issuing Pool for a subordinate. Args: ca_pool_name: The resource name of the issuing CA Pool. issuing_ca_id: The optional CA ID in the CA Pool to validate. Raises: InvalidArgumentException if the CA Pool does not exist or is not enabled. """ try: client = privateca_base.GetClientInstance(api_version='v1') messages = privateca_base.GetMessagesModule(api_version='v1') enabled_state = messages.CertificateAuthority.StateValueValuesEnum.ENABLED ca_list_response = client.projects_locations_caPools_certificateAuthorities.List( messages.PrivatecaProjectsLocationsCaPoolsCertificateAuthoritiesListRequest( parent=ca_pool_name ) ) ca_list = ca_list_response.certificateAuthorities # If a specific CA is targeted, verify its properties if issuing_ca_id: _ValidateIssuingCa(ca_pool_name, issuing_ca_id, ca_list) return # Otherwise verify that there is an available CA to issue from ca_states = [ca.state for ca in ca_list] if enabled_state not in ca_states: raise exceptions.InvalidArgumentException( '--issuer-pool', 'The issuing CA Pool [{}] did not have any CAs in ENABLED state of' ' the {} CAs found. Please create or enable a CA and try again.' .format(ca_pool_name, len(ca_list)), ) except apitools_exceptions.HttpNotFoundError: raise exceptions.InvalidArgumentException( '--issuer-pool', 'The issuing CA Pool [{}] was not found. Please verify this information' ' is correct and try again.'.format(ca_pool_name), ) def _CreateCertificateCreateRequest(issuer_pool_ref, csr, issuer_ca_id, new_ca): """Returns the certificate create request with the given settings. Args: issuer_pool_ref: The resource reference for the issuing CA pool. csr: The Certificate Signing Request. issuer_ca_id: The CA ID of the CA to sign the CSR, if specified. new_ca: The CA object. Returns: A certificate create request. """ messages = privateca_base.GetMessagesModule(api_version='v1') certificate_id = 'subordinate-{}'.format(certificate_utils.GenerateCertId()) issuer_pool_name = issuer_pool_ref.RelativeName() certificate_name = '{}/certificates/{}'.format( issuer_pool_name, certificate_id ) lifetime = new_ca.lifetime cert_request = ( messages.PrivatecaProjectsLocationsCaPoolsCertificatesCreateRequest( certificateId=certificate_id, parent=issuer_pool_name, requestId=request_utils.GenerateRequestId(), issuingCertificateAuthorityId=issuer_ca_id, certificate=messages.Certificate( name=certificate_name, lifetime=lifetime, pemCsr=csr, ), ) ) if new_ca.config.subjectConfig.subject.rdnSequence: cert_request.certificate.subjectMode = ( messages.Certificate.SubjectModeValueValuesEnum.RDN_SEQUENCE ) return cert_request def SignCsr(issuer_pool_ref, csr, issuer_ca_id, new_ca): """Issues a certificate under the given issuer with the given settings. Args: issuer_pool_ref: The resource reference for the issuing CA pool. csr: The Certificate Signing Request. issuer_ca_id: The CA ID of the CA to sign the CSR, if specified. new_ca: The CA object. Returns: The certificate for the new CA. """ client = privateca_base.GetClientInstance(api_version='v1') cert_request = _CreateCertificateCreateRequest( issuer_pool_ref, csr, issuer_ca_id, new_ca ) return client.projects_locations_caPools_certificates.Create(cert_request)