%% @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]). -type header() :: {binary(), binary()}. -type headers() :: [header()]. -type query_param() :: {binary(), binary()}. -type query_params() :: [query_param()]. %% @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 Signature Version 4 (SigV4) 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 AWS documentation. %% %% The following options are supported: %% %%
%%
`uri_encode_path'
%%
%% When `true', the request URI path is URI-encoded during request %% canonicalization, which is required for every service except S3. %% 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'. %%
%%
`body_digest'
%%
%% 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. %%
%%
-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 %%
%%
`:chunk-signature'
%%
%% computed signature of the event, binary string, `bytes' type %%
%%
`:date'
%%
%% millisecond since epoch, `timestamp' type %%
%%
%% %% `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 :: headers(), 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 Signature Version 4 (SigV4) 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 AWS documentation. %% %% The following options are supported: %% %%
%%
`ttl'
%%
%% Time-to-live value that tells how long this URL is valid in seconds. %% Defaults to `86400', which means one day. %%
%%
`uri_encode_path'
%%
%% When `true', the request URI path is URI-encoded during request %% canonicalization, which is required for every service except S3. %% 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'. %%
%%
`session_token'
%%
%% Optional credential parameter if using credentials sourced from the STS service. %%
%%
`body'
%%
%% Request body to compute SHA256 digest for. Defaults to an empty binary. Note that %% `body_digest' always takes precedence when set. %%
%%
`body_digest'
%%
%% 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, which is expected for S3. %%
%%
-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()}, 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), 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">>, <<"host">>}], 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), CanonicalRequest = canonical_request(Method, URLMap, [HostHeader], 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), <>. 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}) -> <> || {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). 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.