Skip to content

Commit

Permalink
Add cryptography-based RSA signer and verifier. (#185)
Browse files Browse the repository at this point in the history
Fixes #183.
  • Loading branch information
dhermes authored and Jon Wayne Parrott committed Feb 8, 2018
1 parent b649b43 commit 1cd8390
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 53 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ docs/_build
.nox/
.tox/
.cache/
.pytest_cache/

# Django test database
db.sqlite3
Expand Down
137 changes: 137 additions & 0 deletions google/auth/crypt/_cryptography_rsa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# 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 verifier and signer that use the ``cryptography`` library.
This is a much faster implementation than the default (in
``google.auth.crypt._python_rsa``), which depends on the pure-Python
``rsa`` library.
"""

import cryptography.exceptions
from cryptography.hazmat import backends
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
import cryptography.x509

from google.auth import _helpers
from google.auth.crypt import base


_CERTIFICATE_MARKER = b'-----BEGIN CERTIFICATE-----'
_BACKEND = backends.default_backend()
_PADDING = padding.PKCS1v15()
_SHA256 = hashes.SHA256()


class RSAVerifier(base.Verifier):
"""Verifies RSA cryptographic signatures using public keys.
Args:
public_key (
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
The public key used to verify signatures.
"""

def __init__(self, public_key):
self._pubkey = public_key

@_helpers.copy_docstring(base.Verifier)
def verify(self, message, signature):
message = _helpers.to_bytes(message)
try:
self._pubkey.verify(signature, message, _PADDING, _SHA256)
return True
except (ValueError, cryptography.exceptions.InvalidSignature):
return False

@classmethod
def from_string(cls, public_key):
"""Construct an Verifier instance from a public key or public
certificate string.
Args:
public_key (Union[str, bytes]): The public key in PEM format or the
x509 public key certificate.
Returns:
Verifier: The constructed verifier.
Raises:
ValueError: If the public key can't be parsed.
"""
public_key_data = _helpers.to_bytes(public_key)

if _CERTIFICATE_MARKER in public_key_data:
cert = cryptography.x509.load_pem_x509_certificate(
public_key_data, _BACKEND)
pubkey = cert.public_key()

else:
pubkey = serialization.load_pem_public_key(
public_key_data, _BACKEND)

return cls(pubkey)


class RSASigner(base.Signer, base.FromServiceAccountMixin):
"""Signs messages with an RSA private key.
Args:
private_key (
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
The private key to sign with.
key_id (str): Optional key ID used to identify this private key. This
can be useful to associate the private key with its associated
public key or certificate.
"""

def __init__(self, private_key, key_id=None):
self._key = private_key
self._key_id = key_id

@property
@_helpers.copy_docstring(base.Signer)
def key_id(self):
return self._key_id

@_helpers.copy_docstring(base.Signer)
def sign(self, message):
message = _helpers.to_bytes(message)
return self._key.sign(
message, _PADDING, _SHA256)

@classmethod
def from_string(cls, key, key_id=None):
"""Construct a RSASigner from a private key in PEM format.
Args:
key (Union[bytes, str]): Private key in PEM format.
key_id (str): An optional key id used to identify the private key.
Returns:
google.auth.crypt._cryptography_rsa.RSASigner: The
constructed signer.
Raises:
ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
into a UTF-8 ``str``.
ValueError: If ``cryptography`` "Could not deserialize key data."
"""
key = _helpers.to_bytes(key)
private_key = serialization.load_pem_private_key(
key, password=None, backend=_BACKEND)
return cls(private_key, key_id=key_id)
Empty file added google/auth/crypt/_helpers.py
Empty file.
47 changes: 1 addition & 46 deletions google/auth/crypt/_python_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@

from __future__ import absolute_import

import io
import json

from pyasn1.codec.der import decoder
from pyasn1_modules import pem
from pyasn1_modules.rfc2459 import Certificate
Expand All @@ -41,8 +38,6 @@
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
'-----END PRIVATE KEY-----')
_PKCS8_SPEC = PrivateKeyInfo()
_JSON_FILE_PRIVATE_KEY = 'private_key'
_JSON_FILE_PRIVATE_KEY_ID = 'private_key_id'


def _bit_list_to_bytes(bit_list):
Expand Down Expand Up @@ -119,7 +114,7 @@ def from_string(cls, public_key):
return cls(pubkey)


class RSASigner(base.Signer):
class RSASigner(base.Signer, base.FromServiceAccountMixin):
"""Signs messages with an RSA private key.
Args:
Expand Down Expand Up @@ -179,43 +174,3 @@ def from_string(cls, key, key_id=None):
raise ValueError('No key could be detected.')

return cls(private_key, key_id=key_id)

@classmethod
def from_service_account_info(cls, info):
"""Creates a Signer instance instance from a dictionary containing
service account info in Google format.
Args:
info (Mapping[str, str]): The service account info in Google
format.
Returns:
google.auth.crypt.Signer: The constructed signer.
Raises:
ValueError: If the info is not in the expected format.
"""
if _JSON_FILE_PRIVATE_KEY not in info:
raise ValueError(
'The private_key field was not found in the service account '
'info.')

return cls.from_string(
info[_JSON_FILE_PRIVATE_KEY],
info.get(_JSON_FILE_PRIVATE_KEY_ID))

@classmethod
def from_service_account_file(cls, filename):
"""Creates a Signer instance from a service account .json file
in Google format.
Args:
filename (str): The path to the service account .json file.
Returns:
google.auth.crypt.Signer: The constructed signer.
"""
with io.open(filename, 'r', encoding='utf-8') as json_file:
data = json.load(json_file)

return cls.from_service_account_info(data)
67 changes: 67 additions & 0 deletions google/auth/crypt/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
"""Base classes for cryptographic signers and verifiers."""

import abc
import io
import json

import six


_JSON_FILE_PRIVATE_KEY = 'private_key'
_JSON_FILE_PRIVATE_KEY_ID = 'private_key_id'


@six.add_metaclass(abc.ABCMeta)
class Verifier(object):
"""Abstract base class for crytographic signature verifiers."""
Expand Down Expand Up @@ -62,3 +68,64 @@ def sign(self, message):
# pylint: disable=missing-raises-doc,redundant-returns-doc
# (pylint doesn't recognize that this is abstract)
raise NotImplementedError('Sign must be implemented')


@six.add_metaclass(abc.ABCMeta)
class FromServiceAccountMixin(object):
"""Mix-in to enable factory constructors for a Signer."""

@abc.abstractmethod
def from_string(cls, key, key_id=None):
"""Construct an Signer instance from a private key string.
Args:
key (str): Private key as a string.
key_id (str): An optional key id used to identify the private key.
Returns:
google.auth.crypt.Signer: The constructed signer.
Raises:
ValueError: If the key cannot be parsed.
"""
raise NotImplementedError('from_string must be implemented')

@classmethod
def from_service_account_info(cls, info):
"""Creates a Signer instance instance from a dictionary containing
service account info in Google format.
Args:
info (Mapping[str, str]): The service account info in Google
format.
Returns:
google.auth.crypt.Signer: The constructed signer.
Raises:
ValueError: If the info is not in the expected format.
"""
if _JSON_FILE_PRIVATE_KEY not in info:
raise ValueError(
'The private_key field was not found in the service account '
'info.')

return cls.from_string(
info[_JSON_FILE_PRIVATE_KEY],
info.get(_JSON_FILE_PRIVATE_KEY_ID))

@classmethod
def from_service_account_file(cls, filename):
"""Creates a Signer instance from a service account .json file
in Google format.
Args:
filename (str): The path to the service account .json file.
Returns:
google.auth.crypt.Signer: The constructed signer.
"""
with io.open(filename, 'r', encoding='utf-8') as json_file:
data = json.load(json_file)

return cls.from_service_account_info(data)
16 changes: 13 additions & 3 deletions google/auth/crypt/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,17 @@

"""RSA cryptography signer and verifier."""

from google.auth.crypt import _python_rsa

RSASigner = _python_rsa.RSASigner
RSAVerifier = _python_rsa.RSAVerifier
try:
# Prefer cryptograph-based RSA implementation.
from google.auth.crypt import _cryptography_rsa

RSASigner = _cryptography_rsa.RSASigner
RSAVerifier = _cryptography_rsa.RSAVerifier
except ImportError: # pragma: NO COVER
# Fallback to pure-python RSA implementation if cryptography is
# unavailable.
from google.auth.crypt import _python_rsa

RSASigner = _python_rsa.RSASigner
RSAVerifier = _python_rsa.RSAVerifier
Loading

0 comments on commit 1cd8390

Please sign in to comment.