# -*- coding: utf-8 -*- # # Copyright 2020 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. """Create a certificate.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from googlecloudsdk.api_lib.cloudkms import cryptokeyversions 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 base from googlecloudsdk.calliope import exceptions from googlecloudsdk.calliope.concepts import deps from googlecloudsdk.command_lib.privateca import flags from googlecloudsdk.command_lib.privateca import key_generation from googlecloudsdk.command_lib.privateca import pem_utils from googlecloudsdk.command_lib.privateca import resource_args from googlecloudsdk.command_lib.util.args import labels_util from googlecloudsdk.command_lib.util.concepts import concept_parsers from googlecloudsdk.command_lib.util.concepts import presentation_specs from googlecloudsdk.core import log from googlecloudsdk.core.util import files import six _KEY_OUTPUT_HELP = """The path where the generated private key file should be written (in PEM format). Note: possession of this key file could allow anybody to act as this certificate's subject. Please make sure that you store this key file in a secure location at all times, and ensure that only authorized users have access to it.""" def _ReadCsr(csr_file): try: return files.ReadFileContents(csr_file) except (files.Error, OSError, IOError): raise exceptions.BadFileException( "Could not read provided CSR file '{}'.".format(csr_file) ) def _WritePemChain(pem_cert, issuing_chain, cert_file): try: pem_chain = [pem_cert] + issuing_chain files.WriteFileContents(cert_file, pem_utils.PemChainForOutput(pem_chain)) except (files.Error, OSError, IOError): raise exceptions.BadFileException( "Could not write certificate to '{}'.".format(cert_file) ) @base.ReleaseTracks(base.ReleaseTrack.GA) @base.DefaultUniverseOnly class Create(base.CreateCommand): r"""Create a new certificate. ## EXAMPLES To create a certificate using a CSR: $ {command} frontend-server-tls \ --issuer-pool=my-pool --issuer-location=us-west1 \ --csr=./csr.pem \ --cert-output-file=./cert.pem \ --validity=P30D To create a certificate using a client-generated key: $ {command} frontend-server-tls \ --issuer-pool=my-pool --issuer-location=us-west1 \ --generate-key \ --key-output-file=./key \ --cert-output-file=./cert.pem \ --dns-san=www.example.com \ --use-preset-profile=leaf_server_tls """ @staticmethod def Args(parser): persistence_group = parser.add_group( mutex=True, required=True, help='Certificate persistence options.' ) base.Argument( '--cert-output-file', help=( 'The path where the resulting PEM-encoded certificate chain file' ' should be written (ordered from leaf to root).' ), required=False, ).AddToParser(persistence_group) base.Argument( '--validate-only', help=( 'If this flag is set, the certificate resource will not be' ' persisted and the returned certificate will not contain the' ' pem_certificate field.' ), action='store_true', default=False, required=False, ).AddToParser(persistence_group) flags.AddValidityFlag(parser, 'certificate', 'P30D', '30 days') labels_util.AddCreateLabelsFlags(parser) cert_generation_group = parser.add_group( mutex=True, required=True, help='Certificate generation method.' ) csr_group = cert_generation_group.add_group( help='To issue a certificate from a CSR use the following:', ) base.Argument( '--csr', help='A PEM-encoded certificate signing request file path.', required=True, ).AddToParser(csr_group) base.Argument( '--rdn-sequence-subject', help=( 'If this value is set then the issued certificate will use the ' 'subject found in the CSR preserving the exact RDN sequence.' ), hidden=True, action='store_true', ).AddToParser(csr_group) non_csr_group = cert_generation_group.add_group( help='Alternatively, you may describe the certificate and key to use.' ) key_group = non_csr_group.add_group( mutex=True, required=True, help=( 'To describe the key that will be used for this certificate, use ' 'one of the following options.' ), ) key_generation_group = key_group.add_group( help='To generate a new key pair, use the following:' ) base.Argument( '--generate-key', help=( 'Use this flag to have a new RSA-2048 private key securely' ' generated on your machine.' ), action='store_const', const=True, default=False, required=True, ).AddToParser(key_generation_group) base.Argument( '--key-output-file', help=_KEY_OUTPUT_HELP, required=True ).AddToParser(key_generation_group) base.Argument( '--ca', help=( 'The name of an existing certificate authority to use for issuing' ' the certificate. If omitted, a certificate authority will be will' ' be chosen from the CA pool by the service on your behalf.' ), required=False, ).AddToParser(parser) subject_group = non_csr_group.add_group( help='The subject names for the certificate.', required=True ) flags.AddSubjectFlags(subject_group) x509_parameters_group = non_csr_group.add_group( mutex=True, help='The x509 configuration used for this certificate.' ) flags.AddInlineX509ParametersFlags( x509_parameters_group, is_ca_command=False, default_max_chain_length=0 ) flags.AddUsePresetProfilesFlag(x509_parameters_group) flags.AddSubjectKeyIdFlag(parser) cert_arg = 'CERTIFICATE' concept_parsers.ConceptParser( [ presentation_specs.ResourcePresentationSpec( cert_arg, resource_args.CreateCertResourceSpec( cert_arg, [Create._GenerateCertificateIdFallthrough()] ), 'The name of the certificate to issue. If the certificate ID ' 'is omitted, a random identifier will be generated according ' 'to the following format: {YYYYMMDD}-{3 random alphanumeric ' 'characters}-{3 random alphanumeric characters}. The ' 'certificate ID is not required when the issuing CA pool is in ' 'the DevOps tier.', required=True, ), presentation_specs.ResourcePresentationSpec( '--template', resource_args.CreateCertificateTemplateResourceSpec( 'certificate_template' ), 'The name of a certificate template to use for issuing this ' 'certificate, if desired. A template may overwrite parts of ' 'the certificate request, and the use of certificate templates ' "may be required and/or regulated by the issuing CA Pool's CA " 'Manager. The specified template must be in the same location ' 'as the issuing CA Pool.', required=False, prefixes=True, ), presentation_specs.ResourcePresentationSpec( '--kms-key-version', resource_args.CreateKmsKeyVersionResourceSpec(), 'An existing KMS key version backing this certificate.', group=key_group, ), ], command_level_fallthroughs={ '--template.location': ['CERTIFICATE.issuer-location'] }, ).AddToParser(parser) # The only time a resource is returned is when args.validate_only is set. parser.display_info.AddFormat('yaml(certificateDescription)') @classmethod def _GenerateCertificateIdFallthrough(cls): cls.id_fallthrough_was_used = False def FallthroughFn(): cls.id_fallthrough_was_used = True return certificate_utils.GenerateCertId() return deps.Fallthrough( function=FallthroughFn, hint='certificate id will default to an automatically generated id', active=False, plural=False, ) def _ValidateArgs(self, args): """Validates the command-line args.""" if args.IsSpecified('use_preset_profile') and args.IsSpecified('template'): raise exceptions.OneOfArgumentsRequiredException( ['--use-preset-profile', '--template'], ( 'To create a certificate, please specify either a preset profile ' 'or a certificate template.' ), ) resource_args.ValidateResourceIsCompleteIfSpecified(args, 'kms_key_version') @classmethod def _PrintWarningsForUnpersistedCert(cls, args): """Prints warnings if certain command-line args are used for an unpersisted cert.""" unused_args = [] if not cls.id_fallthrough_was_used: unused_args.append('certificate ID') if args.IsSpecified('labels'): unused_args.append('labels') if unused_args: names = ', '.join(unused_args) verb = 'was' if len(unused_args) == 1 else 'were' log.warning( '{names} {verb} specified but will not be used since the ' 'issuing CA pool is in the DevOps tier, which does not expose ' 'certificate lifecycle.'.format(names=names, verb=verb) ) def _GetPublicKey(self, args): """Fetches the public key associated with a non-CSR certificate request, as UTF-8 encoded bytes.""" kms_key_version = args.CONCEPTS.kms_key_version.Parse() if args.generate_key: private_key, public_key = key_generation.RSAKeyGen(2048) key_generation.ExportPrivateKey(args.key_output_file, private_key) return public_key elif kms_key_version: public_key_response = cryptokeyversions.GetPublicKey(kms_key_version) # bytes(..) requires an explicit encoding in PY3. return ( bytes(public_key_response.pem) if six.PY2 else bytes(public_key_response.pem, 'utf-8') ) else: # This should not happen because of the required arg group, but protects # in case of future additions. raise exceptions.OneOfArgumentsRequiredException( ['--csr', '--generate-key', '--kms-key-version'], ( 'To create a certificate, please specify either a CSR, the' ' --generate-key flag to create a new key, or the' ' --kms-key-version flag to use an existing KMS key.' ), ) def _GenerateCertificateConfig(self, request, args): public_key = self._GetPublicKey(args) config = self.messages.CertificateConfig() config.publicKey = self.messages.PublicKey() config.publicKey.key = public_key config.publicKey.format = self.messages.PublicKey.FormatValueValuesEnum.PEM config.subjectConfig = flags.ParseSubjectFlags(args) config.x509Config = flags.ParseX509Parameters(args, is_ca_command=False) config.subjectKeyId = flags.ParseSubjectKeyId(args, self.messages) return config def Run(self, args): self.client = privateca_base.GetClientInstance(api_version='v1') self.messages = privateca_base.GetMessagesModule(api_version='v1') self._ValidateArgs(args) cert_ref = args.CONCEPTS.certificate.Parse() labels = labels_util.ParseCreateArgs( args, self.messages.Certificate.LabelsValue ) request = ( self.messages.PrivatecaProjectsLocationsCaPoolsCertificatesCreateRequest() ) request.certificate = self.messages.Certificate() request.certificateId = cert_ref.Name() request.certificate.lifetime = flags.ParseValidityFlag(args) request.certificate.labels = labels request.parent = cert_ref.Parent().RelativeName() request.requestId = request_utils.GenerateRequestId() request.validateOnly = args.validate_only if args.IsSpecified('ca'): request.issuingCertificateAuthorityId = args.ca template_ref = args.CONCEPTS.template.Parse() if template_ref: if template_ref.locationsId != cert_ref.locationsId: raise exceptions.InvalidArgumentException( '--template', 'The certificate template must be in the same location as the ' 'issuing CA Pool.', ) request.certificate.certificateTemplate = template_ref.RelativeName() if args.csr: request.certificate.pemCsr = _ReadCsr(args.csr) if args.rdn_sequence_subject: request.certificate.subjectMode = ( self.messages.Certificate.SubjectModeValueValuesEnum.RDN_SEQUENCE ) else: request.certificate.config = self._GenerateCertificateConfig( request, args ) certificate = self.client.projects_locations_caPools_certificates.Create( request ) # Validate-only certs don't have a resource name or pem certificate. if args.validate_only: return certificate status_message = 'Created Certificate' if certificate.name: status_message += ' [{}]'.format(certificate.name) else: Create._PrintWarningsForUnpersistedCert(args) if certificate.pemCertificate: status_message += ' and saved it to [{}]'.format(args.cert_output_file) _WritePemChain( certificate.pemCertificate, certificate.pemCertificateChain, args.cert_output_file, ) status_message += '.' log.status.Print(status_message)