243 lines
10 KiB
Python
243 lines
10 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2019 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.
|
|
"""Import a provided key from file into KMS using an Import Job."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import os
|
|
import sys
|
|
|
|
from googlecloudsdk.api_lib.cloudkms import base as cloudkms_base
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope import exceptions
|
|
from googlecloudsdk.command_lib.kms import flags
|
|
from googlecloudsdk.command_lib.kms import maps
|
|
from googlecloudsdk.core import log
|
|
from googlecloudsdk.core.util import files
|
|
|
|
|
|
class Import(base.Command):
|
|
r"""Import a version into an existing crypto key.
|
|
|
|
Imports wrapped key material into a new version within an existing crypto key
|
|
following the import procedure documented at
|
|
https://cloud.google.com/kms/docs/importing-a-key.
|
|
|
|
## EXAMPLES
|
|
|
|
The following command will read the files 'path/to/ephemeral/key' and
|
|
'path/to/target/key' and use them to create a new version with algorithm
|
|
'google-symmetric-encryption' within the 'frodo' crypto key, 'fellowship'
|
|
keyring, and 'us-central1' location using import job 'strider' to unwrap the
|
|
provided key material.
|
|
|
|
$ {command} --location=global \
|
|
--keyring=fellowship \
|
|
--key=frodo \
|
|
--import-job=strider \
|
|
--wrapped-key-file=path/to/target/key \
|
|
--algorithm=google-symmetric-encryption
|
|
"""
|
|
|
|
@staticmethod
|
|
def Args(parser):
|
|
flags.AddKeyResourceFlags(parser, 'The containing key to import into.')
|
|
flags.AddCryptoKeyVersionFlag(
|
|
parser, 'to re-import into. Omit this field for first-time import')
|
|
flags.AddRsaAesWrappedKeyFileFlag(parser, 'to import')
|
|
flags.AddWrappedKeyFileFlag(parser, 'to import')
|
|
flags.AddImportedVersionAlgorithmFlag(parser)
|
|
flags.AddRequiredImportJobArgument(parser, 'to import from')
|
|
flags.AddPublicKeyFileFlag(parser)
|
|
flags.AddTargetKeyFileFlag(parser)
|
|
|
|
def _ReadFile(self, path, max_bytes):
|
|
data = files.ReadBinaryFileContents(path)
|
|
if len(data) > max_bytes:
|
|
raise exceptions.BadFileException(
|
|
'The file is larger than the maximum size of {0} bytes.'.format(
|
|
max_bytes))
|
|
return data
|
|
|
|
def _IsSha2ImportMethod(self, import_method, messages):
|
|
return import_method in (
|
|
messages.ImportJob.ImportMethodValueValuesEnum.RSA_OAEP_3072_SHA256,
|
|
messages.ImportJob.ImportMethodValueValuesEnum.RSA_OAEP_4096_SHA256,
|
|
messages.ImportJob.ImportMethodValueValuesEnum
|
|
.RSA_OAEP_3072_SHA256_AES_256, messages.ImportJob
|
|
.ImportMethodValueValuesEnum.RSA_OAEP_4096_SHA256_AES_256)
|
|
|
|
def _IsRsaAesWrappingImportMethod(self, import_method, messages):
|
|
return import_method in (messages.ImportJob.ImportMethodValueValuesEnum
|
|
.RSA_OAEP_3072_SHA1_AES_256,
|
|
messages.ImportJob.ImportMethodValueValuesEnum
|
|
.RSA_OAEP_4096_SHA1_AES_256,
|
|
messages.ImportJob.ImportMethodValueValuesEnum
|
|
.RSA_OAEP_3072_SHA256_AES_256,
|
|
messages.ImportJob.ImportMethodValueValuesEnum
|
|
.RSA_OAEP_4096_SHA256_AES_256)
|
|
|
|
def _ReadPublicKeyBytes(self, args):
|
|
try:
|
|
return self._ReadFile(args.public_key_file, max_bytes=65536)
|
|
except files.Error as e:
|
|
raise exceptions.BadFileException(
|
|
'Failed to read public key file [{0}]: {1}'.format(
|
|
args.public_key_file, e))
|
|
|
|
def _FetchImportJob(self, args, import_job_name, client, messages):
|
|
import_job = client.projects_locations_keyRings_importJobs.Get(
|
|
messages.CloudkmsProjectsLocationsKeyRingsImportJobsGetRequest(
|
|
name=import_job_name))
|
|
if import_job.state != messages.ImportJob.StateValueValuesEnum.ACTIVE:
|
|
raise exceptions.BadArgumentException(
|
|
'import-job', 'Import job [{0}] is not active (state is {1}).'.format(
|
|
import_job_name, import_job.state))
|
|
return import_job
|
|
|
|
def _CkmRsaAesKeyWrap(self, import_method, public_key_bytes, target_key_bytes,
|
|
client, messages):
|
|
try:
|
|
# TODO(b/141249289): Move imports to the top of the file. In the
|
|
# meantime, until we're sure that all Cloud SDK users have the
|
|
# cryptography module available, let's not error out if we can't load the
|
|
# module unless we're actually going down this code path.
|
|
# pylint: disable=g-import-not-at-top
|
|
from cryptography.hazmat.primitives import serialization
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives import keywrap
|
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
from cryptography.hazmat.primitives import hashes
|
|
except ImportError:
|
|
log.err.Print('Cannot load the Pyca cryptography library. Either the '
|
|
'library is not installed, or site packages are not '
|
|
'enabled for the Google Cloud SDK. Please consult '
|
|
'https://cloud.google.com/kms/docs/crypto for further '
|
|
'instructions.')
|
|
sys.exit(1)
|
|
|
|
sha = hashes.SHA1()
|
|
if self._IsSha2ImportMethod(import_method, messages):
|
|
sha = hashes.SHA256()
|
|
|
|
# RSA-OAEP import methods have a maximum target key size that's a function
|
|
# of the RSA modulus size.
|
|
if not self._IsRsaAesWrappingImportMethod(import_method, messages):
|
|
if (
|
|
import_method
|
|
== messages.ImportJob.ImportMethodValueValuesEnum.RSA_OAEP_3072_SHA256
|
|
):
|
|
modulus_byte_length = 3072 // 8
|
|
elif (
|
|
import_method
|
|
== messages.ImportJob.ImportMethodValueValuesEnum.RSA_OAEP_4096_SHA256
|
|
):
|
|
modulus_byte_length = 4096 // 8
|
|
else:
|
|
raise ValueError('unexpected import method: {0}'.format(import_method))
|
|
# per go/rfc/8017#section-7.1.1
|
|
max_target_key_size = modulus_byte_length - (2 * sha.digest_size) - 2
|
|
if len(target_key_bytes) > max_target_key_size:
|
|
raise exceptions.BadFileException(
|
|
'target-key-file',
|
|
"The file is larger than the import method's maximum size of {0} "
|
|
'bytes.'.format(max_target_key_size),
|
|
)
|
|
|
|
aes_wrapped_key = b''
|
|
to_be_rsa_wrapped_key = target_key_bytes
|
|
public_key = serialization.load_pem_public_key(
|
|
public_key_bytes, backend=default_backend())
|
|
if self._IsRsaAesWrappingImportMethod(import_method, messages):
|
|
to_be_rsa_wrapped_key = os.urandom(32) # an ephemeral key
|
|
aes_wrapped_key = keywrap.aes_key_wrap_with_padding(
|
|
to_be_rsa_wrapped_key, target_key_bytes, default_backend())
|
|
rsa_wrapped_key = public_key.encrypt(
|
|
to_be_rsa_wrapped_key,
|
|
padding.OAEP(mgf=padding.MGF1(sha), algorithm=sha, label=None))
|
|
return rsa_wrapped_key + aes_wrapped_key
|
|
|
|
def Run(self, args):
|
|
client = cloudkms_base.GetClientInstance()
|
|
messages = cloudkms_base.GetMessagesModule()
|
|
import_job_name = flags.ParseImportJobName(args).RelativeName()
|
|
|
|
# set wrapped_key_file to wrapped_key_file or rsa_aes_wrapped_key_file
|
|
wrapped_key_file = None
|
|
if args.wrapped_key_file:
|
|
wrapped_key_file = args.wrapped_key_file
|
|
if args.rsa_aes_wrapped_key_file:
|
|
raise exceptions.OneOfArgumentsRequiredException(
|
|
('--wrapped-key-file', '--rsa-aes-wrapped-key-file'),
|
|
'Either wrapped-key-file or rsa-aes-wrapped-key-file should be provided.') # pylint: disable=line-too-long
|
|
else:
|
|
wrapped_key_file = args.rsa_aes_wrapped_key_file
|
|
|
|
if bool(wrapped_key_file) == bool(args.target_key_file):
|
|
raise exceptions.OneOfArgumentsRequiredException(
|
|
('--target-key-file', '--wrapped-key-file/--rsa-aes-wrapped-key-file'), # pylint: disable=line-too-long
|
|
'Either a pre-wrapped key or a key to be wrapped must be provided.')
|
|
|
|
wrapped_key_bytes = None
|
|
if wrapped_key_file:
|
|
try:
|
|
# This should be less than 64KiB.
|
|
wrapped_key_bytes = self._ReadFile(wrapped_key_file, max_bytes=65536)
|
|
except files.Error as e:
|
|
raise exceptions.BadFileException(
|
|
'Failed to read wrapped key file [{0}]: {1}'.format(
|
|
wrapped_key_file, e))
|
|
|
|
import_job = self._FetchImportJob(args, import_job_name, client, messages)
|
|
if args.target_key_file:
|
|
target_key_bytes = None
|
|
try:
|
|
# This should be less than 64KiB.
|
|
target_key_bytes = self._ReadFile(args.target_key_file, max_bytes=8192)
|
|
except files.Error as e:
|
|
raise exceptions.BadFileException(
|
|
'Failed to read target key file [{0}]: {1}'.format(
|
|
args.target_key_file, e))
|
|
|
|
# Read the public key off disk if provided, otherwise, fetch it from KMS.
|
|
public_key_bytes = None
|
|
if args.public_key_file:
|
|
public_key_bytes = self._ReadPublicKeyBytes(args)
|
|
else:
|
|
public_key_bytes = import_job.publicKey.pem.encode('ascii')
|
|
|
|
wrapped_key_bytes = self._CkmRsaAesKeyWrap(import_job.importMethod,
|
|
public_key_bytes,
|
|
target_key_bytes, client,
|
|
messages)
|
|
|
|
# Send the request to KMS.
|
|
req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysCryptoKeyVersionsImportRequest( # pylint: disable=line-too-long
|
|
parent=flags.ParseCryptoKeyName(args).RelativeName())
|
|
req.importCryptoKeyVersionRequest = messages.ImportCryptoKeyVersionRequest(
|
|
algorithm=maps.ALGORITHM_MAPPER_FOR_IMPORT.GetEnumForChoice(
|
|
args.algorithm),
|
|
importJob=import_job_name,
|
|
wrappedKey=wrapped_key_bytes)
|
|
|
|
if args.version:
|
|
req.importCryptoKeyVersionRequest.cryptoKeyVersion = flags.ParseCryptoKeyVersionName(
|
|
args).RelativeName()
|
|
|
|
return client.projects_locations_keyRings_cryptoKeys_cryptoKeyVersions.Import(
|
|
req)
|