Skip to content

Commit b0c459f

Browse files
committed
Refactor certificate checking in crypt.verify_signed_jwt_with_certs.
Moved check into protected function _verify_signature.
1 parent 4c56131 commit b0c459f

File tree

2 files changed

+105
-9
lines changed

2 files changed

+105
-9
lines changed

oauth2client/crypt.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,28 @@ def make_signed_jwt(signer, payload):
9797
return b'.'.join(segments)
9898

9999

100+
def _verify_signature(message, signature, certs):
101+
"""Verifies signed content using a list of certificates.
102+
103+
Args:
104+
message: string or bytes, The message to verify.
105+
signature: string or bytes, The signature on the message.
106+
certs: dict, with the keys as certificate ID strings and the values
107+
certificates in PEM format.
108+
109+
Raises:
110+
AppIdentityError: If none of the certificates can verify the message
111+
against the signature.
112+
"""
113+
for pem in certs.values():
114+
verifier = Verifier.from_string(pem, is_x509_cert=True)
115+
if verifier.verify(message, signature):
116+
return
117+
118+
# If we have not returned, no certificate confirms the signature.
119+
raise AppIdentityError('Invalid token signature')
120+
121+
100122
def verify_signed_jwt_with_certs(jwt, certs, audience):
101123
"""Verify a JWT against public certs.
102124
@@ -119,7 +141,7 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
119141

120142
if len(segments) != 3:
121143
raise AppIdentityError('Wrong number of segments in token: %s' % jwt)
122-
signed = segments[0] + b'.' + segments[1]
144+
message_to_sign = segments[0] + b'.' + segments[1]
123145

124146
signature = _urlsafe_b64decode(segments[2])
125147

@@ -131,14 +153,7 @@ def verify_signed_jwt_with_certs(jwt, certs, audience):
131153
raise AppIdentityError('Can\'t parse token: %s' % json_body)
132154

133155
# Check signature.
134-
verified = False
135-
for pem in certs.values():
136-
verifier = Verifier.from_string(pem, True)
137-
if verifier.verify(signed, signature):
138-
verified = True
139-
break
140-
if not verified:
141-
raise AppIdentityError('Invalid token signature: %s' % jwt)
156+
_verify_signature(message_to_sign, signature, certs)
142157

143158
# Check creation timestamp.
144159
iat = parsed.get('iat')

tests/test_crypt.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,84 @@ def test_with_nonsense_key(self):
7474
self.assertRaises(crypto.Error, crypt.pkcs12_key_as_pem,
7575
credentials.private_key,
7676
credentials.private_key_password)
77+
78+
79+
class Test__verify_signature(unittest.TestCase):
80+
81+
def test_success_single_cert(self):
82+
cert_value = 'cert-value'
83+
certs = {None: cert_value}
84+
message = object()
85+
signature = object()
86+
87+
verifier = mock.MagicMock()
88+
verifier.verify = mock.MagicMock(name='verify', return_value=True)
89+
with mock.patch('oauth2client.crypt.Verifier') as Verifier:
90+
Verifier.from_string = mock.MagicMock(name='from_string',
91+
return_value=verifier)
92+
result = crypt._verify_signature(message, signature, certs)
93+
self.assertEqual(result, None)
94+
95+
# Make sure our mocks were called as expected.
96+
Verifier.from_string.assert_called_once_with(cert_value,
97+
is_x509_cert=True)
98+
verifier.verify.assert_called_once_with(message, signature)
99+
100+
def test_success_multiple_certs(self):
101+
cert_value1 = 'cert-value1'
102+
cert_value2 = 'cert-value2'
103+
cert_value3 = 'cert-value3'
104+
certs = _MockOrderedDict(cert_value1, cert_value2, cert_value3)
105+
message = object()
106+
signature = object()
107+
108+
verifier = mock.MagicMock()
109+
# Use side_effect to force all 3 cert values to be used by failing
110+
# to verify on the first two.
111+
verifier.verify = mock.MagicMock(name='verify',
112+
side_effect=[False, False, True])
113+
with mock.patch('oauth2client.crypt.Verifier') as Verifier:
114+
Verifier.from_string = mock.MagicMock(name='from_string',
115+
return_value=verifier)
116+
result = crypt._verify_signature(message, signature, certs)
117+
self.assertEqual(result, None)
118+
119+
# Make sure our mocks were called three times.
120+
expected_from_string_calls = [
121+
mock.call(cert_value1, is_x509_cert=True),
122+
mock.call(cert_value2, is_x509_cert=True),
123+
mock.call(cert_value3, is_x509_cert=True),
124+
]
125+
self.assertEqual(Verifier.from_string.mock_calls,
126+
expected_from_string_calls)
127+
expected_verify_calls = [mock.call(message, signature)] * 3
128+
self.assertEqual(verifier.verify.mock_calls,
129+
expected_verify_calls)
130+
131+
def test_failure(self):
132+
cert_value = 'cert-value'
133+
certs = {None: cert_value}
134+
message = object()
135+
signature = object()
136+
137+
verifier = mock.MagicMock()
138+
verifier.verify = mock.MagicMock(name='verify', return_value=False)
139+
with mock.patch('oauth2client.crypt.Verifier') as Verifier:
140+
Verifier.from_string = mock.MagicMock(name='from_string',
141+
return_value=verifier)
142+
self.assertRaises(crypt.AppIdentityError, crypt._verify_signature,
143+
message, signature, certs)
144+
145+
# Make sure our mocks were called as expected.
146+
Verifier.from_string.assert_called_once_with(cert_value,
147+
is_x509_cert=True)
148+
verifier.verify.assert_called_once_with(message, signature)
149+
150+
151+
class _MockOrderedDict(object):
152+
153+
def __init__(self, *values):
154+
self._values = values
155+
156+
def values(self):
157+
return self._values

0 commit comments

Comments
 (0)