Skip to content

Commit

Permalink
feat(hb_http_signature): wip signature base and signatures match befo…
Browse files Browse the repository at this point in the history
…re/after parsing #13
  • Loading branch information
TillaTheHun0 committed Dec 10, 2024
1 parent 66ee0b4 commit a5c06d9
Showing 1 changed file with 55 additions and 41 deletions.
96 changes: 55 additions & 41 deletions src/hb_http_signature.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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() :: #{
Expand Down Expand Up @@ -72,34 +74,22 @@

%%% @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(
[binary() | component_identifier()],
#{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">>}}]}
Expand All @@ -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
}.
Expand All @@ -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}}.

Expand All @@ -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
Expand All @@ -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 = <<ComponentsLine/binary, <<"\n">>/binary, <<"\"@signature-params\": ">>/binary, ParamsLine/binary>>,
SignatureBase = join_signature_base(ComponentsLine, ParamsLine),
{ParamsLine, SignatureBase}.

join_signature_base(ComponentsLine, ParamsLine) ->
Expand Down Expand Up @@ -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 ->
Expand Down Expand Up @@ -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() ->
Expand Down

0 comments on commit a5c06d9

Please sign in to comment.