diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl new file mode 100644 index 000000000..ae44e101e --- /dev/null +++ b/include/ejabberd_acme.hrl @@ -0,0 +1,15 @@ + +-record(challenge, { + type = <<"http-01">> :: bitstring(), + status = pending :: pending | valid | invalid, + uri = <<"">> :: bitstring(), + token = <<"">> :: bitstring() + }). + +-type nonce() :: string(). +-type url() :: string(). +-type proplist() :: [{_, _}]. +-type jws() :: map(). +-type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). + +-type acme_challenge() :: #challenge{}. diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index ee7b3034a..f2497840d 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -1,8 +1,7 @@ -module(acme_challenge). -export ([ key_authorization/2 - , challenges_to_objects/1 - , solve_challenges/2 + , solve_challenge/3 ]). %% Challenge Types %% ================ @@ -10,52 +9,79 @@ %% 2. dns-01: https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7.3 %% 3. tls-sni-01: https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7.4 %% 4. (?) oob-01: https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7.5 --define(DEFAULT_HTTP_DIR, "../test-server-for-acme"). --record(challenge, { - type = <<"http-01">> :: bitstring(), - status = pending :: pending | valid | invalid, - uri = <<"">> :: bitstring(), - token = <<"">> :: bitstring() - }). +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("xmpp.hrl"). +-include("ejabberd_acme.hrl"). + +-spec parse_challenge(string(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> Thumbprint = jose_jwk:thumbprint(Key), - io:format("Thumbprint: ~p~n", [Thumbprint]), - + % ?INFO_MSG("Thumbprint: ~p~n", [Thumbprint]), KeyAuthorization = erlang:iolist_to_binary([Token, <<".">>, Thumbprint]), - % io:format("KeyAuthorization: ~p~n", [KeyAuthorization]), + KeyAuthorization. - KeyAuthorization. - -challenges_to_objects(Challenges) -> - [clean_challenge(X) || {X} <- Challenges]. - -clean_challenge(Challenge) -> - {<<"type">>,Type} = proplists:lookup(<<"type">>, Challenge), - {<<"status">>,Status} = proplists:lookup(<<"status">>, Challenge), - {<<"uri">>,Uri} = proplists:lookup(<<"uri">>, Challenge), - {<<"token">>,Token} = proplists:lookup(<<"token">>, Challenge), - #challenge{ - type = Type, - status = list_to_atom(bitstring_to_list(Status)), - uri = Uri, - token = Token - }. - -solve_challenges(Challenges, Key) -> - [solve_challenge(X, Key) || X <- Challenges]. +-spec parse_challenge({proplist()}) -> {ok, acme_challenge()} | {error, _}. +parse_challenge(Challenge0) -> + try + {Challenge} = Challenge0, + {<<"type">>,Type} = proplists:lookup(<<"type">>, Challenge), + {<<"status">>,Status} = proplists:lookup(<<"status">>, Challenge), + {<<"uri">>,Uri} = proplists:lookup(<<"uri">>, Challenge), + {<<"token">>,Token} = proplists:lookup(<<"token">>, Challenge), + Res = #challenge{ + type = Type, + status = list_to_atom(bitstring_to_list(Status)), + uri = Uri, + token = Token + }, + {ok, Res} + catch + _:Error -> + {error, Error} + end. -solve_challenge(Chal = #challenge{type = <<"http-01">>, token=Tkn}, Key) -> - io:format("Http Challenge: ~p~n", [Chal]), + +-spec solve_challenge(bitstring(), [{proplist()}], _) -> {ok, url(), bitstring()} | {error, _}. +solve_challenge(ChallengeType, Challenges, Options) -> + ParsedChallenges = [parse_challenge(Chall) || Chall <- Challenges], + case lists:any(fun is_error/1, ParsedChallenges) of + true -> + ?ERROR_MSG("Error parsing challenges: ~p~n", [Challenges]), + {error, parse_challenge}; + false -> + case [C || {ok, C} <- ParsedChallenges, is_challenge_type(ChallengeType, C)] of + [Challenge] -> + solve_challenge1(Challenge, Options); + _ -> + ?ERROR_MSG("Challenge ~p not found in challenges: ~p~n", [ChallengeType, Challenges]), + {error, not_found} + end + end. + +-spec solve_challenge1(acme_challenge(), _) -> {ok, url(), bitstring()} | {error, _}. +solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, {Key, HttpDir}) -> KeyAuthz = key_authorization(Tkn, Key), - io:format("KeyAuthorization: ~p~n", [KeyAuthz]), + FileLocation = HttpDir ++ "/.well-known/acme-challenge/" ++ bitstring_to_list(Tkn), + case file:write_file(FileLocation, KeyAuthz) of + ok -> + {ok, Chal#challenge.uri, KeyAuthz}; + {error, _} = Err -> + ?ERROR_MSG("Error writing to file: ~s with reason: ~p~n", [FileLocation, Err]), + Err + end; +solve_challenge1(Challenge, _Key) -> + ?INFO_MSG("Challenge: ~p~n", [Challenge]). - %% Create file for authorization - ok = file:write_file(?DEFAULT_HTTP_DIR ++ - "/.well-known/acme-challenge/" ++ - bitstring_to_list(Tkn), KeyAuthz), - {<<"http-01">>, Chal#challenge.uri, KeyAuthz}; -solve_challenge(Challenge, Key) -> - io:format("Challenge: ~p~n", [Challenge]). \ No newline at end of file +%% Useful functions + +is_challenge_type(DesiredType, #challenge{type = Type}) when DesiredType =:= Type -> + true; +is_challenge_type(_DesiredType, #challenge{type = _Type}) -> + false. + +is_error({error, _}) -> true; +is_error(_) -> false. \ No newline at end of file diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 6c4e2d6b7..70f835556 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,8 +1,6 @@ -module (ejabberd_acme). --export([ scenario/3 - , scenario0/1 - , directory/1 +-export([ directory/1 , get_account/3 , new_account/4 @@ -12,23 +10,21 @@ , new_authz/4 , get_authz/1 + + , scenario/3 + , scenario0/2 ]). -include("ejabberd.hrl"). -include("logger.hrl"). -include("xmpp.hrl"). + +-include("ejabberd_acme.hrl"). -include_lib("public_key/include/public_key.hrl"). -define(REQUEST_TIMEOUT, 5000). % 5 seconds. --type nonce() :: string(). --type url() :: string(). --type proplist() :: [{_, _}]. --type jws() :: map(). --type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Get Directory @@ -163,6 +159,12 @@ get_tos(Head) -> none end. +-spec get_challenges(proplist()) -> [{proplist()}]. +get_challenges(Body) -> + {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), + Challenges. + + %% TODO: Fix the duplicated code at the below 4 functions -spec make_post_request(url(), bitstring()) -> @@ -229,21 +231,6 @@ prepare_get_request(Url, HandleRespFun) -> Error end. --spec sign_json_jose(jose_jwk:key(), string()) -> jws(). -sign_json_jose(Key, Json) -> - PubKey = jose_jwk:to_public(Key), - {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), - PubKeyJson = jiffy:decode(BinaryPubKey), - % Jws object containing the algorithm - %% TODO: Dont hardcode the alg - JwsObj = jose_jws:from( - #{ <<"alg">> => <<"ES256">> - % , <<"b64">> => true - , <<"jwk">> => PubKeyJson - }), - %% Signed Message - jose_jws:sign(Key, Json, JwsObj). - -spec sign_json_jose(jose_jwk:key(), string(), nonce()) -> jws(). sign_json_jose(Key, Json, Nonce) -> % Generate a public key @@ -321,6 +308,7 @@ scenario(CAUrl, AccId, PrivateKey) -> AccURL = CAUrl ++ "/acme/reg/" ++ AccId, {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0), + ?INFO_MSG("Account: ~p~n", [Account]), #{"new-authz" := NewAuthz} = Dirs, Req = @@ -335,7 +323,7 @@ scenario(CAUrl, AccId, PrivateKey) -> {Account, Authz, PrivateKey}. -new_user_scenario(CAUrl) -> +new_user_scenario(CAUrl, HttpDir) -> PrivateKey = generate_key(), DirURL = CAUrl ++ "/directory", @@ -381,6 +369,12 @@ new_user_scenario(CAUrl) -> {ok, Authz2, Nonce5} = get_authz(AuthzUrl), + Challenges = get_challenges(Authz2), + ?INFO_MSG("Challenges: ~p~n", [Challenges]), + + {ok, ChallengeUrl, KeyAuthz} = acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), + ?INFO_MSG("File for http-01 challenge written correctly", []), + {Account2, Authz2, PrivateKey}. @@ -388,9 +382,8 @@ generate_key() -> jose_jwk:generate_key({ec, secp256r1}). %% Just a test -scenario0(KeyFile) -> +scenario0(KeyFile, HttpDir) -> PrivateKey = jose_jwk:from_file(KeyFile), - scenario("http://localhost:4000", "2", PrivateKey). - % new_user_scenario("http://localhost:4000"). + % scenario("http://localhost:4000", "2", PrivateKey). + new_user_scenario("http://localhost:4000", HttpDir). -% ejabberd_acme:scenario0("/home/konstantinos/Desktop/Programming/ejabberd/private_key_temporary").