%% @doc This module contains functions for signing requests to AWS services.
-module(aws_signature).
-export([sign_v4/9, sign_v4/10, sign_v4_event/7, sign_v4_query_params/7, sign_v4_query_params/8]).
-export([sign_v4a/10]).
-type header() :: {binary(), binary()}.
-type headers() :: [header()].
-type query_param() :: {binary(), binary()}.
-type query_params() :: [query_param()].
%% @doc Implements the <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html">Asymmetric Signature Version 4 (SigV4a)</a> algorithm.
%%
%% This function takes AWS client credentials and request details,
%% based on which it computes the signature and returns headers
%% extended with the authorization entries.
%%
%% `URL' must be valid, with all components properly escaped.
%% For example, "https://example.com/path%20to" is valid, whereas
%% "https://example.com/path to" is not.
%%
%% It is essential that the provided request details are final
%% and the returned headers are used to make the request. All
%% custom headers need to be assembled before the signature is
%% calculated.
%%
%% The following options are supported:
%%
%% <dl>
%% <dt>`add_payload_hash_header'</dt>
%% <dd>
%% When `true' adds the `X-Amz-Content-Sha256' header to signed requests.
%% Amazon S3 is an example of a service that requires this setting.
%% Defaults to `false'.
%% </dd>
%% <dt>`disable_implicit_payload_hashing'</dt>
%% <dd>
%% When `true' use the "UNSIGNED-PAYLOAD" sentinel instead of computing
%% SHA256 digest of the payload. Defaults to `false'.
%% </dd>
%% </dl>
-spec sign_v4a(binary(), binary(), binary(), [binary()], binary(),
binary(), binary(), headers(), binary(), map())
-> {ok, headers()} | {error, any()}.
sign_v4a(AccessKeyID, SecretAccessKey, SessionToken, Regions,
Service, Method, URL, Headers, Body, Options) ->
aws_sigv4a:sign_request(AccessKeyID, SecretAccessKey, SessionToken, Regions,
Service, Method, URL, Headers, Body, Options).
%% @doc Same as {@link sign_v4/10} with no options.
sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body) ->
sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, []).
%% @doc Implements the <a href="https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">Signature Version 4 (SigV4)</a> algorithm.
%%
%% This function takes AWS client credentials and request details,
%% based on which it computes the signature and returns headers
%% extended with the authorization entries.
%%
%% `DateTime' is a datetime tuple used as the request date.
%% You most likely want to set it to the value of `calendar:universal_time()'
%% when making the request.
%%
%% `URL' must be valid, with all components properly escaped.
%% For example, "https://example.com/path%20to" is valid, whereas
%% "https://example.com/path to" is not.
%%
%% It is essential that the provided request details are final
%% and the returned headers are used to make the request. All
%% custom headers need to be assembled before the signature is
%% calculated.
%%
%% The signature is computed by normalizing request details into
%% a well defined format and combining it with the credentials
%% using a number of cryptographic functions. Upon receiving
%% a request, the server calculates the signature using the same
%% algorithm and compares it with the value received in headers.
%% For more details check out the <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html">AWS documentation</a>.
%%
%% The following options are supported:
%%
%% <dl>
%% <dt>`uri_encode_path'</dt>
%% <dd>
%% When `true', the request URI path is URI-encoded during request
%% canonicalization, <strong>which is required for every service except S3</strong>.
%% Note that the given URL should already be properly encoded, so
%% this results in each segment being URI-encoded twice, as expected
%% by AWS. Defaults to `true'.
%% </dd>
%% <dt>`body_digest'</dt>
%% <dd>
%% Optional SHA256 digest of the request body. This option can be used to provide
%% a fixed digest value, such as "UNSIGNED-PAYLOAD", when sending requests without
%% signing the body.
%% </dd>
%% </dl>
-spec sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, Options) -> FinalHeaders
when AccessKeyID :: binary(),
SecretAccessKey :: binary(),
Region :: binary(),
Service :: binary(),
DateTime :: calendar:datetime(),
Method :: binary(),
URL :: binary(),
Headers :: headers(),
Body :: binary(),
Options :: [Option],
Option ::
{uri_encode_path, boolean()}
| {body_digest, binary()},
FinalHeaders :: headers().
sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, Options)
when is_binary(AccessKeyID),
is_binary(SecretAccessKey),
is_binary(Region),
is_binary(Service),
is_tuple(DateTime),
is_binary(Method),
is_binary(URL),
is_list(Headers),
is_binary(Body),
is_list(Options) ->
URIEncodePath = proplists:get_value(uri_encode_path, Options, true),
URLMap = aws_signature_utils:parse_url(URL),
LongDate = format_datetime_long(DateTime),
ShortDate = format_datetime_short(DateTime),
FinalHeaders0 = add_date_header(Headers, LongDate),
BodyDigest =
case proplists:get_value(body_digest, Options, undefined) of
undefined ->
aws_signature_utils:sha256_hexdigest(Body);
Digest ->
Digest
end,
FinalHeaders = add_content_hash_header(FinalHeaders0, BodyDigest),
CanonicalRequest = canonical_request(Method, URLMap, FinalHeaders, BodyDigest, URIEncodePath),
HashedCanonicalRequest = aws_signature_utils:sha256_hexdigest(CanonicalRequest),
CredentialScope = credential_scope(ShortDate, Region, Service),
SigningKey = signing_key(SecretAccessKey, ShortDate, Region, Service),
StringToSign = string_to_sign(LongDate, CredentialScope, HashedCanonicalRequest),
Signature = aws_signature_utils:hmac_sha256_hexdigest(SigningKey, StringToSign),
SignedHeaders = signed_headers(FinalHeaders),
Authorization = authorization(AccessKeyID, CredentialScope, SignedHeaders, Signature),
add_authorization_header(FinalHeaders, Authorization).
%% @doc Signs an AWS Event Stream message and returns the headers and
%% signature used for next event signing.
%%
%% Headers of a sigv4 signed event message only contains 2 headers
%% <dl>
%% <dt>`:chunk-signature'</dt>
%% <dd>
%% computed signature of the event, binary string, `bytes' type
%% </dd>
%% <dt>`:date'</dt>
%% <dd>
%% millisecond since epoch, `timestamp' type
%% </dd>
%% </dl>
%%
%% `PriorSignature' for the first message is the base16 encoded signv4
%% of the request used to open a connection with the target service.
%%
%% `HeadersString' are the headers of the inner packet, encoded using the
%% EventStream format.
-spec sign_v4_event(SecretAccessKey, Region, Service, DateTime, PriorSignature, HeaderString, Body) -> {Headers, Signature}
when SecretAccessKey :: binary(),
Region :: binary(),
Service :: binary(),
DateTime :: calendar:datetime(),
PriorSignature :: binary(),
HeaderString :: binary(),
Body :: binary(),
Headers :: [{binary(), binary(), atom()}],
Signature :: binary().
sign_v4_event(SecretAccessKey, Region, Service, DateTime, PriorSignature, HeaderString, Body)
when is_binary(SecretAccessKey),
is_binary(Region),
is_binary(Service),
is_tuple(DateTime),
is_binary(PriorSignature),
is_binary(HeaderString),
is_binary(Body) ->
LongDate = format_datetime_long(DateTime),
ShortDate = format_datetime_short(DateTime),
Keypath = credential_scope(ShortDate, Region, Service),
HeaderDigest = aws_signature_utils:sha256_hexdigest(HeaderString),
BodyDigest = aws_signature_utils:sha256_hexdigest(Body),
SigningKey = signing_key(SecretAccessKey, ShortDate, Region, Service),
StringToSign =
string_to_sign_for_event(LongDate, Keypath, PriorSignature, HeaderDigest, BodyDigest),
Signature = aws_signature_utils:hmac_sha256(SigningKey, StringToSign),
EventHeaders = [
{<<":date">>, DateTime, timestamp},
{<<":chunk-signature">>, Signature, byte_array}
],
{EventHeaders, aws_signature_utils:base16(Signature)}.
%% @doc Same as {@link sign_v4_query_params/7} with no options.
sign_v4_query_params(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL) ->
sign_v4_query_params(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, []).
%% @doc Implements the <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html">Signature Version 4 (SigV4)</a> algorithm for query parameters.
%%
%% This function takes AWS client credentials and request details,
%% based on which it computes the signature and returns the URL
%% extended with the signature entries. Note that anchors are ignored
%% in the resulting URL.
%%
%% `DateTime' is a datetime tuple used as the request date.
%% You most likely want to set it to the value of `calendar:universal_time()'
%% when making the request.
%%
%% `URL' must be valid, with all components properly escaped.
%% For example, "https://example.com/path%20to" is valid, whereas
%% "https://example.com/path to" is not.
%%
%% It is essential that the provided request details are final
%% and the returned query params are used to make the request with
%% the provided URL.
%%
%% The signature is computed by normalizing request details into
%% a well defined format and combining it with the credentials
%% using a number of cryptographic functions. Upon receiving
%% a request, the server calculates the signature using the same
%% algorithm and compares it with the value received in headers.
%% For more details check out the <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html">AWS documentation</a>.
%%
%% The following options are supported:
%%
%% <dl>
%% <dt>`ttl'</dt>
%% <dd>
%% Time-to-live value that tells how long this URL is valid in seconds.
%% Defaults to `86400', which means one day.
%% </dd>
%% <dt>`uri_encode_path'</dt>
%% <dd>
%% When `true', the request URI path is URI-encoded during request
%% canonicalization, <strong>which is required for every service except S3</strong>.
%% Note that the given URL should already be properly encoded, so
%% this results in each segment being URI-encoded twice, as expected
%% by AWS. Defaults to `true'.
%% </dd>
%% <dt>`session_token'</dt>
%% <dd>
%% Optional credential parameter if using credentials sourced from the STS service.
%% </dd>
%% <dt>`body'</dt>
%% <dd>
%% Request body to compute SHA256 digest for. Defaults to an empty binary. Note that
%% `body_digest' always takes precedence when set.
%% </dd>
%% <dt>`body_digest'</dt>
%% <dd>
%% Optional SHA256 digest of the request body. This option can be used to provide
%% a fixed digest value, such as "UNSIGNED-PAYLOAD", when sending requests without
%% signing the body, <strong>which is expected for S3</strong>.
%% </dd>
%% <dt>`tags'</dt>
%% <dd>
%% Optional tagging of the object when generating a pre-signed URL.
%% The value of `tags' is a binary() in the format, for example:
%% `<<"key1=value1&key2=value2">>'. The actual request to put or get the object
%% must use the exact `tags' value to ensure the signature is calculated
%% correctly.
%% </dd>
%% </dl>
-spec sign_v4_query_params(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Options) -> FinalURL
when AccessKeyID :: binary(),
SecretAccessKey :: binary(),
Region :: binary(),
Service :: binary(),
DateTime :: calendar:datetime(),
Method :: binary(),
URL :: binary(),
Options :: [Option],
Option ::
{uri_encode_path, boolean()}
| {session_token, binary()}
| {ttl, non_neg_integer()}
| {body, binary()}
| {body_digest, binary()}
| {tags, binary()},
FinalURL :: binary().
sign_v4_query_params(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Options)
when is_binary(AccessKeyID),
is_binary(SecretAccessKey),
is_binary(Region),
is_binary(Service),
is_tuple(DateTime),
is_binary(Method),
is_binary(URL),
is_list(Options) ->
URIEncodePath = proplists:get_value(uri_encode_path, Options, true),
TimeToLive = proplists:get_value(ttl, Options, 86400),
SessionToken = proplists:get_value(session_token, Options, undefined),
Tags = proplists:get_value(tags, Options, undefined),
BodyDigest =
case proplists:get_value(body_digest, Options, undefined) of
undefined ->
Body = proplists:get_value(body, Options, <<"">>),
aws_signature_utils:sha256_hexdigest(Body);
Digest ->
Digest
end,
BaseParams =
[{<<"X-Amz-Algorithm">>, <<"AWS4-HMAC-SHA256">>},
{<<"X-Amz-SignedHeaders">>,
if Tags == undefined ->
<<"host">>;
Tags =/= undefined ->
<<"host%3Bx-amz-tagging">>
end}
],
URLMap = aws_signature_utils:parse_url(URL),
LongDate = format_datetime_long(DateTime),
ShortDate = format_datetime_short(DateTime),
CredentialScope = credential_scope(ShortDate, Region, Service),
FinalQueryParams0 = add_ttl_query_param(BaseParams, TimeToLive),
FinalQueryParams1 =
add_credential_query_param(FinalQueryParams0, CredentialScope, AccessKeyID),
FinalQueryParams2 = maybe_add_session_token_query_param(FinalQueryParams1, SessionToken),
FinalQueryParams = add_date_header(FinalQueryParams2, LongDate),
HostHeader = host_header_from_url(URLMap),
Headers =
if Tags == undefined ->
[HostHeader];
Tags =/= undefined ->
[HostHeader, {<<"X-Amz-Tagging">>, Tags}]
end,
CanonicalRequest =
canonical_request(Method, URLMap, Headers, BodyDigest, URIEncodePath, FinalQueryParams),
HashedCanonicalRequest = aws_signature_utils:sha256_hexdigest(CanonicalRequest),
SigningKey = signing_key(SecretAccessKey, ShortDate, Region, Service),
StringToSign = string_to_sign(LongDate, CredentialScope, HashedCanonicalRequest),
Signature = aws_signature_utils:hmac_sha256_hexdigest(SigningKey, StringToSign),
build_final_url_with_signature(URL, URLMap, FinalQueryParams, Signature).
%% Formats the given datetime into YYMMDDTHHMMSSZ binary string.
-spec format_datetime_long(calendar:datetime()) -> binary().
format_datetime_long({{Y, Mo, D}, {H, Mn, S}}) ->
Date = format_date(Y, Mo, D),
Timestamp = format_timestamp(Date, H, Mn, S),
Timestamp.
format_date(Y, M0, D0) ->
M = maybe_add_padding(M0),
D = maybe_add_padding(D0),
<<(integer_to_binary(Y))/binary, M/binary, D/binary>>.
format_timestamp(Date, H0, Min0, S0) ->
H = maybe_add_padding(H0),
Min = maybe_add_padding(Min0),
S = maybe_add_padding(S0),
<<Date/binary, "T", H/binary, Min/binary, S/binary, "Z">>.
maybe_add_padding(X) when X < 10 ->
<<"0", (integer_to_binary(X))/binary>>;
maybe_add_padding(X) ->
integer_to_binary(X).
%% Formats the given datetime into YYMMDD binary string.
-spec format_datetime_short(calendar:datetime()) -> binary().
format_datetime_short({{Y, Mo, D}, _}) ->
format_date(Y, Mo, D).
-spec add_authorization_header(headers(), binary()) -> headers().
add_authorization_header(Headers, Authorization) ->
[{<<"Authorization">>, Authorization} | Headers].
add_date_header(Headers, LongDate) ->
[{<<"X-Amz-Date">>, LongDate} | Headers].
add_ttl_query_param(QueryParams, TimeToLive) ->
[{<<"X-Amz-Expires">>, integer_to_binary(TimeToLive)} | QueryParams].
add_credential_query_param(QueryParams, Scope, AccessKey) ->
EncodedScope = binary:split(Scope, <<"/">>, [global]),
[{<<"X-Amz-Credential">>,
aws_signature_utils:binary_join([AccessKey | EncodedScope], <<"%2F">>)}
| QueryParams].
host_header_from_url(URLMap) ->
#{host := Host} = URLMap,
{<<"Host">>, Host}.
maybe_add_session_token_query_param(QueryParams, undefined) ->
QueryParams;
maybe_add_session_token_query_param(QueryParams, SessionToken) ->
[{<<"X-Amz-Security-Token">>, SessionToken} | QueryParams].
sort_query_params_with_signature(QueryParams, Signature) ->
FinalQueryParams = [{<<"X-Amz-Signature">>, Signature} | QueryParams],
lists:sort(fun({K1, _}, {K2, _}) -> K1 =< K2 end, FinalQueryParams).
-spec build_final_url_with_signature(binary(), map(), query_params(), binary()) -> binary().
build_final_url_with_signature(OriginalURL, URLMap, QueryParams, Signature) ->
#{query := Query} = URLMap,
FinalQueryParams0 = query_entries(Query) ++ QueryParams,
FinalQueryParams = sort_query_params_with_signature(FinalQueryParams0, Signature),
aws_signature_utils:rebuilds_url_with_query_params(OriginalURL, FinalQueryParams).
%% Adds a X-Amz-Content-SHA256 header which is the hash of the payload.
%%
%% This header is required for S3 when using the v4 signature. Adding it
%% in requests for all services does not cause any issues.
-spec add_content_hash_header(headers(), binary()) -> headers().
add_content_hash_header(Headers, BodyDigest) ->
[{<<"X-Amz-Content-SHA256">>, BodyDigest} | Headers].
%% Generates an AWS4-HMAC-SHA256 authorization signature.
-spec authorization(binary(), binary(), binary(), binary()) -> binary().
authorization(AccessKeyID, CredentialScope, SignedHeaders, Signature) ->
<< "AWS4-HMAC-SHA256 ",
"Credential=", AccessKeyID/binary,
"/", CredentialScope/binary,
",SignedHeaders=", SignedHeaders/binary,
",Signature=", Signature/binary >>.
%% Generates a signing key from a secret access key, a short date in YYMMDD
%% format, a region identifier and a service identifier.
-spec signing_key(binary(), binary(), binary(), binary()) -> binary().
signing_key(SecretAccessKey, ShortDate, Region, Service) ->
SigningKey = << <<"AWS4">>/binary, SecretAccessKey/binary >>,
SignedDate = aws_signature_utils:hmac_sha256(SigningKey, ShortDate),
SignedRegion = aws_signature_utils:hmac_sha256(SignedDate, Region),
SignedService = aws_signature_utils:hmac_sha256(SignedRegion, Service),
aws_signature_utils:hmac_sha256(SignedService, <<"aws4_request">>).
%% Generates a credential scope from a short date in YYMMDD format,
%% a region identifier and a service identifier.
-spec credential_scope(binary(), binary(), binary()) -> binary().
credential_scope(ShortDate, Region, Service) ->
aws_signature_utils:binary_join([ShortDate, Region, Service, <<"aws4_request">>],
<<"/">>).
%% Generates the text to sign from a long date in YYMMDDTHHMMSSZ format,
%% a credential scope and a hashed canonical request.
-spec string_to_sign(binary(), binary(), binary()) -> binary().
string_to_sign(LongDate, CredentialScope, HashedCanonicalRequest) ->
aws_signature_utils:binary_join([<<"AWS4-HMAC-SHA256">>,
LongDate,
CredentialScope,
HashedCanonicalRequest],
<<"\n">>).
-spec string_to_sign_for_event(binary(), binary(), binary(), binary(), binary()) -> binary().
string_to_sign_for_event(LongDate, Keypath, PriorSignature, HeaderDigest, PayloadDigest) ->
aws_signature_utils:binary_join([<<"AWS4-HMAC-SHA256-PAYLOAD">>,
LongDate,
Keypath,
PriorSignature,
HeaderDigest,
PayloadDigest],
<<"\n">>).
%% Processes and merges request values into a canonical request.
-spec canonical_request(binary(), map(), headers(), binary(), boolean()) -> binary().
canonical_request(Method, URL, Headers, Body, URIEncodePath) ->
canonical_request(Method, URL, Headers, Body, URIEncodePath, []).
-spec canonical_request(binary(),
map(),
headers(),
binary(),
boolean(),
query_params()) ->
binary().
canonical_request(Method, URLMap, Headers, BodyDigest, URIEncodePath, AdditionalQueryParams) ->
CanonicalMethod = canonical_method(Method),
#{path := Path, query := Query} = URLMap,
CanonicalURL = canonical_path(Path, URIEncodePath),
QueryEntries = query_entries(Query),
CanonicalQueryString = canonical_query(QueryEntries ++ AdditionalQueryParams),
CanonicalHeaders = canonical_headers(Headers),
SignedHeaders = signed_headers(Headers),
aws_signature_utils:binary_join([CanonicalMethod,
CanonicalURL,
CanonicalQueryString,
CanonicalHeaders,
SignedHeaders,
BodyDigest],
<<"\n">>).
%% Normalizes HTTP method name by uppercasing it.
-spec canonical_method(binary()) -> binary().
canonical_method(Method) ->
list_to_binary(string:to_upper(binary_to_list(Method))).
-spec canonical_path(binary(), boolean()) -> binary().
canonical_path(<<"">>, _URIEncodePath) ->
<<"/">>;
canonical_path(Path, true) ->
aws_signature_utils:uri_encode_path(Path);
canonical_path(Path, false) ->
Path.
%% Normalizes the given query string.
%%
%% Sorts query params by name first, then by value (if present).
%% Appends "=" to params with missing value.
%%
%% For example, "foo=bar&baz" becomes "baz=&foo=bar".
-spec canonical_query(query_params()) -> binary().
canonical_query([]) ->
<<"">>;
canonical_query(QueryParams) when is_list(QueryParams) ->
SortedParts = lists:sort(fun({K1, _}, {K2, _}) -> K1 =< K2 end, QueryParams),
NormalizedParts = lists:map(fun query_entry_to_string/1, SortedParts),
aws_signature_utils:binary_join(NormalizedParts, <<"&">>).
-spec query_entries(binary()) -> [{binary(), binary()}].
query_entries(<<"">>) -> [];
query_entries(Query) ->
Parts = binary:split(Query, <<"&">>, [global]),
SplittedParts = [binary:split(Part, <<"=">>) || Part <- Parts],
lists:map(fun query_entry_to_tuple/1, SplittedParts).
query_entry_to_tuple([Key]) ->
{Key, <<"">>};
query_entry_to_tuple([Key, Value]) ->
{Key, Value}.
-spec query_entry_to_string({binary(), binary()}) -> binary().
query_entry_to_string({K, V}) ->
<<K/binary, "=", V/binary>>.
%% Converts a list of headers to canonical header format.
%%
%% Leading and trailing whitespace around header names and values is
%% stripped, header names are lowercased, and headers are newline-joined
%% in alphabetical order (with a trailing newline).
-spec canonical_headers(headers()) -> binary().
canonical_headers(Headers) ->
CanonicalHeaders = lists:map(fun canonical_header/1, Headers),
SortedCanonicalHeaders =
lists:sort(fun({N1, _}, {N2, _}) -> N1 =< N2 end, CanonicalHeaders),
<< <<N/binary, ":", V/binary, "\n">> || {N, V} <- SortedCanonicalHeaders >>.
-spec canonical_header(header()) -> header().
canonical_header({Name, Value}) ->
N = list_to_binary(string:strip(
string:to_lower(binary_to_list(Name)))),
V = list_to_binary(string:strip(binary_to_list(Value))),
{N, V}.
%% Converts a list of headers to canonical signed header format.
%%
%% Leading and trailing whitespace around names is stripped, header names
%% are lowercased, and header names are semicolon-joined in alphabetical order.
-spec signed_headers(headers()) -> binary().
signed_headers(Headers) ->
aws_signature_utils:binary_join(
lists:sort(
lists:map(fun signed_header/1, Headers)),
<<";">>).
-spec signed_header(header()) -> binary().
signed_header({Name, _}) ->
list_to_binary(string:strip(
string:to_lower(binary_to_list(Name)))).
%%====================================================================
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
%% sign_v4/9 computes AWS Signature Version 4 and returns an updated list of headers
sign_v4_test() ->
AccessKeyID = <<"access-key-id">>,
SecretAccessKey = <<"secret-access-key">>,
Region = <<"us-east-1">>,
Service = <<"ec2">>,
DateTime = {{2015, 5, 14}, {16, 50, 5}},
Method = <<"GET">>,
URL = <<"https://ec2.us-east-1.amazonaws.com/?Action=DescribeInstances&Version=2014-10-01">>,
Headers = [{<<"Host">>, <<"ec2.us-east-1.amazonaws.com">>}, {<<"Header">>, <<"Value">>}],
Body = <<"">>,
Actual = sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body),
Expected = [
{<<"Authorization">>, <<"AWS4-HMAC-SHA256 Credential=access-key-id/20150514/us-east-1/ec2/aws4_request,SignedHeaders=header;host;x-amz-content-sha256;x-amz-date,Signature=595529f9989556c9ce375ddec1b3e63f9d551fe063738b45909c28b25a34a6cb">>},
{<<"X-Amz-Content-SHA256">>, <<"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855">>},
{<<"X-Amz-Date">>, <<"20150514T165005Z">>},
{<<"Host">>, <<"ec2.us-east-1.amazonaws.com">>},
{<<"Header">>, <<"Value">>}],
?assertEqual(Actual, Expected).
%% sign_v4/9 https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#example-signature-GET-object
sign_v4_reference_example_1_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Headers = [{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}, {<<"Range">>, <<"bytes=0-9">>}],
Body = <<"">>,
Actual = sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, [{uri_encode_path, false}]),
Expected = [
{<<"Authorization">>, <<"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41">>},
{<<"X-Amz-Content-SHA256">>, <<"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855">>},
{<<"X-Amz-Date">>, <<"20130524T000000Z">>},
{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>},
{<<"Range">>, <<"bytes=0-9">>}],
?assertEqual(Actual, Expected).
%% sign_v4/9 https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#example-signature-PUT-object
sign_v4_reference_example_2_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"PUT">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test%24file.text">>,
Headers = [
{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>},
{<<"Date">>, <<"Fri, 24 May 2013 00:00:00 GMT">>},
{<<"X-Amz-Storage-Class">>, <<"REDUCED_REDUNDANCY">>}],
Body = <<"Welcome to Amazon S3.">>,
Actual = sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, [{uri_encode_path, false}]),
Expected = [
{<<"Authorization">>, <<"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd">>},
{<<"X-Amz-Content-SHA256">>, <<"44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072">>},
{<<"X-Amz-Date">>, <<"20130524T000000Z">>},
{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>},
{<<"Date">>, <<"Fri, 24 May 2013 00:00:00 GMT">>},
{<<"X-Amz-Storage-Class">>, <<"REDUCED_REDUNDANCY">>}],
?assertEqual(Actual, Expected).
%% sign_v4/9 https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#example-signature-GET-bucket-lifecycle
sign_v4_reference_example_3_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com?lifecycle">>,
Headers = [{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}],
Body = <<"">>,
Actual = sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, [{uri_encode_path, false}]),
Expected = [
{<<"Authorization">>, <<"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543">>},
{<<"X-Amz-Content-SHA256">>, <<"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855">>},
{<<"X-Amz-Date">>, <<"20130524T000000Z">>},
{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}],
?assertEqual(Actual, Expected).
%% sign_v4/9 https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#example-signature-list-bucket
sign_v4_reference_example_4_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com?max-keys=2&prefix=J">>,
Headers = [{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}],
Body = <<"">>,
Actual = sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, [{uri_encode_path, false}]),
Expected = [
{<<"Authorization">>, <<"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7">>},
{<<"X-Amz-Content-SHA256">>, <<"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855">>},
{<<"X-Amz-Date">>, <<"20130524T000000Z">>},
{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}],
?assertEqual(Actual, Expected).
sign_v4_unsigned_payload_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com?max-keys=2&prefix=J">>,
Headers = [{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}],
Body = <<"foo">>,
Actual = sign_v4(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, Headers, Body, [{body_digest, <<"UNSIGNED-PAYLOAD">>}]),
Expected = [
{<<"Authorization">>, <<"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=b1a076428fa68c2c42202ee5a5718b8207f725e451e2157d6b1c393e01fc2e68">>},
{<<"X-Amz-Content-SHA256">>, <<"UNSIGNED-PAYLOAD">>},
{<<"X-Amz-Date">>, <<"20130524T000000Z">>},
{<<"Host">>, <<"examplebucket.s3.amazonaws.com">>}],
?assertEqual(Actual, Expected).
sign_v4_event_test() ->
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"transcribe">>,
DateTime = {{2023, 7, 31}, {11, 36, 12}},
PriorSignature = <<"ce2704cf5f348fd66f179d5883162f223c30b3fb8213fb1bc097bf2ecd34b1b5">>,
% EventStream encoded header of {":date", DateTime, :timestamp}
HeaderString = <<5, 58, 100, 97, 116, 101, 8, 0, 0, 1, 137, 171, 187, 255, 224>>,
{ActualHeaders, ActualSignature} = sign_v4_event(SecretAccessKey, Region, Service, DateTime, PriorSignature, HeaderString, <<>>),
ExpectedHeaders = [
{<<":date">>, DateTime, timestamp},
{<<":chunk-signature">>,<<41, 239, 130, 195, 152, 80, 171, 220, 198, 95, 157, 96, 70, 243, 228, 55, 227, 133, 17, 43, 128, 183, 241, 123, 49, 186, 51, 167, 218, 60, 200, 175>>, byte_array}
],
ExpectedSignature = <<"29ef82c39850abdcc65f9d6046f3e437e385112b80b7f17b31ba33a7da3cc8af">>,
?assertEqual(ActualHeaders, ExpectedHeaders),
?assertEqual(ActualSignature, ExpectedSignature).
%% canonical_headers/1 sorted headers by header name
canonical_headers_test() ->
Headers = [
{<<"User-Agent">>, <<"aws-sdk-ruby3/3.113.1 ruby/2.7.2 x86_64-linux aws-sdk-s3/1.93.0">>},
{<<"X-Amz-Server-Side-Encryption-Customer-Algorithm">>, <<"AES256">>},
{<<"X-Amz-Server-Side-Encryption-Customer-Key-Md5">>, <<"BaUscNABVnd0nRlQecUFPA==">>},
{<<"X-Amz-Server-Side-Encryption-Customer-Key">>, <<"TIjv09mJiv+331Evgfq8eONO2y/G4aztRqEeAwx9y2U=">>},
{<<"Content-Md5">>, <<"VDMfSlWzfS823+nFvkpWzg==">>},
{<<"Host">>, <<"aws-beam-projects-test.s3.amazonaws.com">>}],
Actual = canonical_headers(Headers),
Expected =
<<"content-md5:VDMfSlWzfS823+nFvkpWzg==\n",
"host:aws-beam-projects-test.s3.amazonaws.com\n",
"user-agent:aws-sdk-ruby3/3.113.1 ruby/2.7.2 x86_64-linux aws-sdk-s3/1.93.0\n",
"x-amz-server-side-encryption-customer-algorithm:AES256\n",
"x-amz-server-side-encryption-customer-key:TIjv09mJiv+331Evgfq8eONO2y/G4aztRqEeAwx9y2U=\n",
"x-amz-server-side-encryption-customer-key-md5:BaUscNABVnd0nRlQecUFPA==\n">>,
?assertEqual(Expected, Actual).
%% canonical_request/5 returns a connical request binary string
canonical_request_test() ->
Expected =
<<"GET", $\n,
"/pa%2520th", $\n,
"a=&b=1", $\n,
"host:example.com", $\n, "x-amz-date:20150325T105958Z", $\n, $\n,
"host;x-amz-date", $\n,
"content-sha256">>,
Actual = canonical_request(
<<"get">>,
#{path => <<"/pa%20th">>, query => <<"b=1&a=">>},
[{<<"Host">>, <<"example.com">>}, {<<"X-Amz-Date">>, <<"20150325T105958Z">>}],
<<"content-sha256">>,
true),
?assertEqual(Expected, Actual).
%% canonical_request/4 does not encode the path when disabled
canonical_request_with_encode_uri_path_false_test() ->
Expected =
<<"GET", $\n,
"/pa%20th", $\n,
"", $\n,
$\n,
$\n,
"content-sha256">>,
Actual =
canonical_request(<<"get">>, #{path => <<"/pa%20th">>, query => <<"">>}, [], <<"content-sha256">>, false),
?assertEqual(Expected, Actual).
%% canonical_request/5 returns a canonical request binary string with extra query params
canonical_request_with_extra_query_params_test() ->
Expected =
<<"GET",
$\n,
"/pa%2520th",
$\n,
"a=&b=1&c=2&d=3",
$\n,
"host:example.com",
$\n,
"x-amz-date:20150325T105958Z",
$\n,
$\n,
"host;x-amz-date",
$\n,
"content-sha256">>,
Actual =
canonical_request(<<"get">>,
#{path => <<"/pa%20th">>, query => <<"b=1&a=">>},
[{<<"Host">>, <<"example.com">>},
{<<"X-Amz-Date">>, <<"20150325T105958Z">>}],
<<"content-sha256">>,
true,
[{<<"c">>, <<"2">>}, {<<"d">>, <<"3">>}]),
?assertEqual(Expected, Actual).
%% canonical_request/5 returns a canonical request binary string with only additional query params
canonical_request_with_only_additional_query_params_test() ->
Expected =
<<"GET",
$\n,
"/pa%2520th",
$\n,
"c=2&d=3",
$\n,
"host:example.com",
$\n,
"x-amz-date:20150325T105958Z",
$\n,
$\n,
"host;x-amz-date",
$\n,
"content-sha256">>,
Actual =
canonical_request(<<"get">>,
#{path => <<"/pa%20th">>, query => <<"">>},
[{<<"Host">>, <<"example.com">>},
{<<"X-Amz-Date">>, <<"20150325T105958Z">>}],
<<"content-sha256">>,
true,
[{<<"c">>, <<"2">>}, {<<"d">>, <<"3">>}]),
?assertEqual(Expected, Actual).
%% sign_v4_query_params/7: Example 1 from https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
sign_v4_query_params_reference_example_1_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{body_digest, <<"UNSIGNED-PAYLOAD">>}]),
?assertEqual(Expected, Actual).
%% sign_v4_query_params/7: Example 2 from https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
sign_v4_query_params_reference_example_2_with_session_token_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
SessionToken = <<"my-session-token">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Security-Token=my-session-token&",
"X-Amz-Signature=127498ec2e996f60915eba27520e69b1554fe016da1d36a3dde70f2408551d67&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{body_digest, <<"UNSIGNED-PAYLOAD">>}, {session_token, SessionToken}]),
?assertEqual(Expected, Actual).
sign_v4_query_params_merge_existing_query_params_with_ttl_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt?A-param=value&X-Another=param">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"A-param=value&",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=3600&",
"X-Amz-Signature=ec8b95e4cf1cc811afc9e29eb7c3959f8832b1ddd36800a082d1c8e6d51f6b8a&",
"X-Amz-SignedHeaders=host&",
"X-Another=param">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{ttl, 3600}]),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_put_method_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"PUT">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=2f382d203f44c23831e0b740f8bc389dc4367991d3001843c8a4fccefe56a0ad&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, []),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_no_body_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=2f96f106e896a51445dbd699bd79337027afef2fd1d841506882218daeaf9b3c&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID, SecretAccessKey, Region, Service, DateTime, Method, URL, []),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_body_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=2f803843262d253ddc309d3bdd705c054cf39f863ce347a35c9b66f8f651a62d&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{body, <<"body">>}]),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_body_digest_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{body_digest, <<"UNSIGNED-PAYLOAD">>}]),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_authority_port_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"http://bucket.localhost:9000/test.txt">>,
Expected =
<<"http://bucket.localhost:9000/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=3dd62e9f64b1c393bfc3d2902e5d5474b629113acd965dbd52ea3d874c83921b&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{body_digest, <<"UNSIGNED-PAYLOAD">>}]),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_authority_well_known_port_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"http://bucket.localhost:80/test.txt">>,
Expected =
<<"http://bucket.localhost:80/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=12778f8b6fc2cb5cce0fee8b218428fb8261c99a145613232d47be9aa38d1d85&",
"X-Amz-SignedHeaders=host">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{body_digest, <<"UNSIGNED-PAYLOAD">>}]),
?assertEqual(Expected, Actual).
sign_v4_query_params_with_tagging_test() ->
AccessKeyID = <<"AKIAIOSFODNN7EXAMPLE">>,
SecretAccessKey = <<"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY">>,
Region = <<"us-east-1">>,
Service = <<"s3">>,
DateTime = {{2013, 5, 24}, {0, 0, 0}},
Method = <<"GET">>,
URL = <<"https://examplebucket.s3.amazonaws.com/test.txt">>,
Expected =
<<"https://examplebucket.s3.amazonaws.com/test.txt?",
"X-Amz-Algorithm=AWS4-HMAC-SHA256&",
"X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&",
"X-Amz-Date=20130524T000000Z&",
"X-Amz-Expires=86400&",
"X-Amz-Signature=5c14ef7998d657c3b8293b37abefaef5fa98cc775bcbfffdb2027f4ce05772ef&",
"X-Amz-SignedHeaders=host%3Bx-amz-tagging">>,
Actual =
sign_v4_query_params(AccessKeyID,
SecretAccessKey,
Region,
Service,
DateTime,
Method,
URL,
[{tags, <<"key1=value1&key2=value2">>}]),
?assertEqual(Expected, Actual).
format_date_long_test() ->
Expected = <<"20210126T200815Z">>,
Actual = format_datetime_long({{2021,1,26}, {20,8,15}}),
?assertEqual(Expected, Actual).
format_date_short_test() ->
Expected = <<"20210126">>,
Actual = format_datetime_short({{2021,1,26}, {20,8,15}}),
?assertEqual(Expected, Actual).
-endif.