summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-06-02 20:26:19 +0000
committerGerrit Code Review <review@openstack.org>2020-06-02 20:26:19 +0000
commit7f90daa99927db361280deb7e8e837b18c74ad8b (patch)
treedf359854a1e1359d55ad8d300345257facf0d4cc
parentb2c236304f1969be84f5ec449bdf8df589c150c4 (diff)
parent1c1cf556f81058a63ea0bd5138540b0e6795f7a0 (diff)
downloadkeystone-7f90daa99927db361280deb7e8e837b18c74ad8b.tar.gz
Merge "Check timestamp of signed EC2 token request" into stable/pike
-rw-r--r--keystone/conf/credential.py11
-rw-r--r--keystone/contrib/ec2/controllers.py29
-rw-r--r--keystone/tests/unit/test_contrib_ec2_core.py213
-rw-r--r--releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml28
4 files changed, 276 insertions, 5 deletions
diff --git a/keystone/conf/credential.py b/keystone/conf/credential.py
index b7877a816..048d22283 100644
--- a/keystone/conf/credential.py
+++ b/keystone/conf/credential.py
@@ -46,12 +46,21 @@ share this repository with the repository used to manage keys for Fernet
tokens.
"""))
+auth_ttl = cfg.IntOpt(
+ 'auth_ttl',
+ default=15,
+ help=utils.fmt("""
+The length of time in minutes for which a signed EC2 or S3 token request is
+valid from the timestamp contained in the token request.
+"""))
+
GROUP_NAME = __name__.split('.')[-1]
ALL_OPTS = [
driver,
provider,
- key_repository
+ key_repository,
+ auth_ttl
]
diff --git a/keystone/contrib/ec2/controllers.py b/keystone/contrib/ec2/controllers.py
index 3883a1625..390d45fe5 100644
--- a/keystone/contrib/ec2/controllers.py
+++ b/keystone/contrib/ec2/controllers.py
@@ -33,11 +33,13 @@ Glance to list images needed to perform the requested task.
"""
import abc
+import datetime
import sys
import uuid
from keystoneclient.contrib.ec2 import utils as ec2_utils
from oslo_serialization import jsonutils
+from oslo_utils import timeutils
import six
from six.moves import http_client
@@ -46,11 +48,13 @@ from keystone.common import controller
from keystone.common import dependency
from keystone.common import utils
from keystone.common import wsgi
+import keystone.conf
from keystone import exception
from keystone.i18n import _
from keystone.token import controllers as token_controllers
CRED_TYPE_EC2 = 'ec2'
+CONF = keystone.conf.CONF
@dependency.requires('assignment_api', 'catalog_api', 'credential_api',
@@ -89,6 +93,30 @@ class Ec2ControllerCommon(object):
raise exception.Unauthorized(
message=_('EC2 signature not supplied.'))
+ def _check_timestamp(self, credentials):
+ timestamp = (
+ # AWS Signature v1/v2
+ credentials.get('params', {}).get('Timestamp') or
+ # AWS Signature v4
+ credentials.get('headers', {}).get('X-Amz-Date') or
+ credentials.get('params', {}).get('X-Amz-Date')
+ )
+ if not timestamp:
+ # If the signed payload doesn't include a timestamp then the signer
+ # must have intentionally left it off
+ return
+ try:
+ timestamp = timeutils.parse_isotime(timestamp)
+ timestamp = timeutils.normalize_time(timestamp)
+ except Exception as e:
+ raise exception.Unauthorized(
+ _('Credential timestamp is invalid: %s') % e)
+ auth_ttl = datetime.timedelta(minutes=CONF.credential.auth_ttl)
+ current_time = timeutils.normalize_time(timeutils.utcnow())
+ if current_time > timestamp + auth_ttl:
+ raise exception.Unauthorized(
+ _('Credential is expired'))
+
@abc.abstractmethod
def authenticate(self, context, credentials=None, ec2Credentials=None):
"""Validate a signed EC2 request and provide a token.
@@ -147,6 +175,7 @@ class Ec2ControllerCommon(object):
six.reraise(exception.Unauthorized, exception.Unauthorized(e),
sys.exc_info()[2])
+ self._check_timestamp(credentials)
roles = self.assignment_api.get_roles_for_user_and_project(
user_ref['id'], tenant_ref['id']
)
diff --git a/keystone/tests/unit/test_contrib_ec2_core.py b/keystone/tests/unit/test_contrib_ec2_core.py
index d5f747219..431210bae 100644
--- a/keystone/tests/unit/test_contrib_ec2_core.py
+++ b/keystone/tests/unit/test_contrib_ec2_core.py
@@ -12,9 +12,14 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime
+import hashlib
+
from keystoneclient.contrib.ec2 import utils as ec2_utils
+from oslo_utils import timeutils
from six.moves import http_client
+from keystone.common import utils
from keystone.contrib.ec2 import controllers
from keystone.tests import unit
from keystone.tests.unit import test_v2
@@ -22,6 +27,14 @@ from keystone.tests.unit import test_v3
class EC2ContribCoreV2(test_v2.RestfulTestCase):
+ def setUp(self):
+ super(EC2ContribCoreV2, self).setUp()
+
+ self.cred_blob, self.credential = unit.new_ec2_credential(
+ self.user_foo['id'], self.tenant_bar['id'])
+ self.credential_api.create_credential(
+ self.credential['id'], self.credential)
+
def config_overrides(self):
super(EC2ContribCoreV2, self).config_overrides()
@@ -59,6 +72,7 @@ class EC2ContribCoreV2(test_v2.RestfulTestCase):
credential['id'], credential)
signer = ec2_utils.Ec2Signer(cred_blob['secret'])
+ timestamp = utils.isotime(timeutils.utcnow())
credentials = {
'access': cred_blob['access'],
'secret': cred_blob['secret'],
@@ -68,7 +82,7 @@ class EC2ContribCoreV2(test_v2.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
},
}
credentials['signature'] = signer.generate(credentials)
@@ -107,6 +121,7 @@ class EC2ContribCoreV2(test_v2.RestfulTestCase):
credential['id'], credential)
signer = ec2_utils.Ec2Signer('totally not the secret')
+ timestamp = utils.isotime(timeutils.utcnow())
credentials = {
'access': cred_blob['access'],
'secret': 'totally not the secret',
@@ -116,8 +131,82 @@ class EC2ContribCoreV2(test_v2.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ self.public_request(
+ method='POST',
+ path='/v2.0/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.UNAUTHORIZED)
+
+ def test_authenticate_expired_request(self):
+ self.config_fixture.config(
+ group='credential',
+ auth_ttl=5
+ )
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ past = timeutils.utcnow() - datetime.timedelta(minutes=10)
+ timestamp = utils.isotime(past)
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'SignatureVersion': '2',
+ 'Action': 'Test',
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ self.public_request(
+ method='POST',
+ path='/v2.0/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.UNAUTHORIZED)
+
+ def test_authenticate_expired_request_v4(self):
+ self.config_fixture.config(
+ group='credential',
+ auth_ttl=5
+ )
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ past = timeutils.utcnow() - datetime.timedelta(minutes=10)
+ timestamp = utils.isotime(past)
+ hashed_payload = (
+ 'GET\n'
+ '/\n'
+ 'Action=Test\n'
+ 'host:localhost\n'
+ 'x-amz-date:' + timestamp + '\n'
+ '\n'
+ 'host;x-amz-date\n'
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
+ )
+ body_hash = hashlib.sha256(hashed_payload.encode()).hexdigest()
+ amz_credential = (
+ 'AKIAIOSFODNN7EXAMPLE/%s/us-east-1/iam/aws4_request,' %
+ timestamp[:8])
+
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'Action': 'Test',
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-SignedHeaders': 'host,x-amz-date,',
+ 'X-Amz-Credential': amz_credential
},
+ 'headers': {
+ 'X-Amz-Date': timestamp
+ },
+ 'body_hash': body_hash
}
credentials['signature'] = signer.generate(credentials)
self.public_request(
@@ -140,6 +229,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
def test_valid_authentication_response_with_proper_secret(self):
signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ timestamp = utils.isotime(timeutils.utcnow())
credentials = {
'access': self.cred_blob['access'],
'secret': self.cred_blob['secret'],
@@ -149,8 +239,50 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ resp = self.post(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.OK)
+ self.assertValidProjectScopedTokenResponse(resp, self.user)
+
+ def test_valid_authentication_response_with_signature_v4(self):
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ timestamp = utils.isotime(timeutils.utcnow())
+ hashed_payload = (
+ 'GET\n'
+ '/\n'
+ 'Action=Test\n'
+ 'host:localhost\n'
+ 'x-amz-date:' + timestamp + '\n'
+ '\n'
+ 'host;x-amz-date\n'
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
+ )
+ body_hash = hashlib.sha256(hashed_payload.encode()).hexdigest()
+ amz_credential = (
+ 'AKIAIOSFODNN7EXAMPLE/%s/us-east-1/iam/aws4_request,' %
+ timestamp[:8])
+
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'Action': 'Test',
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-SignedHeaders': 'host,x-amz-date,',
+ 'X-Amz-Credential': amz_credential
+ },
+ 'headers': {
+ 'X-Amz-Date': timestamp
},
+ 'body_hash': body_hash
}
credentials['signature'] = signer.generate(credentials)
resp = self.post(
@@ -178,6 +310,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
def test_authenticate_without_proper_secret_returns_unauthorized(self):
signer = ec2_utils.Ec2Signer('totally not the secret')
+ timestamp = utils.isotime(timeutils.utcnow())
credentials = {
'access': self.cred_blob['access'],
'secret': 'totally not the secret',
@@ -187,8 +320,80 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ self.post(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.UNAUTHORIZED)
+
+ def test_authenticate_expired_request(self):
+ self.config_fixture.config(
+ group='credential',
+ auth_ttl=5
+ )
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ past = timeutils.utcnow() - datetime.timedelta(minutes=10)
+ timestamp = utils.isotime(past)
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'SignatureVersion': '2',
+ 'Action': 'Test',
+ 'Timestamp': timestamp
+ },
+ }
+ credentials['signature'] = signer.generate(credentials)
+ self.post(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http_client.UNAUTHORIZED)
+
+ def test_authenticate_expired_request_v4(self):
+ self.config_fixture.config(
+ group='credential',
+ auth_ttl=5
+ )
+ signer = ec2_utils.Ec2Signer(self.cred_blob['secret'])
+ past = timeutils.utcnow() - datetime.timedelta(minutes=10)
+ timestamp = utils.isotime(past)
+ hashed_payload = (
+ 'GET\n'
+ '/\n'
+ 'Action=Test\n'
+ 'host:localhost\n'
+ 'x-amz-date:' + timestamp + '\n'
+ '\n'
+ 'host;x-amz-date\n'
+ 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
+ )
+ body_hash = hashlib.sha256(hashed_payload.encode()).hexdigest()
+ amz_credential = (
+ 'AKIAIOSFODNN7EXAMPLE/%s/us-east-1/iam/aws4_request,' %
+ timestamp[:8])
+
+ credentials = {
+ 'access': self.cred_blob['access'],
+ 'secret': self.cred_blob['secret'],
+ 'host': 'localhost',
+ 'verb': 'GET',
+ 'path': '/',
+ 'params': {
+ 'Action': 'Test',
+ 'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
+ 'X-Amz-SignedHeaders': 'host,x-amz-date,',
+ 'X-Amz-Credential': amz_credential
+ },
+ 'headers': {
+ 'X-Amz-Date': timestamp
},
+ 'body_hash': body_hash
}
credentials['signature'] = signer.generate(credentials)
self.post(
diff --git a/releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml b/releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml
new file mode 100644
index 000000000..d0732ab4c
--- /dev/null
+++ b/releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml
@@ -0,0 +1,28 @@
+---
+feature:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Added a new config option ``auth_ttl`` in the ``[credential]`` config
+ section to allow configuring the period for which a signed token request
+ from AWS is valid. The default is 15 minutes in accordance with the AWS
+ Signature V4 API reference.
+upgrade:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Added a default TTL of 15 minutes for signed EC2 credential requests,
+ where previously an EC2 signed token request was valid indefinitely. This
+ change in behavior is needed to protect against replay attacks.
+security:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Fixed an incorrect EC2 token validation implementation in which the
+ timestamp of the signed request was ignored, which made EC2 and S3 token
+ requests vulnerable to replay attacks. The default TTL is 15 minutes but
+ is configurable.
+fixes:
+ - |
+ [`bug 1872737 <https://bugs.launchpad.net/keystone/+bug/1872737>`_]
+ Fixed an incorrect EC2 token validation implementation in which the
+ timestamp of the signed request was ignored, which made EC2 and S3 token
+ requests vulnerable to replay attacks. The default TTL is 15 minutes but
+ is configurable.