25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-22 16:20:52 +01:00

Solve http-01 challenge

This commit is contained in:
Konstantinos Kallas 2017-06-03 12:34:15 +03:00
parent ddb043aa71
commit df5d673e63
2 changed files with 191 additions and 39 deletions

61
src/acme_challenge.erl Normal file
View File

@ -0,0 +1,61 @@
-module(acme_challenge).
-export ([ key_authorization/2
, challenges_to_objects/1
, solve_challenges/2
]).
%% Challenge Types
%% ================
%% 1. http-01: https://tools.ietf.org/html/draft-ietf-acme-acme-05#section-7.2
%% 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()
}).
key_authorization(Token, Key) ->
Thumbprint = jose_jwk:thumbprint(Key),
io:format("Thumbprint: ~p~n", [Thumbprint]),
KeyAuthorization = erlang:iolist_to_binary([Token, <<".">>, Thumbprint]),
% io:format("KeyAuthorization: ~p~n", [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].
solve_challenge(Chal = #challenge{type = <<"http-01">>, token=Tkn}, Key) ->
io:format("Http Challenge: ~p~n", [Chal]),
KeyAuthz = key_authorization(Tkn, Key),
io:format("KeyAuthorization: ~p~n", [KeyAuthz]),
%% 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]).

View File

