-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[KeyVault] Merging preview branch keyvault_1.0_preview back to master #2817
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 75 commits
Commits
Show all changes
76 commits
Select commit
Hold shift + click to select a range
4f08d19
updating azure-keyvault version and changelog
bef5e13
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-python
d91b015
adding http_challange to support pop and bearer challanges
35fdd5c
adding classes for jose object handling
1cce33c
message encryption updates from testing
86ad403
removing print statements used for debugging
75566ef
fixing keyvault test base and adding docstrings
455c66b
Updating http challenge headers for message protection
091c6af
Merge pull request #1 from JeffSimmer/mcrypt
2376828
Merge branch 'mcrypt' of https://github.com/schaabs/azure-sdk-for-pyt…
d649ff7
Add backwards compatible ctor for HttpChallange
96f4cac
removing custom\jose.py as its contents has been moved to custom\inte…
428f7f0
updates for PR feedback
a061bef
moving RsaKey class to internal.py and marking internal classes with _
7f3295b
fixing regression introduced with feedback updates
d3df142
updating cek to be regenerated on each encrypted message
bd0be97
adding tests for internals used in message encryption
0d5f0d5
updating aes_hmac encryption / decryption to use cryptography primitives
c750e77
Merge branch 'keyvault_1.0_preview' of https://github.com/Azure/azure…
849f57d
fixing cyrptography version requirement in setup.py
6e610ff
Merge pull request #1828 from schaabs/mcrypt
51acce6
updating azure-keyvault HISTORY.rst for 1.0.0a1 release
e5ddcee
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
8ab56c6
updating autorest generated code from 7.0-preview spec
25f0501
fixing test import
5f16fa4
updating test_key_vault_data recordings to reflect new api-version
aa6fefb
Merge pull request #1883 from schaabs/autorest_7.0-preview
f675ac9
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
e1728dd
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
cebbdd8
rebasing autorest generated changes from 7.0-preview to keyvault_1.0_…
b5b9e1b
Merge pull request #2067 from Azure/restapi_auto_2543
128b24c
Generated from b3108e892b610f91257f1fe99d114ab171191a08
AutorestCI 5c7f140
Generated from 026ea6558477ce3f27bc8b53f41ddae57ded7f30
AutorestCI 505ef8c
Generated from 4a9da431e52626982de1ba1f52e997f7e1989967
AutorestCI 6bbd6c0
Generated from 85da3965e190f9c0d14e1a55faec3eb763adfd30
AutorestCI 694eec6
updating tests and recordings for api version changes
70aa6b0
updating vault mgmt test recordings for new api version
b107c3c
Merge pull request #2112 from Azure/restapi_auto_2603
71ab5cd
Merge pull request #2120 from Azure/restapi_auto_keyvault_preview
ba92c74
regenerated from 7.0-preview swagger
19099ad
Merge pull request #2290 from schaabs/regen-keyvault-preview
d6a9282
updating release notes for alpha 2 release
bfbe914
Merge pull request #2291 from schaabs/release-notes
1cc7ea2
fixing azure-keyvault version formatting
710c122
regen data-plane SDK with autorest.python 3.0
1764f60
regen resource-manager SDK with autorest.python 3.0
af11e2b
updating tests for backwards breaking changes in models
6940345
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
7c7e1fe
Merge pull request #2343 from schaabs/autorest3
6b2606a
updating KeyVaultAuthentication to support reused sessions
2976ad9
setup.py fixes
33b3ffd
update from PR feedback
5f4ee66
Merge pull request #2345 from schaabs/signed-session-fix
bc883a6
updating custom imports to import relatively instead of from namespace
a7c2404
Merge pull request #2378 from schaabs/keyvault_1.0_preview
fa15fb2
updating release notes for beta 1 release
af67531
fixing markup error in HISTORY.rst
f4d980c
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
cb30cb2
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
3350b8c
migrating tests to azure-keyvault\tests and removing legacy tests
3e50c5c
removing legacy tests and service tests __init__.py files to fix load…
9e12bdb
Merge pull request #2590 from schaabs/migrate-tests
ea2768f
Merge remote-tracking branch 'upstream/master' into keyvault_1.0_preview
lmazuel f01d40f
KV Mgmt auto packaging
lmazuel d45b72c
KV Data auto packaging
lmazuel 928ccf5
Restore KV emails
lmazuel 0b888fc
KV still beta
lmazuel 7ed3e54
Merge pull request #2649 from Azure/kv_packaging
lmazuel 1a184b2
Generated from 8acad9d35e1ca7bbf7d4a3584e20a0d16a06c0ef
AutorestCI 103d89a
updating test recordings for api-version=7.0
72d4b6b
regenerating azure-mgmt-keyvault for release version
8293098
adding previously unmigrated vaults operation tests
fd71e24
Merge pull request #2807 from schaabs/kv-rc
f2a92ab
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
1922b61
Merge branch 'master' of https://github.com/Azure/azure-sdk-for-pytho…
de49838
updating release history for azure-keyvault and azure-mgmt-keyvault
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,27 @@ | |
|
|
||
| Release History | ||
| =============== | ||
| 1.0.0b1 (2018-04-10) | ||
| ++++++++++++++++++++ | ||
|
|
||
| * Upgraded to autorest 3.0 generated code | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I used to copy the "General Breaking Changes" from here, to be explicit: |
||
| * 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) | ||
| ++++++++++++++++++ | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
192
azure-keyvault/azure/keyvault/custom/http_message_security.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this file includes a 1.0.0 now? Even if it's just "release 1.0.0b1 as 1.0.0 unchanged"?