From c83f8920bf59563631673c51acd94ce1134a9852 Mon Sep 17 00:00:00 2001 From: Dolph Mathews Date: Thu, 5 Mar 2015 21:12:08 +0000 Subject: [PATCH] Remove redundant creation timestamp from fernet tokens This removes the creation timestamp from the token's payload in favor of extracting the token's creation timestamp from the Fernet token format itself. Change-Id: I170a07adc1fe6418dfaf2c78e1b439339f1c14ed Closes-Bug: 1428717 --- .../tests/unit/token/test_fernet_provider.py | 45 +------------- keystone/token/providers/fernet/core.py | 46 +++++++++++--- .../providers/fernet/token_formatters.py | 61 +++++++------------ 3 files changed, 62 insertions(+), 90 deletions(-) diff --git a/keystone/tests/unit/token/test_fernet_provider.py b/keystone/tests/unit/token/test_fernet_provider.py index 49a4446f4c..09c285d471 100644 --- a/keystone/tests/unit/token/test_fernet_provider.py +++ b/keystone/tests/unit/token/test_fernet_provider.py @@ -114,20 +114,17 @@ class TestScopedTokenFormatter(tests.TestCase, KeyRepositoryTestMixin): exp_project_id = uuid.uuid4().hex # All we are validating here is that the token is encrypted and # decrypted properly, not the actual validity of token data. - exp_issued_at = timeutils.isotime(timeutils.utcnow()) exp_expires_at = timeutils.isotime(timeutils.utcnow()) exp_audit_ids = base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2] token = self.formatter.create_token( - exp_user_id, exp_project_id, exp_issued_at, exp_expires_at, - exp_audit_ids) + exp_user_id, exp_project_id, exp_expires_at, exp_audit_ids) - (user_id, project_id, issued_at, expires_at, audit_ids) = ( + (user_id, project_id, expires_at, audit_ids) = ( self.formatter.validate_token(token[len('F00'):])) self.assertEqual(exp_user_id, user_id) self.assertEqual(exp_project_id, project_id) - self.assertEqual(exp_issued_at, issued_at) self.assertEqual(exp_expires_at, expires_at) self.assertEqual(exp_audit_ids, audit_ids) @@ -140,28 +137,9 @@ class TestScopedTokenFormatter(tests.TestCase, KeyRepositoryTestMixin): user_id, project_id, timeutils.isotime(timeutils.utcnow()), - timeutils.isotime(timeutils.utcnow()), base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2]) self.assertLess(len(encrypted_token), 255) - def test_tampered_encrypted_token_throws_exception(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - # All we are validating here is that the token is encrypted and - # decrypted properly, not the actual validity of token data. - et = self.formatter.create_token( - user_id, - project_id, - timeutils.isotime(timeutils.utcnow()), - timeutils.isotime(timeutils.utcnow()), - base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2]) - some_id = uuid.uuid4().hex - tampered_token = et[:50] + some_id + et[50 + len(some_id):] - - self.assertRaises(exception.Unauthorized, - self.formatter.validate_token, - tampered_token[4:]) - class TestCustomTokenFormatter(TestScopedTokenFormatter): def setUp(self): @@ -210,25 +188,6 @@ class TestTrustTokenFormatter(tests.TestCase, KeyRepositoryTestMixin): user_id, project_id, timeutils.isotime(timeutils.utcnow()), - timeutils.isotime(timeutils.utcnow()), base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2], uuid.uuid4().hex) self.assertLess(len(encrypted_token), 255) - - def test_tampered_encrypted_trust_token_throws_exception(self): - user_id = uuid.uuid4().hex - project_id = uuid.uuid4().hex - - # Grab an encrypted token - et = self.formatter.create_token( - user_id, project_id, - timeutils.isotime(timeutils.utcnow()), - timeutils.isotime(timeutils.utcnow()), - base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2], - uuid.uuid4().hex) - some_id = uuid.uuid4().hex - tampered_token = et[:50] + some_id + et[50 + len(some_id):] - - self.assertRaises(exception.Unauthorized, - self.formatter.validate_token, - tampered_token[6:]) diff --git a/keystone/token/providers/fernet/core.py b/keystone/token/providers/fernet/core.py index c173199bfd..de742b26ac 100644 --- a/keystone/token/providers/fernet/core.py +++ b/keystone/token/providers/fernet/core.py @@ -10,8 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +import base64 +import datetime +import struct + from oslo_config import cfg from oslo_log import log +from oslo_utils import timeutils from keystone.common import dependency from keystone import exception @@ -25,6 +30,12 @@ CONF = cfg.CONF LOG = log.getLogger(__name__) +# Fernet byte indexes as as computed by pypi/keyless_fernet and defined in +# https://github.com/fernet/spec +TIMESTAMP_START = 1 +TIMESTAMP_END = 9 + + @dependency.requires('trust_api') class Provider(common.BaseProvider): def __init__(self, *args, **kwargs): @@ -100,7 +111,6 @@ class Provider(common.BaseProvider): token_id = token_format.create_token( user_id, project_id, - token_data['token']['issued_at'], token_data['token']['expires_at'], token_data['token']['audit_ids'], token_data['token']['OS-TRUST:trust']['id']) @@ -108,7 +118,6 @@ class Provider(common.BaseProvider): token_format = self.token_format_map[fm.UNSCOPED_TOKEN_PREFIX] token_id = token_format.create_token( user_id, - token_data['token']['issued_at'], token_data['token']['expires_at'], token_data['token']['audit_ids']) else: @@ -116,7 +125,6 @@ class Provider(common.BaseProvider): token_id = token_format.create_token( user_id, project_id, - token_data['token']['issued_at'], token_data['token']['expires_at'], token_data['token']['audit_ids']) @@ -132,6 +140,24 @@ class Provider(common.BaseProvider): """ raise exception.NotImplemented() + @classmethod + def _creation_time(cls, fernet_token): + """Returns the creation time of a valid Fernet token.""" + # fernet tokens are base64 encoded, so we need to unpack them first + token_bytes = base64.urlsafe_b64decode(fernet_token) + + # slice into the byte array to get just the timestamp + timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END] + + # convert those bytes to an integer + # (it's a 64-bit "unsigned long long int" in C) + timestamp_int = struct.unpack(">Q", timestamp_bytes)[0] + + # and with an integer, it's trivial to produce a datetime object + created_at = datetime.datetime.utcfromtimestamp(timestamp_int) + + return created_at + def validate_v3_token(self, token_ref): """Validate a V3 formatted token. @@ -159,23 +185,27 @@ class Provider(common.BaseProvider): trust_ref = None if token_format == fm.UNSCOPED_TOKEN_PREFIX: - (user_id, issued_at, expires_at, audit_ids) = ( + (user_id, expires_at, audit_ids) = ( token_formatter.validate_token(token_str)) elif token_format == fm.SCOPED_TOKEN_PREFIX: - (user_id, project_id, issued_at, expires_at, audit_ids) = ( + (user_id, project_id, expires_at, audit_ids) = ( token_formatter.validate_token(token_str)) elif token_format == fm.TRUST_TOKEN_PREFIX: - (user_id, project_id, issued_at, expires_at, audit_ids, - trust_id) = token_formatter.validate_token(token_str) + (user_id, project_id, expires_at, audit_ids, trust_id) = ( + token_formatter.validate_token(token_str)) trust_ref = self.trust_api.get_trust(trust_id) + # rather than appearing in the payload, the creation time is encoded + # into the token format itself + created_at = Provider._creation_time(token_str) + return self.v3_token_data_helper.get_token_data( user_id, method_names=['password', 'token'], project_id=project_id, expires=expires_at, - issued_at=issued_at, + issued_at=timeutils.isotime(created_at), trust=trust_ref, audit_info=audit_ids) diff --git a/keystone/token/providers/fernet/token_formatters.py b/keystone/token/providers/fernet/token_formatters.py index 1833053cee..5c811dc879 100644 --- a/keystone/token/providers/fernet/token_formatters.py +++ b/keystone/token/providers/fernet/token_formatters.py @@ -99,8 +99,8 @@ class BaseTokenFormatter(object): def _convert_int_to_time_string(self, time_int): """Convert a timestamp integer to a string. - :param time_int: integer representing time - :returns: a time formatted string + :param time_int: integer representing timestamp + :returns: a time formatted strings """ time_object = datetime.datetime.utcfromtimestamp(int(time_int)) @@ -131,20 +131,18 @@ class UnscopedTokenFormatter(BaseTokenFormatter): token_format = fm.UNSCOPED_TOKEN_PREFIX - def create_token(self, user_id, created_at, expires_at, audit_ids): + def create_token(self, user_id, expires_at, audit_ids): """Create a unscoped token. :param user_id: identifier of the user in the token request - :param created_at: datetime of the token's creation - :param expires_at_int: datetime of the token's expiration + :param expires_at: datetime of the token's expiration :param audit_ids: list of the token's audit IDs :returns: a string representing the token """ b_user_id = self._convert_uuid_hex_to_bytes(user_id) - issued_at_int = self._convert_time_string_to_int(created_at) expires_at_int = self._convert_time_string_to_int(expires_at) - payload = (b_user_id, issued_at_int, expires_at_int, audit_ids) + payload = (b_user_id, expires_at_int, audit_ids) return self.pack(payload) @@ -160,44 +158,37 @@ class UnscopedTokenFormatter(BaseTokenFormatter): # Rebuild and retrieve token information from the token string b_user_id = payload[0] - issued_at_ts = payload[1] - expires_at_ts = payload[2] - audit_ids = payload[3] + expires_at_ts = payload[1] + audit_ids = payload[2] user_id = self._convert_uuid_bytes_to_hex(b_user_id) - issued_at_str = self._convert_int_to_time_string(issued_at_ts) expires_at_str = self._convert_int_to_time_string(expires_at_ts) - return (user_id, issued_at_str, expires_at_str, audit_ids) + return (user_id, expires_at_str, audit_ids) class ScopedTokenFormatter(BaseTokenFormatter): token_format = fm.SCOPED_TOKEN_PREFIX - def create_token(self, user_id, project_id, created_at, expires_at, - audit_ids): + def create_token(self, user_id, project_id, expires_at, audit_ids): """Create a standard formatted token. :param user_id: ID of the user in the token request :param project_id: ID of the project to scope to - :param created_at: datetime of the token's creation :param expires_at: datetime of the token's expiration :param audit_ids: list of the token's audit IDs :returns: a string representing the token """ - issued_at_int = self._convert_time_string_to_int(created_at) expires_at_int = self._convert_time_string_to_int(expires_at) b_user_id = self._convert_uuid_hex_to_bytes(user_id) if project_id: b_scope_id = self._convert_uuid_hex_to_bytes(project_id) - payload = ( - b_user_id, b_scope_id, issued_at_int, expires_at_int, - audit_ids) + payload = (b_user_id, b_scope_id, expires_at_int, audit_ids) else: - payload = (b_user_id, issued_at_int, expires_at_int, audit_ids) + payload = (b_user_id, expires_at_int, audit_ids) return self.pack(payload) @@ -216,13 +207,11 @@ class ScopedTokenFormatter(BaseTokenFormatter): b_project_id = None if isinstance(payload[1], str): b_project_id = payload[1] - issued_at_ts = payload[2] - expires_at_ts = payload[3] - audit_ids = payload[4] - else: - issued_at_ts = payload[1] expires_at_ts = payload[2] audit_ids = payload[3] + else: + expires_at_ts = payload[1] + audit_ids = payload[2] # Uncompress the IDs user_id = self._convert_uuid_bytes_to_hex(b_user_id) @@ -231,36 +220,33 @@ class ScopedTokenFormatter(BaseTokenFormatter): project_id = self._convert_uuid_bytes_to_hex(b_project_id) # Generate created at and expires at times - issued_at_str = self._convert_int_to_time_string(issued_at_ts) expires_at_str = self._convert_int_to_time_string(expires_at_ts) - return (user_id, project_id, issued_at_str, expires_at_str, audit_ids) + return (user_id, project_id, expires_at_str, audit_ids) class TrustTokenFormatter(BaseTokenFormatter): token_format = fm.TRUST_TOKEN_PREFIX - def create_token(self, user_id, project_id, created_at, expires_at, - audit_ids, trust_id): + def create_token(self, user_id, project_id, expires_at, audit_ids, + trust_id): """Create a trust formatted token. :param user_id: ID of the user in the token request :param project_id: ID of the project to scope to - :param created_at: datetime of the token's creation :param expires_at: datetime of the token's expiration :param audit_ids: list of the token's audit IDs :param trust_id: ID of the trust in effect :returns: a string representing the token """ - issued_at_int = self._convert_time_string_to_int(created_at) expires_at_int = self._convert_time_string_to_int(expires_at) b_user_id = self._convert_uuid_hex_to_bytes(user_id) b_project_id = self._convert_uuid_hex_to_bytes(project_id) b_trust_id = self._convert_uuid_hex_to_bytes(trust_id) - payload = (b_user_id, b_project_id, b_trust_id, issued_at_int, - expires_at_int, audit_ids) + payload = (b_user_id, b_project_id, b_trust_id, expires_at_int, + audit_ids) return self.pack(payload) @@ -278,17 +264,14 @@ class TrustTokenFormatter(BaseTokenFormatter): b_user_id = payload[0] b_project_id = payload[1] b_trust_id = payload[2] - issued_at_ts = payload[3] - expires_at_ts = payload[4] - audit_ids = payload[5] + expires_at_ts = payload[3] + audit_ids = payload[4] # Uncompress the IDs user_id = self._convert_uuid_bytes_to_hex(b_user_id) project_id = self._convert_uuid_bytes_to_hex(b_project_id) trust_id = self._convert_uuid_bytes_to_hex(b_trust_id) # Generate created at and expires at times - issued_at_str = self._convert_int_to_time_string(issued_at_ts) expires_at_str = self._convert_int_to_time_string(expires_at_ts) - return (user_id, project_id, issued_at_str, expires_at_str, audit_ids, - trust_id) + return (user_id, project_id, expires_at_str, audit_ids, trust_id)