Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
4f08d19
updating azure-keyvault version and changelog
Sep 22, 2017
bef5e13
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-python
Oct 10, 2017
d91b015
adding http_challange to support pop and bearer challanges
Oct 12, 2017
35fdd5c
adding classes for jose object handling
Nov 16, 2017
1cce33c
message encryption updates from testing
Dec 1, 2017
86ad403
removing print statements used for debugging
Dec 8, 2017
75566ef
fixing keyvault test base and adding docstrings
Dec 12, 2017
455c66b
Updating http challenge headers for message protection
Dec 13, 2017
091c6af
Merge pull request #1 from JeffSimmer/mcrypt
Dec 13, 2017
2376828
Merge branch 'mcrypt' of https://github.com/schaabs/azure-sdk-for-pyt…
Dec 13, 2017
d649ff7
Add backwards compatible ctor for HttpChallange
Dec 14, 2017
96f4cac
removing custom\jose.py as its contents has been moved to custom\inte…
Dec 14, 2017
428f7f0
updates for PR feedback
Dec 14, 2017
a061bef
moving RsaKey class to internal.py and marking internal classes with _
Dec 14, 2017
7f3295b
fixing regression introduced with feedback updates
Dec 14, 2017
d3df142
updating cek to be regenerated on each encrypted message
Jan 10, 2018
bd0be97
adding tests for internals used in message encryption
Jan 24, 2018
0d5f0d5
updating aes_hmac encryption / decryption to use cryptography primitives
Jan 24, 2018
c750e77
Merge branch 'keyvault_1.0_preview' of https://github.com/Azure/azure…
Jan 24, 2018
849f57d
fixing cyrptography version requirement in setup.py
Jan 24, 2018
6e610ff
Merge pull request #1828 from schaabs/mcrypt
Jan 25, 2018
51acce6
updating azure-keyvault HISTORY.rst for 1.0.0a1 release
Jan 25, 2018
e5ddcee
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
Feb 1, 2018
8ab56c6
updating autorest generated code from 7.0-preview spec
Feb 1, 2018
25f0501
fixing test import
Feb 1, 2018
5f16fa4
updating test_key_vault_data recordings to reflect new api-version
Feb 2, 2018
aa6fefb
Merge pull request #1883 from schaabs/autorest_7.0-preview
Feb 8, 2018
f675ac9
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
Feb 8, 2018
e1728dd
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
Feb 27, 2018
cebbdd8
rebasing autorest generated changes from 7.0-preview to keyvault_1.0_…
Feb 27, 2018
b5b9e1b
Merge pull request #2067 from Azure/restapi_auto_2543
Feb 27, 2018
128b24c
Generated from b3108e892b610f91257f1fe99d114ab171191a08
AutorestCI Mar 2, 2018
5c7f140
Generated from 026ea6558477ce3f27bc8b53f41ddae57ded7f30
AutorestCI Mar 6, 2018
505ef8c
Generated from 4a9da431e52626982de1ba1f52e997f7e1989967
AutorestCI Mar 6, 2018
6bbd6c0
Generated from 85da3965e190f9c0d14e1a55faec3eb763adfd30
AutorestCI Mar 7, 2018
694eec6
updating tests and recordings for api version changes
Mar 7, 2018
70aa6b0
updating vault mgmt test recordings for new api version
Mar 7, 2018
b107c3c
Merge pull request #2112 from Azure/restapi_auto_2603
Mar 7, 2018
71ab5cd
Merge pull request #2120 from Azure/restapi_auto_keyvault_preview
Mar 7, 2018
ba92c74
regenerated from 7.0-preview swagger
Mar 27, 2018
19099ad
Merge pull request #2290 from schaabs/regen-keyvault-preview
Mar 28, 2018
d6a9282
updating release notes for alpha 2 release
Mar 28, 2018
bfbe914
Merge pull request #2291 from schaabs/release-notes
Mar 28, 2018
1cc7ea2
fixing azure-keyvault version formatting
Mar 28, 2018
710c122
regen data-plane SDK with autorest.python 3.0
Apr 3, 2018
1764f60
regen resource-manager SDK with autorest.python 3.0
Apr 3, 2018
af11e2b
updating tests for backwards breaking changes in models
Apr 3, 2018
6940345
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
Apr 3, 2018
7c7e1fe
Merge pull request #2343 from schaabs/autorest3
Apr 3, 2018
6b2606a
updating KeyVaultAuthentication to support reused sessions
Apr 3, 2018
2976ad9
setup.py fixes
Apr 3, 2018
33b3ffd
update from PR feedback
Apr 4, 2018
5f4ee66
Merge pull request #2345 from schaabs/signed-session-fix
Apr 4, 2018
bc883a6
updating custom imports to import relatively instead of from namespace
Apr 10, 2018
a7c2404
Merge pull request #2378 from schaabs/keyvault_1.0_preview
Apr 10, 2018
fa15fb2
updating release notes for beta 1 release
Apr 11, 2018
af67531
fixing markup error in HISTORY.rst
Apr 11, 2018
f4d980c
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
May 18, 2018
cb30cb2
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
May 19, 2018
3350b8c
migrating tests to azure-keyvault\tests and removing legacy tests
May 19, 2018
3e50c5c
removing legacy tests and service tests __init__.py files to fix load…
May 22, 2018
9e12bdb
Merge pull request #2590 from schaabs/migrate-tests
May 22, 2018
ea2768f
Merge remote-tracking branch 'upstream/master' into keyvault_1.0_preview
lmazuel May 29, 2018
f01d40f
KV Mgmt auto packaging
lmazuel May 29, 2018
d45b72c
KV Data auto packaging
lmazuel May 29, 2018
928ccf5
Restore KV emails
lmazuel May 29, 2018
0b888fc
KV still beta
lmazuel May 29, 2018
7ed3e54
Merge pull request #2649 from Azure/kv_packaging
lmazuel May 29, 2018
1035cf2
Generated from 57fd9306b5ab91393d9b60f05ccc6c0d311e21fa
AutorestCI Jun 22, 2018
e6a98a1
Generated from e560fd1b8e44a9e90f7f7f19aa9880736a5e3b7b
AutorestCI Jun 22, 2018
9221077
Generated from e560fd1b8e44a9e90f7f7f19aa9880736a5e3b7b
AutorestCI Jun 25, 2018
50a7d84
Generated from e560fd1b8e44a9e90f7f7f19aa9880736a5e3b7b
AutorestCI Jun 26, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
21 changes: 21 additions & 0 deletions azure-keyvault/HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

