From a5c06d9c12b131f62001f53d98ef4b880ac0123e Mon Sep 17 00:00:00 2001 From: Tyler Hall Date: Tue, 10 Dec 2024 17:35:41 -0500 Subject: [PATCH] feat(hb_http_signature): wip signature base and signatures match before/after parsing #13 --- src/hb_http_signature.erl | 96 ++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/src/hb_http_signature.erl b/src/hb_http_signature.erl index 7ebb62a5..d5c586c4 100644 --- a/src/hb_http_signature.erl +++ b/src/hb_http_signature.erl @@ -5,6 +5,8 @@ % https://datatracker.ietf.org/doc/html/rfc9421#section-2.2.7-14 -define(EMPTY_QUERY_PARAMS, $?). +-include("include/hb.hrl"). + -include_lib("eunit/include/eunit.hrl"). -type fields() :: #{ @@ -72,15 +74,6 @@ %%% @moduledoc This module implements HTTP Message Signatures %%% as described in RFC-9421 https://datatracker.ietf.org/doc/html/rfc9421 -%%% TODO: implement the actual signing of the signature-base using the provided key - -%%% -%%% Ideal API -%%% authority(ComponentIdentifiers, Params) -> Authority -%%% -%%% sign(Authority, Req, Res) -> {ok, {SigName, SigInput, Sig} -%%% verify(Authority, SigName, Msg) -> {ok} -%%% %%% @doc A helper to validate and produce an "Authority" State -spec authority( @@ -88,18 +81,15 @@ #{binary() => binary() | integer()}, {} %TODO: type out a key_pair ) -> authority_state(). -authority(ComponentIdentifiers, SigParams, PubKey = {KeyType, _Pub}) when is_atom(KeyType) -> - authority(ComponentIdentifiers, SigParams, {{}, PubKey}); -authority(ComponentIdentifiers, SigParams, PrivKey = {KeyType, _Priv, Pub}) when is_atom(KeyType) -> +authority(ComponentIdentifiers, SigParams, PubKey = {KeyType = {ALG, _}, _Pub}) when is_atom(ALG) -> + % Only the public key is provided, so use an stub binary for private + % which will trigger errors downstream if it's needed, which is what we want + authority(ComponentIdentifiers, SigParams, {{KeyType, <<>>, PubKey}, PubKey}); +authority(ComponentIdentifiers, SigParams, PrivKey = {KeyType = {ALG, _}, _Priv, Pub}) when is_atom(ALG) -> + % Only the private key was provided, so derive the public from private authority(ComponentIdentifiers, SigParams, {PrivKey, {KeyType, Pub}}); -authority(ComponentIdentifiers, SigParams, KeyPair = {_Priv, {_KeyType, PubKey}}) -> - % TODO: should we check if keyid if already set and throw if so? - % for now, just overwriting - % - % The keyid in the signature params will always be the public key - % of the signer - SigParamsWithKeyId = maps:put(keyid, PubKey, SigParams), - #{ +authority(ComponentIdentifiers, SigParams, KeyPair = {{_, _, _}, {_, _}}) -> + #{ % parse each component identifier into a Structured Field Item: % % <<"\"Example-Dict\";key=\"foo\"">> -> {item, {string, <<"Example-Dict">>}, [{<<"key">>, {string, <<"foo">>}}]} @@ -111,7 +101,7 @@ authority(ComponentIdentifiers, SigParams, KeyPair = {_Priv, {_KeyType, PubKey}} component_identifiers => lists:map(fun sf_item/1, ComponentIdentifiers), % TODO: add checks to allow only valid signature parameters % https://datatracker.ietf.org/doc/html/rfc9421#name-signature-parameters - sig_params => SigParamsWithKeyId, + sig_params => SigParams, % TODO: validate the key is supported? key_pair => KeyPair }. @@ -125,8 +115,18 @@ sign(Authority, Req) -> %%% that can be used to additional signatures to a corresponding HTTP Message -spec sign(authority_state(), request_message(), response_message()) -> {ok, {binary(), binary(), binary()}}. sign(Authority, Req, Res) -> - {Priv, _Pub} = maps:get(key_pair, Authority), - {SignatureInput, SignatureBase} = signature_base(Authority, Req, Res), + {Priv, {KeyType, PubKey}} = maps:get(key_pair, Authority), + % Create the signature base and signature-input values + SigParamsWithKeyAndAlg = maps:merge( + maps:get(sig_params, Authority), + % TODO: determine alg based on KeyType from authority + % TODO: is there a more turn-key way to get the wallet address + #{ alg => <<"rsa-pss-sha512">>, keyid => hb_util:encode(bin(ar_wallet:to_address(PubKey, KeyType))) } + ), + ?no_prod(<<"Is the wallet address as keyid kosher here?">>), + AuthorityWithSigParams = maps:put(sig_params, SigParamsWithKeyAndAlg, Authority), + {SignatureInput, SignatureBase} = signature_base(AuthorityWithSigParams, Req, Res), + % Now perform the actual signing Signature = ar_wallet:sign(Priv, SignatureBase, sha512), {ok, {SignatureInput, Signature}}. @@ -148,18 +148,34 @@ verify(#{ sig_name := SigName, key := Key }, Req, Res) -> % of RFC-9421, we get the error handling here as well. % % See https://datatracker.ietf.org/doc/html/rfc9421#section-3.2-3.2 - SigNameParams = [{<<"key">>}, {string, bin(SigName)}], + SigNameParams = [{<<"key">>, {string, bin(SigName)}}], SignatureIdentifier = {item, {string, <<"signature">>}, SigNameParams}, SignatureInputIdentifier = {item, {string, <<"signature-input">>}, SigNameParams}, % extract signature and signature params case {extract_field(SignatureIdentifier, Req, Res), extract_field(SignatureInputIdentifier, Req, Res)} of - {{ok, Signature}, {ok, SignatureInput}} -> + {{ok, {_, EncodedSignature}}, {ok, {_, SignatureInput}}} -> + % The signature may be encoded ie. as binary, so we need to parse it further + % as a structured field + {item, {_, Signature}, _} = hb_http_structured_fields:parse_item(EncodedSignature), % The value encoded within signature input is also a structured field, - % that encodes the ComponentIdentifiers and the Signature Params. + % specifically an inner list that encodes the ComponentIdentifiers + % and the Signature Params. % - % So we parse this value, and then use it to construct out signature base - {list, ComponentIdentifiers, SigParams} = hb_http_structured_fields:list(SignatureInput), - Authority = authority(ComponentIdentifiers, SigParams, Key), + % So we must parse this value, and then use it to construct the signature base + [{list, ComponentIdentifiers, SigParams}] = hb_http_structured_fields:parse_list(SignatureInput), + % TODO: HACK convert parsed sig params into a map that authority() can handle + % maybe authority() should handle both parsed and unparsed SigParams, similar to ComponentIdentifiers + SigParamsMap = lists:foldl( + % TODO: does not support SF decimal params + fun + ({Name, {_Kind, Value}}, Map) -> maps:put(Name, Value, Map); + ({Name, Value}, Map) -> maps:put(Name, Value, Map) + end, + #{}, + SigParams + ), + % Construct the signature base using the parsed parameters + Authority = authority(ComponentIdentifiers, SigParamsMap, Key), {_, SignatureBase} = signature_base(Authority, Req, Res), {_Priv, Pub} = maps:get(key_pair, Authority), % Now verify the signature base signed with the provided key matches the signature @@ -181,7 +197,7 @@ signature_base(Authority, Req, Res) when is_map(Authority) -> ComponentIdentifiers = maps:get(component_identifiers, Authority), ComponentsLine = signature_components_line(ComponentIdentifiers, Req, Res), ParamsLine = signature_params_line(ComponentIdentifiers, maps:get(sig_params, Authority)), - SignatureBase = <>/binary, <<"\"@signature-params\": ">>/binary, ParamsLine/binary>>, + SignatureBase = join_signature_base(ComponentsLine, ParamsLine), {ParamsLine, SignatureBase}. join_signature_base(ComponentsLine, ParamsLine) -> @@ -329,11 +345,7 @@ extract_field({item, {_Kind, IParsed}, IParams}, Req, Res) -> %%% along with encoded value extract_field_value(RawFields, [Key, IsStrictFormat, IsByteSequenceEncoded]) -> % TODO: (maybe this already works?) empty string for empty header - HasKey = - case Key of - false -> false; - _ -> true - end, + HasKey = case Key of false -> false; _ -> true end, case not (HasKey orelse IsStrictFormat orelse IsByteSequenceEncoded) of % https://datatracker.ietf.org/doc/html/rfc9421#section-2.1-5 true -> @@ -697,21 +709,23 @@ verify_test() -> Authority = authority(ComponentIdentifiers, SigParams, Key), % Create the signature and signature input + % TODO: maybe return the SF data structures instead, to make appending to headers easier? + % OR we could wrap behind an api ie. sf_dictionary_put(Key, SfValue, Dict) {ok, {SignatureInput, Signature}} = sign(Authority, Req, Res), - SigName = <<"awesome">>, + [ParsedSignatureInput] = hb_http_structured_fields:parse_list(SignatureInput), NewHeaders = maps:merge( maps:get(headers, Res), #{ - <<"signature">> => hb_http_structured_fields:dictionary(#{ SigName => {item, {string, Signature}, []} }), - <<"signature-input">> => hb_http_structured_fields:dictionary(#{ SigName => {item, {string, SignatureInput}, []} }) - } + % https://datatracker.ietf.org/doc/html/rfc9421#section-4.2-1 + <<"signature">> => bin(hb_http_structured_fields:dictionary(#{ SigName => {item, {binary, Signature}, []} })), + <<"signature-input">> => bin(hb_http_structured_fields:dictionary(#{ SigName => ParsedSignatureInput })) + } ), SignedRes = maps:put(headers, NewHeaders, Res), - Result = verify(#{ sig_name => SigName, key => Pub }, Req, SignedRes), - erlang:display(Result), + erlang:display({"Result", Result}), ok. join_signature_base_test() ->