Skip to content
Merged
4 changes: 4 additions & 0 deletions src/azure-cli/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Release History

* Support app creation/update with the new sku name ST0, ST1, ST2.

**Key Vault**

* Add a new command `az keyvault key download` for downloading keys.

**Misc**

* Fix #6371: Support filename and environment variable completion in Bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# CUSTOM CHOICE LISTS

secret_encoding_values = secret_text_encoding_values + secret_binary_encoding_values
certificate_format_values = ['PEM', 'DER']
key_format_values = certificate_format_values = ['PEM', 'DER']


# pylint: disable=too-many-locals, too-many-branches, too-many-statements, line-too-long
Expand Down Expand Up @@ -160,6 +160,10 @@ def load_arguments(self, _):
with self.argument_context('keyvault key backup') as c:
c.argument('file_path', options_list=['--file', '-f'], type=file_type, completer=FilesCompleter(), help='Local file path in which to store key backup.')

with self.argument_context('keyvault key download') as c:
c.argument('file_path', options_list=['--file', '-f'], type=file_type, completer=FilesCompleter(), help='File to receive the key contents.')
c.argument('encoding', arg_type=get_enum_type(key_format_values), options_list=['--encoding', '-e'], help='Encoding of the key, default: PEM', default='PEM')

with self.argument_context('keyvault key restore') as c:
c.argument('file_path', options_list=['--file', '-f'], type=file_type, completer=FilesCompleter(), help='Local key backup from which to restore key.')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def load_command_table(self, _):
g.keyvault_custom('backup', 'backup_key', doc_string_source=data_doc_string.format('backup_key'))
g.keyvault_custom('restore', 'restore_key', doc_string_source=data_doc_string.format('restore_key'))
g.keyvault_custom('import', 'import_key')
g.keyvault_custom('download', 'download_key')

with self.command_group('keyvault secret', kv_data_sdk) as g:
g.keyvault_command('list', 'get_secrets')
Expand Down
204 changes: 164 additions & 40 deletions src/azure-cli/azure/cli/command_modules/keyvault/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
# --------------------------------------------------------------------------------------------

# pylint: disable=too-many-lines

import codecs
import json
import math
import os
import time
import struct


from knack.log import get_logger
from knack.util import CLIError

from OpenSSL import crypto
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa, ec
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.serialization import load_pem_private_key, Encoding, PublicFormat
from cryptography.exceptions import UnsupportedAlgorithm


Expand Down Expand Up @@ -620,51 +622,54 @@ def restore_key(client, vault_base_url, file_path):
return client.restore_key(vault_base_url, data)


def _int_to_bytes(i):
h = hex(i)
if len(h) > 1 and h[0:2] == '0x':
h = h[2:]
# need to strip L in python 2.x
h = h.strip('L')
if len(h) % 2:
h = '0' + h
return codecs.decode(h, 'hex')


def _private_rsa_key_to_jwk(rsa_key, jwk):
priv = rsa_key.private_numbers()
jwk.n = _int_to_bytes(priv.public_numbers.n)
jwk.e = _int_to_bytes(priv.public_numbers.e)
jwk.q = _int_to_bytes(priv.q)
jwk.p = _int_to_bytes(priv.p)
jwk.d = _int_to_bytes(priv.d)
jwk.dq = _int_to_bytes(priv.dmq1)
jwk.dp = _int_to_bytes(priv.dmp1)
jwk.qi = _int_to_bytes(priv.iqmp)


def _private_ec_key_to_jwk(ec_key, jwk):
supported_curves = {
'secp256r1': 'P-256',
'secp384r1': 'P-384',
'secp521r1': 'P-521',
'secp256k1': 'SECP256K1'
}
curve = ec_key.private_numbers().public_numbers.curve.name

jwk.crv = supported_curves.get(curve, None)
if not jwk.crv:
raise CLIError("Import failed: Unsupported curve, {}.".format(curve))

jwk.x = _int_to_bytes(ec_key.private_numbers().public_numbers.x)
jwk.y = _int_to_bytes(ec_key.private_numbers().public_numbers.y)
jwk.d = _int_to_bytes(ec_key.private_numbers().private_value)


def import_key(cmd, client, vault_base_url, key_name, protection=None, key_ops=None, disabled=False, expires=None,
not_before=None, tags=None, pem_file=None, pem_password=None, byok_file=None):
""" Import a private key. Supports importing base64 encoded private keys from PEM files.
Supports importing BYOK keys into HSM for premium key vaults. """
KeyAttributes = cmd.get_models('KeyAttributes', resource_type=ResourceType.DATA_KEYVAULT)
JsonWebKey = cmd.get_models('JsonWebKey', resource_type=ResourceType.DATA_KEYVAULT)