Release History
===============
1.0.0b1 (2018-04-10)
++++++++++++++++++++

* Upgraded to autorest 3.0 generated code
* Breaking change in models requiring all init args be specified by keyword


1.0.0a2 (2018-03-28)
++++++++++++++++++++

* Upgrading to API version 7.0-preview
* Adding elliptic curve key support
* Adding managed storage account key backup, restore and soft delete support
* Breaking update to managed storage account SasDefinition creation
* Adding certificate backup and restore support
* Adding certificate transparency

1.0.0a1 (2018-01-25)
++++++++++++++++++++
* Added message encryption support for message encryption enabled vaults

0.3.7 (2017-09-22)
++++++++++++++++++

Expand Down
12 changes: 3 additions & 9 deletions azure-keyvault/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@ Microsoft Azure SDK for Python

This is the Microsoft Azure Key Vault Client Library.

Azure Resource Manager (ARM) is the next generation of management APIs that
replace the old Azure Service Management (ASM).

This package has been tested with Python 2.7, 3.3, 3.4, 3.5 and 3.6.

For the older Azure Service Management (ASM) libraries, see
`azure-servicemanagement-legacy <https://pypi.python.org/pypi/azure-servicemanagement-legacy>`__ library.
This package has been tested with Python 2.7, 3.4, 3.5 and 3.6.

For a more complete set of Azure libraries, see the `azure <https://pypi.python.org/pypi/azure>`__ bundle package.

