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
This commit is contained in:
Dolph Mathews 2015-03-05 21:12:08 +00:00
parent d87768313b
commit c83f8920bf
3 changed files with 62 additions and 90 deletions

View File

@ -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:])

View File

@ -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)

View File

@ -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)