Merge "Check timestamp of signed EC2 token request"
This commit is contained in:
commit
9a9022600e
|
@ -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'])
|
||||
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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,7 +130,7 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
|
|||
'params': {
|
||||
'SignatureVersion': '2',
|
||||
'Action': 'Test',
|
||||
'Timestamp': '2007-01-31T23:59:59Z'
|
||||
'Timestamp': timestamp
|
||||
},
|
||||
}
|
||||
credentials['signature'] = signer.generate(credentials)
|
||||
|
@ -89,3 +138,75 @@ class EC2ContribCoreV3(test_v3.RestfulTestCase):
|
|||
'/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(
|
||||
'/ec2tokens',
|
||||
body={'credentials': credentials},
|
||||
expected_status=http.client.UNAUTHORIZED)
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue