Skip to content
This repository was archived by the owner on Mar 6, 2026. It is now read-only.

Commit 254befe

Browse files
author
Jon Wayne Parrott
authored
Create abstract Verifier and Signer, remove key_id hack from App Engine and IAM signers (#115)
1 parent a209819 commit 254befe

10 files changed

Lines changed: 122 additions & 122 deletions

google/auth/_service_account_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def from_dict(data, require=None):
5151
'fields {}.'.format(', '.join(missing)))
5252

5353
# Create a signer.
54-
signer = crypt.Signer.from_service_account_info(data)
54+
signer = crypt.RSASigner.from_service_account_info(data)
5555

5656
return signer
5757

google/auth/app_engine.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,49 +26,33 @@
2626

2727
from google.auth import _helpers
2828
from google.auth import credentials
29+
from google.auth import crypt
2930

3031
try:
3132
from google.appengine.api import app_identity
3233
except ImportError:
3334
app_identity = None
3435

3536

36-
class Signer(object):
37+
class Signer(crypt.Signer):
3738
"""Signs messages using the App Engine App Identity service.
3839
3940
This can be used in place of :class:`google.auth.crypt.Signer` when
4041
running in the App Engine standard environment.
41-
42-
.. warning::
43-
The App Identity service signs bytes using Google-managed keys.
44-
Because of this it's possible that the key used to sign bytes will
45-
change. In some cases this change can occur between successive calls
46-
to :attr:`key_id` and :meth:`sign`. This could result in a signature
47-
that was signed with a different key than the one indicated by
48-
:attr:`key_id`. It's recommended that if you use this in your code
49-
that you account for this behavior by building in retry logic.
5042
"""
5143

5244
@property
5345
def key_id(self):
5446
"""Optional[str]: The key ID used to identify this private key.
5547
56-
.. note::
57-
This makes a request to the App Identity service.
48+
.. warning::
49+
This is always ``None``. The key ID used by App Engine can not
50+
be reliably determined ahead of time.
5851
"""
59-
key_id, _ = app_identity.sign_blob(b'')
60-
return key_id
61-
62-
@staticmethod
63-
def sign(message):
64-
"""Signs a message.
65-
66-
Args:
67-
message (Union[str, bytes]): The message to be signed.
52+
return None
6853

69-
Returns:
70-
bytes: The signature of the message.
71-
"""
54+
@_helpers.copy_docstring(crypt.Signer)
55+
def sign(self, message):
7256
message = _helpers.to_bytes(message)
7357
_, signature = app_identity.sign_blob(message)
7458
return signature

