1# -*- coding: utf-8 -*- # 2# Copyright 2019 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Import a provided key from file into KMS using an Import Job.""" 16 17from __future__ import absolute_import 18from __future__ import division 19from __future__ import unicode_literals 20 21import os 22import sys 23 24from googlecloudsdk.api_lib.cloudkms import base as cloudkms_base 25from googlecloudsdk.calliope import base 26from googlecloudsdk.calliope import exceptions 27from googlecloudsdk.command_lib.kms import flags 28from googlecloudsdk.command_lib.kms import maps 29from googlecloudsdk.core import log 30from googlecloudsdk.core.util import files 31 32 33class Import(base.Command): 34 r"""Import a version into an existing crypto key. 35 36 Imports wrapped key material into a new version within an existing crypto key 37 following the import procedure documented at 38 https://cloud.google.com/kms/docs/importing-a-key. 39 40 ## EXAMPLES 41 42 The following command will read the files 'path/to/ephemeral/key' and 43 'path/to/target/key' and use them to create a new version with algorithm 44 'google-symmetric-encryption' within the 'frodo' crypto key, 'fellowship' 45 keyring, and 'us-central1' location using import job 'strider' to unwrap the 46 provided key material. 47 48 $ {command} --location=global \ 49 --keyring=fellowship \ 50 --key=frodo \ 51 --import-job=strider \ 52 --rsa-aes-wrapped-key-file=path/to/target/key \ 53 --algorithm=google-symmetric-encryption 54 """ 55 56 @staticmethod 57 def Args(parser): 58 flags.AddKeyResourceFlags(parser, 'The containing key to import into.') 59 flags.AddRsaAesWrappedKeyFileFlag(parser, 'to import') 60 flags.AddImportedVersionAlgorithmFlag(parser) 61 flags.AddRequiredImportJobArgument(parser, 'to import from') 62 flags.AddOptionalPublicKeyFileArgument(parser) 63 flags.AddOptionalTargetKeyFileArgument(parser) 64 65 def _ReadFile(self, path, max_bytes): 66 data = files.ReadBinaryFileContents(path) 67 if len(data) > max_bytes: 68 raise exceptions.BadFileException( 69 'The file is larger than the maximum size of {0} bytes.'.format( 70 max_bytes)) 71 return data 72 73 def _ReadOrFetchPublicKeyBytes(self, args, import_job_name): 74 client = cloudkms_base.GetClientInstance() 75 messages = cloudkms_base.GetMessagesModule() 76 # If the public key was provided, read it off disk. Otherwise, fetch it from 77 # KMS. 78 public_key_bytes = None 79 if args.public_key_file: 80 try: 81 public_key_bytes = self._ReadFile( 82 args.public_key_file, max_bytes=65536) 83 except files.Error as e: 84 raise exceptions.BadFileException( 85 'Failed to read public key file [{0}]: {1}'.format( 86 args.public_key_file, e)) 87 else: 88 import_job = client.projects_locations_keyRings_importJobs.Get( # pylint: disable=line-too-long 89 messages.CloudkmsProjectsLocationsKeyRingsImportJobsGetRequest( 90 name=import_job_name)) 91 if import_job.state != messages.ImportJob.StateValueValuesEnum.ACTIVE: 92 raise exceptions.BadArgumentException( 93 'import-job', 94 'Import job [{0}] is not active (state is {1}).'.format( 95 import_job_name, import_job.state)) 96 public_key_bytes = import_job.publicKey.pem.encode('ascii') 97 return public_key_bytes 98 99 def _CkmRsaAesKeyWrap(self, public_key_bytes, target_key_bytes): 100 try: 101 # TODO(b/141249289): Move imports to the top of the file. In the 102 # meantime, until we're sure that all Cloud SDK users have the 103 # cryptography module available, let's not error out if we can't load the 104 # module unless we're actually going down this code path. 105 # pylint: disable=g-import-not-at-top 106 from cryptography.hazmat.primitives import serialization 107 from cryptography.hazmat.backends import default_backend 108 from cryptography.hazmat.primitives import keywrap 109 from cryptography.hazmat.primitives.asymmetric import padding 110 from cryptography.hazmat.primitives import hashes 111 except ImportError: 112 log.err.Print('Cannot load the Pyca cryptography library. Either the ' 113 'library is not installed, or site packages are not ' 114 'enabled for the Google Cloud SDK. Please consult ' 115 'https://cloud.google.com/kms/docs/crypto for further ' 116 'instructions.') 117 sys.exit(1) 118 119 public_key = serialization.load_pem_public_key( 120 public_key_bytes, backend=default_backend()) 121 ephem_key = os.urandom(32) 122 wrapped_ephem_key = public_key.encrypt(ephem_key, 123 padding.OAEP( 124 mgf=padding.MGF1( 125 algorithm=hashes.SHA1()), 126 algorithm=hashes.SHA1(), 127 label=None)) 128 wrapped_target_key = keywrap.aes_key_wrap_with_padding(ephem_key, 129 target_key_bytes, 130 default_backend()) 131 return wrapped_ephem_key + wrapped_target_key 132 133 def Run(self, args): 134 client = cloudkms_base.GetClientInstance() 135 messages = cloudkms_base.GetMessagesModule() 136 import_job_name = flags.ParseImportJobName(args).RelativeName() 137 138 if bool(args.rsa_aes_wrapped_key_file) == bool(args.target_key_file): 139 raise exceptions.OneOfArgumentsRequiredException( 140 ('--target-key-file', '--rsa-aes-wrapped-key-file'), 141 'Either a pre-wrapped key or a key to be wrapped must be provided.') 142 143 rsa_aes_wrapped_key_bytes = None 144 if args.rsa_aes_wrapped_key_file: 145 try: 146 # This should be less than 64KiB. 147 rsa_aes_wrapped_key_bytes = self._ReadFile( 148 args.rsa_aes_wrapped_key_file, max_bytes=65536) 149 except files.Error as e: 150 raise exceptions.BadFileException( 151 'Failed to read rsa_aes_wrapped_key_file [{0}]: {1}'.format( 152 args.wrapped_target_key_file, e)) 153 154 if args.target_key_file: 155 public_key_bytes = self._ReadOrFetchPublicKeyBytes(args, import_job_name) 156 target_key_bytes = None 157 try: 158 # This should be less than 64KiB. 159 target_key_bytes = self._ReadFile( 160 args.target_key_file, max_bytes=8192) 161 except files.Error as e: 162 raise exceptions.BadFileException( 163 'Failed to read target key file [{0}]: {1}'.format( 164 args.target_key_file, e)) 165 rsa_aes_wrapped_key_bytes = self._CkmRsaAesKeyWrap(public_key_bytes, 166 target_key_bytes) 167 168 # Send the request to KMS. 169 req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysCryptoKeyVersionsImportRequest( # pylint: disable=line-too-long 170 parent=flags.ParseCryptoKeyName(args).RelativeName()) 171 req.importCryptoKeyVersionRequest = messages.ImportCryptoKeyVersionRequest( 172 algorithm=maps.ALGORITHM_MAPPER_FOR_IMPORT.GetEnumForChoice( 173 args.algorithm), 174 importJob=import_job_name, 175 rsaAesWrappedKey=rsa_aes_wrapped_key_bytes) 176 177 return client.projects_locations_keyRings_cryptoKeys_cryptoKeyVersions.Import( 178 req) 179