def _int_to_bytes(i):
h = hex(i)
if len(h) > 1 and h[0:2] == '0x':
h = h[2:]
# need to strip L in python 2.x
h = h.strip('L')
if len(h) % 2:
h = '0' + h
return codecs.decode(h, 'hex')

def _private_rsa_key_to_jwk(rsa_key, jwk):
priv = rsa_key.private_numbers()
jwk.n = _int_to_bytes(priv.public_numbers.n)
jwk.e = _int_to_bytes(priv.public_numbers.e)
jwk.q = _int_to_bytes(priv.q)
jwk.p = _int_to_bytes(priv.p)
jwk.d = _int_to_bytes(priv.d)
jwk.dq = _int_to_bytes(priv.dmq1)
jwk.dp = _int_to_bytes(priv.dmp1)
jwk.qi = _int_to_bytes(priv.iqmp)

def _private_ec_key_to_jwk(ec_key, jwk):
supported_curves = {
'secp256r1': 'P-256',
'secp384r1': 'P-384',
'secp521r1': 'P-521',
'secp256k1': 'SECP256K1'
}
curve = ec_key.private_numbers().public_numbers.curve.name

jwk.crv = supported_curves.get(curve, None)
if not jwk.crv:
raise CLIError("Import failed: Unsupported curve, {}.".format(curve))

jwk.x = _int_to_bytes(ec_key.private_numbers().public_numbers.x)
jwk.y = _int_to_bytes(ec_key.private_numbers().public_numbers.y)
jwk.d = _int_to_bytes(ec_key.private_numbers().private_value)

key_attrs = KeyAttributes(enabled=not disabled, not_before=not_before, expires=expires)
key_obj = JsonWebKey(key_ops=key_ops)
if pem_file:
Expand Down Expand Up @@ -694,6 +699,125 @@ def _private_ec_key_to_jwk(ec_key, jwk):
key_obj.t = byok_data

return client.import_key(vault_base_url, key_name, key_obj, protection == 'hsm', key_attrs, tags)


def _bytes_to_int(b):
len_diff = 4 - len(b) % 4 if len(b) % 4 > 0 else 0
b = len_diff * b'\x00' + b # We have to patch leading zeros for using struct.unpack
bytes_num = int(math.floor(len(b) / 4))
ans = 0
items = struct.unpack('>' + 'I' * bytes_num, b)
for sub_int in items:
ans *= 2 ** 32
ans += sub_int
return ans


def _jwk_to_dict(jwk):
d = {}
if jwk.crv:
d['crv'] = jwk.crv
if jwk.kid:
d['kid'] = jwk.kid
if jwk.kty:
d['kty'] = jwk.kty
if jwk.d:
d['d'] = _bytes_to_int(jwk.d)
if jwk.dp:
d['dp'] = _bytes_to_int(jwk.dp)
if jwk.dq:
d['dq'] = _bytes_to_int(jwk.dq)
if jwk.e:
d['e'] = _bytes_to_int(jwk.e)
if jwk.k:
d['k'] = _bytes_to_int(jwk.k)
if jwk.n:
d['n'] = _bytes_to_int(jwk.n)
if jwk.p:
d['p'] = _bytes_to_int(jwk.p)
if jwk.q:
d['q'] = _bytes_to_int(jwk.q)
if jwk.qi:
d['qi'] = _bytes_to_int(jwk.qi)
if jwk.t:
d['t'] = _bytes_to_int(jwk.t)
if jwk.x:
d['x'] = _bytes_to_int(jwk.x)
if jwk.y:
d['y'] = _bytes_to_int(jwk.y)

return d


def _extract_rsa_public_key_from_jwk(jwk_dict):
e = jwk_dict.get('e', 256)
n = jwk_dict.get('n')
public = rsa.RSAPublicNumbers(e, n)
return public.public_key(default_backend())


def _extract_ec_public_key_from_jwk(jwk_dict):
if not all(k in jwk_dict for k in ['x', 'y', 'crv']):
raise CLIError('Invalid EC key: missing properties(x, y, crv)')

x = jwk_dict.get('x')
y = jwk_dict.get('y')
curves = {
'P-256': ec.SECP256R1,
'P-384': ec.SECP384R1,
'P-521': ec.SECP521R1,
'SECP256K1': ec.SECP256K1
}
curve = curves[jwk_dict['crv']]
public = ec.EllipticCurvePublicNumbers(x, y, curve())
return public.public_key(default_backend())


def download_key(client, file_path, vault_base_url=None, key_name=None, key_version='',
encoding=None, identifier=None): # pylint: disable=unused-argument
""" Download a key from a KeyVault. """
if os.path.isfile(file_path) or os.path.isdir(file_path):
raise CLIError("File or directory named '{}' already exists.".format(file_path))

