|
| 1 | +%% Based on: |
| 2 | +%% https://github.com/aws/smithy-go/blob/main/aws-http-auth/internal/v4/signer.go |
| 3 | +-module(aws_sigv4_internal). |
| 4 | + |
| 5 | +-export([ do/1 |
| 6 | + , resolve_time/1 |
| 7 | + ]). |
| 8 | + |
| 9 | +%% exported for tests |
| 10 | +-export([ build_canonical_request/1 |
| 11 | + , default_is_signed/1 |
| 12 | + , resolve_payload_hash/1 |
| 13 | + , set_required_headers/1 |
| 14 | + ]). |
| 15 | + |
| 16 | +-include("aws_sigv4_internal.hrl"). |
| 17 | + |
| 18 | +-type credentials() :: #credentials{}. |
| 19 | +-type internal_signer() :: #internal_signer{}. |
| 20 | +-type request() :: #request{}. |
| 21 | +-type v4_signer_options() :: #v4_signer_options{}. |
| 22 | + |
| 23 | +-export_type([ credentials/0 |
| 24 | + , headers/0 |
| 25 | + , internal_signer/0 |
| 26 | + , request/0 |
| 27 | + , v4_signer_options/0 |
| 28 | + ]). |
| 29 | + |
| 30 | +-spec do(internal_signer()) -> {ok, headers()} | {error, any()}. |
| 31 | +do(Signer) -> |
| 32 | + Signer1 = init(Signer), |
| 33 | + Signer2 = set_required_headers(Signer1), |
| 34 | + {CanonicalRequest, SignedHeaders} = build_canonical_request(Signer2), |
| 35 | + StringToSign = build_string_to_sign(Signer2, CanonicalRequest), |
| 36 | + case sign_string(Signer2, StringToSign) of |
| 37 | + {ok, Signature} -> {ok, set_authorization_header(Signer2, SignedHeaders, Signature)}; |
| 38 | + {error, _Reason} = Error -> Error |
| 39 | + end. |
| 40 | + |
| 41 | +-spec set_authorization_header(internal_signer(), binary(), binary()) -> headers(). |
| 42 | +set_authorization_header(Signer, SignedHeaders, Signature) -> |
| 43 | + headers_put(<<"Authorization">>, |
| 44 | + build_authorization_header(Signer, SignedHeaders, Signature), |
| 45 | + Signer#internal_signer.request#request.headers). |
| 46 | + |
| 47 | +-spec init(internal_signer()) -> internal_signer(). |
| 48 | +init(Signer) -> |
| 49 | + resolve_payload_hash(init_is_signed(Signer)). |
| 50 | + |
| 51 | +-spec init_is_signed(internal_signer()) -> internal_signer(). |
| 52 | +init_is_signed(Signer) -> |
| 53 | + Options = Signer#internal_signer.options, |
| 54 | + case Options#v4_signer_options.is_signed of |
| 55 | + undefined -> |
| 56 | + Options1 = Options#v4_signer_options{is_signed = fun default_is_signed/1}, |
| 57 | + Signer#internal_signer{options = Options1}; |
| 58 | + IsSigned when is_function(IsSigned, 1) -> Signer |
| 59 | + end. |
| 60 | + |
| 61 | +-spec resolve_payload_hash(internal_signer()) -> internal_signer(). |
| 62 | +resolve_payload_hash(Signer) -> |
| 63 | + case Signer#internal_signer.payload_hash of |
| 64 | + Binary when byte_size(Binary) > 0 -> Signer; |
| 65 | + _ -> |
| 66 | + PayloadHash = |
| 67 | + case Signer#internal_signer.options#v4_signer_options.disable_implicit_payload_hashing of |
| 68 | + true -> ?UNSIGNED_PAYLOAD; |
| 69 | + false -> aws_sigv4_utils:sha256(Signer#internal_signer.request#request.body) |
| 70 | + end, |
| 71 | + Signer#internal_signer{payload_hash = PayloadHash} |
| 72 | + end. |
| 73 | + |
| 74 | +-spec set_required_headers(internal_signer()) -> internal_signer(). |
| 75 | +set_required_headers(Signer) -> |
| 76 | + Request0 = Signer#internal_signer.request, |
| 77 | + Headers0 = Request0#request.headers, |
| 78 | + Funs = |
| 79 | + [ fun set_host_header/2 |
| 80 | + , fun set_date_header/2 |
| 81 | + , fun set_security_token_header/2 |
| 82 | + , fun set_content_sha_header/2 |
| 83 | + ], |
| 84 | + Headers = lists:foldl(fun(F, Hs) -> F(Signer, Hs) end, Headers0, Funs), |
| 85 | + Request = Request0#request{headers = Headers}, |
| 86 | + Signer#internal_signer{request = Request}. |
| 87 | + |
| 88 | +-spec set_host_header(internal_signer(), headers()) -> headers(). |
| 89 | +set_host_header(#internal_signer{request = #request{host = Host}}, Headers) -> |
| 90 | + headers_put(<<"Host">>, Host, Headers). |
| 91 | + |
| 92 | +-spec set_date_header(internal_signer(), headers()) -> headers(). |
| 93 | +set_date_header(#internal_signer{time = Time}, Headers) -> |
| 94 | + headers_put(<<"X-Amz-Date">>, aws_sigv4_utils:format_time_long(Time), Headers). |
| 95 | + |
| 96 | +-spec set_security_token_header(internal_signer(), headers()) -> headers(). |
| 97 | +set_security_token_header(#internal_signer{credentials = Credentials}, Headers) -> |
| 98 | + case Credentials#credentials.session_token of |
| 99 | + SessionToken when byte_size(SessionToken) > 0 -> |
| 100 | + headers_put(<<"X-Amz-Security-Token">>, SessionToken, Headers); |
| 101 | + _ -> Headers |
| 102 | + end. |
| 103 | + |
| 104 | +-spec set_content_sha_header(internal_signer(), headers()) -> headers(). |
| 105 | +set_content_sha_header(#internal_signer{payload_hash = PayloadHash, options = Options}, Headers) -> |
| 106 | + case PayloadHash of |
| 107 | + _ when byte_size(PayloadHash) > 0, Options#v4_signer_options.add_payload_hash_header =:= true -> |
| 108 | + headers_put(<<"X-Amz-Content-Sha256">>, payload_hash_string(PayloadHash), Headers); |
| 109 | + _ -> Headers |
| 110 | + end. |
| 111 | + |
| 112 | +-spec headers_put(binary(), binary(), headers()) -> headers(). |
| 113 | +headers_put(Key, Val, Headers) -> |
| 114 | + lists:keystore(Key, 1, Headers, {Key, Val}). |
| 115 | + |
| 116 | +-spec build_canonical_request(internal_signer()) -> {binary(), binary()}. |
| 117 | +build_canonical_request(Signer) -> |
| 118 | + CanonMethod = build_canonical_method(Signer), |
| 119 | + CanonPath = build_canonical_path(Signer), |
| 120 | + CanonQuery = build_canonical_query(Signer), |
| 121 | + {CanonHeaders, SignedHeaders} = build_canonical_headers(Signer), |
| 122 | + {iolist_to_binary( |
| 123 | + lists:join("\n", |
| 124 | + [ CanonMethod |
| 125 | + , CanonPath |
| 126 | + , CanonQuery |
| 127 | + , CanonHeaders |
| 128 | + , SignedHeaders |
| 129 | + , payload_hash_string(Signer#internal_signer.payload_hash) |
| 130 | + ])), |
| 131 | + SignedHeaders}. |
| 132 | + |
| 133 | +-spec build_canonical_method(internal_signer()) -> binary(). |
| 134 | +build_canonical_method(Signer) -> |
| 135 | + aws_sigv4_utils:toupper(Signer#internal_signer.request#request.method). |
| 136 | + |
| 137 | +-spec build_canonical_path(internal_signer()) -> binary(). |
| 138 | +build_canonical_path(Signer) -> |
| 139 | + URIMap = uri_string:parse(Signer#internal_signer.request#request.url), |
| 140 | + Path = maps:get(path, URIMap, <<>>), |
| 141 | + %% FIXME: we want an escaped path here, it's unclear if it already is |
| 142 | + EscapedPath = |
| 143 | + case byte_size(Path) =:= 0 of |
| 144 | + true -> <<"/">>; |
| 145 | + false -> Path |
| 146 | + end, |
| 147 | + case Signer#internal_signer.options#v4_signer_options.disable_double_path_escape of |
| 148 | + true -> EscapedPath; |
| 149 | + false -> aws_signature_utils:uri_encode_path(EscapedPath) |
| 150 | + end. |
| 151 | + |
| 152 | +-spec build_canonical_query(internal_signer()) -> binary(). |
| 153 | +build_canonical_query(Signer) -> |
| 154 | + URIMap = uri_string:parse(Signer#internal_signer.request#request.url), |
| 155 | + QueryString = maps:get(query, URIMap, <<"">>), |
| 156 | + QueryList0 = uri_string:dissect_query(QueryString), |
| 157 | + QueryMap0 = |
| 158 | + lists:foldl( |
| 159 | + fun({Key, Val0}, QMap) -> |
| 160 | + %% treat "key" like "key=" |
| 161 | + Val = case Val0 of true -> <<>>; _ -> Val0 end, |
| 162 | + Vals = maps:get(Key, QMap, []), |
| 163 | + maps:put(Key, [Val | Vals], QMap) |
| 164 | + end, maps:new(), QueryList0), |
| 165 | + QueryMap1 = |
| 166 | + maps:map( |
| 167 | + fun(_Key, Vals) -> |
| 168 | + lists:reverse(Vals) |
| 169 | + end, QueryMap0), |
| 170 | + QueryList2 = |
| 171 | + lists:sort( |
| 172 | + fun({K1, _Vs1}, {K2, _Vs2}) -> |
| 173 | + K1 =< K2 |
| 174 | + end, maps:to_list(QueryMap1)), |
| 175 | + QueryList3 = |
| 176 | + lists:append( |
| 177 | + lists:map( |
| 178 | + fun({K, Vs}) -> |
| 179 | + lists:map(fun(V) -> {K, V} end, Vs) |
| 180 | + end, QueryList2)), |
| 181 | + case QueryList3 of |
| 182 | + [] -> <<>>; |
| 183 | + _ -> binary:replace(uri_string:compose_query(QueryList3), <<"+">>, <<"%20">>) |
| 184 | + end. |
| 185 | + |
| 186 | +-spec build_canonical_headers(internal_signer()) -> {binary(), binary()}. |
| 187 | +build_canonical_headers(Signer) -> |
| 188 | + IsSigned = Signer#internal_signer.options#v4_signer_options.is_signed, |
| 189 | + SignedHeadersMap = |
| 190 | + lists:foldl( |
| 191 | + fun({Header, Value}, Map) -> |
| 192 | + Lowercase = aws_sigv4_utils:tolower(Header), |
| 193 | + case IsSigned(Lowercase) of |
| 194 | + true -> |
| 195 | + Values = maps:get(Lowercase, Map, []), |
| 196 | + maps:put(Lowercase, [Value | Values], Map); |
| 197 | + false -> Map |
| 198 | + end |
| 199 | + end, maps:new(), Signer#internal_signer.request#request.headers), |
| 200 | + SignedHeadersList = lists:sort(maps:to_list(SignedHeadersMap)), |
| 201 | + SignedHeaders = |
| 202 | + iolist_to_binary( |
| 203 | + lists:map( |
| 204 | + fun({Header, Values}) -> |
| 205 | + [ Header |
| 206 | + , ":" |
| 207 | + , lists:join(",", lists:map(fun aws_sigv4_utils:trimspace/1, lists:reverse(Values))) |
| 208 | + , "\n" |
| 209 | + ] |
| 210 | + end, SignedHeadersList)), |
| 211 | + CanonHeaders = |
| 212 | + iolist_to_binary( |
| 213 | + lists:join(";", lists:map(fun({Header, _Values}) -> Header end, SignedHeadersList))), |
| 214 | + {SignedHeaders, CanonHeaders}. |
| 215 | + |
| 216 | +-spec build_string_to_sign(internal_signer(), binary()) -> binary(). |
| 217 | +build_string_to_sign(Signer, CanonicalRequest) -> |
| 218 | + iolist_to_binary( |
| 219 | + lists:join("\n", |
| 220 | + [ Signer#internal_signer.algorithm |
| 221 | + , aws_sigv4_utils:format_time_long(Signer#internal_signer.time) |
| 222 | + , Signer#internal_signer.credential_scope |
| 223 | + , aws_sigv4_utils:sha256(CanonicalRequest) |
| 224 | + ])). |
| 225 | + |
| 226 | +-spec build_authorization_header(internal_signer(), binary(), binary()) -> binary(). |
| 227 | +build_authorization_header(Signer, SignedHeaders, Signature) -> |
| 228 | + iolist_to_binary( |
| 229 | + [ Signer#internal_signer.algorithm |
| 230 | + , " Credential=" |
| 231 | + , Signer#internal_signer.credentials#credentials.access_key_id |
| 232 | + , "/" |
| 233 | + , Signer#internal_signer.credential_scope |
| 234 | + , ", SignedHeaders=" |
| 235 | + , SignedHeaders |
| 236 | + , ", Signature=" |
| 237 | + , Signature |
| 238 | + ]). |
| 239 | + |
| 240 | +-spec payload_hash_string(binary()) -> binary(). |
| 241 | +payload_hash_string(Hash) -> |
| 242 | + case Hash of |
| 243 | + ?UNSIGNED_PAYLOAD -> Hash; |
| 244 | + _ -> aws_signature_utils:base16(Hash) |
| 245 | + end. |
| 246 | + |
| 247 | +-spec resolve_time(calendar:datetime() | undefined) -> calendar:datetime(). |
| 248 | +resolve_time(undefined) -> calendar:universal_time(); |
| 249 | +resolve_time(Time) -> Time. |
| 250 | + |
| 251 | +-spec default_is_signed(binary()) -> boolean(). |
| 252 | +default_is_signed(Header) -> |
| 253 | + case Header of |
| 254 | + <<"host">> -> true; |
| 255 | + <<"x-amz-", _/binary>> -> true; |
| 256 | + _ -> false |
| 257 | + end. |
| 258 | + |
| 259 | +-spec sign_string(internal_signer(), binary()) -> {ok, binary()} | {error, any()}. |
| 260 | +sign_string(Signer, String) -> |
| 261 | + (Signer#internal_signer.sign_string)(String). |
0 commit comments