From a34bb20e32b342006f9c4a94040c43084fac2345 Mon Sep 17 00:00:00 2001 From: Michael Graeb Date: Fri, 15 Nov 2019 16:09:15 -0800 Subject: [PATCH] AwsSigner & AwsSigningConfig (#92) --- aws-c-auth | 2 +- awscrt/auth.py | 299 +++++++++++++++++++++++++++++++---- source/auth.h | 17 +- source/auth_credentials.c | 8 +- source/auth_signer.c | 187 ++++++++++++++++++++++ source/auth_signing_config.c | 292 ++++++++++++++++++++++++++++++++++ source/module.c | 12 ++ test/test_auth.py | 252 +++++++++++++++++++++++++++++ test/test_credentials.py | 100 ------------ 9 files changed, 1035 insertions(+), 134 deletions(-) create mode 100644 source/auth_signer.c create mode 100644 source/auth_signing_config.c create mode 100644 test/test_auth.py delete mode 100644 test/test_credentials.py diff --git a/aws-c-auth b/aws-c-auth index fecefc800..c0e5b30f9 160000 --- a/aws-c-auth +++ b/aws-c-auth @@ -1 +1 @@ -Subproject commit fecefc8009760218e29f7bf53a8efa0193988f8c +Subproject commit c0e5b30f96428ef2407fd0cfedf558b044bbb512 diff --git a/awscrt/auth.py b/awscrt/auth.py index 52fa26193..0b6fe608c 100644 --- a/awscrt/auth.py +++ b/awscrt/auth.py @@ -14,15 +14,40 @@ from __future__ import absolute_import import _awscrt from awscrt import isinstance_str, NativeResource +from awscrt.http import HttpRequest from awscrt.io import ClientBootstrap from concurrent.futures import Future +import datetime +from enum import IntEnum +import time +try: + _utc = datetime.timezone.utc +except AttributeError: + # Python 2 lacks the datetime.timestamp() method. + # We can do the timestamp math ourselves, but only if datetime.tzinfo is set. + # Python 2 also lacks any predefined tzinfo classes (ex: datetime.timezone.utc), + # so we must define our own. + class _UTC(datetime.tzinfo): + ZERO = datetime.timedelta(0) -class Credentials(NativeResource): + def utcoffset(self, dt): + return _UTC.ZERO + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return _UTC.ZERO + + _utc = _UTC() + + +class AwsCredentials(NativeResource): """ - Credentials are the public/private data needed to sign an authenticated AWS request. + AwsCredentials are the public/private data needed to sign an authenticated AWS request. + AwsCredentials are immutable. """ - __slots__ = () def __init__(self, access_key_id, secret_access_key, session_token=None): @@ -30,7 +55,7 @@ def __init__(self, access_key_id, secret_access_key, session_token=None): assert isinstance_str(secret_access_key) assert isinstance_str(session_token) or session_token is None - super(Credentials, self).__init__() + super(AwsCredentials, self).__init__() self._binding = _awscrt.credentials_new(access_key_id, secret_access_key, session_token) @property @@ -45,19 +70,82 @@ def secret_access_key(self): def session_token(self): return _awscrt.credentials_session_token(self._binding) + def __deepcopy__(self, memo): + # AwsCredentials is immutable, so just return self. + return self + -class CredentialsProviderBase(NativeResource): +class AwsCredentialsProviderBase(NativeResource): """ - Base class for providers that source the Credentials needed to sign an authenticated AWS request. + Base class for providers that source the AwsCredentials needed to sign an authenticated AWS request. """ + __slots__ = () + + def __init__(self, binding=None): + super(AwsCredentialsProviderBase, self).__init__() + + if binding is None: + # TODO: create binding type that lets native code call into python subclass + raise NotImplementedError("Custom subclasses of AwsCredentialsProviderBase are not yet supported") + + self._binding = binding def get_credentials(self): """ - Asynchronously fetch Credentials. + Asynchronously fetch AwsCredentials. - Returns a Future which will contain Credentials (or an exception) + Returns a Future which will contain AwsCredentials (or an exception) when the call completes. The call may complete on a different thread. """ + raise NotImplementedError() + + def close(self): + """ + Signal a provider (and all linked providers) to cancel pending queries and + stop accepting new ones. Useful to hasten shutdown time if you know the provider + is going away. + """ + pass + + +class AwsCredentialsProvider(AwsCredentialsProviderBase): + """ + Credentials providers source the AwsCredentials needed to sign an authenticated AWS request. + + This class provides new() functions for several built-in provider types. + """ + __slots__ = () + + @classmethod + def new_default_chain(cls, client_bootstrap): + """ + Create the default provider chain used by most AWS SDKs. + + Generally: + + (1) Environment + (2) Profile + (3) (conditional, off by default) ECS + (4) (conditional, on by default) EC2 Instance Metadata + """ + assert isinstance(client_bootstrap, ClientBootstrap) + + binding = _awscrt.credentials_provider_new_chain_default(client_bootstrap) + return cls(binding) + + @classmethod + def new_static(cls, access_key_id, secret_access_key, session_token=None): + """ + Create a simple provider that just returns a fixed set of credentials + """ + assert isinstance_str(access_key_id) + assert isinstance_str(secret_access_key) + assert isinstance_str(session_token) or session_token is None + + binding = _awscrt.credentials_provider_new_static(access_key_id, secret_access_key, session_token) + return cls(binding) + + def get_credentials(self): future = Future() def _on_complete(error_code, access_key_id, secret_access_key, session_token): @@ -65,7 +153,7 @@ def _on_complete(error_code, access_key_id, secret_access_key, session_token): if error_code: future.set_exception(Exception(error_code)) # TODO: Actual exceptions for error_codes else: - credentials = Credentials(access_key_id, secret_access_key, session_token) + credentials = AwsCredentials(access_key_id, secret_access_key, session_token) future.set_result(credentials) except Exception as e: @@ -87,36 +175,187 @@ def close(self): _awscrt.credentials_provider_shutdown(self._binding) -class DefaultCredentialsProviderChain(CredentialsProviderBase): +class AwsSigningAlgorithm(IntEnum): + """ + Which signing algorithm to use. + + SigV4Header: Use Signature Version 4 to sign headers. + SigV4QueryParam: Use Signature Version 4 to sign query parameters. """ - Providers source the Credentials needed to sign an authenticated AWS request. - This is the default provider chain used by most AWS SDKs. + SigV4Header = 0 + SigV4QueryParam = 1 - Generally: - (1) Environment - (2) Profile - (3) (conditional, off by default) ECS - (4) (conditional, on by default) EC2 Instance Metadata +class AwsSigningConfig(NativeResource): """ + Configuration for use in AWS-related signing. + AwsSigningConfig is immutable. - def __init__(self, client_bootstrap): - assert isinstance(client_bootstrap, ClientBootstrap) + It is good practice to use a new config for each signature, or the date might get too old. + Naive dates (lacking timezone info) are assumed to be in local time. + """ + __slots__ = () + + _attributes = ('algorithm', 'credentials_provider', 'region', 'service', 'date', 'should_sign_param', + 'use_double_uri_encode', 'should_normalize_uri_path', 'sign_body') + + def __init__(self, + algorithm, # type: AwsSigningAlgorithm + credentials_provider, # type: AwsCredentialsProviderBase + region, # type: str + service, # type: str + date=datetime.datetime.now(_utc), # type: datetime.datetime + should_sign_param=None, # type: Optional[Callable[[str], bool]] + use_double_uri_encode=False, # type: bool + should_normalize_uri_path=True, # type: bool + sign_body=True # type: bool + ): + # type: (...) -> None - super(DefaultCredentialsProviderChain, self).__init__() - self._binding = _awscrt.credentials_provider_new_chain_default(client_bootstrap) + assert isinstance(algorithm, AwsSigningAlgorithm) + assert isinstance(credentials_provider, AwsCredentialsProviderBase) + assert isinstance_str(region) + assert isinstance_str(service) + assert isinstance(date, datetime.datetime) + assert callable(should_sign_param) or should_sign_param is None + + super(AwsSigningConfig, self).__init__() + + try: + timestamp = date.timestamp() + except AttributeError: + # Python 2 doesn't have datetime.timestamp() function. + # If it did we could just call it from binding code instead of calculating it here. + if date.tzinfo is None: + timestamp = time.mktime(date.timetuple()) + else: + epoch = datetime.datetime(1970, 1, 1, tzinfo=_utc) + timestamp = (date - epoch).total_seconds() + self._binding = _awscrt.signing_config_new( + algorithm, + credentials_provider, + region, + service, + date, + timestamp, + should_sign_param, + use_double_uri_encode, + should_normalize_uri_path, + sign_body) -class StaticCredentialsProvider(CredentialsProviderBase): + def replace(self, **kwargs): + """ + Return an AwsSigningConfig with the same attributes, except for those + attributes given new values by whichever keyword arguments are specified. + """ + args = {x: kwargs.get(x, getattr(self, x)) for x in AwsSigningConfig._attributes} + return AwsSigningConfig(**args) + + @property + def algorithm(self): + """Which AwsSigningAlgorithm to invoke""" + return AwsSigningAlgorithm(_awscrt.signing_config_get_algorithm(self._binding)) + + @property + def credentials_provider(self): + """AwsCredentialsProvider to fetch signing credentials with""" + return _awscrt.signing_config_get_credentials_provider(self._binding) + + @property + def region(self): + """The region to sign against""" + return _awscrt.signing_config_get_region(self._binding) + + @property + def service(self): + """Name of service to sign a request for""" + return _awscrt.signing_config_get_service(self._binding) + + @property + def date(self): + """datetime.datetime to use during the signing process""" + return _awscrt.signing_config_get_date(self._binding) + + @property + def should_sign_param(self): + """ + Optional function to control which parameters (header or query) are a part of the canonical request. + Function signature is: (name) -> bool + Skipping auth-required params will result in an unusable signature. + Headers injected by the signing process are not skippable. + This function does not override the internal check function (x-amzn-trace-id, user-agent), but rather + supplements it. In particular, a header will get signed if and only if it returns true to both + the internal check (skips x-amzn-trace-id, user-agent) and this function (if defined). + """ + return _awscrt.signing_config_get_should_sign_param(self._binding) + + @property + def use_double_uri_encode(self): + """ + We assume the uri will be encoded once in preparation for transmission. Certain services + do not decode before checking signature, requiring us to actually double-encode the uri in the canonical request + in order to pass a signature check. + """ + return _awscrt.signing_config_get_use_double_uri_encode(self._binding) + + @property + def should_normalize_uri_path(self): + """Controls whether or not the uri paths should be normalized when building the canonical request""" + return _awscrt.signing_config_get_should_normalize_uri_path(self._binding) + + @property + def sign_body(self): + """ + If true adds the x-amz-content-sha256 header (with appropriate value) to the canonical request, + otherwise does nothing + """ + return _awscrt.signing_config_get_sign_body(self._binding) + + +class AwsSigner(NativeResource): """ - Providers source the Credentials needed to sign an authenticated AWS request. - This is a simple provider that just returns a fixed set of credentials + A signer that performs AWS http request signing. + + When using this signer to sign AWS http requests: + + (1) Do not add the following headers to requests before signing, they may be added by the signer: + x-amz-content-sha256, + X-Amz-Date, + Authorization + + (2) Do not add the following query params to requests before signing, they may be added by the signer: + X-Amz-Signature, + X-Amz-Date, + X-Amz-Credential, + X-Amz-Algorithm, + X-Amz-SignedHeaders """ - def __init__(self, access_key_id, secret_access_key, session_token=None): - assert isinstance_str(access_key_id) - assert isinstance_str(secret_access_key) - assert isinstance_str(session_token) or session_token is None + def __init__(self): + super(AwsSigner, self).__init__() + self._binding = _awscrt.signer_new_aws() + + def sign(self, http_request, signing_config): + """ + Asynchronously transform the HttpRequest according to the signing algorithm. + Returns a Future whose result will be the signed HttpRequest. - super(StaticCredentialsProvider, self).__init__() - self._binding = _awscrt.credentials_provider_new_static(access_key_id, secret_access_key, session_token) + It is good practice to use a new config for each signature, or the date might get too old. + """ + assert isinstance(http_request, HttpRequest) + assert isinstance(signing_config, AwsSigningConfig) + + future = Future() + + def _on_complete(error_code): + try: + if error_code: + future.set_exception(Exception(error_code)) # TODO: Actual exceptions for error_codes + else: + future.set_result(http_request) + except Exception as e: + future.set_exception(e) + + _awscrt.signer_sign_request(self, http_request, signing_config, _on_complete) + return future diff --git a/source/auth.h b/source/auth.h index ed54e7801..b3267dc8f 100644 --- a/source/auth.h +++ b/source/auth.h @@ -25,14 +25,29 @@ PyObject *aws_py_credentials_session_token(PyObject *self, PyObject *args); PyObject *aws_py_credentials_provider_get_credentials(PyObject *self, PyObject *args); PyObject *aws_py_credentials_provider_shutdown(PyObject *self, PyObject *args); - PyObject *aws_py_credentials_provider_new_chain_default(PyObject *self, PyObject *args); PyObject *aws_py_credentials_provider_new_static(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_new(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_algorithm(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_credentials_provider(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_region(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_service(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_date(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_should_sign_param(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_use_double_uri_encode(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_should_normalize_uri_path(PyObject *self, PyObject *args); +PyObject *aws_py_signing_config_get_sign_body(PyObject *self, PyObject *args); + +PyObject *aws_py_signer_new_aws(PyObject *self, PyObject *args); +PyObject *aws_py_signer_sign_request(PyObject *self, PyObject *args); + /* Given a python object, return a pointer to its underlying native type. * If NULL is returned, a python error has been set */ struct aws_credentials *aws_py_get_credentials(PyObject *credentials); struct aws_credentials_provider *aws_py_get_credentials_provider(PyObject *credentials_provider); +struct aws_signing_config_aws *aws_py_get_signing_config(PyObject *signing_config); +struct aws_signer *aws_py_get_signer(PyObject *signer); #endif // AWS_CRT_PYTHON_AUTH_H diff --git a/source/auth_credentials.c b/source/auth_credentials.c index 5f44eb62c..0874978f5 100644 --- a/source/auth_credentials.c +++ b/source/auth_credentials.c @@ -171,7 +171,11 @@ struct aws_credentials_provider *aws_py_get_credentials_provider(PyObject *crede return native; } -int s_aws_string_to_cstr_and_ssize(const struct aws_string *source, const char **out_cstr, Py_ssize_t *out_ssize) { +static int s_aws_string_to_cstr_and_ssize( + const struct aws_string *source, + const char **out_cstr, + Py_ssize_t *out_ssize) { + *out_cstr = NULL; *out_ssize = 0; if (source) { @@ -280,7 +284,7 @@ PyObject *aws_py_credentials_provider_shutdown(PyObject *self, PyObject *args) { /* Create binding and capsule. * Helper function for every aws_py_credentials_provider_new_XYZ() function */ -PyObject *s_new_credentials_provider_binding_and_capsule(struct credentials_provider_binding **out_binding) { +static PyObject *s_new_credentials_provider_binding_and_capsule(struct credentials_provider_binding **out_binding) { *out_binding = NULL; struct credentials_provider_binding *binding = diff --git a/source/auth_signer.c b/source/auth_signer.c new file mode 100644 index 000000000..d3d6cc47b --- /dev/null +++ b/source/auth_signer.c @@ -0,0 +1,187 @@ +/* + * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +#include "auth.h" + +#include "http.h" + +#include +#include + +static const char *s_capsule_name_signer = "aws_signer"; + +/* Signer capsule contains raw aws_signer struct. There is no intermediate binding struct. */ + +/* Runs when GC destroys the capsule containing the binding */ +static void s_signer_capsule_destructor(PyObject *py_capsule) { + struct aws_signer *signer = PyCapsule_GetPointer(py_capsule, s_capsule_name_signer); + aws_signer_destroy(signer); +} + +struct aws_signer *aws_py_get_signer(PyObject *py_signer) { + struct aws_signer *native = NULL; + + PyObject *py_capsule = PyObject_GetAttrString(py_signer, "_binding"); + if (py_capsule) { + native = PyCapsule_GetPointer(py_capsule, s_capsule_name_signer); + Py_DECREF(py_capsule); + } + + return native; +} + +PyObject *aws_py_signer_new_aws(PyObject *self, PyObject *args) { + (void)self; + + if (!PyArg_ParseTuple(args, "")) { + return NULL; + } + + struct aws_signer *signer = aws_signer_new_aws(aws_py_get_allocator()); + if (!signer) { + return PyErr_AwsLastError(); + } + + /* From hereon, we need to clean up if errors occur. */ + + PyObject *py_capsule = PyCapsule_New(signer, s_capsule_name_signer, s_signer_capsule_destructor); + if (!py_capsule) { + aws_signer_destroy(signer); + return NULL; + } + + return py_capsule; +} + +/* Object that stays alive for duration async signing operation */ +struct async_signing_data { + PyObject *py_signer; + PyObject *py_http_request; + struct aws_http_message *http_request; /* owned by py_http_request, do not clean up. */ + PyObject *py_signing_config; + PyObject *py_on_complete; + struct aws_signable *signable; +}; + +static void s_async_signing_data_destroy(struct async_signing_data *async_data) { + if (async_data) { + Py_XDECREF(async_data->py_signer); + Py_XDECREF(async_data->py_http_request); + Py_XDECREF(async_data->py_signing_config); + Py_XDECREF(async_data->py_on_complete); + aws_signable_destroy(async_data->signable); + } +} + +static void s_signing_complete(struct aws_signing_result *signing_result, int error_code, void *userdata) { + struct async_signing_data *async_data = userdata; + + if (!error_code) { + struct aws_allocator *allocator = aws_py_get_allocator(); + + if (aws_apply_signing_result_to_http_request(async_data->http_request, allocator, signing_result)) { + error_code = aws_last_error(); + } + } + + /*************** GIL ACQUIRE ***************/ + PyGILState_STATE state = PyGILState_Ensure(); + + PyObject *py_result = PyObject_CallFunction(async_data->py_on_complete, "(i)", error_code); + if (py_result) { + Py_DECREF(py_result); + } else { + PyErr_WriteUnraisable(PyErr_Occurred()); + } + + s_async_signing_data_destroy(async_data); + + PyGILState_Release(state); + /*************** GIL RELEASE ***************/ +} + +PyObject *aws_py_signer_sign_request(PyObject *self, PyObject *args) { + (void)self; + + PyObject *py_signer; + PyObject *py_http_request; + PyObject *py_signing_config; + PyObject *py_on_complete; + if (!PyArg_ParseTuple(args, "OOOO", &py_signer, &py_http_request, &py_signing_config, &py_on_complete)) { + return NULL; + } + + struct aws_signer *signer = aws_py_get_signer(py_signer); + if (!signer) { + return NULL; + } + + struct aws_http_message *http_request = aws_py_get_http_message(py_http_request); + if (!http_request) { + return NULL; + } + + struct aws_signing_config_aws *signing_config = aws_py_get_signing_config(py_signing_config); + if (!signing_config) { + return NULL; + } + + AWS_FATAL_ASSERT(py_on_complete != Py_None); + + struct aws_allocator *alloc = aws_py_get_allocator(); + + struct async_signing_data *async_data = aws_mem_calloc(alloc, 1, sizeof(struct async_signing_data)); + if (!async_data) { + return PyErr_AwsLastError(); + } + + /* From hereon, we need to clean up if anything goes wrong. + * Fortunately async_data's destroy fn will clean up anything stored inside of it. */ + + async_data->py_signer = py_signer; + Py_INCREF(async_data->py_signer); + + async_data->py_http_request = py_http_request; + Py_INCREF(async_data->py_http_request); + + async_data->http_request = http_request; + + async_data->py_signing_config = py_signing_config; + Py_INCREF(async_data->py_signing_config); + + async_data->py_on_complete = py_on_complete; + Py_INCREF(async_data->py_on_complete); + + async_data->signable = aws_signable_new_http_request(aws_py_get_allocator(), http_request); + if (!async_data->signable) { + goto error; + } + + if (aws_signer_sign_request( + signer, + async_data->signable, + (struct aws_signing_config_base *)signing_config, + s_signing_complete, + async_data)) { + PyErr_SetAwsLastError(); + goto error; + } + + Py_RETURN_NONE; + +error: + s_async_signing_data_destroy(async_data); + return NULL; +} diff --git a/source/auth_signing_config.c b/source/auth_signing_config.c new file mode 100644 index 000000000..b9f859305 --- /dev/null +++ b/source/auth_signing_config.c @@ -0,0 +1,292 @@ +/* + * Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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. + */ + +#include "auth.h" + +#include + +static const char *s_capsule_name_signing_config = "aws_signing_config_aws"; + +/** + * Bind a Python AwsSigningConfig to a native aws_signing_config_aws. + */ +struct config_binding { + struct aws_signing_config_aws native; + + struct aws_byte_buf string_storage; + + /** + * Python objects that must outlive this. + * These all wrap values referenced from the native aws_signing_config_aws. + * For example, the python AwsCredentialsProvider whose native resource is referenced by + * native.credentials_provider. These values are never NULL (unless construction failed), they are Py_None if they + * are not valid. + */ + + PyObject *py_credentials_provider; + PyObject *py_date; /* Store original value so that user doesn't see different timezone after set/get */ + PyObject *py_should_sign_param_fn; +}; + +static void s_signing_config_capsule_destructor(PyObject *py_capsule) { + struct config_binding *binding = PyCapsule_GetPointer(py_capsule, s_capsule_name_signing_config); + + aws_byte_buf_clean_up(&binding->string_storage); + + Py_XDECREF(binding->py_credentials_provider); + Py_XDECREF(binding->py_should_sign_param_fn); + Py_XDECREF(binding->py_date); +} + +static bool s_should_sign_param(const struct aws_byte_cursor *name, void *userdata) { + bool should_sign = true; + struct config_binding *binding = userdata; + AWS_FATAL_ASSERT(binding->py_should_sign_param_fn != Py_None); + + /*************** GIL ACQUIRE ***************/ + PyGILState_STATE state = PyGILState_Ensure(); + + PyObject *py_result = PyObject_CallFunction(binding->py_should_sign_param_fn, "(s#)", name->ptr, name->len); + if (py_result) { + should_sign = PyObject_IsTrue(py_result); + Py_DECREF(py_result); + } else { + PyErr_WriteUnraisable(PyErr_Occurred()); + } + + PyGILState_Release(state); + /*************** GIL RELEASE ***************/ + + return should_sign; +} + +PyObject *aws_py_signing_config_new(PyObject *self, PyObject *args) { + (void)self; + + int algorithm; + PyObject *py_credentials_provider; + struct aws_byte_cursor region; + struct aws_byte_cursor service; + PyObject *py_date; + double timestamp; + PyObject *py_should_sign_param_fn; + PyObject *py_use_double_uri_encode; + PyObject *py_should_normalize_uri_path; + PyObject *py_sign_body; + if (!PyArg_ParseTuple( + args, + "iOs#s#OdOOOO", + &algorithm, + &py_credentials_provider, + ®ion.ptr, + ®ion.len, + &service.ptr, + &service.len, + &py_date, + ×tamp, + &py_should_sign_param_fn, + &py_use_double_uri_encode, + &py_should_normalize_uri_path, + &py_sign_body)) { + + return NULL; + } + + struct config_binding *binding = aws_mem_calloc(aws_py_get_allocator(), 1, sizeof(struct config_binding)); + if (!binding) { + return PyErr_AwsLastError(); + } + + /* From hereon, we need to clean up if errors occur. + * Fortunately, the capsule destructor will clean up anything stored inside the binding */ + + PyObject *py_capsule = PyCapsule_New(binding, s_capsule_name_signing_config, s_signing_config_capsule_destructor); + if (!py_capsule) { + aws_mem_release(aws_py_get_allocator(), binding); + return NULL; + } + + /* set primitive types */ + binding->native.config_type = AWS_SIGNING_CONFIG_AWS; + binding->native.algorithm = algorithm; + binding->native.use_double_uri_encode = PyObject_IsTrue(py_use_double_uri_encode); + binding->native.should_normalize_uri_path = PyObject_IsTrue(py_should_normalize_uri_path); + binding->native.sign_body = PyObject_IsTrue(py_sign_body); + + /* credentials_provider */ + binding->native.credentials_provider = aws_py_get_credentials_provider(py_credentials_provider); + if (!binding->native.credentials_provider) { + goto error; + } + binding->py_credentials_provider = py_credentials_provider; + Py_INCREF(binding->py_credentials_provider); + + /* strings: service, region */ + size_t total_string_len; + if (aws_add_size_checked(region.len, service.len, &total_string_len)) { + PyErr_SetAwsLastError(); + goto error; + } + + if (aws_byte_buf_init(&binding->string_storage, aws_py_get_allocator(), total_string_len)) { + PyErr_SetAwsLastError(); + goto error; + } + + binding->native.region.ptr = binding->string_storage.buffer + binding->string_storage.len; + binding->native.region.len = region.len; + aws_byte_buf_write_from_whole_cursor(&binding->string_storage, region); + + binding->native.service.ptr = binding->string_storage.buffer + binding->string_storage.len; + binding->native.service.len = service.len; + aws_byte_buf_write_from_whole_cursor(&binding->string_storage, service); + + /* date: store original datetime python object so user doesn't see different timezones after set/get */ + aws_date_time_init_epoch_secs(&binding->native.date, timestamp); + binding->py_date = py_date; + Py_INCREF(binding->py_date); + + /* should_sign_param */ + if (py_should_sign_param_fn == Py_None) { + binding->native.should_sign_param = NULL; + binding->native.should_sign_param_ud = NULL; + } else { + binding->native.should_sign_param = s_should_sign_param; + binding->native.should_sign_param_ud = binding; + } + binding->py_should_sign_param_fn = py_should_sign_param_fn; + Py_INCREF(binding->py_should_sign_param_fn); + + /* success! */ + return py_capsule; + +error: + Py_DECREF(py_capsule); + return NULL; +} + +struct aws_signing_config_aws *aws_py_get_signing_config(PyObject *py_signing_config) { + struct aws_signing_config_aws *native = NULL; + + PyObject *py_capsule = PyObject_GetAttrString(py_signing_config, "_binding"); + if (py_capsule) { + struct config_binding *binding = PyCapsule_GetPointer(py_capsule, s_capsule_name_signing_config); + if (binding) { + native = &binding->native; + AWS_FATAL_ASSERT(native); + } + Py_DECREF(py_capsule); + } + + return native; +} + +/** + * Common start to every getter. Parse arguments and return binding. + */ +static struct config_binding *s_common_get(PyObject *self, PyObject *args) { + (void)self; + PyObject *py_capsule; + if (!PyArg_ParseTuple(args, "O", &py_capsule)) { + return NULL; + } + + struct config_binding *binding = PyCapsule_GetPointer(py_capsule, s_capsule_name_signing_config); + return binding; +} + +PyObject *aws_py_signing_config_get_algorithm(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + return PyLong_FromLong(binding->native.algorithm); +} + +PyObject *aws_py_signing_config_get_credentials_provider(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + Py_INCREF(binding->py_credentials_provider); + return binding->py_credentials_provider; +} + +PyObject *aws_py_signing_config_get_region(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + return PyString_FromAwsByteCursor(&binding->native.region); +} + +PyObject *aws_py_signing_config_get_service(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + return PyString_FromAwsByteCursor(&binding->native.service); +} + +PyObject *aws_py_signing_config_get_date(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + Py_INCREF(binding->py_date); + return binding->py_date; +} + +PyObject *aws_py_signing_config_get_should_sign_param(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + Py_INCREF(binding->py_should_sign_param_fn); + return binding->py_should_sign_param_fn; +} + +PyObject *aws_py_signing_config_get_use_double_uri_encode(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + return PyBool_FromLong(binding->native.use_double_uri_encode); +} + +PyObject *aws_py_signing_config_get_should_normalize_uri_path(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + return PyBool_FromLong(binding->native.should_normalize_uri_path); +} + +PyObject *aws_py_signing_config_get_sign_body(PyObject *self, PyObject *args) { + struct config_binding *binding = s_common_get(self, args); + if (!binding) { + return NULL; + } + + return PyBool_FromLong(binding->native.sign_body); +} diff --git a/source/module.c b/source/module.c index 94e32b634..817090382 100644 --- a/source/module.c +++ b/source/module.c @@ -309,6 +309,18 @@ static PyMethodDef s_module_methods[] = { AWS_PY_METHOD_DEF(credentials_provider_shutdown, METH_VARARGS), AWS_PY_METHOD_DEF(credentials_provider_new_chain_default, METH_VARARGS), AWS_PY_METHOD_DEF(credentials_provider_new_static, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_new, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_algorithm, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_credentials_provider, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_region, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_service, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_date, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_should_sign_param, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_use_double_uri_encode, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_should_normalize_uri_path, METH_VARARGS), + AWS_PY_METHOD_DEF(signing_config_get_sign_body, METH_VARARGS), + AWS_PY_METHOD_DEF(signer_new_aws, METH_VARARGS), + AWS_PY_METHOD_DEF(signer_sign_request, METH_VARARGS), {NULL, NULL, 0, NULL}, }; diff --git a/test/test_auth.py b/test/test_auth.py new file mode 100644 index 000000000..6e360ac04 --- /dev/null +++ b/test/test_auth.py @@ -0,0 +1,252 @@ +# Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0 +# +# or in the "license" file accompanying this file. This file 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. + +from __future__ import absolute_import +import awscrt.auth +import awscrt.io +import datetime +import os +from test import NativeResourceTest + +EXAMPLE_ACCESS_KEY_ID = 'example_access_key_id' +EXAMPLE_SECRET_ACCESS_KEY = 'example_secret_access_key' +EXAMPLE_SESSION_TOKEN = 'example_session_token' + + +class ScopedEnvironmentVariable(object): + """ + Set environment variable for lifetime of this object. + """ + + def __init__(self, key, value): + self.key = key + self.prev_value = os.environ.get(key) + os.environ[key] = value + + def __del__(self): + if self.prev_value is None: + del os.environ[self.key] + else: + os.environ[self.key] = self.prev_value + + +class TestCredentials(NativeResourceTest): + def test_create(self): + credentials = awscrt.auth.AwsCredentials( + EXAMPLE_ACCESS_KEY_ID, + EXAMPLE_SECRET_ACCESS_KEY, + EXAMPLE_SESSION_TOKEN) + self.assertEqual(EXAMPLE_ACCESS_KEY_ID, credentials.access_key_id) + self.assertEqual(EXAMPLE_SECRET_ACCESS_KEY, credentials.secret_access_key) + self.assertEqual(EXAMPLE_SESSION_TOKEN, credentials.session_token) + + def test_create_no_session_token(self): + credentials = awscrt.auth.AwsCredentials(EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY) + self.assertEqual(EXAMPLE_ACCESS_KEY_ID, credentials.access_key_id) + self.assertEqual(EXAMPLE_SECRET_ACCESS_KEY, credentials.secret_access_key) + self.assertIsNone(credentials.session_token) + + +class TestProvider(NativeResourceTest): + def test_static_provider(self): + provider = awscrt.auth.AwsCredentialsProvider.new_static( + EXAMPLE_ACCESS_KEY_ID, + EXAMPLE_SECRET_ACCESS_KEY, + EXAMPLE_SESSION_TOKEN) + + future = provider.get_credentials() + credentials = future.result() + + self.assertEqual(EXAMPLE_ACCESS_KEY_ID, credentials.access_key_id) + self.assertEqual(EXAMPLE_SECRET_ACCESS_KEY, credentials.secret_access_key) + self.assertEqual(EXAMPLE_SESSION_TOKEN, credentials.session_token) + + # TODO: test currently broken because None session_token comes back as empty string do to inconsistent use of + # aws_byte_cursor by value/pointer in aws-c-auth APIs. + # + # def test_static_provider_no_session_token(self): + # provider = AwsCredentialsProvider.new_static( + # self.example_access_key_id, + # self.example_secret_access_key) + + # future = provider.get_credentials() + # credentials = future.result() + + # self.assertEqual(self.example_access_key_id, credentials.access_key_id) + # self.assertEqual(self.example_secret_access_key, credentials.secret_access_key) + # self.assertIsNone(credentials.session_token) + + def test_default_provider(self): + # Use environment variable to force specific credentials file + scoped_env = ScopedEnvironmentVariable('AWS_SHARED_CREDENTIALS_FILE', 'test/resources/credentials_test') + + event_loop_group = awscrt.io.EventLoopGroup() + bootstrap = awscrt.io.ClientBootstrap(event_loop_group) + provider = awscrt.auth.AwsCredentialsProvider.new_default_chain(bootstrap) + + future = provider.get_credentials() + credentials = future.result() + + self.assertEqual('credentials_test_access_key_id', credentials.access_key_id) + self.assertEqual('credentials_test_secret_access_key', credentials.secret_access_key) + self.assertIsNone(credentials.session_token) + + del scoped_env + + +class TestSigningConfig(NativeResourceTest): + def test_create(self): + algorithm = awscrt.auth.AwsSigningAlgorithm.SigV4QueryParam + credentials_provider = awscrt.auth.AwsCredentialsProvider.new_static( + EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY) + region = 'us-west-2' + service = 'aws-suborbital-ion-cannon' + date = datetime.datetime(year=2000, month=1, day=1) + + def should_sign_param(name): + return not name.tolower().startswith('x-do-not-sign') + + use_double_uri_encode = True + should_normalize_uri_path = False + sign_body = False + + cfg = awscrt.auth.AwsSigningConfig(algorithm=algorithm, + credentials_provider=credentials_provider, + region=region, + service=service, + date=date, + should_sign_param=should_sign_param, + use_double_uri_encode=use_double_uri_encode, + should_normalize_uri_path=should_normalize_uri_path, + sign_body=sign_body) + + self.assertIs(algorithm, cfg.algorithm) # assert IS enum, not just EQUAL + self.assertIs(credentials_provider, cfg.credentials_provider) + self.assertEqual(region, cfg.region) + self.assertEqual(service, cfg.service) + self.assertEqual(date, cfg.date) + self.assertIs(should_sign_param, cfg.should_sign_param) + self.assertEqual(use_double_uri_encode, cfg.use_double_uri_encode) + self.assertEqual(should_normalize_uri_path, cfg.should_normalize_uri_path) + self.assertEqual(sign_body, cfg.sign_body) + + def test_replace(self): + credentials_provider = awscrt.auth.AwsCredentialsProvider.new_static( + EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY) + + # nondefault values, to be sure they're carried over correctly + orig_cfg = awscrt.auth.AwsSigningConfig(algorithm=awscrt.auth.AwsSigningAlgorithm.SigV4QueryParam, + credentials_provider=credentials_provider, + region='us-west-1', + service='aws-suborbital-ion-cannon', + date=datetime.datetime(year=2000, month=1, day=1), + should_sign_param=lambda x: False, + use_double_uri_encode=True, + should_normalize_uri_path=False, + sign_body=False) + + # Call replace on single attribute, then assert that ONLY the one attribute differs + def _replace_attr(name, value): + new_cfg = orig_cfg.replace(**{name: value}) + self.assertIsNot(orig_cfg, new_cfg) # must return new object + + self.assertEqual(value, getattr(new_cfg, name)) # must replace specified value + + # check that only the one attribute differs + for attr in awscrt.auth.AwsSigningConfig._attributes: + if attr == name: + self.assertNotEqual(getattr(orig_cfg, attr), getattr(new_cfg, attr), + "replaced value should not match original") + else: + self.assertEqual(getattr(orig_cfg, attr), getattr(new_cfg, attr), + "value should match original") + + _replace_attr('algorithm', awscrt.auth.AwsSigningAlgorithm.SigV4Header) + _replace_attr('credentials_provider', + awscrt.auth.AwsCredentialsProvider.new_static(EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY)) + _replace_attr('region', 'us-west-2') + _replace_attr('service', 'aws-nothing-but-bees') + _replace_attr('date', datetime.datetime(year=2001, month=1, day=1)) + _replace_attr('should_sign_param', lambda x: True) + _replace_attr('use_double_uri_encode', False) + _replace_attr('should_normalize_uri_path', True) + _replace_attr('sign_body', True) + + # check that we can replace multiple values at once + new_cfg = orig_cfg.replace(region='us-west-3', service='aws-slow-blinking') + self.assertEqual('us-west-3', new_cfg.region) + self.assertEqual('aws-slow-blinking', new_cfg.service) + self.assertEqual(orig_cfg.should_sign_param, new_cfg.should_sign_param) + + +# Test values copied from aws-c-auth/tests/aws-sig-v4-test-suite/get-vanilla" +SIGV4TEST_ACCESS_KEY_ID = 'AKIDEXAMPLE' +SIGV4TEST_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' +SIGV4TEST_SESSION_TOKEN = None +SIGV4TEST_SERVICE = 'service' +SIGV4TEST_REGION = 'us-east-1' +SIGV4TEST_METHOD = 'GET' +SIGV4TEST_PATH = '/' +SIGV4TEST_DATE = datetime.datetime(year=2015, month=8, day=30, hour=12, minute=36, second=0, tzinfo=awscrt.auth._utc) +SIGV4TEST_UNSIGNED_HEADERS = [ + ('Host', 'example.amazonaws.com'), +] +SIGV4TEST_SIGNED_HEADERS = [ + ('Host', + 'example.amazonaws.com'), + ('X-Amz-Date', + '20150830T123600Z'), + ('Authorization', + 'AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31')] + + +class TestSigner(NativeResourceTest): + + def test_create(self): + signer = awscrt.auth.AwsSigner() + + def test_signing_sigv4_headers(self): + signer = awscrt.auth.AwsSigner() + + credentials_provider = awscrt.auth.AwsCredentialsProvider.new_static( + SIGV4TEST_ACCESS_KEY_ID, SIGV4TEST_SECRET_ACCESS_KEY, SIGV4TEST_SESSION_TOKEN) + + signing_config = awscrt.auth.AwsSigningConfig( + algorithm=awscrt.auth.AwsSigningAlgorithm.SigV4Header, + credentials_provider=credentials_provider, + region=SIGV4TEST_REGION, + service=SIGV4TEST_SERVICE, + date=SIGV4TEST_DATE, + sign_body=False) + + http_request = awscrt.http.HttpRequest( + method=SIGV4TEST_METHOD, + path=SIGV4TEST_PATH, + headers=SIGV4TEST_UNSIGNED_HEADERS) + + signing_future = signer.sign(http_request, signing_config) + + signing_result = signing_future.result(10) + + self.assertIs(http_request, signing_result) # should be same object + + self.assertEqual(SIGV4TEST_METHOD, http_request.method) + self.assertEqual(SIGV4TEST_PATH, http_request.path) + + # existing headers should remain + for prev_header in SIGV4TEST_UNSIGNED_HEADERS: + self.assertIn(prev_header, http_request.headers) + + # signed headers must be present + for signed_header in SIGV4TEST_SIGNED_HEADERS: + self.assertIn(signed_header, http_request.headers) diff --git a/test/test_credentials.py b/test/test_credentials.py deleted file mode 100644 index 4a41dea3c..000000000 --- a/test/test_credentials.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"). -# You may not use this file except in compliance with the License. -# A copy of the License is located at -# -# http://aws.amazon.com/apache2.0 -# -# or in the "license" file accompanying this file. This file 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. - -from __future__ import absolute_import -from awscrt.auth import Credentials, DefaultCredentialsProviderChain, StaticCredentialsProvider -from awscrt.io import ClientBootstrap, EventLoopGroup -import os -from test import NativeResourceTest - -EXAMPLE_ACCESS_KEY_ID = 'example_access_key_id' -EXAMPLE_SECRET_ACCESS_KEY = 'example_secret_access_key' -EXAMPLE_SESSION_TOKEN = 'example_session_token' - - -class ScopedEnvironmentVariable(object): - """ - Set environment variable for lifetime of this object. - """ - - def __init__(self, key, value): - self.key = key - self.prev_value = os.environ.get(key) - os.environ[key] = value - - def __del__(self): - if self.prev_value is None: - del os.environ[self.key] - else: - os.environ[self.key] = self.prev_value - - -class TestCredentials(NativeResourceTest): - def test_create(self): - credentials = Credentials(EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY, EXAMPLE_SESSION_TOKEN) - self.assertEqual(EXAMPLE_ACCESS_KEY_ID, credentials.access_key_id) - self.assertEqual(EXAMPLE_SECRET_ACCESS_KEY, credentials.secret_access_key) - self.assertEqual(EXAMPLE_SESSION_TOKEN, credentials.session_token) - - def test_create_no_session_token(self): - credentials = Credentials(EXAMPLE_ACCESS_KEY_ID, EXAMPLE_SECRET_ACCESS_KEY) - self.assertEqual(EXAMPLE_ACCESS_KEY_ID, credentials.access_key_id) - self.assertEqual(EXAMPLE_SECRET_ACCESS_KEY, credentials.secret_access_key) - self.assertIsNone(credentials.session_token) - - -class TestProvider(NativeResourceTest): - def test_static_provider(self): - provider = StaticCredentialsProvider( - EXAMPLE_ACCESS_KEY_ID, - EXAMPLE_SECRET_ACCESS_KEY, - EXAMPLE_SESSION_TOKEN) - - future = provider.get_credentials() - credentials = future.result() - - self.assertEqual(EXAMPLE_ACCESS_KEY_ID, credentials.access_key_id) - self.assertEqual(EXAMPLE_SECRET_ACCESS_KEY, credentials.secret_access_key) - self.assertEqual(EXAMPLE_SESSION_TOKEN, credentials.session_token) - - # TODO: test currently broken because None session_token comes back as empty string do to inconsistent use of - # aws_byte_cursor by value/pointer in aws-c-auth APIs. - # - # def test_static_provider_no_session_token(self): - # provider = StaticCredentialsProvider( - # self.example_access_key_id, - # self.example_secret_access_key) - - # future = provider.get_credentials() - # credentials = future.result() - - # self.assertEqual(self.example_access_key_id, credentials.access_key_id) - # self.assertEqual(self.example_secret_access_key, credentials.secret_access_key) - # self.assertIsNone(credentials.session_token) - - def test_default_provider(self): - # Use environment variable to force specific credentials file - scoped_env = ScopedEnvironmentVariable('AWS_SHARED_CREDENTIALS_FILE', 'test/resources/credentials_test') - - event_loop_group = EventLoopGroup() - bootstrap = ClientBootstrap(event_loop_group) - provider = DefaultCredentialsProviderChain(bootstrap) - - future = provider.get_credentials() - credentials = future.result() - - self.assertEqual('credentials_test_access_key_id', credentials.access_key_id) - self.assertEqual('credentials_test_secret_access_key', credentials.secret_access_key) - self.assertIsNone(credentials.session_token) - - del scoped_env