From bbb193ef84e6e471f434044083208cab2c90c188 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 10 Jan 2020 09:22:48 -0800 Subject: [PATCH 1/6] support password-protected certificates --- .../azure-identity/azure/identity/_base.py | 15 +++--- .../_credentials/client_credential.py | 4 +- .../aio/_credentials/client_credential.py | 4 +- .../tests/certificate-with-password.pem | 51 +++++++++++++++++++ .../tests/test_certificate_credential.py | 9 ++-- 5 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 sdk/identity/azure-identity/tests/certificate-with-password.pem diff --git a/sdk/identity/azure-identity/azure/identity/_base.py b/sdk/identity/azure-identity/azure/identity/_base.py index 98850e5f7231..1a114ef03d41 100644 --- a/sdk/identity/azure-identity/azure/identity/_base.py +++ b/sdk/identity/azure-identity/azure/identity/_base.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: # pylint:disable=unused-import - from typing import Any, Mapping + from typing import Any, Optional, Union class ClientSecretCredentialBase(object): @@ -46,20 +46,23 @@ def __init__(self, tenant_id, client_id, secret, **kwargs): # pylint:disable=un class CertificateCredentialBase(ABC): """Sans I/O base for certificate credentials""" - def __init__(self, tenant_id, client_id, certificate_path, **kwargs): # pylint:disable=unused-argument - # type: (str, str, str, **Any) -> None + def __init__(self, tenant_id, client_id, certificate_path, password=None, **kwargs): # pylint:disable=unused-argument + # type: (str, str, str, Optional[Union[str, bytes]], **Any) -> None if not certificate_path: raise ValueError( - "certificate_path must be the path to a PEM file containing an " - "x509 certificate and its private key, not protected with a password" + "'certificate_path' must be the path to a PEM file containing an" + "x509 certificate and its private key" ) super(CertificateCredentialBase, self).__init__() + if isinstance(password, six.text_type): + password = password.encode(encoding="utf-8") + with open(certificate_path, "rb") as f: pem_bytes = f.read() - private_key = serialization.load_pem_private_key(pem_bytes, password=None, backend=default_backend()) + private_key = serialization.load_pem_private_key(pem_bytes, password=password, backend=default_backend()) cert = x509.load_pem_x509_certificate(pem_bytes, default_backend()) fingerprint = cert.fingerprint(hashes.SHA1()) diff --git a/sdk/identity/azure-identity/azure/identity/_credentials/client_credential.py b/sdk/identity/azure-identity/azure/identity/_credentials/client_credential.py index 10eae06c2b2a..57372f86322f 100644 --- a/sdk/identity/azure-identity/azure/identity/_credentials/client_credential.py +++ b/sdk/identity/azure-identity/azure/identity/_credentials/client_credential.py @@ -56,11 +56,13 @@ class CertificateCredential(CertificateCredentialBase): :param str tenant_id: ID of the service principal's tenant. Also called its 'directory' ID. :param str client_id: the service principal's client ID :param str certificate_path: path to a PEM-encoded certificate file including the private key. - This file must not be password-protected. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` defines authorities for other clouds. + :keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate + requires a different encoding, pass appropriately encoded bytes instead. + :paramtype password: str or bytes """ def get_token(self, *scopes, **kwargs): # pylint:disable=unused-argument diff --git a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_credential.py b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_credential.py index 79ea22931960..816941dd09b2 100644 --- a/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_credential.py +++ b/sdk/identity/azure-identity/azure/identity/aio/_credentials/client_credential.py @@ -60,11 +60,13 @@ class CertificateCredential(CertificateCredentialBase, AsyncCredentialBase): :param str tenant_id: ID of the service principal's tenant. Also called its 'directory' ID. :param str client_id: the service principal's client ID :param str certificate_path: path to a PEM-encoded certificate file including the private key - This file must not be password-protected. :keyword str authority: Authority of an Azure Active Directory endpoint, for example 'login.microsoftonline.com', the authority for Azure Public Cloud (which is the default). :class:`~azure.identity.KnownAuthorities` defines authorities for other clouds. + :keyword password: The certificate's password. If a unicode string, it will be encoded as UTF-8. If the certificate + requires a different encoding, pass appropriately encoded bytes instead. + :paramtype password: str or bytes """ async def __aenter__(self): diff --git a/sdk/identity/azure-identity/tests/certificate-with-password.pem b/sdk/identity/azure-identity/tests/certificate-with-password.pem new file mode 100644 index 000000000000..bca1bb581766 --- /dev/null +++ b/sdk/identity/azure-identity/tests/certificate-with-password.pem @@ -0,0 +1,51 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFHDBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQIpDOLr9sNuTwCAggA +MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECIWEFMkTyS60BIIEyPvMPGyf1shr +ql0UnZzMWDz/9bqdRIZe5N7F1qYjgZNms/QXVzZOQ2J9YwaLHbwpEv2QigfHXq/3 +nLyTm4HFyg3qpCqWa33A0r/v5B6WtjgYbuPePqpM2UV34CMGylkmhMVUUbs1X6j7 +ezwmipq/paOslokC0RYl16cQD/uLTD0usTDtWoEs3S4gbcGUj/b9Ll2urG9zBYwI +faSWQcwcRgfYk/OZSbv6zNT74dYMAOW3mjsS/ackc/+h2XWFQSVjN7BBXamfCQ3C +qE27Py85PP8Qt7MbHuoj8rMuoaQ3NIi++1RkW6cyFo5n+HBi7YYCP74loG1OCTN8 +H5StIi2aLbls9ZbQJHLM2+J1tBwJqIR/UogITESV++17ZVHfDLk8uaad7i6Kj+u5 +6vbruehnFqo5P80lZRuqHfGf/5v8Hbsve/zL24wdQ+tFDHaC6v+kiz9unnO/+k86 +9gph/WTly4N4wJhdxhoYxJLMdPcWk6AxA7ZsJ/mI9+t8iHdSOZY91FyN3sDlDB4C +yLi8t1WP+VB9KfMjSN0AuULWrwwQ1YGRUsKaS9pxTy8MbXQ1OgXGGHzHKDm6vqyp +Jow9wD8Ql+V7zPsNgBpeRWXzA5VS6nEyIuOolkJnNoC5d69/LtDaBn58TZQ4z1Ja +wGXG6n9BeFrgwgH5X5kGslLDXZ71V/aT0HHoBbiAPWf90teccGJ5nVXv3kMaC/zc +klzNCrQ2koFphQgW1bU/FZ46yd2rvlFo8wbxAwRieldZpkwRcFVKz+cRh4/QsTpl +uPKtPpI0c2jgiSReNXi2kRlkOPg1UVHnapvv2yRUoq4NvzaOegSVJG1oe/XdLS02 +5JoDNajEcIvHQpLZL1ecQSwpge843mW4F5twm8/1MKY8G2CTXjKif77n3WVR2Tvp +RdOm86TbIB4FpbLAqiN11A/8cLNfVmioQkNdULLELfKiOeQZABSMPJMVGVxgODN0 +nP31BMibPayqApLLEFIQbSLhZvWIJ5ircZ5XKPHeuqpnxFoeIwrGQuqHo6gvSp5J +CPv4Pul43y0s3vxRpJAqmXO4aAzsPsrYGJiNckbD43OxRV9ZDDeD1Wrc4u82zQxo +frSy5XhVPsKH1lFZ7l6te4Tkro4vMxRVu+W2acToJI6QZct0xrlp+ScmD/9CEOGU +Wj5SN4Y8YW+UfeYhDAhntzruRx4HjdocbvwYsY9gAkim4P9AdiG6eeRUyU9GCUwd +MrjMt5HUzsTQiIyN9jnv9yWNdYmzgJ2V0ZOwVHaEZhZnkgYoK0O/NXSg0FsPo5LU +qdYncK+BMAGnEl/riaJRmnsIH29jKPjZDOvfo1+0UJM3+zPjYy4985+CH7xWGWnb +NQFBtwiPyWzlDyV3119T4rY+Ad6z90vG4hgvpvue1Qcuaure8NwkUEEh1/d8PN20 +kP4vWhpDeHbO5R5byXlJMNzmgVm3mc2t6mA/ouUcMmUOTvjdYALXqgw9RgOsqkob +DNjEK4VZUu3Vd6AsK+s796KTLgQvZhrcahoer/88j7Nu0PyQGrVN202IfmbjIwer +NNcieLmok6r2k8GvyUYP51hpdkXO5j3BsrtBeq4xn3qxzOtEUL8ITZ/BU20+xJq6 +GoGDjvCSBpzesnQFlvUtEw== +-----END ENCRYPTED PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIUHBH8mppwjLI2dFOQ7haLnd6iRjQwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMDAxMDMxODE2MzlaGA8yMTk5 +MDYwODE4MTYzOVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANMXamdgXR+0B2b6zt2nURcYcwC0YrqjvTH/ofF3 +MjUzZ1uKziPNxxAYrUY0O0zIcZWo9Aqfi10vS5oNya/aDrKoWxVRCsLltAV9dbLJ +65zF7wbVE7ZnZ7Nknop+ytd1t1VNTlpbxgWdT6z/WTn4ydqH7Hlh0Ucu2Q3QGQL3 +G9He0kOMog4Y0myxP2xNGjLoig2kh60KEwtxbudOxVN4rLpqhT/1n/L5s+7rznKc +cB4MRqPJMdycIYhTD2mfp/E9hDWRcVJY+9GlqzyxXFTsDsO1SzGgpMEjdO5mtc6N +A0dd8fZQLt1BHLFJlpsuk5Fk40y7HtT3kYKUcD55Xd0pd6ECAwEAAaNTMFEwHQYD +VR0OBBYEFKG65qd+cChhFLB8y4po+vL3HwxuMB8GA1UdIwQYMBaAFKG65qd+cChh +FLB8y4po+vL3HwxuMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +AAlEqWO8tGKprQrqfdZDsMyKyne+WYYA7RqRxYzKJ5j1zsPa4f7RKhLYzx6TqWK3 +keBc6aw9aeNYNqTBeDLGHRHRRwdHwwU5HdqowhE0sEQzOIJvs2JK5L+LcoSkTViy +PzidZ0qoAHRluuFw8Ag9sahcQfao6rqJOFY/16KEjDthATGo/4mHRsuAM+xza+2m +GbqJH/iO/q0lsPb3culm8aoJNxULTHrU5YWhuGvRypSYrfdL7RBkzW4VEt5LcRK6 +KcfmfHMrjPl/XxSSvrBmly7nYNH80DGSMRP/lnrQ8OS+hSiDy1KBaCcNhja5Dyzn +K0dXlMGmWrnDMs8m+4cUoIM= +-----END CERTIFICATE----- diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index da554ecd52a4..4b15a0fe9a4f 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -22,7 +22,7 @@ from mock import Mock, patch # type: ignore CERT_PATH = os.path.join(os.path.dirname(__file__), "certificate.pem") - +CERT_WITH_PASSWORD_PATH = os.path.join(os.path.dirname(__file__), "certificate-with-password.pem") def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) @@ -65,9 +65,10 @@ def mock_send(request, **kwargs): validate_url(request.url) return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) - cred = CertificateCredential(tenant_id, "client_id", CERT_PATH, transport=Mock(send=mock_send), authority=authority) - token = cred.get_token("scope") - assert token.token == access_token + for cert_path, password in ((CERT_PATH, None), (CERT_WITH_PASSWORD_PATH, 'password')): + cred = CertificateCredential(tenant_id, "client_id", cert_path, password=password, transport=Mock(send=mock_send), authority=authority) + token = cred.get_token("scope") + assert token.token == access_token def test_request_body(): From a43058b3b9de657ecf29f768cea8f8fd659efcc1 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Fri, 10 Jan 2020 09:47:23 -0800 Subject: [PATCH 2/6] offline tests --- .../tests/test_certificate_credential.py | 30 ++++++++++------- .../test_certificate_credential_async.py | 33 ++++++++++--------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential.py b/sdk/identity/azure-identity/tests/test_certificate_credential.py index 4b15a0fe9a4f..ce280e4ef066 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential.py @@ -12,17 +12,21 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding +import pytest from six.moves.urllib_parse import urlparse from helpers import build_aad_response, urlsafeb64_decode, mock_response, Request, validating_transport try: - from unittest.mock import Mock, patch + from unittest.mock import Mock except ImportError: # python < 3.3 - from mock import Mock, patch # type: ignore + from mock import Mock # type: ignore CERT_PATH = os.path.join(os.path.dirname(__file__), "certificate.pem") CERT_WITH_PASSWORD_PATH = os.path.join(os.path.dirname(__file__), "certificate-with-password.pem") +CERT_PASSWORD = "password" +BOTH_CERTS = ((CERT_PATH, None), (CERT_WITH_PASSWORD_PATH, CERT_PASSWORD)) + def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) @@ -50,7 +54,8 @@ def test_user_agent(): credential.get_token("scope") -def test_request_url(): +@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) +def test_request_url(cert_path, cert_password): authority = "authority.com" tenant_id = "expected_tenant" access_token = "***" @@ -65,13 +70,15 @@ def mock_send(request, **kwargs): validate_url(request.url) return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) - for cert_path, password in ((CERT_PATH, None), (CERT_WITH_PASSWORD_PATH, 'password')): - cred = CertificateCredential(tenant_id, "client_id", cert_path, password=password, transport=Mock(send=mock_send), authority=authority) - token = cred.get_token("scope") - assert token.token == access_token + cred = CertificateCredential( + tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority + ) + token = cred.get_token("scope") + assert token.token == access_token -def test_request_body(): +@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) +def test_request_body(cert_path, cert_password): access_token = "***" authority = "authority.com" client_id = "client-id" @@ -82,14 +89,15 @@ def mock_send(request, **kwargs): assert request.body["grant_type"] == "client_credentials" assert request.body["scope"] == expected_scope - with open(CERT_PATH, "rb") as cert_file: + with open(cert_path, "rb") as cert_file: validate_jwt(request, client_id, cert_file.read()) return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) - cred = CertificateCredential(tenant_id, client_id, CERT_PATH, transport=Mock(send=mock_send), authority=authority) + cred = CertificateCredential( + tenant_id, client_id, cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority + ) token = cred.get_token(expected_scope) - assert token.token == access_token diff --git a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py index d14e929693b3..01af51a454a3 100644 --- a/sdk/identity/azure-identity/tests/test_certificate_credential_async.py +++ b/sdk/identity/azure-identity/tests/test_certificate_credential_async.py @@ -2,9 +2,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -import json -import os -from unittest.mock import Mock, patch +from unittest.mock import Mock from urllib.parse import urlparse from azure.core.pipeline.policies import ContentDecodePolicy, SansIOHTTPPolicy @@ -15,10 +13,7 @@ from helpers import build_aad_response, urlsafeb64_decode, mock_response, Request from helpers_async import async_validating_transport, AsyncMockTransport -from test_certificate_credential import validate_jwt - - -CERT_PATH = os.path.join(os.path.dirname(__file__), "certificate.pem") +from test_certificate_credential import BOTH_CERTS, CERT_PATH, validate_jwt @pytest.mark.asyncio @@ -72,28 +67,32 @@ async def test_user_agent(): @pytest.mark.asyncio -async def test_request_url(): +@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) +async def test_request_url(cert_path, cert_password): authority = "authority.com" tenant_id = "expected_tenant" access_token = "***" def validate_url(url): - scheme, netloc, path, _, _, _ = urlparse(url) - assert scheme == "https" - assert netloc == authority - assert path.startswith("/" + tenant_id) + parsed = urlparse(url) + assert parsed.scheme == "https" + assert parsed.netloc == authority + assert parsed.path.startswith("/" + tenant_id) async def mock_send(request, **kwargs): validate_url(request.url) return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) - cred = CertificateCredential(tenant_id, "client_id", CERT_PATH, transport=Mock(send=mock_send), authority=authority) + cred = CertificateCredential( + tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority + ) token = await cred.get_token("scope") assert token.token == access_token @pytest.mark.asyncio -async def test_request_body(): +@pytest.mark.parametrize("cert_path,cert_password", BOTH_CERTS) +async def test_request_body(cert_path, cert_password): access_token = "***" authority = "authority.com" client_id = "client-id" @@ -104,12 +103,14 @@ async def mock_send(request, **kwargs): assert request.body["grant_type"] == "client_credentials" assert request.body["scope"] == expected_scope - with open(CERT_PATH, "rb") as cert_file: + with open(cert_path, "rb") as cert_file: validate_jwt(request, client_id, cert_file.read()) return mock_response(json_payload={"token_type": "Bearer", "expires_in": 42, "access_token": access_token}) - cred = CertificateCredential(tenant_id, client_id, CERT_PATH, transport=Mock(send=mock_send), authority=authority) + cred = CertificateCredential( + tenant_id, client_id, cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority + ) token = await cred.get_token("scope") assert token.token == access_token From 7ae401d8de93095d4a65fe95db9d00c803882e60 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 13 Jan 2020 11:58:05 -0800 Subject: [PATCH 3/6] live tests --- sdk/identity/azure-identity/conftest.py | 28 ++++++++++++++++--- .../azure-identity/tests/test_live.py | 10 +++++++ .../azure-identity/tests/test_live_async.py | 11 ++++++++ sdk/identity/tests.yml | 2 ++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/sdk/identity/azure-identity/conftest.py b/sdk/identity/azure-identity/conftest.py index ed71382fbb00..cb35f368240f 100644 --- a/sdk/identity/azure-identity/conftest.py +++ b/sdk/identity/azure-identity/conftest.py @@ -63,13 +63,11 @@ def live_service_principal(): # pylint:disable=inconsistent-return-statements @pytest.fixture() def live_certificate(live_service_principal): # pylint:disable=inconsistent-return-statements,redefined-outer-name - """Fixture for live tests needing a certificate. - Skips them when environment configuration is incomplete. - """ + """Provides a path to a PEM-encoded certificate with no password""" pem_content = os.environ.get("PEM_CONTENT") if not pem_content: - pytest.skip("Environment has no value for 'PEM_CONTENT'") + pytest.skip("Expected PEM content in environment variable 'PEM_CONTENT'") return pem_path = os.path.join(os.path.dirname(__file__), "certificate.pem") @@ -81,6 +79,28 @@ def live_certificate(live_service_principal): # pylint:disable=inconsistent-ret pytest.skip("Failed to write file '{}': {}".format(pem_path, ex)) +@pytest.fixture() +def live_certificate_with_password(live_service_principal): + """Provides a path to a PEM-encoded, password-protected certificate, and its password""" + + pem_content = os.environ.get("PEM_CONTENT_PASSWORD_PROTECTED") + password = os.environ.get("CERTIFICATE_PASSWORD") + if not (pem_content and password): + pytest.skip( + "Expected password-protected PEM content in environment variable 'PEM_CONTENT_PASSWORD_PROTECTED'" + + " and the password in 'CERTIFICATE_PASSWORD'" + ) + return + + pem_path = os.path.join(os.path.dirname(__file__), "certificate-with-password.pem") + try: + with open(pem_path, "w") as pem_file: + pem_file.write(pem_content) + return dict(live_service_principal, cert_path=pem_path, password=password) + except IOError as ex: + pytest.skip("Failed to write file '{}': {}".format(pem_path, ex)) + + @pytest.fixture() def live_user_details(): user_details = { diff --git a/sdk/identity/azure-identity/tests/test_live.py b/sdk/identity/azure-identity/tests/test_live.py index 63548951ee5f..7139883a8321 100644 --- a/sdk/identity/azure-identity/tests/test_live.py +++ b/sdk/identity/azure-identity/tests/test_live.py @@ -37,6 +37,16 @@ def test_certificate_credential(live_certificate): get_token(credential) +def test_certificate_credential_with_password(live_certificate_with_password): + credential = CertificateCredential( + live_certificate_with_password["tenant_id"], + live_certificate_with_password["client_id"], + live_certificate_with_password["cert_path"], + password=live_certificate_with_password["password"], + ) + get_token(credential) + + def test_client_secret_credential(live_service_principal): credential = ClientSecretCredential( live_service_principal["tenant_id"], diff --git a/sdk/identity/azure-identity/tests/test_live_async.py b/sdk/identity/azure-identity/tests/test_live_async.py index 13b26a721734..548f035c26bb 100644 --- a/sdk/identity/azure-identity/tests/test_live_async.py +++ b/sdk/identity/azure-identity/tests/test_live_async.py @@ -28,6 +28,17 @@ async def test_certificate_credential(live_certificate): await get_token(credential) +@pytest.mark.asyncio +async def test_certificate_credential_with_password(live_certificate_with_password): + credential = CertificateCredential( + live_certificate_with_password["tenant_id"], + live_certificate_with_password["client_id"], + live_certificate_with_password["cert_path"], + password=live_certificate_with_password["password"], + ) + await get_token(credential) + + @pytest.mark.asyncio async def test_client_secret_credential(live_service_principal): credential = ClientSecretCredential( diff --git a/sdk/identity/tests.yml b/sdk/identity/tests.yml index d92a67ec91c0..0ce1a0fc39a1 100644 --- a/sdk/identity/tests.yml +++ b/sdk/identity/tests.yml @@ -15,4 +15,6 @@ jobs: AZURE_CLIENT_ID: $(python-identity-client-id) AZURE_CLIENT_SECRET: $(python-identity-client-secret) AZURE_TENANT_ID: $(aad-azure-sdk-test-tenant-id) + CERTIFICATE_PASSWORD: $(python-identity-certificate-password) PEM_CONTENT: $(python-identity-certificate) + PEM_CONTENT_PASSWORD_PROTECTED: $(python-identity-certificate-with-password) From cddf9271ca3ea36a2259284175528bbd92fb4746 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 13 Jan 2020 13:30:36 -0800 Subject: [PATCH 4/6] update readme --- sdk/identity/azure-identity/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/identity/azure-identity/README.md b/sdk/identity/azure-identity/README.md index e44003aaeb44..8be7c426e2ef 100644 --- a/sdk/identity/azure-identity/README.md +++ b/sdk/identity/azure-identity/README.md @@ -176,10 +176,14 @@ This example demonstrates authenticating the `SecretClient` from the from azure.identity import CertificateCredential from azure.keyvault.secrets import SecretClient -# requires a PEM-encoded certificate with private key, not protected with a password +# requires a PEM-encoded certificate with private key cert_path = "/app/certs/certificate.pem" credential = CertificateCredential(tenant_id, client_id, cert_path) +# if the private key is password protected, provide a 'password' keyword argument +credential = CertificateCredential(tenant_id, client_id, cert_path, password="cert-password") + + client = SecretClient("https://my-vault.vault.azure.net", credential) ``` From 39172ad5449573911702f9707214cabab37396cf Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 13 Jan 2020 13:37:29 -0800 Subject: [PATCH 5/6] update history --- sdk/identity/azure-identity/HISTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/identity/azure-identity/HISTORY.md b/sdk/identity/azure-identity/HISTORY.md index 35122e3b983a..3cc1b34ec5ee 100644 --- a/sdk/identity/azure-identity/HISTORY.md +++ b/sdk/identity/azure-identity/HISTORY.md @@ -6,6 +6,8 @@ ([#8945](https://github.com/Azure/azure-sdk-for-python/pull/8945)) - Async credentials are async context managers and have an async `close` method ([#9090](https://github.com/Azure/azure-sdk-for-python/pull/9090)) +- `CertificateCredential` supports password-protected private keys +([#9434](https://github.com/Azure/azure-sdk-for-python/pull/9434)) ## 1.1.0 (2019-11-27) From 66ec559da441af880ae8fbb44a7c04e41e82f5c3 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Tue, 28 Jan 2020 10:06:23 -0800 Subject: [PATCH 6/6] certificate password is a kwarg --- sdk/identity/azure-identity/azure/identity/_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/identity/azure-identity/azure/identity/_base.py b/sdk/identity/azure-identity/azure/identity/_base.py index 1a114ef03d41..bf37df00fdbe 100644 --- a/sdk/identity/azure-identity/azure/identity/_base.py +++ b/sdk/identity/azure-identity/azure/identity/_base.py @@ -46,16 +46,16 @@ def __init__(self, tenant_id, client_id, secret, **kwargs): # pylint:disable=un class CertificateCredentialBase(ABC): """Sans I/O base for certificate credentials""" - def __init__(self, tenant_id, client_id, certificate_path, password=None, **kwargs): # pylint:disable=unused-argument - # type: (str, str, str, Optional[Union[str, bytes]], **Any) -> None + def __init__(self, tenant_id, client_id, certificate_path, **kwargs): # pylint:disable=unused-argument + # type: (str, str, str, **Any) -> None if not certificate_path: raise ValueError( - "'certificate_path' must be the path to a PEM file containing an" - "x509 certificate and its private key" + "'certificate_path' must be the path to a PEM file containing an x509 certificate and its private key" ) super(CertificateCredentialBase, self).__init__() + password = kwargs.pop("password", None) if isinstance(password, six.text_type): password = password.encode(encoding="utf-8")