Hello community,

here is the log from the commit of package python-google-auth for 
openSUSE:Factory checked in at 2020-06-05 19:58:42
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-google-auth (Old)
 and      /work/SRC/openSUSE:Factory/.python-google-auth.new.3606 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-google-auth"

Fri Jun  5 19:58:42 2020 rev:9 rq:808798 version:1.15.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-google-auth/python-google-auth.changes    
2020-05-01 11:06:28.374949678 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-google-auth.new.3606/python-google-auth.changes
  2020-06-05 19:58:46.487639948 +0200
@@ -1,0 +2,9 @@
+Mon May 25 11:24:30 UTC 2020 - Paolo Stivanin <[email protected]>
+
+- Update to 1.15.0:
+  * encrypted mtls private key support 
+  * signBytes for impersonated credentials 
+  * catch exceptions.RefreshError 
+  * support string type response.data
+
+-------------------------------------------------------------------

Old:
----
  google-auth-1.14.1.tar.gz

New:
----
  google-auth-1.15.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-google-auth.spec ++++++
--- /var/tmp/diff_new_pack.I4MuLn/_old  2020-06-05 19:58:47.503643463 +0200
+++ /var/tmp/diff_new_pack.I4MuLn/_new  2020-06-05 19:58:47.503643463 +0200
@@ -18,7 +18,7 @@
 
 %{?!python_module:%define python_module() python-%{**} python3-%{**}}
 Name:           python-google-auth
-Version:        1.14.1
+Version:        1.15.0
 Release:        0
 Summary:        Google Authentication Library
 License:        Apache-2.0

++++++ google-auth-1.14.1.tar.gz -> google-auth-1.15.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/PKG-INFO 
new/google-auth-1.15.0/PKG-INFO
--- old/google-auth-1.14.1/PKG-INFO     2020-04-22 00:04:21.881367000 +0200
+++ new/google-auth-1.15.0/PKG-INFO     2020-05-18 19:28:26.764173000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: google-auth
-Version: 1.14.1
+Version: 1.15.0
 Summary: Google Authentication Library
 Home-page: https://github.com/googleapis/google-auth-library-python
 Author: Google Cloud Platform
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google/auth/exceptions.py 
new/google-auth-1.15.0/google/auth/exceptions.py
--- old/google-auth-1.14.1/google/auth/exceptions.py    2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/auth/exceptions.py    2020-05-18 
19:26:54.000000000 +0200
@@ -39,3 +39,7 @@
 class MutualTLSChannelError(GoogleAuthError):
     """Used to indicate that mutual TLS channel creation is failed, or mutual
     TLS channel credentials is missing or invalid."""
+
+
+class ClientCertError(GoogleAuthError):
+    """Used to indicate that client certificate is missing or invalid."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google-auth-1.14.1/google/auth/impersonated_credentials.py 
new/google-auth-1.15.0/google/auth/impersonated_credentials.py
--- old/google-auth-1.14.1/google/auth/impersonated_credentials.py      
2020-04-22 00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/auth/impersonated_credentials.py      
2020-05-18 19:26:54.000000000 +0200
@@ -88,13 +88,18 @@
 
     response = request(url=iam_endpoint, method="POST", headers=headers, 
body=body)
 
-    response_body = response.data.decode("utf-8")
+    # support both string and bytes type response.data
+    response_body = (
+        response.data.decode("utf-8")
+        if hasattr(response.data, "decode")
+        else response.data
+    )
 
     if response.status != http_client.OK:
         exceptions.RefreshError(_REFRESH_ERROR, response_body)
 
     try:
-        token_response = json.loads(response.data.decode("utf-8"))
+        token_response = json.loads(response_body)
         token = token_response["accessToken"]
         expiry = datetime.strptime(token_response["expireTime"], 
"%Y-%m-%dT%H:%M:%SZ")
 
@@ -259,7 +264,10 @@
 
         iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)
 
-        body = {"payload": base64.b64encode(message), "delegates": 
self._delegates}
+        body = {
+            "payload": base64.b64encode(message).decode("utf-8"),
+            "delegates": self._delegates,
+        }
 
         headers = {"Content-Type": "application/json"}
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google/auth/jwt.py 
new/google-auth-1.15.0/google/auth/jwt.py
--- old/google-auth-1.14.1/google/auth/jwt.py   2020-04-22 00:02:48.000000000 
+0200
+++ new/google-auth-1.15.0/google/auth/jwt.py   2020-05-18 19:26:54.000000000 
+0200
@@ -67,7 +67,7 @@
 _DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
 _DEFAULT_MAX_CACHE_SIZE = 10
 _ALGORITHM_TO_VERIFIER_CLASS = {"RS256": crypt.RSAVerifier}
-_CRYPTOGRAPHY_BASED_ALGORITHMS = set(["ES256"])
+_CRYPTOGRAPHY_BASED_ALGORITHMS = frozenset(["ES256"])
 
 if es256 is not None:  # pragma: NO COVER
     _ALGORITHM_TO_VERIFIER_CLASS["ES256"] = es256.ES256Verifier
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google-auth-1.14.1/google/auth/transport/_mtls_helper.py 
new/google-auth-1.15.0/google/auth/transport/_mtls_helper.py
--- old/google-auth-1.14.1/google/auth/transport/_mtls_helper.py        
2020-04-22 00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/auth/transport/_mtls_helper.py        
2020-05-18 19:26:54.000000000 +0200
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Helper functions for getting mTLS cert and key, for internal use only."""
+"""Helper functions for getting mTLS cert and key."""
 
 import json
 import logging