@ -35,15 +35,31 @@
-include_lib("public_key/include/public_key.hrl").
% -define(CA_URL, "https://acme-v01.api.letsencrypt.org").
-define(CA_URL, "https://acme-staging.api.letsencrypt.org").
% -define(CA_URL, "http://localhost:4000").
-define(DEFAULT_DIRECTORY, ?CA_URL ++ "/directory").
-define(DEFAULT_NEW_NONCE, ?CA_URL ++ "/acme/new_nonce").
-define(DEFAULT_ACCOUNT, "2273801").
-define(DEFAULT_KEY_FILE, "private_key_temporary").
-define(LOCAL_TESTING, true).
-ifdef(LOCAL_TESTING).
-define(CA_URL, "http://localhost:4000").
-define(DEFAULT_ACCOUNT, "2").
-define(DEFAULT_TOS, <<"http://boulder:4000/terms/v1">>).
-define(DEFAULT_AUTHZ,
<<"http://localhost:4000/acme/authz/XDAfMW6xBdRogD2-VIfTxlzo4RTlaE2U6x0yrwxnXlw">>).
-else.
-define(CA_URL, "https://acme-staging.api.letsencrypt.org").
-define(DEFAULT_ACCOUNT, "2273801").
-define(DEFAULT_TOS, <<"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf">>).
-define(DEFAULT_AUTHZ, <<"">>).
-endif.
-record(state, {
ca_url = ?CA_URL :: list(),
@ -94,10 +110,10 @@ get_certificate(Pid, Options) ->
ok.
get_authz(Pid, Options) ->
ok.
gen_server:call(Pid, ?FUNCTION_NAME).
complete_challenge(Pid, Options) ->
ok.
gen_server:call(Pid, {?FUNCTION_NAME, Options}).
deactivate_authz(Pid, Options) ->
ok.
@ -127,8 +143,7 @@ handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) ->
{ok, {_Status, Head, Body}} = httpc:request(get, {Url, []}, [], []),
%% Decode the json string
Result = jiffy:decode(Body),
{Directories} = Result,
{Directories} = jiffy:decode(Body),
StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} ||
{X,Y} <- Directories],
@ -137,11 +152,10 @@ handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) ->
Nonce = get_nonce(Head),
%% Update the directories in state
%% TODO: Get the merge of the old and the new dictionary
NewDirs = maps:from_list(StrDirectories),
% io:format("New directories: ~p~n", [NewDirs]),
{reply, {ok, Result}, S#state{dirs = NewDirs, nonce = Nonce}};
{reply, {ok, {Directories}}, S#state{dirs = NewDirs, nonce = Nonce}};
handle_call(new_nonce, _From, S = #state{dirs=Dirs}) ->
%% Get url from all directories
#{"new_nonce" := Url} = Dirs,
@ -166,7 +180,7 @@ handle_call(new_reg, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) -
%% Jose
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
io:format("Signed Body: ~p~n", [SignedBody]),
% io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
@ -193,7 +207,7 @@ handle_call({account_info, AccountId}, _From, S = #state{ca_url = Ca, dirs=Dirs,
%% Jose
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
io:format("Signed Body: ~p~n", [SignedBody]),
% io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
@ -221,7 +235,7 @@ handle_call({update_account, AccountId}, _From, S = #state{ca_url = Ca, dirs=Dir
%% Jose
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
io:format("Signed Body: ~p~n", [SignedBody]),
% io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
@ -248,7 +262,7 @@ handle_call(new_cert, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce})
%% Jose
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
io:format("Signed Body: ~p~n", [SignedBody]),
% io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
@ -266,8 +280,13 @@ handle_call(new_authz, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce})
#{"new-authz" := Url} = Dirs,
%% Make the request body
ReqBody = jiffy:encode({[
{ <<"resource">>, <<"new-authz">>}
ReqBody = jiffy:encode({
[ { <<"identifier">>, {
[ {<<"type">>, <<"dns">>}
, {<<"value">>, <<"my-acme-test-ejabberd.com">>}
] }}
, {<<"existing">>, <<"accept">>}
, { <<"resource">>, <<"new-authz">>}
] }),
%% Get the key from a file
@ -275,7 +294,50 @@ handle_call(new_authz, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce})
%% Jose
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
io:format("Signed Body: ~p~n", [SignedBody]),
% io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
%% Post request
{ok, {Status, Head, Body}} =
httpc:request(post, {Url, [], "application/jose+json", FinalBody}, [], []),
% Get and save the new nonce
NewNonce = get_nonce(Head),
{reply, {ok, {Status, Head, Body}}, S#state{nonce=NewNonce}};
handle_call(get_authz, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
%% Get url from all directories
Url = bitstring_to_list(?DEFAULT_AUTHZ),
%% Post request
{ok, {Status, Head, Body}} =
% httpc:request(post, {Url, [], "application/jose+json", FinalBody}, [], []),
httpc:request(Url),
% Get and save the new nonce
NewNonce = get_nonce(Head),
{reply, {ok, {Status, Head, Body}}, S#state{nonce=NewNonce}};
handle_call({complete_challenge, [Solution]}, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
%% Get url from all directories
{ChallengeType, BitUrl, KeyAuthz} = Solution,
Url = bitstring_to_list(BitUrl),
%% Make the request body
ReqBody = jiffy:encode({
[ { <<"keyAuthorization">>, KeyAuthz}
, {<<"type">>, ChallengeType}
, { <<"resource">>, <<"challenge">>}
] }),
%% Get the key from a file
Key = jose_jwk:from_file(?DEFAULT_KEY_FILE),
%% Jose
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
% io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
@ -314,6 +376,10 @@ get_nonce(Head) ->
{"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head),
Nonce.
get_challenges({Body}) ->
{<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body),
Challenges.
%% Test
@ -321,7 +387,6 @@ generate_key() ->
% Generate a key for now
Key = jose_jwk:generate_key({ec, secp256r1}),
io:format("Key: ~p~n", [Key]),
Key.
sign_a_json_object_using_jose(Key, Json, Url, Nonce) ->
@ -331,17 +396,11 @@ sign_a_json_object_using_jose(Key, Json, Url, Nonce) ->
{_, BinaryPubKey} = jose_jwk:to_binary(PubKey),
% io:format("Public Key: ~p~n", [BinaryPubKey]),
PubKeyJson = jiffy:decode(BinaryPubKey),
io:format("Public Key: ~p~n", [PubKeyJson]),
% KeyOkp = jose_jwk:to_okp(Key),
% io:format("Key Okp: ~p~n", [KeyOkp]),
% io:format("Public Key: ~p~n", [PubKeyJson]),
% Jws object containing the algorithm
JwsObj = jose_jws:from(
#{
% <<"alg">> => <<"HS256">>
<<"alg">> => <<"ES256">>
#{ <<"alg">> => <<"ES256">>
%% Im not sure if it is needed
% , <<"b64">> => true
, <<"jwk">> => PubKeyJson
@ -349,14 +408,13 @@ sign_a_json_object_using_jose(Key, Json, Url, Nonce) ->
}),
% io:format("Jws: ~p~n", [JwsObj]),
%% Signed Message
Signed = jose_jws:sign(Key, Json, JwsObj),
% io:format("Signed: ~p~n", [Signed]),
%% Peek protected
Protected = jose_jws:peek_protected(Signed),
io:format("Protected: ~p~n", [jiffy:decode(Protected)]),
% io:format("Protected: ~p~n", [jiffy:decode(Protected)]),
%% Peek Payload
Payload = jose_jws:peek_payload(Signed),
@ -369,16 +427,16 @@ sign_a_json_object_using_jose(Key, Json, Url, Nonce) ->
Signed.
scenario() ->
% scenario_new_account().
scenario_old_account().
scenario_old_account() ->
{ok, Pid} = start(),
io:format("Server started: ~p~n", [Pid]),
{ok, Result} = directory(Pid, []),
io:format("Directory result: ~p~n", [Result]),
% %% Request the creation of a new account
% {ok, {Status, Head, Body}} = new_reg(Pid, []),
% io:format("New account~nHead: ~p~nBody: ~p~n", [{Status, Head}, jiffy:decode(Body)]),
%% Get the info of an existing account
% {ok, {Status1, Head1, Body1}} = account_info(Pid, ?DEFAULT_ACCOUNT),
% io:format("Account: ~p~nHead: ~p~nBody: ~p~n",
@ -389,14 +447,47 @@ scenario() ->
io:format("Account: ~p~nHead: ~p~nBody: ~p~n",
[?DEFAULT_ACCOUNT, {Status1, Head1}, jiffy:decode(Body1)]),
%% New authorization
% {ok, {Status2, Head2, Body2}} = new_authz(Pid, []),
% io:format("New Authz~nHead: ~p~nBody: ~p~n",
% [{Status2, Head2}, jiffy:decode(Body2)]),
%% Get authorization
{ok, {Status2, Head2, Body2}} = get_authz(Pid, []),
io:format("Get Authz~nHead: ~p~nBody: ~p~n",
[{Status2, Head2}, jiffy:decode(Body2)]),
% Challenges = get_challenges(jiffy:decode(Body2)),
% % io:format("Challenges: ~p~n", [Challenges]),
% ChallengeObjects = acme_challenge:challenges_to_objects(Challenges),
% % io:format("Challenges: ~p~n", [ChallengeObjects]),
% %% Create a key-authorization
% Key = jose_jwk:from_file(?DEFAULT_KEY_FILE),
% % acme_challenge:key_authorization(<<"pipi">>, Key),
% Solutions = acme_challenge:solve_challenges(ChallengeObjects, Key),
% io:format("Solutions: ~p~n", [Solutions]),
% {ok, {Status3, Head3, Body3}} =
% complete_challenge(Pid, [X || X <- Solutions, X =/= ok]),
% io:format("Complete_challenge~nHead: ~p~nBody: ~p~n",
% [{Status3, Head3}, jiffy:decode(Body3)]),
%% New certification
% {ok, {Status2, Head2, Body2}} = new_cert(Pid, []),
% io:format("New Cert~nHead: ~p~nBody: ~p~n",
% [{Status2, Head2}, jiffy:decode(Body2)]),
%% New authorization
{ok, {Status2, Head2, Body2}} = new_authz(Pid, []),
io:format("New Authz~nHead: ~p~nBody: ~p~n",
[{Status2, Head2}, jiffy:decode(Body2)]),
ok.
scenario_new_account() ->
{ok, Pid} = start(),
io:format("Server started: ~p~n", [Pid]),
{ok, Result} = directory(Pid, []),
io:format("Directory result: ~p~n", [Result]),
%% Request the creation of a new account
{ok, {Status, Head, Body}} = new_reg(Pid, []),
io:format("New account~nHead: ~p~nBody: ~p~n", [{Status, Head}, jiffy:decode(Body)]).