335 lines
11 KiB
Python
335 lines
11 KiB
Python
# -*- 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)
|