Expand Down Expand Up @@ -37,8 +31,8 @@ Usage
=====

For code examples, see `Key Vault
<https://azure-sdk-for-python.readthedocs.org/en/latest/sample_azure-keyvault.html>`__
on readthedocs.org.
<https://docs.microsoft.com/python/api/overview/azure/key-vault>`__
on docs.microsoft.com.


Provide Feedback
Expand Down
9 changes: 7 additions & 2 deletions azure-keyvault/azure/keyvault/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from .custom import http_bearer_challenge_cache as HttpBearerChallengeCache
from .custom.http_bearer_challenge import HttpBearerChallenge
from .custom.http_challenge import HttpChallenge
from .custom.key_vault_client import CustomKeyVaultClient as KeyVaultClient
from .custom.key_vault_id import (KeyVaultId,
KeyId,
Expand All @@ -20,7 +21,8 @@
CertificateOperationId,
StorageAccountId,
StorageSasDefinitionId)
from .custom.key_vault_authentication import KeyVaultAuthentication, KeyVaultAuthBase
from .custom.key_vault_authentication import KeyVaultAuthentication, KeyVaultAuthBase, AccessToken
from .custom.http_message_security import generate_pop_key
from .version import VERSION

__all__ = ['KeyVaultClient',
Expand All @@ -34,8 +36,11 @@
'StorageSasDefinitionId',
'HttpBearerChallengeCache',
'HttpBearerChallenge',
'HttpChallenge',
'KeyVaultAuthentication',
'KeyVaultAuthBase']
'KeyVaultAuthBase',
'generate_pop_key',
'AccessToken']

__version__ = VERSION

115 changes: 115 additions & 0 deletions azure-keyvault/azure/keyvault/custom/http_challenge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#---------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#---------------------------------------------------------------------------------------------

try:
import urllib.parse as parse
except ImportError:
import urlparse as parse # pylint: disable=import-error


class HttpChallenge(object):

def __init__(self, request_uri, challenge, response_headers=None):
""" Parses an HTTP WWW-Authentication Bearer challenge from a server. """
self.source_authority = self._validate_request_uri(request_uri)
self.source_uri = request_uri
self._parameters = {}

# get the scheme of the challenge and remove from the challenge string
trimmed_challenge = self._validate_challenge(challenge)
split_challenge = trimmed_challenge.split(' ', 1)
self.scheme = split_challenge[0]
trimmed_challenge = split_challenge[1]

# split trimmed challenge into comma-separated name=value pairs. Values are expected
# to be surrounded by quotes which are stripped here.
for item in trimmed_challenge.split(','):
# process name=value pairs
comps = item.split('=')
if len(comps) == 2:
key = comps[0].strip(' "')
value = comps[1].strip(' "')
if key:
self._parameters[key] = value

# minimum set of parameters
if not self._parameters:
raise ValueError('Invalid challenge parameters')

# must specify authorization or authorization_uri
if 'authorization' not in self._parameters and 'authorization_uri' not in self._parameters:
raise ValueError('Invalid challenge parameters')

# if the response headers were supplied
if response_headers:
# get the message signing key and message key encryption key from the headers
self.server_signature_key = response_headers.get('x-ms-message-signing-key', None)
self.server_encryption_key = response_headers.get('x-ms-message-encryption-key', None)

def is_bearer_challenge(self):
""" Tests whether the HttpChallenge a Bearer challenge.
rtype: bool """
if not self.scheme:
return False

return self.scheme.lower() == 'bearer'

def is_pop_challenge(self):
""" Tests whether the HttpChallenge is a proof of possession challenge.
rtype: bool """
if not self.scheme:
return False

return self.scheme.lower() == 'pop'

def get_value(self, key):
return self._parameters.get(key)

def get_authorization_server(self):
""" Returns the URI for the authorization server if present, otherwise empty string. """
value = ''
for key in ['authorization_uri', 'authorization']:
value = self.get_value(key) or ''
if value:
break
return value

