Skip to content

Commit 1cd8390

Browse files
dhermesJon Wayne Parrott
authored andcommitted
Add cryptography-based RSA signer and verifier. (#185)
Fixes #183.
1 parent b649b43 commit 1cd8390

File tree

10 files changed

+386
-53
lines changed

10 files changed

+386
-53
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ docs/_build
1212
.nox/
1313
.tox/
1414
.cache/
15+
.pytest_cache/
1516

1617
# Django test database
1718
db.sqlite3
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright 2017 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""RSA verifier and signer that use the ``cryptography`` library.
16+
17+
This is a much faster implementation than the default (in
18+
``google.auth.crypt._python_rsa``), which depends on the pure-Python
19+
``rsa`` library.
20+
"""
21+
22+
import cryptography.exceptions
23+
from cryptography.hazmat import backends
24+
from cryptography.hazmat.primitives import hashes
25+
from cryptography.hazmat.primitives import serialization
26+
from cryptography.hazmat.primitives.asymmetric import padding
27+
import cryptography.x509
28+
29+
from google.auth import _helpers
30+
from google.auth.crypt import base
31+
32+
33+
_CERTIFICATE_MARKER = b'-----BEGIN CERTIFICATE-----'
34+
_BACKEND = backends.default_backend()
35+
_PADDING = padding.PKCS1v15()
36+
_SHA256 = hashes.SHA256()
37+
38+
39+
class RSAVerifier(base.Verifier):
40+
"""Verifies RSA cryptographic signatures using public keys.
41+
42+
Args:
43+
public_key (
44+
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
45+
The public key used to verify signatures.
46+
"""
47+
48+
def __init__(self, public_key):
49+
self._pubkey = public_key
50+
51+
@_helpers.copy_docstring(base.Verifier)
52+
def verify(self, message, signature):
53+
message = _helpers.to_bytes(message)
54+
try:
55+
self._pubkey.verify(signature, message, _PADDING, _SHA256)
56+
return True
57+
except (ValueError, cryptography.exceptions.InvalidSignature):
58+
return False
59+
60+
@classmethod
61+
def from_string(cls, public_key):
62+
"""Construct an Verifier instance from a public key or public
63+
certificate string.
64+
65+
Args:
66+
public_key (Union[str, bytes]): The public key in PEM format or the
67+
x509 public key certificate.
68+
69+
Returns:
70+
Verifier: The constructed verifier.
71+
72+
Raises:
73+
ValueError: If the public key can't be parsed.
74+
"""
75+
public_key_data = _helpers.to_bytes(public_key)
76+
77+
if _CERTIFICATE_MARKER in public_key_data:
78+
cert = cryptography.x509.load_pem_x509_certificate(
79+
public_key_data, _BACKEND)
80+
pubkey = cert.public_key()
81+
82+
else:
83+
pubkey = serialization.load_pem_public_key(
84+
public_key_data, _BACKEND)
85+
86+
return cls(pubkey)
87+
88+
89+
class RSASigner(base.Signer, base.FromServiceAccountMixin):
90+
"""Signs messages with an RSA private key.
91+
92+
Args:
93+
private_key (
94+
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
95+
The private key to sign with.
96+
key_id (str): Optional key ID used to identify this private key. This
97+
can be useful to associate the private key with its associated
98+
public key or certificate.
99+
"""
100+
101+
def __init__(self, private_key, key_id=None):
102+
self._key = private_key
103+
self._key_id = key_id
104+
105+
@property
106+
@_helpers.copy_docstring(base.Signer)
107+
def key_id(self):
108+
return self._key_id
109+
110+
@_helpers.copy_docstring(base.Signer)
111+
def sign(self, message):
112+
message = _helpers.to_bytes(message)
113+
return self._key.sign(
114+
message, _PADDING, _SHA256)
115+
116+
@classmethod
117+
def from_string(cls, key, key_id=None):
118+
"""Construct a RSASigner from a private key in PEM format.
119+
120+
Args:
121+
key (Union[bytes, str]): Private key in PEM format.
122+
key_id (str): An optional key id used to identify the private key.
123+
124+
Returns:
125+
google.auth.crypt._cryptography_rsa.RSASigner: The
126+
constructed signer.
127+
128+
Raises:
129+
ValueError: If ``key`` is not ``bytes`` or ``str`` (unicode).
130+
UnicodeDecodeError: If ``key`` is ``bytes`` but cannot be decoded
131+
into a UTF-8 ``str``.
132+
ValueError: If ``cryptography`` "Could not deserialize key data."
133+
"""
134+
key = _helpers.to_bytes(key)
135+
private_key = serialization.load_pem_private_key(
136+
key, password=None, backend=_BACKEND)
137+
return cls(private_key, key_id=key_id)

google/auth/crypt/_helpers.py

Whitespace-only changes.

google/auth/crypt/_python_rsa.py

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@
2121

2222
from __future__ import absolute_import
2323

24-
import io
25-
import json
26-
2724
from pyasn1.codec.der import decoder
2825
from pyasn1_modules import pem
2926
from pyasn1_modules.rfc2459 import Certificate
@@ -41,8 +38,6 @@
4138
_PKCS8_MARKER = ('-----BEGIN PRIVATE KEY-----',
4239
'-----END PRIVATE KEY-----')
4340
_PKCS8_SPEC = PrivateKeyInfo()
44-
_JSON_FILE_PRIVATE_KEY = 'private_key'
45-
_JSON_FILE_PRIVATE_KEY_ID = 'private_key_id'
4641

4742

4843
def _bit_list_to_bytes(bit_list):
@@ -119,7 +114,7 @@ def from_string(cls, public_key):
119114
return cls(pubkey)
120115

121116

122-
class RSASigner(base.Signer):
117+
class RSASigner(base.Signer, base.FromServiceAccountMixin):
123118
"""Signs messages with an RSA private key.
124119
125120
Args:
@@ -179,43 +174,3 @@ def from_string(cls, key, key_id=None):
179174
raise ValueError('No key could be detected.')
180175

181176
return cls(private_key, key_id=key_id)
182-
183-
@classmethod
184-
def from_service_account_info(cls, info):
185-
"""Creates a Signer instance instance from a dictionary containing
186-
service account info in Google format.
187-
188-
Args:
189-
info (Mapping[str, str]): The service account info in Google
190-
format.
191-
192-
Returns:
193-
google.auth.crypt.Signer: The constructed signer.
194-
195-
Raises:
196-
ValueError: If the info is not in the expected format.
197-
"""
198-
if _JSON_FILE_PRIVATE_KEY not in info:
199-
raise ValueError(
200-
'The private_key field was not found in the service account '
201-
'info.')
202-
203-
return cls.from_string(
204-
info[_JSON_FILE_PRIVATE_KEY],
205-
info.get(_JSON_FILE_PRIVATE_KEY_ID))
206-
207-
@classmethod
208-
def from_service_account_file(cls, filename):
209-
"""Creates a Signer instance from a service account .json file
210-
in Google format.
211-
212-
Args:
213-
filename (str): The path to the service account .json file.
214-
215-
Returns:
216-
google.auth.crypt.Signer: The constructed signer.
217-
"""
218-
with io.open(filename, 'r', encoding='utf-8') as json_file:
219-
data = json.load(json_file)
220-
221-
return cls.from_service_account_info(data)

google/auth/crypt/base.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,16 @@
1515
"""Base classes for cryptographic signers and verifiers."""
1616

1717
import abc
18+
import io
19+
import json
1820

1921
import six
2022

2123

24+
_JSON_FILE_PRIVATE_KEY = 'private_key'
25+
_JSON_FILE_PRIVATE_KEY_ID = 'private_key_id'
26+
27+
2228
@six.add_metaclass(abc.ABCMeta)
2329
class Verifier(object):
2430
"""Abstract base class for crytographic signature verifiers."""
@@ -62,3 +68,64 @@ def sign(self, message):
6268
# pylint: disable=missing-raises-doc,redundant-returns-doc
6369
# (pylint doesn't recognize that this is abstract)
6470
raise NotImplementedError('Sign must be implemented')
71+
72+
73+
@six.add_metaclass(abc.ABCMeta)
74+
class FromServiceAccountMixin(object):
75+
"""Mix-in to enable factory constructors for a Signer."""
76+
77+
@abc.abstractmethod
78+
def from_string(cls, key, key_id=None):
79+
"""Construct an Signer instance from a private key string.
80+
81+
Args:
82+
key (str): Private key as a string.
83+
key_id (str): An optional key id used to identify the private key.
84+
85+
Returns:
86+
google.auth.crypt.Signer: The constructed signer.
87+
88+
Raises:
89+
ValueError: If the key cannot be parsed.
90+
"""
91+
raise NotImplementedError('from_string must be implemented')
92+
93+
@classmethod
94+
def from_service_account_info(cls, info):
95+
"""Creates a Signer instance instance from a dictionary containing
96+
service account info in Google format.
97+
98+
Args:
99+
info (Mapping[str, str]): The service account info in Google
100+
format.
101+
102+
Returns:
103+
google.auth.crypt.Signer: The constructed signer.
104+
105+
Raises:
106+
ValueError: If the info is not in the expected format.
107+
"""
108+
if _JSON_FILE_PRIVATE_KEY not in info:
109+
raise ValueError(
110+
'The private_key field was not found in the service account '
111+
'info.')
112+
113+
return cls.from_string(
114+
info[_JSON_FILE_PRIVATE_KEY],
115+
info.get(_JSON_FILE_PRIVATE_KEY_ID))
116+
117+
@classmethod
118+
def from_service_account_file(cls, filename):
119+
"""Creates a Signer instance from a service account .json file
120+
in Google format.
121+
122+
Args:
123+
filename (str): The path to the service account .json file.
124+
125+
Returns:
126+
google.auth.crypt.Signer: The constructed signer.
127+
"""
128+
with io.open(filename, 'r', encoding='utf-8') as json_file:
129+
data = json.load(json_file)
130+
131+
return cls.from_service_account_info(data)

google/auth/crypt/rsa.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,17 @@
1414

1515
"""RSA cryptography signer and verifier."""
1616

17-
from google.auth.crypt import _python_rsa
1817

19-
RSASigner = _python_rsa.RSASigner
20-
RSAVerifier = _python_rsa.RSAVerifier
18+
try:
19+
# Prefer cryptograph-based RSA implementation.
20+
from google.auth.crypt import _cryptography_rsa
21+
22+
RSASigner = _cryptography_rsa.RSASigner
23+
RSAVerifier = _cryptography_rsa.RSAVerifier
24+
except ImportError: # pragma: NO COVER
25+
# Fallback to pure-python RSA implementation if cryptography is
26+
# unavailable.
27+
from google.auth.crypt import _python_rsa
28+
29+
RSASigner = _python_rsa.RSASigner
30+
RSAVerifier = _python_rsa.RSAVerifier

0 commit comments

Comments
 (0)