@@ -20,6 +20,10 @@
 import re
 import subprocess
 
+import six
+
+from google.auth import exceptions
+
 CONTEXT_AWARE_METADATA_PATH = "~/.secureConnect/context_aware_metadata.json"
 _CERT_PROVIDER_COMMAND = "cert_provider_command"
 _CERT_REGEX = re.compile(
@@ -30,6 +34,7 @@
 # "-----BEGIN PRIVATE KEY-----...",
 # "-----BEGIN EC PRIVATE KEY-----...",
 # "-----BEGIN RSA PRIVATE KEY-----..."
+# "-----BEGIN ENCRYPTED PRIVATE KEY-----"
 _KEY_REGEX = re.compile(
     b"-----BEGIN [A-Z ]*PRIVATE KEY-----.+-----END [A-Z ]*PRIVATE 
KEY-----\r?\n?",
     re.DOTALL,
@@ -38,6 +43,11 @@
 _LOGGER = logging.getLogger(__name__)
 
 
+_PASSPHRASE_REGEX = re.compile(
+    b"-----BEGIN PASSPHRASE-----(.+)-----END PASSPHRASE-----", re.DOTALL
+)
+
+
 def _check_dca_metadata_path(metadata_path):
     """Checks for context aware metadata. If it exists, returns the absolute 
path;
     otherwise returns None.
@@ -65,57 +75,109 @@
         Dict[str, str]: The metadata.
 
     Raises:
-        ValueError: If failed to parse metadata as JSON.
+        google.auth.exceptions.ClientCertError: If failed to parse metadata as 
JSON.
     """
-    with open(metadata_path) as f:
-        metadata = json.load(f)
+    try:
+        with open(metadata_path) as f:
+            metadata = json.load(f)
+    except ValueError as caught_exc:
+        new_exc = exceptions.ClientCertError(caught_exc)
+        six.raise_from(new_exc, caught_exc)
 
     return metadata
 
 
-def get_client_ssl_credentials(metadata_json):
-    """Returns the client side mTLS cert and key.
+def _run_cert_provider_command(command, expect_encrypted_key=False):
+    """Run the provided command, and return client side mTLS cert, key and
+    passphrase.
 
     Args:
-        metadata_json (Dict[str, str]): metadata JSON file which contains the 
cert
-            provider command.
+        command (List[str]): cert provider command.
+        expect_encrypted_key (bool): If encrypted private key is expected.
 
     Returns:
-        Tuple[bytes, bytes]: client certificate and key, both in PEM format.
+        Tuple[bytes, bytes, bytes]: client certificate bytes in PEM format, key
+            bytes in PEM format and passphrase bytes.
 
     Raises:
-        OSError: If the cert provider command failed to run.
-        RuntimeError: If the cert provider command has a runtime error.
-        ValueError: If the metadata json file doesn't contain the cert provider
-            command or if the command doesn't produce both the client 
certificate
-            and client key.
-    """
-    # TODO: implement an in-memory cache of cert and key so we don't have to
-    # run cert provider command every time.
-
-    # Check the cert provider command existence in the metadata json file.
-    if _CERT_PROVIDER_COMMAND not in metadata_json:
-        raise ValueError("Cert provider command is not found")
-
-    # Execute the command. It throws OsError in case of system failure.
-    command = metadata_json[_CERT_PROVIDER_COMMAND]
-    process = subprocess.Popen(command, stdout=subprocess.PIPE, 
stderr=subprocess.PIPE)
-    stdout, stderr = process.communicate()
+        google.auth.exceptions.ClientCertError: if problems occurs when running
+            the cert provider command or generating cert, key and passphrase.
+    """
+    try:
+        process = subprocess.Popen(
+            command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+        )
+        stdout, stderr = process.communicate()
+    except OSError as caught_exc:
+        new_exc = exceptions.ClientCertError(caught_exc)
+        six.raise_from(new_exc, caught_exc)
 
     # Check cert provider command execution error.
     if process.returncode != 0:
-        raise RuntimeError(
+        raise exceptions.ClientCertError(
             "Cert provider command returns non-zero status code %s" % 
process.returncode
         )
 
-    # Extract certificate (chain) and key.
+    # Extract certificate (chain), key and passphrase.
     cert_match = re.findall(_CERT_REGEX, stdout)
     if len(cert_match) != 1:
-        raise ValueError("Client SSL certificate is missing or invalid")
+        raise exceptions.ClientCertError("Client SSL certificate is missing or 
invalid")
     key_match = re.findall(_KEY_REGEX, stdout)
     if len(key_match) != 1:
-        raise ValueError("Client SSL key is missing or invalid")
-    return cert_match[0], key_match[0]
+        raise exceptions.ClientCertError("Client SSL key is missing or 
invalid")
+    passphrase_match = re.findall(_PASSPHRASE_REGEX, stdout)
+
+    if expect_encrypted_key:
+        if len(passphrase_match) != 1:
+            raise exceptions.ClientCertError("Passphrase is missing or 
invalid")
+        if b"ENCRYPTED" not in key_match[0]:
+            raise exceptions.ClientCertError("Encrypted private key is 
expected")
+        return cert_match[0], key_match[0], passphrase_match[0].strip()
+
+    if b"ENCRYPTED" in key_match[0]:
+        raise exceptions.ClientCertError("Encrypted private key is not 
expected")
+    if len(passphrase_match) > 0:
+        raise exceptions.ClientCertError("Passphrase is not expected")
+    return cert_match[0], key_match[0], None
+
+
+def get_client_ssl_credentials(generate_encrypted_key=False):
+    """Returns the client side certificate, private key and passphrase.
+
+    Args:
+        generate_encrypted_key (bool): If set to True, encrypted private key
+            and passphrase will be generated; otherwise, unencrypted private 
key
+            will be generated and passphrase will be None.
+
+    Returns:
+        Tuple[bool, bytes, bytes, bytes]:
+            A boolean indicating if cert, key and passphrase are obtained, the
+            cert bytes and key bytes both in PEM format, and passphrase bytes.
+
+    Raises:
+        google.auth.exceptions.ClientCertError: if problems occurs when getting
+            the cert, key and passphrase.
+    """
+    metadata_path = _check_dca_metadata_path(CONTEXT_AWARE_METADATA_PATH)
+
+    if metadata_path:
+        metadata_json = _read_dca_metadata_file(metadata_path)
+
+        if _CERT_PROVIDER_COMMAND not in metadata_json:
+            raise exceptions.ClientCertError("Cert provider command is not 
found")
+
+        command = metadata_json[_CERT_PROVIDER_COMMAND]
+
+        if generate_encrypted_key and "--with_passphrase" not in command:
+            command.append("--with_passphrase")
+
+        # Execute the command.
+        cert, key, passphrase = _run_cert_provider_command(
+            command, expect_encrypted_key=generate_encrypted_key
+        )
+        return True, cert, key, passphrase
+
+    return False, None, None, None
 
 
 def get_client_cert_and_key(client_cert_callback=None):
@@ -135,20 +197,54 @@
             and key bytes both in PEM format.
 
     Raises:
-        OSError: If the cert provider command failed to run.
-        RuntimeError: If the cert provider command has a runtime error.
-        ValueError: If the metadata json file doesn't contain the cert provider
-            command or if the command doesn't produce both the client 
certificate
-            and client key.
+        google.auth.exceptions.ClientCertError: if problems occurs when getting
+            the cert and key.
     """
     if client_cert_callback:
         cert, key = client_cert_callback()
         return True, cert, key
 
-    metadata_path = _check_dca_metadata_path(CONTEXT_AWARE_METADATA_PATH)
-    if metadata_path:
-        metadata = _read_dca_metadata_file(metadata_path)
-        cert, key = get_client_ssl_credentials(metadata)
-        return True, cert, key
+    has_cert, cert, key, _ = 
get_client_ssl_credentials(generate_encrypted_key=False)
+    return has_cert, cert, key
+
+
+def decrypt_private_key(key, passphrase):
+    """A helper function to decrypt the private key with the given passphrase.
+    google-auth library doesn't support passphrase protected private key for
+    mutual TLS channel. This helper function can be used to decrypt the
+    passphrase protected private key in order to estalish mutual TLS channel.
+
+    For example, if you have a function which produces client cert, passphrase
+    protected private key and passphrase, you can convert it to a client cert
+    callback function accepted by google-auth::
+
+        from google.auth.transport import _mtls_helper
+
+        def your_client_cert_function():
+            return cert, encrypted_key, passphrase
+
+        # callback accepted by google-auth for mutual TLS channel.
+        def client_cert_callback():
+            cert, encrypted_key, passphrase = your_client_cert_function()
+            decrypted_key = _mtls_helper.decrypt_private_key(encrypted_key,
+                passphrase)
+            return cert, decrypted_key
+
+    Args:
+        key (bytes): The private key bytes in PEM format.
+        passphrase (bytes): The passphrase bytes.
+
+    Returns:
+        bytes: The decrypted private key in PEM format.
+
+    Raises:
+        ImportError: If pyOpenSSL is not installed.
+        OpenSSL.crypto.Error: If there is any problem decrypting the private 
key.
+    """
+    from OpenSSL import crypto
+
+    # First convert encrypted_key_bytes to PKey object
+    pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key, 
passphrase=passphrase)
 
-    return False, None, None
+    # Then dump the decrypted key bytes
+    return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google/auth/transport/grpc.py 
new/google-auth-1.15.0/google/auth/transport/grpc.py
--- old/google-auth-1.14.1/google/auth/transport/grpc.py        2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/auth/transport/grpc.py        2020-05-18 
19:26:54.000000000 +0200
@@ -264,13 +264,10 @@
 
     def __init__(self):
         # Load client SSL credentials.
-        self._context_aware_metadata_path = 
_mtls_helper._check_dca_metadata_path(
+        metadata_path = _mtls_helper._check_dca_metadata_path(
             _mtls_helper.CONTEXT_AWARE_METADATA_PATH
         )
-        if self._context_aware_metadata_path:
-            self._is_mtls = True
-        else:
-            self._is_mtls = False
+        self._is_mtls = metadata_path is not None
 
     @property
     def ssl_credentials(self):
@@ -288,16 +285,13 @@
             google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel
                 creation failed for any reason.
         """
-        if self._context_aware_metadata_path:
+        if self._is_mtls:
             try:
-                metadata = _mtls_helper._read_dca_metadata_file(
-                    self._context_aware_metadata_path
-                )
-                cert, key = _mtls_helper.get_client_ssl_credentials(metadata)
+                _, cert, key, _ = _mtls_helper.get_client_ssl_credentials()
                 self._ssl_credentials = grpc.ssl_channel_credentials(
                     certificate_chain=cert, private_key=key
                 )
-            except (OSError, RuntimeError, ValueError) as caught_exc:
+            except exceptions.ClientCertError as caught_exc:
                 new_exc = exceptions.MutualTLSChannelError(caught_exc)
                 six.raise_from(new_exc, caught_exc)
         else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google/auth/transport/requests.py 
new/google-auth-1.15.0/google/auth/transport/requests.py
--- old/google-auth-1.14.1/google/auth/transport/requests.py    2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/auth/transport/requests.py    2020-05-18 
19:26:54.000000000 +0200
@@ -373,11 +373,9 @@
                 mtls_adapter = _MutualTlsAdapter(cert, key)
                 self.mount("https://";, mtls_adapter)
         except (
+            exceptions.ClientCertError,
             ImportError,
             OpenSSL.crypto.Error,
-            OSError,
-            RuntimeError,
-            ValueError,
         ) as caught_exc:
             new_exc = exceptions.MutualTLSChannelError(caught_exc)
             six.raise_from(new_exc, caught_exc)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google/auth/transport/urllib3.py 
new/google-auth-1.15.0/google/auth/transport/urllib3.py
--- old/google-auth-1.14.1/google/auth/transport/urllib3.py     2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/auth/transport/urllib3.py     2020-05-18 
19:26:54.000000000 +0200
@@ -316,11 +316,9 @@
             else:
                 self.http = _make_default_http()
         except (
+            exceptions.ClientCertError,
             ImportError,
             OpenSSL.crypto.Error,
-            OSError,
-            RuntimeError,
-            ValueError,
         ) as caught_exc:
             new_exc = exceptions.MutualTLSChannelError(caught_exc)
             six.raise_from(new_exc, caught_exc)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google/oauth2/id_token.py 
new/google-auth-1.15.0/google/oauth2/id_token.py
--- old/google-auth-1.14.1/google/oauth2/id_token.py    2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/google/oauth2/id_token.py    2020-05-18 
19:26:54.000000000 +0200
@@ -212,7 +212,7 @@
         )
         credentials.refresh(request)
         return credentials.token
-    except (ImportError, exceptions.TransportError):
+    except (ImportError, exceptions.TransportError, exceptions.RefreshError):
         pass
 
     # 2. Try to use service account credentials to get ID token.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/google_auth.egg-info/PKG-INFO 
new/google-auth-1.15.0/google_auth.egg-info/PKG-INFO
--- old/google-auth-1.14.1/google_auth.egg-info/PKG-INFO        2020-04-22 
00:04:21.000000000 +0200
+++ new/google-auth-1.15.0/google_auth.egg-info/PKG-INFO        2020-05-18 
19:28:26.000000000 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 1.2
 Name: google-auth
-Version: 1.14.1
+Version: 1.15.0
 Summary: Google Authentication Library
 Home-page: https://github.com/googleapis/google-auth-library-python
 Author: Google Cloud Platform
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/setup.py 
new/google-auth-1.15.0/setup.py
--- old/google-auth-1.14.1/setup.py     2020-04-22 00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/setup.py     2020-05-18 19:26:54.000000000 +0200
@@ -30,7 +30,7 @@
 with io.open("README.rst", "r") as fh:
     long_description = fh.read()
 
-version = "1.14.1"
+version = "1.15.0"
 
 setup(
     name="google-auth",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google-auth-1.14.1/tests/test_impersonated_credentials.py 
new/google-auth-1.15.0/tests/test_impersonated_credentials.py
--- old/google-auth-1.14.1/tests/test_impersonated_credentials.py       
2020-04-22 00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/tests/test_impersonated_credentials.py       
2020-05-18 19:26:54.000000000 +0200
@@ -132,10 +132,17 @@
         assert not credentials.valid
         assert credentials.expired
 
-    def make_request(self, data, status=http_client.OK, headers=None, 
side_effect=None):
+    def make_request(
+        self,
+        data,
+        status=http_client.OK,
+        headers=None,
+        side_effect=None,
+        use_data_bytes=True,
+    ):
         response = mock.create_autospec(transport.Response, instance=False)
         response.status = status
-        response.data = _helpers.to_bytes(data)
+        response.data = _helpers.to_bytes(data) if use_data_bytes else data
         response.headers = headers or {}
 
         request = mock.create_autospec(transport.Request, instance=False)
@@ -144,7 +151,8 @@
 
         return request
 
-    def test_refresh_success(self, mock_donor_credentials):
+    @pytest.mark.parametrize("use_data_bytes", [True, False])
+    def test_refresh_success(self, use_data_bytes, mock_donor_credentials):
         credentials = self.make_credentials(lifetime=None)
         token = "token"
 
@@ -154,7 +162,9 @@
         response_body = {"accessToken": token, "expireTime": expire_time}
 
         request = self.make_request(
-            data=json.dumps(response_body), status=http_client.OK
+            data=json.dumps(response_body),
+            status=http_client.OK,
+            use_data_bytes=use_data_bytes,
         )
 
         credentials.refresh(request)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google-auth-1.14.1/tests/transport/test__mtls_helper.py 
new/google-auth-1.15.0/tests/transport/test__mtls_helper.py
--- old/google-auth-1.14.1/tests/transport/test__mtls_helper.py 2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/tests/transport/test__mtls_helper.py 2020-05-18 
19:26:54.000000000 +0200
@@ -16,14 +16,34 @@
 import re
 
 import mock
+from OpenSSL import crypto
 import pytest
 
+from google.auth import exceptions
 from google.auth.transport import _mtls_helper
 
 CONTEXT_AWARE_METADATA = {"cert_provider_command": ["some command"]}
 
 CONTEXT_AWARE_METADATA_NO_CERT_PROVIDER_COMMAND = {}
 
+ENCRYPTED_EC_PRIVATE_KEY = b"""-----BEGIN ENCRYPTED PRIVATE KEY-----
+MIHkME8GCSqGSIb3DQEFDTBCMCkGCSqGSIb3DQEFDDAcBAgl2/yVgs1h3QICCAAw
+DAYIKoZIhvcNAgkFADAVBgkrBgEEAZdVAQIECJk2GRrvxOaJBIGQXIBnMU4wmciT
+uA6yD8q0FxuIzjG7E2S6tc5VRgSbhRB00eBO3jWmO2pBybeQW+zVioDcn50zp2ts
+wYErWC+LCm1Zg3r+EGnT1E1GgNoODbVQ3AEHlKh1CGCYhEovxtn3G+Fjh7xOBrNB
+saVVeDb4tHD4tMkiVVUBrUcTZPndP73CtgyGHYEphasYPzEz3+AU
+-----END ENCRYPTED PRIVATE KEY-----"""
+
+EC_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvCNi1NoDY1oMqPHIgXI8RBbTYGi/
+brEjbre1nSiQW11xRTJbVeETdsuP0EAu2tG3PcRhhwDfeJ8zXREgTBurNw==
+-----END PUBLIC KEY-----"""
+
+PASSPHRASE = b"""-----BEGIN PASSPHRASE-----
+password
+-----END PASSPHRASE-----"""
+PASSPHRASE_VALUE = b"password"
+
 
 def check_cert_and_key(content, expected_cert, expected_key):
     success = True
@@ -115,11 +135,11 @@
     def test_file_not_json(self):
         # read a file which is not json format.
         metadata_path = os.path.join(pytest.data_dir, "privatekey.pem")
-        with pytest.raises(ValueError):
+        with pytest.raises(exceptions.ClientCertError):
             _mtls_helper._read_dca_metadata_file(metadata_path)
 
 
-class TestGetClientSslCredentials(object):
+class TestRunCertProviderCommand(object):
     def create_mock_process(self, output, error):
         # There are two steps to execute a script with subprocess.Popen.
         # (1) process = subprocess.Popen([comannds])
@@ -137,9 +157,20 @@
         mock_popen.return_value = self.create_mock_process(
             pytest.public_cert_bytes + pytest.private_key_bytes, b""
         )
-        cert, key = 
_mtls_helper.get_client_ssl_credentials(CONTEXT_AWARE_METADATA)
+        cert, key, passphrase = 
_mtls_helper._run_cert_provider_command(["command"])
         assert cert == pytest.public_cert_bytes
         assert key == pytest.private_key_bytes
+        assert passphrase is None
+
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, 
b""
+        )
+        cert, key, passphrase = _mtls_helper._run_cert_provider_command(
+            ["command"], expect_encrypted_key=True
+        )
+        assert cert == pytest.public_cert_bytes
+        assert key == ENCRYPTED_EC_PRIVATE_KEY
+        assert passphrase == PASSPHRASE_VALUE
 
     @mock.patch("subprocess.Popen", autospec=True)
     def test_success_with_cert_chain(self, mock_popen):
@@ -147,44 +178,185 @@
         mock_popen.return_value = self.create_mock_process(
             PUBLIC_CERT_CHAIN_BYTES + pytest.private_key_bytes, b""
         )
-        cert, key = 
_mtls_helper.get_client_ssl_credentials(CONTEXT_AWARE_METADATA)
+        cert, key, passphrase = 
_mtls_helper._run_cert_provider_command(["command"])
         assert cert == PUBLIC_CERT_CHAIN_BYTES
         assert key == pytest.private_key_bytes
+        assert passphrase is None
 
-    def test_missing_cert_provider_command(self):
-        with pytest.raises(ValueError):
-            assert _mtls_helper.get_client_ssl_credentials(
-                CONTEXT_AWARE_METADATA_NO_CERT_PROVIDER_COMMAND
-            )
+        mock_popen.return_value = self.create_mock_process(
+            PUBLIC_CERT_CHAIN_BYTES + ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, 
b""
+        )
+        cert, key, passphrase = _mtls_helper._run_cert_provider_command(
+            ["command"], expect_encrypted_key=True
+        )
+        assert cert == PUBLIC_CERT_CHAIN_BYTES
+        assert key == ENCRYPTED_EC_PRIVATE_KEY
+        assert passphrase == PASSPHRASE_VALUE
 
     @mock.patch("subprocess.Popen", autospec=True)
     def test_missing_cert(self, mock_popen):
         mock_popen.return_value = self.create_mock_process(
             pytest.private_key_bytes, b""
         )
-        with pytest.raises(ValueError):
-            assert 
_mtls_helper.get_client_ssl_credentials(CONTEXT_AWARE_METADATA)
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+        mock_popen.return_value = self.create_mock_process(
+            ENCRYPTED_EC_PRIVATE_KEY + PASSPHRASE, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
 
     @mock.patch("subprocess.Popen", autospec=True)
     def test_missing_key(self, mock_popen):
         mock_popen.return_value = self.create_mock_process(
             pytest.public_cert_bytes, b""
         )
-        with pytest.raises(ValueError):
-            assert 
_mtls_helper.get_client_ssl_credentials(CONTEXT_AWARE_METADATA)
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + PASSPHRASE, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_missing_passphrase(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_passphrase_not_expected(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, 
b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_encrypted_key_expected(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + pytest.private_key_bytes + PASSPHRASE, 
b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(
+                ["command"], expect_encrypted_key=True
+            )
+
+    @mock.patch("subprocess.Popen", autospec=True)
+    def test_unencrypted_key_expected(self, mock_popen):
+        mock_popen.return_value = self.create_mock_process(
+            pytest.public_cert_bytes + ENCRYPTED_EC_PRIVATE_KEY, b""
+        )
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
 
     @mock.patch("subprocess.Popen", autospec=True)
     def test_cert_provider_returns_error(self, mock_popen):
         mock_popen.return_value = self.create_mock_process(b"", b"some error")
         mock_popen.return_value.returncode = 1
-        with pytest.raises(RuntimeError):
-            assert 
_mtls_helper.get_client_ssl_credentials(CONTEXT_AWARE_METADATA)
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
 
     @mock.patch("subprocess.Popen", autospec=True)
     def test_popen_raise_exception(self, mock_popen):
         mock_popen.side_effect = OSError()
-        with pytest.raises(OSError):
-            assert 
_mtls_helper.get_client_ssl_credentials(CONTEXT_AWARE_METADATA)
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper._run_cert_provider_command(["command"])
+
+
+class TestGetClientSslCredentials(object):
+    @mock.patch(
+        "google.auth.transport._mtls_helper._run_cert_provider_command", 
autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", 
autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", 
autospec=True
+    )
+    def test_success(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_run_cert_provider_command,
+    ):
+        mock_check_dca_metadata_path.return_value = True
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["command"]
+        }
+        mock_run_cert_provider_command.return_value = (b"cert", b"key", None)
+        has_cert, cert, key, passphrase = 
_mtls_helper.get_client_ssl_credentials()
+        assert has_cert
+        assert cert == b"cert"
+        assert key == b"key"
+        assert passphrase is None
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", 
autospec=True
+    )
+    def test_success_without_metadata(self, mock_check_dca_metadata_path):
+        mock_check_dca_metadata_path.return_value = False
+        has_cert, cert, key, passphrase = 
_mtls_helper.get_client_ssl_credentials()
+        assert not has_cert
+        assert cert is None
+        assert key is None
+        assert passphrase is None
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._run_cert_provider_command", 
autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", 
autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", 
autospec=True
+    )
+    def test_success_with_encrypted_key(
+        self,
+        mock_check_dca_metadata_path,
+        mock_read_dca_metadata_file,
+        mock_run_cert_provider_command,
+    ):
+        mock_check_dca_metadata_path.return_value = True
+        mock_read_dca_metadata_file.return_value = {
+            "cert_provider_command": ["command"]
+        }
+        mock_run_cert_provider_command.return_value = (b"cert", b"key", 
b"passphrase")
+        has_cert, cert, key, passphrase = 
_mtls_helper.get_client_ssl_credentials(
+            generate_encrypted_key=True
+        )
+        assert has_cert
+        assert cert == b"cert"
+        assert key == b"key"
+        assert passphrase == b"passphrase"
+        mock_run_cert_provider_command.assert_called_once_with(
+            ["command", "--with_passphrase"], expect_encrypted_key=True
+        )
+
+    @mock.patch(
+        "google.auth.transport._mtls_helper._read_dca_metadata_file", 
autospec=True
+    )
+    @mock.patch(
+        "google.auth.transport._mtls_helper._check_dca_metadata_path", 
autospec=True
+    )
+    def test_missing_cert_command(
+        self, mock_check_dca_metadata_path, mock_read_dca_metadata_file
+    ):
+        mock_check_dca_metadata_path.return_value = True
+        mock_read_dca_metadata_file.return_value = {}
+        with pytest.raises(exceptions.ClientCertError):
+            _mtls_helper.get_client_ssl_credentials()
 
 
 class TestGetClientCertAndKey(object):
@@ -198,32 +370,38 @@
         assert key == pytest.private_key_bytes
 
     @mock.patch(
-        "google.auth.transport._mtls_helper._check_dca_metadata_path", 
autospec=True
-    )
-    def test_no_metadata(self, mock_check_dca_metadata_path):
-        mock_check_dca_metadata_path.return_value = None
-
-        found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key()
-        assert not found_cert_key
-
-    @mock.patch(
         "google.auth.transport._mtls_helper.get_client_ssl_credentials", 
autospec=True
     )
-    @mock.patch(
-        "google.auth.transport._mtls_helper._check_dca_metadata_path", 
autospec=True
-    )
-    def test_use_metadata(
-        self, mock_check_dca_metadata_path, mock_get_client_ssl_credentials
-    ):
-        mock_check_dca_metadata_path.return_value = os.path.join(
-            pytest.data_dir, "context_aware_metadata.json"
-        )
+    def test_use_metadata(self, mock_get_client_ssl_credentials):
         mock_get_client_ssl_credentials.return_value = (
+            True,
             pytest.public_cert_bytes,
             pytest.private_key_bytes,
+            None,
         )
 
         found_cert_key, cert, key = _mtls_helper.get_client_cert_and_key()
         assert found_cert_key
         assert cert == pytest.public_cert_bytes
         assert key == pytest.private_key_bytes
+
+
+class TestDecryptPrivateKey(object):
+    def test_success(self):
+        decrypted_key = _mtls_helper.decrypt_private_key(
+            ENCRYPTED_EC_PRIVATE_KEY, PASSPHRASE_VALUE
+        )
+        private_key = crypto.load_privatekey(crypto.FILETYPE_PEM, 
decrypted_key)
+        public_key = crypto.load_publickey(crypto.FILETYPE_PEM, EC_PUBLIC_KEY)
+        x509 = crypto.X509()
+        x509.set_pubkey(public_key)
+
+        # Test the decrypted key works by signing and verification.
+        signature = crypto.sign(private_key, b"data", "sha256")
+        crypto.verify(x509, signature, b"data", "sha256")
+
+    def test_crypto_error(self):
+        with pytest.raises(crypto.Error):
+            _mtls_helper.decrypt_private_key(
+                ENCRYPTED_EC_PRIVATE_KEY, b"wrong_password"
+            )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/tests/transport/test_grpc.py 
new/google-auth-1.15.0/tests/transport/test_grpc.py
--- old/google-auth-1.14.1/tests/transport/test_grpc.py 2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/tests/transport/test_grpc.py 2020-05-18 
19:26:54.000000000 +0200
@@ -129,7 +129,12 @@
         read_dca_metadata_file.return_value = {
             "cert_provider_command": ["some command"]
         }
-        get_client_ssl_credentials.return_value = (PUBLIC_CERT_BYTES, 
PRIVATE_KEY_BYTES)
+        get_client_ssl_credentials.return_value = (
+            True,
+            PUBLIC_CERT_BYTES,
+            PRIVATE_KEY_BYTES,
+            None,
+        )
 
         channel = google.auth.transport.grpc.secure_authorized_channel(
             credentials, request, target, options=mock.sentinel.options
@@ -314,7 +319,7 @@
         }
 
         # Mock that client cert and key are not loaded and exception is raised.
-        mock_get_client_ssl_credentials.side_effect = ValueError()
+        mock_get_client_ssl_credentials.side_effect = 
exceptions.ClientCertError()
 
         with pytest.raises(exceptions.MutualTLSChannelError):
             assert google.auth.transport.grpc.SslCredentials().ssl_credentials
@@ -331,8 +336,10 @@
             "cert_provider_command": ["some command"]
         }
         mock_get_client_ssl_credentials.return_value = (
+            True,
             PUBLIC_CERT_BYTES,
             PRIVATE_KEY_BYTES,
+            None,
         )
 
         ssl_credentials = google.auth.transport.grpc.SslCredentials()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/tests/transport/test_requests.py 
new/google-auth-1.15.0/tests/transport/test_requests.py
--- old/google-auth-1.14.1/tests/transport/test_requests.py     2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/tests/transport/test_requests.py     2020-05-18 
19:26:54.000000000 +0200
@@ -55,12 +55,12 @@
 
     def test_tracks_elapsed_time_w_numeric_timeout(self, frozen_time):
         with self.make_guard(timeout=10) as guard:
-            frozen_time.tick(delta=3.8)
+            frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
         assert guard.remaining_timeout == 6.2
 
     def test_tracks_elapsed_time_w_tuple_timeout(self, frozen_time):
         with self.make_guard(timeout=(16, 19)) as guard:
-            frozen_time.tick(delta=3.8)
+            frozen_time.tick(delta=datetime.timedelta(seconds=3.8))
         assert guard.remaining_timeout == (12.2, 15.2)
 
     def test_noop_if_no_timeout(self, frozen_time):
@@ -72,13 +72,13 @@
     def test_timeout_error_w_numeric_timeout(self, frozen_time):
         with pytest.raises(requests.exceptions.Timeout):
             with self.make_guard(timeout=10) as guard:
-                frozen_time.tick(delta=10.001)
+                frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
         assert guard.remaining_timeout == pytest.approx(-0.001)
 
     def test_timeout_error_w_tuple_timeout(self, frozen_time):
         with pytest.raises(requests.exceptions.Timeout):
             with self.make_guard(timeout=(11, 10)) as guard:
-                frozen_time.tick(delta=10.001)
+                frozen_time.tick(delta=datetime.timedelta(seconds=10.001))
         assert guard.remaining_timeout == pytest.approx((0.999, -0.001))
 
     def test_custom_timeout_error_type(self, frozen_time):
@@ -87,7 +87,7 @@
 
         with pytest.raises(FooError):
             with self.make_guard(timeout=1, timeout_error_type=FooError):
-                frozen_time.tick(2)
+                frozen_time.tick(delta=datetime.timedelta(seconds=2))
 
     def test_lets_suite_errors_bubble_up(self, frozen_time):
         with pytest.raises(IndexError):
@@ -222,7 +222,7 @@
             authed_session.request("GET", self.TEST_URL)
 
         expected_timeout = google.auth.transport.requests._DEFAULT_TIMEOUT
-        assert patched_request.call_args.kwargs.get("timeout") == 
expected_timeout
+        assert patched_request.call_args[1]["timeout"] == expected_timeout
 
     def test_request_no_refresh(self):
         credentials = mock.Mock(wraps=CredentialsStub())
@@ -268,7 +268,9 @@
         assert adapter.requests[1].headers["authorization"] == "token1"
 
     def test_request_max_allowed_time_timeout_error(self, frozen_time):
-        tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
 
         credentials = mock.Mock(
             wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
@@ -286,7 +288,9 @@
             authed_session.request("GET", self.TEST_URL, max_allowed_time=0.9)
 
     def test_request_max_allowed_time_w_transport_timeout_no_error(self, 
frozen_time):
-        tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
 
         credentials = mock.Mock(
             wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
@@ -308,7 +312,9 @@
         authed_session.request("GET", self.TEST_URL, timeout=0.5, 
max_allowed_time=3.1)
 
     def test_request_max_allowed_time_w_refresh_timeout_no_error(self, 
frozen_time):
-        tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
 
         credentials = mock.Mock(
             wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
@@ -333,7 +339,9 @@
         authed_session.request("GET", self.TEST_URL, timeout=60, 
max_allowed_time=3.1)
 
     def test_request_timeout_w_refresh_timeout_timeout_error(self, 
frozen_time):
-        tick_one_second = functools.partial(frozen_time.tick, delta=1.0)
+        tick_one_second = functools.partial(
+            frozen_time.tick, delta=datetime.timedelta(seconds=1.0)
+        )
 
         credentials = mock.Mock(
             wraps=TimeTickCredentialsStub(time_tick=tick_one_second)
@@ -421,7 +429,7 @@
         "google.auth.transport._mtls_helper.get_client_cert_and_key", 
autospec=True
     )
     def test_configure_mtls_channel_exceptions(self, 
mock_get_client_cert_and_key):
-        mock_get_client_cert_and_key.side_effect = ValueError()
+        mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
 
         auth_session = google.auth.transport.requests.AuthorizedSession(
             credentials=mock.Mock()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-1.14.1/tests/transport/test_urllib3.py 
new/google-auth-1.15.0/tests/transport/test_urllib3.py
--- old/google-auth-1.14.1/tests/transport/test_urllib3.py      2020-04-22 
00:02:48.000000000 +0200
+++ new/google-auth-1.15.0/tests/transport/test_urllib3.py      2020-05-18 
19:26:54.000000000 +0200
@@ -233,7 +233,7 @@
             credentials=mock.Mock()
         )
 
-        mock_get_client_cert_and_key.side_effect = ValueError()
+        mock_get_client_cert_and_key.side_effect = exceptions.ClientCertError()
         with pytest.raises(exceptions.MutualTLSChannelError):
             authed_http.configure_mtls_channel()
 


Reply via email to