def get_resource(self):
""" Returns the resource if present, otherwise empty string. """
return self.get_value('resource') or ''

def get_scope(self):
""" Returns the scope if present, otherwise empty string. """
return self.get_value('scope') or ''

def supports_pop(self):
""" Returns True if challenge supports pop token auth else False """
return self._parameters.get('supportspop', '').lower() == 'true'

def supports_message_protection(self):
""" Returns True if challenge vault supports message protection """
return self.supports_pop() and self.server_encryption_key and self.server_signature_key

def _validate_challenge(self, challenge):
""" Verifies that the challenge is a valid auth challenge and returns the key=value pairs. """
if not challenge:
raise ValueError('Challenge cannot be empty')

return challenge.strip()

# pylint: disable=no-self-use
def _validate_request_uri(self, uri):
""" Extracts the host authority from the given URI. """
if not uri:
raise ValueError('request_uri cannot be empty')

uri = parse.urlparse(uri)
if not uri.netloc:
raise ValueError('request_uri must be an absolute URI')

if uri.scheme.lower() not in ['http', 'https']:
raise ValueError('request_uri must be HTTP or HTTPS')

return uri.netloc
192 changes: 192 additions & 0 deletions azure-keyvault/azure/keyvault/custom/http_message_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#---------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#---------------------------------------------------------------------------------------------

import json
import time
import os
from .internal import _a128cbc_hs256_encrypt, _a128cbc_hs256_decrypt, _JwsHeader, _JwsObject, \
_JweHeader, _JweObject, _str_to_b64url, _bstr_to_b64url, _b64_to_bstr, _RsaKey


def generate_pop_key():
"""
Generates a key which can be used for Proof Of Possession token authentication.
:return:
"""
return _RsaKey.generate()


class HttpMessageSecurity(object):
"""
Used for message authorization, encryption and decrtyption.

This class is intended for internal use only. Details are subject to non-compatible changes, consumers of the
azure-keyvault module should not take dependencies on this class or its current implementation.
"""
def __init__(self, client_security_token=None,
client_signature_key=None,
client_encryption_key=None,
server_signature_key=None,
server_encryption_key=None):
self.client_security_token = client_security_token
self.client_signature_key = client_signature_key
self.client_encryption_key = client_encryption_key
self.server_signature_key = server_signature_key
self.server_encryption_key = server_encryption_key

def protect_request(self, request):
"""
Adds authorization header, and encrypts and signs the request if supported on the specific request.
:param request: unprotected request to apply security protocol
:return: protected request with appropriate security protocal applied
"""
# Setup the auth header on the request
# Due to limitations in the service we hard code the auth scheme to 'Bearer' as the service will fail with any
# other scheme or a different casing such as 'bearer', once this is fixed the following line should be replaced:
# request.headers['Authorization'] = '{} {}'.format(auth[0], auth[1])
request.headers['Authorization'] = '{} {}'.format('Bearer', self.client_security_token)

# if the current message security doesn't support message protection, or the body is empty
# skip protection and return the original request
if not self.supports_protection() or len(request.body) == 0:
return request

plain_text = request.body

# if the client encryption key is specified add it to the body of the request
if self.client_encryption_key:
# note that this assumes that the body is already json and not simple string content
# this is true for all requests which currently support message encryption, but might
# need to be revisited when the types of
body_dict = json.loads(plain_text)
body_dict['rek'] = {'jwk': self.client_encryption_key.to_jwk().serialize()}
plain_text = json.dumps(body_dict).encode(encoding='utf8')

# build the header for the jws body
jws_header = _JwsHeader()
jws_header.alg = 'RS256'
jws_header.kid = self.client_signature_key.kid
jws_header.at = self.client_security_token
jws_header.ts = int(time.time())
jws_header.typ = 'PoP'

jws = _JwsObject()

jws.protected = jws_header.to_compact_header()
jws.payload = self._protect_payload(plain_text)
data = (jws.protected + '.' + jws.payload).encode('ascii')
jws.signature = _bstr_to_b64url(self.client_signature_key.sign(data))

