From b848f24ac800202c8b0ed3c773c3fbba87163b62 Mon Sep 17 00:00:00 2001 From: Yishi Wang Date: Thu, 8 May 2025 14:32:46 +0800 Subject: [PATCH 1/2] az keyvault key get-attestation: Support getting a MHSM key's attestation --- .../cli/command_modules/keyvault/_help.py | 6 + .../cli/command_modules/keyvault/_params.py | 9 +- .../command_modules/keyvault/_transformers.py | 5 + .../cli/command_modules/keyvault/commands.py | 1 + .../cli/command_modules/keyvault/custom.py | 19 ++- .../test_keyvault_hsm_key_attestation.yaml | 143 ++++++++++++++++++ .../tests/latest/test_keyvault_commands.py | 21 +++ 7 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/recordings/test_keyvault_hsm_key_attestation.yaml diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/_help.py b/src/azure-cli/azure/cli/command_modules/keyvault/_help.py index 7875c681487..784115a283e 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/_help.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/_help.py @@ -456,6 +456,12 @@ material of a key itself cannot be changed. This operation requires the keys/update permission. """ +helps['keyvault key get-attestation'] = """ +type: command +short-summary: Get a key's attestation blob. +long-summary: This command is applicable to any key stored in Azure Key Vault Managed HSM. This operation requires the keys/get permission. +""" + helps['keyvault key show-deleted'] = """ type: command short-summary: Get the public part of a deleted key. diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/_params.py b/src/azure-cli/azure/cli/command_modules/keyvault/_params.py index 3f95b25cb90..be14918ab35 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/_params.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/_params.py @@ -288,7 +288,7 @@ class CLISecurityDomainOperation(str, Enum): # keys track2 for scope in ['create', 'import', 'set-attributes', 'show', 'show-deleted', 'delete', 'list', 'list-deleted', 'list-versions', 'encrypt', 'decrypt', 'sign', 'verify', 'recover', 'purge', 'download', - 'backup', 'restore', 'rotate', 'rotation-policy show', 'rotation-policy update']: + 'backup', 'restore', 'rotate', 'get-attestation', 'rotation-policy show', 'rotation-policy update']: with self.argument_context('keyvault key {}'.format(scope), arg_group='Id') as c: c.argument('name', options_list=['--name', '-n'], id_part='child_name_1', required=False, completer=get_keyvault_name_completion_list('key'), @@ -315,6 +315,13 @@ class CLISecurityDomainOperation(str, Enum): help='The recovery id of the key. If specified all other \'Id\' arguments should be omitted.', validator=validate_keyvault_resource_id('key')) + with self.argument_context('keyvault key get-attestation') as c: + c.argument('file_path', options_list=['--file', '-f'], type=file_type, completer=FilesCompleter(), + help="File to receive the key's attestation contents.") + c.extra('hsm_name', data_plane_hsm_name_type, required=False, arg_group='Id', + help='Name of the HSM. Required if --id is not specified.') + c.ignore('vault_base_url') + with self.argument_context('keyvault key list') as c: c.extra('include_managed', arg_type=get_three_state_flag(), default=False, help='Include managed keys.') diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/_transformers.py b/src/azure-cli/azure/cli/command_modules/keyvault/_transformers.py index 98647ab3d03..921f81ee873 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/_transformers.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/_transformers.py @@ -122,6 +122,11 @@ def transform_key_output(result, **command_args): if value and isinstance(value, bytes): setattr(result.key, attr, base64.b64encode(value)) + # Avoid returning attestation info together with key properties + # Customer should use specific `az keyvault key get-attestation` command + if result.properties._attributes: + result.properties._attributes.attestation = None + output = { 'attributes': result.properties._attributes, 'key': result.key, diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/commands.py b/src/azure-cli/azure/cli/command_modules/keyvault/commands.py index ce0fca3c807..109a46fd14e 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/commands.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/commands.py @@ -153,6 +153,7 @@ def load_command_table(self, _): g.keyvault_custom('create', 'create_key', transform=transform_key_output, validator=validate_key_create) g.keyvault_command('set-attributes', 'update_key_properties', transform=transform_key_output) g.keyvault_command('show', 'get_key', transform=transform_key_output) + g.keyvault_custom('get-attestation', 'get_key_attestation') g.keyvault_custom('import', 'import_key', transform=transform_key_output) g.keyvault_custom('get-policy-template', 'get_policy_template', is_preview=True) g.keyvault_custom('encrypt', 'encrypt_key', is_preview=True, transform=transform_key_encryption_output) diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/custom.py b/src/azure-cli/azure/cli/command_modules/keyvault/custom.py index 973ac8c3eb7..fe6f5163d8d 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/custom.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/custom.py @@ -35,7 +35,7 @@ from cryptography.x509 import load_pem_x509_certificate from knack.log import get_logger -from knack.util import CLIError +from knack.util import CLIError, todict logger = get_logger(__name__) @@ -1101,6 +1101,23 @@ def list_keys(client, maxresults=None, include_managed=False): return [_ for _ in result if not getattr(_, 'managed')] if result else result return result +def get_key_attestation(client, name, version=None, file_path=None): + key = client.get_key_attestation(name=name, version=version) + key_attestation = key.properties.attestation + if not file_path: + return key_attestation + + if os.path.isfile(file_path) or os.path.isdir(file_path): + raise CLIError("File or directory named '{}' already exists.".format(file_path)) + + try: + from ._command_type import _encode_hex + with open(file_path, 'w') as outfile: + json.dump(todict(_encode_hex(key_attestation)), outfile) + except Exception as ex: # pylint: disable=broad-except + if os.path.isfile(file_path): + os.remove(file_path) + raise ex def delete_key(client, name): return client.begin_delete_key(name).result() diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/recordings/test_keyvault_hsm_key_attestation.yaml b/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/recordings/test_keyvault_hsm_key_attestation.yaml new file mode 100644 index 00000000000..48294401556 --- /dev/null +++ b/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/recordings/test_keyvault_hsm_key_attestation.yaml @@ -0,0 +1,143 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - keyvault key create + Connection: + - keep-alive + Content-Length: + - '0' + Content-Type: + - application/json + ParameterSetName: + - --hsm-name -n + User-Agent: + - AZURECLI/2.72.0 azsdk-python-core/1.31.0 Python/3.11.9 (Windows-10-10.0.26100-SP0) + method: POST + uri: https://clitesthsmkeyats000002.managedhsm.azure.net/keys/key1-000003/create?api-version=7.6-preview.2 + response: + body: + string: '' + headers: + cache-control: + - no-cache + content-length: + - '0' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + www-authenticate: + - Bearer authorization="https://login.microsoftonline.com/b5ee6c06-c2c2-4e3c-8606-5f170cee077a", + resource="https://managedhsm.azure.net" + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-server-latency: + - '2' + status: + code: 401 + message: Unauthorized +- request: + body: '{"kty": "RSA", "attributes": {"enabled": true}}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - keyvault key create + Connection: + - keep-alive + Content-Length: + - '47' + Content-Type: + - application/json + ParameterSetName: + - --hsm-name -n + User-Agent: + - AZURECLI/2.72.0 azsdk-python-core/1.31.0 Python/3.11.9 (Windows-10-10.0.26100-SP0) + method: POST + uri: https://clitesthsmkeyats000002.managedhsm.azure.net/keys/key1-000003/create?api-version=7.6-preview.2 + response: + body: + string: '{"attributes":{"created":1746685312,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1746685312},"key":{"e":"AQAB","key_ops":["wrapKey","decrypt","encrypt","unwrapKey","sign","verify"],"kid":"https://clitesthsmkeyats000002.managedhsm.azure.net/keys/key1-000003/7071fabc480c0ca186f786a9b78c9456","kty":"RSA-HSM","n":"iHYH_d5bhETKeSc5PJ1_f3bvF3JfmpNfrK7MAjoAvOiIyQSamA-5-YPwA-KZRE9rDh9g6ns8NAELvG7B516r3AO1fBsflP_fTeIMZa-xDS61RMldmXCCDD8OPkAvKqzgSpHOIbg5UJh9ODGtBssydlS_77fqWZQPEjkkN3frWvvpJZmr3THifV4vkXAKwHld2nvFgVQRlx_BTc8S92lXSxZ3kTU0t9aayoS-2BN8s0o_ZC14-UfiP5IB0xkjELfOa897aZ_I3djdvWKdWbJsObiSHMZCQ9VEIFL6i8Ih9_SeU6H7ORwwMKvjjExq-O_vrUC64V2zYs5lqX1GY8-M-Q"}}' + headers: + cache-control: + - no-cache + content-length: + - '726' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=167.220.255.14;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - uksouth + x-ms-server-latency: + - '85' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + CommandName: + - keyvault key get-attestation + Connection: + - keep-alive + ParameterSetName: + - --hsm-name --name + User-Agent: + - AZURECLI/2.72.0 azsdk-python-core/1.31.0 Python/3.11.9 (Windows-10-10.0.26100-SP0) + method: GET + uri: https://clitesthsmkeyats000002.managedhsm.azure.net/keys/key1-000003/attestation?api-version=7.6-preview.2 + response: + body: + string: '{"attributes":{"attestation":{"certificatePemFile":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURnRENDQW1nQ0FRRXdEUVlKS29aSWh2Y05BUUVMQlFBd2daRXhDekFKQmdOVkJBWVRBbFZUTVJNd0VRWUQKVlFRSURBcERZV3hwWm05eWJtbGhNUkV3RHdZRFZRUUhEQWhUWVc0Z1NtOXpaVEVWTUJNR0ExVUVDZ3dNUTJGMgphWFZ0TENCSmJtTXVNUmN3RlFZRFZRUUxEQTVNYVhGMWFXUlRaV04xY21sMGVURXFNQ2dHQTFVRUF3d2hiRzlqCllXeGpZUzVzYVhGMWFXUnpaV04xY21sMGVTNWpZWFpwZFcwdVkyOXRNQjRYRFRJeE1URXhNakU0TXpZeU1sb1gKRFRNeE1URXhNREU0TXpZeU1sb3dlakZFTUFrR0ExVUVCaE1DVlZNd0NRWURWUVFJREFKRFFUQU5CZ05WQkFvTQpCa05oZG1sMWJUQU5CZ05WQkFzTUJrNHpSa2xRVXpBT0JnTlZCQWNNQjFOaGJrcHZjMlV4TWpBd0JnTlZCQU1NCktVaFRUVG8xTGpOSE1qRTBNaTFKUTAwd01ETTFOalVzSUdadmNpQnViMjR0UmtsUVV5QnRiMlJsTUlJQklqQU4KQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbjA2UkhOSUE4bWI0SkNKWjQxN3pMRjNIRzIrLwpCMCtYZFkvRjRHTGNtY3htblkyVkxUeGM3Q2MvR0VaMnl1dFlUVzdCckhTNldiQjlUZ3lHNVJCMXlCcGpSQ3hGCmthWWhPdVp4ZWJEWThRSXM0Z1ZzRjlya0hCUjgvSDIxb1RDd1FZc3I4TjZDS0ZuQkdvY2R4MXV6a3VDcWRlK1EKR0hzSE1MUk10SXljK0M0dnJVczYrdHpsUjhkSVc4ZE44OTlsbnkzdnNNVUo0c0MraFJ3cXBNUGRrZEQrbnJTSQppeUlrK0FyS1NqV1ZHcitmN0pmZEJ5N0VYaFBtVTZSTmRpQ0ZlSUlCUHRLZERkcThld0pQWHB5dC9qTDR0aDZWClA1ekpCYm0wVDJ5ekk2VGFGQVhDbzRLV1RqeUptSy9HdUFRSnJqVzJKcjlXWnRwN1paTTc3alRSNFFJREFRQUIKTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFEUmxyeUJva2I1YlRic2luYjY3SWo5bU14b1F2M3U0WmcyVkFjKwpUWXdubzZtSzVLa1I1WTdFeHlUUklYV1Z1Y2dENzBnTTFJR1RkQlZzV0VxaC83UCt3ZkFVMHZ0Ykk2ZVk5V3czCmExc3J0OWIySG9SdDcwUFN1MUtGZVdWZmpZZUMyYy9oaHU5dWFSN0xRU0ZPTjhHK2svVEpGUi85am1YaGNUL2cKZi9yeHYrSy8zRm1WOU5HRlBsUHdWalpNc1BXbGR4NVloN0MwczJQMFR0bkJubGhxeUZVRldWKzJQVDNhczRjVApaTFFsdDZNLzhlemJoZll5UlM2NU5ZaWJEVnI5VG9TSG5qZ09iMjBua1hYMHFGYnlJSm83YStMZHhSSXBNYmJiCnRIQ1ljOENKRUlNK2N6UUFUN3lqWXpFcXB1dFh4ZHdndndXSDdzSnByUk1WRTM4QwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCi0tLS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlEZ2pDQ0FtcWdBd0lCQWdJQkFUQU5CZ2txaGtpRzl3MEJBUXNGQURCNk1VUXdDUVlEVlFRR0V3SlZVekFKCkJnTlZCQWdNQWtOQk1BMEdBMVVFQ2d3R1EyRjJhWFZ0TUEwR0ExVUVDd3dHVGpOR1NWQlRNQTRHQTFVRUJ3d0gKVTJGdVNtOXpaVEV5TURBR0ExVUVBd3dwU0ZOTk9qVXVNMGN5TVRReUxVbERUVEF3TXpVMk5Td2dabTl5SUc1dgpiaTFHU1ZCVElHMXZaR1V3SGhjTk1qVXdOVEE0TURZeE1qUXlXaGNOTXpVd05UQTJNRFl4TWpReVdqQ0JqakZFCk1Ba0dBMVVFQmhNQ1ZWTXdDUVlEVlFRSURBSkRRVEFOQmdOVkJBb01Ca05oZG1sMWJUQU5CZ05WQkFzTUJrNHoKUmtsUVV6QU9CZ05WQkFjTUIxTmhia3B2YzJVeFJqQkVCZ05WQkFNTVBVaFRUVHBDTmpoR01UbEdOemczTkRGRwpOVEJETXpJelJqazBRa1l5TkRZM05URTZVRUZTVkU0Nk1Td2dabTl5SUc1dmJpMUdTVkJUSUcxdlpHVXdnZ0VpCk1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ1RrbEVEVXdpQjNBaklIakFZUG8xc0pWNWQKUDdsVnY5QkVmZFhuWDNuQzFCRjVyVHFORis3cjVGTG41dXN2d3NVN25pNkJFVHowWUZTeGRKTTJyUXhHdThWagp0bGduMDBZcnBOeWJDMkF5dUtUcVRzdU0rNEdaU2U0MzFSQ3lhbmJnWVAybzJyazY5dldYSkpyVXdYRGhWZDhlCnZ6Q01ZeWx5VFpPempvcVptSXpxa29BOERzR2c0aW9QT2VyalBVblhzS3hxdGZlblVsRW9yOW5pYVFyZzZpWCsKVmhmdzdzcmF4MHRpU2JrRGEyYjcvcHNSYWRUQUhzTTZ2L1MyMVI4TzFqalVDVWErbzFDMUhldHB1ZXMzaWFaQgphR2RCdGFMSjVxcUhvb3ZzY1V6MUVNamszaGZTLzFXREVBWnU0MnpsendSZ0FtVzM2RjB3bEFSTlp6UWJBZ01CCkFBRXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQzhjQy9zdG9RNXp5eWlXaU91VU9VQWtoSTQxOU9lRXJzZE0KTGpJRjJaVGlIZUtOQ3VNd0FvRmpNckhtQVd5OUljcnJRUm8yeTNmK3cwNHYwU3ZLOUFMRU1aN2J4dDNpZ3U5SQpwQWZQM0RySVpMOUN4N281TG96MElzSUh0TElRM1dKWXA2dVVTeE43Y0F4aUxvT0VFdkJRc2YybVRLN1hreVlHCjZVdjB4V2I5eVJ3cVRvZ2JzYkNrZGhvdEM3Qmg1OGMveDVWd0c1Z0VrczRxZUxuaE1kZTVzMkFSem9ra1ZudU4KZHFzOEpQdk9BK1VIZmVDQWRHWjdGTTVjWFRxdjhxVFlnUldROWtER3RaRzlRUUszWWdpRzZLdFplV3VYUjNwawpRQTJqalB6T0oraGRMQ3pBQUQ1UGNnM2pxUWtCNHV6b01kNkFONWFDc1BvZGN0ZnBjbVE9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR6akNDQXJhZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREIzTVFzd0NRWURWUVFHRXdKVlV6RVQKTUJFR0ExVUVDQXdLVjJGemFHbHVaM1J2YmpFUU1BNEdBMVVFQnd3SFVtVmtiVzl1WkRFZU1Cd0dBMVVFQ2d3VgpUV2xqY205emIyWjBJRU52Y25CdmNtRjBhVzl1TVNFd0h3WURWUVFEREJoamJHbDBaWE4wYUhOdGEyVjVZWFJ6ClluWjZjR1p5ZW0wd0lCY05NalV3TlRBNE1EWXhOREl6V2hnUE1qQTFNREExTURJd05qRTBNak5hTUhjeEN6QUoKQmdOVkJBWVRBbFZUTVJNd0VRWURWUVFJREFwWFlYTm9hVzVuZEc5dU1SQXdEZ1lEVlFRSERBZFNaV1J0YjI1awpNUjR3SEFZRFZRUUtEQlZOYVdOeWIzTnZablFnUTI5eWNHOXlZWFJwYjI0eElUQWZCZ05WQkFNTUdHTnNhWFJsCmMzUm9jMjFyWlhsaGRITmlkbnB3Wm5KNmJUQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0MKZ2dFQkFKZ3FQejRqbjdkUldneER6NlR1VytxQ2Zib21KeWpHQitTY05Fc2FoYmJtQ2tBelJ3WWpoSmcrdFp5NQp0T3dkWGdCV2tsNGViZ1Q2NnBIMmxNRUcrNjU2S3lFaWF1aVJ0TCsyVGw2UHY1ekZWVzBLN2VDYjd4TDlnd1pECk4vbFdiVUo2TTU5ZXBpcmVxL1dQUmlkOUovNXI0QW5sR3YxaHhtaXVPM1dRbDU2SHBnOVE1Vm10SHhwMWtmdUcKL2x0K21LeGlFTWdobEhJaVZ2UXY2cDJUQ3ZNbkNmVlJscnhOdXIvTWtQUTVES0lRSEp2SlhHSEtOU1dqSTV3ZQpneHMva0EyR282dnVkN1c5dGtBVUZNcllQdU1vL0FpSFB2aGZiSVZmdTJqTnhNanVhN0hTYVBGb29mcmMydjNmCmx1cEJLR1AzNEJKRm9QSmc4VHJTdGZLNFhtTUNBd0VBQWFOak1HRXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QU8KQmdOVkhROEJBZjhFQkFNQ0FvUXdIUVlEVlIwT0JCWUVGQ3lZSnQ5Z2tCZ0ZuMFV5Y3BNak5ieEZDM3M0TUI4RwpBMVVkSXdRWU1CYUFGQ3lZSnQ5Z2tCZ0ZuMFV5Y3BNak5ieEZDM3M0TUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCCkFRQmFFWCtaQk9LRkxnYXRTMjlpTldXWkRvQ2lxaGZQUE16WWlnRkF6ZEw1eWFzaVhqZkFtQ28rYlBrZHovVkkKZHYvM2xRKzlrQ29veGh4L28weWVKSXNYbzZaWW5JRTN2aE5tR05FZEdpcUVBSG1OUFJuYUhEcThncEVrbllJcQpFdkt4SHF5TnludXp4VDlLK0wyMnJpcGRFNXIyTjJVd0hTanpNZ3ZJOFlxOHJPU3BhWHpTSnVkcCtTWXZ6VlI0Ck96RHBMUlIyamR2MFZOdmNyZ2x4QjhNNXhjWjRrNXN0R3RDalg0VG5yR1h1dTVySkRGSmUwRExrOTd3VUp6enoKclBkZHJLUzZpYVFZbWpKT3RzK0tSeHZxQ25hYTZRSndpMnFIQ1JhR2QxcjFDRDVWZTVtK21QUjNjWUg1YUdvaQpyR3NyL2s1ejUvaG1FTXN1WGpOSG9QWDAKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQotLS0tLUJFR0lOIENFUlRJRklDQVRFLS0tLS0KTUlJRDRqQ0NBc3FnQXdJQkFnSUlmLy8vLy8vLy8vOHdEUVlKS29aSWh2Y05BUUVMQlFBd2R6RUxNQWtHQTFVRQpCaE1DVlZNeEV6QVJCZ05WQkFnTUNsZGhjMmhwYm1kMGIyNHhFREFPQmdOVkJBY01CMUpsWkcxdmJtUXhIakFjCkJnTlZCQW9NRlUxcFkzSnZjMjltZENCRGIzSndiM0poZEdsdmJqRWhNQjhHQTFVRUF3d1lZMnhwZEdWemRHaHoKYld0bGVXRjBjMkoyZW5CbWNucHRNQ0FYRFRJMU1EVXdPREEyTVRRek0xb1lEekl3TlRBd05UQXlNRFl4TkRNegpXakNCbGpFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVE4d0RRWURWUVFLREFaRFlYWnBkVzB4CkR6QU5CZ05WQkFzTUJrNHpSa2xRVXpFUU1BNEdBMVVFQnd3SFUyRnVTbTl6WlRGR01FUUdBMVVFQXd3OVNGTk4KT2tJMk9FWXhPVVkzT0RjME1VWTFNRU16TWpOR09UUkNSakkwTmpjMU1UcFFRVkpVVGpveExDQm1iM0lnYm05dQpMVVpKVUZNZ2JXOWtaVENDQVNJd0RRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFKT1NVUU5UCkNJSGNDTWdlTUJnK2pXd2xYbDAvdVZXLzBFUjkxZWRmZWNMVUVYbXRPbzBYN3V2a1V1Zm02eS9DeFR1ZUxvRVIKUFBSZ1ZMRjBremF0REVhN3hXTzJXQ2ZUUml1azNKc0xZREs0cE9wT3k0ejdnWmxKN2pmVkVMSnFkdUJnL2FqYQp1VHIyOVpja210VEJjT0ZWM3g2L01JeGpLWEpOazdPT2lwbVlqT3FTZ0R3T3dhRGlLZzg1NnVNOVNkZXdyR3ExCjk2ZFNVU2l2MmVKcEN1RHFKZjVXRi9EdXl0ckhTMkpKdVFOclp2ditteEZwMU1BZXd6cS85TGJWSHc3V09OUUoKUnI2alVMVWQ2Mm01NnplSnBrRm9aMEcxb3NubXFvZWlpK3h4VFBVUXlPVGVGOUwvVllNUUJtN2piT1hQQkdBQwpaYmZvWFRDVUJFMW5OQnNDQXdFQUFhTlFNRTR3REFZRFZSMFRBUUgvQkFJd0FEQWRCZ05WSFE0RUZnUVVvbnJKCmVCOUFPdDgvOGZudStGZGRKcmhuRElNd0h3WURWUjBqQkJnd0ZvQVVMSmdtMzJDUUdBV2ZSVEp5a3lNMXZFVUwKZXpnd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFDMVluVUw4SmxHZTdvdGZuK1B6VVJXV1FQbDhXS20xOWJoRQpvTTNjdFVueGVIRkVUVjREcjZ1NjI1U1dMUWJEUGVhcDMzMFgwaW5OVlk2QjlLMFdPVzM1RDBuMlpzRnBJTWxuCnJCazU2dXkxRWdCY0hkV2QyZ1dHRlVFWHhQOG1BY2pJY1diakVON0llVFkvdTBzdUtFYjRueUk2dHlLeVRuSVMKK2NpZXE5U1JRL1Rka2xjZUtObkx6TWtqL2hOYjdDTE4vTHAvN3JCQk1hMjhQZlI0bENDOEc4UUtjQlFoSVBzTApscGNObEdiaFBqa0srUUVZbktsVFlzaUk0N2ZVcStYY3FZTGpFZjFJeUMwWWdaNXlnallpNFk1WE92eVZqc3NSClFuVDFZRUVEVGkyNk9tZHVEMU1xVFJsdGhTVDBMdzAreFUwc3ZNcTFMK2dSUUhaMkRLTT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo","privateKeyAttestation":"AAAAAAAAAAAAAAAAAAAAAAAAA9cAAAAOAAAALQAAA8sAAAAAAAAAAQMAAAEAAAAAAQAAAAABAAAAAQEAAAACAAAAAQEAAAEEAAAAAQAAAAEFAAAAAQEAAAEGAAAAAQAAAAEHAAAAAQEAAAEIAAAAAQEAAAEKAAAAAQAAAAFjAAAAAQEAAAEDAAAAAQEAAAFiAAAAAQAAAAADAAAAgGN1c3RvbWVyX2tleQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgAAAIAva2V5cy9rZXkxLWhuaGdzbGxpbGZjbDNwZHRwbGQvNzA3MWZhYmM0ODBjMGNhMTg2Zjc4NmE5Yjc4Yzk0NTYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAA0AAAABAfAAAA8AAAABAQAAAWEAAAAEAAAEwQAAAXMAAAADwKWhAAABIAAAAQCIdgf93luERMp5Jzk8nX9_du8Xcl-ak1-srswCOgC86IjJBJqYD7n5g_AD4plET2sOH2Dqezw0AQu8bsHnXqvcA7V8Gx-U_99N4gxlr7ENLrVEyV2ZcIIMPw4-QC8qrOBKkc4huDlQmH04Ma0GyzJ2VL_vt-pZlA8SOSQ3d-ta--klmavdMeJ9Xi-RcArAeV3ae8WBVBGXH8FNzxL3aVdLFneRNTS31prKhL7YE3yzSj9kLXj5R-I_kgHTGSMQt85rz3tpn8jd2N29Yp1Zsmw5uJIcxkJD1UQgUvqLwiH39J5Tofs5HDAwq-OMTGr47--tQLrhXbNizmWpfUZjz4z5AAABIQAAAAQAAAgAAAABIgAAAAMBAAGAAAAAAAAABAAAAoAAAACGAAAAAQAAAAIQAAAAAQAAAAFwAAAAAQEAAAFmAAAABAAAAAqAAAACAAAAAQCAAAADAAAAAQAAAAICAAAAAQCAAAAEAAAAAQEAABAAAAAACAAAAAAAAAAAAAABCQAAAAEAAAABCwAAAAEAEAAB9QAAAAEAAAADAQAAAAT_____AAADAAAAAAT_____AAADAgAAAAT_____AAADAwAAAAT_____AAADCAAAAAT_____AAADBAAAAAT_____AAABDAAAAAEAAAABZQAAAAEBAAABZAAAAAEBAAABcgAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFI3LLo3a5qBHKzvvS3ADgOrcuiduYN-iHIlmj08xEAwSM_CbDrw4M8Uk2G9fk5kMhGpkX7sM-CCf6may9i-ZUVt8fjtEeGORylHmCgE7OOl_gIuZcC0hrm67veLGbi7nVYXky3lHu4NSl_oX0ZTsqkdtH4GdvIBUbUKavH78nn3ONGq4Dzp6qUlPngeU5ELAHWl_OcXypXHddRCcbWCx4i4E8uMZcyfGamMzkFCjNPm3WGMVqVI6KERTj3n2G7dhEwqbbgHb8-JpdVCMXIGTPWogXNoNMt9CkAEnL07nhl2iVytFRe97naBn4x5yWr4iFLas5vSa5iVOfTb7CtIZC4","publicKeyAttestation":"AAAAAAAAAAAAAAAAAAAAAAAAA8sAAAANAAAAKwAAA78AAAAAAAAAAQIAAAEAAAAAAQAAAAABAAAAAQEAAAACAAAAAQEAAAEEAAAAAQEAAAEFAAAAAQAAAAEGAAAAAQEAAAEHAAAAAQAAAAEIAAAAAQAAAAEKAAAAAQEAAAFjAAAAAQEAAAEDAAAAAQAAAAFiAAAAAQEAAAADAAAAgGN1c3RvbWVyX2tleQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgAAAIAva2V5cy9rZXkxLWhuaGdzbGxpbGZjbDNwZHRwbGQvNzA3MWZhYmM0ODBjMGNhMTg2Zjc4NmE5Yjc4Yzk0NTYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAA0AAAABAPAAAA8AAAABAAAAAWEAAAAEAAACAAAAAXMAAAADwKWhAAABIAAAAQCIdgf93luERMp5Jzk8nX9_du8Xcl-ak1-srswCOgC86IjJBJqYD7n5g_AD4plET2sOH2Dqezw0AQu8bsHnXqvcA7V8Gx-U_99N4gxlr7ENLrVEyV2ZcIIMPw4-QC8qrOBKkc4huDlQmH04Ma0GyzJ2VL_vt-pZlA8SOSQ3d-ta--klmavdMeJ9Xi-RcArAeV3ae8WBVBGXH8FNzxL3aVdLFneRNTS31prKhL7YE3yzSj9kLXj5R-I_kgHTGSMQt85rz3tpn8jd2N29Yp1Zsmw5uJIcxkJD1UQgUvqLwiH39J5Tofs5HDAwq-OMTGr47--tQLrhXbNizmWpfUZjz4z5AAABIQAAAAQAAAgAAAABIgAAAAMBAAEAAACGAAAAAQAAAAIQAAAAAQAAAAFwAAAAAQEAAAFmAAAABAAAAAqAAAACAAAAAQCAAAADAAAAAQCAAAAEAAAAAQEAABAAAAAACAAAAAAAAAAAAAABCQAAAAEAAAABCwAAAAEAEAAB9QAAAAEAAAADAQAAAAT_____AAADAAAAAAT_____AAADAgAAAAT_____AAADAwAAAAT_____AAADCAAAAAT_____AAADBAAAAAT_____AAABDAAAAAEAAAABZQAAAAEAAAABZAAAAAEAAAABcgAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHcnxgfbCteJyLXWP9FxBex5FcA2tLm_8_sV9ctCnt5MwHQOU-K6OM65RPsZbN18jrcEKroVNUT4O7GwOncwsqHfdjeproLHBm4H_F_WskGJ9Djs4gVVxcYYR3xypuWR594o75QjDKlCbAtwZDFooSkxfBZGtlCsytS3on9lym7BhO3ZL0X572eXSkX5YE2ZcdfvMbaQKZIkKWD7gunMgAwRJ9dszcqdegc9Vv3iGj5v1C_gSS0sToLsVLfMKmxNJQ6iXQdgmYDTimPTioLdFb9ZXXfLYWWgxWHRQ07Jz-zBKZdbHJP7qGyy320oF4jojTvmKGufp94leD_xddtI58M","version":"MRVL-1"},"created":1746685312,"enabled":true,"exportable":false,"recoverableDays":7,"recoveryLevel":"CustomizedRecoverable+Purgeable","updated":1746685312},"key":{"e":"AQAB","key_ops":["verify","sign","unwrapKey","decrypt","encrypt","wrapKey"],"kid":"https://clitesthsmkeyats000002.managedhsm.azure.net/keys/key1-000003/7071fabc480c0ca186f786a9b78c9456","kty":"RSA-HSM","n":"iHYH_d5bhETKeSc5PJ1_f3bvF3JfmpNfrK7MAjoAvOiIyQSamA-5-YPwA-KZRE9rDh9g6ns8NAELvG7B516r3AO1fBsflP_fTeIMZa-xDS61RMldmXCCDD8OPkAvKqzgSpHOIbg5UJh9ODGtBssydlS_77fqWZQPEjkkN3frWvvpJZmr3THifV4vkXAKwHld2nvFgVQRlx_BTc8S92lXSxZ3kTU0t9aayoS-2BN8s0o_ZC14-UfiP5IB0xkjELfOa897aZ_I3djdvWKdWbJsObiSHMZCQ9VEIFL6i8Ih9_SeU6H7ORwwMKvjjExq-O_vrUC64V2zYs5lqX1GY8-M-Q"}}' + headers: + cache-control: + - no-cache + content-length: + - '11999' + content-security-policy: + - default-src 'self' + content-type: + - application/json; charset=utf-8 + strict-transport-security: + - max-age=31536000; includeSubDomains + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-ms-build-version: + - 1.0.20250326-1-0961b288-develop + x-ms-keyvault-network-info: + - conn_type=Ipv4;addr=167.220.255.14;act_addr_fam=Ipv4; + x-ms-keyvault-region: + - uksouth + x-ms-server-latency: + - '12' + status: + code: 200 + message: OK +version: 1 diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py b/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py index 36f58ec2241..03deddeebb8 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/tests/latest/test_keyvault_commands.py @@ -1371,6 +1371,27 @@ def test_keyvault_hsm_key_random(self, resource_group, managed_hsm): result = self.cmd('keyvault key random --count 1 --id {hsm_url}').get_output_in_json() self.assertIsNotNone(result['value']) + + @serial_test() + @ResourceGroupPreparer(name_prefix='cli_test_hsm_key_attestation') + @ManagedHSMPreparer(name_prefix='clitesthsmkeyats', certs_path=CERTS_DIR, roles=['Managed HSM Crypto Officer', 'Managed HSM Crypto User']) + def test_keyvault_hsm_key_attestation(self, resource_group, managed_hsm): + self.kwargs.update({ + 'hsm_name': managed_hsm, + 'hsm_url': 'https://{}.managedhsm.azure.net'.format(managed_hsm), + 'key': self.create_random_name('key1-', 24) + }) + + # create a key + hsm_key = self.cmd('keyvault key create --hsm-name {hsm_name} -n {key}').get_output_in_json() + self.kwargs['hsm_kid'] = hsm_key['key']['kid'] + self.assertNotIn('attestation', hsm_key['attributes']) + + # get key's attestation + key_attestation = self.cmd('keyvault key get-attestation --hsm-name {hsm_name} --name {key}').get_output_in_json() + self.assertIn('publicKeyAttestation', key_attestation) + self.assertIn('privateKeyAttestation', key_attestation) + @serial_test() @ResourceGroupPreparer(name_prefix='cli_test_hsm_key') @ManagedHSMPreparer(name_prefix='clitesthsmkey', certs_path=CERTS_DIR, roles=['Managed HSM Crypto Officer', 'Managed HSM Crypto User']) From 9c0b9a8e16f22206761a9387370a047591a3360b Mon Sep 17 00:00:00 2001 From: Yishi Wang Date: Thu, 8 May 2025 15:30:07 +0800 Subject: [PATCH 2/2] add help examles --- src/azure-cli/azure/cli/command_modules/keyvault/_help.py | 7 +++++++ .../azure/cli/command_modules/keyvault/_params.py | 2 +- src/azure-cli/azure/cli/command_modules/keyvault/custom.py | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/_help.py b/src/azure-cli/azure/cli/command_modules/keyvault/_help.py index 784115a283e..b5d46f164a2 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/_help.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/_help.py @@ -460,6 +460,13 @@ type: command short-summary: Get a key's attestation blob. long-summary: This command is applicable to any key stored in Azure Key Vault Managed HSM. This operation requires the keys/get permission. +examples: + - name: Get a key's attestation. + text: | + az keyvault key get-attestation --hsm-name myhsm -n mykey + - name: Save the key's attestation to local file. + text: | + az keyvault key get-attestation --hsm-name myhsm -n mykey -f mykeyattestation.json """ helps['keyvault key show-deleted'] = """ diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/_params.py b/src/azure-cli/azure/cli/command_modules/keyvault/_params.py index be14918ab35..800abb6e5b7 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/_params.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/_params.py @@ -317,7 +317,7 @@ class CLISecurityDomainOperation(str, Enum): with self.argument_context('keyvault key get-attestation') as c: c.argument('file_path', options_list=['--file', '-f'], type=file_type, completer=FilesCompleter(), - help="File to receive the key's attestation contents.") + help="File to receive the key's attestation if you want to save it.") c.extra('hsm_name', data_plane_hsm_name_type, required=False, arg_group='Id', help='Name of the HSM. Required if --id is not specified.') c.ignore('vault_base_url') diff --git a/src/azure-cli/azure/cli/command_modules/keyvault/custom.py b/src/azure-cli/azure/cli/command_modules/keyvault/custom.py index fe6f5163d8d..3577a8ff230 100644 --- a/src/azure-cli/azure/cli/command_modules/keyvault/custom.py +++ b/src/azure-cli/azure/cli/command_modules/keyvault/custom.py @@ -1101,6 +1101,7 @@ def list_keys(client, maxresults=None, include_managed=False): return [_ for _ in result if not getattr(_, 'managed')] if result else result return result + def get_key_attestation(client, name, version=None, file_path=None): key = client.get_key_attestation(name=name, version=version) key_attestation = key.properties.attestation @@ -1119,6 +1120,7 @@ def get_key_attestation(client, name, version=None, file_path=None): os.remove(file_path) raise ex + def delete_key(client, name): return client.begin_delete_key(name).result() @@ -2133,7 +2135,6 @@ def full_backup(cmd, client, storage_resource_uri=None, storage_account_name=Non storage_resource_uri = construct_storage_uri( cmd.cli_ctx.cloud.suffixes.storage_endpoint, storage_account_name, blob_container_name) poller = client.begin_backup(storage_resource_uri, sas_token=token, use_managed_identity=use_managed_identity) - from knack.util import todict result = todict(poller.result()) result['status'] = poller.status() return result