Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding cryptography Signer. #185

Merged
merged 6 commits into from
Feb 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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