diff --git a/google/auth/crypt/__init__.py b/google/auth/crypt/__init__.py new file mode 100644 index 000000000..fa59662e0 --- /dev/null +++ b/google/auth/crypt/__init__.py @@ -0,0 +1,79 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Cryptography helpers for verifying and signing messages. + +The simplest way to verify signatures is using :func:`verify_signature`:: + + cert = open('certs.pem').read() + valid = crypt.verify_signature(message, signature, cert) + +If you're going to verify many messages with the same certificate, you can use +:class:`RSAVerifier`:: + + cert = open('certs.pem').read() + verifier = crypt.RSAVerifier.from_string(cert) + valid = verifier.verify(message, signature) + +To sign messages use :class:`RSASigner` with a private key:: + + private_key = open('private_key.pem').read() + signer = crypt.RSASigner(private_key) + signature = signer.sign(message) +""" + +import six + +from google.auth.crypt import base +from google.auth.crypt import rsa + + +__all__ = [ + 'RSASigner', + 'RSAVerifier', + 'Signer', + 'Verifier', +] + +# Aliases to maintain the v1.0.0 interface, as the crypt module was split +# into submodules. +Signer = base.Signer +Verifier = base.Verifier +RSASigner = rsa.RSASigner +RSAVerifier = rsa.RSAVerifier + + +def verify_signature(message, signature, certs): + """Verify an RSA cryptographic signature. + + Checks that the provided ``signature`` was generated from ``bytes`` using + the private key associated with the ``cert``. + + Args: + message (Union[str, bytes]): The plaintext message. + signature (Union[str, bytes]): The cryptographic signature to check. + certs (Union[Sequence, str, bytes]): The certificate or certificates + to use to check the signature. + + Returns: + bool: True if the signature is valid, otherwise False. + """ + if isinstance(certs, (six.text_type, six.binary_type)): + certs = [certs] + + for cert in certs: + verifier = rsa.RSAVerifier.from_string(cert) + if verifier.verify(message, signature): + return True + return False diff --git a/google/auth/crypt.py b/google/auth/crypt/_python_rsa.py similarity index 68% rename from google/auth/crypt.py rename to google/auth/crypt/_python_rsa.py index 65bf37f22..1f6384d2c 100644 --- a/google/auth/crypt.py +++ b/google/auth/crypt/_python_rsa.py @@ -12,33 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Cryptography helpers for verifying and signing messages. +"""Pure-Python RSA cryptography implementation. Uses the ``rsa``, ``pyasn1`` and ``pyasn1_modules`` packages to parse PEM files storing PKCS#1 or PKCS#8 keys as well as certificates. There is no support for p12 files. +""" -The simplest way to verify signatures is using :func:`verify_signature`:: - - cert = open('certs.pem').read() - valid = crypt.verify_signature(message, signature, cert) - -If you're going to verify many messages with the same certificate, you can use -:class:`RSAVerifier`:: - - cert = open('certs.pem').read() - verifier = crypt.RSAVerifier.from_string(cert) - valid = verifier.verify(message, signature) - - -To sign messages use :class:`RSASigner` with a private key:: - - private_key = open('private_key.pem').read() - signer = crypt.RSASigner(private_key) - signature = signer.sign(message) +from __future__ import absolute_import -""" -import abc import io import json @@ -50,6 +32,7 @@ import six from google.auth import _helpers +from google.auth.crypt import base _POW2 = (128, 64, 32, 16, 8, 4, 2, 1) _CERTIFICATE_MARKER = b'-----BEGIN CERTIFICATE-----' @@ -84,28 +67,7 @@ def _bit_list_to_bytes(bit_list): return bytes(byte_vals) -@six.add_metaclass(abc.ABCMeta) -class Verifier(object): - """Abstract base class for crytographic signature verifiers.""" - - @abc.abstractmethod - def verify(self, message, signature): - """Verifies a message against a cryptographic signature. - - Args: - message (Union[str, bytes]): The message to verify. - signature (Union[str, bytes]): The cryptography signature to check. - - Returns: - bool: True if message was signed by the private key associated - with the public key that this object was constructed with. - """ - # pylint: disable=missing-raises-doc,redundant-returns-doc - # (pylint doesn't recognize that this is abstract) - raise NotImplementedError('Verify must be implemented') - - -class RSAVerifier(Verifier): +class RSAVerifier(base.Verifier): """Verifies RSA cryptographic signatures using public keys. Args: @@ -116,7 +78,7 @@ class RSAVerifier(Verifier): def __init__(self, public_key): self._pubkey = public_key - @_helpers.copy_docstring(Verifier) + @_helpers.copy_docstring(base.Verifier) def verify(self, message, signature): message = _helpers.to_bytes(message) try: @@ -157,56 +119,7 @@ def from_string(cls, public_key): return cls(pubkey) -def verify_signature(message, signature, certs): - """Verify an RSA cryptographic signature. - - Checks that the provided ``signature`` was generated from ``bytes`` using - the private key associated with the ``cert``. - - Args: - message (Union[str, bytes]): The plaintext message. - signature (Union[str, bytes]): The cryptographic signature to check. - certs (Union[Sequence, str, bytes]): The certificate or certificates - to use to check the signature. - - Returns: - bool: True if the signature is valid, otherwise False. - """ - if isinstance(certs, (six.text_type, six.binary_type)): - certs = [certs] - - for cert in certs: - verifier = RSAVerifier.from_string(cert) - if verifier.verify(message, signature): - return True - return False - - -@six.add_metaclass(abc.ABCMeta) -class Signer(object): - """Abstract base class for cryptographic signers.""" - - @abc.abstractproperty - def key_id(self): - """Optional[str]: The key ID used to identify this private key.""" - raise NotImplementedError('Key id must be implemented') - - @abc.abstractmethod - def sign(self, message): - """Signs a message. - - Args: - message (Union[str, bytes]): The message to be signed. - - Returns: - bytes: The signature of the message. - """ - # pylint: disable=missing-raises-doc,redundant-returns-doc - # (pylint doesn't recognize that this is abstract) - raise NotImplementedError('Sign must be implemented') - - -class RSASigner(Signer): +class RSASigner(base.Signer): """Signs messages with an RSA private key. Args: @@ -221,11 +134,11 @@ def __init__(self, private_key, key_id=None): self._key_id = key_id @property - @_helpers.copy_docstring(Signer) + @_helpers.copy_docstring(base.Signer) def key_id(self): return self._key_id - @_helpers.copy_docstring(Signer) + @_helpers.copy_docstring(base.Signer) def sign(self, message): message = _helpers.to_bytes(message) return rsa.pkcs1.sign(message, self._key, 'SHA-256') diff --git a/google/auth/crypt/base.py b/google/auth/crypt/base.py new file mode 100644 index 000000000..05c5a2bf5 --- /dev/null +++ b/google/auth/crypt/base.py @@ -0,0 +1,64 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Base classes for cryptographic signers and verifiers.""" + +import abc + +import six + + +@six.add_metaclass(abc.ABCMeta) +class Verifier(object): + """Abstract base class for crytographic signature verifiers.""" + + @abc.abstractmethod + def verify(self, message, signature): + """Verifies a message against a cryptographic signature. + + Args: + message (Union[str, bytes]): The message to verify. + signature (Union[str, bytes]): The cryptography signature to check. + + Returns: + bool: True if message was signed by the private key associated + with the public key that this object was constructed with. + """ + # pylint: disable=missing-raises-doc,redundant-returns-doc + # (pylint doesn't recognize that this is abstract) + raise NotImplementedError('Verify must be implemented') + + +@six.add_metaclass(abc.ABCMeta) +class Signer(object): + """Abstract base class for cryptographic signers.""" + + @abc.abstractproperty + def key_id(self): + """Optional[str]: The key ID used to identify this private key.""" + raise NotImplementedError('Key id must be implemented') + + @abc.abstractmethod + def sign(self, message): + """Signs a message. + + Args: + message (Union[str, bytes]): The message to be signed. + + Returns: + bytes: The signature of the message. + """ + # pylint: disable=missing-raises-doc,redundant-returns-doc + # (pylint doesn't recognize that this is abstract) + raise NotImplementedError('Sign must be implemented') diff --git a/google/auth/crypt/rsa.py b/google/auth/crypt/rsa.py new file mode 100644 index 000000000..d0bf2a0b9 --- /dev/null +++ b/google/auth/crypt/rsa.py @@ -0,0 +1,20 @@ +# Copyright 2017 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""RSA cryptography signer and verifier.""" + +from google.auth.crypt import _python_rsa + +RSASigner = _python_rsa.RSASigner +RSAVerifier = _python_rsa.RSAVerifier diff --git a/tests/crypt/__init__.py b/tests/crypt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_crypt.py b/tests/crypt/test__python_rsa.py similarity index 66% rename from tests/test_crypt.py rename to tests/crypt/test__python_rsa.py index 56612dae3..cff1034bc 100644 --- a/tests/test_crypt.py +++ b/tests/crypt/test__python_rsa.py @@ -22,10 +22,10 @@ import six from google.auth import _helpers -from google.auth import crypt +from google.auth.crypt import _python_rsa -DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data') # To generate privatekey.pem, privatekey.pub, and public_cert.pem: # $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \ @@ -42,12 +42,6 @@ with open(os.path.join(DATA_DIR, 'public_cert.pem'), 'rb') as fh: PUBLIC_CERT_BYTES = fh.read() -# To generate other_cert.pem: -# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out other_cert.pem - -with open(os.path.join(DATA_DIR, 'other_cert.pem'), 'rb') as fh: - OTHER_CERT_BYTES = fh.read() - # To generate pem_from_pkcs12.pem and privatekey.p12: # $ openssl pkcs12 -export -out privatekey.p12 -inkey privatekey.pem \ # > -in public_cert.pem @@ -67,72 +61,50 @@ SERVICE_ACCOUNT_INFO = json.load(fh) -def test_verify_signature(): - to_sign = b'foo' - signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) - signature = signer.sign(to_sign) - - assert crypt.verify_signature( - to_sign, signature, PUBLIC_CERT_BYTES) - - # List of certs - assert crypt.verify_signature( - to_sign, signature, [OTHER_CERT_BYTES, PUBLIC_CERT_BYTES]) - - -def test_verify_signature_failure(): - to_sign = b'foo' - signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) - signature = signer.sign(to_sign) - - assert not crypt.verify_signature( - to_sign, signature, OTHER_CERT_BYTES) - - class TestRSAVerifier(object): def test_verify_success(self): to_sign = b'foo' - signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) + signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) - verifier = crypt.RSAVerifier.from_string(PUBLIC_KEY_BYTES) + verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_unicode_success(self): to_sign = u'foo' - signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) + signer = _python_rsa.RSASigner.from_string(PRIVATE_KEY_BYTES) actual_signature = signer.sign(to_sign) - verifier = crypt.RSAVerifier.from_string(PUBLIC_KEY_BYTES) + verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) assert verifier.verify(to_sign, actual_signature) def test_verify_failure(self): - verifier = crypt.RSAVerifier.from_string(PUBLIC_KEY_BYTES) + verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) bad_signature1 = b'' assert not verifier.verify(b'foo', bad_signature1) bad_signature2 = b'a' assert not verifier.verify(b'foo', bad_signature2) def test_from_string_pub_key(self): - verifier = crypt.RSAVerifier.from_string(PUBLIC_KEY_BYTES) - assert isinstance(verifier, crypt.RSAVerifier) + verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_KEY_BYTES) + assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_key_unicode(self): public_key = _helpers.from_bytes(PUBLIC_KEY_BYTES) - verifier = crypt.RSAVerifier.from_string(public_key) - assert isinstance(verifier, crypt.RSAVerifier) + verifier = _python_rsa.RSAVerifier.from_string(public_key) + assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_cert(self): - verifier = crypt.RSAVerifier.from_string(PUBLIC_CERT_BYTES) - assert isinstance(verifier, crypt.RSAVerifier) + verifier = _python_rsa.RSAVerifier.from_string(PUBLIC_CERT_BYTES) + assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_cert_unicode(self): public_cert = _helpers.from_bytes(PUBLIC_CERT_BYTES) - verifier = crypt.RSAVerifier.from_string(public_cert) - assert isinstance(verifier, crypt.RSAVerifier) + verifier = _python_rsa.RSAVerifier.from_string(public_cert) + assert isinstance(verifier, _python_rsa.RSAVerifier) assert isinstance(verifier._pubkey, rsa.key.PublicKey) def test_from_string_pub_cert_failure(self): @@ -144,32 +116,32 @@ def test_from_string_pub_cert_failure(self): with load_pem_patch as load_pem: with pytest.raises(ValueError): - crypt.RSAVerifier.from_string(cert_bytes) + _python_rsa.RSAVerifier.from_string(cert_bytes) load_pem.assert_called_once_with(cert_bytes, 'CERTIFICATE') class TestRSASigner(object): def test_from_string_pkcs1(self): - signer = crypt.RSASigner.from_string(PKCS1_KEY_BYTES) - assert isinstance(signer, crypt.RSASigner) + signer = _python_rsa.RSASigner.from_string(PKCS1_KEY_BYTES) + assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs1_unicode(self): key_bytes = _helpers.from_bytes(PKCS1_KEY_BYTES) - signer = crypt.RSASigner.from_string(key_bytes) - assert isinstance(signer, crypt.RSASigner) + signer = _python_rsa.RSASigner.from_string(key_bytes) + assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs8(self): - signer = crypt.RSASigner.from_string(PKCS8_KEY_BYTES) - assert isinstance(signer, crypt.RSASigner) + signer = _python_rsa.RSASigner.from_string(PKCS8_KEY_BYTES) + assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs8_extra_bytes(self): key_bytes = PKCS8_KEY_BYTES _, pem_bytes = pem.readPemBlocksFromFile( six.StringIO(_helpers.from_bytes(key_bytes)), - crypt._PKCS8_MARKER) + _python_rsa._PKCS8_MARKER) key_info, remaining = None, 'extra' decode_patch = mock.patch( @@ -179,44 +151,44 @@ def test_from_string_pkcs8_extra_bytes(self): with decode_patch as decode: with pytest.raises(ValueError): - crypt.RSASigner.from_string(key_bytes) + _python_rsa.RSASigner.from_string(key_bytes) # Verify mock was called. decode.assert_called_once_with( - pem_bytes, asn1Spec=crypt._PKCS8_SPEC) + pem_bytes, asn1Spec=_python_rsa._PKCS8_SPEC) def test_from_string_pkcs8_unicode(self): key_bytes = _helpers.from_bytes(PKCS8_KEY_BYTES) - signer = crypt.RSASigner.from_string(key_bytes) - assert isinstance(signer, crypt.RSASigner) + signer = _python_rsa.RSASigner.from_string(key_bytes) + assert isinstance(signer, _python_rsa.RSASigner) assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_string_pkcs12(self): with pytest.raises(ValueError): - crypt.RSASigner.from_string(PKCS12_KEY_BYTES) + _python_rsa.RSASigner.from_string(PKCS12_KEY_BYTES) def test_from_string_bogus_key(self): key_bytes = 'bogus-key' with pytest.raises(ValueError): - crypt.RSASigner.from_string(key_bytes) + _python_rsa.RSASigner.from_string(key_bytes) def test_from_service_account_info(self): - signer = crypt.RSASigner.from_service_account_info( + signer = _python_rsa.RSASigner.from_service_account_info( SERVICE_ACCOUNT_INFO) assert signer.key_id == SERVICE_ACCOUNT_INFO[ - crypt._JSON_FILE_PRIVATE_KEY_ID] + _python_rsa._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.key.PrivateKey) def test_from_service_account_info_missing_key(self): with pytest.raises(ValueError) as excinfo: - crypt.RSASigner.from_service_account_info({}) + _python_rsa.RSASigner.from_service_account_info({}) - assert excinfo.match(crypt._JSON_FILE_PRIVATE_KEY) + assert excinfo.match(_python_rsa._JSON_FILE_PRIVATE_KEY) def test_from_service_account_file(self): - signer = crypt.RSASigner.from_service_account_file( + signer = _python_rsa.RSASigner.from_service_account_file( SERVICE_ACCOUNT_JSON_FILE) assert signer.key_id == SERVICE_ACCOUNT_INFO[ - crypt._JSON_FILE_PRIVATE_KEY_ID] + _python_rsa._JSON_FILE_PRIVATE_KEY_ID] assert isinstance(signer._key, rsa.key.PrivateKey) diff --git a/tests/crypt/test_crypt.py b/tests/crypt/test_crypt.py new file mode 100644 index 000000000..d8b1d00a8 --- /dev/null +++ b/tests/crypt/test_crypt.py @@ -0,0 +1,59 @@ +# Copyright 2016 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from google.auth import crypt + + +DATA_DIR = os.path.join(os.path.dirname(__file__), '..', 'data') + +# To generate privatekey.pem, privatekey.pub, and public_cert.pem: +# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out public_cert.pem \ +# > -keyout privatekey.pem +# $ openssl rsa -in privatekey.pem -pubout -out privatekey.pub + +with open(os.path.join(DATA_DIR, 'privatekey.pem'), 'rb') as fh: + PRIVATE_KEY_BYTES = fh.read() + +with open(os.path.join(DATA_DIR, 'public_cert.pem'), 'rb') as fh: + PUBLIC_CERT_BYTES = fh.read() + +# To generate other_cert.pem: +# $ openssl req -new -newkey rsa:1024 -x509 -nodes -out other_cert.pem + +with open(os.path.join(DATA_DIR, 'other_cert.pem'), 'rb') as fh: + OTHER_CERT_BYTES = fh.read() + + +def test_verify_signature(): + to_sign = b'foo' + signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) + signature = signer.sign(to_sign) + + assert crypt.verify_signature( + to_sign, signature, PUBLIC_CERT_BYTES) + + # List of certs + assert crypt.verify_signature( + to_sign, signature, [OTHER_CERT_BYTES, PUBLIC_CERT_BYTES]) + + +def test_verify_signature_failure(): + to_sign = b'foo' + signer = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES) + signature = signer.sign(to_sign) + + assert not crypt.verify_signature( + to_sign, signature, OTHER_CERT_BYTES)