-module(aws@internal@sigv4).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/sigv4.gleam").
-export([make_credentials/3, canonical_request/3, string_to_sign/4, signing_key/4, signature/2, authorization_header/6, sign/3, presigned_url/5]).
-export_type([signing_options/0, signing_credentials/0, canonical_parts/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
-type signing_options() :: {signing_options,
binary(),
binary(),
binary(),
boolean(),
boolean(),
boolean()}.
-type signing_credentials() :: {signing_credentials,
binary(),
binary(),
gleam@option:option(binary())}.
-type canonical_parts() :: {canonical_parts,
binary(),
binary(),
binary(),
list(aws@internal@http_request:header())}.
-file("src/aws/internal/sigv4.gleam", 39).
?DOC(
" Convenience constructor mirroring the most common case: static keys\n"
" with no session token.\n"
).
-spec make_credentials(binary(), binary(), gleam@option:option(binary())) -> signing_credentials().
make_credentials(Access_key_id, Secret_access_key, Session_token) ->
{signing_credentials, Access_key_id, Secret_access_key, Session_token}.
-file("src/aws/internal/sigv4.gleam", 314).
?DOC(
" Assemble the canonical-request line block both header-auth and\n"
" query-auth need. The five `\\n`-joined parts are identical\n"
" between the two flows; only the inputs differ (header-auth\n"
" signs in-headers, query-auth signs an auth-augmented query\n"
" string).\n"
).
-spec build_creq(binary(), binary(), binary(), binary(), binary(), binary()) -> binary().
build_creq(
Method,
Canonical_uri,
Canonical_query,
Canonical_headers_block,
Signed_headers_list,
Payload_hash
) ->
<<<<<<<<<<<<<<<<<<<<Method/binary, "\n"/utf8>>/binary,
Canonical_uri/binary>>/binary,
"\n"/utf8>>/binary,
Canonical_query/binary>>/binary,
"\n"/utf8>>/binary,
Canonical_headers_block/binary>>/binary,
"\n"/utf8>>/binary,
Signed_headers_list/binary>>/binary,
"\n"/utf8>>/binary,
Payload_hash/binary>>.
-file("src/aws/internal/sigv4.gleam", 384).
-spec headers_for_signing(
list(aws@internal@http_request:header()),
signing_credentials(),
signing_options()
) -> list(aws@internal@http_request:header()).
headers_for_signing(Prepared, Creds, Opts) ->
case {erlang:element(4, Creds), erlang:element(7, Opts)} of
{{some, _}, true} ->
gleam@list:filter(
Prepared,
fun(H) ->
string:lowercase(erlang:element(2, H)) /= <<"x-amz-security-token"/utf8>>
end
);
{_, _} ->
Prepared
end.
-file("src/aws/internal/sigv4.gleam", 398).
-spec upsert_header(
list(aws@internal@http_request:header()),
binary(),
binary(),
boolean()
) -> list(aws@internal@http_request:header()).
upsert_header(Headers, Name, Value, Replace) ->
Lower = string:lowercase(Name),
Already_present = gleam@list:any(
Headers,
fun(H) -> string:lowercase(erlang:element(2, H)) =:= Lower end
),
case {Already_present, Replace} of
{true, true} ->
gleam@list:map(
Headers,
fun(H@1) ->
case string:lowercase(erlang:element(2, H@1)) =:= Lower of
true ->
{header, erlang:element(2, H@1), Value};
false ->
H@1
end
end
);
{true, false} ->
Headers;
{false, _} ->
lists:append(Headers, [{header, Name, Value}])
end.
-file("src/aws/internal/sigv4.gleam", 359).
-spec prepare_headers(
aws@internal@http_request:http_request(),
signing_credentials(),
signing_options(),
binary()
) -> list(aws@internal@http_request:header()).
prepare_headers(Req, Creds, Opts, Payload_hash) ->
With_date = upsert_header(
erlang:element(5, Req),
<<"X-Amz-Date"/utf8>>,
erlang:element(2, Opts),
true
),
With_body = case erlang:element(6, Opts) of
true ->
upsert_header(
With_date,
<<"X-Amz-Content-Sha256"/utf8>>,
Payload_hash,
true
);
false ->
With_date
end,
case {erlang:element(4, Creds), erlang:element(7, Opts)} of
{{some, Token}, false} ->
upsert_header(
With_body,
<<"X-Amz-Security-Token"/utf8>>,
Token,
true
);
{_, _} ->
With_body
end.
-file("src/aws/internal/sigv4.gleam", 60).
-spec canonical_request(
aws@internal@http_request:http_request(),
signing_credentials(),
signing_options()
) -> canonical_parts().
canonical_request(Req, Creds, Opts) ->
Payload_hash = case erlang:element(6, Opts) of
true ->
aws_ffi:hex_encode(aws_ffi:sha256(erlang:element(6, Req)));
false ->
aws_ffi:hex_encode(
aws_ffi:sha256(gleam_stdlib:identity(<<""/utf8>>))
)
end,
Prepared = prepare_headers(Req, Creds, Opts, Payload_hash),
Signing_headers = headers_for_signing(Prepared, Creds, Opts),
Canonical_headers_block = aws@internal@sigv4_canonical:canonical_headers(
Signing_headers
),
Signed_headers_list = aws@internal@sigv4_canonical:signed_headers(
Signing_headers
),
Canonical_uri = aws@internal@sigv4_canonical:build_canonical_uri(
erlang:element(3, Req),
erlang:element(5, Opts)
),
Canonical_query = aws@internal@sigv4_canonical:canonical_query_string(
erlang:element(4, Req)
),
Creq = build_creq(
erlang:element(2, Req),
Canonical_uri,
Canonical_query,
Canonical_headers_block,
Signed_headers_list,
Payload_hash
),
{canonical_parts, Creq, Signed_headers_list, Payload_hash, Prepared}.
-file("src/aws/internal/sigv4.gleam", 97).
-spec string_to_sign(binary(), binary(), binary(), binary()) -> binary().
string_to_sign(Canonical, Timestamp, Region, Service) ->
Date = gleam@string:slice(Timestamp, 0, 8),
Scope = <<<<<<<<<<Date/binary, "/"/utf8>>/binary, Region/binary>>/binary,
"/"/utf8>>/binary,
Service/binary>>/binary,
"/aws4_request"/utf8>>,
Hash = aws_ffi:hex_encode(aws_ffi:sha256(gleam_stdlib:identity(Canonical))),
<<<<<<<<<<"AWS4-HMAC-SHA256\n"/utf8, Timestamp/binary>>/binary, "\n"/utf8>>/binary,
Scope/binary>>/binary,
"\n"/utf8>>/binary,
Hash/binary>>.
-file("src/aws/internal/sigv4.gleam", 109).
-spec signing_key(binary(), binary(), binary(), binary()) -> bitstring().
signing_key(Secret, Date, Region, Service) ->
K_secret = gleam_stdlib:identity(<<"AWS4"/utf8, Secret/binary>>),
K_date = aws_ffi:hmac_sha256(K_secret, gleam_stdlib:identity(Date)),
K_region = aws_ffi:hmac_sha256(K_date, gleam_stdlib:identity(Region)),
K_service = aws_ffi:hmac_sha256(K_region, gleam_stdlib:identity(Service)),
aws_ffi:hmac_sha256(
K_service,
gleam_stdlib:identity(<<"aws4_request"/utf8>>)
).
-file("src/aws/internal/sigv4.gleam", 122).
-spec signature(bitstring(), binary()) -> binary().
signature(Key, Sts) ->
aws_ffi:hex_encode(aws_ffi:hmac_sha256(Key, gleam_stdlib:identity(Sts))).
-file("src/aws/internal/sigv4.gleam", 126).
-spec authorization_header(
signing_credentials(),
binary(),
binary(),
binary(),
binary(),
binary()
) -> binary().
authorization_header(
Creds,
Timestamp,
Region,
Service,
Signed_headers,
Signature
) ->
Date = gleam@string:slice(Timestamp, 0, 8),
<<<<<<<<<<<<<<<<<<<<<<"AWS4-HMAC-SHA256 Credential="/utf8,
(erlang:element(2, Creds))/binary>>/binary,
"/"/utf8>>/binary,
Date/binary>>/binary,
"/"/utf8>>/binary,
Region/binary>>/binary,
"/"/utf8>>/binary,
Service/binary>>/binary,
"/aws4_request, SignedHeaders="/utf8>>/binary,
Signed_headers/binary>>/binary,
", Signature="/utf8>>/binary,
Signature/binary>>.
-file("src/aws/internal/sigv4.gleam", 149).
-spec sign(
aws@internal@http_request:http_request(),
signing_credentials(),
signing_options()
) -> aws@internal@http_request:http_request().
sign(Req, Creds, Opts) ->
Parts = canonical_request(Req, Creds, Opts),
Sts = string_to_sign(
erlang:element(2, Parts),
erlang:element(2, Opts),
erlang:element(3, Opts),
erlang:element(4, Opts)
),
Date = gleam@string:slice(erlang:element(2, Opts), 0, 8),
Key = signing_key(
erlang:element(3, Creds),
Date,
erlang:element(3, Opts),
erlang:element(4, Opts)
),
Sig = signature(Key, Sts),
Auth = authorization_header(
Creds,
erlang:element(2, Opts),
erlang:element(3, Opts),
erlang:element(4, Opts),
erlang:element(3, Parts),
Sig
),
With_session = case {erlang:element(4, Creds), erlang:element(7, Opts)} of
{{some, Token}, true} ->
lists:append(
erlang:element(5, Parts),
[{header, <<"X-Amz-Security-Token"/utf8>>, Token}]
);
{_, _} ->
erlang:element(5, Parts)
end,
Final_headers = lists:append(
With_session,
[{header, <<"Authorization"/utf8>>, Auth}]
),
{http_request,
erlang:element(2, Req),
erlang:element(3, Req),
erlang:element(4, Req),
Final_headers,
erlang:element(6, Req)}.
-file("src/aws/internal/sigv4.gleam", 342).
-spec merge_query(binary(), list({binary(), binary()})) -> binary().
merge_query(Existing, Auth_params) ->
Auth_pairs = begin
_pipe = Auth_params,
_pipe@1 = gleam@list:map(
_pipe,
fun(P) ->
<<<<(erlang:element(1, P))/binary, "="/utf8>>/binary,
(aws@internal@uri:encode_component(erlang:element(2, P)))/binary>>
end
),
gleam@string:join(_pipe@1, <<"&"/utf8>>)
end,
case Existing of
<<""/utf8>> ->
Auth_pairs;
_ ->
<<<<Existing/binary, "&"/utf8>>/binary, Auth_pairs/binary>>
end.
-file("src/aws/internal/sigv4.gleam", 335).
-spec host_from_headers(list(aws@internal@http_request:header())) -> binary().
host_from_headers(Headers) ->
case gleam@list:find(
Headers,
fun(H) -> string:lowercase(erlang:element(2, H)) =:= <<"host"/utf8>> end
) of
{ok, H@1} ->
erlang:element(3, H@1);
{error, _} ->
<<""/utf8>>
end.
-file("src/aws/internal/sigv4.gleam", 211).
?DOC(
" Build a SigV4 presigned URL — the \"query-string auth\" variant\n"
" callers reach for to share short-lived links to S3 objects, etc.\n"
" The auth components (`X-Amz-Algorithm`, `X-Amz-Credential`,\n"
" `X-Amz-Date`, `X-Amz-Expires`, `X-Amz-SignedHeaders`,\n"
" `X-Amz-Security-Token` when present, and `X-Amz-Signature`) land\n"
" in the URL query string rather than headers. Only the `Host`\n"
" header is signed.\n"
"\n"
" `payload_hash` controls the canonical-request payload line:\n"
" * `Some(\"UNSIGNED-PAYLOAD\")` — the S3 convention for shared\n"
" download URLs (the caller doesn't get to choose the body).\n"
" * `Some(hex)` — caller-provided body hash; matches a known\n"
" request body that will be sent against the signed URL.\n"
" * `None` — the standard SigV4 path, honouring `opts.sign_body`:\n"
" `True` ⇒ `sha256(req.body)`, `False` ⇒ `sha256(\"\")` (the\n"
" hash of the empty body). The v4 test suite uses this path.\n"
"\n"
" `expires_seconds` is bounded by SigV4 to `[1, 604800]` (1 second\n"
" to 7 days). The function doesn't enforce the bound; AWS rejects\n"
" out-of-range values at the server side.\n"
"\n"
" Returns the full URL (`https://<host><path>?<signed-query>`)\n"
" ready to hand to a caller. Existing `req.query` entries are\n"
" preserved and merged with the auth params.\n"
).
-spec presigned_url(
aws@internal@http_request:http_request(),
signing_credentials(),
signing_options(),
integer(),
gleam@option:option(binary())
) -> binary().
presigned_url(Req, Creds, Opts, Expires_seconds, Payload_hash) ->
Host = host_from_headers(erlang:element(5, Req)),
Date = gleam@string:slice(erlang:element(2, Opts), 0, 8),
Credential_scope = <<<<<<<<<<<<<<(erlang:element(2, Creds))/binary,
"/"/utf8>>/binary,
Date/binary>>/binary,
"/"/utf8>>/binary,
(erlang:element(3, Opts))/binary>>/binary,
"/"/utf8>>/binary,
(erlang:element(4, Opts))/binary>>/binary,
"/aws4_request"/utf8>>,
Signed_headers_list = aws@internal@sigv4_canonical:signed_headers(
erlang:element(5, Req)
),
Canonical_headers_block = aws@internal@sigv4_canonical:canonical_headers(
erlang:element(5, Req)
),
Auth_params = [{<<"X-Amz-Algorithm"/utf8>>, <<"AWS4-HMAC-SHA256"/utf8>>},
{<<"X-Amz-Credential"/utf8>>, Credential_scope},
{<<"X-Amz-Date"/utf8>>, erlang:element(2, Opts)},
{<<"X-Amz-Expires"/utf8>>, erlang:integer_to_binary(Expires_seconds)},
{<<"X-Amz-SignedHeaders"/utf8>>, Signed_headers_list}],
Auth_params_for_signing = case {erlang:element(4, Creds),
erlang:element(7, Opts)} of
{{some, Token}, false} ->
lists:append(
Auth_params,
[{<<"X-Amz-Security-Token"/utf8>>, Token}]
);
{_, _} ->
Auth_params
end,
Merged_query = merge_query(erlang:element(4, Req), Auth_params_for_signing),
Canonical_uri = aws@internal@sigv4_canonical:build_canonical_uri(
erlang:element(3, Req),
erlang:element(5, Opts)
),
Canonical_query = aws@internal@sigv4_canonical:canonical_query_string(
Merged_query
),
Payload_hash@1 = case Payload_hash of
{some, H} ->
H;
_ ->
case erlang:element(6, Opts) of
true ->
aws_ffi:hex_encode(aws_ffi:sha256(erlang:element(6, Req)));
false ->
aws_ffi:hex_encode(
aws_ffi:sha256(gleam_stdlib:identity(<<""/utf8>>))
)
end
end,
Creq = build_creq(
erlang:element(2, Req),
Canonical_uri,
Canonical_query,
Canonical_headers_block,
Signed_headers_list,
Payload_hash@1
),
Sts = string_to_sign(
Creq,
erlang:element(2, Opts),
erlang:element(3, Opts),
erlang:element(4, Opts)
),
Key = signing_key(
erlang:element(3, Creds),
Date,
erlang:element(3, Opts),
erlang:element(4, Opts)
),
Sig = signature(Key, Sts),
Url_with_signature = <<<<<<<<<<<<"https://"/utf8, Host/binary>>/binary,
Canonical_uri/binary>>/binary,
"?"/utf8>>/binary,
Canonical_query/binary>>/binary,
"&X-Amz-Signature="/utf8>>/binary,
Sig/binary>>,
case {erlang:element(4, Creds), erlang:element(7, Opts)} of
{{some, Token@1}, true} ->
<<<<Url_with_signature/binary, "&X-Amz-Security-Token="/utf8>>/binary,
(aws@internal@uri:encode_component(Token@1))/binary>>;
{_, _} ->
Url_with_signature
end.