Configurable token hash algorithm

Tokens were always hashed with MD5. This change allows tokens to
be hashed with SHA256 (or any other algorithm supported by the
keystoneclient token hash function). This is for security
hardening.

There's a new configuration option 'hash_algorithm' in the [token]
section. This is the algorithm to use for hashing PKI tokens, so is
used

a) when storing the token in the db
b) as the hash in the revocation list

hash_algorithm defaults to 'md5' for backwards compatibility.

SecurityImpact
DocImpact
Closes-Bug: #1174499

Change-Id: Iafe3c975d59818c8f362647f7ea5149a03deee47
This commit is contained in:
Brant Knudson 2014-04-09 19:13:09 -05:00
parent da4d4a1035
commit bf4ff96472
5 changed files with 81 additions and 3 deletions

View File

@ -1317,6 +1317,13 @@
# value)
#revoke_by_id=true
# The hash algorithm to use for PKI tokens. This can be set to
# any algorithm that hashlib supports. WARNING: Before
# changing this value, the auth_token middleware must be
# configured with the hash_algorithms, otherwise token
# revocation will not be processed correctly. (string value)
#hash_algorithm=md5
[trust]

View File

@ -216,6 +216,13 @@ FILE_OPTIONS = {
'list of tokens to revoke. Only disable if you are '
'switching to using the Revoke extension with a '
'backend other than KVS, which stores events in memory.'),
cfg.StrOpt('hash_algorithm', default='md5',
help="The hash algorithm to use for PKI tokens. This can "
"be set to any algorithm that hashlib supports. "
"WARNING: Before changing this value, the auth_token "
"middleware must be configured with the "
"hash_algorithms, otherwise token revocation will "
"not be processed correctly."),
],
'revoke': [
cfg.StrOpt('driver',

View File

@ -3221,9 +3221,9 @@ class TokenTests(object):
self.assertIn(token_id, revoked_tokens)
self.assertIn(token2_id, revoked_tokens)
def test_predictable_revoked_pki_token_id(self):
def _test_predictable_revoked_pki_token_id(self, hash_fn):
token_id = self._create_token_id()
token_id_hash = hashlib.md5(token_id).hexdigest()
token_id_hash = hash_fn(token_id).hexdigest()
token = {'user': {'id': uuid.uuid4().hex}}
self.token_api.create_token(token_id, token)
@ -3235,6 +3235,13 @@ class TokenTests(object):
for t in self.token_api.list_revoked_tokens():
self.assertIn('expires', t)
def test_predictable_revoked_pki_token_id_default(self):
self._test_predictable_revoked_pki_token_id(hashlib.md5)
def test_predictable_revoked_pki_token_id_sha256(self):
self.config_fixture.config(group='token', hash_algorithm='sha256')
self._test_predictable_revoked_pki_token_id(hashlib.sha256)
def test_predictable_revoked_uuid_token_id(self):
token_id = uuid.uuid4().hex
token = {'user': {'id': uuid.uuid4().hex}}

View File

@ -12,9 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
import uuid
from keystoneclient.common import cms
import six
from testtools import matchers
from keystone.common import extension
from keystone import config
@ -1209,6 +1212,54 @@ class JsonTestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests):
def assertValidRevocationListResponse(self, response):
self.assertIsNotNone(response.result['signed'])
def _fetch_parse_revocation_list(self):
token1 = self.get_scoped_token()
token2 = self.get_scoped_token()
self.admin_request(method='DELETE',
path='/v2.0/tokens/%s' % token2,
token=token1)
r = self.admin_request(
method='GET',
path='/v2.0/tokens/revoked',
token=token1,
expected_status=200)
signed_text = r.result['signed']
data_json = cms.cms_verify(signed_text, CONF.signing.certfile,
CONF.signing.ca_certs)
data = json.loads(data_json)
return (data, token2)
def test_fetch_revocation_list_md5(self):
"""If the server is configured for md5, then the revocation list has
tokens hashed with MD5.
"""
# The default hash algorithm is md5.
hash_algorithm = 'md5'
(data, token) = self._fetch_parse_revocation_list()
token_hash = cms.cms_hash_token(token, mode=hash_algorithm)
self.assertThat(token_hash, matchers.Equals(data['revoked'][0]['id']))
def test_fetch_revocation_list_sha256(self):
"""If the server is configured for sha256, then the revocation list has
tokens hashed with SHA256
"""
hash_algorithm = 'sha256'
self.config_fixture.config(group='token',
hash_algorithm=hash_algorithm)
(data, token) = self._fetch_parse_revocation_list()
token_hash = cms.cms_hash_token(token, mode=hash_algorithm)
self.assertThat(token_hash, matchers.Equals(data['revoked'][0]['id']))
def test_create_update_user_json_invalid_enabled_type(self):
# Enforce usage of boolean for 'enabled' field in JSON
token = self.get_scoped_token()
@ -1334,6 +1385,12 @@ class RevokeApiJsonTestCase(JsonTestCase):
def test_fetch_revocation_list_admin_200(self):
self.skipTest('Revoke API disables revocation_list.')
def test_fetch_revocation_list_md5(self):
self.skipTest('Revoke API disables revocation_list.')
def test_fetch_revocation_list_sha256(self):
self.skipTest('Revoke API disables revocation_list.')
class XmlTestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests):
xmlns = 'http://docs.openstack.org/identity/api/v2.0'

View File

@ -122,7 +122,7 @@ class Manager(manager.Manager):
returns the passed-in value (such as a UUID token ID or an
existing hash).
"""
return cms.cms_hash_token(token_id)
return cms.cms_hash_token(token_id, mode=CONF.token.hash_algorithm)
def _assert_valid(self, token_id, token_ref):
"""Raise TokenNotFound if the token is expired."""