24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-07-02 23:06:21 +02:00
xmpp.chapril.org-ejabberd/src/mod_acme.erl

403 lines
11 KiB
Erlang
Raw Normal View History

2017-05-08 14:35:11 +02:00
-module(mod_acme).
-behaviour(gen_server).
%% API
-export([ start/0
, stop/1
%% I tried to follow the naming convention found in the acme spec
, directory/2
, new_nonce/2
%% Account
2017-05-17 15:55:26 +02:00
, new_reg/2
2017-05-08 14:35:11 +02:00
, update_account/2
, account_info/2 %% TODO: Maybe change to get_account
, account_key_change/2
, deactivate_account/2
%% Orders/Certificates
2017-05-17 15:55:26 +02:00
, new_cert/2
2017-05-08 14:35:11 +02:00
, new_authz/2
, get_certificate/2
, get_authz/2
, complete_challenge/2
, deactivate_authz/2
, revoke_cert/2]).
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]).
-export([scenario/0]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("xmpp.hrl").
2017-05-15 00:41:09 +02:00
-include_lib("public_key/include/public_key.hrl").
2017-05-08 14:35:11 +02:00
% -define(CA_URL, "https://acme-v01.api.letsencrypt.org").
-define(CA_URL, "https://acme-staging.api.letsencrypt.org").
2017-05-15 00:41:09 +02:00
% -define(CA_URL, "http://localhost:4000").
2017-05-08 14:35:11 +02:00
2017-05-08 19:29:58 +02:00
-define(DEFAULT_DIRECTORY, ?CA_URL ++ "/directory").
-define(DEFAULT_NEW_NONCE, ?CA_URL ++ "/acme/new_nonce").
2017-05-17 15:55:26 +02:00
-define(DEFAULT_ACCOUNT, "2273801").
-define(DEFAULT_KEY_FILE, "private_key_temporary").
-define(DEFAULT_TOS, <<"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf">>).
2017-05-08 14:35:11 +02:00
-record(state, {
ca_url = ?CA_URL :: list(),
dir_url = ?DEFAULT_DIRECTORY :: list(),
2017-05-15 00:41:09 +02:00
dirs = maps:new(),
2017-05-17 15:55:26 +02:00
nonce = "",
account = none
2017-05-08 14:35:11 +02:00
}).
%% This will be initially just be filled with stub functions
start() ->
gen_server:start(?MODULE, [], []).
stop(Pid) ->
gen_server:stop(Pid).
%% Stub functions
directory(Pid, Options) ->
gen_server:call(Pid, ?FUNCTION_NAME).
new_nonce(Pid, Options) ->
gen_server:call(Pid, ?FUNCTION_NAME).
2017-05-17 15:55:26 +02:00
new_reg(Pid, Options) ->
2017-05-08 19:29:58 +02:00
gen_server:call(Pid, ?FUNCTION_NAME).
2017-05-08 14:35:11 +02:00
2017-05-17 15:55:26 +02:00
update_account(Pid, AccountId) ->
%% TODO: This has to have more info ofcourse
gen_server:call(Pid, {?FUNCTION_NAME, AccountId}).
2017-05-08 14:35:11 +02:00
2017-05-17 15:55:26 +02:00
account_info(Pid, AccountId) ->
gen_server:call(Pid, {?FUNCTION_NAME, AccountId}).
2017-05-08 14:35:11 +02:00
account_key_change(Pid, Options) ->
ok.
deactivate_account(Pid, Options) ->
ok.
2017-05-17 15:55:26 +02:00
new_cert(Pid, Options) ->
gen_server:call(Pid, ?FUNCTION_NAME).
2017-05-08 14:35:11 +02:00
new_authz(Pid, Options) ->
2017-05-17 15:55:26 +02:00
gen_server:call(Pid, ?FUNCTION_NAME).
2017-05-08 14:35:11 +02:00
get_certificate(Pid, Options) ->
ok.
get_authz(Pid, Options) ->
ok.
complete_challenge(Pid, Options) ->
ok.
deactivate_authz(Pid, Options) ->
ok.
revoke_cert(Pid, Options) ->
ok.
%% GEN SERVER
init([]) ->
%% TODO: Not the correct way of doing it
ok = application:start(inets),
ok = application:start(crypto),
ok = application:start(asn1),
ok = application:start(public_key),
ok = application:start(ssl),
2017-05-09 22:27:37 +02:00
ok = application:start(base64url),
ok = application:start(jose),
2017-05-08 14:35:11 +02:00
{ok, #state{}}.
2017-05-08 19:29:58 +02:00
handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) ->
%% Make the get request
2017-05-15 00:41:09 +02:00
{ok, {_Status, Head, Body}} = httpc:request(get, {Url, []}, [], []),
2017-05-08 19:29:58 +02:00
%% Decode the json string
2017-05-08 14:35:11 +02:00
Result = jiffy:decode(Body),
2017-05-08 19:29:58 +02:00
{Directories} = Result,
StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} ||
{X,Y} <- Directories],
2017-05-15 00:41:09 +02:00
% Find and save the replay nonce
% io:format("Directory Head Response: ~p~n", [Head]),
2017-05-17 15:55:26 +02:00
Nonce = get_nonce(Head),
2017-05-15 00:41:09 +02:00
2017-05-08 19:29:58 +02:00
%% 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]),
2017-05-15 00:41:09 +02:00
{reply, {ok, Result}, S#state{dirs = NewDirs, nonce = Nonce}};
2017-05-08 19:29:58 +02:00
handle_call(new_nonce, _From, S = #state{dirs=Dirs}) ->
%% Get url from all directories
#{"new_nonce" := Url} = Dirs,
{ok, {Status, Head, []}} =
httpc:request(head, {Url, []}, [], []),
{reply, {ok, {Status, Head}}, S};
2017-05-17 15:55:26 +02:00
handle_call(new_reg, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
2017-05-08 19:29:58 +02:00
%% Get url from all directories
#{"new-reg" := Url} = Dirs,
%% Make the request body
2017-05-15 00:41:09 +02:00
ReqBody = jiffy:encode({
2017-05-17 15:55:26 +02:00
[ { <<"contact">>, [<<"mailto:cert-admin@example.com">>]}
2017-05-15 00:41:09 +02:00
, { <<"resource">>, <<"new-reg">>}
]}),
2017-05-08 19:29:58 +02:00
2017-05-17 15:55:26 +02:00
%% Generate a key for the first time use
Key = generate_key(),
%% Write the key to a file
jose_jwk:to_file(?DEFAULT_KEY_FILE, Key),
%% 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),
%% 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({account_info, AccountId}, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
%% Get url from accountId
Url = Ca ++ "/acme/reg/" ++ AccountId,
%% Make the request body
ReqBody = jiffy:encode({[
{ <<"resource">>, <<"reg">>}
]}),
%% Get the key from a file
Key = jose_jwk:from_file(?DEFAULT_KEY_FILE),
2017-05-09 22:27:37 +02:00
%% Jose
2017-05-17 15:55:26 +02:00
{_, SignedBody} = sign_a_json_object_using_jose(Key, ReqBody, Url, Nonce),
2017-05-15 00:41:09 +02:00
io:format("Signed Body: ~p~n", [SignedBody]),
%% Encode the Signed body with jiffy
FinalBody = jiffy:encode(SignedBody),
2017-05-09 22:27:37 +02:00
2017-05-17 15:55:26 +02:00
%% Post request
2017-05-08 19:29:58 +02:00
{ok, {Status, Head, Body}} =
2017-05-15 00:41:09 +02:00
httpc:request(post, {Url, [], "application/jose+json", FinalBody}, [], []),
2017-05-17 15:55:26 +02:00
% Get and save the new nonce
NewNonce = get_nonce(Head),
{reply, {ok, {Status, Head, Body}}, S#state{nonce=NewNonce}};
handle_call({update_account, AccountId}, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
%% Get url from accountId
Url = Ca ++ "/acme/reg/" ++ AccountId,
%% Make the request body
ReqBody = jiffy:encode({[
{ <<"resource">>, <<"reg">>},
{ <<"agreement">>, ?DEFAULT_TOS}
]}),
%% 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),
%% 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(new_cert, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
%% Get url from all directories
#{"new-cert" := Url} = Dirs,
%% Make the request body
ReqBody = jiffy:encode({[
{ <<"resource">>, <<"new-cert">>}
]}),
%% 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),
%% 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(new_authz, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) ->
%% Get url from all directories
#{"new-authz" := Url} = Dirs,
%% Make the request body
ReqBody = jiffy:encode({[
{ <<"resource">>, <<"new-authz">>}
]}),
%% 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),
%% 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}};
2017-05-08 14:35:11 +02:00
handle_call(stop, _From, State) ->
{stop, normal, ok, State}.
handle_cast(Msg, State) ->
?WARNING_MSG("unexpected cast: ~p", [Msg]),
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
2017-05-08 19:29:58 +02:00
%% Util functions
2017-05-08 14:35:11 +02:00
final_url(Urls) ->
Joined = lists:join("/", Urls),
lists:flatten(Joined).
2017-05-17 15:55:26 +02:00
get_nonce(Head) ->
{"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head),
Nonce.
2017-05-08 19:29:58 +02:00
%% Test
2017-05-17 15:55:26 +02:00
generate_key() ->
2017-05-09 22:27:37 +02:00
% Generate a key for now
2017-05-15 00:41:09 +02:00
Key = jose_jwk:generate_key({ec, secp256r1}),
2017-05-09 22:27:37 +02:00
io:format("Key: ~p~n", [Key]),
2017-05-17 15:55:26 +02:00
Key.
sign_a_json_object_using_jose(Key, Json, Url, Nonce) ->
2017-05-15 00:41:09 +02:00
% Generate a public key
PubKey = jose_jwk:to_public(Key),
2017-05-17 15:55:26 +02:00
% io:format("Public Key: ~p~n", [PubKey]),
2017-05-15 00:41:09 +02:00
{_, BinaryPubKey} = jose_jwk:to_binary(PubKey),
2017-05-17 15:55:26 +02:00
% io:format("Public Key: ~p~n", [BinaryPubKey]),
2017-05-15 00:41:09 +02:00
PubKeyJson = jiffy:decode(BinaryPubKey),
io:format("Public Key: ~p~n", [PubKeyJson]),
% KeyOkp = jose_jwk:to_okp(Key),
% io:format("Key Okp: ~p~n", [KeyOkp]),
2017-05-09 22:27:37 +02:00
% Jws object containing the algorithm
2017-05-15 00:41:09 +02:00
JwsObj = jose_jws:from(
#{
% <<"alg">> => <<"HS256">>
<<"alg">> => <<"ES256">>
%% Im not sure if it is needed
% , <<"b64">> => true
, <<"jwk">> => PubKeyJson
, <<"nonce">> => list_to_bitstring(Nonce)
}),
2017-05-17 15:55:26 +02:00
% io:format("Jws: ~p~n", [JwsObj]),
2017-05-15 00:41:09 +02:00
2017-05-09 22:27:37 +02:00
%% Signed Message
Signed = jose_jws:sign(Key, Json, JwsObj),
2017-05-17 15:55:26 +02:00
% io:format("Signed: ~p~n", [Signed]),
2017-05-09 22:27:37 +02:00
2017-05-15 00:41:09 +02:00
%% Peek protected
Protected = jose_jws:peek_protected(Signed),
io:format("Protected: ~p~n", [jiffy:decode(Protected)]),
%% Peek Payload
Payload = jose_jws:peek_payload(Signed),
io:format("Payload: ~p~n", [jiffy:decode(Payload)]),
2017-05-09 22:27:37 +02:00
%% Verify
2017-05-17 15:55:26 +02:00
% {true, _} = jose_jws:verify(Key, Signed),
% io:format("Verify: ~p~n", [jose_jws:verify(Key, Signed)]),
2017-05-15 00:41:09 +02:00
2017-05-09 22:27:37 +02:00
Signed.
2017-05-08 14:35:11 +02:00
scenario() ->
{ok, Pid} = start(),
io:format("Server started: ~p~n", [Pid]),
{ok, Result} = directory(Pid, []),
io:format("Directory result: ~p~n", [Result]),
2017-05-17 15:55:26 +02:00
% %% 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",
% [?DEFAULT_ACCOUNT, {Status1, Head1}, jiffy:decode(Body1)]),
%% Update the account to agree to terms and services
{ok, {Status1, Head1, Body1}} = update_account(Pid, ?DEFAULT_ACCOUNT),
io:format("Account: ~p~nHead: ~p~nBody: ~p~n",
[?DEFAULT_ACCOUNT, {Status1, Head1}, jiffy:decode(Body1)]),
%% 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)]),
2017-05-08 14:35:11 +02:00
ok.
2017-05-08 19:29:58 +02:00