keystone/keystone/tests/unit/test_contrib_ec2_core.py
Colleen Murphy ab89ea7490 Check timestamp of signed EC2 token request
EC2 token requests contain a signature that signs the entire request,
including the access timestamp. While the signature is checked, the
timestamp is not, and so these signed requests remain valid
indefinitely, leaving the token API vulnerable to replay attacks. This
change introduces a configurable TTL for signed token requests and
ensures that the timestamp is actually validated against it.

The check will work for either an AWS Signature v1/v2 'Timestamp'
parameter[1] or the AWS Signature v4 'X-Aws-Date' header or
parameter[2].

Although this technically adds a new feature and the default value of
the feature changes behavior, this change is required to protect
credential holders and therefore must be backported to all supported
branches.

[1] https://docs.aws.amazon.com/general/latest/gr/signature-version-2.html
[2] https://docs.aws.amazon.com/general/latest/gr/sigv4-date-handling.html

Change-Id: Idb10267338b4204b435df233c636046a1ce5711f
Closes-bug: #1872737
2020-04-28 11:45:24 -07:00

213 lines
7.3 KiB
Python

# Copyright 2012 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# 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
PROVIDERS = provider_api.ProviderAPIs
class EC2ContribCoreV3(test_v3.RestfulTestCase):
def setUp(self):
super(EC2ContribCoreV3, self).setUp()
self.cred_blob, self.credential = unit.new_ec2_credential(
self.user['id'], self.project_id)
PROVIDERS.credential_api.create_credential(
self.credential['id'], self.credential)
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'],
'host': 'localhost',
'verb': 'GET',
'path': '/',
'params': {
'SignatureVersion': '2',
'Action': 'Test',
'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(
'/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',
body={},
expected_status=http.client.BAD_REQUEST)
def test_authenticate_without_json_request_returns_bad_request(self):
self.post(
'/ec2tokens',
body='not json',
expected_status=http.client.BAD_REQUEST)
def test_authenticate_without_request_body_returns_bad_request(self):
self.post(
'/ec2tokens',
expected_status=http.client.BAD_REQUEST)
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',
'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(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)