google/auth/crypt.py

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,21 @@
2424
valid = crypt.verify_signature(message, signature, cert)
2525
2626
If you're going to verify many messages with the same certificate, you can use
27-
:class:`Verifier`::
27+
:class:`RSAVerifier`::
2828
2929
cert = open('certs.pem').read()
30-
verifier = crypt.Verifier.from_string(cert)
30+
verifier = crypt.RSAVerifier.from_string(cert)
3131
valid = verifier.verify(message, signature)
3232
3333
34-
To sign messages use :class:`Signer` with a private key::
34+
To sign messages use :class:`RSASigner` with a private key::
3535
3636
private_key = open('private_key.pem').read()
37-
signer = crypt.Signer(private_key)
37+
signer = crypt.RSASigner(private_key)
3838
signature = signer.sign(message)
3939
4040
"""
41+
import abc
4142
import io
4243
import json
4344

@@ -77,23 +78,17 @@ def _bit_list_to_bytes(bit_list):
7778
byte_vals = bytearray()
7879
for start in six.moves.xrange(0, num_bits, 8):
7980
curr_bits = bit_list[start:start + 8]
80-
char_val = sum(val * digit
81-
for val, digit in six.moves.zip(_POW2, curr_bits))
81+
char_val = sum(
82+
val * digit for val, digit in six.moves.zip(_POW2, curr_bits))
8283
byte_vals.append(char_val)
8384
return bytes(byte_vals)
8485

8586

87+
@six.add_metaclass(abc.ABCMeta)
8688
class Verifier(object):
87-
"""This object is used to verify cryptographic signatures.
88-
89-
Args:
90-
public_key (rsa.key.PublicKey): The public key used to verify
91-
signatures.
92-
"""
93-
94-
def __init__(self, public_key):
95-
self._pubkey = public_key
89+
"""Abstract base class for crytographic signature verifiers."""
9690

91+
@abc.abstractmethod
9792
def verify(self, message, signature):
9893
"""Verifies a message against a cryptographic signature.
9994
@@ -105,6 +100,24 @@ def verify(self, message, signature):
105100
bool: True if message was signed by the private key associated
106101
with the public key that this object was constructed with.
107102
"""
103+
# pylint: disable=missing-raises-doc,redundant-returns-doc
104+
# (pylint doesn't recognize that this is abstract)
105+
raise NotImplementedError('Verify must be implemented')
106+
107+
108+
class RSAVerifier(Verifier):
109+
"""Verifies RSA cryptographic signatures using public keys.
110+
111+
Args:
112+
public_key (rsa.key.PublicKey): The public key used to verify
113+
signatures.
114+
"""
115+
116+
def __init__(self, public_key):
117+
self._pubkey = public_key
118+
119+
@_helpers.copy_docstring(Verifier)
120+
def verify(self, message, signature):
108121
message = _helpers.to_bytes(message)
109122
try:
110123
return rsa.pkcs1.verify(message, signature, self._pubkey)
@@ -145,7 +158,7 @@ def from_string(cls, public_key):
145158

146159

147160
def verify_signature(message, signature, certs):
148-
"""Verify a cryptographic signature.
161+
"""Verify an RSA cryptographic signature.
149162
150163
Checks that the provided ``signature`` was generated from ``bytes`` using
151164
the private key associated with the ``cert``.
@@ -163,27 +176,22 @@ def verify_signature(message, signature, certs):
163176
certs = [certs]
164177

165178
for cert in certs:
166-
verifier = Verifier.from_string(cert)
179+
verifier = RSAVerifier.from_string(cert)
167180
if verifier.verify(message, signature):
168181
return True
169182
return False
170183

171184

185+
@six.add_metaclass(abc.ABCMeta)
172186
class Signer(object):
173-
"""Signs messages with a private key.
174-
175-
Args:
176-
private_key (rsa.key.PrivateKey): The private key to sign with.
177-
key_id (str): Optional key ID used to identify this private key. This
178-
can be useful to associate the private key with its associated
179-
public key or certificate.
180-
"""
187+
"""Abstract base class for cryptographic signers."""
181188

182-
def __init__(self, private_key, key_id=None):
183-
self._key = private_key
184-
self.key_id = key_id
189+
@abc.abstractproperty
190+
def key_id(self):
185191
"""Optional[str]: The key ID used to identify this private key."""
192+
raise NotImplementedError('Key id must be implemented')
186193

194+
@abc.abstractmethod
187195
def sign(self, message):
188196
"""Signs a message.
189197
@@ -193,6 +201,32 @@ def sign(self, message):
193201
Returns:
194202
bytes: The signature of the message.
195203
"""
204+
# pylint: disable=missing-raises-doc,redundant-returns-doc
205+
# (pylint doesn't recognize that this is abstract)
206+
raise NotImplementedError('Sign must be implemented')
207+
208+
209+
class RSASigner(Signer):
210+
"""Signs messages with an RSA private key.
211+
212+
Args:
213+
private_key (rsa.key.PrivateKey): The private key to sign with.
214+
key_id (str): Optional key ID used to identify this private key. This
215+
can be useful to associate the private key with its associated
216+
public key or certificate.
217+
"""
218+
219+
def __init__(self, private_key, key_id=None):
220+
self._key = private_key
221+
self._key_id = key_id
222+
223+
@property
224+
@_helpers.copy_docstring(Signer)
225+
def key_id(self):
226+
return self._key_id
227+
228+
@_helpers.copy_docstring(Signer)
229+
def sign(self, message):
196230
message = _helpers.to_bytes(message)
197231
return rsa.pkcs1.sign(message, self._key, 'SHA-256')
198232

google/auth/iam.py

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,28 +25,20 @@
2525
from six.moves import http_client
2626

2727
from google.auth import _helpers
28+
from google.auth import crypt
2829
from google.auth import exceptions
2930

3031
_IAM_API_ROOT_URI = 'https://iam.googleapis.com/v1'
3132
_SIGN_BLOB_URI = (
3233
_IAM_API_ROOT_URI + '/projects/-/serviceAccounts/{}:signBlob?alt=json')
3334

3435

35-
class Signer(object):
36+
class Signer(crypt.Signer):
3637
"""Signs messages using the IAM `signBlob API`_.
3738
3839
This is useful when you need to sign bytes but do not have access to the
3940
credential's private key file.
4041
41-
.. warning::
42-
The IAM API signs bytes using Google-managed keys. Because of this
43-
it's possible that the key used to sign bytes will change. In some
44-
cases this change can occur between successive calls to :attr:`key_id`
45-
and :meth:`sign`. This could result in a signature that was signed
46-
with a different key than the one indicated by :attr:`key_id`. It's
47-
recommended that if you use this in your code that you account for
48-
this behavior by building in retry logic.
49-
5042
.. _signBlob API:
5143
https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts
5244
/signBlob
@@ -98,20 +90,13 @@ def _make_signing_request(self, message):
9890
def key_id(self):
9991
"""Optional[str]: The key ID used to identify this private key.
10092
101-
.. note::
102-
This makes an API request to the IAM API.
93+
.. warning::
94+
This is always ``None``. The key ID used by IAM can not
95+
be reliably determined ahead of time.
10396
"""
104-
response = self._make_signing_request('')
105-
return response['keyId']
97+
return None
10698

