2017-06-03 11:34:15 +02:00
|
|
|
-module(acme_challenge).
|
|
|
|
|
2017-07-12 18:23:52 +02:00
|
|
|
-export ([key_authorization/2,
|
|
|
|
solve_challenge/3,
|
2017-11-14 13:12:33 +01:00
|
|
|
process/2,
|
2017-11-15 08:01:30 +01:00
|
|
|
register_hooks/1,
|
|
|
|
unregister_hooks/1,
|
|
|
|
acme_handler/3
|
2017-06-03 11:34:15 +02:00
|
|
|
]).
|
|
|
|
%% 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
|
|
|
|
|
2017-06-17 18:06:39 +02:00
|
|
|
-include("ejabberd.hrl").
|
|
|
|
-include("logger.hrl").
|
|
|
|
-include("xmpp.hrl").
|
2017-07-12 18:23:52 +02:00
|
|
|
-include("ejabberd_http.hrl").
|
2017-06-17 18:06:39 +02:00
|
|
|
-include("ejabberd_acme.hrl").
|
|
|
|
|
2017-11-14 13:12:33 +01:00
|
|
|
%% This is the default endpoint for the http challenge
|
2017-11-15 08:01:30 +01:00
|
|
|
%% This hooks is called from ejabberd_http
|
|
|
|
acme_handler(Handlers, _Host, Request) ->
|
|
|
|
case Request#request.path of
|
|
|
|
[<<".well-known">>|_] ->
|
|
|
|
[{[<<".well-known">>],acme_challenge}|Handlers];
|
|
|
|
_ ->
|
|
|
|
Handlers
|
|
|
|
end.
|
2017-08-25 11:08:16 +02:00
|
|
|
|
2017-07-12 18:23:52 +02:00
|
|
|
%% TODO: Maybe validate request here??
|
2017-08-25 11:08:16 +02:00
|
|
|
process(LocalPath, _Request) ->
|
2017-07-12 18:23:52 +02:00
|
|
|
Result = ets_get_key_authorization(LocalPath),
|
|
|
|
{200,
|
|
|
|
[{<<"Content-Type">>, <<"text/plain">>}],
|
|
|
|
Result}.
|
|
|
|
|
2017-11-15 08:01:30 +01:00
|
|
|
register_hooks(_Domain) ->
|
|
|
|
?INFO_MSG("Registering hook for ACME HTTP headers", []),
|
|
|
|
ejabberd_hooks:add(http_request_handlers, ?MODULE, acme_handler, 50).
|
|
|
|
|
|
|
|
unregister_hooks(_Domain) ->
|
|
|
|
?INFO_MSG("Unregistering hook for ACME HTTP headers", []),
|
|
|
|
ejabberd_hooks:delete(http_request_handlers, ?MODULE, acme_handler, 50).
|
2017-07-12 18:23:52 +02:00
|
|
|
|
2017-06-26 18:03:21 +02:00
|
|
|
-spec key_authorization(bitstring(), jose_jwk:key()) -> bitstring().
|
2017-06-03 11:34:15 +02:00
|
|
|
key_authorization(Token, Key) ->
|
2017-06-22 13:47:56 +02:00
|
|
|
Thumbprint = jose_jwk:thumbprint(Key),
|
|
|
|
KeyAuthorization = erlang:iolist_to_binary([Token, <<".">>, Thumbprint]),
|
|
|
|
KeyAuthorization.
|
2017-06-03 11:34:15 +02:00
|
|
|
|
2017-06-17 18:06:39 +02:00
|
|
|
-spec parse_challenge({proplist()}) -> {ok, acme_challenge()} | {error, _}.
|
|
|
|
parse_challenge(Challenge0) ->
|
2017-06-22 13:47:56 +02:00
|
|
|
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),
|
2017-06-26 18:03:21 +02:00
|
|
|
Res =
|
2017-06-22 13:47:56 +02:00
|
|
|
#challenge{
|
|
|
|
type = Type,
|
|
|
|
status = list_to_atom(bitstring_to_list(Status)),
|
|
|
|
uri = bitstring_to_list(Uri),
|
|
|
|
token = Token
|
|
|
|
},
|
|
|
|
{ok, Res}
|
|
|
|
catch
|
|
|
|
_:Error ->
|
|
|
|
{error, Error}
|
|
|
|
end.
|
2017-06-03 11:34:15 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
2017-06-26 18:03:21 +02:00
|
|
|
-spec solve_challenge(bitstring(), [{proplist()}], _) ->
|
|
|
|
{ok, url(), bitstring()} | {error, _}.
|
2017-06-17 18:06:39 +02:00
|
|
|
solve_challenge(ChallengeType, Challenges, Options) ->
|
2017-06-22 13:47:56 +02:00
|
|
|
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.
|
2017-06-03 11:34:15 +02:00
|
|
|
|
2017-06-26 18:03:21 +02:00
|
|
|
-spec solve_challenge1(acme_challenge(), {jose_jwk:key(), string()}) ->
|
|
|
|
{ok, url(), bitstring()} | {error, _}.
|
2017-07-17 09:48:57 +02:00
|
|
|
solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, Key) ->
|
2017-06-22 13:47:56 +02:00
|
|
|
KeyAuthz = key_authorization(Tkn, Key),
|
2017-07-12 18:23:52 +02:00
|
|
|
%% save_key_authorization(Chal, Tkn, KeyAuthz, HttpDir);
|
|
|
|
ets_put_key_authorization(Tkn, KeyAuthz),
|
|
|
|
{ok, Chal#challenge.uri, KeyAuthz};
|
|
|
|
solve_challenge1(Challenge, _Key) ->
|
2018-01-27 17:35:38 +01:00
|
|
|
?ERROR_MSG("Unknown Challenge Type: ~p", [Challenge]),
|
2017-08-19 16:47:05 +02:00
|
|
|
{error, unknown_challenge}.
|
2017-07-12 18:23:52 +02:00
|
|
|
|
|
|
|
|
|
|
|
-spec ets_put_key_authorization(bitstring(), bitstring()) -> ok.
|
|
|
|
ets_put_key_authorization(Tkn, KeyAuthz) ->
|
|
|
|
Tab = ets_get_acme_table(),
|
|
|
|
Key = [<<"acme-challenge">>, Tkn],
|
|
|
|
ets:insert(Tab, {Key, KeyAuthz}),
|
|
|
|
ok.
|
|
|
|
|
|
|
|
-spec ets_get_key_authorization([bitstring()]) -> bitstring().
|
|
|
|
ets_get_key_authorization(Key) ->
|
|
|
|
Tab = ets_get_acme_table(),
|
2017-09-06 17:10:38 +02:00
|
|
|
case ets:lookup(Tab, Key) of
|
2017-07-12 18:23:52 +02:00
|
|
|
[{Key, KeyAuthz}] ->
|
2017-09-06 17:10:38 +02:00
|
|
|
ets:delete(Tab, Key),
|
2017-07-12 18:23:52 +02:00
|
|
|
KeyAuthz;
|
|
|
|
_ ->
|
|
|
|
?ERROR_MSG("Unable to serve key authorization in: ~p", [Key]),
|
|
|
|
<<"">>
|
|
|
|
end.
|
|
|
|
|
|
|
|
-spec ets_get_acme_table() -> atom().
|
|
|
|
ets_get_acme_table() ->
|
|
|
|
case ets:info(acme) of
|
|
|
|
undefined ->
|
|
|
|
ets:new(acme, [named_table, public]);
|
|
|
|
_ ->
|
|
|
|
acme
|
|
|
|
end.
|
2017-06-17 18:06:39 +02:00
|
|
|
|
|
|
|
%% Useful functions
|
|
|
|
|
|
|
|
is_challenge_type(DesiredType, #challenge{type = Type}) when DesiredType =:= Type ->
|
2017-06-22 13:47:56 +02:00
|
|
|
true;
|
2017-06-17 18:06:39 +02:00
|
|
|
is_challenge_type(_DesiredType, #challenge{type = _Type}) ->
|
2017-06-22 13:47:56 +02:00
|
|
|
false.
|
2017-06-17 18:06:39 +02:00
|
|
|
|
2017-06-26 18:03:21 +02:00
|
|
|
-spec is_error({'error', _}) -> 'true';
|
|
|
|
({'ok', _}) -> 'false'.
|
2017-06-17 18:06:39 +02:00
|
|
|
is_error({error, _}) -> true;
|
2017-06-22 13:47:56 +02:00
|
|
|
is_error(_) -> false.
|