request.headers['Content-Type'] = 'application/jose+json'

request.prepare_body(data=jws.to_flattened_jws(), files=None)

return request

def unprotect_response(self, response, **kwargs):
"""
Removes protection from the specified response
:param request: response from the key vault service
:return: unprotected response with any security protocal encryption removed
"""
body = response.content
# if the current message security doesn't support message protection, the body is empty, or the request failed
# skip protection and return the original response
if not self.supports_protection() or len(response.content) == 0 or response.status_code != 200:
return response

# ensure the content-type is application/jose+json
if 'application/jose+json' not in response.headers.get('content-type', '').lower():
raise ValueError('Invalid protected response')

# deserialize the response into a JwsObject, using response.text so requests handles the encoding
jws = _JwsObject().deserialize(body)

# deserialize the protected header
jws_header = _JwsHeader.from_compact_header(jws.protected)

# ensure the jws signature kid matches the key from original challenge
# and the alg matches expected signature alg
if jws_header.kid != self.server_signature_key.kid \
or jws_header.alg != 'RS256':
raise ValueError('Invalid protected response')

# validate the signature of the jws
data = (jws.protected + '.' + jws.payload).encode('ascii')
# verify will raise an InvalidSignature exception if the signature doesn't match
self.server_signature_key.verify(signature=_b64_to_bstr(jws.signature), data=data)

# get the unprotected response body
decrypted = self._unprotect_payload(jws.payload)

response._content = decrypted
response.headers['Content-Type'] = 'application/json'

return response

def supports_protection(self):
"""
Determines if the the current HttpMessageSecurity object supports the message protection protocol.
:return: True if the current object supports protection, otherwise False
"""
return self.client_signature_key \
and self.client_encryption_key \
and self.server_signature_key \
and self.server_encryption_key

def _protect_payload(self, plaintext):
# create the jwe header for the payload
kek = self.server_encryption_key
jwe_header = _JweHeader()
jwe_header.alg = 'RSA-OAEP'
jwe_header.kid = kek.kid
jwe_header.enc = 'A128CBC-HS256'

# create the jwe object
jwe = _JweObject()
jwe.protected = jwe_header.to_compact_header()

# generate the content encryption key and iv
cek = os.urandom(32)
iv = os.urandom(16)
jwe.iv = _bstr_to_b64url(iv)
# wrap the cek using the server encryption key
wrapped = _bstr_to_b64url(kek.encrypt(cek))
jwe.encrypted_key = wrapped

# encrypt the plaintext body with the cek using the protected header
# as the authdata to get the ciphertext and the authtag
ciphertext, tag = _a128cbc_hs256_encrypt(cek, iv, plaintext, jwe.protected.encode('ascii'))

jwe.ciphertext = _bstr_to_b64url(ciphertext)
jwe.tag = _bstr_to_b64url(tag)

# flatten and encode the jwe for the final jws payload content
flat = jwe.to_flattened_jwe()
return _str_to_b64url(flat)

def _unprotect_payload(self, payload):
# deserialize the payload
jwe = _JweObject().deserialize_b64(payload)

# deserialize the payload header
jwe_header = _JweHeader.from_compact_header(jwe.protected)

# ensure the kid matches the specified client encryption key
# and the key wrap alg and the data encryption enc match the expected
if self.client_encryption_key.kid != jwe_header.kid \
or jwe_header.alg != 'RSA-OAEP' \
or jwe_header.enc != 'A128CBC-HS256':
raise ValueError('Invalid protected response')

# unwrap the cek using the client encryption key
cek = self.client_encryption_key.decrypt(_b64_to_bstr(jwe.encrypted_key))

# decrypt the cipher text to get the unprotected body content
return _a128cbc_hs256_decrypt(cek,
_b64_to_bstr(jwe.iv),
_b64_to_bstr(jwe.ciphertext),
jwe.protected.encode('ascii'),
_b64_to_bstr(jwe.tag))
Loading