99+
@_helpers.copy_docstring(crypt.Signer)
107100
def sign(self, message):
108-
"""Signs a message.
109-
110-
Args:
111-
message (Union[str, bytes]): The message to be signed.
112-
113-
Returns:
114-
bytes: The signature of the message.
115-
"""
116101
response = self._make_signing_request(message)
117102
return base64.b64decode(response['signature'])

tests/oauth2/test_service_account.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444

4545
@pytest.fixture(scope='module')
4646
def signer():
47-
return crypt.Signer.from_string(PRIVATE_KEY_BYTES, '1')
47+
return crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, '1')
4848

4949

5050
class TestCredentials(object):

tests/test__service_account_info.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
def test_from_dict():
3333
signer = _service_account_info.from_dict(SERVICE_ACCOUNT_INFO)
34-
assert isinstance(signer, crypt.Signer)
34+
assert isinstance(signer, crypt.RSASigner)
3535
assert signer.key_id == SERVICE_ACCOUNT_INFO['private_key_id']
3636

3737

@@ -59,5 +59,5 @@ def test_from_filename():
5959
for key, value in six.iteritems(SERVICE_ACCOUNT_INFO):
6060
assert info[key] == value
6161

62-
assert isinstance(signer, crypt.Signer)
62+
assert isinstance(signer, crypt.RSASigner)
6363
assert signer.key_id == SERVICE_ACCOUNT_INFO['private_key_id']

tests/test_app_engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_key_id(self, app_identity_mock):
4848

4949
signer = app_engine.Signer()
5050

51-
assert signer.key_id == mock.sentinel.key_id
51+
assert signer.key_id is None
5252

5353
def test_sign(self, app_identity_mock):
5454
app_identity_mock.sign_blob.return_value = (

0 commit comments

Comments
 (0)