key = client.get_key(vault_base_url, key_name, key_version)
json_web_key = _jwk_to_dict(key.key)
key_type = json_web_key['kty']
pub_key = ''

if key_type in ['RSA', 'RSA-HSM']:
pub_key = _extract_rsa_public_key_from_jwk(json_web_key)
elif key_type in ['EC', 'EC-HSM']:
pub_key = _extract_ec_public_key_from_jwk(json_web_key)
else:
raise CLIError('Unsupported key type: {}'.format(key_type))

def _to_der(k):
return k.public_bytes(
encoding=Encoding.DER,
format=PublicFormat.SubjectPublicKeyInfo
)

def _to_pem(k):
return k.public_bytes(
encoding=Encoding.PEM,
format=PublicFormat.SubjectPublicKeyInfo
)

methods = {
'DER': _to_der,
'PEM': _to_pem
}

if encoding not in methods.keys():
raise CLIError('Unsupported encoding: {}'.format(encoding))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's prompt the supported encoding as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jiasli Good idea, I will add this.


try:
with open(file_path, 'wb') as f:
f.write(methods[encoding](pub_key))
except Exception as ex: # pylint: disable=broad-except
if os.path.isfile(file_path):
os.remove(file_path)
raise ex
# endregion


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIIe338KHD5vHeNeB1Fhs68Kpx2jHjpqh/QZptSME/iL0oAoGCCqGSM49
AwEHoUQDQgAEclUrtHdYtNdfl7cueN47hqM6SuyPn18rpsLutGJ/qHeHqvZdnL1b
+aJBNReZlaS24LlJVjsmxn/Zcg1BID0RWg==
-----END EC PRIVATE KEY-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEclUrtHdYtNdfl7cueN47hqM6SuyP
n18rpsLutGJ/qHeHqvZdnL1b+aJBNReZlaS24LlJVjsmxn/Zcg1BID0RWg==
-----END PUBLIC KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIMy+ZSqbwORvuWLCmEH12TJUM6zD7QGxgYczeU/2AZCyoAcGBSuBBAAK
oUQDQgAEH7Thoa/qhy7HoBSFPKOL9edHRqr305sCa7ozYkRLIe0WR98MA4LwXpZD
F3do0CMNqIYLzTJsG37DLngQZ4rJPw==
-----END EC PRIVATE KEY-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEH7Thoa/qhy7HoBSFPKOL9edHRqr305sC
a7ozYkRLIe0WR98MA4LwXpZDF3do0CMNqIYLzTJsG37DLngQZ4rJPw==
-----END PUBLIC KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIGkAgEBBDBG234N7Zwr021c4vi6M9mMz+Q3DVsAyahEqsNNY/XT3k5X3z4tfnth
/rgciYIxv1agBwYFK4EEACKhZANiAAReoBk8F60AVNazrJ9zUK1BU887MT41d5OA
oxPC5OocJGPvkSaK4NDZOOYj0IEvIw5F5y1QybXqhyfvj/0ydAxDulvvfDfmdcJ/
5o6rVip7XyFq9Kz6gnBNbQVVMbR7E/M=
-----END EC PRIVATE KEY-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEXqAZPBetAFTWs6yfc1CtQVPPOzE+NXeT
gKMTwuTqHCRj75EmiuDQ2TjmI9CBLyMORectUMm16ocn74/9MnQMQ7pb73w35nXC
f+aOq1Yqe18havSs+oJwTW0FVTG0exPz
-----END PUBLIC KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-----BEGIN EC PARAMETERS-----
BgUrgQQAIw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MIHcAgEBBEIBnqXLzys3RKGPLPC7jIbo6rIAXiXVjx4bnZu/CpuiFpnZiqUB5+pf
Kd/X1Av6ikeVYCmM1bXAxIfP4BGCUWGy2wugBwYFK4EEACOhgYkDgYYABAB8/6kF
Fy9ujL9GRaW1c2Fz9wXb1h6bg1zCWCgNWljOIEXcis8m+9Q4wfYFu51zKUI3QXqs
DkPJts9ru/xm2NOZJQDJaU+9LNPHryvkd44xniyWbWDbvPD/js6zKDCVMlvmo7CS
aOYj3QWEzoWDMVxV39KrxYVYeSrRQW2czlYrtGQQGg==
-----END EC PRIVATE KEY-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN PUBLIC KEY-----
MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAfP+pBRcvboy/RkWltXNhc/cF29Ye
m4NcwlgoDVpYziBF3IrPJvvUOMH2BbudcylCN0F6rA5DybbPa7v8ZtjTmSUAyWlP
vSzTx68r5HeOMZ4slm1g27zw/47OsygwlTJb5qOwkmjmI90FhM6FgzFcVd/Sq8WF
WHkq0UFtnM5WK7RkEBo=
-----END PUBLIC KEY-----
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAzJEYKuhIeHvKqZZtJRDtQjg17ChAZtY/yJfNqv30xnzkzOZw
Vmy7NViVQIRnkCYvAMCstja+EpEpeW02LD83teNQRKM9Nie74g7ehDn6ZZCSdQQ5
4TQemt26hMfnFVaUl376p0E398IP1leHcvYOaiVgT/ffUhNhgIUJ5sIphtf4D1p5
bWFYvs/Gd+A+Fk38hnerpl7JRF4y6p5WGJzvXKKs0QVSaQ1NHD7L1tQ0xTKra3FS
/BGwD7ZdgBZerqzBPgxRjuxzq2DQJlHtvINsFjJhsMtwI11UV5+tIBOEWPSBTIoQ
pUWGB+/4jZ44IpKlBDQuc1kqa2Y8+eO53Gxn+QIDAQABAoIBAQDK3zIai1YjtpDb
8oS3d7v0Kg6/74M++Uc0Ref/pe90UTQPSJEsBJT8aKdL3oNeX5/JnUsrQcrqWu/I
rlhFNUSoq5BVIZZ4+JrJq3ldpKoAw4mbZt+Hycp4R2DMgftYHA8s1w75hCJfISPX
q+J2TjMpbXvAks/0c6gEbuvM382THSDa+bA+BvYwilBBT347WkLdtuN9J+9qFyV9
ROJRRMYh3xGOcgs/mmlOgTSubU3Q2sIQHBTLtngOA4g456g4IH5G09GoEpAIQA8e
G7xzmkv+ECtqe9V0A/9njWIXNbO+V+01dUDiAKnnT5uzbPGT48hnA05WLUAQO2qP
5ZE4H6shAoGBAPQUXR2ByYX0e/VBF3lUo94yikRae6giM0k8MQwE1Y49FfuZwqp8
7E4hmuxlOJnvAQjzIQoHawL0fO7qLkvYcDJPxT5ROCeHKOCyJ6tGmVojppqiuq1G
RpOZ2XfIEt00rQguufYIr0V7OeP2X27Sveq0sJ/CYgX4rGABvq5OOW91AoGBANaO
t5T4qkuuxvxARx65S+imTg66U4aGz25rCj7LvcVhKmm2XCYz1ynAR81Tnh/t0RMd
8M3mUxRZU1MrbMCeYZbt5ClWmMfHKtZa29PYYcvVvqNBnqlbPGD7fBK+JyGnnd57
CQLA0WnmSDT0vBOSN+wH/Gk2IX3ekLMGBk1Zzyn1AoGAUhsfj7N/NR6fLEtvOBNu
5Gof9QpzGoYWtoYXAbIGnMiTwoVg5LUNUOMhGHCcb7vknzwaWyNPrjjMZhpE5KK0
a1hGQ8ZSm4luCNglXAptv9LKUq53GZ7QUwqoCxE0t1Dm/B+r0sXtH/Rp7vOL+t3N
oUyTNcrP6q5SXiF4IW6TB5kCgYAwNDhCm+uGvWmvWrGf0Xmgd1yqKmqBmuAXqqzO
lu+33LCut23Ul2kL1EtNci/gdIm4hc2INOsNc1QpJ2RzkiHSyver4ezJVZHmPtuM
qNyv8wG1pBSFcB4Mm/OwMlCQWxw40+OeXrut0zL90s4+h2dQ/CpVaPf1U3+m+P+J
eVf10QKBgDJZ0xVeun+aPMXqcwNppqvfKfMWsLb/Xx77O3q9SYG1JRXgIIneL8nD
8RwVQn8PbPcqdayFHZrmwhdgzIH/7dVEwd+QQKzeYos8Fvc4egpTZUrmZ1y5XeFt
OHl9TmHLSA1h8cJj0HrWCjyyi2cW+2PhY9K88V57KEn0yv5hjoBg
-----END RSA PRIVATE KEY-----
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzJEYKuhIeHvKqZZtJRDt
Qjg17ChAZtY/yJfNqv30xnzkzOZwVmy7NViVQIRnkCYvAMCstja+EpEpeW02LD83
teNQRKM9Nie74g7ehDn6ZZCSdQQ54TQemt26hMfnFVaUl376p0E398IP1leHcvYO
aiVgT/ffUhNhgIUJ5sIphtf4D1p5bWFYvs/Gd+A+Fk38hnerpl7JRF4y6p5WGJzv
XKKs0QVSaQ1NHD7L1tQ0xTKra3FS/BGwD7ZdgBZerqzBPgxRjuxzq2DQJlHtvINs
FjJhsMtwI11UV5+tIBOEWPSBTIoQpUWGB+/4jZ44IpKlBDQuc1kqa2Y8+eO53Gxn
+QIDAQAB
-----END PUBLIC KEY-----
Loading