Introduce Keystone Fernet Token
We want to introduce how to setup Fernet token in Keystone, and trace keystone source code (kilo) to understand how get token and validate token works.
Installation
Assume you have installed OpenStack or Keystone already. You can see my Blog How to install Keystone.
http://gogosatellite.blogspot.tw/2016/03/keystone-domain-endpoint.html
Then, you can change the setting based on a normal deployment of Keystone.
edit openrc
export OS_TOKEN=iamadmin
export OS_SERVICE_ENDPOINT=http://controller:35357/v2.0
Set up Environment
source openrc
mkdir /etc/keystone/fernet-keys/
chown -R keystone:keystone /etc/keystone/fernet-keys
keystone-manage fernet_setup --keystone-user keystone --keystone-group keystone
edit /etc/keystone/keystone.conf
[token]
provider = keystone.token.providers.fernet.Provider
#provider = keystone.token.providers.uuid.Provider
driver = keystone.token.persistence.backends.sql.Token
We use fernet token instead of uuid shown as above. We keeyp the setting of driver but it takes no effect.
Restart keystone service
service apache2 restart
to get token from Keystone.
curl -si -d @token-request2.json -H "Content-type: application/json" http://172.16.235.128:35357/v3/auth/tokens
where token-request2.json
{
"auth": {
"identity": {
"methods": [
"password"
],
"password": {
"user": {
"domain": {
"name": "Default"
},
"name": "newuser",
"password": "newuser"
}
}
},
"scope": {
"project": {
"domain": {
"name": "Default"
},
"name": "testtenant"
}
}
}
}
Result
You can see the X-Subject-Token, we got the Fernet Token. Where getToken.sh is just a curl command we discussed above.
junmeindeMacBook-Pro:openstack_api junmein$ bash getToken.sh
HTTP/1.1 201 Created
Date: Thu, 26 May 2016 11:10:16 GMT
Server: Apache/2.4.7 (Ubuntu)
X-Subject-Token: gAAAAABXRtmaD0t0r3n0suqWsEOu4Yp76JaLgOJYLdzRJDNx0FwTb5oBVn429dMmMhiQFVGXltQVvlrhaVBaB9SkYnuTqxq06LnjoHkTmZd-afF5fP-g0YX9n2pG6ESNFgNepgmQANQqEl0oVBrC_S8SrKP9f8QcJkuxBDSwQfKIi3mylbvugK4%3D
Vary: X-Auth-Token
X-Distribution: Ubuntu
x-openstack-request-id: req-58735ed6-aeed-4fcb-99b2-bf03512fc45f
Content-Length: 2065
Content-Type: application/json
{"token": {"methods": ["password"], "roles": [{"id": "9fe2ff9ee4384b1894a90878d3e92bab", "name": "_member_"}], "expires_at": "2016-05-26T12:10:17.283270Z", "project": {"domain": {"id": "default", "name": "Default"}, "id": "258b879e4df748caa1bac3416d38a819", "name": "testtenant"}, "catalog": [{"endpoints": [{"region_id": "RegionOne", "url": "http://controller:35357/v2.0", "region": "RegionOne", "interface": "admin", "id": "02f744638b2f44
To make sure the token table in the database won't be increased, since fernet token is not persistent.
mysql -uroot -pshark -e 'use keystone; select * from token;'|wc -l
How to Create Token using Fernet Token Algorithm
def create_token(self, user_id, expires_at, audit_ids, methods=None,
domain_id=None, project_id=None, trust_id=None,
federated_info=None):
"""Given a set of payload attributes, generate a Fernet token."""
.
.
elif project_id:
version = ProjectScopedPayload.version
payload = ProjectScopedPayload.assemble(
user_id,
methods,
project_id,
expires_at,
audit_ids)
class ProjectScopedPayload(BasePayload):
version = 2
@classmethod
def assemble(cls, user_id, methods, project_id, expires_at, audit_ids):
"""Assemble the payload of a project-scoped token.
:param user_id: ID of the user in the token request
:param methods: list of authentication methods used
:param project_id: ID of the project to scope to
:param expires_at: datetime of the token's expiration
:param audit_ids: list of the token's audit IDs
:returns: the payload of a project-scoped token
"""
b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
methods = auth_plugins.convert_method_list_to_integer(methods)
b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id)
expires_at_int = cls._convert_time_string_to_int(expires_at)
b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes,
audit_ids))
return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids)
So the paidload is a list that one can access it by using paidload[0]. It's quite a beautiful skill that we will discuss it later.
and then it will form the token as follows.
versioned_payload = (version,) + payload
serialized_payload = msgpack.packb(versioned_payload)
token = self.pack(serialized_payload)
return token
so the serial encryption packs are
- version
- user_id
- method
- project_id
- expire_at
- audit_ids
We can see what b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id)
doing.
@classmethod
def convert_uuid_hex_to_bytes(cls, uuid_string):
"""Compress UUID formatted strings to bytes.
:param uuid_string: uuid string to compress to bytes
:returns: a byte representation of the uuid
"""
# TODO(lbragstad): Wrap this in an exception. Not sure what the case
# would be where we couldn't handle what we've been given but incase
# the integrity of the token has been compromised.
uuid_obj = uuid.UUID(uuid_string)
return uuid_obj.bytes
Just ,transfer uuid hex to bytes or int。
What is Serialize Paidload !! Godd Stuff!!
It's a beautiful way to pack a accessable attibute, list, to a string and send it to socket or mq. And we then unpack it and access it as a list directly. That's msgpack doing.
This is an example, to realize what is msgpack doing. This is called serialize paidload.
ori = ('ddd', 'aaa', 'bbb', 'ccc')
pack = ��ddd�aaa�bbb�ccc
unpack = ['ddd', 'aaa', 'bbb', 'ccc']
In keystone we pack it and encrypt it. After decrypt it we can access a list directly.
encrypt key
def crypto(self):
"""Return a cryptography instance.
You can extend this class with a custom crypto @property to provide
your own token encoding / decoding. For example, using a different
cryptography library (e.g. ``python-keyczar``) or to meet arbitrary
security requirements.
This @property just needs to return an object that implements
``encrypt(plaintext)`` and ``decrypt(ciphertext)``.
"""
keys = utils.load_keys()
.
.
.
that contains primary, secondary, ..... and rotation. and finally encrypt it by pack function.
def load_keys():
"""Load keys from disk into a list.
The first key in the list is the primary key used for encryption. All
other keys are active secondary keys that can be used for decrypting
tokens.
"""
if not validate_key_repository():
return []
# build a dictionary of key_number:encryption_key pairs
keys = dict()
for filename in os.listdir(CONF.fernet_tokens.key_repository):
path = os.path.join(CONF.fernet_tokens.key_repository, str(filename))
if os.path.isfile(path):
with open(path, 'r') as key_file:
try:
key_id = int(filename)
except ValueError:
pass
else:
keys[key_id] = key_file.read()
if len(keys) != CONF.fernet_tokens.max_active_keys:
# If there haven't been enough key rotations to reach max_active_keys,
# or if the configured value of max_active_keys has changed since the
# last rotation, then reporting the discrepancy might be useful. Once
# the number of keys matches max_active_keys, this log entry is too
# repetitive to be useful.
LOG.info(_LI(
'Loaded %(count)s encryption keys from: %(dir)s'), {
'count': len(keys),
'dir': CONF.fernet_tokens.key_repository})
# return the encryption_keys, sorted by key number, descending
return [keys[x] for x in sorted(keys.keys(), reverse=True)]
Now we want to solve the fernet key problem.
Before obtaining the fernet token service, we must setup by keystone-manage.
As the steps we done before.
mkdir /etc/keystone/fernet-keys/
$ keystone-manage fernet_setup
and the configuration
[fernet_tokens]
# key repository where the fernet keys are stored
key_repository = /etc/keystone/fernet-keys/
# maximum number of keys in key repository
max_active_keys = # default is 3
$ ls /etc/keystone/fernet-keys
0 1 2 3 4
An example for fernet.MultiFernet
https://cryptography.io/en/latest/fernet/
https://media.readthedocs.org/pdf/cryptography/latest/cryptography.pdf
>>> from cryptography.fernet import Fernet, MultiFernet
>>> key1 = Fernet(Fernet.generate_key())
>>> key2 = Fernet(Fernet.generate_key())
>>> f = MultiFernet([key1, key2])
>>> token = f.encrypt(b"Secret message!")
.
.
>>> f.decrypt(token)
'Secret message!'
After load serveral keys, and pass to fernet encrypt function to get encrpt result of token.
and where the key we get is from the directory of /etc/keystone/fernet-keys/
.
Key Rotation
What is MultiFernet doing, according to the offcial site's explanation of MultiFernet:
MultiFernet performs all encryption options using the first key in the list provided. MultiFernet attempts to decrypt tokens with each key in turn. A cryptography.fernet.InvalidToken exception is raised if the correct key is not found in the list provided. Key rotation makes it easy to replace old keys. You can add your new key at the front of the list to start encrypting new messages, and remove old keys as they are no longer needed.
It will use the first key to encrypt the data. Then decrypt by each key in turn, since the key will rotate for security.
where is the key location
root@controller:~# ls /etc/keystone/fernet-keys/
0 1
to see content of the key
root@controller:~# cat /etc/keystone/fernet-keys/0
BIoWWHPzDcoNRhFsg3TFzrRHoYVlL1MECDreHBREqJo=
Conclusion: Generate Token and Validate Token
使用project based Fernet Token包含以下物件: Fernet algorithsm uses the bellow parameters.
- version
- user_id
- method
- project_id
- expire_at
- audit_ids
The process of getting a token:
transfer the above hex string object to **bytes** or **int** -> to form a list -> serialize pack(msgpack.pack) -> Rotated MutiFernet Encryption (Fernet.MultiFernet(key1, key2..)) -> Token.
The process of validating a token (inverse process):
Token -> Rotated MultiFernet Decryption -> serialize unpack(msgpack.unpack) -> list -> access list -> bytes int object transfers to hex string -> to check expire time .. -> validate or invalidate token.
Now we realize how is the keystone to get token and validate token.
And next chapter, we want to talk more about validate token.
How To Validate Token
def validate_token(self, token):
"""Validates a Fernet token and returns the payload attributes."""
# Convert v2 unicode token to a string
if not isinstance(token, six.binary_type):
token = token.encode('ascii')
serialized_payload = self.unpack(token)
versioned_payload = msgpack.unpackb(serialized_payload)
version, payload = versioned_payload[0], versioned_payload[1:]
# depending on the formatter, these may or may not be defined
domain_id = None
project_id = None
trust_id = None
federated_info = None
if version == UnscopedPayload.version:
(user_id, methods, expires_at, audit_ids) = (
UnscopedPayload.disassemble(payload))
elif version == DomainScopedPayload.version:
(user_id, methods, domain_id, expires_at, audit_ids) = (
DomainScopedPayload.disassemble(payload))
elif version == ProjectScopedPayload.version:
(user_id, methods, project_id, expires_at, audit_ids) = (
ProjectScopedPayload.disassemble(payload))
elif version == TrustScopedPayload.version:
(user_id, methods, project_id, expires_at, audit_ids, trust_id) = (
TrustScopedPayload.disassemble(payload))
elif version == FederatedPayload.version:
(user_id, methods, expires_at, audit_ids, federated_info) = (
FederatedPayload.disassemble(payload))
else:
# If the token_format is not recognized, raise ValidationError.
raise exception.ValidationError(_(
'This is not a recognized Fernet payload version: %s') %
version)
# rather than appearing in the payload, the creation time is encoded
# into the token format itself
created_at = TokenFormatter.creation_time(token)
created_at = timeutils.isotime(at=created_at, subsecond=True)
expires_at = timeutils.parse_isotime(expires_at)
expires_at = timeutils.isotime(at=expires_at, subsecond=True)
return (user_id, methods, audit_ids, domain_id, project_id, trust_id,
federated_info, created_at, expires_at)
def disassemble(cls, payload):
"""Disassemble a payload into the component data.
:param payload: the payload of a token
:return: a tuple containing the user_id, auth methods, project_id,
expires_at_str, and audit_ids
"""
(is_stored_as_bytes, user_id) = payload[0]
if is_stored_as_bytes:
user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id)
methods = auth_plugins.convert_integer_to_method_list(payload[1])
(is_stored_as_bytes, project_id) = payload[2]
if is_stored_as_bytes:
project_id = cls.attempt_convert_uuid_bytes_to_hex(project_id)
expires_at_str = cls._convert_int_to_time_string(payload[3])
audit_ids = list(map(provider.base64_encode, payload[4]))
return (user_id, methods, project_id, expires_at_str, audit_ids)
The process is very similar to generate Token.
No comments:
Post a Comment