summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2020-05-02 02:46:05 +0000
committerGerrit Code Review <review@openstack.org>2020-05-02 02:46:05 +0000
commit9e00e99866572435e71f5d825e035a84a1c5d7c8 (patch)
treec435f7c0af9854c34d3e0ce8f7cfccdadea509f5
parentaef912187b74bb34d8e380008683f97c8ff4b213 (diff)
parent8d5becbe4b463f6a5a24a1929dd0f48dab6ae027 (diff)
downloadkeystone-9e00e99866572435e71f5d825e035a84a1c5d7c8.tar.gz
Merge "Check timestamp of signed EC2 token request" into stable/ussuri
-rw-r--r--keystone/api/_shared/EC2_S3_Resource.py31
-rw-r--r--keystone/conf/credential.py11
-rw-r--r--keystone/tests/unit/test_contrib_ec2_core.py125
-rw-r--r--releasenotes/notes/bug-1872737-f8e1ad3b6705b766.yaml28
4 files changed, 192 insertions, 3 deletions
diff --git a/keystone/api/_shared/EC2_S3_Resource.py b/keystone/api/_shared/EC2_S3_Resource.py
index 021cf4ad5..803254816 100644
--- a/keystone/api/_shared/EC2_S3_Resource.py
+++ b/keystone/api/_shared/EC2_S3_Resource.py
@@ -12,15 +12,20 @@
# Common base resource for EC2 and S3 Authentication
+import datetime
+
from oslo_serialization import jsonutils
+from oslo_utils import timeutils
from werkzeug import exceptions
from keystone.common import provider_api
from keystone.common import utils
+import keystone.conf
from keystone import exception as ks_exceptions
from keystone.i18n import _
from keystone.server import flask as ks_flask
+CONF = keystone.conf.CONF
PROVIDERS = provider_api.ProviderAPIs
CRED_TYPE_EC2 = 'ec2'
@@ -36,6 +41,31 @@ class ResourceBase(ks_flask.ResourceBase):
# the ABC module.
raise NotImplementedError()
+ @staticmethod
+ def _check_timestamp(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 ks_exceptions.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 ks_exceptions.Unauthorized(
+ _('Credential is expired'))
+
def handle_authenticate(self):
# TODO(morgan): convert this dirty check to JSON Schema validation
# this mirrors the previous behavior of the webob system where an
@@ -101,6 +131,7 @@ class ResourceBase(ks_flask.ResourceBase):
except AssertionError as e:
raise ks_exceptions.Unauthorized from e
+ self._check_timestamp(credentials)
roles = PROVIDERS.assignment_api.get_roles_for_user_and_project(
user_ref['id'], project_ref['id'])
diff --git a/keystone/conf/credential.py b/keystone/conf/credential.py
index 3ccf6e633..2066fbdfb 100644
--- a/keystone/conf/credential.py
+++ b/keystone/conf/credential.py
@@ -61,6 +61,14 @@ Time to cache credential data in seconds. This has no effect unless global
caching is enabled.
"""))
+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 = [
@@ -68,7 +76,8 @@ ALL_OPTS = [
provider,
key_repository,
caching,
- cache_time
+ cache_time,
+ auth_ttl
]
diff --git a/keystone/tests/unit/test_contrib_ec2_core.py b/keystone/tests/unit/test_contrib_ec2_core.py
index 1242142e0..4b514f898 100644
--- a/keystone/tests/unit/test_contrib_ec2_core.py
+++ b/keystone/tests/unit/test_contrib_ec2_core.py
@@ -12,10 +12,15 @@
# License for the specific language governing permissions and limitations
# under the License.
+import datetime
+import hashlib
+
import http.client
from keystoneclient.contrib.ec2 import utils as ec2_utils
+from oslo_utils import timeutils
from keystone.common import provider_api
+from keystone.common import utils
from keystone.tests import unit
from keystone.tests.unit import test_v3
@@ -34,6 +39,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'],
@@ -43,7 +49,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
'params': {
'SignatureVersion': '2',
'Action': 'Test',
- 'Timestamp': '2007-01-31T23:59:59Z'
+ 'Timestamp': timestamp
},
}
credentials['signature'] = signer.generate(credentials)
@@ -53,6 +59,48 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
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(
+ '/ec2tokens',
+ body={'credentials': credentials},
+ expected_status=http.client.OK)
+ self.assertValidProjectScopedTokenResponse(resp, self.user)
+
def test_authenticate_with_empty_body_returns_bad_request(self):
self.post(
'/ec2tokens',
@@ -72,6 +120,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',
@@ -81,8 +130,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.