From 05362b9a7dda54e8dd1876e7686fa0dba7d0afca Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 8 May 2017 15:35:11 +0300 Subject: [PATCH 01/75] Very basic acme client, only stubs --- run_acme.sh | 3 + src/mod_acme.erl | 158 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100755 run_acme.sh create mode 100644 src/mod_acme.erl diff --git a/run_acme.sh b/run_acme.sh new file mode 100755 index 000000000..400ee04ac --- /dev/null +++ b/run_acme.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +erl -pa ebin deps/jiffy/ebin deps/fast_tls/ebin -noshell -s mod_acme scenario -s erlang halt \ No newline at end of file diff --git a/src/mod_acme.erl b/src/mod_acme.erl new file mode 100644 index 000000000..8b53130a3 --- /dev/null +++ b/src/mod_acme.erl @@ -0,0 +1,158 @@ +-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 + , new_account/2 + , update_account/2 + , account_info/2 %% TODO: Maybe change to get_account + , account_key_change/2 + , deactivate_account/2 + %% Orders/Certificates + , new_order/2 + , 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"). + +% -define(CA_URL, "https://acme-v01.api.letsencrypt.org"). +-define(CA_URL, "https://acme-staging.api.letsencrypt.org"). + +-define(DEFAULT_DIRECTORY, "directory"). + +-define(DEFAULT_NEW_NONCE, "acme/new_nonce"). + + +-record(dirs, { + new_nonce = ?DEFAULT_NEW_NONCE + }). + +-record(state, { + ca_url = ?CA_URL :: list(), + dir_url = ?DEFAULT_DIRECTORY :: list(), + dirs = #dirs{} + }). + +%% 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). + +new_account(Pid, Options) -> + ok. + +update_account(Pid, Options) -> + ok. + +account_info(Pid, Options) -> + ok. + +account_key_change(Pid, Options) -> + ok. + +deactivate_account(Pid, Options) -> + ok. + +new_order(Pid, Options) -> + ok. + +new_authz(Pid, Options) -> + ok. + +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), + {ok, #state{}}. + +handle_call(directory, _From, S = #state{ca_url = Ca, dir_url=Dir}) -> + Url = final_url([Ca, Dir]), + {ok, {_Status, _Head, Body}} = httpc:request(get, {Url, []}, [], []), + Result = jiffy:decode(Body), + {reply, {ok, Result}, S}; +handle_call(new_nonce, _From, S = #state{ca_url = Ca, dirs=Dirs}) -> + #dirs{new_nonce=New_nonce_url} = Dirs, + Url = final_url([Ca, New_nonce_url]), + {ok, {Status, Head, []}} = httpc:request(head, {Url, []}, [], []), + {reply, {ok, {Status, Head}}, S}; +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}. + + +final_url(Urls) -> + Joined = lists:join("/", Urls), + lists:flatten(Joined). + +scenario() -> + {ok, Pid} = start(), + io:format("Server started: ~p~n", [Pid]), + + {ok, Result} = directory(Pid, []), + io:format("Directory result: ~p~n", [Result]), + + {ok, Result1} = new_nonce(Pid, []), + io:format("New nonce result: ~p~n", [Result1]), + ok. From 67a00939dbaa7d3a2105ec98f0ed7b8dd436f7dc Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 8 May 2017 20:29:58 +0300 Subject: [PATCH 02/75] Small improvements to the acme module --- src/mod_acme.erl | 61 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/src/mod_acme.erl b/src/mod_acme.erl index 8b53130a3..984744dde 100644 --- a/src/mod_acme.erl +++ b/src/mod_acme.erl @@ -36,19 +36,14 @@ % -define(CA_URL, "https://acme-v01.api.letsencrypt.org"). -define(CA_URL, "https://acme-staging.api.letsencrypt.org"). --define(DEFAULT_DIRECTORY, "directory"). +-define(DEFAULT_DIRECTORY, ?CA_URL ++ "/directory"). --define(DEFAULT_NEW_NONCE, "acme/new_nonce"). - - --record(dirs, { - new_nonce = ?DEFAULT_NEW_NONCE - }). +-define(DEFAULT_NEW_NONCE, ?CA_URL ++ "/acme/new_nonce"). -record(state, { ca_url = ?CA_URL :: list(), dir_url = ?DEFAULT_DIRECTORY :: list(), - dirs = #dirs{} + dirs = maps:new() }). %% This will be initially just be filled with stub functions @@ -67,7 +62,7 @@ new_nonce(Pid, Options) -> gen_server:call(Pid, ?FUNCTION_NAME). new_account(Pid, Options) -> - ok. + gen_server:call(Pid, ?FUNCTION_NAME). update_account(Pid, Options) -> ok. @@ -115,16 +110,38 @@ init([]) -> ok = application:start(ssl), {ok, #state{}}. -handle_call(directory, _From, S = #state{ca_url = Ca, dir_url=Dir}) -> - Url = final_url([Ca, Dir]), +handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> + %% Make the get request {ok, {_Status, _Head, Body}} = httpc:request(get, {Url, []}, [], []), + + %% Decode the json string Result = jiffy:decode(Body), - {reply, {ok, Result}, S}; -handle_call(new_nonce, _From, S = #state{ca_url = Ca, dirs=Dirs}) -> - #dirs{new_nonce=New_nonce_url} = Dirs, - Url = final_url([Ca, New_nonce_url]), - {ok, {Status, Head, []}} = httpc:request(head, {Url, []}, [], []), - {reply, {ok, {Status, Head}}, S}; + {Directories} = Result, + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + {X,Y} <- Directories], + + %% 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}}; +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}; +handle_call(new_account, _From, S = #state{ca_url = Ca, dirs=Dirs}) -> + %% Get url from all directories + #{"new-reg" := Url} = Dirs, + + %% Make the request body + ReqBody = jiffy:encode({[]}), + + {ok, {Status, Head, Body}} = + httpc:request(post, {Url, [], "application/jose+json", ReqBody}, [], []), + {reply, {ok, {Status, Head, Body}}, S}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. @@ -141,11 +158,16 @@ terminate(_Reason, _State) -> code_change(_OldVsn, State, _Extra) -> {ok, State}. +%% Util functions final_url(Urls) -> Joined = lists:join("/", Urls), lists:flatten(Joined). + + +%% Test + scenario() -> {ok, Pid} = start(), io:format("Server started: ~p~n", [Pid]), @@ -153,6 +175,7 @@ scenario() -> {ok, Result} = directory(Pid, []), io:format("Directory result: ~p~n", [Result]), - {ok, Result1} = new_nonce(Pid, []), - io:format("New nonce result: ~p~n", [Result1]), + {ok, Result1} = new_account(Pid, []), + io:format("New account result: ~p~n", [Result1]), ok. + From 02dbe39b067bcff5ad2c2b09b254a57351a6ab84 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 9 May 2017 23:27:37 +0300 Subject: [PATCH 03/75] Examining jose functionality --- rebar.config | 1 + run_acme.sh | 7 ++++++- src/mod_acme.erl | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index b553931e6..5d5bf98a1 100644 --- a/rebar.config +++ b/rebar.config @@ -31,6 +31,7 @@ {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}}, {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.1"}}}, {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}}, + {jose, ".*", {git, "git://github.com/potatosalad/erlang-jose.git", {branch, "master"}}}, {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", "31e035b"}}}, {if_var_true, pgsql, {p1_pgsql, ".*", {git, "https://github.com/processone/p1_pgsql", diff --git a/run_acme.sh b/run_acme.sh index 400ee04ac..c07204a07 100755 --- a/run_acme.sh +++ b/run_acme.sh @@ -1,3 +1,8 @@ #!/bin/bash -erl -pa ebin deps/jiffy/ebin deps/fast_tls/ebin -noshell -s mod_acme scenario -s erlang halt \ No newline at end of file +erl -pa ebin \ +deps/jiffy/ebin \ +deps/fast_tls/ebin \ +deps/jose/ebin \ +deps/base64url/ebin \ +-noshell -s mod_acme scenario -s erlang halt \ No newline at end of file diff --git a/src/mod_acme.erl b/src/mod_acme.erl index 984744dde..292cafbcb 100644 --- a/src/mod_acme.erl +++ b/src/mod_acme.erl @@ -108,6 +108,10 @@ init([]) -> ok = application:start(asn1), ok = application:start(public_key), ok = application:start(ssl), + + ok = application:start(base64url), + ok = application:start(jose), + {ok, #state{}}. handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> @@ -139,6 +143,9 @@ handle_call(new_account, _From, S = #state{ca_url = Ca, dirs=Dirs}) -> %% Make the request body ReqBody = jiffy:encode({[]}), + %% Jose + % SignedBody = sign_a_json_object_using_jose(ReqBody), + {ok, {Status, Head, Body}} = httpc:request(post, {Url, [], "application/jose+json", ReqBody}, [], []), {reply, {ok, {Status, Head, Body}}, S}; @@ -168,6 +175,27 @@ final_url(Urls) -> %% Test +sign_a_json_object_using_jose(Json) -> + % Generate a key for now + Key = jose_jwk:generate_key({okp, 'Ed448'}), + io:format("Key: ~p~n", [Key]), + + % Jws object containing the algorithm + JwsObj = jose_jws:from(#{<<"alg">> => <<"Ed448">>}), + io:format("Jws: ~p~n", [JwsObj]), + + %% Signed Message + Signed = jose_jws:sign(Key, Json, JwsObj), + io:format("Signed: ~p~n", [Signed]), + + %% Compact Message + Compact = jose_jws:compact(Signed), + io:format("Compact: ~p~n", [Compact]), + + %% Verify + io:format("Verify: ~p~n", [jose_jws:verify(Key, Signed)]), + Signed. + scenario() -> {ok, Pid} = start(), io:format("Server started: ~p~n", [Pid]), From 88365ed50727f3fe62b1340b554550a54306e6b4 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 15 May 2017 01:41:09 +0300 Subject: [PATCH 04/75] New account functional, very crude --- src/mod_acme.erl | 95 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/src/mod_acme.erl b/src/mod_acme.erl index 292cafbcb..163f4bf1d 100644 --- a/src/mod_acme.erl +++ b/src/mod_acme.erl @@ -32,9 +32,11 @@ -include("logger.hrl"). -include("xmpp.hrl"). + -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"). @@ -43,7 +45,8 @@ -record(state, { ca_url = ?CA_URL :: list(), dir_url = ?DEFAULT_DIRECTORY :: list(), - dirs = maps:new() + dirs = maps:new(), + nonce = "" }). %% This will be initially just be filled with stub functions @@ -116,7 +119,7 @@ init([]) -> handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> %% Make the get request - {ok, {_Status, _Head, Body}} = httpc:request(get, {Url, []}, [], []), + {ok, {_Status, Head, Body}} = httpc:request(get, {Url, []}, [], []), %% Decode the json string Result = jiffy:decode(Body), @@ -124,30 +127,45 @@ handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || {X,Y} <- Directories], + % Find and save the replay nonce + % io:format("Directory Head Response: ~p~n", [Head]), + {"replay-nonce", Nonce} = proplists:lookup("replay-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}}; + {reply, {ok, Result}, S#state{dirs = NewDirs, nonce = Nonce}}; 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}; -handle_call(new_account, _From, S = #state{ca_url = Ca, dirs=Dirs}) -> +handle_call(new_account, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) -> %% Get url from all directories #{"new-reg" := Url} = Dirs, %% Make the request body - ReqBody = jiffy:encode({[]}), + ReqBody = jiffy:encode({ + [ { <<"contact">>, + [ + <<"mailto:cert-admin@example.com">> + ] + } + , { <<"resource">>, <<"new-reg">>} + ]}), %% Jose - % SignedBody = sign_a_json_object_using_jose(ReqBody), + {_, SignedBody} = sign_a_json_object_using_jose(ReqBody, Url, Nonce), + io:format("Signed Body: ~p~n", [SignedBody]), + + %% Encode the Signed body with jiffy + FinalBody = jiffy:encode(SignedBody), {ok, {Status, Head, Body}} = - httpc:request(post, {Url, [], "application/jose+json", ReqBody}, [], []), + httpc:request(post, {Url, [], "application/jose+json", FinalBody}, [], []), {reply, {ok, {Status, Head, Body}}, S}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. @@ -175,25 +193,76 @@ final_url(Urls) -> %% Test -sign_a_json_object_using_jose(Json) -> +sign_a_json_object_using_jose(Json, Url, Nonce) -> % Generate a key for now - Key = jose_jwk:generate_key({okp, 'Ed448'}), + Key = jose_jwk:generate_key({ec, secp256r1}), io:format("Key: ~p~n", [Key]), + % Generate a public key + PubKey = jose_jwk:to_public(Key), + io:format("Public Key: ~p~n", [PubKey]), + {_, 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]), + + % Jws object containing the algorithm - JwsObj = jose_jws:from(#{<<"alg">> => <<"Ed448">>}), + JwsObj = jose_jws:from( + #{ + % <<"alg">> => <<"HS256">> + <<"alg">> => <<"ES256">> + %% Im not sure if it is needed + % , <<"b64">> => true + , <<"jwk">> => PubKeyJson + % , <<"url">> => list_to_bitstring(Url) + , <<"nonce">> => list_to_bitstring(Nonce) + }), io:format("Jws: ~p~n", [JwsObj]), + % ProtectedObj = jose_jws:signing_input(Json, + % #{ <<"alg">> => <<"HS256">> + % %% Im not sure if it is needed + % , <<"jwk">> => PubKeyJson + % , <<"url">> => Url + % , <<"nonce">> => Nonce + % }, JwsObj), + % io:format("ProtectedObj: ~p~n", [ProtectedObj]), + + % {Modules, ProtectedBinary} = to_binary(JwsObj), + % io:format("ProtectedObj: ~p~n", [ProtectedObj]), + % Protected = base64url:encode(ProtectedBinary), + % Payload = base64url:encode(PlainText), + % SigningInput = signing_input(PlainText, Protected, NewJWS), + % Signature = base64url:encode(ALGModule:sign(Key, SigningInput, NewALG)), + % {Modules, maps:put(<<"payload">>, Payload, signature_to_map(Protected, Header, Key, Signature))}; + %% Signed Message Signed = jose_jws:sign(Key, Json, JwsObj), io:format("Signed: ~p~n", [Signed]), - %% Compact Message - Compact = jose_jws:compact(Signed), - io:format("Compact: ~p~n", [Compact]), + %% 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)]), %% Verify io:format("Verify: ~p~n", [jose_jws:verify(Key, Signed)]), + + % %% To binary + % Binary = jose_jws:to_binary(Signed), + % io:format("Binary: ~p~n", [jose_jws:to_binary(Signed)]), + + % %% To map + % Map = jose_jws:to_map(Signed), + % io:format("Map: ~p~n", [jose_jws:to_map(Signed)]), + Signed. scenario() -> From ddb043aa71e55a312da05808e9caf807b50c319c Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 17 May 2017 16:55:26 +0300 Subject: [PATCH 05/75] More account support(Update/Info) --- src/mod_acme.erl | 232 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 178 insertions(+), 54 deletions(-) diff --git a/src/mod_acme.erl b/src/mod_acme.erl index 163f4bf1d..4d6c1bdd8 100644 --- a/src/mod_acme.erl +++ b/src/mod_acme.erl @@ -9,13 +9,13 @@ , directory/2 , new_nonce/2 %% Account - , new_account/2 + , new_reg/2 , update_account/2 , account_info/2 %% TODO: Maybe change to get_account , account_key_change/2 , deactivate_account/2 %% Orders/Certificates - , new_order/2 + , new_cert/2 , new_authz/2 , get_certificate/2 , get_authz/2 @@ -39,14 +39,18 @@ % -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(DEFAULT_TOS, <<"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf">>). -record(state, { ca_url = ?CA_URL :: list(), dir_url = ?DEFAULT_DIRECTORY :: list(), dirs = maps:new(), - nonce = "" + nonce = "", + account = none }). %% This will be initially just be filled with stub functions @@ -64,14 +68,15 @@ directory(Pid, Options) -> new_nonce(Pid, Options) -> gen_server:call(Pid, ?FUNCTION_NAME). -new_account(Pid, Options) -> +new_reg(Pid, Options) -> gen_server:call(Pid, ?FUNCTION_NAME). -update_account(Pid, Options) -> - ok. +update_account(Pid, AccountId) -> + %% TODO: This has to have more info ofcourse + gen_server:call(Pid, {?FUNCTION_NAME, AccountId}). -account_info(Pid, Options) -> - ok. +account_info(Pid, AccountId) -> + gen_server:call(Pid, {?FUNCTION_NAME, AccountId}). account_key_change(Pid, Options) -> ok. @@ -79,11 +84,11 @@ account_key_change(Pid, Options) -> deactivate_account(Pid, Options) -> ok. -new_order(Pid, Options) -> - ok. +new_cert(Pid, Options) -> + gen_server:call(Pid, ?FUNCTION_NAME). new_authz(Pid, Options) -> - ok. + gen_server:call(Pid, ?FUNCTION_NAME). get_certificate(Pid, Options) -> ok. @@ -129,7 +134,7 @@ handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> % Find and save the replay nonce % io:format("Directory Head Response: ~p~n", [Head]), - {"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head), + Nonce = get_nonce(Head), %% Update the directories in state %% TODO: Get the merge of the old and the new dictionary @@ -143,30 +148,146 @@ handle_call(new_nonce, _From, S = #state{dirs=Dirs}) -> {ok, {Status, Head, []}} = httpc:request(head, {Url, []}, [], []), {reply, {ok, {Status, Head}}, S}; -handle_call(new_account, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) -> +handle_call(new_reg, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) -> %% Get url from all directories #{"new-reg" := Url} = Dirs, %% Make the request body ReqBody = jiffy:encode({ - [ { <<"contact">>, - [ - <<"mailto:cert-admin@example.com">> - ] - } + [ { <<"contact">>, [<<"mailto:cert-admin@example.com">>]} , { <<"resource">>, <<"new-reg">>} ]}), + %% 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(ReqBody, Url, Nonce), + {_, 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}, [], []), - {reply, {ok, {Status, Head, Body}}, S}; + + %% 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), + + %% 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({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}}; handle_call(stop, _From, State) -> {stop, normal, ok, State}. @@ -189,20 +310,26 @@ final_url(Urls) -> Joined = lists:join("/", Urls), lists:flatten(Joined). +get_nonce(Head) -> + {"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head), + Nonce. %% Test -sign_a_json_object_using_jose(Json, Url, Nonce) -> +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) -> % Generate a public key PubKey = jose_jwk:to_public(Key), - io:format("Public Key: ~p~n", [PubKey]), + % io:format("Public Key: ~p~n", [PubKey]), {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), - io:format("Public Key: ~p~n", [BinaryPubKey]), + % io:format("Public Key: ~p~n", [BinaryPubKey]), PubKeyJson = jiffy:decode(BinaryPubKey), io:format("Public Key: ~p~n", [PubKeyJson]), @@ -218,31 +345,14 @@ sign_a_json_object_using_jose(Json, Url, Nonce) -> %% Im not sure if it is needed % , <<"b64">> => true , <<"jwk">> => PubKeyJson - % , <<"url">> => list_to_bitstring(Url) , <<"nonce">> => list_to_bitstring(Nonce) }), - io:format("Jws: ~p~n", [JwsObj]), + % io:format("Jws: ~p~n", [JwsObj]), - % ProtectedObj = jose_jws:signing_input(Json, - % #{ <<"alg">> => <<"HS256">> - % %% Im not sure if it is needed - % , <<"jwk">> => PubKeyJson - % , <<"url">> => Url - % , <<"nonce">> => Nonce - % }, JwsObj), - % io:format("ProtectedObj: ~p~n", [ProtectedObj]), - - % {Modules, ProtectedBinary} = to_binary(JwsObj), - % io:format("ProtectedObj: ~p~n", [ProtectedObj]), - % Protected = base64url:encode(ProtectedBinary), - % Payload = base64url:encode(PlainText), - % SigningInput = signing_input(PlainText, Protected, NewJWS), - % Signature = base64url:encode(ALGModule:sign(Key, SigningInput, NewALG)), - % {Modules, maps:put(<<"payload">>, Payload, signature_to_map(Protected, Header, Key, Signature))}; %% Signed Message Signed = jose_jws:sign(Key, Json, JwsObj), - io:format("Signed: ~p~n", [Signed]), + % io:format("Signed: ~p~n", [Signed]), %% Peek protected Protected = jose_jws:peek_protected(Signed), @@ -253,15 +363,8 @@ sign_a_json_object_using_jose(Json, Url, Nonce) -> io:format("Payload: ~p~n", [jiffy:decode(Payload)]), %% Verify - io:format("Verify: ~p~n", [jose_jws:verify(Key, Signed)]), - - % %% To binary - % Binary = jose_jws:to_binary(Signed), - % io:format("Binary: ~p~n", [jose_jws:to_binary(Signed)]), - - % %% To map - % Map = jose_jws:to_map(Signed), - % io:format("Map: ~p~n", [jose_jws:to_map(Signed)]), + % {true, _} = jose_jws:verify(Key, Signed), + % io:format("Verify: ~p~n", [jose_jws:verify(Key, Signed)]), Signed. @@ -272,7 +375,28 @@ scenario() -> {ok, Result} = directory(Pid, []), io:format("Directory result: ~p~n", [Result]), - {ok, Result1} = new_account(Pid, []), - io:format("New account result: ~p~n", [Result1]), + % %% 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)]), ok. From df5d673e631a1c4b31ad886db6046cfab2617457 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 3 Jun 2017 12:34:15 +0300 Subject: [PATCH 06/75] Solve http-01 challenge --- src/acme_challenge.erl | 61 +++++++++++++++ src/mod_acme.erl | 169 +++++++++++++++++++++++++++++++---------- 2 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 src/acme_challenge.erl diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl new file mode 100644 index 000000000..ee7b3034a --- /dev/null +++ b/src/acme_challenge.erl @@ -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]). \ No newline at end of file diff --git a/src/mod_acme.erl b/src/mod_acme.erl index 4d6c1bdd8..3579a2ec9 100644 --- a/src/mod_acme.erl +++ b/src/mod_acme.erl @@ -32,18 +32,34 @@ -include("logger.hrl"). -include("xmpp.hrl"). - -include_lib("public_key/include/public_key.hrl"). +-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. @@ -125,10 +141,9 @@ init([]) -> handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> %% Make the get request {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,16 +280,64 @@ 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 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]), + % 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)]), + ok. - %% 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)]). \ No newline at end of file From 926de60f5da52c6398f463d1b357bddadbf7946e Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 5 Jun 2017 17:10:37 +0300 Subject: [PATCH 07/75] Support for new_cert --- run_acme.sh | 1 + src/mod_acme.erl | 134 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/run_acme.sh b/run_acme.sh index c07204a07..bec34e7e1 100755 --- a/run_acme.sh +++ b/run_acme.sh @@ -5,4 +5,5 @@ deps/jiffy/ebin \ deps/fast_tls/ebin \ deps/jose/ebin \ deps/base64url/ebin \ +deps/xmpp/ebin \ -noshell -s mod_acme scenario -s erlang halt \ No newline at end of file diff --git a/src/mod_acme.erl b/src/mod_acme.erl index 3579a2ec9..0fbb1bca3 100644 --- a/src/mod_acme.erl +++ b/src/mod_acme.erl @@ -252,11 +252,28 @@ handle_call(new_cert, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) %% Get url from all directories #{"new-cert" := Url} = Dirs, + MyCSR = make_csr(), + % file:write_file("myCSR.der", CSR), + % {ok, CSR} = file:read_file("CSR.der"), + % io:format("CSR: ~p~nMy Encoded CSR: ~p~nCorrect Encoded CSR: ~p~n", + % [ public_key:der_decode('CertificationRequest', CSR) + % , MyCSR + % , CSR]), + + CSRbase64 = base64url:encode(MyCSR), + + % io:format("CSR base64: ~p~n", [CSRbase64]), + {MegS, Sec, MicS} = erlang:timestamp(), + NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), + NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), + %% Make the request body ReqBody = jiffy:encode({[ - { <<"resource">>, <<"new-cert">>} + {<<"resource">>, <<"new-cert">>}, + {<<"csr">>, CSRbase64}, + {<<"notBefore">>, NotBefore}, + {<<"NotAfter">>, NotAfter} ]}), - %% Get the key from a file Key = jose_jwk:from_file(?DEFAULT_KEY_FILE), @@ -269,7 +286,7 @@ handle_call(new_cert, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) %% Post request {ok, {Status, Head, Body}} = - httpc:request(post, {Url, [], "application/jose+json", FinalBody}, [], []), + httpc:request(post, {Url, [], "application/pkix-cert", FinalBody}, [], []), % Get and save the new nonce NewNonce = get_nonce(Head), @@ -426,9 +443,102 @@ sign_a_json_object_using_jose(Key, Json, Url, Nonce) -> Signed. +make_csr() -> + + SigningKey = jose_jwk:from_pem_file("csr_signing_private_key.key"), + {_, PrivateKey} = jose_jwk:to_key(SigningKey), + % io:format("PrivateKey: ~p~n", [PrivateKey]), + + PubKey = jose_jwk:to_public(SigningKey), + % io:format("Public Key: ~p~n", [PubKey]), + + {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), + % io:format("Public Key: ~p~n", [BinaryPubKey]), + + {_, RawPubKey} = jose_jwk:to_key(PubKey), + % io:format("Raw Public Key: ~p~n", [RawPubKey]), + {{_, RawBinPubKey}, _} = RawPubKey, + % io:format("Encoded Raw Public Key: ~p~n", [RawBinPubKey]), + + %% TODO: Understand how to extract the information below from the key struct + AlgoID = #'CertificationRequestInfo_subjectPKInfo_algorithm'{ + algorithm = {1,2,840,10045,2,1}, %% Very dirty + parameters = {asn1_OPENTYPE,<<6,8,42,134,72,206,61,3,1,7>>} + }, + SubPKInfo = #'CertificationRequestInfo_subjectPKInfo'{ + algorithm = AlgoID, %% Very dirty + subjectPublicKey = RawBinPubKey %% public_key:der_encode('ECPoint', RawPubKey) + }, + + CommonName = #'AttributeTypeAndValue'{ + type = {2,5,4,3}, + % value = list_to_bitstring([12,25] ++ "my-acme-test-ejabberd.com") + value = length_bitstring(<<"my-acme-test-ejabberd.com">>) + }, + CountryName = #'AttributeTypeAndValue'{ + type = {2,5,4,6}, + value = length_bitstring(<<"US">>) + }, + StateOrProvinceName = #'AttributeTypeAndValue'{ + type = {2,5,4,8}, + value = length_bitstring(<<"California">>) + }, + LocalityName = #'AttributeTypeAndValue'{ + type = {2,5,4,7}, + value = length_bitstring(<<"San Jose">>) + }, + OrganizationName = #'AttributeTypeAndValue'{ + type = {2,5,4,10}, + value = length_bitstring(<<"Example">>) + }, + CRI = #'CertificationRequestInfo'{ + version = 0, + % subject = {rdnSequence, [[CommonName]]}, + subject = {rdnSequence, + [ [CommonName] + , [CountryName] + , [StateOrProvinceName] + , [LocalityName] + , [OrganizationName]]}, + subjectPKInfo = SubPKInfo, + attributes = [] + }, + EncodedCRI = public_key:der_encode( + 'CertificationRequestInfo', + CRI), + + SignedCRI = public_key:sign(EncodedCRI, 'sha256', PrivateKey), + + SigningAlgoID = #'CertificationRequest_signatureAlgorithm'{ + algorithm = [1,2,840,10045,4,3,2], %% Very dirty + parameters = asn1_NOVALUE + }, + + CSR = #'CertificationRequest'{ + certificationRequestInfo = CRI, + signatureAlgorithm = SigningAlgoID, + signature = SignedCRI + }, + Result = public_key:der_encode( + 'CertificationRequest', + CSR), + % io:format("My CSR: ~p~n", [CSR]), + + Result. + +%% TODO: Find a correct function to do this +length_bitstring(Bitstring) -> + Size = size(Bitstring), + case Size < 127 of + true -> + <<12, Size, Bitstring/binary>>; + false -> + error(not_implemented) + end. + scenario() -> - % scenario_new_account(). - scenario_old_account(). + % scenario_new_account(). + scenario_old_account(). scenario_old_account() -> {ok, Pid} = start(), @@ -458,7 +568,7 @@ scenario_old_account() -> [{Status2, Head2}, jiffy:decode(Body2)]), % Challenges = get_challenges(jiffy:decode(Body2)), - % % io:format("Challenges: ~p~n", [Challenges]), + % io:format("Challenges: ~p~n", [Challenges]), % ChallengeObjects = acme_challenge:challenges_to_objects(Challenges), % % io:format("Challenges: ~p~n", [ChallengeObjects]), @@ -474,11 +584,15 @@ scenario_old_account() -> % complete_challenge(Pid, [X || X <- Solutions, X =/= ok]), % io:format("Complete_challenge~nHead: ~p~nBody: ~p~n", % [{Status3, Head3}, jiffy:decode(Body3)]), + + % Get a certification + {ok, {Status4, Head4, Body4}} = + new_cert(Pid, []), + io:format("New Cert~nHead: ~p~nBody: ~p~n", + [{Status4, Head4}, Body4]), + + % make_csr(), - %% New certification - % {ok, {Status2, Head2, Body2}} = new_cert(Pid, []), - % io:format("New Cert~nHead: ~p~nBody: ~p~n", - % [{Status2, Head2}, jiffy:decode(Body2)]), ok. scenario_new_account() -> From 53d47483c8423bf75fda3db33a444938d7aae1cf Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 9 Jun 2017 15:49:27 +0300 Subject: [PATCH 08/75] Implement some basic account handling functions --- run_acme.sh | 2 +- src/{mod_acme.erl => acme_experimental.erl} | 2 +- src/ejabberd_acme.erl | 219 ++++++++++++++++++++ 3 files changed, 221 insertions(+), 2 deletions(-) rename src/{mod_acme.erl => acme_experimental.erl} (99%) create mode 100644 src/ejabberd_acme.erl diff --git a/run_acme.sh b/run_acme.sh index bec34e7e1..c4c7df4d9 100755 --- a/run_acme.sh +++ b/run_acme.sh @@ -6,4 +6,4 @@ deps/fast_tls/ebin \ deps/jose/ebin \ deps/base64url/ebin \ deps/xmpp/ebin \ --noshell -s mod_acme scenario -s erlang halt \ No newline at end of file +-noshell -s acme_experimental scenario -s erlang halt \ No newline at end of file diff --git a/src/mod_acme.erl b/src/acme_experimental.erl similarity index 99% rename from src/mod_acme.erl rename to src/acme_experimental.erl index 0fbb1bca3..08fdc6ff4 100644 --- a/src/mod_acme.erl +++ b/src/acme_experimental.erl @@ -1,4 +1,4 @@ --module(mod_acme). +-module(acme_experimental). -behaviour(gen_server). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl new file mode 100644 index 000000000..517fdf10a --- /dev/null +++ b/src/ejabberd_acme.erl @@ -0,0 +1,219 @@ +-module (ejabberd_acme). + +-export([ scenario/3 + , scenario0/0 + , directory/1 + , get_account/3 + , new_account/4 + , update_account/4 + ]). + +-include("ejabberd.hrl"). +-include("logger.hrl"). +-include("xmpp.hrl"). +-include_lib("public_key/include/public_key.hrl"). + +-define(REQUEST_TIMEOUT, 5000). % 5 seconds. +-define(DIRURL, "directory"). +-define(REGURL, "/acme/reg/"). + +-define(DEFAULT_KEY_FILE, "private_key_temporary"). + + +directory(DirURL) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(get, {DirURL, []}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + %% Decode the json string + {Directories} = jiffy:decode(Body), + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + {X,Y} <- Directories], + % Find and save the replay nonce + Nonce = get_nonce(Head), + %% Return Map of Directories + NewDirs = maps:from_list(StrDirectories), + {ok, NewDirs, Nonce}; + {ok, {{_, Code, _}, Head, _Body}} -> + ?ERROR_MSG("Got unexpected status code from <~s>: ~B", + [DirURL, Code]), + Nonce = get_nonce(Head), + {error, unexpected_code, Nonce}; + {error, Reason} -> + ?ERROR_MSG("Error requesting directory from <~s>: ~p", + [DirURL, Reason]), + {error, Reason} + end. + +new_account(NewAccURl, PrivateKey, Req, Nonce) -> + %% Make the request body + ReqBody = jiffy:encode({[{ <<"resource">>, <<"new-reg">>}] ++ Req}), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, NewAccURl, Nonce), + %% Encode the Signed body with jiffy + FinalBody = jiffy:encode(SignedBody), + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {NewAccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + %% Decode the json string + {Return} = jiffy:decode(Body), + TOSUrl = get_TOS(Head), + % Find and save the replay nonce + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}; + {ok, {{_, 409 = Code, _}, Head, Body}} -> + ?ERROR_MSG("Got status code: ~B from <~s>, Body: ~s", + [NewAccURl, Code, Body]), + NewNonce = get_nonce(Head), + {error, key_in_use, NewNonce}; + {ok, {{_, Code, _}, Head, Body}} -> + ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", + [NewAccURl, Code, Body]), + NewNonce = get_nonce(Head), + {error, unexpected_code, NewNonce}; + {error, Reason} -> + ?ERROR_MSG("Error requesting directory from <~s>: ~p", + [NewAccURl, Reason]), + {error, Reason, Nonce} + end. + +update_account(AccURl, PrivateKey, Req, Nonce) -> + %% Make the request body + ReqBody = jiffy:encode({[{ <<"resource">>, <<"reg">>}] ++ Req}), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, AccURl, Nonce), + %% Encode the Signed body with jiffy + FinalBody = jiffy:encode(SignedBody), + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + %% Decode the json string + {Return} = jiffy:decode(Body), + % Find and save the replay nonce + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}; + {ok, {{_, Code, _}, Head, Body}} -> + ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", + [AccURl, Code, Body]), + NewNonce = get_nonce(Head), + {error, unexpected_code, NewNonce}; + {error, Reason} -> + ?ERROR_MSG("Error requesting directory from <~s>: ~p", + [AccURl, Reason]), + {error, Reason, Nonce} + end. + + +get_account(AccURl, PrivateKey, Nonce) -> + %% Make the request body + ReqBody = jiffy:encode({[ + { <<"resource">>, <<"reg">>} + ]}), + %% Jose Sign + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, AccURl, Nonce), + %% Encode the Signed body with jiffy + FinalBody = jiffy:encode(SignedBody), + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + %% Decode the json string + {Return} = jiffy:decode(Body), + TOSUrl = get_TOS(Head), + % Find and save the replay nonce + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}; + {ok, {{_, Code, _}, Head, Body}} -> + ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Head: ~s", + [AccURl, Code, Body]), + NewNonce = get_nonce(Head), + {error, unexpected_code, NewNonce}; + {error, Reason} -> + ?ERROR_MSG("Error requesting directory from <~s>: ~p", + [AccURl, Reason]), + {error, Reason, Nonce} + end. + + +%% +%% Useful funs +%% + +get_nonce(Head) -> + {"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head), + Nonce. + +%% Very bad way to extract this +%% TODO: Find a better way +get_TOS(Head) -> + try + [{_, Link}] = [{K, V} || {K, V} <- Head, + K =:= "link" andalso lists:suffix("\"terms-of-service\"", V)], + [Link1, _] = string:tokens(Link, ";"), + Link2 = string:strip(Link1, left, $<), + string:strip(Link2, right, $>) + catch + _:_ -> + no_tos + end. + + +sign_json_jose(Key, Json, Url, Nonce) -> + % Generate a public key + 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 + , <<"nonce">> => list_to_bitstring(Nonce) + }), + + %% Signed Message + jose_jws:sign(Key, Json, JwsObj). + +%% +%% Debugging Funcs -- They are only used for the development phase +%% + +%% A typical acme workflow +scenario(CAUrl, AccId, PrivateKey) -> + + DirURL = CAUrl ++ "/" ++ ?DIRURL, + {ok, Dirs, Nonce0} = directory(DirURL), + + AccURL = CAUrl ++ ?REGURL ++ AccId, + {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0). + +new_user_scenario(CAUrl) -> + PrivateKey = generate_key(), + + DirURL = CAUrl ++ "/" ++ ?DIRURL, + {ok, Dirs, Nonce0} = directory(DirURL), + + #{"new-reg" := NewAccURL} = Dirs, + Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], + {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), + + {_, AccId} = proplists:lookup(<<"id">>, Account), + AccURL = CAUrl ++ ?REGURL ++ integer_to_list(AccId), + Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], + {ok, Account1, Nonce2} = update_account(AccURL, PrivateKey, Req1, Nonce1), + + {Account1, PrivateKey}. + +generate_key() -> + jose_jwk:generate_key({ec, secp256r1}). + +%% Just a test +scenario0() -> + PrivateKey = jose_jwk:from_file(?DEFAULT_KEY_FILE), + % scenario("http://localhost:4000", "2", PrivateKey). + new_user_scenario("http://localhost:4000"). From 167edacb5f62a0fb394e0eedb34e1b675c9197fc Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 9 Jun 2017 18:53:54 +0300 Subject: [PATCH 09/75] Make Stylistic Changes in order to conform to guidelines: 1. Remove trailing whitespace 2. Remove Macros 3. Handle all erroneous response codes the same way 4. Add specs Also don't return nonces anymore when the http response is negative. --- src/ejabberd_acme.erl | 106 +++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 54 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 517fdf10a..b8671b75d 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,7 +1,7 @@ -module (ejabberd_acme). -export([ scenario/3 - , scenario0/0 + , scenario0/1 , directory/1 , get_account/3 , new_account/4 @@ -14,12 +14,15 @@ -include_lib("public_key/include/public_key.hrl"). -define(REQUEST_TIMEOUT, 5000). % 5 seconds. --define(DIRURL, "directory"). --define(REGURL, "/acme/reg/"). - --define(DEFAULT_KEY_FILE, "private_key_temporary"). +-type nonce() :: string(). +-type url() :: string(). +-type proplist() :: [{_, _}]. +-type jws() :: map(). + +-spec directory(url()) -> + {ok, map(), nonce()} | {error, _}. directory(DirURL) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], @@ -27,66 +30,63 @@ directory(DirURL) -> {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string {Directories} = jiffy:decode(Body), - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || {X,Y} <- Directories], % Find and save the replay nonce Nonce = get_nonce(Head), %% Return Map of Directories NewDirs = maps:from_list(StrDirectories), {ok, NewDirs, Nonce}; - {ok, {{_, Code, _}, Head, _Body}} -> + {ok, {{_, Code, _}, _Head, _Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B", [DirURL, Code]), - Nonce = get_nonce(Head), - {error, unexpected_code, Nonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [DirURL, Reason]), {error, Reason} end. +-spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. new_account(NewAccURl, PrivateKey, Req, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{ <<"resource">>, <<"new-reg">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, NewAccURl, Nonce), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, + case httpc:request(post, {NewAccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string {Return} = jiffy:decode(Body), - TOSUrl = get_TOS(Head), + TOSUrl = get_tos(Head), % Find and save the replay nonce NewNonce = get_nonce(Head), {ok, {TOSUrl, Return}, NewNonce}; - {ok, {{_, 409 = Code, _}, Head, Body}} -> - ?ERROR_MSG("Got status code: ~B from <~s>, Body: ~s", - [NewAccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, key_in_use, NewNonce}; - {ok, {{_, Code, _}, Head, Body}} -> + {ok, {{_, Code, _}, _Head, Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", [NewAccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, unexpected_code, NewNonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [NewAccURl, Reason]), - {error, Reason, Nonce} + {error, Reason} end. +-spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. update_account(AccURl, PrivateKey, Req, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{ <<"resource">>, <<"reg">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, AccURl, Nonce), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, + case httpc:request(post, {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string @@ -94,79 +94,78 @@ update_account(AccURl, PrivateKey, Req, Nonce) -> % Find and save the replay nonce NewNonce = get_nonce(Head), {ok, Return, NewNonce}; - {ok, {{_, Code, _}, Head, Body}} -> + {ok, {{_, Code, _}, _Head, Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", [AccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, unexpected_code, NewNonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [AccURl, Reason]), - {error, Reason, Nonce} + {error, Reason} end. - +-spec get_account(url(), jose_jwk:key(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. get_account(AccURl, PrivateKey, Nonce) -> %% Make the request body - ReqBody = jiffy:encode({[ - { <<"resource">>, <<"reg">>} - ]}), + ReqBody = jiffy:encode({[{<<"resource">>, <<"reg">>}]}), %% Jose Sign - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, AccURl, Nonce), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, + case httpc:request(post, {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string {Return} = jiffy:decode(Body), - TOSUrl = get_TOS(Head), + TOSUrl = get_tos(Head), % Find and save the replay nonce NewNonce = get_nonce(Head), {ok, {TOSUrl, Return}, NewNonce}; - {ok, {{_, Code, _}, Head, Body}} -> + {ok, {{_, Code, _}, _Head, Body}} -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Head: ~s", [AccURl, Code, Body]), - NewNonce = get_nonce(Head), - {error, unexpected_code, NewNonce}; + {error, unexpected_code}; {error, Reason} -> ?ERROR_MSG("Error requesting directory from <~s>: ~p", [AccURl, Reason]), - {error, Reason, Nonce} + {error, Reason} end. %% %% Useful funs %% - +-spec get_nonce(proplist()) -> nonce() | 'none'. get_nonce(Head) -> - {"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head), - Nonce. + case proplists:lookup("replay-nonce", Head) of + {"replay-nonce", Nonce} -> Nonce; + none -> none + end. %% Very bad way to extract this %% TODO: Find a better way -get_TOS(Head) -> +-spec get_tos(proplist()) -> url() | 'none'. +get_tos(Head) -> try - [{_, Link}] = [{K, V} || {K, V} <- Head, + [{_, Link}] = [{K, V} || {K, V} <- Head, K =:= "link" andalso lists:suffix("\"terms-of-service\"", V)], [Link1, _] = string:tokens(Link, ";"), Link2 = string:strip(Link1, left, $<), string:strip(Link2, right, $>) catch _:_ -> - no_tos + none end. - -sign_json_jose(Key, Json, Url, Nonce) -> +-spec sign_json_jose(jose_jwk:key(), string(), nonce()) -> jws(). +sign_json_jose(Key, Json, Nonce) -> % Generate a public key 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( @@ -185,17 +184,16 @@ sign_json_jose(Key, Json, Url, Nonce) -> %% A typical acme workflow scenario(CAUrl, AccId, PrivateKey) -> - - DirURL = CAUrl ++ "/" ++ ?DIRURL, + DirURL = CAUrl ++ "/directory", {ok, Dirs, Nonce0} = directory(DirURL), - AccURL = CAUrl ++ ?REGURL ++ AccId, + AccURL = CAUrl ++ "/acme/reg/" ++ AccId, {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0). new_user_scenario(CAUrl) -> PrivateKey = generate_key(), - DirURL = CAUrl ++ "/" ++ ?DIRURL, + DirURL = CAUrl ++ "/directory", {ok, Dirs, Nonce0} = directory(DirURL), #{"new-reg" := NewAccURL} = Dirs, @@ -203,7 +201,7 @@ new_user_scenario(CAUrl) -> {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), {_, AccId} = proplists:lookup(<<"id">>, Account), - AccURL = CAUrl ++ ?REGURL ++ integer_to_list(AccId), + AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], {ok, Account1, Nonce2} = update_account(AccURL, PrivateKey, Req1, Nonce1), @@ -213,7 +211,7 @@ generate_key() -> jose_jwk:generate_key({ec, secp256r1}). %% Just a test -scenario0() -> - PrivateKey = jose_jwk:from_file(?DEFAULT_KEY_FILE), +scenario0(KeyFile) -> + PrivateKey = jose_jwk:from_file(KeyFile), % scenario("http://localhost:4000", "2", PrivateKey). new_user_scenario("http://localhost:4000"). From 911b8188d2ff0a206ab4bd35a5783cc224ccb38f Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 9 Jun 2017 19:47:50 +0300 Subject: [PATCH 10/75] Refactor the http response handlers. Encapsulate some dangerous calls with try catch. --- src/ejabberd_acme.erl | 143 ++++++++++++++++++++++-------------------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index b8671b75d..75654c31a 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -23,33 +23,31 @@ -spec directory(url()) -> {ok, map(), nonce()} | {error, _}. -directory(DirURL) -> +directory(Url) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(get, {DirURL, []}, HttpOptions, Options) of + case httpc:request(get, {Url, []}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> %% Decode the json string - {Directories} = jiffy:decode(Body), - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X,Y} <- Directories], - % Find and save the replay nonce - Nonce = get_nonce(Head), - %% Return Map of Directories - NewDirs = maps:from_list(StrDirectories), - {ok, NewDirs, Nonce}; - {ok, {{_, Code, _}, _Head, _Body}} -> - ?ERROR_MSG("Got unexpected status code from <~s>: ~B", - [DirURL, Code]), - {error, unexpected_code}; - {error, Reason} -> - ?ERROR_MSG("Error requesting directory from <~s>: ~p", - [DirURL, Reason]), - {error, Reason} + case decode(Body) of + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason}; + Directories -> + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + {X,Y} <- Directories], + Nonce = get_nonce(Head), + %% Return Map of Directories + NewDirs = maps:from_list(StrDirectories), + {ok, NewDirs, Nonce} + end; + Error -> + failed_http_request(Error, Url) end. -spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. -new_account(NewAccURl, PrivateKey, Req, Nonce) -> +new_account(Url, PrivateKey, Req, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{ <<"resource">>, <<"new-reg">>}] ++ Req}), {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), @@ -58,27 +56,24 @@ new_account(NewAccURl, PrivateKey, Req, Nonce) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], case httpc:request(post, - {NewAccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of + {Url, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - %% Decode the json string - {Return} = jiffy:decode(Body), - TOSUrl = get_tos(Head), - % Find and save the replay nonce - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}; - {ok, {{_, Code, _}, _Head, Body}} -> - ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", - [NewAccURl, Code, Body]), - {error, unexpected_code}; - {error, Reason} -> - ?ERROR_MSG("Error requesting directory from <~s>: ~p", - [NewAccURl, Reason]), - {error, Reason} + case decode(Body) of + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason}; + Return -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce} + end; + Error -> + failed_http_request(Error, Url) end. -spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. -update_account(AccURl, PrivateKey, Req, Nonce) -> +update_account(Url, PrivateKey, Req, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{ <<"resource">>, <<"reg">>}] ++ Req}), {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), @@ -87,26 +82,23 @@ update_account(AccURl, PrivateKey, Req, Nonce) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], case httpc:request(post, - {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of + {Url, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - %% Decode the json string - {Return} = jiffy:decode(Body), - % Find and save the replay nonce - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}; - {ok, {{_, Code, _}, _Head, Body}} -> - ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", - [AccURl, Code, Body]), - {error, unexpected_code}; - {error, Reason} -> - ?ERROR_MSG("Error requesting directory from <~s>: ~p", - [AccURl, Reason]), - {error, Reason} + case decode(Body) of + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason}; + Return -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce} + end; + Error -> + failed_http_request(Error, Url) end. -spec get_account(url(), jose_jwk:key(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. -get_account(AccURl, PrivateKey, Nonce) -> +get_account(Url, PrivateKey, Nonce) -> %% Make the request body ReqBody = jiffy:encode({[{<<"resource">>, <<"reg">>}]}), %% Jose Sign @@ -116,22 +108,19 @@ get_account(AccURl, PrivateKey, Nonce) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], case httpc:request(post, - {AccURl, [], "application/jose+json", FinalBody}, HttpOptions, Options) of + {Url, [], "application/jose+json", FinalBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - %% Decode the json string - {Return} = jiffy:decode(Body), - TOSUrl = get_tos(Head), - % Find and save the replay nonce - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}; - {ok, {{_, Code, _}, _Head, Body}} -> - ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Head: ~s", - [AccURl, Code, Body]), - {error, unexpected_code}; - {error, Reason} -> - ?ERROR_MSG("Error requesting directory from <~s>: ~p", - [AccURl, Reason]), - {error, Reason} + case decode(Body) of + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason}; + Return -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce} + end; + Error -> + failed_http_request(Error, Url) end. @@ -178,6 +167,26 @@ sign_json_jose(Key, Json, Nonce) -> %% Signed Message jose_jws:sign(Key, Json, JwsObj). +decode(Json) -> + try + {Result} = jiffy:decode(Json), + Result + catch + _:Reason -> + {error, Reason} + end. + +-spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}. +failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) -> + ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", + [Url, Code, Body]), + {error, unexpected_code}; +failed_http_request({error, Reason}, Url) -> + ?ERROR_MSG("Error making a request to <~s>: ~p", + [Url, Reason]), + {error, Reason}. + + %% %% Debugging Funcs -- They are only used for the development phase %% @@ -213,5 +222,5 @@ generate_key() -> %% Just a test scenario0(KeyFile) -> 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"). From c25aa8378f34605aee0de4d709dd8caf4ee4586c Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 12 Jun 2017 15:31:48 +0300 Subject: [PATCH 11/75] Add new-authz, refactor the http requests that all used the same code --- src/ejabberd_acme.erl | 202 +++++++++++++++++++++++++++++++----------- 1 file changed, 151 insertions(+), 51 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 75654c31a..f77f8cfa3 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -3,9 +3,14 @@ -export([ scenario/3 , scenario0/1 , directory/1 + , get_account/3 , new_account/4 , update_account/4 + , delete_account/3 + % , key_roll_over/5 + + , new_authz/4 ]). -include("ejabberd.hrl"). @@ -28,7 +33,6 @@ directory(Url) -> HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], case httpc:request(get, {Url, []}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - %% Decode the json string case decode(Body) of {error, Reason} -> ?ERROR_MSG("Problem decoding: ~s", [Body]), @@ -45,6 +49,13 @@ directory(Url) -> failed_http_request(Error, Url) end. + +%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Account Handling +%% +%%%%%%%%%%%%%%%%%%%%%%%%% + -spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. new_account(Url, PrivateKey, Req, Nonce) -> @@ -53,22 +64,13 @@ new_account(Url, PrivateKey, Req, Nonce) -> {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, - {Url, [], "application/jose+json", FinalBody}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - case decode(Body) of - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason}; - Return -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce} - end; + case make_post_request(Url, FinalBody) of + {ok, Head, Return} -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}; Error -> - failed_http_request(Error, Url) + Error end. -spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> @@ -79,21 +81,12 @@ update_account(Url, PrivateKey, Req, Nonce) -> {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, - {Url, [], "application/jose+json", FinalBody}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - case decode(Body) of - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason}; - Return -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce} - end; + case make_post_request(Url, FinalBody) of + {ok, Head, Return} -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}; Error -> - failed_http_request(Error, Url) + Error end. -spec get_account(url(), jose_jwk:key(), nonce()) -> @@ -105,25 +98,63 @@ get_account(Url, PrivateKey, Nonce) -> {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), %% Encode the Signed body with jiffy FinalBody = jiffy:encode(SignedBody), - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, - {Url, [], "application/jose+json", FinalBody}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - case decode(Body) of - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason}; - Return -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce} - end; + case make_post_request(Url, FinalBody) of + {ok, Head, Return} -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}; Error -> - failed_http_request(Error, Url) + Error end. +-spec delete_account(url(), jose_jwk:key(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +delete_account(Url, PrivateKey, Nonce) -> + %% Make the request body + ReqBody = jiffy:encode({ + [ {<<"resource">>, <<"reg">>} + , {<<"status">>, <<"deactivated">>} + ]}), + %% Jose Sign + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), + %% Encode the Signed body with jiffy + FinalBody = jiffy:encode(SignedBody), + case make_post_request(Url, FinalBody) of + {ok, Head, Return} -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}; + Error -> + Error + end. + +%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Authorization Handling +%% +%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec new_authz(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +new_authz(Url, PrivateKey, Req, Nonce) -> + %% Make the request body + ReqBody = jiffy:encode({ + [ { <<"resource">>, <<"new-authz">>}] ++ Req}), + {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), + %% Encode the Signed body with jiffy + FinalBody = jiffy:encode(SignedBody), + case make_post_request(Url, FinalBody) of + {ok, Head, Return} -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}; + Error -> + Error + end. + + + + + %% %% Useful funs %% @@ -149,11 +180,45 @@ get_tos(Head) -> none end. +make_post_request(Url, ReqBody) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + case decode(Body) of + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason}; + Return -> + {ok, Head, Return} + end; + Error -> + failed_http_request(Error, Url) + 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 PubKey = jose_jwk:to_public(Key), + % ?INFO_MSG("Key: ~p", [Key]), {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), + % ?INFO_MSG("Key Record: ~p", [jose_jwk:to_map(Key)]), PubKeyJson = jiffy:decode(BinaryPubKey), % Jws object containing the algorithm %% TODO: Dont hardcode the alg @@ -197,13 +262,27 @@ scenario(CAUrl, AccId, PrivateKey) -> {ok, Dirs, Nonce0} = directory(DirURL), AccURL = CAUrl ++ "/acme/reg/" ++ AccId, - {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0). + {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0), + + #{"new-authz" := NewAuthz} = Dirs, + Req = + [ { <<"identifier">>, { + [ {<<"type">>, <<"dns">>} + , {<<"value">>, <<"my-acme-test.com">>} + ] }} + , {<<"existing">>, <<"accept">>} + ], + {ok, Authz, Nonce2} = new_authz(NewAuthz, PrivateKey, Req, Nonce1), + + {Account, Authz, PrivateKey}. + new_user_scenario(CAUrl) -> PrivateKey = generate_key(), DirURL = CAUrl ++ "/directory", {ok, Dirs, Nonce0} = directory(DirURL), + ?INFO_MSG("Directories: ~p", [Dirs]), #{"new-reg" := NewAccURL} = Dirs, Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], @@ -211,10 +290,29 @@ new_user_scenario(CAUrl) -> {_, AccId} = proplists:lookup(<<"id">>, Account), AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), - Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account1, Nonce2} = update_account(AccURL, PrivateKey, Req1, Nonce1), + {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), + ?INFO_MSG("Old account: ~p~n", [Account1]), - {Account1, PrivateKey}. + Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], + {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), + + %% + %% Delete account + %% + + {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), + {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), + ?INFO_MSG("New account: ~p~n", [Account4]), + + % NewKey = generate_key(), + % KeyChangeUrl = CAUrl ++ "/acme/key-change/", + % {ok, Account3, Nonce4} = key_roll_over(KeyChangeUrl, AccURL, PrivateKey, NewKey, Nonce3), + % ?INFO_MSG("Changed key: ~p~n", [Account3]), + + % {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, NewKey, Nonce4), + % ?INFO_MSG("New account:~p~n", [Account4]), + + {Account4, PrivateKey}. generate_key() -> jose_jwk:generate_key({ec, secp256r1}). @@ -222,5 +320,7 @@ generate_key() -> %% Just a test scenario0(KeyFile) -> 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"). + +% ejabberd_acme:scenario0("/home/konstantinos/Desktop/Programming/ejabberd/private_key_temporary"). From 4b1c59e199d55142ab17a4fb6733f1e09cff8937 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 12 Jun 2017 21:35:43 +0300 Subject: [PATCH 12/75] Major Refactoring, Separated Logic from Requests --- src/ejabberd_acme.erl | 178 ++++++++++++++++++++++-------------------- 1 file changed, 94 insertions(+), 84 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index f77f8cfa3..772118631 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -11,6 +11,7 @@ % , key_roll_over/5 , new_authz/4 + % , get_authz/3 ]). -include("ejabberd.hrl"). @@ -25,6 +26,7 @@ -type url() :: string(). -type proplist() :: [{_, _}]. -type jws() :: map(). +-type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). -spec directory(url()) -> {ok, map(), nonce()} | {error, _}. @@ -34,130 +36,78 @@ directory(Url) -> case httpc:request(get, {Url, []}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> case decode(Body) of - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason}; - Directories -> + {ok, Directories} -> StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || {X,Y} <- Directories], Nonce = get_nonce(Head), %% Return Map of Directories NewDirs = maps:from_list(StrDirectories), - {ok, NewDirs, Nonce} + {ok, NewDirs, Nonce}; + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason} end; Error -> failed_http_request(Error, Url) end. -%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Account Handling %% -%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. new_account(Url, PrivateKey, Req, Nonce) -> %% Make the request body - ReqBody = jiffy:encode({[{ <<"resource">>, <<"new-reg">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), - %% Encode the Signed body with jiffy - FinalBody = jiffy:encode(SignedBody), - case make_post_request(Url, FinalBody) of - {ok, Head, Return} -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}; - Error -> - Error - end. + EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). -spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. update_account(Url, PrivateKey, Req, Nonce) -> %% Make the request body - ReqBody = jiffy:encode({[{ <<"resource">>, <<"reg">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), - %% Encode the Signed body with jiffy - FinalBody = jiffy:encode(SignedBody), - case make_post_request(Url, FinalBody) of - {ok, Head, Return} -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}; - Error -> - Error - end. + EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). -spec get_account(url(), jose_jwk:key(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. get_account(Url, PrivateKey, Nonce) -> %% Make the request body - ReqBody = jiffy:encode({[{<<"resource">>, <<"reg">>}]}), - %% Jose Sign - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), - %% Encode the Signed body with jiffy - FinalBody = jiffy:encode(SignedBody), - case make_post_request(Url, FinalBody) of - {ok, Head, Return} -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}; - Error -> - Error - end. - + EJson = {[{<<"resource">>, <<"reg">>}]}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). -spec delete_account(url(), jose_jwk:key(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. delete_account(Url, PrivateKey, Nonce) -> - %% Make the request body - ReqBody = jiffy:encode({ + EJson = { [ {<<"resource">>, <<"reg">>} , {<<"status">>, <<"deactivated">>} - ]}), - %% Jose Sign - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), - %% Encode the Signed body with jiffy - FinalBody = jiffy:encode(SignedBody), - case make_post_request(Url, FinalBody) of - {ok, Head, Return} -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}; - Error -> - Error - end. + ]}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). -%%%%%%%%%%%%%%%%%%%%%%%%% + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Authorization Handling %% -%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_authz(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. new_authz(Url, PrivateKey, Req, Nonce) -> - %% Make the request body - ReqBody = jiffy:encode({ - [ { <<"resource">>, <<"new-authz">>}] ++ Req}), - {_, SignedBody} = sign_json_jose(PrivateKey, ReqBody, Nonce), - %% Encode the Signed body with jiffy - FinalBody = jiffy:encode(SignedBody), - case make_post_request(Url, FinalBody) of - {ok, Head, Return} -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}; - Error -> - Error - end. - - - + EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Useful funs %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + -spec get_nonce(proplist()) -> nonce() | 'none'. get_nonce(Head) -> case proplists:lookup("replay-nonce", Head) of @@ -180,6 +130,9 @@ get_tos(Head) -> none end. + +-spec make_post_request(url(), bitstring()) -> + {ok, proplist(), proplist()} | {error, _}. make_post_request(Url, ReqBody) -> Options = [], HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], @@ -187,16 +140,33 @@ make_post_request(Url, ReqBody) -> {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> case decode(Body) of + {ok, Return} -> + {ok, Head, Return}; {error, Reason} -> ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason}; - Return -> - {ok, Head, Return} + {error, Reason} end; Error -> failed_http_request(Error, Url) end. +-spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), + nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. +prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> + case encode(EJson) of + {ok, ReqBody} -> + FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), + case make_post_request(Url, FinalBody) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end; + {error, Reason} -> + ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), + {error, Reason} + end. + -spec sign_json_jose(jose_jwk:key(), string()) -> jws(). sign_json_jose(Key, Json) -> PubKey = jose_jwk:to_public(Key), @@ -228,19 +198,56 @@ sign_json_jose(Key, Json, Nonce) -> , <<"jwk">> => PubKeyJson , <<"nonce">> => list_to_bitstring(Nonce) }), - %% Signed Message jose_jws:sign(Key, Json, JwsObj). -decode(Json) -> +-spec sign_encode_json_jose(jose_jwk:key(), string(), nonce()) -> bitstring(). +sign_encode_json_jose(Key, Json, Nonce) -> + {_, Signed} = sign_json_jose(Key, Json, Nonce), + %% This depends on jose library, so we can consider it safe + jiffy:encode(Signed). + +encode(EJson) -> try - {Result} = jiffy:decode(Json), - Result + {ok, jiffy:encode(EJson)} catch _:Reason -> {error, Reason} end. +decode(Json) -> + try + {Result} = jiffy:decode(Json), + {ok, Result} + catch + _:Reason -> + {error, Reason} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle Response Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec get_response({ok, proplist(), proplist()}) -> {ok, proplist(), nonce()}. +get_response({ok, Head, Return}) -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}. + +-spec get_response_tos({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. +get_response_tos({ok, Head, Return}) -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle Failed HTTP Requests +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + -spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}. failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", @@ -252,9 +259,12 @@ failed_http_request({error, Reason}, Url) -> {error, Reason}. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Debugging Funcs -- They are only used for the development phase %% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% A typical acme workflow scenario(CAUrl, AccId, PrivateKey) -> @@ -268,7 +278,7 @@ scenario(CAUrl, AccId, PrivateKey) -> Req = [ { <<"identifier">>, { [ {<<"type">>, <<"dns">>} - , {<<"value">>, <<"my-acme-test.com">>} + , {<<"value">>, <<"my-acme-test-ejabberd.com">>} ] }} , {<<"existing">>, <<"accept">>} ], From 032ce9e53caca7ac6b7e976d065e97daa16067e5 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 14 Jun 2017 12:12:43 +0300 Subject: [PATCH 13/75] Refactor get requests, Implement authorization handling functions --- src/ejabberd_acme.erl | 150 +++++++++++++++++++++++++++++------------- 1 file changed, 104 insertions(+), 46 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 772118631..c3960388f 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -11,7 +11,7 @@ % , key_roll_over/5 , new_authz/4 - % , get_authz/3 + , get_authz/1 ]). -include("ejabberd.hrl"). @@ -28,29 +28,17 @@ -type jws() :: map(). -type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Get Directory +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + -spec directory(url()) -> {ok, map(), nonce()} | {error, _}. directory(Url) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(get, {Url, []}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - case decode(Body) of - {ok, Directories} -> - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X,Y} <- Directories], - Nonce = get_nonce(Head), - %% Return Map of Directories - NewDirs = maps:from_list(StrDirectories), - {ok, NewDirs, Nonce}; - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason} - end; - Error -> - failed_http_request(Error, Url) - end. - + prepare_get_request(Url, fun get_dirs/1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -99,7 +87,45 @@ delete_account(Url, PrivateKey, Nonce) -> {ok, proplist(), nonce()} | {error, _}. new_authz(Url, PrivateKey, Req, Nonce) -> EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). + +-spec get_authz(url()) -> + {ok, proplist(), nonce()} | {error, _}. +get_authz(Url) -> + prepare_get_request(Url, fun get_response/1). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle Response Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec get_dirs({ok, proplist(), proplist()}) -> {ok, map(), nonce()}. +get_dirs({ok, Head, Return}) -> + NewNonce = get_nonce(Head), + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + {X,Y} <- Return], + NewDirs = maps:from_list(StrDirectories), + {ok, NewDirs, NewNonce}. + +-spec get_response({ok, proplist(), proplist()}) -> {ok, proplist(), nonce()}. +get_response({ok, Head, Return}) -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}. + +-spec get_response_tos({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. +get_response_tos({ok, Head, Return}) -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}. + +-spec get_response_location({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. +get_response_location({ok, Head, Return}) -> + Location = get_location(Head), + NewNonce = get_nonce(Head), + {ok, {Location, Return}, NewNonce}. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -115,6 +141,13 @@ get_nonce(Head) -> none -> none end. +-spec get_location(proplist()) -> url() | 'none'. +get_location(Head) -> + case proplists:lookup("location", Head) of + {"location", Location} -> Location; + none -> none + end. + %% Very bad way to extract this %% TODO: Find a better way -spec get_tos(proplist()) -> url() | 'none'. @@ -130,6 +163,7 @@ get_tos(Head) -> none end. +%% TODO: Fix the duplicated code at the below 4 functions -spec make_post_request(url(), bitstring()) -> {ok, proplist(), proplist()} | {error, _}. @@ -150,6 +184,24 @@ make_post_request(Url, ReqBody) -> failed_http_request(Error, Url) end. +-spec make_get_request(url()) -> + {ok, proplist(), proplist()} | {error, _}. +make_get_request(Url) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(get, {Url, []}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + case decode(Body) of + {ok, Return} -> + {ok, Head, Return}; + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason} + end; + Error -> + failed_http_request(Error, Url) + end. + -spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> @@ -167,6 +219,16 @@ prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> {error, Reason} end. +-spec prepare_get_request(url(), handle_resp_fun()) -> + {ok, _, nonce()} | {error, _}. +prepare_get_request(Url, HandleRespFun) -> + case make_get_request(Url) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end. + -spec sign_json_jose(jose_jwk:key(), string()) -> jws(). sign_json_jose(Key, Json) -> PubKey = jose_jwk:to_public(Key), @@ -224,22 +286,6 @@ decode(Json) -> {error, Reason} end. -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Handle Response Functions -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec get_response({ok, proplist(), proplist()}) -> {ok, proplist(), nonce()}. -get_response({ok, Head, Return}) -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}. - --spec get_response_tos({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. -get_response_tos({ok, Head, Return}) -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -306,13 +352,10 @@ new_user_scenario(CAUrl) -> Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), - %% - %% Delete account - %% - - {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), - {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), - ?INFO_MSG("New account: ~p~n", [Account4]), + % %% Delete account + % {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), + % {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), + % ?INFO_MSG("New account: ~p~n", [Account4]), % NewKey = generate_key(), % KeyChangeUrl = CAUrl ++ "/acme/key-change/", @@ -321,8 +364,23 @@ new_user_scenario(CAUrl) -> % {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, NewKey, Nonce4), % ?INFO_MSG("New account:~p~n", [Account4]), + % {Account4, PrivateKey}. + + AccIdBin = list_to_bitstring(integer_to_list(AccId)), + #{"new-authz" := NewAuthz} = Dirs, + Req2 = + [ { <<"identifier">>, { + [ {<<"type">>, <<"dns">>} + , {<<"value">>, << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>} + ] }} + , {<<"existing">>, <<"accept">>} + ], + {ok, {AuthzUrl, Authz}, Nonce4} = new_authz(NewAuthz, PrivateKey, Req2, Nonce3), + + {ok, Authz2, Nonce5} = get_authz(AuthzUrl), + + {Account2, Authz2, PrivateKey}. - {Account4, PrivateKey}. generate_key() -> jose_jwk:generate_key({ec, secp256r1}). From 133d2ae6d50e6b8e2fe9b1ccb6e2d15e39305c8d Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 15 Jun 2017 11:47:29 +0300 Subject: [PATCH 14/75] Derive the alg field of the JWS object using a erlang-jose library function rather than hardcoding --- src/ejabberd_acme.erl | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index c3960388f..6c4e2d6b7 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -252,14 +252,16 @@ sign_json_jose(Key, Json, Nonce) -> {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), % ?INFO_MSG("Key Record: ~p", [jose_jwk:to_map(Key)]), PubKeyJson = jiffy:decode(BinaryPubKey), - % Jws object containing the algorithm - %% TODO: Dont hardcode the alg - JwsObj = jose_jws:from( - #{ <<"alg">> => <<"ES256">> + %% TODO: Ensure this works for all cases + AlgMap = jose_jwk:signer(Key), + % ?INFO_MSG("Algorithm:~p~n", [AlgMap]), + JwsMap = + #{ <<"jwk">> => PubKeyJson % , <<"b64">> => true - , <<"jwk">> => PubKeyJson , <<"nonce">> => list_to_bitstring(Nonce) - }), + }, + JwsObj0 = maps:merge(JwsMap, AlgMap), + JwsObj = jose_jws:from(JwsObj0), %% Signed Message jose_jws:sign(Key, Json, JwsObj). @@ -388,7 +390,7 @@ generate_key() -> %% Just a test scenario0(KeyFile) -> 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"). % ejabberd_acme:scenario0("/home/konstantinos/Desktop/Programming/ejabberd/private_key_temporary"). From 1d1250b0560cdc242ddca30446f298ae1a8bbe62 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 17 Jun 2017 19:06:39 +0300 Subject: [PATCH 15/75] Cleanup acme_challenge.erl, move types and records in ejabberd_acme.hrl --- include/ejabberd_acme.hrl | 15 ++++++ src/acme_challenge.erl | 108 +++++++++++++++++++++++--------------- src/ejabberd_acme.erl | 53 ++++++++----------- 3 files changed, 105 insertions(+), 71 deletions(-) create mode 100644 include/ejabberd_acme.hrl 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"). From dc4c00a78ccfacb927d6c476bd11c9b41772eabe Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sun, 18 Jun 2017 13:20:47 +0300 Subject: [PATCH 16/75] Add support for solving http-01 challenge --- src/acme_challenge.erl | 7 +++--- src/ejabberd_acme.erl | 57 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index f2497840d..1be246929 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -1,6 +1,6 @@ -module(acme_challenge). --export ([ key_authorization/2 +-export ([ key_authorization/2 , solve_challenge/3 ]). %% Challenge Types @@ -16,7 +16,7 @@ -include("ejabberd_acme.hrl"). --spec parse_challenge(string(), jose_jwk:key()) -> bitstring(). +-spec key_authorization(string(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> Thumbprint = jose_jwk:thumbprint(Key), % ?INFO_MSG("Thumbprint: ~p~n", [Thumbprint]), @@ -34,7 +34,7 @@ parse_challenge(Challenge0) -> Res = #challenge{ type = Type, status = list_to_atom(bitstring_to_list(Status)), - uri = Uri, + uri = bitstring_to_list(Uri), token = Token }, {ok, Res} @@ -73,6 +73,7 @@ solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, {Key, HttpD ?ERROR_MSG("Error writing to file: ~s with reason: ~p~n", [FileLocation, Err]), Err end; +%% TODO: Fill stub solve_challenge1(Challenge, _Key) -> ?INFO_MSG("Challenge: ~p~n", [Challenge]). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 70f835556..671ade835 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -10,7 +10,9 @@ , new_authz/4 , get_authz/1 - + + , solve_challenge/4 + , scenario/3 , scenario0/2 ]). @@ -23,6 +25,7 @@ -include_lib("public_key/include/public_key.hrl"). -define(REQUEST_TIMEOUT, 5000). % 5 seconds. +-define(MAX_POLL_REQUESTS, 20). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -90,6 +93,11 @@ new_authz(Url, PrivateKey, Req, Nonce) -> get_authz(Url) -> prepare_get_request(Url, fun get_response/1). +-spec solve_challenge(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +solve_challenge(Url, PrivateKey, Req, Nonce) -> + EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -164,6 +172,37 @@ get_challenges(Body) -> {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), Challenges. +-spec get_authz_until_valid(url()) -> + {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid(Url) -> + get_authz_until_valid(Url, ?MAX_POLL_REQUESTS). + +-spec get_authz_until_valid(url(), non_neg_integer()) -> + {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid(Url, 0) -> + ?ERROR_MSG("Maximum request limit waiting for validation reached", []), + {error, max_request_limit}; +get_authz_until_valid(Url, N) -> + case get_authz(Url) of + {ok, Resp, Nonce} -> + case is_authz_valid(Resp) of + true -> + {ok, Resp, Nonce}; + false -> + get_authz_until_valid(Url, N-1) + end; + {error, _} = Err -> + Err + end. + +-spec is_authz_valid(proplist()) -> boolean(). +is_authz_valid(Authz) -> + case proplists:lookup(<<"status">>, Authz) of + {<<"status">>, <<"valid">>} -> + true; + none -> + false + end. %% TODO: Fix the duplicated code at the below 4 functions @@ -204,7 +243,7 @@ make_get_request(Url) -> failed_http_request(Error, Url) end. --spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), +-spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> case encode(EJson) of @@ -370,12 +409,22 @@ new_user_scenario(CAUrl, HttpDir) -> {ok, Authz2, Nonce5} = get_authz(AuthzUrl), Challenges = get_challenges(Authz2), - ?INFO_MSG("Challenges: ~p~n", [Challenges]), + % ?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}. + Req3 = + [ {<<"type">>, <<"http-01">>} + , {<<"keyAuthorization">>, KeyAuthz} + ], + {ok, SolvedChallenge, Nonce6} = solve_challenge(ChallengeUrl, PrivateKey, Req3, Nonce5), + ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), + + timer:sleep(2000), + {ok, Authz3, Nonce7} = get_authz_until_valid(AuthzUrl), + + {Account2, Authz2, Authz3, PrivateKey}. generate_key() -> From dd79dea81db2d81028e60579c12fbfe86702f2f0 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 22 Jun 2017 11:31:50 +0300 Subject: [PATCH 17/75] Support new_cert, make certificate request --- src/ejabberd_acme.erl | 495 ++++++++++++++++++++++++++++++------------ 1 file changed, 360 insertions(+), 135 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 671ade835..2fc8dddfc 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -10,8 +10,9 @@ , new_authz/4 , get_authz/1 + , complete_challenge/4 - , solve_challenge/4 + , new_cert/4 , scenario/3 , scenario0/2 @@ -26,11 +27,12 @@ -define(REQUEST_TIMEOUT, 5000). % 5 seconds. -define(MAX_POLL_REQUESTS, 20). +-define(POLL_WAIT_TIME, 500). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% -%% Get Directory +%% Directory %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -93,12 +95,26 @@ new_authz(Url, PrivateKey, Req, Nonce) -> get_authz(Url) -> prepare_get_request(Url, fun get_response/1). --spec solve_challenge(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec complete_challenge(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. -solve_challenge(Url, PrivateKey, Req, Nonce) -> +complete_challenge(Url, PrivateKey, Req, Nonce) -> EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Certificate Handling +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec new_cert(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +new_cert(Url, PrivateKey, Req, Nonce) -> + EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, "application/pkix-cert"). + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Handle Response Functions @@ -109,7 +125,7 @@ solve_challenge(Url, PrivateKey, Req, Nonce) -> get_dirs({ok, Head, Return}) -> NewNonce = get_nonce(Head), StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X,Y} <- Return], + {X, Y} <- Return], NewDirs = maps:from_list(StrDirectories), {ok, NewDirs, NewNonce}. @@ -131,6 +147,304 @@ get_response_location({ok, Head, Return}) -> {ok, {Location, Return}, NewNonce}. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Certificate Request Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% For now we accept only generating a key of +%% specific type for signing the csr +%% TODO: Make this function handle more signing keys +%% 1. Derive oid from Key +%% 2. Derive the whole algo objects from Key +%% TODO: Encode Strings using length. + +-spec make_csr(proplist()) -> binary(). +make_csr(Attributes) -> + Key = generate_key(), + + {_, KeyKey} = jose_jwk:to_key(Key), + + KeyPub = jose_jwk:to_public(Key), + + try + SubPKInfoAlgo = subject_pk_info_algo(KeyPub), + + {ok, RawBinPubKey} = raw_binary_public_key(KeyPub), + SubPKInfo = subject_pk_info(SubPKInfoAlgo, RawBinPubKey), + + {ok, Subject} = attributes_from_list(Attributes), + + CRI = certificate_request_info(SubPKInfo, Subject), + ?INFO_MSG("CRI: ~p~n", [CRI]), + {ok, EncodedCRI} = der_encode( + 'CertificationRequestInfo', + CRI), + + SignedCRI = public_key:sign(EncodedCRI, 'sha256', KeyKey), + + SignatureAlgo = signature_algo(Key, 'sha256'), + + CSR = certification_request(CRI, SignatureAlgo, SignedCRI), + + {ok, DerCSR} = der_encode( + 'CertificationRequest', + CSR), + + Result = base64url:encode(DerCSR), + + Result + catch + _:{badmatch, {error, bad_public_key}} -> + {error, bad_public_key}; + _:{badmatch, {error, bad_attributes}} -> + {error, bad_public_key}; + _:{badmatch, {error, der_encode}} -> + {error, der_encode} + end. + + + +subject_pk_info_algo(_KeyPub) -> + #'SubjectPublicKeyInfoAlgorithm'{ + algorithm = ?'id-ecPublicKey', + parameters = {asn1_OPENTYPE,<<6,8,42,134,72,206,61,3,1,7>>} + }. + +subject_pk_info(Algo, RawBinPubKey) -> + #'SubjectPublicKeyInfo-PKCS-10'{ + algorithm = Algo, + subjectPublicKey = RawBinPubKey + }. + +certificate_request_info(SubPKInfo, Subject) -> + #'CertificationRequestInfo'{ + version = 0, + subject = Subject, + subjectPKInfo = SubPKInfo, + attributes = [] + }. + +signature_algo(_Key, _Hash) -> + #'CertificationRequest_signatureAlgorithm'{ + algorithm = ?'ecdsa-with-SHA256', + parameters = asn1_NOVALUE + }. + +certification_request(CRI, SignatureAlgo, SignedCRI) -> + #'CertificationRequest'{ + certificationRequestInfo = CRI, + signatureAlgorithm = SignatureAlgo, + signature = SignedCRI + }. + +raw_binary_public_key(KeyPub) -> + try + {_, RawPubKey} = jose_jwk:to_key(KeyPub), + {{_, RawBinPubKey}, _} = RawPubKey, + {ok, RawBinPubKey} + catch + _:_ -> + ?ERROR_MSG("Bad public key: ~p~n", [KeyPub]), + {error, bad_public_key} + end. + +der_encode(Type, Term) -> + try + {ok, public_key:der_encode(Type, Term)} + catch + _:_ -> + ?ERROR_MSG("Cannot DER encode: ~p, with asn1type: ~p", [Term, Type]), + {error, der_encode} + end. + +%% TODO: I haven't found a function that does that, but there must exist one +length_bitstring(Bitstring) -> + ?INFO_MSG("Bitstring: ~p", [Bitstring]), + Size = byte_size(Bitstring), + ?INFO_MSG("Size: ~p", [Size]), + case Size =< 127 of + true -> + <<12:8, Size:8, Bitstring/binary>>; + false -> + LenOctets = binary:encode_unsigned(Size), + FirstOctet = byte_size(LenOctets), + <<12:8, 1:1, FirstOctet:7, LenOctets:(FirstOctet * 8), Bitstring/binary>> + end. + + +%% +%% Attributes Parser +%% + +attributes_from_list(Attrs) -> + ParsedAttrs = [attribute_parser_fun(Attr) || Attr <- Attrs], + case lists:any(fun is_error/1, ParsedAttrs) of + true -> + {error, bad_attributes}; + false -> + {ok, {rdnSequence, [[PAttr] || PAttr <- ParsedAttrs]}} + end. + +attribute_parser_fun({AttrName, AttrVal}) -> + try + #'AttributeTypeAndValue'{ + type = attribute_oid(AttrName), + value = length_bitstring(list_to_bitstring(AttrVal)) + } + catch + _:_ -> + ?ERROR_MSG("Bad attribute: ~p~n", [{AttrName, AttrVal}]), + {error, bad_attributes} + end. + +attribute_oid(commonName) -> ?'id-at-commonName'; +attribute_oid(countryName) -> ?'id-at-countryName'; +attribute_oid(stateOrProvinceName) -> ?'id-at-stateOrProvinceName'; +attribute_oid(localityName) -> ?'id-at-localityName'; +attribute_oid(organizationName) -> ?'id-at-organizationName'; +attribute_oid(_) -> error(bad_attributes). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Authorization Polling +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec get_authz_until_valid(url()) -> + {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid(Url) -> + get_authz_until_valid(Url, ?MAX_POLL_REQUESTS). + +-spec get_authz_until_valid(url(), non_neg_integer()) -> + {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid(Url, 0) -> + ?ERROR_MSG("Maximum request limit waiting for validation reached", []), + {error, max_request_limit}; +get_authz_until_valid(Url, N) -> + case get_authz(Url) of + {ok, Resp, Nonce} -> + case is_authz_valid(Resp) of + true -> + {ok, Resp, Nonce}; + false -> + timer:sleep(?POLL_WAIT_TIME), + get_authz_until_valid(Url, N-1) + end; + {error, _} = Err -> + Err + end. + +-spec is_authz_valid(proplist()) -> boolean(). +is_authz_valid(Authz) -> + case proplists:lookup(<<"status">>, Authz) of + {<<"status">>, <<"valid">>} -> + true; + _ -> + false + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Request Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% TODO: Fix the duplicated code at the below 4 functions +-spec make_post_request(url(), bitstring(), string()) -> + {ok, proplist(), proplist()} | {error, _}. +make_post_request(Url, ReqBody, ResponseType) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + decode_response(Head, Body, ResponseType); + Error -> + failed_http_request(Error, Url) + end. + +-spec make_get_request(url(), string()) -> + {ok, proplist(), proplist()} | {error, _}. +make_get_request(Url, ResponseType) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(get, {Url, []}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + decode_response(Head, Body, ResponseType); + Error -> + failed_http_request(Error, Url) + end. + +-spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), + nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. +prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> + prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, "application/jose+json"). + +-spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), + nonce(), handle_resp_fun(), string()) -> {ok, _, nonce()} | {error, _}. +prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, ResponseType) -> + case encode(EJson) of + {ok, ReqBody} -> + FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), + case make_post_request(Url, FinalBody, ResponseType) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end; + {error, Reason} -> + ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), + {error, Reason} + end. + +-spec prepare_get_request(url(), handle_resp_fun()) -> + {ok, _, nonce()} | {error, _}. +prepare_get_request(Url, HandleRespFun) -> + prepare_get_request(Url, HandleRespFun, "application/jose+json"). + +-spec prepare_get_request(url(), handle_resp_fun(), string()) -> + {ok, _, nonce()} | {error, _}. +prepare_get_request(Url, HandleRespFun, ResponseType) -> + case make_get_request(Url, ResponseType) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Jose Json Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec sign_json_jose(jose_jwk:key(), string(), nonce()) -> jws(). +sign_json_jose(Key, Json, Nonce) -> + PubKey = jose_jwk:to_public(Key), + {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), + PubKeyJson = jiffy:decode(BinaryPubKey), + %% TODO: Ensure this works for all cases + AlgMap = jose_jwk:signer(Key), + JwsMap = + #{ <<"jwk">> => PubKeyJson + % , <<"b64">> => true + , <<"nonce">> => list_to_bitstring(Nonce) + }, + JwsObj0 = maps:merge(JwsMap, AlgMap), + JwsObj = jose_jws:from(JwsObj0), + jose_jws:sign(Key, Json, JwsObj). + +-spec sign_encode_json_jose(jose_jwk:key(), string(), nonce()) -> bitstring(). +sign_encode_json_jose(Key, Json, Nonce) -> + {_, Signed} = sign_json_jose(Key, Json, Nonce), + %% This depends on jose library, so we can consider it safe + jiffy:encode(Signed). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -172,131 +486,17 @@ get_challenges(Body) -> {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), Challenges. --spec get_authz_until_valid(url()) -> - {ok, proplist(), nonce()} | {error, _}. -get_authz_until_valid(Url) -> - get_authz_until_valid(Url, ?MAX_POLL_REQUESTS). - --spec get_authz_until_valid(url(), non_neg_integer()) -> - {ok, proplist(), nonce()} | {error, _}. -get_authz_until_valid(Url, 0) -> - ?ERROR_MSG("Maximum request limit waiting for validation reached", []), - {error, max_request_limit}; -get_authz_until_valid(Url, N) -> - case get_authz(Url) of - {ok, Resp, Nonce} -> - case is_authz_valid(Resp) of - true -> - {ok, Resp, Nonce}; - false -> - get_authz_until_valid(Url, N-1) - end; - {error, _} = Err -> - Err - end. - --spec is_authz_valid(proplist()) -> boolean(). -is_authz_valid(Authz) -> - case proplists:lookup(<<"status">>, Authz) of - {<<"status">>, <<"valid">>} -> - true; - none -> - false - end. - -%% TODO: Fix the duplicated code at the below 4 functions - --spec make_post_request(url(), bitstring()) -> - {ok, proplist(), proplist()} | {error, _}. -make_post_request(Url, ReqBody) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, - {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - case decode(Body) of - {ok, Return} -> - {ok, Head, Return}; - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason} - end; - Error -> - failed_http_request(Error, Url) - end. - --spec make_get_request(url()) -> - {ok, proplist(), proplist()} | {error, _}. -make_get_request(Url) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(get, {Url, []}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - case decode(Body) of - {ok, Return} -> - {ok, Head, Return}; - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason} - end; - Error -> - failed_http_request(Error, Url) - end. - --spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), - nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. -prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> - case encode(EJson) of - {ok, ReqBody} -> - FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), - case make_post_request(Url, FinalBody) of - {ok, Head, Return} -> - HandleRespFun({ok, Head, Return}); - Error -> - Error - end; +decode_response(Head, Body, "application/pkix-cert") -> + {ok, Head, Body}; +decode_response(Head, Body, "application/jose+json") -> + case decode(Body) of + {ok, Return} -> + {ok, Head, Return}; {error, Reason} -> - ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), + ?ERROR_MSG("Problem decoding: ~s", [Body]), {error, Reason} end. --spec prepare_get_request(url(), handle_resp_fun()) -> - {ok, _, nonce()} | {error, _}. -prepare_get_request(Url, HandleRespFun) -> - case make_get_request(Url) of - {ok, Head, Return} -> - HandleRespFun({ok, Head, Return}); - Error -> - Error - end. - --spec sign_json_jose(jose_jwk:key(), string(), nonce()) -> jws(). -sign_json_jose(Key, Json, Nonce) -> - % Generate a public key - PubKey = jose_jwk:to_public(Key), - % ?INFO_MSG("Key: ~p", [Key]), - {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), - % ?INFO_MSG("Key Record: ~p", [jose_jwk:to_map(Key)]), - PubKeyJson = jiffy:decode(BinaryPubKey), - %% TODO: Ensure this works for all cases - AlgMap = jose_jwk:signer(Key), - % ?INFO_MSG("Algorithm:~p~n", [AlgMap]), - JwsMap = - #{ <<"jwk">> => PubKeyJson - % , <<"b64">> => true - , <<"nonce">> => list_to_bitstring(Nonce) - }, - JwsObj0 = maps:merge(JwsMap, AlgMap), - JwsObj = jose_jws:from(JwsObj0), - %% Signed Message - jose_jws:sign(Key, Json, JwsObj). - --spec sign_encode_json_jose(jose_jwk:key(), string(), nonce()) -> bitstring(). -sign_encode_json_jose(Key, Json, Nonce) -> - {_, Signed} = sign_json_jose(Key, Json, Nonce), - %% This depends on jose library, so we can consider it safe - jiffy:encode(Signed). - encode(EJson) -> try {ok, jiffy:encode(EJson)} @@ -314,7 +514,8 @@ decode(Json) -> {error, Reason} end. - +is_error({error, _}) -> true; +is_error(_) -> false. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -367,7 +568,7 @@ new_user_scenario(CAUrl, HttpDir) -> DirURL = CAUrl ++ "/directory", {ok, Dirs, Nonce0} = directory(DirURL), - ?INFO_MSG("Directories: ~p", [Dirs]), + % ?INFO_MSG("Directories: ~p", [Dirs]), #{"new-reg" := NewAccURL} = Dirs, Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], @@ -376,7 +577,7 @@ new_user_scenario(CAUrl, HttpDir) -> {_, AccId} = proplists:lookup(<<"id">>, Account), AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), - ?INFO_MSG("Old account: ~p~n", [Account1]), + % ?INFO_MSG("Old account: ~p~n", [Account1]), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), @@ -397,10 +598,11 @@ new_user_scenario(CAUrl, HttpDir) -> AccIdBin = list_to_bitstring(integer_to_list(AccId)), #{"new-authz" := NewAuthz} = Dirs, + DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, Req2 = [ { <<"identifier">>, { [ {<<"type">>, <<"dns">>} - , {<<"value">>, << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>} + , {<<"value">>, DomainName} ] }} , {<<"existing">>, <<"accept">>} ], @@ -411,28 +613,51 @@ new_user_scenario(CAUrl, HttpDir) -> Challenges = get_challenges(Authz2), % ?INFO_MSG("Challenges: ~p~n", [Challenges]), - {ok, ChallengeUrl, KeyAuthz} = acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), + {ok, ChallengeUrl, KeyAuthz} = + acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), ?INFO_MSG("File for http-01 challenge written correctly", []), Req3 = [ {<<"type">>, <<"http-01">>} , {<<"keyAuthorization">>, KeyAuthz} ], - {ok, SolvedChallenge, Nonce6} = solve_challenge(ChallengeUrl, PrivateKey, Req3, Nonce5), - ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), + {ok, SolvedChallenge, Nonce6} = complete_challenge(ChallengeUrl, PrivateKey, Req3, Nonce5), + % ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), - timer:sleep(2000), + % timer:sleep(2000), {ok, Authz3, Nonce7} = get_authz_until_valid(AuthzUrl), - {Account2, Authz2, Authz3, PrivateKey}. + #{"new-cert" := NewCert} = Dirs, + CSRSubject = [ {commonName, bitstring_to_list(DomainName)} + , {organizationName, "Example Corp"}], + CSR = make_csr(CSRSubject), + {MegS, Sec, MicS} = erlang:timestamp(), + NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), + NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), + Req4 = + [ {<<"csr">>, CSR} + , {<<"notBefore">>, NotBefore} + , {<<"NotAfter">>, NotAfter} + ], + {ok, Certificate, Nonce8} = new_cert(NewCert, PrivateKey, Req4, Nonce7), + + {Account2, Authz2, Authz3, CSR, Certificate, PrivateKey}. generate_key() -> jose_jwk:generate_key({ec, secp256r1}). +scenario3() -> + CSRSubject = [ {commonName, "my-acme-test-ejabberd.com"} + , {organizationName, "Example Corp"}], + CSR = make_csr(CSRSubject). + + + %% Just a test scenario0(KeyFile, HttpDir) -> PrivateKey = jose_jwk:from_file(KeyFile), % scenario("http://localhost:4000", "2", PrivateKey). new_user_scenario("http://localhost:4000", HttpDir). + % scenario3(). From 396bd5eb3d3b091d4f746237f73fc50fbfd8d0b7 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 22 Jun 2017 11:38:40 +0300 Subject: [PATCH 18/75] Removed some ?INFO_MSG --- src/ejabberd_acme.erl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 2fc8dddfc..056ea83b0 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -177,7 +177,6 @@ make_csr(Attributes) -> {ok, Subject} = attributes_from_list(Attributes), CRI = certificate_request_info(SubPKInfo, Subject), - ?INFO_MSG("CRI: ~p~n", [CRI]), {ok, EncodedCRI} = der_encode( 'CertificationRequestInfo', CRI), @@ -261,9 +260,7 @@ der_encode(Type, Term) -> %% TODO: I haven't found a function that does that, but there must exist one length_bitstring(Bitstring) -> - ?INFO_MSG("Bitstring: ~p", [Bitstring]), Size = byte_size(Bitstring), - ?INFO_MSG("Size: ~p", [Size]), case Size =< 127 of true -> <<12:8, Size:8, Bitstring/binary>>; From 330456bcf0a4bc28e38178ab86c779c9c7efd030 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 22 Jun 2017 14:47:56 +0300 Subject: [PATCH 19/75] Indent using Emacs --- src/acme_challenge.erl | 101 +++--- src/ejabberd_acme.erl | 715 ++++++++++++++++++++--------------------- 2 files changed, 404 insertions(+), 412 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index 1be246929..b27fc1ee7 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -1,7 +1,7 @@ -module(acme_challenge). --export ([ key_authorization/2 - , solve_challenge/3 +-export ([ key_authorization/2, + solve_challenge/3 ]). %% Challenge Types %% ================ @@ -18,71 +18,72 @@ -spec key_authorization(string(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> - Thumbprint = jose_jwk:thumbprint(Key), - % ?INFO_MSG("Thumbprint: ~p~n", [Thumbprint]), - KeyAuthorization = erlang:iolist_to_binary([Token, <<".">>, Thumbprint]), - KeyAuthorization. + Thumbprint = jose_jwk:thumbprint(Key), + %% ?INFO_MSG("Thumbprint: ~p~n", [Thumbprint]), + KeyAuthorization = erlang:iolist_to_binary([Token, <<".">>, Thumbprint]), + KeyAuthorization. -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 = bitstring_to_list(Uri), - token = Token - }, - {ok, Res} - catch - _:Error -> - {error, Error} - end. + 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 = bitstring_to_list(Uri), + token = Token + }, + {ok, Res} + catch + _:Error -> + {error, Error} + end. -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. + 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), - 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; + KeyAuthz = key_authorization(Tkn, Key), + 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; %% TODO: Fill stub solve_challenge1(Challenge, _Key) -> - ?INFO_MSG("Challenge: ~p~n", [Challenge]). + ?INFO_MSG("Challenge: ~p~n", [Challenge]). %% Useful functions is_challenge_type(DesiredType, #challenge{type = Type}) when DesiredType =:= Type -> - true; + true; is_challenge_type(_DesiredType, #challenge{type = _Type}) -> - false. + false. is_error({error, _}) -> true; -is_error(_) -> false. \ No newline at end of file +is_error(_) -> false. diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 056ea83b0..927cccf33 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,22 +1,18 @@ -module (ejabberd_acme). --export([ directory/1 - - , get_account/3 - , new_account/4 - , update_account/4 - , delete_account/3 - % , key_roll_over/5 - - , new_authz/4 - , get_authz/1 - , complete_challenge/4 - - , new_cert/4 - - , scenario/3 - , scenario0/2 - ]). +-export([directory/1, + get_account/3, + new_account/4, + update_account/4, + delete_account/3, + new_authz/4, + get_authz/1, + complete_challenge/4, + new_cert/4, + scenario/3, + scenario0/2 + %% , key_roll_over/5 + ]). -include("ejabberd.hrl"). -include("logger.hrl"). @@ -36,10 +32,9 @@ %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec directory(url()) -> - {ok, map(), nonce()} | {error, _}. +-spec directory(url()) -> {ok, map(), nonce()} | {error, _}. directory(Url) -> - prepare_get_request(Url, fun get_dirs/1). + prepare_get_request(Url, fun get_dirs/1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -48,34 +43,33 @@ directory(Url) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, {url(), proplist()}, nonce()} | {error, _}. + {ok, {url(), proplist()}, nonce()} | {error, _}. new_account(Url, PrivateKey, Req, Nonce) -> - %% Make the request body - EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). + %% Make the request body + EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). -spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, proplist(), nonce()} | {error, _}. update_account(Url, PrivateKey, Req, Nonce) -> - %% Make the request body - EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + %% Make the request body + EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). -spec get_account(url(), jose_jwk:key(), nonce()) -> - {ok, {url(), proplist()}, nonce()} | {error, _}. + {ok, {url(), proplist()}, nonce()} | {error, _}. get_account(Url, PrivateKey, Nonce) -> - %% Make the request body - EJson = {[{<<"resource">>, <<"reg">>}]}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). + %% Make the request body + EJson = {[{<<"resource">>, <<"reg">>}]}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). -spec delete_account(url(), jose_jwk:key(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, proplist(), nonce()} | {error, _}. delete_account(Url, PrivateKey, Nonce) -> - EJson = { - [ {<<"resource">>, <<"reg">>} - , {<<"status">>, <<"deactivated">>} - ]}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + EJson = + {[{<<"resource">>, <<"reg">>}, + {<<"status">>, <<"deactivated">>}]}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -85,21 +79,20 @@ delete_account(Url, PrivateKey, Nonce) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_authz(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, proplist(), nonce()} | {error, _}. new_authz(Url, PrivateKey, Req, Nonce) -> - EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). + EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). --spec get_authz(url()) -> - {ok, proplist(), nonce()} | {error, _}. +-spec get_authz(url()) -> {ok, proplist(), nonce()} | {error, _}. get_authz(Url) -> - prepare_get_request(Url, fun get_response/1). + prepare_get_request(Url, fun get_response/1). -spec complete_challenge(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, proplist(), nonce()} | {error, _}. complete_challenge(Url, PrivateKey, Req, Nonce) -> - EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -109,10 +102,10 @@ complete_challenge(Url, PrivateKey, Req, Nonce) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_cert(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, proplist(), nonce()} | {error, _}. new_cert(Url, PrivateKey, Req, Nonce) -> - EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, "application/pkix-cert"). + EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, "application/pkix-cert"). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -123,28 +116,28 @@ new_cert(Url, PrivateKey, Req, Nonce) -> -spec get_dirs({ok, proplist(), proplist()}) -> {ok, map(), nonce()}. get_dirs({ok, Head, Return}) -> - NewNonce = get_nonce(Head), - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X, Y} <- Return], - NewDirs = maps:from_list(StrDirectories), - {ok, NewDirs, NewNonce}. + NewNonce = get_nonce(Head), + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + {X, Y} <- Return], + NewDirs = maps:from_list(StrDirectories), + {ok, NewDirs, NewNonce}. -spec get_response({ok, proplist(), proplist()}) -> {ok, proplist(), nonce()}. get_response({ok, Head, Return}) -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}. + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}. -spec get_response_tos({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. get_response_tos({ok, Head, Return}) -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}. + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}. -spec get_response_location({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. get_response_location({ok, Head, Return}) -> - Location = get_location(Head), - NewNonce = get_nonce(Head), - {ok, {Location, Return}, NewNonce}. + Location = get_location(Head), + NewNonce = get_nonce(Head), + {ok, {Location, Return}, NewNonce}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -162,113 +155,113 @@ get_response_location({ok, Head, Return}) -> -spec make_csr(proplist()) -> binary(). make_csr(Attributes) -> - Key = generate_key(), + Key = generate_key(), - {_, KeyKey} = jose_jwk:to_key(Key), + {_, KeyKey} = jose_jwk:to_key(Key), - KeyPub = jose_jwk:to_public(Key), + KeyPub = jose_jwk:to_public(Key), - try - SubPKInfoAlgo = subject_pk_info_algo(KeyPub), + try + SubPKInfoAlgo = subject_pk_info_algo(KeyPub), - {ok, RawBinPubKey} = raw_binary_public_key(KeyPub), - SubPKInfo = subject_pk_info(SubPKInfoAlgo, RawBinPubKey), + {ok, RawBinPubKey} = raw_binary_public_key(KeyPub), + SubPKInfo = subject_pk_info(SubPKInfoAlgo, RawBinPubKey), - {ok, Subject} = attributes_from_list(Attributes), + {ok, Subject} = attributes_from_list(Attributes), - CRI = certificate_request_info(SubPKInfo, Subject), - {ok, EncodedCRI} = der_encode( - 'CertificationRequestInfo', - CRI), + CRI = certificate_request_info(SubPKInfo, Subject), + {ok, EncodedCRI} = der_encode( + 'CertificationRequestInfo', + CRI), - SignedCRI = public_key:sign(EncodedCRI, 'sha256', KeyKey), + SignedCRI = public_key:sign(EncodedCRI, 'sha256', KeyKey), - SignatureAlgo = signature_algo(Key, 'sha256'), + SignatureAlgo = signature_algo(Key, 'sha256'), - CSR = certification_request(CRI, SignatureAlgo, SignedCRI), + CSR = certification_request(CRI, SignatureAlgo, SignedCRI), - {ok, DerCSR} = der_encode( - 'CertificationRequest', - CSR), + {ok, DerCSR} = der_encode( + 'CertificationRequest', + CSR), - Result = base64url:encode(DerCSR), + Result = base64url:encode(DerCSR), - Result - catch - _:{badmatch, {error, bad_public_key}} -> - {error, bad_public_key}; - _:{badmatch, {error, bad_attributes}} -> - {error, bad_public_key}; - _:{badmatch, {error, der_encode}} -> - {error, der_encode} - end. + Result + catch + _:{badmatch, {error, bad_public_key}} -> + {error, bad_public_key}; + _:{badmatch, {error, bad_attributes}} -> + {error, bad_public_key}; + _:{badmatch, {error, der_encode}} -> + {error, der_encode} + end. subject_pk_info_algo(_KeyPub) -> - #'SubjectPublicKeyInfoAlgorithm'{ - algorithm = ?'id-ecPublicKey', - parameters = {asn1_OPENTYPE,<<6,8,42,134,72,206,61,3,1,7>>} - }. + #'SubjectPublicKeyInfoAlgorithm'{ + algorithm = ?'id-ecPublicKey', + parameters = {asn1_OPENTYPE,<<6,8,42,134,72,206,61,3,1,7>>} + }. subject_pk_info(Algo, RawBinPubKey) -> - #'SubjectPublicKeyInfo-PKCS-10'{ - algorithm = Algo, - subjectPublicKey = RawBinPubKey - }. + #'SubjectPublicKeyInfo-PKCS-10'{ + algorithm = Algo, + subjectPublicKey = RawBinPubKey + }. certificate_request_info(SubPKInfo, Subject) -> - #'CertificationRequestInfo'{ - version = 0, - subject = Subject, - subjectPKInfo = SubPKInfo, - attributes = [] - }. + #'CertificationRequestInfo'{ + version = 0, + subject = Subject, + subjectPKInfo = SubPKInfo, + attributes = [] + }. signature_algo(_Key, _Hash) -> - #'CertificationRequest_signatureAlgorithm'{ - algorithm = ?'ecdsa-with-SHA256', - parameters = asn1_NOVALUE - }. + #'CertificationRequest_signatureAlgorithm'{ + algorithm = ?'ecdsa-with-SHA256', + parameters = asn1_NOVALUE + }. certification_request(CRI, SignatureAlgo, SignedCRI) -> - #'CertificationRequest'{ - certificationRequestInfo = CRI, - signatureAlgorithm = SignatureAlgo, - signature = SignedCRI - }. + #'CertificationRequest'{ + certificationRequestInfo = CRI, + signatureAlgorithm = SignatureAlgo, + signature = SignedCRI + }. raw_binary_public_key(KeyPub) -> - try - {_, RawPubKey} = jose_jwk:to_key(KeyPub), - {{_, RawBinPubKey}, _} = RawPubKey, - {ok, RawBinPubKey} - catch - _:_ -> - ?ERROR_MSG("Bad public key: ~p~n", [KeyPub]), - {error, bad_public_key} - end. + try + {_, RawPubKey} = jose_jwk:to_key(KeyPub), + {{_, RawBinPubKey}, _} = RawPubKey, + {ok, RawBinPubKey} + catch + _:_ -> + ?ERROR_MSG("Bad public key: ~p~n", [KeyPub]), + {error, bad_public_key} + end. der_encode(Type, Term) -> - try - {ok, public_key:der_encode(Type, Term)} - catch - _:_ -> - ?ERROR_MSG("Cannot DER encode: ~p, with asn1type: ~p", [Term, Type]), - {error, der_encode} - end. + try + {ok, public_key:der_encode(Type, Term)} + catch + _:_ -> + ?ERROR_MSG("Cannot DER encode: ~p, with asn1type: ~p", [Term, Type]), + {error, der_encode} + end. %% TODO: I haven't found a function that does that, but there must exist one length_bitstring(Bitstring) -> - Size = byte_size(Bitstring), - case Size =< 127 of - true -> - <<12:8, Size:8, Bitstring/binary>>; - false -> - LenOctets = binary:encode_unsigned(Size), - FirstOctet = byte_size(LenOctets), - <<12:8, 1:1, FirstOctet:7, LenOctets:(FirstOctet * 8), Bitstring/binary>> - end. + Size = byte_size(Bitstring), + case Size =< 127 of + true -> + <<12:8, Size:8, Bitstring/binary>>; + false -> + LenOctets = binary:encode_unsigned(Size), + FirstOctet = byte_size(LenOctets), + <<12:8, 1:1, FirstOctet:7, LenOctets:(FirstOctet * 8), Bitstring/binary>> + end. %% @@ -276,25 +269,25 @@ length_bitstring(Bitstring) -> %% attributes_from_list(Attrs) -> - ParsedAttrs = [attribute_parser_fun(Attr) || Attr <- Attrs], - case lists:any(fun is_error/1, ParsedAttrs) of - true -> - {error, bad_attributes}; - false -> - {ok, {rdnSequence, [[PAttr] || PAttr <- ParsedAttrs]}} - end. + ParsedAttrs = [attribute_parser_fun(Attr) || Attr <- Attrs], + case lists:any(fun is_error/1, ParsedAttrs) of + true -> + {error, bad_attributes}; + false -> + {ok, {rdnSequence, [[PAttr] || PAttr <- ParsedAttrs]}} + end. attribute_parser_fun({AttrName, AttrVal}) -> - try - #'AttributeTypeAndValue'{ - type = attribute_oid(AttrName), - value = length_bitstring(list_to_bitstring(AttrVal)) - } - catch - _:_ -> - ?ERROR_MSG("Bad attribute: ~p~n", [{AttrName, AttrVal}]), - {error, bad_attributes} - end. + try + #'AttributeTypeAndValue'{ + type = attribute_oid(AttrName), + value = length_bitstring(list_to_bitstring(AttrVal)) + } + catch + _:_ -> + ?ERROR_MSG("Bad attribute: ~p~n", [{AttrName, AttrVal}]), + {error, bad_attributes} + end. attribute_oid(commonName) -> ?'id-at-commonName'; attribute_oid(countryName) -> ?'id-at-countryName'; @@ -310,38 +303,37 @@ attribute_oid(_) -> error(bad_attributes). %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec get_authz_until_valid(url()) -> - {ok, proplist(), nonce()} | {error, _}. +-spec get_authz_until_valid(url()) -> {ok, proplist(), nonce()} | {error, _}. get_authz_until_valid(Url) -> - get_authz_until_valid(Url, ?MAX_POLL_REQUESTS). + get_authz_until_valid(Url, ?MAX_POLL_REQUESTS). -spec get_authz_until_valid(url(), non_neg_integer()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, proplist(), nonce()} | {error, _}. get_authz_until_valid(Url, 0) -> - ?ERROR_MSG("Maximum request limit waiting for validation reached", []), - {error, max_request_limit}; + ?ERROR_MSG("Maximum request limit waiting for validation reached", []), + {error, max_request_limit}; get_authz_until_valid(Url, N) -> - case get_authz(Url) of - {ok, Resp, Nonce} -> - case is_authz_valid(Resp) of - true -> - {ok, Resp, Nonce}; - false -> - timer:sleep(?POLL_WAIT_TIME), - get_authz_until_valid(Url, N-1) - end; - {error, _} = Err -> - Err - end. + case get_authz(Url) of + {ok, Resp, Nonce} -> + case is_authz_valid(Resp) of + true -> + {ok, Resp, Nonce}; + false -> + timer:sleep(?POLL_WAIT_TIME), + get_authz_until_valid(Url, N-1) + end; + {error, _} = Err -> + Err + end. -spec is_authz_valid(proplist()) -> boolean(). is_authz_valid(Authz) -> - case proplists:lookup(<<"status">>, Authz) of - {<<"status">>, <<"valid">>} -> - true; - _ -> - false - end. + case proplists:lookup(<<"status">>, Authz) of + {<<"status">>, <<"valid">>} -> + true; + _ -> + false + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -352,66 +344,66 @@ is_authz_valid(Authz) -> %% TODO: Fix the duplicated code at the below 4 functions -spec make_post_request(url(), bitstring(), string()) -> - {ok, proplist(), proplist()} | {error, _}. + {ok, proplist(), proplist()} | {error, _}. make_post_request(Url, ReqBody, ResponseType) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, - {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - decode_response(Head, Body, ResponseType); - Error -> - failed_http_request(Error, Url) - end. + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + decode_response(Head, Body, ResponseType); + Error -> + failed_http_request(Error, Url) + end. -spec make_get_request(url(), string()) -> - {ok, proplist(), proplist()} | {error, _}. + {ok, proplist(), proplist()} | {error, _}. make_get_request(Url, ResponseType) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(get, {Url, []}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - decode_response(Head, Body, ResponseType); - Error -> - failed_http_request(Error, Url) - end. + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(get, {Url, []}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + decode_response(Head, Body, ResponseType); + Error -> + failed_http_request(Error, Url) + end. -spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), - nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. + nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> - prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, "application/jose+json"). + prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, "application/jose+json"). -spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), - nonce(), handle_resp_fun(), string()) -> {ok, _, nonce()} | {error, _}. + nonce(), handle_resp_fun(), string()) -> {ok, _, nonce()} | {error, _}. prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, ResponseType) -> - case encode(EJson) of - {ok, ReqBody} -> - FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), - case make_post_request(Url, FinalBody, ResponseType) of - {ok, Head, Return} -> - HandleRespFun({ok, Head, Return}); - Error -> - Error - end; - {error, Reason} -> - ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), - {error, Reason} - end. + case encode(EJson) of + {ok, ReqBody} -> + FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), + case make_post_request(Url, FinalBody, ResponseType) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end; + {error, Reason} -> + ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), + {error, Reason} + end. -spec prepare_get_request(url(), handle_resp_fun()) -> - {ok, _, nonce()} | {error, _}. + {ok, _, nonce()} | {error, _}. prepare_get_request(Url, HandleRespFun) -> - prepare_get_request(Url, HandleRespFun, "application/jose+json"). + prepare_get_request(Url, HandleRespFun, "application/jose+json"). -spec prepare_get_request(url(), handle_resp_fun(), string()) -> - {ok, _, nonce()} | {error, _}. + {ok, _, nonce()} | {error, _}. prepare_get_request(Url, HandleRespFun, ResponseType) -> - case make_get_request(Url, ResponseType) of - {ok, Head, Return} -> - HandleRespFun({ok, Head, Return}); - Error -> - Error - end. + case make_get_request(Url, ResponseType) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -428,19 +420,19 @@ sign_json_jose(Key, Json, Nonce) -> %% TODO: Ensure this works for all cases AlgMap = jose_jwk:signer(Key), JwsMap = - #{ <<"jwk">> => PubKeyJson - % , <<"b64">> => true - , <<"nonce">> => list_to_bitstring(Nonce) - }, + #{ <<"jwk">> => PubKeyJson, + %% <<"b64">> => true, + <<"nonce">> => list_to_bitstring(Nonce) + }, JwsObj0 = maps:merge(JwsMap, AlgMap), JwsObj = jose_jws:from(JwsObj0), jose_jws:sign(Key, Json, JwsObj). -spec sign_encode_json_jose(jose_jwk:key(), string(), nonce()) -> bitstring(). sign_encode_json_jose(Key, Json, Nonce) -> - {_, Signed} = sign_json_jose(Key, Json, Nonce), - %% This depends on jose library, so we can consider it safe - jiffy:encode(Signed). + {_, Signed} = sign_json_jose(Key, Json, Nonce), + %% This depends on jose library, so we can consider it safe + jiffy:encode(Signed). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -451,32 +443,33 @@ sign_encode_json_jose(Key, Json, Nonce) -> -spec get_nonce(proplist()) -> nonce() | 'none'. get_nonce(Head) -> - case proplists:lookup("replay-nonce", Head) of - {"replay-nonce", Nonce} -> Nonce; - none -> none - end. + case proplists:lookup("replay-nonce", Head) of + {"replay-nonce", Nonce} -> Nonce; + none -> none + end. -spec get_location(proplist()) -> url() | 'none'. get_location(Head) -> - case proplists:lookup("location", Head) of - {"location", Location} -> Location; - none -> none - end. + case proplists:lookup("location", Head) of + {"location", Location} -> Location; + none -> none + end. %% Very bad way to extract this %% TODO: Find a better way -spec get_tos(proplist()) -> url() | 'none'. get_tos(Head) -> - try - [{_, Link}] = [{K, V} || {K, V} <- Head, - K =:= "link" andalso lists:suffix("\"terms-of-service\"", V)], - [Link1, _] = string:tokens(Link, ";"), - Link2 = string:strip(Link1, left, $<), - string:strip(Link2, right, $>) - catch - _:_ -> - none - end. + try + [{_, Link}] = [{K, V} || {K, V} <- Head, + K =:= "link" andalso + lists:suffix("\"terms-of-service\"", V)], + [Link1, _] = string:tokens(Link, ";"), + Link2 = string:strip(Link1, left, $<), + string:strip(Link2, right, $>) + catch + _:_ -> + none + end. -spec get_challenges(proplist()) -> [{proplist()}]. get_challenges(Body) -> @@ -484,32 +477,32 @@ get_challenges(Body) -> Challenges. decode_response(Head, Body, "application/pkix-cert") -> - {ok, Head, Body}; + {ok, Head, Body}; decode_response(Head, Body, "application/jose+json") -> - case decode(Body) of - {ok, Return} -> - {ok, Head, Return}; - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason} - end. + case decode(Body) of + {ok, Return} -> + {ok, Head, Return}; + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason} + end. encode(EJson) -> - try - {ok, jiffy:encode(EJson)} - catch - _:Reason -> - {error, Reason} - end. + try + {ok, jiffy:encode(EJson)} + catch + _:Reason -> + {error, Reason} + end. decode(Json) -> - try - {Result} = jiffy:decode(Json), - {ok, Result} - catch - _:Reason -> - {error, Reason} - end. + try + {Result} = jiffy:decode(Json), + {ok, Result} + catch + _:Reason -> + {error, Reason} + end. is_error({error, _}) -> true; is_error(_) -> false. @@ -522,13 +515,13 @@ is_error(_) -> false. -spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}. failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) -> - ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", - [Url, Code, Body]), - {error, unexpected_code}; + ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", + [Url, Code, Body]), + {error, unexpected_code}; failed_http_request({error, Reason}, Url) -> - ?ERROR_MSG("Error making a request to <~s>: ~p", - [Url, Reason]), - {error, Reason}. + ?ERROR_MSG("Error making a request to <~s>: ~p", + [Url, Reason]), + {error, Reason}. @@ -540,121 +533,119 @@ failed_http_request({error, Reason}, Url) -> %% A typical acme workflow scenario(CAUrl, AccId, PrivateKey) -> - DirURL = CAUrl ++ "/directory", - {ok, Dirs, Nonce0} = directory(DirURL), + DirURL = CAUrl ++ "/directory", + {ok, Dirs, Nonce0} = directory(DirURL), - AccURL = CAUrl ++ "/acme/reg/" ++ AccId, - {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0), - ?INFO_MSG("Account: ~p~n", [Account]), + 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 = - [ { <<"identifier">>, { - [ {<<"type">>, <<"dns">>} - , {<<"value">>, <<"my-acme-test-ejabberd.com">>} - ] }} - , {<<"existing">>, <<"accept">>} - ], - {ok, Authz, Nonce2} = new_authz(NewAuthz, PrivateKey, Req, Nonce1), + #{"new-authz" := NewAuthz} = Dirs, + Req = + [{<<"identifier">>, + {[{<<"type">>, <<"dns">>}, + {<<"value">>, <<"my-acme-test-ejabberd.com">>}]}}, + {<<"existing">>, <<"accept">>} + ], + {ok, Authz, Nonce2} = new_authz(NewAuthz, PrivateKey, Req, Nonce1), - {Account, Authz, PrivateKey}. + {Account, Authz, PrivateKey}. new_user_scenario(CAUrl, HttpDir) -> - PrivateKey = generate_key(), + PrivateKey = generate_key(), - DirURL = CAUrl ++ "/directory", - {ok, Dirs, Nonce0} = directory(DirURL), - % ?INFO_MSG("Directories: ~p", [Dirs]), + DirURL = CAUrl ++ "/directory", + {ok, Dirs, Nonce0} = directory(DirURL), + %% ?INFO_MSG("Directories: ~p", [Dirs]), - #{"new-reg" := NewAccURL} = Dirs, - Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), + #{"new-reg" := NewAccURL} = Dirs, + Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], + {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), - {_, AccId} = proplists:lookup(<<"id">>, Account), - AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), - {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), - % ?INFO_MSG("Old account: ~p~n", [Account1]), + {_, AccId} = proplists:lookup(<<"id">>, Account), + AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), + {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), + %% ?INFO_MSG("Old account: ~p~n", [Account1]), - Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), + Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], + {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), - % %% Delete account - % {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), - % {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), - % ?INFO_MSG("New account: ~p~n", [Account4]), + %% Delete account + %% {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), + %% {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), + %% ?INFO_MSG("New account: ~p~n", [Account4]), - % NewKey = generate_key(), - % KeyChangeUrl = CAUrl ++ "/acme/key-change/", - % {ok, Account3, Nonce4} = key_roll_over(KeyChangeUrl, AccURL, PrivateKey, NewKey, Nonce3), - % ?INFO_MSG("Changed key: ~p~n", [Account3]), + %% NewKey = generate_key(), + %% KeyChangeUrl = CAUrl ++ "/acme/key-change/", + %% {ok, Account3, Nonce4} = key_roll_over(KeyChangeUrl, AccURL, PrivateKey, NewKey, Nonce3), + %% ?INFO_MSG("Changed key: ~p~n", [Account3]), - % {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, NewKey, Nonce4), - % ?INFO_MSG("New account:~p~n", [Account4]), - % {Account4, PrivateKey}. + %% {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, NewKey, Nonce4), + %% ?INFO_MSG("New account:~p~n", [Account4]), + %% {Account4, PrivateKey}. - AccIdBin = list_to_bitstring(integer_to_list(AccId)), - #{"new-authz" := NewAuthz} = Dirs, - DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, - Req2 = - [ { <<"identifier">>, { - [ {<<"type">>, <<"dns">>} - , {<<"value">>, DomainName} - ] }} - , {<<"existing">>, <<"accept">>} - ], - {ok, {AuthzUrl, Authz}, Nonce4} = new_authz(NewAuthz, PrivateKey, Req2, Nonce3), + AccIdBin = list_to_bitstring(integer_to_list(AccId)), + #{"new-authz" := NewAuthz} = Dirs, + DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, + Req2 = + [{<<"identifier">>, + {[{<<"type">>, <<"dns">>}, + {<<"value">>, DomainName}]}}, + {<<"existing">>, <<"accept">>} + ], + {ok, {AuthzUrl, Authz}, Nonce4} = new_authz(NewAuthz, PrivateKey, Req2, Nonce3), - {ok, Authz2, Nonce5} = get_authz(AuthzUrl), + {ok, Authz2, Nonce5} = get_authz(AuthzUrl), - Challenges = get_challenges(Authz2), - % ?INFO_MSG("Challenges: ~p~n", [Challenges]), + 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", []), + {ok, ChallengeUrl, KeyAuthz} = + acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), + ?INFO_MSG("File for http-01 challenge written correctly", []), - Req3 = - [ {<<"type">>, <<"http-01">>} - , {<<"keyAuthorization">>, KeyAuthz} - ], - {ok, SolvedChallenge, Nonce6} = complete_challenge(ChallengeUrl, PrivateKey, Req3, Nonce5), - % ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), + Req3 = + [ {<<"type">>, <<"http-01">>} + , {<<"keyAuthorization">>, KeyAuthz} + ], + {ok, SolvedChallenge, Nonce6} = complete_challenge(ChallengeUrl, PrivateKey, Req3, Nonce5), + %% ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), - % timer:sleep(2000), - {ok, Authz3, Nonce7} = get_authz_until_valid(AuthzUrl), + %% timer:sleep(2000), + {ok, Authz3, Nonce7} = get_authz_until_valid(AuthzUrl), - #{"new-cert" := NewCert} = Dirs, - CSRSubject = [ {commonName, bitstring_to_list(DomainName)} - , {organizationName, "Example Corp"}], - CSR = make_csr(CSRSubject), - {MegS, Sec, MicS} = erlang:timestamp(), - NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), - NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), - Req4 = - [ {<<"csr">>, CSR} - , {<<"notBefore">>, NotBefore} - , {<<"NotAfter">>, NotAfter} - ], - {ok, Certificate, Nonce8} = new_cert(NewCert, PrivateKey, Req4, Nonce7), + #{"new-cert" := NewCert} = Dirs, + CSRSubject = [{commonName, bitstring_to_list(DomainName)}, + {organizationName, "Example Corp"}], + CSR = make_csr(CSRSubject), + {MegS, Sec, MicS} = erlang:timestamp(), + NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), + NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), + Req4 = + [{<<"csr">>, CSR}, + {<<"notBefore">>, NotBefore}, + {<<"NotAfter">>, NotAfter} + ], + {ok, Certificate, Nonce8} = new_cert(NewCert, PrivateKey, Req4, Nonce7), - {Account2, Authz2, Authz3, CSR, Certificate, PrivateKey}. + {Account2, Authz2, Authz3, CSR, Certificate, PrivateKey}. generate_key() -> - jose_jwk:generate_key({ec, secp256r1}). + jose_jwk:generate_key({ec, secp256r1}). scenario3() -> - CSRSubject = [ {commonName, "my-acme-test-ejabberd.com"} - , {organizationName, "Example Corp"}], - CSR = make_csr(CSRSubject). + CSRSubject = [{commonName, "my-acme-test-ejabberd.com"}, + {organizationName, "Example Corp"}], + CSR = make_csr(CSRSubject). %% Just a test scenario0(KeyFile, HttpDir) -> - PrivateKey = jose_jwk:from_file(KeyFile), - % scenario("http://localhost:4000", "2", PrivateKey). - new_user_scenario("http://localhost:4000", HttpDir). - % scenario3(). + PrivateKey = jose_jwk:from_file(KeyFile), + %% scenario("http://localhost:4000", "2", PrivateKey). + new_user_scenario("http://localhost:4000", HttpDir). +%% scenario3(). From 637d9b054b352de231f6616d5b2b639d05e6554f Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 26 Jun 2017 19:03:21 +0300 Subject: [PATCH 20/75] Support get-cert, revoke-cert. Also cleaned some typespecs --- src/acme_challenge.erl | 12 ++-- src/ejabberd_acme.erl | 136 ++++++++++++++++++++++++++++++----------- 2 files changed, 110 insertions(+), 38 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index b27fc1ee7..433de8143 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -16,7 +16,7 @@ -include("ejabberd_acme.hrl"). --spec key_authorization(string(), jose_jwk:key()) -> bitstring(). +-spec key_authorization(bitstring(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> Thumbprint = jose_jwk:thumbprint(Key), %% ?INFO_MSG("Thumbprint: ~p~n", [Thumbprint]), @@ -31,7 +31,7 @@ parse_challenge(Challenge0) -> {<<"status">>,Status} = proplists:lookup(<<"status">>, Challenge), {<<"uri">>,Uri} = proplists:lookup(<<"uri">>, Challenge), {<<"token">>,Token} = proplists:lookup(<<"token">>, Challenge), - Res = + Res = #challenge{ type = Type, status = list_to_atom(bitstring_to_list(Status)), @@ -46,7 +46,8 @@ parse_challenge(Challenge0) -> --spec solve_challenge(bitstring(), [{proplist()}], _) -> {ok, url(), bitstring()} | {error, _}. +-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 @@ -63,7 +64,8 @@ solve_challenge(ChallengeType, Challenges, Options) -> end end. --spec solve_challenge1(acme_challenge(), _) -> {ok, url(), bitstring()} | {error, _}. +-spec solve_challenge1(acme_challenge(), {jose_jwk:key(), string()}) -> + {ok, url(), bitstring()} | {error, _}. solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, {Key, HttpDir}) -> KeyAuthz = key_authorization(Tkn, Key), FileLocation = HttpDir ++ "/.well-known/acme-challenge/" ++ bitstring_to_list(Tkn), @@ -85,5 +87,7 @@ is_challenge_type(DesiredType, #challenge{type = Type}) when DesiredType =:= Typ is_challenge_type(_DesiredType, #challenge{type = _Type}) -> false. +-spec is_error({'error', _}) -> 'true'; + ({'ok', _}) -> 'false'. is_error({error, _}) -> true; is_error(_) -> false. diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 927cccf33..7820048d3 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,17 +1,25 @@ -module (ejabberd_acme). -export([directory/1, + %% Account get_account/3, new_account/4, update_account/4, delete_account/3, + %% Authorization new_authz/4, get_authz/1, complete_challenge/4, + %% Certificate new_cert/4, + get_cert/1, + revoke_cert/4, + %% Debugging Scenarios scenario/3, scenario0/2 - %% , key_roll_over/5 + %% Not yet implemented + %% key_roll_over/5 + %% delete_authz/3 ]). -include("ejabberd.hrl"). @@ -23,7 +31,7 @@ -define(REQUEST_TIMEOUT, 5000). % 5 seconds. -define(MAX_POLL_REQUESTS, 20). --define(POLL_WAIT_TIME, 500). +-define(POLL_WAIT_TIME, 500). % 500 ms. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -45,21 +53,18 @@ directory(Url) -> -spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. new_account(Url, PrivateKey, Req, Nonce) -> - %% Make the request body EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). -spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. update_account(Url, PrivateKey, Req, Nonce) -> - %% Make the request body EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). -spec get_account(url(), jose_jwk:key(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. get_account(Url, PrivateKey, Nonce) -> - %% Make the request body EJson = {[{<<"resource">>, <<"reg">>}]}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). @@ -79,7 +84,7 @@ delete_account(Url, PrivateKey, Nonce) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_authz(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, {url(), proplist()}, nonce()} | {error, _}. new_authz(Url, PrivateKey, Req, Nonce) -> EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). @@ -102,10 +107,22 @@ complete_challenge(Url, PrivateKey, Req, Nonce) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec new_cert(url(), jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. + {ok, {url(), list()}, nonce()} | {error, _}. new_cert(Url, PrivateKey, Req, Nonce) -> EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, "application/pkix-cert"). + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1, + "application/pkix-cert"). + +-spec get_cert(url()) -> {ok, list(), nonce()} | {error, _}. +get_cert(Url) -> + prepare_get_request(Url, fun get_response/1, "application/pkix-cert"). + +-spec revoke_cert(url(), jose_jwk:key(), proplist(), nonce()) -> + {ok, _, nonce()} | {error, _}. +revoke_cert(Url, PrivateKey, Req, Nonce) -> + EJson = {[{<<"resource">>, <<"revoke-cert">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, + "application/pkix-cert"). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -153,40 +170,28 @@ get_response_location({ok, Head, Return}) -> %% 2. Derive the whole algo objects from Key %% TODO: Encode Strings using length. --spec make_csr(proplist()) -> binary(). +-spec make_csr(proplist()) -> {binary(), jose_jwk:key()}. make_csr(Attributes) -> Key = generate_key(), - {_, KeyKey} = jose_jwk:to_key(Key), - KeyPub = jose_jwk:to_public(Key), - try SubPKInfoAlgo = subject_pk_info_algo(KeyPub), - {ok, RawBinPubKey} = raw_binary_public_key(KeyPub), SubPKInfo = subject_pk_info(SubPKInfoAlgo, RawBinPubKey), - {ok, Subject} = attributes_from_list(Attributes), - CRI = certificate_request_info(SubPKInfo, Subject), {ok, EncodedCRI} = der_encode( 'CertificationRequestInfo', CRI), - SignedCRI = public_key:sign(EncodedCRI, 'sha256', KeyKey), - SignatureAlgo = signature_algo(Key, 'sha256'), - CSR = certification_request(CRI, SignatureAlgo, SignedCRI), - {ok, DerCSR} = der_encode( 'CertificationRequest', CSR), - Result = base64url:encode(DerCSR), - - Result + {Result, Key} catch _:{badmatch, {error, bad_public_key}} -> {error, bad_public_key}; @@ -260,7 +265,7 @@ length_bitstring(Bitstring) -> false -> LenOctets = binary:encode_unsigned(Size), FirstOctet = byte_size(LenOctets), - <<12:8, 1:1, FirstOctet:7, LenOctets:(FirstOctet * 8), Bitstring/binary>> + <<12:8, 1:1, FirstOctet:7, Size:(FirstOctet * 8), Bitstring/binary>> end. @@ -289,6 +294,7 @@ attribute_parser_fun({AttrName, AttrVal}) -> {error, bad_attributes} end. +-spec attribute_oid(atom()) -> tuple(). attribute_oid(commonName) -> ?'id-at-commonName'; attribute_oid(countryName) -> ?'id-at-countryName'; attribute_oid(stateOrProvinceName) -> ?'id-at-stateOrProvinceName'; @@ -412,7 +418,7 @@ prepare_get_request(Url, HandleRespFun, ResponseType) -> %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec sign_json_jose(jose_jwk:key(), string(), nonce()) -> jws(). +-spec sign_json_jose(jose_jwk:key(), bitstring(), nonce()) -> {_, jws()}. sign_json_jose(Key, Json, Nonce) -> PubKey = jose_jwk:to_public(Key), {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), @@ -428,7 +434,7 @@ sign_json_jose(Key, Json, Nonce) -> JwsObj = jose_jws:from(JwsObj0), jose_jws:sign(Key, Json, JwsObj). --spec sign_encode_json_jose(jose_jwk:key(), string(), nonce()) -> bitstring(). +-spec sign_encode_json_jose(jose_jwk:key(), bitstring(), nonce()) -> bitstring(). sign_encode_json_jose(Key, Json, Nonce) -> {_, Signed} = sign_json_jose(Key, Json, Nonce), %% This depends on jose library, so we can consider it safe @@ -571,11 +577,6 @@ new_user_scenario(CAUrl, HttpDir) -> Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), - %% Delete account - %% {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), - %% {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), - %% ?INFO_MSG("New account: ~p~n", [Account4]), - %% NewKey = generate_key(), %% KeyChangeUrl = CAUrl ++ "/acme/key-change/", %% {ok, Account3, Nonce4} = key_roll_over(KeyChangeUrl, AccURL, PrivateKey, NewKey, Nonce3), @@ -618,7 +619,7 @@ new_user_scenario(CAUrl, HttpDir) -> #{"new-cert" := NewCert} = Dirs, CSRSubject = [{commonName, bitstring_to_list(DomainName)}, {organizationName, "Example Corp"}], - CSR = make_csr(CSRSubject), + {CSR, CSRKey} = make_csr(CSRSubject), {MegS, Sec, MicS} = erlang:timestamp(), NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), @@ -627,9 +628,34 @@ new_user_scenario(CAUrl, HttpDir) -> {<<"notBefore">>, NotBefore}, {<<"NotAfter">>, NotAfter} ], - {ok, Certificate, Nonce8} = new_cert(NewCert, PrivateKey, Req4, Nonce7), + {ok, {CertUrl, Certificate}, Nonce8} = new_cert(NewCert, PrivateKey, Req4, Nonce7), - {Account2, Authz2, Authz3, CSR, Certificate, PrivateKey}. + + {ok, Certificate2, Nonce9} = get_cert(CertUrl), + + DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate2), plain), + %% ?INFO_MSG("DecodedCert: ~p~n", [DecodedCert]), + PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert), + %% ?INFO_MSG("PemEntryCert: ~p~n", [PemEntryCert]), + + {_, CSRKeyKey} = jose_jwk:to_key(CSRKey), + PemEntryKey = public_key:pem_entry_encode('ECPrivateKey', CSRKeyKey), + %% ?INFO_MSG("PemKey: ~p~n", [jose_jwk:to_pem(CSRKey)]), + %% ?INFO_MSG("PemEntryKey: ~p~n", [PemEntryKey]), + + PemCert = public_key:pem_encode([PemEntryKey, PemEntryCert]), + %% ?INFO_MSG("PemCert: ~p~n", [PemCert]), + + ok = file:write_file(HttpDir ++ "/my_server.pem", PemCert), + + Base64Cert = base64url:encode(Certificate2), + #{"revoke-cert" := RevokeCert} = Dirs, + Req5 = [{<<"certificate">>, Base64Cert}], + {ok, [], Nonce10} = revoke_cert(RevokeCert, PrivateKey, Req5, Nonce9), + + {ok, Certificate3, Nonce11} = get_cert(CertUrl), + + {Account2, Authz3, CSR, Certificate, PrivateKey}. generate_key() -> @@ -638,14 +664,56 @@ generate_key() -> scenario3() -> CSRSubject = [{commonName, "my-acme-test-ejabberd.com"}, {organizationName, "Example Corp"}], - CSR = make_csr(CSRSubject). + {CSR, CSRKey} = make_csr(CSRSubject). +%% It doesn't seem to work, The user can get a new authorization even though the account has been deleted +delete_account_scenario(CAUrl) -> + PrivateKey = generate_key(), + + DirURL = CAUrl ++ "/directory", + {ok, Dirs, Nonce0} = directory(DirURL), + %% ?INFO_MSG("Directories: ~p", [Dirs]), + + #{"new-reg" := NewAccURL} = Dirs, + Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], + {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), + + {_, AccId} = proplists:lookup(<<"id">>, Account), + AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), + {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), + %% ?INFO_MSG("Old account: ~p~n", [Account1]), + + Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], + {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), + + %% Delete account + {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), + + timer:sleep(3000), + + {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), + ?INFO_MSG("New account: ~p~n", [Account4]), + + AccIdBin = list_to_bitstring(integer_to_list(AccId)), + #{"new-authz" := NewAuthz} = Dirs, + DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, + Req2 = + [{<<"identifier">>, + {[{<<"type">>, <<"dns">>}, + {<<"value">>, DomainName}]}}, + {<<"existing">>, <<"accept">>} + ], + {ok, {AuthzUrl, Authz}, Nonce6} = new_authz(NewAuthz, PrivateKey, Req2, Nonce5), + + {ok, Account1, Account3, Authz}. %% Just a test scenario0(KeyFile, HttpDir) -> PrivateKey = jose_jwk:from_file(KeyFile), %% scenario("http://localhost:4000", "2", PrivateKey). + %% delete_account_scenario("http://localhost:4000"). new_user_scenario("http://localhost:4000", HttpDir). + %% scenario3(). From d3c477646fdcd760ddb92751233371cfbf826a4c Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 3 Jul 2017 13:37:32 +0300 Subject: [PATCH 21/75] Add support for command get_certificates, very crude --- ejabberd.yml.example | 10 + include/ejabberd_acme.hrl | 22 ++- src/ejabberd_acme.erl | 388 +++++++++++++++++++++++++++++++------- src/ejabberd_admin.erl | 122 +++++++----- 4 files changed, 415 insertions(+), 127 deletions(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index c55830563..aeffc5a35 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -659,6 +659,16 @@ language: "en" ## ## captcha_limit: 5 +###. ==== +###' ACME + +## +## Must contain a contact and a directory that the Http Challenges can be solved at +## +acme: + contact: "mailto:cert-admin-ejabberd@example.com" + http_dir: "/home/konstantinos/Desktop/Programming/test-server-for-acme/" + ###. ======= ###' MODULES diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index ae44e101e..ff35c99e5 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -1,14 +1,26 @@ -record(challenge, { - type = <<"http-01">> :: bitstring(), - status = pending :: pending | valid | invalid, - uri = <<"">> :: bitstring(), - token = <<"">> :: bitstring() - }). + type = <<"http-01">> :: bitstring(), + status = pending :: pending | valid | invalid, + uri = "" :: url(), + token = <<"">> :: bitstring() + }). + +-record(data_acc, { + id :: list(), + key :: jose_jwk:key() + }). + +-record(data, { + account = none :: #data_acc{} | 'none' + }). + + -type nonce() :: string(). -type url() :: string(). -type proplist() :: [{_, _}]. +-type dirs() :: #{string() => url()}. -type jws() :: map(). -type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 7820048d3..aa7c0ac37 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,10 +1,11 @@ -module (ejabberd_acme). --export([directory/1, +-export([%% Directory + directory/1, %% Account - get_account/3, new_account/4, update_account/4, + get_account/3, delete_account/3, %% Authorization new_authz/4, @@ -14,9 +15,14 @@ new_cert/4, get_cert/1, revoke_cert/4, - %% Debugging Scenarios + %% Ejabberdctl Commands + get_certificates/3, + %% Command Options Validity + is_valid_account_opt/1, + %% Debugging Scenarios scenario/3, - scenario0/2 + scenario0/2, + new_user_scenario/2 %% Not yet implemented %% key_roll_over/5 %% delete_authz/3 @@ -40,8 +46,9 @@ %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec directory(url()) -> {ok, map(), nonce()} | {error, _}. -directory(Url) -> +-spec directory(url()) -> {ok, dirs(), nonce()} | {error, _}. +directory(CAUrl) -> + Url = CAUrl ++ "/directory", prepare_get_request(Url, fun get_dirs/1). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -50,27 +57,31 @@ directory(Url) -> %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec new_account(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec new_account(dirs(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. -new_account(Url, PrivateKey, Req, Nonce) -> +new_account(Dirs, PrivateKey, Req, Nonce) -> + #{"new-reg" := Url} = Dirs, EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). --spec update_account(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec update_account({url(), string()}, jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. -update_account(Url, PrivateKey, Req, Nonce) -> +update_account({CAUrl, AccId}, PrivateKey, Req, Nonce) -> + Url = CAUrl ++ "/acme/reg/" ++ AccId, EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). --spec get_account(url(), jose_jwk:key(), nonce()) -> +-spec get_account({url(), string()}, jose_jwk:key(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. -get_account(Url, PrivateKey, Nonce) -> +get_account({CAUrl, AccId}, PrivateKey, Nonce) -> + Url = CAUrl ++ "/acme/reg/" ++ AccId, EJson = {[{<<"resource">>, <<"reg">>}]}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). --spec delete_account(url(), jose_jwk:key(), nonce()) -> +-spec delete_account({url(), string()}, jose_jwk:key(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. -delete_account(Url, PrivateKey, Nonce) -> +delete_account({CAUrl, AccId}, PrivateKey, Nonce) -> + Url = CAUrl ++ "/acme/reg/" ++ AccId, EJson = {[{<<"resource">>, <<"reg">>}, {<<"status">>, <<"deactivated">>}]}, @@ -83,19 +94,22 @@ delete_account(Url, PrivateKey, Nonce) -> %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec new_authz(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec new_authz(dirs(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), proplist()}, nonce()} | {error, _}. -new_authz(Url, PrivateKey, Req, Nonce) -> +new_authz(Dirs, PrivateKey, Req, Nonce) -> + #{"new-authz" := Url} = Dirs, EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). --spec get_authz(url()) -> {ok, proplist(), nonce()} | {error, _}. -get_authz(Url) -> +-spec get_authz({url(), string()}) -> {ok, proplist(), nonce()} | {error, _}. +get_authz({CAUrl, AuthzId}) -> + Url = CAUrl ++ "/acme/authz/" ++ AuthzId, prepare_get_request(Url, fun get_response/1). --spec complete_challenge(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec complete_challenge({url(), string(), string()}, jose_jwk:key(), proplist(), nonce()) -> {ok, proplist(), nonce()} | {error, _}. -complete_challenge(Url, PrivateKey, Req, Nonce) -> +complete_challenge({CAUrl, AuthzId, ChallId}, PrivateKey, Req, Nonce) -> + Url = CAUrl ++ "/acme/challenge/" ++ AuthzId ++ "/" ++ ChallId, EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). @@ -106,20 +120,23 @@ complete_challenge(Url, PrivateKey, Req, Nonce) -> %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec new_cert(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec new_cert(dirs(), jose_jwk:key(), proplist(), nonce()) -> {ok, {url(), list()}, nonce()} | {error, _}. -new_cert(Url, PrivateKey, Req, Nonce) -> +new_cert(Dirs, PrivateKey, Req, Nonce) -> + #{"new-cert" := Url} = Dirs, EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1, "application/pkix-cert"). --spec get_cert(url()) -> {ok, list(), nonce()} | {error, _}. -get_cert(Url) -> +-spec get_cert({url(), string()}) -> {ok, list(), nonce()} | {error, _}. +get_cert({CAUrl, CertId}) -> + Url = CAUrl ++ "/acme/cert/" ++ CertId, prepare_get_request(Url, fun get_response/1, "application/pkix-cert"). --spec revoke_cert(url(), jose_jwk:key(), proplist(), nonce()) -> +-spec revoke_cert(dirs(), jose_jwk:key(), proplist(), nonce()) -> {ok, _, nonce()} | {error, _}. -revoke_cert(Url, PrivateKey, Req, Nonce) -> +revoke_cert(Dirs, PrivateKey, Req, Nonce) -> + #{"revoke-cert" := Url} = Dirs, EJson = {[{<<"resource">>, <<"revoke-cert">>}] ++ Req}, prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, "application/pkix-cert"). @@ -294,7 +311,7 @@ attribute_parser_fun({AttrName, AttrVal}) -> {error, bad_attributes} end. --spec attribute_oid(atom()) -> tuple(). +-spec attribute_oid(atom()) -> tuple() | no_return(). attribute_oid(commonName) -> ?'id-at-commonName'; attribute_oid(countryName) -> ?'id-at-countryName'; attribute_oid(stateOrProvinceName) -> ?'id-at-stateOrProvinceName'; @@ -309,24 +326,24 @@ attribute_oid(_) -> error(bad_attributes). %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec get_authz_until_valid(url()) -> {ok, proplist(), nonce()} | {error, _}. -get_authz_until_valid(Url) -> - get_authz_until_valid(Url, ?MAX_POLL_REQUESTS). +-spec get_authz_until_valid({url(), string()}) -> {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid({CAUrl, AuthzId}) -> + get_authz_until_valid({CAUrl, AuthzId}, ?MAX_POLL_REQUESTS). --spec get_authz_until_valid(url(), non_neg_integer()) -> +-spec get_authz_until_valid({url(), string()}, non_neg_integer()) -> {ok, proplist(), nonce()} | {error, _}. -get_authz_until_valid(Url, 0) -> +get_authz_until_valid({_CAUrl, _AuthzId}, 0) -> ?ERROR_MSG("Maximum request limit waiting for validation reached", []), {error, max_request_limit}; -get_authz_until_valid(Url, N) -> - case get_authz(Url) of +get_authz_until_valid({CAUrl, AuthzId}, N) -> + case get_authz({CAUrl, AuthzId}) of {ok, Resp, Nonce} -> case is_authz_valid(Resp) of true -> {ok, Resp, Nonce}; false -> timer:sleep(?POLL_WAIT_TIME), - get_authz_until_valid(Url, N-1) + get_authz_until_valid({CAUrl, AuthzId}, N-1) end; {error, _} = Err -> Err @@ -461,6 +478,17 @@ get_location(Head) -> none -> none end. +-spec location_to_id(url()) -> {ok, string()} | {error, not_found}. +location_to_id(Url0) -> + Url = string:strip(Url0, right, $/), + case string:rchr(Url, $/) of + 0 -> + ?ERROR_MSG("Couldn't find id in url: ~p~n", [Url]), + {error, not_found}; + Ind -> + {ok, string:sub_string(Url, Ind+1)} + end. + %% Very bad way to extract this %% TODO: Find a better way -spec get_tos(proplist()) -> url() | 'none'. @@ -529,7 +557,228 @@ failed_http_request({error, Reason}, Url) -> [Url, Reason]), {error, Reason}. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle Config and Persistence Files +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +persistent_file() -> + MnesiaDir = mnesia:system_info(directory), + filename:join(MnesiaDir, "acme.DAT"). + +read_persistent() -> + case file:read_file(persistent_file()) of + {ok, Binary} -> + {ok, binary_to_term(Binary)}; + {error, enoent} -> + {ok, #data{}}; + {error, Reason} -> + ?ERROR_MSG("Error: ~p reading acme data file", [Reason]), + {error, Reason} + end. + +write_persistent(Data) -> + Binary = term_to_binary(Data), + case file:write_file(persistent_file(), Binary) of + ok -> ok; + {error, Reason} -> + ?ERROR_MSG("Error: ~p writing acme data file", [Reason]), + {error, Reason} + end. + +get_account_persistent(#data{account = Account}) -> + case Account of + #data_acc{id = AccId, key = PrivateKey} -> + {ok, AccId, PrivateKey}; + none -> + none + end. + +set_account_persistent(Data = #data{}, {AccId, PrivateKey}) -> + NewAcc = #data_acc{id = AccId, key = PrivateKey}, + Data#data{account = NewAcc}. + +get_config_contact() -> + case ejabberd_config:get_option(acme, undefined) of + undefined -> + ?ERROR_MSG("No acme configuration has been specified", []), + {error, configuration}; + Acme -> + case lists:keyfind(contact, 1, Acme) of + {contact, Contact} -> + {ok, Contact}; + false -> + ?ERROR_MSG("No contact has been specified", []), + {error, configuration_contact} + end + end. + +get_config_hosts() -> + case ejabberd_config:get_option(hosts, undefined) of + undefined -> + ?ERROR_MSG("No hosts have been specified", []), + {error, configuration_hosts}; + Hosts -> + {ok, Hosts} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Command Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% +%% Check Validity of command options +%% + +is_valid_account_opt("old-account") -> true; +is_valid_account_opt("new-account") -> true; +is_valid_account_opt(_) -> false. + +%% +%% Get Certificate +%% + +%% Needs a hell lot of cleaning +get_certificates(CAUrl, HttpDir, NewAccountOpt) -> + try + get_certificates0(CAUrl, HttpDir, NewAccountOpt) + catch + E:R -> + {E,R} + end. + +get_certificates0(CAUrl, HttpDir, "old-account") -> + %% Read Persistent Data + {ok, Data} = read_persistent(), + + %% Get the current account + case get_account_persistent(Data) of + none -> + ?ERROR_MSG("No existing account", []), + {error, no_old_account}; + {ok, _AccId, PrivateKey} -> + get_certificates1(CAUrl, HttpDir, PrivateKey) + end; +get_certificates0(CAUrl, HttpDir, "new-account") -> + %% Get contact from configuration file + {ok, Contact} = get_config_contact(), + + %% Generate a Key + PrivateKey = generate_key(), + + %% Create a new account + {ok, _Id} = create_new_account(CAUrl, Contact, Key), + + %% Write Persistent Data + {ok, Data} = read_persistent(), + NewData = set_account_persistent(Data, {Id, Key}), + ok = write_persistent(NewData), + + get_certificates1(CAUrl, HttpDir, PrivateKey). + + +get_certificates1(CAUrl, HttpDir, PrivateKey) -> + %% Read Config + {ok, Hosts} = get_config_hosts(), + + %% Get a certificate for each host + PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey, HttpDir) || Host <- Hosts], + {AccId, PrivateKey, PemCertKeys}. + + +get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> + ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), + case create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) of + {ok, Authz} -> + create_new_certificate(CAUrl, DomainName, PrivateKey); + {error, authorization} -> + {error, {authorization, {host, DomainName}}} + end. + +%% TODO: +%% Find a way to ask the user if he accepts the TOS +create_new_account(CAUrl, Contact, PrivateKey) -> + try + {ok, Dirs, Nonce0} = directory(CAUrl), + Req0 = [{ <<"contact">>, [Contact]}], + {ok, {TOS, Account}, Nonce1} = new_account(Dirs, PrivateKey, Req0, Nonce0), + {<<"id">>, AccIdInt} = lists:keyfind(<<"id">>, 1, Account), + AccId = integer_to_list(AccIdInt), + Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], + {ok, Account2, _Nonce2} = update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce1), + {ok, AccId} + catch + E:R -> + {error,create_new_account} + end. + + +create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> + try + {ok, Dirs, Nonce0} = directory(CAUrl), + Req0 = [{<<"identifier">>, + {[{<<"type">>, <<"dns">>}, + {<<"value">>, DomainName}]}}, + {<<"existing">>, <<"accept">>}], + {ok, {AuthzUrl, Authz}, Nonce1} = new_authz(Dirs, PrivateKey, Req0, Nonce0), + {ok, AuthzId} = location_to_id(AuthzUrl), + + Challenges = get_challenges(Authz), + {ok, ChallengeUrl, KeyAuthz} = + acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), + {ok, ChallengeId} = location_to_id(ChallengeUrl), + Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], + {ok, SolvedChallenge, Nonce2} = + complete_challenge({CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), + + {ok, AuthzValid, _Nonce} = get_authz_until_valid({CAUrl, AuthzId}), + {ok, AuthzValid} + catch + E:R -> + ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", + [{E,R}, DomainName]), + {error, authorization} + end. + +create_new_certificate(CAUrl, DomainName, PrivateKey) -> + try + {ok, Dirs, Nonce0} = directory(CAUrl), + CSRSubject = [{commonName, bitstring_to_list(DomainName)}], + {CSR, CSRKey} = make_csr(CSRSubject), + {NotBefore, NotAfter} = not_before_not_after(), + Req = + [{<<"csr">>, CSR}, + {<<"notBefore">>, NotBefore}, + {<<"NotAfter">>, NotAfter} + ], + {ok, {CertUrl, Certificate}, Nonce1} = new_cert(Dirs, PrivateKey, Req, Nonce0), + + {ok, CertId} = location_to_id(CertUrl), + + DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate), plain), + PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert), + + {_, CSRKeyKey} = jose_jwk:to_key(CSRKey), + PemEntryKey = public_key:pem_entry_encode('ECPrivateKey', CSRKeyKey), + + PemCertKey = public_key:pem_encode([PemEntryKey, PemEntryCert]), + + {ok, PemCertKey} + catch + E:R -> + ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", + [{E,R}, DomainName]), + {error, certificate} + end. +not_before_not_after() -> + %% TODO: Make notBefore and notAfter like they do it in other clients + {MegS, Sec, MicS} = erlang:timestamp(), + NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), + NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), + {NotBefore, NotAfter}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -539,21 +788,18 @@ failed_http_request({error, Reason}, Url) -> %% A typical acme workflow scenario(CAUrl, AccId, PrivateKey) -> - DirURL = CAUrl ++ "/directory", - {ok, Dirs, Nonce0} = directory(DirURL), + {ok, Dirs, Nonce0} = directory(CAUrl), - AccURL = CAUrl ++ "/acme/reg/" ++ AccId, - {ok, {_TOS, Account}, Nonce1} = get_account(AccURL, PrivateKey, Nonce0), + {ok, {_TOS, Account}, Nonce1} = get_account({CAUrl, AccId}, PrivateKey, Nonce0), ?INFO_MSG("Account: ~p~n", [Account]), - #{"new-authz" := NewAuthz} = Dirs, Req = [{<<"identifier">>, {[{<<"type">>, <<"dns">>}, {<<"value">>, <<"my-acme-test-ejabberd.com">>}]}}, {<<"existing">>, <<"accept">>} ], - {ok, Authz, Nonce2} = new_authz(NewAuthz, PrivateKey, Req, Nonce1), + {ok, Authz, Nonce2} = new_authz(Dirs, PrivateKey, Req, Nonce1), {Account, Authz, PrivateKey}. @@ -561,21 +807,19 @@ scenario(CAUrl, AccId, PrivateKey) -> new_user_scenario(CAUrl, HttpDir) -> PrivateKey = generate_key(), - DirURL = CAUrl ++ "/directory", - {ok, Dirs, Nonce0} = directory(DirURL), + {ok, Dirs, Nonce0} = directory(CAUrl), %% ?INFO_MSG("Directories: ~p", [Dirs]), - #{"new-reg" := NewAccURL} = Dirs, Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), + {ok, {TOS, Account}, Nonce1} = new_account(Dirs, PrivateKey, Req0, Nonce0), - {_, AccId} = proplists:lookup(<<"id">>, Account), - AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), - {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), + {_, AccIdInt} = proplists:lookup(<<"id">>, Account), + AccId = integer_to_list(AccIdInt), + {ok, {_TOS, Account1}, Nonce2} = get_account({CAUrl, AccId}, PrivateKey, Nonce1), %% ?INFO_MSG("Old account: ~p~n", [Account1]), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), + {ok, Account2, Nonce3} = update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), %% NewKey = generate_key(), %% KeyChangeUrl = CAUrl ++ "/acme/key-change/", @@ -586,8 +830,7 @@ new_user_scenario(CAUrl, HttpDir) -> %% ?INFO_MSG("New account:~p~n", [Account4]), %% {Account4, PrivateKey}. - AccIdBin = list_to_bitstring(integer_to_list(AccId)), - #{"new-authz" := NewAuthz} = Dirs, + AccIdBin = list_to_bitstring(integer_to_list(AccIdInt)), DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, Req2 = [{<<"identifier">>, @@ -595,26 +838,29 @@ new_user_scenario(CAUrl, HttpDir) -> {<<"value">>, DomainName}]}}, {<<"existing">>, <<"accept">>} ], - {ok, {AuthzUrl, Authz}, Nonce4} = new_authz(NewAuthz, PrivateKey, Req2, Nonce3), + {ok, {AuthzUrl, Authz}, Nonce4} = new_authz(Dirs, PrivateKey, Req2, Nonce3), - {ok, Authz2, Nonce5} = get_authz(AuthzUrl), + {ok, AuthzId} = location_to_id(AuthzUrl), + {ok, Authz2, Nonce5} = get_authz({CAUrl, AuthzId}), + ?INFO_MSG("AuthzUrl: ~p~n", [AuthzUrl]), Challenges = get_challenges(Authz2), - %% ?INFO_MSG("Challenges: ~p~n", [Challenges]), + ?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", []), + {ok, ChallengeId} = location_to_id(ChallengeUrl), Req3 = [ {<<"type">>, <<"http-01">>} , {<<"keyAuthorization">>, KeyAuthz} ], - {ok, SolvedChallenge, Nonce6} = complete_challenge(ChallengeUrl, PrivateKey, Req3, Nonce5), + {ok, SolvedChallenge, Nonce6} = complete_challenge({CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce5), %% ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), %% timer:sleep(2000), - {ok, Authz3, Nonce7} = get_authz_until_valid(AuthzUrl), + {ok, Authz3, Nonce7} = get_authz_until_valid({CAUrl, AuthzId}), #{"new-cert" := NewCert} = Dirs, CSRSubject = [{commonName, bitstring_to_list(DomainName)}, @@ -628,10 +874,11 @@ new_user_scenario(CAUrl, HttpDir) -> {<<"notBefore">>, NotBefore}, {<<"NotAfter">>, NotAfter} ], - {ok, {CertUrl, Certificate}, Nonce8} = new_cert(NewCert, PrivateKey, Req4, Nonce7), + {ok, {CertUrl, Certificate}, Nonce8} = new_cert(Dirs, PrivateKey, Req4, Nonce7), + ?INFO_MSG("CertUrl: ~p~n", [CertUrl]), - - {ok, Certificate2, Nonce9} = get_cert(CertUrl), + {ok, CertId} = location_to_id(CertUrl), + {ok, Certificate2, Nonce9} = get_cert({CAUrl, CertId}), DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate2), plain), %% ?INFO_MSG("DecodedCert: ~p~n", [DecodedCert]), @@ -649,9 +896,8 @@ new_user_scenario(CAUrl, HttpDir) -> ok = file:write_file(HttpDir ++ "/my_server.pem", PemCert), Base64Cert = base64url:encode(Certificate2), - #{"revoke-cert" := RevokeCert} = Dirs, Req5 = [{<<"certificate">>, Base64Cert}], - {ok, [], Nonce10} = revoke_cert(RevokeCert, PrivateKey, Req5, Nonce9), + {ok, [], Nonce10} = revoke_cert(Dirs, PrivateKey, Req5, Nonce9), {ok, Certificate3, Nonce11} = get_cert(CertUrl), @@ -675,28 +921,26 @@ delete_account_scenario(CAUrl) -> {ok, Dirs, Nonce0} = directory(DirURL), %% ?INFO_MSG("Directories: ~p", [Dirs]), - #{"new-reg" := NewAccURL} = Dirs, Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = new_account(NewAccURL, PrivateKey, Req0, Nonce0), + {ok, {TOS, Account}, Nonce1} = new_account(Dirs, PrivateKey, Req0, Nonce0), - {_, AccId} = proplists:lookup(<<"id">>, Account), - AccURL = CAUrl ++ "/acme/reg/" ++ integer_to_list(AccId), - {ok, {_TOS, Account1}, Nonce2} = get_account(AccURL, PrivateKey, Nonce1), + {_, AccIdInt} = proplists:lookup(<<"id">>, Account), + AccId = integer_to_list(AccIdInt), + {ok, {_TOS, Account1}, Nonce2} = get_account({CAUrl, AccId}, PrivateKey, Nonce1), %% ?INFO_MSG("Old account: ~p~n", [Account1]), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = update_account(AccURL, PrivateKey, Req1, Nonce2), + {ok, Account2, Nonce3} = update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), %% Delete account - {ok, Account3, Nonce4} = delete_account(AccURL, PrivateKey, Nonce3), + {ok, Account3, Nonce4} = delete_account({CAUrl, AccId}, PrivateKey, Nonce3), timer:sleep(3000), - {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, PrivateKey, Nonce4), + {ok, {_TOS, Account4}, Nonce5} = get_account({CAUrl, AccId}, PrivateKey, Nonce4), ?INFO_MSG("New account: ~p~n", [Account4]), - AccIdBin = list_to_bitstring(integer_to_list(AccId)), - #{"new-authz" := NewAuthz} = Dirs, + AccIdBin = list_to_bitstring(integer_to_list(AccIdInt)), DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, Req2 = [{<<"identifier">>, @@ -704,7 +948,7 @@ delete_account_scenario(CAUrl) -> {<<"value">>, DomainName}]}}, {<<"existing">>, <<"accept">>} ], - {ok, {AuthzUrl, Authz}, Nonce6} = new_authz(NewAuthz, PrivateKey, Req2, Nonce5), + {ok, {AuthzUrl, Authz}, Nonce6} = new_authz(Dirs, PrivateKey, Req2, Nonce5), {ok, Account1, Account3, Authz}. diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 8b4af2857..9b8be03ee 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -44,6 +44,8 @@ registered_users/1, %% Migration jabberd1.4 import_file/1, import_dir/1, + %% Acme + get_certificate/2, %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia @@ -104,7 +106,7 @@ get_commands_spec() -> module = ?MODULE, function = status, result_desc = "Result tuple", result_example = {ok, <<"The node ejabberd@localhost is started with status: started" - "ejabberd X.X is running in that node">>}, + "ejabberd X.X is running in that node">>}, args = [], result = {res, restuple}}, #ejabberd_commands{name = stop, tags = [server], desc = "Stop ejabberd gracefully", @@ -126,9 +128,9 @@ get_commands_spec() -> #ejabberd_commands{name = stop_kindly, tags = [server], desc = "Inform users and rooms, wait, and stop the server", longdesc = "Provide the delay in seconds, and the " - "announcement quoted, for example: \n" - "ejabberdctl stop_kindly 60 " - "\\\"The server will stop in one minute.\\\"", + "announcement quoted, for example: \n" + "ejabberdctl stop_kindly 60 " + "\\\"The server will stop in one minute.\\\"", module = ?MODULE, function = stop_kindly, args_desc = ["Seconds to wait", "Announcement to send, with quotes"], args_example = [60, <<"Server will stop now.">>], @@ -192,7 +194,7 @@ get_commands_spec() -> result_example = [<<"user1">>, <<"user2">>], args = [{host, binary}], result = {users, {list, {username, string}}}}, - #ejabberd_commands{name = registered_vhosts, tags = [server], + #ejabberd_commands{name = registered_vhosts, tags = [server], desc = "List all registered vhosts in SERVER", module = ?MODULE, function = registered_vhosts, result_desc = "List of available vhosts", @@ -215,7 +217,7 @@ get_commands_spec() -> #ejabberd_commands{name = leave_cluster, tags = [cluster], desc = "Remove and shutdown Node from the running cluster", longdesc = "This command can be run from any running node of the cluster, " - "even the node to be removed.", + "even the node to be removed.", module = ?MODULE, function = leave_cluster, args_desc = ["Nodename of the node to kick from the cluster"], args_example = [<<"ejabberd1@machine8">>], @@ -243,6 +245,14 @@ get_commands_spec() -> args = [{file, string}], result = {res, restuple}}, + #ejabberd_commands{name = get_certificate, tags = [acme], + desc = "Gets a certificate for the specified domain", + module = ?MODULE, function = get_certificate, + args_desc = ["Full path to the http serving directory", + "Whether to create a new account or use the existing one"], + args = [{dir, string}, {option, string}], + result = {certificate, string}}, + #ejabberd_commands{name = import_piefxis, tags = [mnesia], desc = "Import users data from a PIEFXIS file (XEP-0227)", module = ejabberd_piefxis, function = import_file, @@ -321,9 +331,9 @@ get_commands_spec() -> desc = "Change the erlang node name in a backup file", module = ?MODULE, function = mnesia_change_nodename, args_desc = ["Name of the old erlang node", "Name of the new node", - "Path to old backup file", "Path to the new backup file"], + "Path to old backup file", "Path to the new backup file"], args_example = ["ejabberd@machine1", "ejabberd@machine2", - "/var/lib/ejabberd/old.backup", "/var/lib/ejabberd/new.backup"], + "/var/lib/ejabberd/old.backup", "/var/lib/ejabberd/new.backup"], args = [{oldnodename, string}, {newnodename, string}, {oldbackup, string}, {newbackup, string}], result = {res, restuple}}, @@ -421,7 +431,7 @@ stop_kindly(DelaySeconds, AnnouncementTextString) -> {"Stopping ejabberd", application, stop, [ejabberd]}, {"Stopping Mnesia", mnesia, stop, []}, {"Stopping Erlang node", init, stop, []} - ], + ], NumberLast = length(Steps), TimestampStart = calendar:datetime_to_gregorian_seconds({date(), time()}), lists:foldl( @@ -469,8 +479,8 @@ update_module(ModuleNameBin) when is_binary(ModuleNameBin) -> update_module(ModuleNameString) -> ModuleName = list_to_atom(ModuleNameString), case ejabberd_update:update([ModuleName]) of - {ok, _Res} -> {ok, []}; - {error, Reason} -> {error, Reason} + {ok, _Res} -> {ok, []}; + {error, Reason} -> {error, Reason} end. %%% @@ -500,7 +510,7 @@ registered_users(Host) -> lists:map(fun({U, _S}) -> U end, SUsers). registered_vhosts() -> - ?MYHOSTS. + ?MYHOSTS. reload_config() -> ejabberd_config:reload_file(). @@ -542,6 +552,18 @@ import_dir(Path) -> {cannot_import_dir, String} end. +%%% +%%% Acme +%%% + +get_certificate(HttpDir, UseNewAccount) -> + case ejabberd_acme:is_valid_account_opt(UseNewAccount) of + true -> + ejabberd_acme:get_certificates("http://localhost:4000", HttpDir, UseNewAccount); + false -> + String = io_lib:format("Invalid account option: ~p", [UseNewAccount]), + {invalid_option, String} + end. %%% %%% Purge DB @@ -726,45 +748,45 @@ mnesia_change_nodename(FromString, ToString, Source, Target) -> Switch = fun (Node) when Node == From -> - io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), - To; - (Node) when Node == To -> + io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), + To; + (Node) when Node == To -> %% throw({error, already_exists}); - io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), - Node; - (Node) -> - io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), - Node - end, - Convert = - fun - ({schema, db_nodes, Nodes}, Acc) -> - io:format(" +++ db_nodes ~p~n", [Nodes]), - {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; - ({schema, version, Version}, Acc) -> - io:format(" +++ version: ~p~n", [Version]), - {[{schema, version, Version}], Acc}; - ({schema, cookie, Cookie}, Acc) -> - io:format(" +++ cookie: ~p~n", [Cookie]), - {[{schema, cookie, Cookie}], Acc}; - ({schema, Tab, CreateList}, Acc) -> - io:format("~n * Checking table: '~p'~n", [Tab]), - Keys = [ram_copies, disc_copies, disc_only_copies], - OptSwitch = - fun({Key, Val}) -> - case lists:member(Key, Keys) of - true -> - io:format(" + Checking key: '~p'~n", [Key]), - {Key, lists:map(Switch, Val)}; - false-> {Key, Val} - end - end, - Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, - Res; - (Other, Acc) -> - {[Other], Acc} - end, - mnesia:traverse_backup(Source, Target, Convert, switched). + io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), + Node; + (Node) -> + io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), + Node + end, +Convert = +fun + ({schema, db_nodes, Nodes}, Acc) -> + io:format(" +++ db_nodes ~p~n", [Nodes]), + {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; + ({schema, version, Version}, Acc) -> + io:format(" +++ version: ~p~n", [Version]), + {[{schema, version, Version}], Acc}; + ({schema, cookie, Cookie}, Acc) -> + io:format(" +++ cookie: ~p~n", [Cookie]), + {[{schema, cookie, Cookie}], Acc}; + ({schema, Tab, CreateList}, Acc) -> + io:format("~n * Checking table: '~p'~n", [Tab]), + Keys = [ram_copies, disc_copies, disc_only_copies], + OptSwitch = + fun({Key, Val}) -> + case lists:member(Key, Keys) of + true -> + io:format(" + Checking key: '~p'~n", [Key]), + {Key, lists:map(Switch, Val)}; + false-> {Key, Val} + end + end, + Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, + Res; + (Other, Acc) -> + {[Other], Acc} +end, +mnesia:traverse_backup(Source, Target, Convert, switched). clear_cache() -> Nodes = ejabberd_cluster:get_nodes(), From 56fc0efbc872891991d4f9ce0a24d43101795a03 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 4 Jul 2017 11:44:22 +0300 Subject: [PATCH 22/75] Split ACME module into two 1. A communications module that handles all requets/responses and other low level stuff that have to do with the ACME CA 2. A head module that will do all the useful stuff --- src/ejabberd_acme.erl | 454 +++++-------------------------------- src/ejabberd_acme_comm.erl | 387 +++++++++++++++++++++++++++++++ 2 files changed, 440 insertions(+), 401 deletions(-) create mode 100644 src/ejabberd_acme_comm.erl diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index aa7c0ac37..e1cee923e 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,24 +1,11 @@ -module (ejabberd_acme). --export([%% Directory - directory/1, - %% Account - new_account/4, - update_account/4, - get_account/3, - delete_account/3, - %% Authorization - new_authz/4, - get_authz/1, - complete_challenge/4, - %% Certificate - new_cert/4, - get_cert/1, - revoke_cert/4, - %% Ejabberdctl Commands +-export([%% Ejabberdctl Commands get_certificates/3, %% Command Options Validity is_valid_account_opt/1, + %% Misc + generate_key/0, %% Debugging Scenarios scenario/3, scenario0/2, @@ -35,144 +22,6 @@ -include("ejabberd_acme.hrl"). -include_lib("public_key/include/public_key.hrl"). --define(REQUEST_TIMEOUT, 5000). % 5 seconds. --define(MAX_POLL_REQUESTS, 20). --define(POLL_WAIT_TIME, 500). % 500 ms. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Directory -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec directory(url()) -> {ok, dirs(), nonce()} | {error, _}. -directory(CAUrl) -> - Url = CAUrl ++ "/directory", - prepare_get_request(Url, fun get_dirs/1). - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Account Handling -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec new_account(dirs(), jose_jwk:key(), proplist(), nonce()) -> - {ok, {url(), proplist()}, nonce()} | {error, _}. -new_account(Dirs, PrivateKey, Req, Nonce) -> - #{"new-reg" := Url} = Dirs, - EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). - --spec update_account({url(), string()}, jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. -update_account({CAUrl, AccId}, PrivateKey, Req, Nonce) -> - Url = CAUrl ++ "/acme/reg/" ++ AccId, - EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). - --spec get_account({url(), string()}, jose_jwk:key(), nonce()) -> - {ok, {url(), proplist()}, nonce()} | {error, _}. -get_account({CAUrl, AccId}, PrivateKey, Nonce) -> - Url = CAUrl ++ "/acme/reg/" ++ AccId, - EJson = {[{<<"resource">>, <<"reg">>}]}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). - --spec delete_account({url(), string()}, jose_jwk:key(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. -delete_account({CAUrl, AccId}, PrivateKey, Nonce) -> - Url = CAUrl ++ "/acme/reg/" ++ AccId, - EJson = - {[{<<"resource">>, <<"reg">>}, - {<<"status">>, <<"deactivated">>}]}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Authorization Handling -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec new_authz(dirs(), jose_jwk:key(), proplist(), nonce()) -> - {ok, {url(), proplist()}, nonce()} | {error, _}. -new_authz(Dirs, PrivateKey, Req, Nonce) -> - #{"new-authz" := Url} = Dirs, - EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). - --spec get_authz({url(), string()}) -> {ok, proplist(), nonce()} | {error, _}. -get_authz({CAUrl, AuthzId}) -> - Url = CAUrl ++ "/acme/authz/" ++ AuthzId, - prepare_get_request(Url, fun get_response/1). - --spec complete_challenge({url(), string(), string()}, jose_jwk:key(), proplist(), nonce()) -> - {ok, proplist(), nonce()} | {error, _}. -complete_challenge({CAUrl, AuthzId, ChallId}, PrivateKey, Req, Nonce) -> - Url = CAUrl ++ "/acme/challenge/" ++ AuthzId ++ "/" ++ ChallId, - EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Certificate Handling -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec new_cert(dirs(), jose_jwk:key(), proplist(), nonce()) -> - {ok, {url(), list()}, nonce()} | {error, _}. -new_cert(Dirs, PrivateKey, Req, Nonce) -> - #{"new-cert" := Url} = Dirs, - EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1, - "application/pkix-cert"). - --spec get_cert({url(), string()}) -> {ok, list(), nonce()} | {error, _}. -get_cert({CAUrl, CertId}) -> - Url = CAUrl ++ "/acme/cert/" ++ CertId, - prepare_get_request(Url, fun get_response/1, "application/pkix-cert"). - --spec revoke_cert(dirs(), jose_jwk:key(), proplist(), nonce()) -> - {ok, _, nonce()} | {error, _}. -revoke_cert(Dirs, PrivateKey, Req, Nonce) -> - #{"revoke-cert" := Url} = Dirs, - EJson = {[{<<"resource">>, <<"revoke-cert">>}] ++ Req}, - prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, - "application/pkix-cert"). - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Handle Response Functions -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec get_dirs({ok, proplist(), proplist()}) -> {ok, map(), nonce()}. -get_dirs({ok, Head, Return}) -> - NewNonce = get_nonce(Head), - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X, Y} <- Return], - NewDirs = maps:from_list(StrDirectories), - {ok, NewDirs, NewNonce}. - --spec get_response({ok, proplist(), proplist()}) -> {ok, proplist(), nonce()}. -get_response({ok, Head, Return}) -> - NewNonce = get_nonce(Head), - {ok, Return, NewNonce}. - --spec get_response_tos({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. -get_response_tos({ok, Head, Return}) -> - TOSUrl = get_tos(Head), - NewNonce = get_nonce(Head), - {ok, {TOSUrl, Return}, NewNonce}. - --spec get_response_location({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. -get_response_location({ok, Head, Return}) -> - Location = get_location(Head), - NewNonce = get_nonce(Head), - {ok, {Location, Return}, NewNonce}. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -320,143 +169,6 @@ attribute_oid(organizationName) -> ?'id-at-organizationName'; attribute_oid(_) -> error(bad_attributes). -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Authorization Polling -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec get_authz_until_valid({url(), string()}) -> {ok, proplist(), nonce()} | {error, _}. -get_authz_until_valid({CAUrl, AuthzId}) -> - get_authz_until_valid({CAUrl, AuthzId}, ?MAX_POLL_REQUESTS). - --spec get_authz_until_valid({url(), string()}, non_neg_integer()) -> - {ok, proplist(), nonce()} | {error, _}. -get_authz_until_valid({_CAUrl, _AuthzId}, 0) -> - ?ERROR_MSG("Maximum request limit waiting for validation reached", []), - {error, max_request_limit}; -get_authz_until_valid({CAUrl, AuthzId}, N) -> - case get_authz({CAUrl, AuthzId}) of - {ok, Resp, Nonce} -> - case is_authz_valid(Resp) of - true -> - {ok, Resp, Nonce}; - false -> - timer:sleep(?POLL_WAIT_TIME), - get_authz_until_valid({CAUrl, AuthzId}, N-1) - end; - {error, _} = Err -> - Err - end. - --spec is_authz_valid(proplist()) -> boolean(). -is_authz_valid(Authz) -> - case proplists:lookup(<<"status">>, Authz) of - {<<"status">>, <<"valid">>} -> - true; - _ -> - false - end. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Request Functions -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% TODO: Fix the duplicated code at the below 4 functions --spec make_post_request(url(), bitstring(), string()) -> - {ok, proplist(), proplist()} | {error, _}. -make_post_request(Url, ReqBody, ResponseType) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(post, - {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - decode_response(Head, Body, ResponseType); - Error -> - failed_http_request(Error, Url) - end. - --spec make_get_request(url(), string()) -> - {ok, proplist(), proplist()} | {error, _}. -make_get_request(Url, ResponseType) -> - Options = [], - HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], - case httpc:request(get, {Url, []}, HttpOptions, Options) of - {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> - decode_response(Head, Body, ResponseType); - Error -> - failed_http_request(Error, Url) - end. - --spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), - nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. -prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> - prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, "application/jose+json"). - --spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), - nonce(), handle_resp_fun(), string()) -> {ok, _, nonce()} | {error, _}. -prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, ResponseType) -> - case encode(EJson) of - {ok, ReqBody} -> - FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), - case make_post_request(Url, FinalBody, ResponseType) of - {ok, Head, Return} -> - HandleRespFun({ok, Head, Return}); - Error -> - Error - end; - {error, Reason} -> - ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), - {error, Reason} - end. - --spec prepare_get_request(url(), handle_resp_fun()) -> - {ok, _, nonce()} | {error, _}. -prepare_get_request(Url, HandleRespFun) -> - prepare_get_request(Url, HandleRespFun, "application/jose+json"). - --spec prepare_get_request(url(), handle_resp_fun(), string()) -> - {ok, _, nonce()} | {error, _}. -prepare_get_request(Url, HandleRespFun, ResponseType) -> - case make_get_request(Url, ResponseType) of - {ok, Head, Return} -> - HandleRespFun({ok, Head, Return}); - Error -> - Error - end. - - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Jose Json Functions -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec sign_json_jose(jose_jwk:key(), bitstring(), nonce()) -> {_, jws()}. -sign_json_jose(Key, Json, Nonce) -> - PubKey = jose_jwk:to_public(Key), - {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), - PubKeyJson = jiffy:decode(BinaryPubKey), - %% TODO: Ensure this works for all cases - AlgMap = jose_jwk:signer(Key), - JwsMap = - #{ <<"jwk">> => PubKeyJson, - %% <<"b64">> => true, - <<"nonce">> => list_to_bitstring(Nonce) - }, - JwsObj0 = maps:merge(JwsMap, AlgMap), - JwsObj = jose_jws:from(JwsObj0), - jose_jws:sign(Key, Json, JwsObj). - --spec sign_encode_json_jose(jose_jwk:key(), bitstring(), nonce()) -> bitstring(). -sign_encode_json_jose(Key, Json, Nonce) -> - {_, Signed} = sign_json_jose(Key, Json, Nonce), - %% This depends on jose library, so we can consider it safe - jiffy:encode(Signed). - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -464,20 +176,6 @@ sign_encode_json_jose(Key, Json, Nonce) -> %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec get_nonce(proplist()) -> nonce() | 'none'. -get_nonce(Head) -> - case proplists:lookup("replay-nonce", Head) of - {"replay-nonce", Nonce} -> Nonce; - none -> none - end. - --spec get_location(proplist()) -> url() | 'none'. -get_location(Head) -> - case proplists:lookup("location", Head) of - {"location", Location} -> Location; - none -> none - end. - -spec location_to_id(url()) -> {ok, string()} | {error, not_found}. location_to_id(Url0) -> Url = string:strip(Url0, right, $/), @@ -489,74 +187,14 @@ location_to_id(Url0) -> {ok, string:sub_string(Url, Ind+1)} end. -%% Very bad way to extract this -%% TODO: Find a better way --spec get_tos(proplist()) -> url() | 'none'. -get_tos(Head) -> - try - [{_, Link}] = [{K, V} || {K, V} <- Head, - K =:= "link" andalso - lists:suffix("\"terms-of-service\"", V)], - [Link1, _] = string:tokens(Link, ";"), - Link2 = string:strip(Link1, left, $<), - string:strip(Link2, right, $>) - catch - _:_ -> - none - end. - -spec get_challenges(proplist()) -> [{proplist()}]. get_challenges(Body) -> {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), Challenges. -decode_response(Head, Body, "application/pkix-cert") -> - {ok, Head, Body}; -decode_response(Head, Body, "application/jose+json") -> - case decode(Body) of - {ok, Return} -> - {ok, Head, Return}; - {error, Reason} -> - ?ERROR_MSG("Problem decoding: ~s", [Body]), - {error, Reason} - end. - -encode(EJson) -> - try - {ok, jiffy:encode(EJson)} - catch - _:Reason -> - {error, Reason} - end. - -decode(Json) -> - try - {Result} = jiffy:decode(Json), - {ok, Result} - catch - _:Reason -> - {error, Reason} - end. - is_error({error, _}) -> true; is_error(_) -> false. -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Handle Failed HTTP Requests -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - --spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}. -failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) -> - ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", - [Url, Code, Body]), - {error, unexpected_code}; -failed_http_request({error, Reason}, Url) -> - ?ERROR_MSG("Error making a request to <~s>: ~p", - [Url, Reason]), - {error, Reason}. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Handle Config and Persistence Files @@ -670,11 +308,11 @@ get_certificates0(CAUrl, HttpDir, "new-account") -> PrivateKey = generate_key(), %% Create a new account - {ok, _Id} = create_new_account(CAUrl, Contact, Key), + {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), %% Write Persistent Data {ok, Data} = read_persistent(), - NewData = set_account_persistent(Data, {Id, Key}), + NewData = set_account_persistent(Data, {Id, PrivateKey}), ok = write_persistent(NewData), get_certificates1(CAUrl, HttpDir, PrivateKey). @@ -686,13 +324,13 @@ get_certificates1(CAUrl, HttpDir, PrivateKey) -> %% Get a certificate for each host PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey, HttpDir) || Host <- Hosts], - {AccId, PrivateKey, PemCertKeys}. + {ok, PrivateKey, PemCertKeys}. get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), case create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) of - {ok, Authz} -> + {ok, _Authz} -> create_new_certificate(CAUrl, DomainName, PrivateKey); {error, authorization} -> {error, {authorization, {host, DomainName}}} @@ -702,13 +340,15 @@ get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> %% Find a way to ask the user if he accepts the TOS create_new_account(CAUrl, Contact, PrivateKey) -> try - {ok, Dirs, Nonce0} = directory(CAUrl), + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), Req0 = [{ <<"contact">>, [Contact]}], - {ok, {TOS, Account}, Nonce1} = new_account(Dirs, PrivateKey, Req0, Nonce0), + {ok, {TOS, Account}, Nonce1} = + ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), {<<"id">>, AccIdInt} = lists:keyfind(<<"id">>, 1, Account), AccId = integer_to_list(AccIdInt), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, _Nonce2} = update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce1), + {ok, _Account2, _Nonce2} = + ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce1), {ok, AccId} catch E:R -> @@ -718,12 +358,13 @@ create_new_account(CAUrl, Contact, PrivateKey) -> create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> try - {ok, Dirs, Nonce0} = directory(CAUrl), + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), Req0 = [{<<"identifier">>, {[{<<"type">>, <<"dns">>}, {<<"value">>, DomainName}]}}, {<<"existing">>, <<"accept">>}], - {ok, {AuthzUrl, Authz}, Nonce1} = new_authz(Dirs, PrivateKey, Req0, Nonce0), + {ok, {AuthzUrl, Authz}, Nonce1} = + ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req0, Nonce0), {ok, AuthzId} = location_to_id(AuthzUrl), Challenges = get_challenges(Authz), @@ -731,10 +372,10 @@ create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), {ok, ChallengeId} = location_to_id(ChallengeUrl), Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], - {ok, SolvedChallenge, Nonce2} = - complete_challenge({CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), + {ok, SolvedChallenge, Nonce2} = ejabberd_acme_comm:complete_challenge( + {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), - {ok, AuthzValid, _Nonce} = get_authz_until_valid({CAUrl, AuthzId}), + {ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), {ok, AuthzValid} catch E:R -> @@ -745,7 +386,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> create_new_certificate(CAUrl, DomainName, PrivateKey) -> try - {ok, Dirs, Nonce0} = directory(CAUrl), + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), CSRSubject = [{commonName, bitstring_to_list(DomainName)}], {CSR, CSRKey} = make_csr(CSRSubject), {NotBefore, NotAfter} = not_before_not_after(), @@ -754,7 +395,7 @@ create_new_certificate(CAUrl, DomainName, PrivateKey) -> {<<"notBefore">>, NotBefore}, {<<"NotAfter">>, NotAfter} ], - {ok, {CertUrl, Certificate}, Nonce1} = new_cert(Dirs, PrivateKey, Req, Nonce0), + {ok, {CertUrl, Certificate}, Nonce1} = ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req, Nonce0), {ok, CertId} = location_to_id(CertUrl), @@ -788,9 +429,10 @@ not_before_not_after() -> %% A typical acme workflow scenario(CAUrl, AccId, PrivateKey) -> - {ok, Dirs, Nonce0} = directory(CAUrl), + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), - {ok, {_TOS, Account}, Nonce1} = get_account({CAUrl, AccId}, PrivateKey, Nonce0), + {ok, {_TOS, Account}, Nonce1} = + ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce0), ?INFO_MSG("Account: ~p~n", [Account]), Req = @@ -799,7 +441,7 @@ scenario(CAUrl, AccId, PrivateKey) -> {<<"value">>, <<"my-acme-test-ejabberd.com">>}]}}, {<<"existing">>, <<"accept">>} ], - {ok, Authz, Nonce2} = new_authz(Dirs, PrivateKey, Req, Nonce1), + {ok, Authz, Nonce2} = ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req, Nonce1), {Account, Authz, PrivateKey}. @@ -807,19 +449,21 @@ scenario(CAUrl, AccId, PrivateKey) -> new_user_scenario(CAUrl, HttpDir) -> PrivateKey = generate_key(), - {ok, Dirs, Nonce0} = directory(CAUrl), + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), %% ?INFO_MSG("Directories: ~p", [Dirs]), Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = new_account(Dirs, PrivateKey, Req0, Nonce0), + {ok, {TOS, Account}, Nonce1} = ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), {_, AccIdInt} = proplists:lookup(<<"id">>, Account), AccId = integer_to_list(AccIdInt), - {ok, {_TOS, Account1}, Nonce2} = get_account({CAUrl, AccId}, PrivateKey, Nonce1), + {ok, {_TOS, Account1}, Nonce2} = + ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce1), %% ?INFO_MSG("Old account: ~p~n", [Account1]), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), + {ok, Account2, Nonce3} = + ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), %% NewKey = generate_key(), %% KeyChangeUrl = CAUrl ++ "/acme/key-change/", @@ -838,10 +482,11 @@ new_user_scenario(CAUrl, HttpDir) -> {<<"value">>, DomainName}]}}, {<<"existing">>, <<"accept">>} ], - {ok, {AuthzUrl, Authz}, Nonce4} = new_authz(Dirs, PrivateKey, Req2, Nonce3), + {ok, {AuthzUrl, Authz}, Nonce4} = + ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req2, Nonce3), {ok, AuthzId} = location_to_id(AuthzUrl), - {ok, Authz2, Nonce5} = get_authz({CAUrl, AuthzId}), + {ok, Authz2, Nonce5} = ejabberd_acme_comm:get_authz({CAUrl, AuthzId}), ?INFO_MSG("AuthzUrl: ~p~n", [AuthzUrl]), Challenges = get_challenges(Authz2), @@ -856,11 +501,12 @@ new_user_scenario(CAUrl, HttpDir) -> [ {<<"type">>, <<"http-01">>} , {<<"keyAuthorization">>, KeyAuthz} ], - {ok, SolvedChallenge, Nonce6} = complete_challenge({CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce5), + {ok, SolvedChallenge, Nonce6} = ejabberd_acme_comm:complete_challenge( + {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce5), %% ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), %% timer:sleep(2000), - {ok, Authz3, Nonce7} = get_authz_until_valid({CAUrl, AuthzId}), + {ok, Authz3, Nonce7} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), #{"new-cert" := NewCert} = Dirs, CSRSubject = [{commonName, bitstring_to_list(DomainName)}, @@ -874,11 +520,12 @@ new_user_scenario(CAUrl, HttpDir) -> {<<"notBefore">>, NotBefore}, {<<"NotAfter">>, NotAfter} ], - {ok, {CertUrl, Certificate}, Nonce8} = new_cert(Dirs, PrivateKey, Req4, Nonce7), + {ok, {CertUrl, Certificate}, Nonce8} = + ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req4, Nonce7), ?INFO_MSG("CertUrl: ~p~n", [CertUrl]), {ok, CertId} = location_to_id(CertUrl), - {ok, Certificate2, Nonce9} = get_cert({CAUrl, CertId}), + {ok, Certificate2, Nonce9} = ejabberd_acme_comm:get_cert({CAUrl, CertId}), DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate2), plain), %% ?INFO_MSG("DecodedCert: ~p~n", [DecodedCert]), @@ -897,9 +544,9 @@ new_user_scenario(CAUrl, HttpDir) -> Base64Cert = base64url:encode(Certificate2), Req5 = [{<<"certificate">>, Base64Cert}], - {ok, [], Nonce10} = revoke_cert(Dirs, PrivateKey, Req5, Nonce9), + {ok, [], Nonce10} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req5, Nonce9), - {ok, Certificate3, Nonce11} = get_cert(CertUrl), + {ok, Certificate3, Nonce11} = ejabberd_acme_comm:get_cert(CertUrl), {Account2, Authz3, CSR, Certificate, PrivateKey}. @@ -918,26 +565,30 @@ delete_account_scenario(CAUrl) -> PrivateKey = generate_key(), DirURL = CAUrl ++ "/directory", - {ok, Dirs, Nonce0} = directory(DirURL), + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(DirURL), %% ?INFO_MSG("Directories: ~p", [Dirs]), Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = new_account(Dirs, PrivateKey, Req0, Nonce0), + {ok, {TOS, Account}, Nonce1} = ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), {_, AccIdInt} = proplists:lookup(<<"id">>, Account), AccId = integer_to_list(AccIdInt), - {ok, {_TOS, Account1}, Nonce2} = get_account({CAUrl, AccId}, PrivateKey, Nonce1), + {ok, {_TOS, Account1}, Nonce2} = + ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce1), %% ?INFO_MSG("Old account: ~p~n", [Account1]), Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), + {ok, Account2, Nonce3} = + ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), %% Delete account - {ok, Account3, Nonce4} = delete_account({CAUrl, AccId}, PrivateKey, Nonce3), + {ok, Account3, Nonce4} = + ejabberd_acme_comm:delete_account({CAUrl, AccId}, PrivateKey, Nonce3), timer:sleep(3000), - {ok, {_TOS, Account4}, Nonce5} = get_account({CAUrl, AccId}, PrivateKey, Nonce4), + {ok, {_TOS, Account4}, Nonce5} = + ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce4), ?INFO_MSG("New account: ~p~n", [Account4]), AccIdBin = list_to_bitstring(integer_to_list(AccIdInt)), @@ -948,7 +599,8 @@ delete_account_scenario(CAUrl) -> {<<"value">>, DomainName}]}}, {<<"existing">>, <<"accept">>} ], - {ok, {AuthzUrl, Authz}, Nonce6} = new_authz(Dirs, PrivateKey, Req2, Nonce5), + {ok, {AuthzUrl, Authz}, Nonce6} = + ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req2, Nonce5), {ok, Account1, Account3, Authz}. diff --git a/src/ejabberd_acme_comm.erl b/src/ejabberd_acme_comm.erl new file mode 100644 index 000000000..804d46531 --- /dev/null +++ b/src/ejabberd_acme_comm.erl @@ -0,0 +1,387 @@ +-module(ejabberd_acme_comm). +-export([%% Directory + directory/1, + %% Account + new_account/4, + update_account/4, + get_account/3, + delete_account/3, + %% Authorization + new_authz/4, + get_authz/1, + complete_challenge/4, + %% Authorization polling + get_authz_until_valid/1, + %% Certificate + new_cert/4, + get_cert/1, + revoke_cert/4 + %% Not yet implemented + %% key_roll_over/5 + %% delete_authz/3 + ]). + +-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. +-define(MAX_POLL_REQUESTS, 20). +-define(POLL_WAIT_TIME, 500). % 500 ms. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Directory +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec directory(url()) -> {ok, dirs(), nonce()} | {error, _}. +directory(CAUrl) -> + Url = CAUrl ++ "/directory", + prepare_get_request(Url, fun get_dirs/1). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Account Handling +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec new_account(dirs(), jose_jwk:key(), proplist(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. +new_account(Dirs, PrivateKey, Req, Nonce) -> + #{"new-reg" := Url} = Dirs, + EJson = {[{ <<"resource">>, <<"new-reg">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). + +-spec update_account({url(), string()}, jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +update_account({CAUrl, AccId}, PrivateKey, Req, Nonce) -> + Url = CAUrl ++ "/acme/reg/" ++ AccId, + EJson = {[{ <<"resource">>, <<"reg">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + +-spec get_account({url(), string()}, jose_jwk:key(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. +get_account({CAUrl, AccId}, PrivateKey, Nonce) -> + Url = CAUrl ++ "/acme/reg/" ++ AccId, + EJson = {[{<<"resource">>, <<"reg">>}]}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_tos/1). + +-spec delete_account({url(), string()}, jose_jwk:key(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +delete_account({CAUrl, AccId}, PrivateKey, Nonce) -> + Url = CAUrl ++ "/acme/reg/" ++ AccId, + EJson = + {[{<<"resource">>, <<"reg">>}, + {<<"status">>, <<"deactivated">>}]}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Authorization Handling +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec new_authz(dirs(), jose_jwk:key(), proplist(), nonce()) -> + {ok, {url(), proplist()}, nonce()} | {error, _}. +new_authz(Dirs, PrivateKey, Req, Nonce) -> + #{"new-authz" := Url} = Dirs, + EJson = {[{<<"resource">>, <<"new-authz">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1). + +-spec get_authz({url(), string()}) -> {ok, proplist(), nonce()} | {error, _}. +get_authz({CAUrl, AuthzId}) -> + Url = CAUrl ++ "/acme/authz/" ++ AuthzId, + prepare_get_request(Url, fun get_response/1). + +-spec complete_challenge({url(), string(), string()}, jose_jwk:key(), proplist(), nonce()) -> + {ok, proplist(), nonce()} | {error, _}. +complete_challenge({CAUrl, AuthzId, ChallId}, PrivateKey, Req, Nonce) -> + Url = CAUrl ++ "/acme/challenge/" ++ AuthzId ++ "/" ++ ChallId, + EJson = {[{<<"resource">>, <<"challenge">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Certificate Handling +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec new_cert(dirs(), jose_jwk:key(), proplist(), nonce()) -> + {ok, {url(), list()}, nonce()} | {error, _}. +new_cert(Dirs, PrivateKey, Req, Nonce) -> + #{"new-cert" := Url} = Dirs, + EJson = {[{<<"resource">>, <<"new-cert">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response_location/1, + "application/pkix-cert"). + +-spec get_cert({url(), string()}) -> {ok, list(), nonce()} | {error, _}. +get_cert({CAUrl, CertId}) -> + Url = CAUrl ++ "/acme/cert/" ++ CertId, + prepare_get_request(Url, fun get_response/1, "application/pkix-cert"). + +-spec revoke_cert(dirs(), jose_jwk:key(), proplist(), nonce()) -> + {ok, _, nonce()} | {error, _}. +revoke_cert(Dirs, PrivateKey, Req, Nonce) -> + #{"revoke-cert" := Url} = Dirs, + EJson = {[{<<"resource">>, <<"revoke-cert">>}] ++ Req}, + prepare_post_request(Url, PrivateKey, EJson, Nonce, fun get_response/1, + "application/pkix-cert"). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle Response Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec get_dirs({ok, proplist(), proplist()}) -> {ok, map(), nonce()}. +get_dirs({ok, Head, Return}) -> + NewNonce = get_nonce(Head), + StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || + {X, Y} <- Return], + NewDirs = maps:from_list(StrDirectories), + {ok, NewDirs, NewNonce}. + +-spec get_response({ok, proplist(), proplist()}) -> {ok, proplist(), nonce()}. +get_response({ok, Head, Return}) -> + NewNonce = get_nonce(Head), + {ok, Return, NewNonce}. + +-spec get_response_tos({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. +get_response_tos({ok, Head, Return}) -> + TOSUrl = get_tos(Head), + NewNonce = get_nonce(Head), + {ok, {TOSUrl, Return}, NewNonce}. + +-spec get_response_location({ok, proplist(), proplist()}) -> {ok, {url(), proplist()}, nonce()}. +get_response_location({ok, Head, Return}) -> + Location = get_location(Head), + NewNonce = get_nonce(Head), + {ok, {Location, Return}, NewNonce}. + + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Authorization Polling +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec get_authz_until_valid({url(), string()}) -> {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid({CAUrl, AuthzId}) -> + get_authz_until_valid({CAUrl, AuthzId}, ?MAX_POLL_REQUESTS). + +-spec get_authz_until_valid({url(), string()}, non_neg_integer()) -> + {ok, proplist(), nonce()} | {error, _}. +get_authz_until_valid({_CAUrl, _AuthzId}, 0) -> + ?ERROR_MSG("Maximum request limit waiting for validation reached", []), + {error, max_request_limit}; +get_authz_until_valid({CAUrl, AuthzId}, N) -> + case get_authz({CAUrl, AuthzId}) of + {ok, Resp, Nonce} -> + case is_authz_valid(Resp) of + true -> + {ok, Resp, Nonce}; + false -> + timer:sleep(?POLL_WAIT_TIME), + get_authz_until_valid({CAUrl, AuthzId}, N-1) + end; + {error, _} = Err -> + Err + end. + +-spec is_authz_valid(proplist()) -> boolean(). +is_authz_valid(Authz) -> + case proplists:lookup(<<"status">>, Authz) of + {<<"status">>, <<"valid">>} -> + true; + _ -> + false + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Request Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% TODO: Fix the duplicated code at the below 4 functions +-spec make_post_request(url(), bitstring(), string()) -> + {ok, proplist(), proplist()} | {error, _}. +make_post_request(Url, ReqBody, ResponseType) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(post, + {Url, [], "application/jose+json", ReqBody}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + decode_response(Head, Body, ResponseType); + Error -> + failed_http_request(Error, Url) + end. + +-spec make_get_request(url(), string()) -> + {ok, proplist(), proplist()} | {error, _}. +make_get_request(Url, ResponseType) -> + Options = [], + HttpOptions = [{timeout, ?REQUEST_TIMEOUT}], + case httpc:request(get, {Url, []}, HttpOptions, Options) of + {ok, {{_, Code, _}, Head, Body}} when Code >= 200, Code =< 299 -> + decode_response(Head, Body, ResponseType); + Error -> + failed_http_request(Error, Url) + end. + +-spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), + nonce(), handle_resp_fun()) -> {ok, _, nonce()} | {error, _}. +prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun) -> + prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, "application/jose+json"). + +-spec prepare_post_request(url(), jose_jwk:key(), jiffy:json_value(), + nonce(), handle_resp_fun(), string()) -> {ok, _, nonce()} | {error, _}. +prepare_post_request(Url, PrivateKey, EJson, Nonce, HandleRespFun, ResponseType) -> + case encode(EJson) of + {ok, ReqBody} -> + FinalBody = sign_encode_json_jose(PrivateKey, ReqBody, Nonce), + case make_post_request(Url, FinalBody, ResponseType) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end; + {error, Reason} -> + ?ERROR_MSG("Error: ~p when encoding: ~p", [Reason, EJson]), + {error, Reason} + end. + +-spec prepare_get_request(url(), handle_resp_fun()) -> + {ok, _, nonce()} | {error, _}. +prepare_get_request(Url, HandleRespFun) -> + prepare_get_request(Url, HandleRespFun, "application/jose+json"). + +-spec prepare_get_request(url(), handle_resp_fun(), string()) -> + {ok, _, nonce()} | {error, _}. +prepare_get_request(Url, HandleRespFun, ResponseType) -> + case make_get_request(Url, ResponseType) of + {ok, Head, Return} -> + HandleRespFun({ok, Head, Return}); + Error -> + Error + end. + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Jose Json Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec sign_json_jose(jose_jwk:key(), bitstring(), nonce()) -> {_, jws()}. +sign_json_jose(Key, Json, Nonce) -> + PubKey = jose_jwk:to_public(Key), + {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), + PubKeyJson = jiffy:decode(BinaryPubKey), + %% TODO: Ensure this works for all cases + AlgMap = jose_jwk:signer(Key), + JwsMap = + #{ <<"jwk">> => PubKeyJson, + %% <<"b64">> => true, + <<"nonce">> => list_to_bitstring(Nonce) + }, + JwsObj0 = maps:merge(JwsMap, AlgMap), + JwsObj = jose_jws:from(JwsObj0), + jose_jws:sign(Key, Json, JwsObj). + +-spec sign_encode_json_jose(jose_jwk:key(), bitstring(), nonce()) -> bitstring(). +sign_encode_json_jose(Key, Json, Nonce) -> + {_, Signed} = sign_json_jose(Key, Json, Nonce), + %% This depends on jose library, so we can consider it safe + jiffy:encode(Signed). + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Useful funs +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec get_nonce(proplist()) -> nonce() | 'none'. +get_nonce(Head) -> + case proplists:lookup("replay-nonce", Head) of + {"replay-nonce", Nonce} -> Nonce; + none -> none + end. + +-spec get_location(proplist()) -> url() | 'none'. +get_location(Head) -> + case proplists:lookup("location", Head) of + {"location", Location} -> Location; + none -> none + end. + +%% Very bad way to extract this +%% TODO: Find a better way +-spec get_tos(proplist()) -> url() | 'none'. +get_tos(Head) -> + try + [{_, Link}] = [{K, V} || {K, V} <- Head, + K =:= "link" andalso + lists:suffix("\"terms-of-service\"", V)], + [Link1, _] = string:tokens(Link, ";"), + Link2 = string:strip(Link1, left, $<), + string:strip(Link2, right, $>) + catch + _:_ -> + none + end. + +decode_response(Head, Body, "application/pkix-cert") -> + {ok, Head, Body}; +decode_response(Head, Body, "application/jose+json") -> + case decode(Body) of + {ok, Return} -> + {ok, Head, Return}; + {error, Reason} -> + ?ERROR_MSG("Problem decoding: ~s", [Body]), + {error, Reason} + end. + +encode(EJson) -> + try + {ok, jiffy:encode(EJson)} + catch + _:Reason -> + {error, Reason} + end. + +decode(Json) -> + try + {Result} = jiffy:decode(Json), + {ok, Result} + catch + _:Reason -> + {error, Reason} + end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle Failed HTTP Requests +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}. +failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) -> + ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", + [Url, Code, Body]), + {error, unexpected_code}; +failed_http_request({error, Reason}, Url) -> + ?ERROR_MSG("Error making a request to <~s>: ~p", + [Url, Reason]), + {error, Reason}. From 586612413899259b4dc2e8d85951fc97bb18942e Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 7 Jul 2017 17:37:44 +0300 Subject: [PATCH 23/75] Clean up get_certificate code --- include/ejabberd_acme.hrl | 4 + src/ejabberd_acme.erl | 386 ++++++++++++++++++++++---------------- 2 files changed, 228 insertions(+), 162 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index ff35c99e5..e696429b0 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -25,3 +25,7 @@ -type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). -type acme_challenge() :: #challenge{}. + +-type account_opt() :: string(). + +-type pem_certificate() :: bitstring(). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index e1cee923e..2f64d1c88 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -23,6 +23,179 @@ -include_lib("public_key/include/public_key.hrl"). + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Command Functions +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%% +%% Check Validity of command options +%% + +-spec is_valid_account_opt(string()) -> boolean(). +is_valid_account_opt("old-account") -> true; +is_valid_account_opt("new-account") -> true; +is_valid_account_opt(_) -> false. + +%% +%% Get Certificate +%% + +%% Needs a hell lot of cleaning +-spec get_certificates(url(), string(), account_opt()) -> + [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | + {'error', _}. +get_certificates(CAUrl, HttpDir, NewAccountOpt) -> + try + get_certificates0(CAUrl, HttpDir, NewAccountOpt) + catch + E:R -> + %% ?ERROR_MSG("Unknown ~p:~p", [E, R]), + {error, get_certificates} + end. + +-spec get_certificates0(url(), string(), account_opt()) -> + [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | + {'error', _}. +get_certificates0(CAUrl, HttpDir, "old-account") -> + %% Read Persistent Data + {ok, Data} = read_persistent(), + + %% Get the current account + case get_account_persistent(Data) of + none -> + ?ERROR_MSG("No existing account", []), + {error, no_old_account}; + {ok, _AccId, PrivateKey} -> + get_certificates1(CAUrl, HttpDir, PrivateKey) + end; +get_certificates0(CAUrl, HttpDir, "new-account") -> + %% Get contact from configuration file + {ok, Contact} = get_config_contact(), + + %% Generate a Key + PrivateKey = generate_key(), + + %% Create a new account + {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), + + %% Write Persistent Data + {ok, Data} = read_persistent(), + NewData = set_account_persistent(Data, {Id, PrivateKey}), + ok = write_persistent(NewData), + + get_certificates1(CAUrl, HttpDir, PrivateKey). + +-spec get_certificates1(url(), string(), jose_jwk:key()) -> + {'ok', [{'ok', pem_certificate()} | {'error', _}]} | + {'error', _}. +get_certificates1(CAUrl, HttpDir, PrivateKey) -> + %% Read Config + {ok, Hosts} = get_config_hosts(), + + %% Get a certificate for each host + PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey, HttpDir) || Host <- Hosts], + + %% Save Certificates + SavedCerts = [save_certificate(Cert) || Cert <- PemCertKeys], + + %% Format the result to send back to ejabberdctl + %% Result + SavedCerts. + +-spec get_certificate(url(), bitstring(), jose_jwk:key(), string()) -> + {'ok', pem_certificate()} | + {'error', _}. +get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> + ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), + case create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) of + {ok, _Authz} -> + create_new_certificate(CAUrl, DomainName, PrivateKey); + {error, authorization} -> + {error, DomainName, authorization} + end. + +%% TODO: +%% Find a way to ask the user if he accepts the TOS +create_new_account(CAUrl, Contact, PrivateKey) -> + try + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), + Req0 = [{ <<"contact">>, [Contact]}], + {ok, {TOS, Account}, Nonce1} = + ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), + {<<"id">>, AccIdInt} = lists:keyfind(<<"id">>, 1, Account), + AccId = integer_to_list(AccIdInt), + Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], + {ok, _Account2, _Nonce2} = + ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce1), + {ok, AccId} + catch + E:R -> + {error,create_new_account} + end. + + +create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> + try + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), + Req0 = [{<<"identifier">>, + {[{<<"type">>, <<"dns">>}, + {<<"value">>, DomainName}]}}, + {<<"existing">>, <<"accept">>}], + {ok, {AuthzUrl, Authz}, Nonce1} = + ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req0, Nonce0), + {ok, AuthzId} = location_to_id(AuthzUrl), + + Challenges = get_challenges(Authz), + {ok, ChallengeUrl, KeyAuthz} = + acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), + {ok, ChallengeId} = location_to_id(ChallengeUrl), + Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], + {ok, SolvedChallenge, Nonce2} = ejabberd_acme_comm:complete_challenge( + {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), + + {ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), + {ok, AuthzValid} + catch + E:R -> + ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", + [{E,R}, DomainName]), + {error, authorization} + end. + +create_new_certificate(CAUrl, DomainName, PrivateKey) -> + try + {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), + CSRSubject = [{commonName, bitstring_to_list(DomainName)}], + {CSR, CSRKey} = make_csr(CSRSubject), + {NotBefore, NotAfter} = not_before_not_after(), + Req = + [{<<"csr">>, CSR}, + {<<"notBefore">>, NotBefore}, + {<<"NotAfter">>, NotAfter} + ], + {ok, {CertUrl, Certificate}, Nonce1} = ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req, Nonce0), + + {ok, CertId} = location_to_id(CertUrl), + + DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate), plain), + PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert), + + {_, CSRKeyKey} = jose_jwk:to_key(CSRKey), + PemEntryKey = public_key:pem_entry_encode('ECPrivateKey', CSRKeyKey), + + PemCertKey = public_key:pem_encode([PemEntryKey, PemEntryCert]), + + {ok, DomainName, PemCertKey} + catch + E:R -> + ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", + [{E,R}, DomainName]), + {error, certificate} + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Certificate Request Functions @@ -34,7 +207,7 @@ %% TODO: Make this function handle more signing keys %% 1. Derive oid from Key %% 2. Derive the whole algo objects from Key -%% TODO: Encode Strings using length. +%% TODO: Encode Strings using length using a library function -spec make_csr(proplist()) -> {binary(), jose_jwk:key()}. make_csr(Attributes) -> @@ -192,6 +365,13 @@ get_challenges(Body) -> {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), Challenges. +not_before_not_after() -> + %% TODO: Make notBefore and notAfter like they do it in other clients + {MegS, Sec, MicS} = erlang:timestamp(), + NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), + NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), + {NotBefore, NotAfter}. + is_error({error, _}) -> true; is_error(_) -> false. @@ -237,19 +417,47 @@ set_account_persistent(Data = #data{}, {AccId, PrivateKey}) -> NewAcc = #data_acc{id = AccId, key = PrivateKey}, Data#data{account = NewAcc}. -get_config_contact() -> +save_certificate({error, _, _} = Error) -> + Error; +save_certificate({ok, DomainName, Cert}) -> + try + {ok, CertDir} = get_config_cert_dir(), + DomainString = bitstring_to_list(DomainName), + CertificateFile = filename:join([CertDir, DomainString ++ "_cert.pem"]), + case file:write_file(CertificateFile, Cert) of + ok -> + {ok, DomainName, saved}; + {error, Reason} -> + ?ERROR_MSG("Error: ~p saving certificate at file: ~p", + [Reason, CertificateFile]), + {error, DomainName, saving} + end + catch + E:R -> + {error, DomainName, saving} + end. + +get_config_acme() -> case ejabberd_config:get_option(acme, undefined) of undefined -> ?ERROR_MSG("No acme configuration has been specified", []), {error, configuration}; Acme -> + {ok, Acme} + end. + +get_config_contact() -> + case get_config_acme() of + {ok, Acme} -> case lists:keyfind(contact, 1, Acme) of {contact, Contact} -> {ok, Contact}; false -> ?ERROR_MSG("No contact has been specified", []), {error, configuration_contact} - end + end; + {error, Reason} -> + {error, Reason} end. get_config_hosts() -> @@ -261,166 +469,20 @@ get_config_hosts() -> {ok, Hosts} end. -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Command Functions -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% -%% Check Validity of command options -%% - -is_valid_account_opt("old-account") -> true; -is_valid_account_opt("new-account") -> true; -is_valid_account_opt(_) -> false. - -%% -%% Get Certificate -%% - -%% Needs a hell lot of cleaning -get_certificates(CAUrl, HttpDir, NewAccountOpt) -> - try - get_certificates0(CAUrl, HttpDir, NewAccountOpt) - catch - E:R -> - {E,R} +get_config_cert_dir() -> + case get_config_acme() of + {ok, Acme} -> + case lists:keyfind(cert_dir, 1, Acme) of + {cert_dir, CertDir} -> + {ok, CertDir}; + false -> + ?ERROR_MSG("No certificate directory has been specified", []), + {error, configuration_cert_dir} + end; + {error, Reason} -> + {error, Reason} end. -get_certificates0(CAUrl, HttpDir, "old-account") -> - %% Read Persistent Data - {ok, Data} = read_persistent(), - - %% Get the current account - case get_account_persistent(Data) of - none -> - ?ERROR_MSG("No existing account", []), - {error, no_old_account}; - {ok, _AccId, PrivateKey} -> - get_certificates1(CAUrl, HttpDir, PrivateKey) - end; -get_certificates0(CAUrl, HttpDir, "new-account") -> - %% Get contact from configuration file - {ok, Contact} = get_config_contact(), - - %% Generate a Key - PrivateKey = generate_key(), - - %% Create a new account - {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), - - %% Write Persistent Data - {ok, Data} = read_persistent(), - NewData = set_account_persistent(Data, {Id, PrivateKey}), - ok = write_persistent(NewData), - - get_certificates1(CAUrl, HttpDir, PrivateKey). - - -get_certificates1(CAUrl, HttpDir, PrivateKey) -> - %% Read Config - {ok, Hosts} = get_config_hosts(), - - %% Get a certificate for each host - PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey, HttpDir) || Host <- Hosts], - {ok, PrivateKey, PemCertKeys}. - - -get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> - ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), - case create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) of - {ok, _Authz} -> - create_new_certificate(CAUrl, DomainName, PrivateKey); - {error, authorization} -> - {error, {authorization, {host, DomainName}}} - end. - -%% TODO: -%% Find a way to ask the user if he accepts the TOS -create_new_account(CAUrl, Contact, PrivateKey) -> - try - {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), - Req0 = [{ <<"contact">>, [Contact]}], - {ok, {TOS, Account}, Nonce1} = - ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), - {<<"id">>, AccIdInt} = lists:keyfind(<<"id">>, 1, Account), - AccId = integer_to_list(AccIdInt), - Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, _Account2, _Nonce2} = - ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce1), - {ok, AccId} - catch - E:R -> - {error,create_new_account} - end. - - -create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> - try - {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), - Req0 = [{<<"identifier">>, - {[{<<"type">>, <<"dns">>}, - {<<"value">>, DomainName}]}}, - {<<"existing">>, <<"accept">>}], - {ok, {AuthzUrl, Authz}, Nonce1} = - ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req0, Nonce0), - {ok, AuthzId} = location_to_id(AuthzUrl), - - Challenges = get_challenges(Authz), - {ok, ChallengeUrl, KeyAuthz} = - acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), - {ok, ChallengeId} = location_to_id(ChallengeUrl), - Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], - {ok, SolvedChallenge, Nonce2} = ejabberd_acme_comm:complete_challenge( - {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), - - {ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), - {ok, AuthzValid} - catch - E:R -> - ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", - [{E,R}, DomainName]), - {error, authorization} - end. - -create_new_certificate(CAUrl, DomainName, PrivateKey) -> - try - {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), - CSRSubject = [{commonName, bitstring_to_list(DomainName)}], - {CSR, CSRKey} = make_csr(CSRSubject), - {NotBefore, NotAfter} = not_before_not_after(), - Req = - [{<<"csr">>, CSR}, - {<<"notBefore">>, NotBefore}, - {<<"NotAfter">>, NotAfter} - ], - {ok, {CertUrl, Certificate}, Nonce1} = ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req, Nonce0), - - {ok, CertId} = location_to_id(CertUrl), - - DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate), plain), - PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert), - - {_, CSRKeyKey} = jose_jwk:to_key(CSRKey), - PemEntryKey = public_key:pem_entry_encode('ECPrivateKey', CSRKeyKey), - - PemCertKey = public_key:pem_encode([PemEntryKey, PemEntryCert]), - - {ok, PemCertKey} - catch - E:R -> - ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", - [{E,R}, DomainName]), - {error, certificate} - end. -not_before_not_after() -> - %% TODO: Make notBefore and notAfter like they do it in other clients - {MegS, Sec, MicS} = erlang:timestamp(), - NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), - NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), - {NotBefore, NotAfter}. - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Debugging Funcs -- They are only used for the development phase @@ -546,7 +608,7 @@ new_user_scenario(CAUrl, HttpDir) -> Req5 = [{<<"certificate">>, Base64Cert}], {ok, [], Nonce10} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req5, Nonce9), - {ok, Certificate3, Nonce11} = ejabberd_acme_comm:get_cert(CertUrl), + {ok, Certificate3, Nonce11} = ejabberd_acme_comm:get_cert({CAUrl, CertId}), {Account2, Authz3, CSR, Certificate, PrivateKey}. From c50f6c218ffc5bcce75fc57c4df63dca8dca8374 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 7 Jul 2017 18:32:07 +0300 Subject: [PATCH 24/75] Clean up code by adding throws instead of passing the error value --- src/ejabberd_acme.erl | 110 ++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 2f64d1c88..74a293f63 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -51,26 +51,23 @@ get_certificates(CAUrl, HttpDir, NewAccountOpt) -> try get_certificates0(CAUrl, HttpDir, NewAccountOpt) catch + throw:Throw -> + Throw; E:R -> - %% ?ERROR_MSG("Unknown ~p:~p", [E, R]), + ?ERROR_MSG("Unknown ~p:~p", [E, R]), {error, get_certificates} end. -spec get_certificates0(url(), string(), account_opt()) -> [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | - {'error', _}. + no_return(). get_certificates0(CAUrl, HttpDir, "old-account") -> %% Read Persistent Data {ok, Data} = read_persistent(), %% Get the current account - case get_account_persistent(Data) of - none -> - ?ERROR_MSG("No existing account", []), - {error, no_old_account}; - {ok, _AccId, PrivateKey} -> - get_certificates1(CAUrl, HttpDir, PrivateKey) - end; + {ok, _AccId, PrivateKey} = ensure_account_exists(Data), + get_certificates1(CAUrl, HttpDir, PrivateKey); get_certificates0(CAUrl, HttpDir, "new-account") -> %% Get contact from configuration file {ok, Contact} = get_config_contact(), @@ -89,8 +86,8 @@ get_certificates0(CAUrl, HttpDir, "new-account") -> get_certificates1(CAUrl, HttpDir, PrivateKey). -spec get_certificates1(url(), string(), jose_jwk:key()) -> - {'ok', [{'ok', pem_certificate()} | {'error', _}]} | - {'error', _}. + [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | + no_return(). get_certificates1(CAUrl, HttpDir, PrivateKey) -> %% Read Config {ok, Hosts} = get_config_hosts(), @@ -106,15 +103,19 @@ get_certificates1(CAUrl, HttpDir, PrivateKey) -> SavedCerts. -spec get_certificate(url(), bitstring(), jose_jwk:key(), string()) -> - {'ok', pem_certificate()} | - {'error', _}. + {'ok', bitstring(), pem_certificate()} | + {'error', bitstring(), _}. get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), - case create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) of - {ok, _Authz} -> - create_new_certificate(CAUrl, DomainName, PrivateKey); - {error, authorization} -> - {error, DomainName, authorization} + try + {ok, _Authz} = create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir), + create_new_certificate(CAUrl, DomainName, PrivateKey) + catch + throw:Throw -> + Throw; + E:R -> + ?ERROR_MSG("Unknown ~p:~p", [E, R]), + {error, DomainName, get_certificate} end. %% TODO: @@ -133,7 +134,9 @@ create_new_account(CAUrl, Contact, PrivateKey) -> {ok, AccId} catch E:R -> - {error,create_new_account} + ?ERROR_MSG("Error: ~p creating an account for contact", + [{E,R}, Contact]), + throw({error,create_new_account}) end. @@ -153,7 +156,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), {ok, ChallengeId} = location_to_id(ChallengeUrl), Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], - {ok, SolvedChallenge, Nonce2} = ejabberd_acme_comm:complete_challenge( + {ok, _SolvedChallenge, _Nonce2} = ejabberd_acme_comm:complete_challenge( {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), {ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), @@ -162,7 +165,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> E:R -> ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", [{E,R}, DomainName]), - {error, authorization} + throw({error, DomainName, authorization}) end. create_new_certificate(CAUrl, DomainName, PrivateKey) -> @@ -176,9 +179,8 @@ create_new_certificate(CAUrl, DomainName, PrivateKey) -> {<<"notBefore">>, NotBefore}, {<<"NotAfter">>, NotAfter} ], - {ok, {CertUrl, Certificate}, Nonce1} = ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req, Nonce0), - - {ok, CertId} = location_to_id(CertUrl), + {ok, {_CertUrl, Certificate}, _Nonce1} = + ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req, Nonce0), DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate), plain), PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert), @@ -193,7 +195,16 @@ create_new_certificate(CAUrl, DomainName, PrivateKey) -> E:R -> ?ERROR_MSG("Error: ~p getting an authorization for domain: ~p~n", [{E,R}, DomainName]), - {error, certificate} + throw({error, DomainName, certificate}) + end. + +ensure_account_exists(Data) -> + case get_account_persistent(Data) of + none -> + ?ERROR_MSG("No existing account", []), + {error, no_old_account}; + {ok, AccId, PrivateKey} -> + {ok, AccId, PrivateKey} end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -393,7 +404,7 @@ read_persistent() -> {ok, #data{}}; {error, Reason} -> ?ERROR_MSG("Error: ~p reading acme data file", [Reason]), - {error, Reason} + throw({error, Reason}) end. write_persistent(Data) -> @@ -402,7 +413,7 @@ write_persistent(Data) -> ok -> ok; {error, Reason} -> ?ERROR_MSG("Error: ~p writing acme data file", [Reason]), - {error, Reason} + throw({error, Reason}) end. get_account_persistent(#data{account = Account}) -> @@ -430,10 +441,13 @@ save_certificate({ok, DomainName, Cert}) -> {error, Reason} -> ?ERROR_MSG("Error: ~p saving certificate at file: ~p", [Reason, CertificateFile]), - {error, DomainName, saving} + throw({error, DomainName, saving}) end catch + throw:Throw -> + Throw; E:R -> + ?ERROR_MSG("unknown ~p:~p", [E,R]), {error, DomainName, saving} end. @@ -441,46 +455,38 @@ get_config_acme() -> case ejabberd_config:get_option(acme, undefined) of undefined -> ?ERROR_MSG("No acme configuration has been specified", []), - {error, configuration}; + throw({error, configuration}); Acme -> {ok, Acme} end. get_config_contact() -> - case get_config_acme() of - {ok, Acme} -> - case lists:keyfind(contact, 1, Acme) of - {contact, Contact} -> - {ok, Contact}; - false -> - ?ERROR_MSG("No contact has been specified", []), - {error, configuration_contact} - end; - {error, Reason} -> - {error, Reason} + {ok, Acme} = get_config_acme(), + case lists:keyfind(contact, 1, Acme) of + {contact, Contact} -> + {ok, Contact}; + false -> + ?ERROR_MSG("No contact has been specified", []), + throw({error, configuration_contact}) end. get_config_hosts() -> case ejabberd_config:get_option(hosts, undefined) of undefined -> ?ERROR_MSG("No hosts have been specified", []), - {error, configuration_hosts}; + throw({error, configuration_hosts}); Hosts -> {ok, Hosts} end. get_config_cert_dir() -> - case get_config_acme() of - {ok, Acme} -> - case lists:keyfind(cert_dir, 1, Acme) of - {cert_dir, CertDir} -> - {ok, CertDir}; - false -> - ?ERROR_MSG("No certificate directory has been specified", []), - {error, configuration_cert_dir} - end; - {error, Reason} -> - {error, Reason} + {ok, Acme} = get_config_acme(), + case lists:keyfind(cert_dir, 1, Acme) of + {cert_dir, CertDir} -> + {ok, CertDir}; + false -> + ?ERROR_MSG("No certificate directory has been specified", []), + {error, configuration_cert_dir} end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% From b4b4e247dd078df7d26b3978356326fcd78042c7 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 7 Jul 2017 19:40:57 +0300 Subject: [PATCH 25/75] Add the certificate directory in ejabberd.yml.ecample --- ejabberd.yml.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index aeffc5a35..5e6f9b631 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -668,7 +668,8 @@ language: "en" acme: contact: "mailto:cert-admin-ejabberd@example.com" http_dir: "/home/konstantinos/Desktop/Programming/test-server-for-acme/" - + cert_dir: "/usr/local/var/lib/ejabberd/" + ###. ======= ###' MODULES From 5199ede4a257b3b225e47c6f1b8f28b12d63e8c6 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 11 Jul 2017 11:11:00 +0300 Subject: [PATCH 26/75] Changle acme file permissions Also changed some specs --- src/ejabberd_acme.erl | 63 ++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 74a293f63..60bb31bec 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -49,6 +49,7 @@ is_valid_account_opt(_) -> false. {'error', _}. get_certificates(CAUrl, HttpDir, NewAccountOpt) -> try + ?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]), get_certificates0(CAUrl, HttpDir, NewAccountOpt) catch throw:Throw -> @@ -67,22 +68,13 @@ get_certificates0(CAUrl, HttpDir, "old-account") -> %% Get the current account {ok, _AccId, PrivateKey} = ensure_account_exists(Data), + get_certificates1(CAUrl, HttpDir, PrivateKey); + get_certificates0(CAUrl, HttpDir, "new-account") -> - %% Get contact from configuration file - {ok, Contact} = get_config_contact(), - - %% Generate a Key - PrivateKey = generate_key(), - - %% Create a new account - {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), - - %% Write Persistent Data - {ok, Data} = read_persistent(), - NewData = set_account_persistent(Data, {Id, PrivateKey}), - ok = write_persistent(NewData), - + %% Create a new account and save it to disk + {ok, _Id, PrivateKey} = create_save_new_account(CAUrl), + get_certificates1(CAUrl, HttpDir, PrivateKey). -spec get_certificates1(url(), string(), jose_jwk:key()) -> @@ -118,8 +110,28 @@ get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> {error, DomainName, get_certificate} end. +-spec create_save_new_account(url()) -> {'ok', string(), jose_jwk:key()} | no_return(). +create_save_new_account(CAUrl) -> + %% Get contact from configuration file + {ok, Contact} = get_config_contact(), + + %% Generate a Key + PrivateKey = generate_key(), + + %% Create a new account + {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), + + %% Write Persistent Data + {ok, Data} = read_persistent(), + NewData = set_account_persistent(Data, {Id, PrivateKey}), + ok = write_persistent(NewData), + + {ok, Id, PrivateKey}. + %% TODO: %% Find a way to ask the user if he accepts the TOS +-spec create_new_account(url(), bitstring(), jose_jwk:key()) -> {'ok', string()} | + no_return(). create_new_account(CAUrl, Contact, PrivateKey) -> try {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), @@ -139,7 +151,8 @@ create_new_account(CAUrl, Contact, PrivateKey) -> throw({error,create_new_account}) end. - +-spec create_new_authorization(url(), bitstring(), jose_jwk:key(), bitstring()) -> + {'ok', proplist()} | no_return(). create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> try {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), @@ -396,11 +409,16 @@ persistent_file() -> MnesiaDir = mnesia:system_info(directory), filename:join(MnesiaDir, "acme.DAT"). +%% The persistent file should be rread and written only by its owner +persistent_file_mode() -> + 8#400 + 8#200. + read_persistent() -> case file:read_file(persistent_file()) of {ok, Binary} -> {ok, binary_to_term(Binary)}; {error, enoent} -> + create_persistent(), {ok, #data{}}; {error, Reason} -> ?ERROR_MSG("Error: ~p reading acme data file", [Reason]), @@ -416,6 +434,21 @@ write_persistent(Data) -> throw({error, Reason}) end. +create_persistent() -> + Binary = term_to_binary(#data{}), + case file:write_file(persistent_file(), Binary) of + ok -> + case file:change_mode(persistent_file(), persistent_file_mode()) of + ok -> ok; + {error, Reason} -> + ?ERROR_MSG("Error: ~p changing acme data file mode", [Reason]), + throw({error, Reason}) + end; + {error, Reason} -> + ?ERROR_MSG("Error: ~p creating acme data file", [Reason]), + throw({error, Reason}) + end. + get_account_persistent(#data{account = Account}) -> case Account of #data_acc{id = AccId, key = PrivateKey} -> From 77a96b0ec6c793ce0f5741d4683edd2ee9b3b877 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 12 Jul 2017 19:23:52 +0300 Subject: [PATCH 27/75] Solve acme challenges using built in http server --- ejabberd.yml.example | 4 ++- src/acme_challenge.erl | 62 ++++++++++++++++++++++++++++++++------ src/ejabberd_acme.erl | 13 ++++---- src/ejabberd_acme_comm.erl | 6 ++++ 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 5e6f9b631..7a75c5533 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -160,6 +160,7 @@ listen: request_handlers: "/websocket": ejabberd_http_ws "/api": mod_http_api + "/.well-known": acme_challenge ## "/pub/archive": mod_http_fileserver web_admin: true http_bind: true @@ -668,7 +669,8 @@ language: "en" acme: contact: "mailto:cert-admin-ejabberd@example.com" http_dir: "/home/konstantinos/Desktop/Programming/test-server-for-acme/" - cert_dir: "/usr/local/var/lib/ejabberd/" + +cert_dir: "/usr/local/var/lib/ejabberd/" ###. ======= ###' MODULES diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index 433de8143..2638e0ddf 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -1,7 +1,9 @@ -module(acme_challenge). --export ([ key_authorization/2, - solve_challenge/3 +-export ([key_authorization/2, + solve_challenge/3, + + process/2 ]). %% Challenge Types %% ================ @@ -13,9 +15,19 @@ -include("ejabberd.hrl"). -include("logger.hrl"). -include("xmpp.hrl"). - +-include("ejabberd_http.hrl"). -include("ejabberd_acme.hrl"). +%% TODO: Maybe validate request here?? +process(LocalPath, Request) -> + Result = ets_get_key_authorization(LocalPath), + ?INFO_MSG("Trying to serve: ~p at: ~p", [Request, LocalPath]), + ?INFO_MSG("Http Response: ~p", [Result]), + {200, + [{<<"Content-Type">>, <<"text/plain">>}], + Result}. + + -spec key_authorization(bitstring(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> Thumbprint = jose_jwk:thumbprint(Key), @@ -68,17 +80,49 @@ solve_challenge(ChallengeType, Challenges, Options) -> {ok, url(), bitstring()} | {error, _}. solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, {Key, HttpDir}) -> KeyAuthz = key_authorization(Tkn, Key), + %% save_key_authorization(Chal, Tkn, KeyAuthz, HttpDir); + ets_put_key_authorization(Tkn, KeyAuthz), + {ok, Chal#challenge.uri, KeyAuthz}; +solve_challenge1(Challenge, _Key) -> + ?INFO_MSG("Challenge: ~p~n", [Challenge]). + + +save_key_authorization(Chal, Tkn, KeyAuthz, HttpDir) -> 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]), + {error, Reason} = Err -> + ?ERROR_MSG("Error writing to file: ~s with reason: ~p~n", [FileLocation, Reason]), Err - end; -%% TODO: Fill stub -solve_challenge1(Challenge, _Key) -> - ?INFO_MSG("Challenge: ~p~n", [Challenge]). + end. + +-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(), + case ets:take(Tab, Key) of + [{Key, KeyAuthz}] -> + 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. %% Useful functions diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 60bb31bec..209ac46c5 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -513,13 +513,12 @@ get_config_hosts() -> end. get_config_cert_dir() -> - {ok, Acme} = get_config_acme(), - case lists:keyfind(cert_dir, 1, Acme) of - {cert_dir, CertDir} -> - {ok, CertDir}; - false -> - ?ERROR_MSG("No certificate directory has been specified", []), - {error, configuration_cert_dir} + case ejabberd_config:get_option(cert_dir, undefined) of + undefined -> + ?ERROR_MSG("No cert_dir configuration has been specified", []), + throw({error, configuration}); + CertDir -> + {ok, CertDir} end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/ejabberd_acme_comm.erl b/src/ejabberd_acme_comm.erl index 804d46531..b66d5f610 100644 --- a/src/ejabberd_acme_comm.erl +++ b/src/ejabberd_acme_comm.erl @@ -32,6 +32,12 @@ -define(MAX_POLL_REQUESTS, 20). -define(POLL_WAIT_TIME, 500). % 500 ms. +%%% +%%% This module contains functions that implement all necessary http +%%% requests to the ACME Certificate Authority. Its purpose is to +%%% facilitate the acme client implementation by separating the +%%% handling/validating/parsing of all the needed http requests. +%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% From 4d977535f200beccbb6a72f5a76ce99b5c34569d Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 09:35:37 +0300 Subject: [PATCH 28/75] Make some persistent data wrapper functions --- include/ejabberd_acme.hrl | 12 ++++++++++-- src/ejabberd_acme.erl | 29 +++++++++++++++++------------ 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index e696429b0..1164585e1 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -11,10 +11,18 @@ key :: jose_jwk:key() }). --record(data, { - account = none :: #data_acc{} | 'none' +-record(data_cert, { + domain :: list(), + pem :: binary() }). +-record(data, { + account = none :: #data_acc{} | 'none', + certs = [] :: [#data_cert{}] + }). + + + -type nonce() :: string(). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 209ac46c5..2b103d69f 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -55,7 +55,7 @@ get_certificates(CAUrl, HttpDir, NewAccountOpt) -> throw:Throw -> Throw; E:R -> - ?ERROR_MSG("Unknown ~p:~p", [E, R]), + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), {error, get_certificates} end. @@ -63,11 +63,8 @@ get_certificates(CAUrl, HttpDir, NewAccountOpt) -> [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | no_return(). get_certificates0(CAUrl, HttpDir, "old-account") -> - %% Read Persistent Data - {ok, Data} = read_persistent(), - %% Get the current account - {ok, _AccId, PrivateKey} = ensure_account_exists(Data), + {ok, _AccId, PrivateKey} = ensure_account_exists(), get_certificates1(CAUrl, HttpDir, PrivateKey); @@ -106,7 +103,7 @@ get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> throw:Throw -> Throw; E:R -> - ?ERROR_MSG("Unknown ~p:~p", [E, R]), + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), {error, DomainName, get_certificate} end. @@ -122,9 +119,7 @@ create_save_new_account(CAUrl) -> {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), %% Write Persistent Data - {ok, Data} = read_persistent(), - NewData = set_account_persistent(Data, {Id, PrivateKey}), - ok = write_persistent(NewData), + ok = write_account_persistent({Id, PrivateKey}), {ok, Id, PrivateKey}. @@ -211,11 +206,11 @@ create_new_certificate(CAUrl, DomainName, PrivateKey) -> throw({error, DomainName, certificate}) end. -ensure_account_exists(Data) -> - case get_account_persistent(Data) of +ensure_account_exists() -> + case read_account_persistent() of none -> ?ERROR_MSG("No existing account", []), - {error, no_old_account}; + throw({error, no_old_account}); {ok, AccId, PrivateKey} -> {ok, AccId, PrivateKey} end. @@ -461,6 +456,16 @@ set_account_persistent(Data = #data{}, {AccId, PrivateKey}) -> NewAcc = #data_acc{id = AccId, key = PrivateKey}, Data#data{account = NewAcc}. +write_account_persistent({AccId, PrivateKey}) -> + {ok, Data} = read_persistent(), + NewData = set_account_persistent(Data, {AccId, PrivateKey}), + ok = write_persistent(NewData). + +read_account_persistent() -> + {ok, Data} = read_persistent(), + get_account_persistent(Data). + + save_certificate({error, _, _} = Error) -> Error; save_certificate({ok, DomainName, Cert}) -> From 478a12637d01da27d1ee3c3c8e323bd615bca555 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 09:40:36 +0300 Subject: [PATCH 29/75] Separate the persistent data structure functions --- src/ejabberd_acme.erl | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 2b103d69f..029f13edd 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -394,6 +394,28 @@ not_before_not_after() -> is_error({error, _}) -> true; is_error(_) -> false. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Handle the persistent data structure +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +data_empty() -> + #data{}. + +data_get_account(#data{account = Account}) -> + case Account of + #data_acc{id = AccId, key = PrivateKey} -> + {ok, AccId, PrivateKey}; + none -> + none + end. + +data_set_account(Data = #data{}, {AccId, PrivateKey}) -> + NewAcc = #data_acc{id = AccId, key = PrivateKey}, + Data#data{account = NewAcc}. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Handle Config and Persistence Files @@ -414,7 +436,7 @@ read_persistent() -> {ok, binary_to_term(Binary)}; {error, enoent} -> create_persistent(), - {ok, #data{}}; + {ok, data_empty()}; {error, Reason} -> ?ERROR_MSG("Error: ~p reading acme data file", [Reason]), throw({error, Reason}) @@ -430,7 +452,7 @@ write_persistent(Data) -> end. create_persistent() -> - Binary = term_to_binary(#data{}), + Binary = term_to_binary(data_empty()), case file:write_file(persistent_file(), Binary) of ok -> case file:change_mode(persistent_file(), persistent_file_mode()) of @@ -444,27 +466,14 @@ create_persistent() -> throw({error, Reason}) end. -get_account_persistent(#data{account = Account}) -> - case Account of - #data_acc{id = AccId, key = PrivateKey} -> - {ok, AccId, PrivateKey}; - none -> - none - end. - -set_account_persistent(Data = #data{}, {AccId, PrivateKey}) -> - NewAcc = #data_acc{id = AccId, key = PrivateKey}, - Data#data{account = NewAcc}. - write_account_persistent({AccId, PrivateKey}) -> {ok, Data} = read_persistent(), - NewData = set_account_persistent(Data, {AccId, PrivateKey}), + NewData = data_set_account(Data, {AccId, PrivateKey}), ok = write_persistent(NewData). read_account_persistent() -> {ok, Data} = read_persistent(), - get_account_persistent(Data). - + data_get_account(Data). save_certificate({error, _, _} = Error) -> Error; From 9cf596c67b83eb690d8776ee500ade221af8e47d Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 09:59:38 +0300 Subject: [PATCH 30/75] Change the persistent data structure from a record to a proplist This is done so that possible future updates to the data structure don't break existing code. With this change it will be possible to update the data structure and keep the same old persistent data file, which will still have the expected list format but with more properties --- include/ejabberd_acme.hrl | 6 ------ src/ejabberd_acme.erl | 16 ++++++++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index 1164585e1..0fea5bdf5 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -16,12 +16,6 @@ pem :: binary() }). --record(data, { - account = none :: #data_acc{} | 'none', - certs = [] :: [#data_cert{}] - }). - - diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 029f13edd..5d0608c93 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -401,19 +401,19 @@ is_error(_) -> false. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% data_empty() -> - #data{}. + []. -data_get_account(#data{account = Account}) -> - case Account of - #data_acc{id = AccId, key = PrivateKey} -> +data_get_account(Data) -> + case lists:keyfind(account, 1, Data) of + {account, #data_acc{id = AccId, key = PrivateKey}} -> {ok, AccId, PrivateKey}; - none -> + false -> none end. -data_set_account(Data = #data{}, {AccId, PrivateKey}) -> - NewAcc = #data_acc{id = AccId, key = PrivateKey}, - Data#data{account = NewAcc}. +data_set_account(Data, {AccId, PrivateKey}) -> + NewAcc = {account, #data_acc{id = AccId, key = PrivateKey}}, + lists:keystore(account, 1, Data, NewAcc). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% From fa3108e6e2e1480915182ac6efb61a677092ef2b Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 10:42:09 +0300 Subject: [PATCH 31/75] Save acquired certificates in persistent storage --- include/ejabberd_acme.hrl | 4 -- src/ejabberd_acme.erl | 80 ++++++++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index 0fea5bdf5..cb711b272 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -11,10 +11,6 @@ key :: jose_jwk:key() }). --record(data_cert, { - domain :: list(), - pem :: binary() - }). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 5d0608c93..527cef13f 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -39,6 +39,7 @@ is_valid_account_opt("old-account") -> true; is_valid_account_opt("new-account") -> true; is_valid_account_opt(_) -> false. + %% %% Get Certificate %% @@ -403,6 +404,10 @@ is_error(_) -> false. data_empty() -> []. +%% +%% Account +%% + data_get_account(Data) -> case lists:keyfind(account, 1, Data) of {account, #data_acc{id = AccId, key = PrivateKey}} -> @@ -415,6 +420,27 @@ data_set_account(Data, {AccId, PrivateKey}) -> NewAcc = {account, #data_acc{id = AccId, key = PrivateKey}}, lists:keystore(account, 1, Data, NewAcc). +%% +%% Certificates +%% + +data_get_certificates(Data) -> + case lists:keyfind(certs, 1, Data) of + {certs, Certs} -> + {ok, Certs}; + false -> + {ok, []} + end. + +data_set_certificates(Data, NewCerts) -> + lists:keystore(certs, 1, Data, {certs, NewCerts}). + +%% ATM we preserve one certificate for each domain +data_add_certificate(Data, {Domain, PemCert}) -> + {ok, Certs} = data_get_certificates(Data), + NewCerts = lists:keystore(Domain, 1, Certs, {Domain, PemCert}), + data_set_certificates(Data, NewCerts). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -475,6 +501,15 @@ read_account_persistent() -> {ok, Data} = read_persistent(), data_get_account(Data). +read_certificates_persistent() -> + {ok, Data} = read_persistent(), + data_get_certificates(Data). + +add_certificate_persistent({Domain, PemCert}) -> + {ok, Data} = read_persistent(), + NewData = data_add_certificate(Data, {Domain, PemCert}), + ok = write_persistent(NewData). + save_certificate({error, _, _} = Error) -> Error; save_certificate({ok, DomainName, Cert}) -> @@ -482,22 +517,30 @@ save_certificate({ok, DomainName, Cert}) -> {ok, CertDir} = get_config_cert_dir(), DomainString = bitstring_to_list(DomainName), CertificateFile = filename:join([CertDir, DomainString ++ "_cert.pem"]), - case file:write_file(CertificateFile, Cert) of - ok -> - {ok, DomainName, saved}; - {error, Reason} -> - ?ERROR_MSG("Error: ~p saving certificate at file: ~p", - [Reason, CertificateFile]), - throw({error, DomainName, saving}) - end + %% TODO: At some point do the following using a Transaction so + %% that there is no certificate saved if it cannot be added in + %% certificate persistent storage + write_cert(CertificateFile, Cert, DomainName), + add_certificate_persistent({DomainName, Cert}), + {ok, DomainName, saved} catch throw:Throw -> Throw; E:R -> - ?ERROR_MSG("unknown ~p:~p", [E,R]), + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), {error, DomainName, saving} end. +write_cert(CertificateFile, Cert, DomainName) -> + case file:write_file(CertificateFile, Cert) of + ok -> + {ok, DomainName, saved}; + {error, Reason} -> + ?ERROR_MSG("Error: ~p saving certificate at file: ~p", + [Reason, CertificateFile]), + throw({error, DomainName, saving}) + end. + get_config_acme() -> case ejabberd_config:get_option(acme, undefined) of undefined -> @@ -535,6 +578,25 @@ get_config_cert_dir() -> {ok, CertDir} end. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Transaction Fun +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +transaction([{Fun, Rollback} | Rest]) -> + try + {ok, Result} = Fun(), + [Result | transaction(Rest)] + catch Type:Reason -> + Rollback(), + erlang:raise(Type, Reason, erlang:get_stacktrace()) + end; +transaction([Fun | Rest]) -> + % not every action require cleanup on error + transaction([{Fun, fun () -> ok end} | Rest]); +transaction([]) -> []. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Debugging Funcs -- They are only used for the development phase From 09c3496ff14102ff8de7621bd1cdca8e4640cfb3 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 10:48:57 +0300 Subject: [PATCH 32/75] Remove httpdir from some function arguments as we now use the built in ejabberd http server for authorizations --- src/acme_challenge.erl | 2 +- src/ejabberd_acme.erl | 36 ++++++++++++++++++------------------ src/ejabberd_admin.erl | 11 +++++------ 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index 2638e0ddf..081e10429 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -78,7 +78,7 @@ solve_challenge(ChallengeType, Challenges, Options) -> -spec solve_challenge1(acme_challenge(), {jose_jwk:key(), string()}) -> {ok, url(), bitstring()} | {error, _}. -solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, {Key, HttpDir}) -> +solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, Key) -> KeyAuthz = key_authorization(Tkn, Key), %% save_key_authorization(Chal, Tkn, KeyAuthz, HttpDir); ets_put_key_authorization(Tkn, KeyAuthz), diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 527cef13f..43d9eae29 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,7 +1,7 @@ -module (ejabberd_acme). -export([%% Ejabberdctl Commands - get_certificates/3, + get_certificates/2, %% Command Options Validity is_valid_account_opt/1, %% Misc @@ -45,13 +45,13 @@ is_valid_account_opt(_) -> false. %% %% Needs a hell lot of cleaning --spec get_certificates(url(), string(), account_opt()) -> +-spec get_certificates(url(), account_opt()) -> [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | {'error', _}. -get_certificates(CAUrl, HttpDir, NewAccountOpt) -> +get_certificates(CAUrl, NewAccountOpt) -> try ?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]), - get_certificates0(CAUrl, HttpDir, NewAccountOpt) + get_certificates0(CAUrl, NewAccountOpt) catch throw:Throw -> Throw; @@ -60,30 +60,30 @@ get_certificates(CAUrl, HttpDir, NewAccountOpt) -> {error, get_certificates} end. --spec get_certificates0(url(), string(), account_opt()) -> +-spec get_certificates0(url(), account_opt()) -> [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | no_return(). -get_certificates0(CAUrl, HttpDir, "old-account") -> +get_certificates0(CAUrl, "old-account") -> %% Get the current account {ok, _AccId, PrivateKey} = ensure_account_exists(), - get_certificates1(CAUrl, HttpDir, PrivateKey); + get_certificates1(CAUrl, PrivateKey); -get_certificates0(CAUrl, HttpDir, "new-account") -> +get_certificates0(CAUrl, "new-account") -> %% Create a new account and save it to disk {ok, _Id, PrivateKey} = create_save_new_account(CAUrl), - get_certificates1(CAUrl, HttpDir, PrivateKey). + get_certificates1(CAUrl, PrivateKey). --spec get_certificates1(url(), string(), jose_jwk:key()) -> +-spec get_certificates1(url(), jose_jwk:key()) -> [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | no_return(). -get_certificates1(CAUrl, HttpDir, PrivateKey) -> +get_certificates1(CAUrl, PrivateKey) -> %% Read Config {ok, Hosts} = get_config_hosts(), %% Get a certificate for each host - PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey, HttpDir) || Host <- Hosts], + PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts], %% Save Certificates SavedCerts = [save_certificate(Cert) || Cert <- PemCertKeys], @@ -92,13 +92,13 @@ get_certificates1(CAUrl, HttpDir, PrivateKey) -> %% Result SavedCerts. --spec get_certificate(url(), bitstring(), jose_jwk:key(), string()) -> +-spec get_certificate(url(), bitstring(), jose_jwk:key()) -> {'ok', bitstring(), pem_certificate()} | {'error', bitstring(), _}. -get_certificate(CAUrl, DomainName, PrivateKey, HttpDir) -> +get_certificate(CAUrl, DomainName, PrivateKey) -> ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), try - {ok, _Authz} = create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir), + {ok, _Authz} = create_new_authorization(CAUrl, DomainName, PrivateKey), create_new_certificate(CAUrl, DomainName, PrivateKey) catch throw:Throw -> @@ -147,9 +147,9 @@ create_new_account(CAUrl, Contact, PrivateKey) -> throw({error,create_new_account}) end. --spec create_new_authorization(url(), bitstring(), jose_jwk:key(), bitstring()) -> +-spec create_new_authorization(url(), bitstring(), jose_jwk:key()) -> {'ok', proplist()} | no_return(). -create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> +create_new_authorization(CAUrl, DomainName, PrivateKey) -> try {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), Req0 = [{<<"identifier">>, @@ -162,7 +162,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey, HttpDir) -> Challenges = get_challenges(Authz), {ok, ChallengeUrl, KeyAuthz} = - acme_challenge:solve_challenge(<<"http-01">>, Challenges, {PrivateKey, HttpDir}), + acme_challenge:solve_challenge(<<"http-01">>, Challenges, PrivateKey), {ok, ChallengeId} = location_to_id(ChallengeUrl), Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], {ok, _SolvedChallenge, _Nonce2} = ejabberd_acme_comm:complete_challenge( diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 9b8be03ee..2d255f1e1 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -45,7 +45,7 @@ %% Migration jabberd1.4 import_file/1, import_dir/1, %% Acme - get_certificate/2, + get_certificate/1, %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia @@ -248,9 +248,8 @@ get_commands_spec() -> #ejabberd_commands{name = get_certificate, tags = [acme], desc = "Gets a certificate for the specified domain", module = ?MODULE, function = get_certificate, - args_desc = ["Full path to the http serving directory", - "Whether to create a new account or use the existing one"], - args = [{dir, string}, {option, string}], + args_desc = ["Whether to create a new account or use the existing one"], + args = [{option, string}], result = {certificate, string}}, #ejabberd_commands{name = import_piefxis, tags = [mnesia], @@ -556,10 +555,10 @@ import_dir(Path) -> %%% Acme %%% -get_certificate(HttpDir, UseNewAccount) -> +get_certificate(UseNewAccount) -> case ejabberd_acme:is_valid_account_opt(UseNewAccount) of true -> - ejabberd_acme:get_certificates("http://localhost:4000", HttpDir, UseNewAccount); + ejabberd_acme:get_certificates("http://localhost:4000", UseNewAccount); false -> String = io_lib:format("Invalid account option: ~p", [UseNewAccount]), {invalid_option, String} From 8fe551cc68ac7ddce26c4f33e5db36fbd98a1590 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 11:39:27 +0300 Subject: [PATCH 33/75] Add a stub for the list-certificates command --- src/ejabberd_acme.erl | 19 +++++++++++++++++++ src/ejabberd_admin.erl | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 43d9eae29..25ae0ce10 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -2,8 +2,10 @@ -export([%% Ejabberdctl Commands get_certificates/2, + list_certificates/1, %% Command Options Validity is_valid_account_opt/1, + is_valid_verbose_opt/1, %% Misc generate_key/0, %% Debugging Scenarios @@ -39,6 +41,23 @@ is_valid_account_opt("old-account") -> true; is_valid_account_opt("new-account") -> true; is_valid_account_opt(_) -> false. +-spec is_valid_verbose_opt(string()) -> boolean(). +is_valid_verbose_opt("plain") -> true; +is_valid_verbose_opt("verbose") -> true; +is_valid_verbose_opt(_) -> false. + +%% +%% List Certificates +%% + +list_certificates(Verbose) -> + {ok, Certs} = read_certificates_persistent(), + case Verbose of + "plain" -> + [{Domain, certificate} || {Domain, _Cert} <- Certs]; + "verbose" -> + Certs + end. %% %% Get Certificate diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 2d255f1e1..c3e7f5982 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -46,6 +46,7 @@ import_file/1, import_dir/1, %% Acme get_certificate/1, + list_certificates/1, %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia @@ -251,6 +252,15 @@ get_commands_spec() -> args_desc = ["Whether to create a new account or use the existing one"], args = [{option, string}], result = {certificate, string}}, + #ejabberd_commands{name = list_certificates, tags = [acme], + desc = "Lists all curently handled certificates and their respective domains", + module = ?MODULE, function = list_certificates, + args_desc = ["Whether to print the whole certificate or just some metadata. Possible values: plain | verbose"], + args = [{option, string}], + result = {certificates, {list, + {certificate, {tuple, + [{domain, string}, + {cert, string}]}}}}}, #ejabberd_commands{name = import_piefxis, tags = [mnesia], desc = "Import users data from a PIEFXIS file (XEP-0227)", @@ -564,6 +574,16 @@ get_certificate(UseNewAccount) -> {invalid_option, String} end. +list_certificates(Verbose) -> + case ejabberd_acme:is_valid_verbose_opt(Verbose) of + true -> + ejabberd_acme:list_certificates(Verbose); + false -> + String = io_lib:format("Invalid verbose option: ~p", [Verbose]), + {invalid_option, String} + end. + + %%% %%% Purge DB %%% From 2e18122cd908290c0d4f9819cbf14ba9abf639be Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Mon, 17 Jul 2017 13:40:53 +0300 Subject: [PATCH 34/75] Print validity in list-certificates --- src/ejabberd_acme.erl | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 25ae0ce10..a70321c74 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -54,11 +54,40 @@ list_certificates(Verbose) -> {ok, Certs} = read_certificates_persistent(), case Verbose of "plain" -> - [{Domain, certificate} || {Domain, _Cert} <- Certs]; + [{Domain, certificate_metadata(PemCert)} || {Domain, PemCert} <- Certs]; "verbose" -> Certs end. +%% TODO: Make this cleaner and more secure +certificate_metadata(PemCert) -> + PemList = public_key:pem_decode(PemCert), + PemEntryCert = lists:keyfind('Certificate', 1, PemList), + #'Certificate'{tbsCertificate = #'TBSCertificate'{ + subject = {rdnSequence, SubjectList}, + validity = Validity}} + = public_key:pem_entry_decode(PemEntryCert), + + %% Find the commonName + %% TODO: Not the best way to find the commonName + ?INFO_MSG("Subject List: ~p", [SubjectList]), + ShallowSubjectList = [Attribute || [Attribute] <- SubjectList], + {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList), + + %% Find the notAfter date + %% TODO: Find a library function to decode utc time + #'Validity'{notAfter = {utcTime, UtcTime}} = Validity, + [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime, + YEAR = case list_to_integer([Y1,Y2]) >= 50 of + true -> "19" ++ [Y1,Y2]; + _ -> "20" ++ [Y1,Y2] + end, + NotAfter = lists:flatten(io_lib:format("Valid until: ~s-~s-~s ~s:~s:~s", + [YEAR, [MO1,MO2], [D1,D2], + [H1,H2], [MI1,MI2], [S1,S2]])), + + NotAfter. + %% %% Get Certificate %% @@ -704,8 +733,7 @@ new_user_scenario(CAUrl, HttpDir) -> {ok, Authz3, Nonce7} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), #{"new-cert" := NewCert} = Dirs, - CSRSubject = [{commonName, bitstring_to_list(DomainName)}, - {organizationName, "Example Corp"}], + CSRSubject = [{commonName, bitstring_to_list(DomainName)}], {CSR, CSRKey} = make_csr(CSRSubject), {MegS, Sec, MicS} = erlang:timestamp(), NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), From 9ce1f12b66b35070ad2b2e9bf745e455f96c79b9 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 18 Jul 2017 13:28:44 +0300 Subject: [PATCH 35/75] Pretty print list-certificates --- include/ejabberd_acme.hrl | 5 +++ src/ejabberd_acme.erl | 71 +++++++++++++++++++++++++++++---------- src/ejabberd_admin.erl | 5 +-- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index cb711b272..8e8e558a0 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -11,6 +11,11 @@ key :: jose_jwk:key() }). +-record(data_cert, { + domain :: list(), + pem :: jose_jwk:key(), + path :: file:filename() + }). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index a70321c74..3ba318bda 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -54,27 +54,56 @@ list_certificates(Verbose) -> {ok, Certs} = read_certificates_persistent(), case Verbose of "plain" -> - [{Domain, certificate_metadata(PemCert)} || {Domain, PemCert} <- Certs]; + [format_certificate(DataCert) || {_Key, DataCert} <- Certs]; "verbose" -> Certs end. -%% TODO: Make this cleaner and more secure -certificate_metadata(PemCert) -> +%% TODO: Make this cleaner and more robust +format_certificate(DataCert) -> + #data_cert{ + domain = DomainName, + pem = PemCert, + path = Path + } = DataCert, + PemList = public_key:pem_decode(PemCert), PemEntryCert = lists:keyfind('Certificate', 1, PemList), - #'Certificate'{tbsCertificate = #'TBSCertificate'{ - subject = {rdnSequence, SubjectList}, - validity = Validity}} - = public_key:pem_entry_decode(PemEntryCert), - + Certificate = public_key:pem_entry_decode(PemEntryCert), + %% Find the commonName - %% TODO: Not the best way to find the commonName - ?INFO_MSG("Subject List: ~p", [SubjectList]), - ShallowSubjectList = [Attribute || [Attribute] <- SubjectList], - {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList), + _CommonName = get_commonName(Certificate), %% Find the notAfter date + NotAfter = get_notAfter(Certificate), + + format_certificate1(DomainName, NotAfter, Path). + +format_certificate1(DomainName, NotAfter, Path) -> + Result = lists:flatten(io_lib:format( + " Domain: ~s~n" + " Valid until: ~s UTC~n" + " Path: ~s", + [DomainName, NotAfter, Path])), + Result. + +get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> + #'TBSCertificate'{ + subject = {rdnSequence, SubjectList} + } = TbsCertificate, + + %% TODO: Not the best way to find the commonName + ShallowSubjectList = [Attribute || [Attribute] <- SubjectList], + {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList), + + %% TODO: Remove the length-encoding from the commonName before returning it + CommonName. + +get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) -> + #'TBSCertificate'{ + validity = Validity + } = TbsCertificate, + %% TODO: Find a library function to decode utc time #'Validity'{notAfter = {utcTime, UtcTime}} = Validity, [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime, @@ -82,7 +111,7 @@ certificate_metadata(PemCert) -> true -> "19" ++ [Y1,Y2]; _ -> "20" ++ [Y1,Y2] end, - NotAfter = lists:flatten(io_lib:format("Valid until: ~s-~s-~s ~s:~s:~s", + NotAfter = lists:flatten(io_lib:format("~s-~s-~s ~s:~s:~s", [YEAR, [MO1,MO2], [D1,D2], [H1,H2], [MI1,MI2], [S1,S2]])), @@ -484,9 +513,9 @@ data_set_certificates(Data, NewCerts) -> lists:keystore(certs, 1, Data, {certs, NewCerts}). %% ATM we preserve one certificate for each domain -data_add_certificate(Data, {Domain, PemCert}) -> +data_add_certificate(Data, DataCert = #data_cert{domain=Domain}) -> {ok, Certs} = data_get_certificates(Data), - NewCerts = lists:keystore(Domain, 1, Certs, {Domain, PemCert}), + NewCerts = lists:keystore(Domain, 1, Certs, {Domain, DataCert}), data_set_certificates(Data, NewCerts). @@ -553,11 +582,12 @@ read_certificates_persistent() -> {ok, Data} = read_persistent(), data_get_certificates(Data). -add_certificate_persistent({Domain, PemCert}) -> +add_certificate_persistent(DataCert) -> {ok, Data} = read_persistent(), - NewData = data_add_certificate(Data, {Domain, PemCert}), + NewData = data_add_certificate(Data, DataCert), ok = write_persistent(NewData). + save_certificate({error, _, _} = Error) -> Error; save_certificate({ok, DomainName, Cert}) -> @@ -569,7 +599,12 @@ save_certificate({ok, DomainName, Cert}) -> %% that there is no certificate saved if it cannot be added in %% certificate persistent storage write_cert(CertificateFile, Cert, DomainName), - add_certificate_persistent({DomainName, Cert}), + DataCert = #data_cert{ + domain = DomainName, + pem = Cert, + path = CertificateFile + }, + add_certificate_persistent(DataCert), {ok, DomainName, saved} catch throw:Throw -> diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index c3e7f5982..af0cb978b 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -257,10 +257,7 @@ get_commands_spec() -> module = ?MODULE, function = list_certificates, args_desc = ["Whether to print the whole certificate or just some metadata. Possible values: plain | verbose"], args = [{option, string}], - result = {certificates, {list, - {certificate, {tuple, - [{domain, string}, - {cert, string}]}}}}}, + result = {certificates, {list,{certificate, string}}}}, #ejabberd_commands{name = import_piefxis, tags = [mnesia], desc = "Import users data from a PIEFXIS file (XEP-0227)", From 09918b59128b2ba8eeeb2ec67f2adf3a49cc418d Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sun, 23 Jul 2017 21:47:22 +0300 Subject: [PATCH 36/75] Add a try catch arounf list certificates --- src/ejabberd_acme.erl | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 3ba318bda..9ce4bc268 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -51,6 +51,17 @@ is_valid_verbose_opt(_) -> false. %% list_certificates(Verbose) -> + try + list_certificates0(Verbose) + catch + throw:Throw -> + Throw; + E:R -> + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), + {error, get_certificates} + end. + +list_certificates0(Verbose) -> {ok, Certs} = read_certificates_persistent(), case Verbose of "plain" -> From 92e38190aa02f03f05a303147d3cd88a7b48363e Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 25 Jul 2017 14:13:40 +0300 Subject: [PATCH 37/75] Encode strings using a library function and not my custom made --- src/ejabberd_acme.erl | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 9ce4bc268..cb7b6525e 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -230,7 +230,7 @@ create_new_account(CAUrl, Contact, PrivateKey) -> {ok, AccId} catch E:R -> - ?ERROR_MSG("Error: ~p creating an account for contact", + ?ERROR_MSG("Error: ~p creating an account for contact: ~p", [{E,R}, Contact]), throw({error,create_new_account}) end. @@ -403,19 +403,6 @@ der_encode(Type, Term) -> {error, der_encode} end. -%% TODO: I haven't found a function that does that, but there must exist one -length_bitstring(Bitstring) -> - Size = byte_size(Bitstring), - case Size =< 127 of - true -> - <<12:8, Size:8, Bitstring/binary>>; - false -> - LenOctets = binary:encode_unsigned(Size), - FirstOctet = byte_size(LenOctets), - <<12:8, 1:1, FirstOctet:7, Size:(FirstOctet * 8), Bitstring/binary>> - end. - - %% %% Attributes Parser %% @@ -433,7 +420,9 @@ attribute_parser_fun({AttrName, AttrVal}) -> try #'AttributeTypeAndValue'{ type = attribute_oid(AttrName), - value = length_bitstring(list_to_bitstring(AttrVal)) + %% TODO: Check if every attribute should be encoded as common name + value = public_key:der_encode('X520CommonName', {printableString, AttrVal}) + %% value = length_bitstring(list_to_bitstring(AttrVal)) } catch _:_ -> @@ -819,9 +808,17 @@ new_user_scenario(CAUrl, HttpDir) -> {Account2, Authz3, CSR, Certificate, PrivateKey}. - +-ifdef(GENERATE_RSA_KEY). generate_key() -> + ?INFO_MSG("Generate RSA key pair~n", []), + Key = public_key:generate_key({rsa, 2048, 65537}), + jose_jwk:from_key(Key). +-else. +generate_key() -> + ?INFO_MSG("Generate EC key pair~n", []), jose_jwk:generate_key({ec, secp256r1}). +-endif. + scenario3() -> CSRSubject = [{commonName, "my-acme-test-ejabberd.com"}, From 1a506da9323fb2740daafcf8638945f08e389529 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 26 Jul 2017 09:52:44 +0300 Subject: [PATCH 38/75] Add an erl_opt so that rsa can be used when the otp version is enough --- rebar.config | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rebar.config b/rebar.config index b6afda498..2e0220633 100644 --- a/rebar.config +++ b/rebar.config @@ -93,6 +93,8 @@ {if_have_fun, {crypto, strong_rand_bytes, 1}, {d, 'STRONG_RAND_BYTES'}}, {if_have_fun, {gb_sets, iterator_from, 2}, {d, 'GB_SETS_ITERATOR_FROM'}}, {if_have_fun, {public_key, short_name_hash, 1}, {d, 'SHORT_NAME_HASH'}}, + %% {if_have_fun, {public_key, generate_key, 1}, {d, 'GENERATE_RSA_KEY'}}, + {if_version_above, "19", {d, 'GENERATE_RSA_KEY'}}, {if_var_true, hipe, native}, {src_dirs, [asn1, src, {if_var_true, tools, tools}, From cc6f4b90fb6cfd317cf65113fc15386df7606364 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 27 Jul 2017 18:25:44 +0300 Subject: [PATCH 39/75] Support certificate revocation --- include/ejabberd_acme.hrl | 6 ++-- src/ejabberd_acme.erl | 72 ++++++++++++++++++++++++++++++++++++--- src/ejabberd_admin.erl | 9 +++++ 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index 8e8e558a0..ef6afe601 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -12,9 +12,9 @@ }). -record(data_cert, { - domain :: list(), - pem :: jose_jwk:key(), - path :: file:filename() + domain :: bitstring(), + pem :: bitstring(), + path :: bitstring() }). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index cb7b6525e..db959e5ea 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -3,11 +3,13 @@ -export([%% Ejabberdctl Commands get_certificates/2, list_certificates/1, + revoke_certificate/2, %% Command Options Validity is_valid_account_opt/1, is_valid_verbose_opt/1, %% Misc generate_key/0, + to_public/1, %% Debugging Scenarios scenario/3, scenario0/2, @@ -46,6 +48,48 @@ is_valid_verbose_opt("plain") -> true; is_valid_verbose_opt("verbose") -> true; is_valid_verbose_opt(_) -> false. +%% +%% Revoke Certificate +%% + +%% Add a try-catch to this stub +revoke_certificate(CAUrl, Domain) -> + revoke_certificate0(CAUrl, Domain). + +revoke_certificate0(CAUrl, Domain) -> + BinDomain = list_to_bitstring(Domain), + case domain_certificate_exists(BinDomain) of + {BinDomain, Certificate} -> + ?INFO_MSG("Certificate: ~p found!!", [Certificate]), + ok = revoke_certificate1(CAUrl, Certificate), + {ok, deleted}; + false -> + {error, not_found} + end. + +revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) -> + {ok, _AccId, PrivateKey} = ensure_account_exists(), + + Certificate = prepare_certificate_revoke(PemEncodedCert), + + {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), + + Req = [{<<"certificate">>, Certificate}], + {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce), + ok. + +prepare_certificate_revoke(PemEncodedCert) -> + PemList = public_key:pem_decode(PemEncodedCert), + PemCertEnc = lists:keyfind('Certificate', 1, PemList), + PemCert = public_key:pem_entry_decode(PemCertEnc), + DerCert = public_key:der_encode('Certificate', PemCert), + Base64Cert = base64url:encode(DerCert), + Base64Cert. + +domain_certificate_exists(Domain) -> + {ok, Certs} = read_certificates_persistent(), + lists:keyfind(Domain, 1, Certs). + %% %% List Certificates %% @@ -58,7 +102,7 @@ list_certificates(Verbose) -> Throw; E:R -> ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), - {error, get_certificates} + {error, list_certificates} end. list_certificates0(Verbose) -> @@ -321,7 +365,7 @@ ensure_account_exists() -> make_csr(Attributes) -> Key = generate_key(), {_, KeyKey} = jose_jwk:to_key(Key), - KeyPub = jose_jwk:to_public(Key), + KeyPub = to_public(Key), try SubPKInfoAlgo = subject_pk_info_algo(KeyPub), {ok, RawBinPubKey} = raw_binary_public_key(KeyPub), @@ -420,7 +464,9 @@ attribute_parser_fun({AttrName, AttrVal}) -> try #'AttributeTypeAndValue'{ type = attribute_oid(AttrName), - %% TODO: Check if every attribute should be encoded as common name + %% TODO: Check if every attribute should be encoded as + %% common name. Actually it doesn't matter in + %% practice. Only in theory in order to have cleaner code. value = public_key:der_encode('X520CommonName', {printableString, AttrVal}) %% value = length_bitstring(list_to_bitstring(AttrVal)) } @@ -469,6 +515,22 @@ not_before_not_after() -> NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), {NotBefore, NotAfter}. +to_public(PrivateKey) -> + jose_jwk:to_public(PrivateKey). + %% case jose_jwk:to_key(PrivateKey) of + %% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} -> + %% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}, + %% jose_jwk:from_key(Public); + %% _ -> + %% jose_jwk:to_public(PrivateKey) + %% end. + +%% to_public(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) -> +%% #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}; +%% to_public(PrivateKey) -> +%% jose_jwk:to_public(PrivateKey). + + is_error({error, _}) -> true; is_error(_) -> false. @@ -812,7 +874,9 @@ new_user_scenario(CAUrl, HttpDir) -> generate_key() -> ?INFO_MSG("Generate RSA key pair~n", []), Key = public_key:generate_key({rsa, 2048, 65537}), - jose_jwk:from_key(Key). + Key1 = Key#'RSAPrivateKey'{version = 'two-prime'}, + jose_jwk:from_key(Key1). + %% jose_jwk:generate_key({rsa, 2048}). -else. generate_key() -> ?INFO_MSG("Generate EC key pair~n", []), diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index af0cb978b..c68a08315 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -47,6 +47,7 @@ %% Acme get_certificate/1, list_certificates/1, + revoke_certificate/1, %% Purge DB delete_expired_messages/0, delete_old_messages/1, %% Mnesia @@ -258,6 +259,12 @@ get_commands_spec() -> args_desc = ["Whether to print the whole certificate or just some metadata. Possible values: plain | verbose"], args = [{option, string}], result = {certificates, {list,{certificate, string}}}}, + #ejabberd_commands{name = revoke_certificate, tags = [acme], + desc = "Revokes the selected certificate", + module = ?MODULE, function = revoke_certificate, + args_desc = ["The domain of the certificate in question"], + args = [{domain, string}], + result = {res, restuple}}, #ejabberd_commands{name = import_piefxis, tags = [mnesia], desc = "Import users data from a PIEFXIS file (XEP-0227)", @@ -580,6 +587,8 @@ list_certificates(Verbose) -> {invalid_option, String} end. +revoke_certificate(Domain) -> + ejabberd_acme:revoke_certificate("http://localhost:4000", Domain). %%% %%% Purge DB From 3abe3aeeecc8043e61a8068d535120d274c68ac2 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 29 Jul 2017 19:10:06 +0300 Subject: [PATCH 40/75] Finish revoke_certificate and add specs 1. Add a try catch in the final revoke_certificate function 2. Also delete the certificate from persistent memory when it is done revoked --- include/ejabberd_acme.hrl | 21 ++- src/ejabberd_acme.erl | 341 ++++++++++++++++++++++---------------- 2 files changed, 219 insertions(+), 143 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index ef6afe601..4ef3bedbe 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -10,25 +10,40 @@ id :: list(), key :: jose_jwk:key() }). +-type data_acc() :: #data_acc{}. -record(data_cert, { domain :: bitstring(), - pem :: bitstring(), - path :: bitstring() + pem :: pem(), + path :: string() }). +-type data_cert() :: #data_cert{}. +%% +%% Types +%% +%% The main data type that ejabberd_acme keeps +-type acme_data() :: proplist(). +%% The list of certificates kept in data +-type data_certs() :: proplist(bitstring(), data_cert()). + +%% The certificate saved in pem format +-type pem() :: bitstring(). -type nonce() :: string(). -type url() :: string(). -type proplist() :: [{_, _}]. +-type proplist(X,Y) :: [{X,Y}]. -type dirs() :: #{string() => url()}. -type jws() :: map(). -type handle_resp_fun() :: fun(({ok, proplist(), proplist()}) -> {ok, _, nonce()}). -type acme_challenge() :: #challenge{}. +%% Options -type account_opt() :: string(). +-type verbose_opt() :: string(). + --type pem_certificate() :: bitstring(). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index db959e5ea..5cb5857d4 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -48,129 +48,7 @@ is_valid_verbose_opt("plain") -> true; is_valid_verbose_opt("verbose") -> true; is_valid_verbose_opt(_) -> false. -%% -%% Revoke Certificate -%% -%% Add a try-catch to this stub -revoke_certificate(CAUrl, Domain) -> - revoke_certificate0(CAUrl, Domain). - -revoke_certificate0(CAUrl, Domain) -> - BinDomain = list_to_bitstring(Domain), - case domain_certificate_exists(BinDomain) of - {BinDomain, Certificate} -> - ?INFO_MSG("Certificate: ~p found!!", [Certificate]), - ok = revoke_certificate1(CAUrl, Certificate), - {ok, deleted}; - false -> - {error, not_found} - end. - -revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) -> - {ok, _AccId, PrivateKey} = ensure_account_exists(), - - Certificate = prepare_certificate_revoke(PemEncodedCert), - - {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), - - Req = [{<<"certificate">>, Certificate}], - {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce), - ok. - -prepare_certificate_revoke(PemEncodedCert) -> - PemList = public_key:pem_decode(PemEncodedCert), - PemCertEnc = lists:keyfind('Certificate', 1, PemList), - PemCert = public_key:pem_entry_decode(PemCertEnc), - DerCert = public_key:der_encode('Certificate', PemCert), - Base64Cert = base64url:encode(DerCert), - Base64Cert. - -domain_certificate_exists(Domain) -> - {ok, Certs} = read_certificates_persistent(), - lists:keyfind(Domain, 1, Certs). - -%% -%% List Certificates -%% - -list_certificates(Verbose) -> - try - list_certificates0(Verbose) - catch - throw:Throw -> - Throw; - E:R -> - ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), - {error, list_certificates} - end. - -list_certificates0(Verbose) -> - {ok, Certs} = read_certificates_persistent(), - case Verbose of - "plain" -> - [format_certificate(DataCert) || {_Key, DataCert} <- Certs]; - "verbose" -> - Certs - end. - -%% TODO: Make this cleaner and more robust -format_certificate(DataCert) -> - #data_cert{ - domain = DomainName, - pem = PemCert, - path = Path - } = DataCert, - - PemList = public_key:pem_decode(PemCert), - PemEntryCert = lists:keyfind('Certificate', 1, PemList), - Certificate = public_key:pem_entry_decode(PemEntryCert), - - %% Find the commonName - _CommonName = get_commonName(Certificate), - - %% Find the notAfter date - NotAfter = get_notAfter(Certificate), - - format_certificate1(DomainName, NotAfter, Path). - -format_certificate1(DomainName, NotAfter, Path) -> - Result = lists:flatten(io_lib:format( - " Domain: ~s~n" - " Valid until: ~s UTC~n" - " Path: ~s", - [DomainName, NotAfter, Path])), - Result. - -get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> - #'TBSCertificate'{ - subject = {rdnSequence, SubjectList} - } = TbsCertificate, - - %% TODO: Not the best way to find the commonName - ShallowSubjectList = [Attribute || [Attribute] <- SubjectList], - {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList), - - %% TODO: Remove the length-encoding from the commonName before returning it - CommonName. - -get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) -> - #'TBSCertificate'{ - validity = Validity - } = TbsCertificate, - - %% TODO: Find a library function to decode utc time - #'Validity'{notAfter = {utcTime, UtcTime}} = Validity, - [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime, - YEAR = case list_to_integer([Y1,Y2]) >= 50 of - true -> "19" ++ [Y1,Y2]; - _ -> "20" ++ [Y1,Y2] - end, - NotAfter = lists:flatten(io_lib:format("~s-~s-~s ~s:~s:~s", - [YEAR, [MO1,MO2], [D1,D2], - [H1,H2], [MI1,MI2], [S1,S2]])), - - NotAfter. %% %% Get Certificate @@ -212,7 +90,7 @@ get_certificates0(CAUrl, "new-account") -> no_return(). get_certificates1(CAUrl, PrivateKey) -> %% Read Config - {ok, Hosts} = get_config_hosts(), + Hosts = get_config_hosts(), %% Get a certificate for each host PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts], @@ -225,7 +103,7 @@ get_certificates1(CAUrl, PrivateKey) -> SavedCerts. -spec get_certificate(url(), bitstring(), jose_jwk:key()) -> - {'ok', bitstring(), pem_certificate()} | + {'ok', bitstring(), pem()} | {'error', bitstring(), _}. get_certificate(CAUrl, DomainName, PrivateKey) -> ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), @@ -243,7 +121,7 @@ get_certificate(CAUrl, DomainName, PrivateKey) -> -spec create_save_new_account(url()) -> {'ok', string(), jose_jwk:key()} | no_return(). create_save_new_account(CAUrl) -> %% Get contact from configuration file - {ok, Contact} = get_config_contact(), + Contact = get_config_contact(), %% Generate a Key PrivateKey = generate_key(), @@ -309,6 +187,8 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) -> throw({error, DomainName, authorization}) end. +-spec create_new_certificate(url(), bitstring(), jose_jwk:key()) -> + {ok, bitstring(), pem()}. create_new_certificate(CAUrl, DomainName, PrivateKey) -> try {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), @@ -339,6 +219,7 @@ create_new_certificate(CAUrl, DomainName, PrivateKey) -> throw({error, DomainName, certificate}) end. +-spec ensure_account_exists() -> {ok, string(), jose_jwk:key()}. ensure_account_exists() -> case read_account_persistent() of none -> @@ -348,6 +229,152 @@ ensure_account_exists() -> {ok, AccId, PrivateKey} end. + +%% +%% List Certificates +%% +-spec list_certificates(verbose_opt()) -> [string()] | [any()] | {error, _}. +list_certificates(Verbose) -> + try + list_certificates0(Verbose) + catch + throw:Throw -> + Throw; + E:R -> + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), + {error, list_certificates} + end. + +-spec list_certificates0(verbose_opt()) -> [string()] | [any()]. +list_certificates0(Verbose) -> + Certs = read_certificates_persistent(), + case Verbose of + "plain" -> + [format_certificate(DataCert) || {_Key, DataCert} <- Certs]; + "verbose" -> + Certs + end. + +%% TODO: Make this cleaner and more robust +-spec format_certificate(data_cert()) -> string(). +format_certificate(DataCert) -> + #data_cert{ + domain = DomainName, + pem = PemCert, + path = Path + } = DataCert, + + PemList = public_key:pem_decode(PemCert), + PemEntryCert = lists:keyfind('Certificate', 1, PemList), + Certificate = public_key:pem_entry_decode(PemEntryCert), + + %% Find the commonName + _CommonName = get_commonName(Certificate), + + %% Find the notAfter date + NotAfter = get_notAfter(Certificate), + + format_certificate1(DomainName, NotAfter, Path). + +-spec format_certificate1(bitstring(), string(), string()) -> string(). +format_certificate1(DomainName, NotAfter, Path) -> + Result = lists:flatten(io_lib:format( + " Domain: ~s~n" + " Valid until: ~s UTC~n" + " Path: ~s", + [DomainName, NotAfter, Path])), + Result. + +-spec get_commonName(#'Certificate'{}) -> string(). +get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> + #'TBSCertificate'{ + subject = {rdnSequence, SubjectList} + } = TbsCertificate, + + %% TODO: Not the best way to find the commonName + ShallowSubjectList = [Attribute || [Attribute] <- SubjectList], + {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList), + + %% TODO: Remove the length-encoding from the commonName before returning it + CommonName. + +-spec get_notAfter(#'Certificate'{}) -> string(). +get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) -> + #'TBSCertificate'{ + validity = Validity + } = TbsCertificate, + + %% TODO: Find a library function to decode utc time + #'Validity'{notAfter = {utcTime, UtcTime}} = Validity, + [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime, + YEAR = case list_to_integer([Y1,Y2]) >= 50 of + true -> "19" ++ [Y1,Y2]; + _ -> "20" ++ [Y1,Y2] + end, + NotAfter = lists:flatten(io_lib:format("~s-~s-~s ~s:~s:~s", + [YEAR, [MO1,MO2], [D1,D2], + [H1,H2], [MI1,MI2], [S1,S2]])), + + NotAfter. + + +%% +%% Revoke Certificate +%% + +%% Add a try-catch to this stub +-spec revoke_certificate(url(), string()) -> {ok, deleted} | {error, _}. +revoke_certificate(CAUrl, Domain) -> + try + revoke_certificate0(CAUrl, Domain) + catch + throw:Throw -> + Throw; + E:R -> + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), + {error, revoke_certificate} + end. + +-spec revoke_certificate0(url(), string()) -> {ok, deleted} | {error, not_found}. +revoke_certificate0(CAUrl, Domain) -> + BinDomain = list_to_bitstring(Domain), + case domain_certificate_exists(BinDomain) of + {BinDomain, Certificate} -> + ?INFO_MSG("Certificate: ~p found!!", [Certificate]), + ok = revoke_certificate1(CAUrl, Certificate), + {ok, deleted}; + false -> + {error, not_found} + end. + +-spec revoke_certificate1(url(), data_cert()) -> ok. +revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) -> + {ok, _AccId, PrivateKey} = ensure_account_exists(), + + Certificate = prepare_certificate_revoke(PemEncodedCert), + + {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), + + Req = [{<<"certificate">>, Certificate}], + {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce), + ok = remove_certificate_persistent(Cert), + ok. + +-spec prepare_certificate_revoke(pem()) -> bitstring(). +prepare_certificate_revoke(PemEncodedCert) -> + PemList = public_key:pem_decode(PemEncodedCert), + PemCertEnc = lists:keyfind('Certificate', 1, PemList), + PemCert = public_key:pem_entry_decode(PemCertEnc), + DerCert = public_key:der_encode('Certificate', PemCert), + Base64Cert = base64url:encode(DerCert), + Base64Cert. + +-spec domain_certificate_exists(bitstring()) -> {bitstring(), data_cert()} | false. +domain_certificate_exists(Domain) -> + Certs = read_certificates_persistent(), + lists:keyfind(Domain, 1, Certs). + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Certificate Request Functions @@ -508,13 +535,16 @@ get_challenges(Body) -> {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), Challenges. +-spec not_before_not_after() -> {binary(), binary()}. not_before_not_after() -> - %% TODO: Make notBefore and notAfter like they do it in other clients + %% TODO: Make notBefore and notAfter configurable somewhere {MegS, Sec, MicS} = erlang:timestamp(), - NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), - NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), + NotBefore = xmpp_util:encode_timestamp({MegS, Sec, MicS}), + %% The certificate will be valid for 60 Days after today + NotAfter = xmpp_util:encode_timestamp({MegS+5, Sec+184000, MicS}), {NotBefore, NotAfter}. +-spec to_public(jose_jwk:key()) -> jose_jwk:key(). to_public(PrivateKey) -> jose_jwk:to_public(PrivateKey). %% case jose_jwk:to_key(PrivateKey) of @@ -530,7 +560,7 @@ to_public(PrivateKey) -> %% to_public(PrivateKey) -> %% jose_jwk:to_public(PrivateKey). - +-spec is_error(_) -> boolean(). is_error({error, _}) -> true; is_error(_) -> false. @@ -539,7 +569,7 @@ is_error(_) -> false. %% Handle the persistent data structure %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - +-spec data_empty() -> []. data_empty() -> []. @@ -547,6 +577,7 @@ data_empty() -> %% Account %% +-spec data_get_account(acme_data()) -> {ok, list(), jose_jwk:key()} | none. data_get_account(Data) -> case lists:keyfind(account, 1, Data) of {account, #data_acc{id = AccId, key = PrivateKey}} -> @@ -555,6 +586,7 @@ data_get_account(Data) -> none end. +-spec data_set_account(acme_data(), {list(), jose_jwk:key()}) -> acme_data(). data_set_account(Data, {AccId, PrivateKey}) -> NewAcc = {account, #data_acc{id = AccId, key = PrivateKey}}, lists:keystore(account, 1, Data, NewAcc). @@ -563,23 +595,32 @@ data_set_account(Data, {AccId, PrivateKey}) -> %% Certificates %% +-spec data_get_certificates(acme_data()) -> data_certs(). data_get_certificates(Data) -> case lists:keyfind(certs, 1, Data) of {certs, Certs} -> - {ok, Certs}; + Certs; false -> - {ok, []} + [] end. +-spec data_set_certificates(acme_data(), data_certs()) -> acme_data(). data_set_certificates(Data, NewCerts) -> lists:keystore(certs, 1, Data, {certs, NewCerts}). %% ATM we preserve one certificate for each domain +-spec data_add_certificate(acme_data(), data_cert()) -> acme_data(). data_add_certificate(Data, DataCert = #data_cert{domain=Domain}) -> - {ok, Certs} = data_get_certificates(Data), + Certs = data_get_certificates(Data), NewCerts = lists:keystore(Domain, 1, Certs, {Domain, DataCert}), data_set_certificates(Data, NewCerts). +-spec data_remove_certificate(acme_data(), data_cert()) -> acme_data(). +data_remove_certificate(Data, DataCert = #data_cert{domain=Domain}) -> + Certs = data_get_certificates(Data), + NewCerts = lists:keydelete(Domain, 1, Certs), + data_set_certificates(Data, NewCerts). + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -587,14 +628,17 @@ data_add_certificate(Data, DataCert = #data_cert{domain=Domain}) -> %% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +-spec persistent_file() -> file:filename(). persistent_file() -> MnesiaDir = mnesia:system_info(directory), filename:join(MnesiaDir, "acme.DAT"). -%% The persistent file should be rread and written only by its owner +%% The persistent file should be read and written only by its owner +-spec persistent_file_mode() -> 384. persistent_file_mode() -> 8#400 + 8#200. +-spec read_persistent() -> {ok, acme_data()} | no_return(). read_persistent() -> case file:read_file(persistent_file()) of {ok, Binary} -> @@ -607,6 +651,7 @@ read_persistent() -> throw({error, Reason}) end. +-spec write_persistent(acme_data()) -> ok | no_return(). write_persistent(Data) -> Binary = term_to_binary(Data), case file:write_file(persistent_file(), Binary) of @@ -616,6 +661,7 @@ write_persistent(Data) -> throw({error, Reason}) end. +-spec create_persistent() -> ok | no_return(). create_persistent() -> Binary = term_to_binary(data_empty()), case file:write_file(persistent_file(), Binary) of @@ -631,30 +677,40 @@ create_persistent() -> throw({error, Reason}) end. +-spec write_account_persistent({list(), jose_jwk:key()}) -> ok | no_return(). write_account_persistent({AccId, PrivateKey}) -> {ok, Data} = read_persistent(), NewData = data_set_account(Data, {AccId, PrivateKey}), ok = write_persistent(NewData). +-spec read_account_persistent() -> {ok, list(), jose_jwk:key()} | none. read_account_persistent() -> {ok, Data} = read_persistent(), data_get_account(Data). +-spec read_certificates_persistent() -> data_certs(). read_certificates_persistent() -> {ok, Data} = read_persistent(), data_get_certificates(Data). +-spec add_certificate_persistent(data_cert()) -> ok. add_certificate_persistent(DataCert) -> {ok, Data} = read_persistent(), NewData = data_add_certificate(Data, DataCert), ok = write_persistent(NewData). +-spec remove_certificate_persistent(data_cert()) -> ok. +remove_certificate_persistent(DataCert) -> + {ok, Data} = read_persistent(), + NewData = data_remove_certificate(Data, DataCert), + ok = write_persistent(NewData). +-spec save_certificate({ok, bitstring(), binary()} | {error, _, _}) -> {ok, bitstring(), saved}. save_certificate({error, _, _} = Error) -> Error; save_certificate({ok, DomainName, Cert}) -> try - {ok, CertDir} = get_config_cert_dir(), + CertDir = get_config_cert_dir(), DomainString = bitstring_to_list(DomainName), CertificateFile = filename:join([CertDir, DomainString ++ "_cert.pem"]), %% TODO: At some point do the following using a Transaction so @@ -676,6 +732,7 @@ save_certificate({ok, DomainName, Cert}) -> {error, DomainName, saving} end. +-spec write_cert(file:filename(), binary(), bitstring()) -> {ok, bitstring(), saved}. write_cert(CertificateFile, Cert, DomainName) -> case file:write_file(CertificateFile, Cert) of ok -> @@ -686,41 +743,45 @@ write_cert(CertificateFile, Cert, DomainName) -> throw({error, DomainName, saving}) end. +-spec get_config_acme() -> [{atom(), bitstring()}]. get_config_acme() -> case ejabberd_config:get_option(acme, undefined) of undefined -> ?ERROR_MSG("No acme configuration has been specified", []), throw({error, configuration}); Acme -> - {ok, Acme} + Acme end. +-spec get_config_contact() -> bitstring(). get_config_contact() -> - {ok, Acme} = get_config_acme(), + Acme = get_config_acme(), case lists:keyfind(contact, 1, Acme) of {contact, Contact} -> - {ok, Contact}; + Contact; false -> ?ERROR_MSG("No contact has been specified", []), throw({error, configuration_contact}) end. +-spec get_config_hosts() -> [bitstring()]. get_config_hosts() -> case ejabberd_config:get_option(hosts, undefined) of undefined -> ?ERROR_MSG("No hosts have been specified", []), throw({error, configuration_hosts}); Hosts -> - {ok, Hosts} + Hosts end. +-spec get_config_cert_dir() -> file:filename(). get_config_cert_dir() -> case ejabberd_config:get_option(cert_dir, undefined) of undefined -> ?ERROR_MSG("No cert_dir configuration has been specified", []), throw({error, configuration}); CertDir -> - {ok, CertDir} + CertDir end. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% From ac7105d39e6122ab564b2eb3b811cbdf7a3fc6df Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 2 Aug 2017 19:36:11 +0300 Subject: [PATCH 41/75] Implement verbose list_certificates option --- src/ejabberd_acme.erl | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 5cb5857d4..230ce1100 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -248,16 +248,11 @@ list_certificates(Verbose) -> -spec list_certificates0(verbose_opt()) -> [string()] | [any()]. list_certificates0(Verbose) -> Certs = read_certificates_persistent(), - case Verbose of - "plain" -> - [format_certificate(DataCert) || {_Key, DataCert} <- Certs]; - "verbose" -> - Certs - end. + [format_certificate(DataCert, Verbose) || {_Key, DataCert} <- Certs]. %% TODO: Make this cleaner and more robust --spec format_certificate(data_cert()) -> string(). -format_certificate(DataCert) -> +-spec format_certificate(data_cert(), verbose_opt()) -> string(). +format_certificate(DataCert, Verbose) -> #data_cert{ domain = DomainName, pem = PemCert, @@ -274,10 +269,15 @@ format_certificate(DataCert) -> %% Find the notAfter date NotAfter = get_notAfter(Certificate), - format_certificate1(DomainName, NotAfter, Path). + case Verbose of + "plain" -> + format_certificate_plain(DomainName, NotAfter, Path); + "verbose" -> + format_certificate_verbose(DomainName, NotAfter, PemCert) + end. --spec format_certificate1(bitstring(), string(), string()) -> string(). -format_certificate1(DomainName, NotAfter, Path) -> +-spec format_certificate_plain(bitstring(), string(), string()) -> string(). +format_certificate_plain(DomainName, NotAfter, Path) -> Result = lists:flatten(io_lib:format( " Domain: ~s~n" " Valid until: ~s UTC~n" @@ -285,6 +285,15 @@ format_certificate1(DomainName, NotAfter, Path) -> [DomainName, NotAfter, Path])), Result. +-spec format_certificate_verbose(bitstring(), string(), bitstring()) -> string(). +format_certificate_verbose(DomainName, NotAfter, PemCert) -> + Result = lists:flatten(io_lib:format( + " Domain: ~s~n" + " Valid until: ~s UTC~n" + " Certificate In PEM format: ~n~s", + [DomainName, NotAfter, PemCert])), + Result. + -spec get_commonName(#'Certificate'{}) -> string(). get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> #'TBSCertificate'{ From e6e8e64f84ea30abd3f9c6c4e5a2be5a54df2a54 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 2 Aug 2017 21:10:49 +0300 Subject: [PATCH 42/75] Improve return format of get_certificates command --- src/ejabberd_acme.erl | 41 +++++++++++++++++++++++++++++++++++++---- src/ejabberd_admin.erl | 9 +++++---- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 230ce1100..cf8e5bfa6 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -56,7 +56,9 @@ is_valid_verbose_opt(_) -> false. %% Needs a hell lot of cleaning -spec get_certificates(url(), account_opt()) -> - [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | + {'ok', [{'ok', bitstring(), 'saved'}]} | + {'error', + [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} | {'error', _}. get_certificates(CAUrl, NewAccountOpt) -> try @@ -71,7 +73,9 @@ get_certificates(CAUrl, NewAccountOpt) -> end. -spec get_certificates0(url(), account_opt()) -> - [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | + {'ok', [{'ok', bitstring(), 'saved'}]} | + {'error', + [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} | no_return(). get_certificates0(CAUrl, "old-account") -> %% Get the current account @@ -86,7 +90,9 @@ get_certificates0(CAUrl, "new-account") -> get_certificates1(CAUrl, PrivateKey). -spec get_certificates1(url(), jose_jwk:key()) -> - [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}] | + {'ok', [{'ok', bitstring(), 'saved'}]} | + {'error', + [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} | no_return(). get_certificates1(CAUrl, PrivateKey) -> %% Read Config @@ -100,7 +106,33 @@ get_certificates1(CAUrl, PrivateKey) -> %% Format the result to send back to ejabberdctl %% Result - SavedCerts. + format_get_certificates_result(SavedCerts). + +-spec format_get_certificates_result([{'ok', bitstring(), 'saved'} | + {'error', bitstring(), _}]) -> + string(). +format_get_certificates_result(Certs) -> + Cond = lists:all(fun(Cert) -> + not is_error(Cert) + end, Certs), + FormattedCerts = lists:join($\n, + [format_get_certificate(C) || C <- Certs]), + case Cond of + true -> + Result = io_lib:format("Success:~n~s", [FormattedCerts]), + lists:flatten(Result); + _ -> + Result = io_lib:format("Error with one or more certificates~n~s", [lists:flatten(FormattedCerts)]), + lists:flatten(Result) + end. + +-spec format_get_certificate({'ok', bitstring(), 'saved'} | + {'error', bitstring(), _}) -> + string(). +format_get_certificate({ok, Domain, saved}) -> + io_lib:format(" Certificate for domain: \"~s\" acquired and saved", [Domain]); +format_get_certificate({error, Domain, Reason}) -> + io_lib:format(" Error for domain: \"~s\", with reason: \'~s\'", [Domain, Reason]). -spec get_certificate(url(), bitstring(), jose_jwk:key()) -> {'ok', bitstring(), pem()} | @@ -571,6 +603,7 @@ to_public(PrivateKey) -> -spec is_error(_) -> boolean(). is_error({error, _}) -> true; +is_error({error, _, _}) -> true; is_error(_) -> false. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index c68a08315..eaa22aeb4 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -248,17 +248,18 @@ get_commands_spec() -> result = {res, restuple}}, #ejabberd_commands{name = get_certificate, tags = [acme], - desc = "Gets a certificate for the specified domain", + desc = "Gets a certificate for the specified domain. Can be used with {old-account|new-account}.", module = ?MODULE, function = get_certificate, args_desc = ["Whether to create a new account or use the existing one"], + args_example = ["old-account | new-account"], args = [{option, string}], - result = {certificate, string}}, + result = {certificates, string}}, #ejabberd_commands{name = list_certificates, tags = [acme], - desc = "Lists all curently handled certificates and their respective domains", + desc = "Lists all curently handled certificates and their respective domains in {plain|verbose} format", module = ?MODULE, function = list_certificates, args_desc = ["Whether to print the whole certificate or just some metadata. Possible values: plain | verbose"], args = [{option, string}], - result = {certificates, {list,{certificate, string}}}}, + result = {certificates, {list, {certificate, string}}}}, #ejabberd_commands{name = revoke_certificate, tags = [acme], desc = "Revokes the selected certificate", module = ?MODULE, function = revoke_certificate, From 48254a1e10652be4387ef3e778dbda043823b3f5 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 8 Aug 2017 12:23:13 +0300 Subject: [PATCH 43/75] Change certificate notAfter to 90 days As stated in Let's Encrypt FAQ: https://letsencrypt.org/docs/faq/ --- src/ejabberd_acme.erl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index cf8e5bfa6..99dcdd6df 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -14,9 +14,6 @@ scenario/3, scenario0/2, new_user_scenario/2 - %% Not yet implemented - %% key_roll_over/5 - %% delete_authz/3 ]). -include("ejabberd.hrl"). @@ -581,8 +578,8 @@ not_before_not_after() -> %% TODO: Make notBefore and notAfter configurable somewhere {MegS, Sec, MicS} = erlang:timestamp(), NotBefore = xmpp_util:encode_timestamp({MegS, Sec, MicS}), - %% The certificate will be valid for 60 Days after today - NotAfter = xmpp_util:encode_timestamp({MegS+5, Sec+184000, MicS}), + %% The certificate will be valid for 90 Days after today + NotAfter = xmpp_util:encode_timestamp({MegS+7, Sec+776000, MicS}), {NotBefore, NotAfter}. -spec to_public(jose_jwk:key()) -> jose_jwk:key(). From 7fa9a387ae814a912973a2a77751e19cadd4568c Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 8 Aug 2017 12:45:57 +0300 Subject: [PATCH 44/75] Try catch when formatting certificates --- src/ejabberd_acme.erl | 55 +++++++++++++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 99dcdd6df..30befdcb8 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -259,6 +259,7 @@ ensure_account_exists() -> end. + %% %% List Certificates %% @@ -288,21 +289,27 @@ format_certificate(DataCert, Verbose) -> path = Path } = DataCert, - PemList = public_key:pem_decode(PemCert), - PemEntryCert = lists:keyfind('Certificate', 1, PemList), - Certificate = public_key:pem_entry_decode(PemEntryCert), + try + PemList = public_key:pem_decode(PemCert), + PemEntryCert = lists:keyfind('Certificate', 1, PemList), + Certificate = public_key:pem_entry_decode(PemEntryCert), - %% Find the commonName - _CommonName = get_commonName(Certificate), + %% Find the commonName + _CommonName = get_commonName(Certificate), - %% Find the notAfter date - NotAfter = get_notAfter(Certificate), + %% Find the notAfter date + NotAfter = get_notAfter(Certificate), - case Verbose of - "plain" -> - format_certificate_plain(DomainName, NotAfter, Path); - "verbose" -> - format_certificate_verbose(DomainName, NotAfter, PemCert) + case Verbose of + "plain" -> + format_certificate_plain(DomainName, NotAfter, Path); + "verbose" -> + format_certificate_verbose(DomainName, NotAfter, PemCert) + end + catch + E:R -> + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), + fail_format_certificate(DomainName) end. -spec format_certificate_plain(bitstring(), string(), string()) -> string(). @@ -323,6 +330,14 @@ format_certificate_verbose(DomainName, NotAfter, PemCert) -> [DomainName, NotAfter, PemCert])), Result. +-spec fail_format_certificate(bitstring()) -> string(). +fail_format_certificate(DomainName) -> + Result = lists:flatten(io_lib:format( + " Domain: ~s~n" + " Failed to format Certificate", + [DomainName])), + Result. + -spec get_commonName(#'Certificate'{}) -> string(). get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> #'TBSCertificate'{ @@ -337,13 +352,9 @@ get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> CommonName. -spec get_notAfter(#'Certificate'{}) -> string(). -get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) -> - #'TBSCertificate'{ - validity = Validity - } = TbsCertificate, - +get_notAfter(Certificate) -> + UtcTime = get_utc_validity(Certificate), %% TODO: Find a library function to decode utc time - #'Validity'{notAfter = {utcTime, UtcTime}} = Validity, [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcTime, YEAR = case list_to_integer([Y1,Y2]) >= 50 of true -> "19" ++ [Y1,Y2]; @@ -355,6 +366,14 @@ get_notAfter(#'Certificate'{tbsCertificate = TbsCertificate}) -> NotAfter. +-spec get_utc_validity(#'Certificate'{}) -> string(). +get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) -> + #'TBSCertificate'{ + validity = Validity + } = TbsCertificate, + + #'Validity'{notAfter = {utcTime, UtcTime}} = Validity, + UtcTime. %% %% Revoke Certificate From 9756b452d6b36867be53e89e2975d1752734a731 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 8 Aug 2017 16:38:19 +0300 Subject: [PATCH 45/75] Implement renew_certificate command This command renews the certificates for all domains that already have a certificate that has expired or is close to expiring. It is meant to be run automatically more often than the renewal process because if the certificates are valid nothing happens --- src/ejabberd_acme.erl | 185 ++++++++++++++++++++++++++++++++--------- src/ejabberd_admin.erl | 10 ++- 2 files changed, 153 insertions(+), 42 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 30befdcb8..c9e50c678 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -2,6 +2,7 @@ -export([%% Ejabberdctl Commands get_certificates/2, + renew_certificates/1, list_certificates/1, revoke_certificate/2, %% Command Options Validity @@ -52,11 +53,7 @@ is_valid_verbose_opt(_) -> false. %% %% Needs a hell lot of cleaning --spec get_certificates(url(), account_opt()) -> - {'ok', [{'ok', bitstring(), 'saved'}]} | - {'error', - [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} | - {'error', _}. +-spec get_certificates(url(), account_opt()) -> string() | {'error', _}. get_certificates(CAUrl, NewAccountOpt) -> try ?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]), @@ -69,11 +66,7 @@ get_certificates(CAUrl, NewAccountOpt) -> {error, get_certificates} end. --spec get_certificates0(url(), account_opt()) -> - {'ok', [{'ok', bitstring(), 'saved'}]} | - {'error', - [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} | - no_return(). +-spec get_certificates0(url(), account_opt()) -> string(). get_certificates0(CAUrl, "old-account") -> %% Get the current account {ok, _AccId, PrivateKey} = ensure_account_exists(), @@ -83,14 +76,10 @@ get_certificates0(CAUrl, "old-account") -> get_certificates0(CAUrl, "new-account") -> %% Create a new account and save it to disk {ok, _Id, PrivateKey} = create_save_new_account(CAUrl), - + get_certificates1(CAUrl, PrivateKey). --spec get_certificates1(url(), jose_jwk:key()) -> - {'ok', [{'ok', bitstring(), 'saved'}]} | - {'error', - [{'ok', bitstring(), 'saved'} | {'error', bitstring(), _}]} | - no_return(). +-spec get_certificates1(url(), jose_jwk:key()) -> string(). get_certificates1(CAUrl, PrivateKey) -> %% Read Config Hosts = get_config_hosts(), @@ -105,7 +94,7 @@ get_certificates1(CAUrl, PrivateKey) -> %% Result format_get_certificates_result(SavedCerts). --spec format_get_certificates_result([{'ok', bitstring(), 'saved'} | +-spec format_get_certificates_result([{'ok', bitstring(), _} | {'error', bitstring(), _}]) -> string(). format_get_certificates_result(Certs) -> @@ -123,11 +112,15 @@ format_get_certificates_result(Certs) -> lists:flatten(Result) end. --spec format_get_certificate({'ok', bitstring(), 'saved'} | +-spec format_get_certificate({'ok', bitstring(), _} | {'error', bitstring(), _}) -> string(). format_get_certificate({ok, Domain, saved}) -> io_lib:format(" Certificate for domain: \"~s\" acquired and saved", [Domain]); +format_get_certificate({ok, Domain, not_found}) -> + io_lib:format(" Certificate for domain: \"~s\" not found, so it was not renewed", [Domain]); +format_get_certificate({ok, Domain, exists}) -> + io_lib:format(" Certificate for domain: \"~s\" is not close to expiring", [Domain]); format_get_certificate({error, Domain, Reason}) -> io_lib:format(" Error for domain: \"~s\", with reason: \'~s\'", [Domain, Reason]). @@ -160,7 +153,7 @@ create_save_new_account(CAUrl) -> %% Write Persistent Data ok = write_account_persistent({Id, PrivateKey}), - + {ok, Id, PrivateKey}. %% TODO: @@ -205,7 +198,7 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) -> {ok, ChallengeId} = location_to_id(ChallengeUrl), Req3 = [{<<"type">>, <<"http-01">>},{<<"keyAuthorization">>, KeyAuthz}], {ok, _SolvedChallenge, _Nonce2} = ejabberd_acme_comm:complete_challenge( - {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), + {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce1), {ok, AuthzValid, _Nonce} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), {ok, AuthzValid} @@ -259,6 +252,81 @@ ensure_account_exists() -> end. +%% +%% Renew Certificates +%% +-spec renew_certificates(url()) -> string() | {'error', _}. +renew_certificates(CAUrl) -> + try + renew_certificates0(CAUrl) + catch + throw:Throw -> + Throw; + E:R -> + ?ERROR_MSG("Unknown ~p:~p, ~p", [E, R, erlang:get_stacktrace()]), + {error, get_certificates} + end. + +-spec renew_certificates0(url()) -> string(). +renew_certificates0(CAUrl) -> + %% Get the current account + {ok, _AccId, PrivateKey} = ensure_account_exists(), + + %% Read Config + Hosts = get_config_hosts(), + + %% Get a certificate for each host + PemCertKeys = [renew_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts], + + %% Save Certificates + SavedCerts = [save_renewed_certificate(Cert) || Cert <- PemCertKeys], + + %% Format the result to send back to ejabberdctl + %% Result + format_get_certificates_result(SavedCerts). + +-spec renew_certificate(url(), bitstring(), jose_jwk:key()) -> + {'ok', bitstring(), _} | + {'error', bitstring(), _}. +renew_certificate(CAUrl, DomainName, PrivateKey) -> + case cert_to_expire(DomainName) of + true -> + get_certificate(CAUrl, DomainName, PrivateKey); + {false, not_found} -> + {ok, DomainName, not_found}; + {false, PemCert} -> + {ok, DomainName, exists} + end. + +-spec cert_to_expire(bitstring()) -> 'true' | + {'false', pem()} | + {'false', not_found}. +cert_to_expire(DomainName) -> + Certs = read_certificates_persistent(), + case lists:keyfind(DomainName, 1, Certs) of + {DomainName, #data_cert{pem = Pem}} -> + Certificate = pem_to_certificate(Pem), + Validity = get_utc_validity(Certificate), + case close_to_expire(Validity) of + true -> + true; + false -> + {false, Pem} + end; + false -> + {false, not_found} + end. + +-spec close_to_expire(string()) -> boolean(). +close_to_expire(Validity) -> + {ValidDate, _ValidTime} = utc_string_to_datetime(Validity), + ValidDays = calendar:date_to_gregorian_days(ValidDate), + + {CurrentDate, _CurrentTime} = calendar:universal_time(), + CurrentDays = calendar:date_to_gregorian_days(CurrentDate), + CurrentDays > ValidDays - 30. + + %% %% List Certificates @@ -288,11 +356,9 @@ format_certificate(DataCert, Verbose) -> pem = PemCert, path = Path } = DataCert, - + try - PemList = public_key:pem_decode(PemCert), - PemEntryCert = lists:keyfind('Certificate', 1, PemList), - Certificate = public_key:pem_entry_decode(PemEntryCert), + Certificate = pem_to_certificate(PemCert), %% Find the commonName _CommonName = get_commonName(Certificate), @@ -343,11 +409,11 @@ get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> #'TBSCertificate'{ subject = {rdnSequence, SubjectList} } = TbsCertificate, - + %% TODO: Not the best way to find the commonName ShallowSubjectList = [Attribute || [Attribute] <- SubjectList], {_, _, CommonName} = lists:keyfind(attribute_oid(commonName), 2, ShallowSubjectList), - + %% TODO: Remove the length-encoding from the commonName before returning it CommonName. @@ -409,9 +475,9 @@ revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) -> {ok, _AccId, PrivateKey} = ensure_account_exists(), Certificate = prepare_certificate_revoke(PemEncodedCert), - + {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), - + Req = [{<<"certificate">>, Certificate}], {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce), ok = remove_certificate_persistent(Cert), @@ -604,19 +670,44 @@ not_before_not_after() -> -spec to_public(jose_jwk:key()) -> jose_jwk:key(). to_public(PrivateKey) -> jose_jwk:to_public(PrivateKey). - %% case jose_jwk:to_key(PrivateKey) of - %% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} -> - %% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}, - %% jose_jwk:from_key(Public); - %% _ -> - %% jose_jwk:to_public(PrivateKey) - %% end. - +%% case jose_jwk:to_key(PrivateKey) of +%% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} -> +%% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}, +%% jose_jwk:from_key(Public); +%% _ -> +%% jose_jwk:to_public(PrivateKey) +%% end. + %% to_public(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) -> %% #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}; %% to_public(PrivateKey) -> %% jose_jwk:to_public(PrivateKey). +-spec pem_to_certificate(pem()) -> #'Certificate'{}. +pem_to_certificate(Pem) -> + PemList = public_key:pem_decode(Pem), + PemEntryCert = lists:keyfind('Certificate', 1, PemList), + Certificate = public_key:pem_entry_decode(PemEntryCert), + Certificate. + +%% TODO: Find a better and more robust way to parse the utc string +-spec utc_string_to_datetime(string()) -> calendar:datetime(). +utc_string_to_datetime(UtcString) -> + try + [Y1,Y2,MO1,MO2,D1,D2,H1,H2,MI1,MI2,S1,S2,$Z] = UtcString, + Year = list_to_integer("20" ++ [Y1,Y2]), + Month = list_to_integer([MO1, MO2]), + Day = list_to_integer([D1,D2]), + Hour = list_to_integer([H1,H2]), + Minute = list_to_integer([MI1,MI2]), + Second = list_to_integer([S1,S2]), + {{Year, Month, Day}, {Hour, Minute, Second}} + catch + E:R -> + ?ERROR_MSG("Unable to parse UTC string", []), + throw({error, utc_string_to_datetime}) + end. + -spec is_error(_) -> boolean(). is_error({error, _}) -> true; is_error({error, _, _}) -> true; @@ -763,7 +854,8 @@ remove_certificate_persistent(DataCert) -> NewData = data_remove_certificate(Data, DataCert), ok = write_persistent(NewData). --spec save_certificate({ok, bitstring(), binary()} | {error, _, _}) -> {ok, bitstring(), saved}. +-spec save_certificate({ok, bitstring(), binary()} | {error, _, _}) -> + {ok, bitstring(), saved} | {error, bitstring(), _}. save_certificate({error, _, _} = Error) -> Error; save_certificate({ok, DomainName, Cert}) -> @@ -790,6 +882,17 @@ save_certificate({ok, DomainName, Cert}) -> {error, DomainName, saving} end. +-spec save_renewed_certificate({ok, bitstring(), _} | {error, _, _}) -> + {ok, bitstring(), _} | {error, bitstring(), _}. +save_renewed_certificate({error, _, _} = Error) -> + Error; +save_renewed_certificate({ok, _, not_found} = Cert) -> + Cert; +save_renewed_certificate({ok, _, exists} = Cert) -> + Cert; +save_renewed_certificate({ok, DomainName, Cert}) -> + save_certificate({ok, DomainName, Cert}). + -spec write_cert(file:filename(), binary(), bitstring()) -> {ok, bitstring(), saved}. write_cert(CertificateFile, Cert, DomainName) -> case file:write_file(CertificateFile, Cert) of @@ -853,11 +956,11 @@ transaction([{Fun, Rollback} | Rest]) -> {ok, Result} = Fun(), [Result | transaction(Rest)] catch Type:Reason -> - Rollback(), - erlang:raise(Type, Reason, erlang:get_stacktrace()) + Rollback(), + erlang:raise(Type, Reason, erlang:get_stacktrace()) end; transaction([Fun | Rest]) -> - % not every action require cleanup on error + % not every action require cleanup on error transaction([{Fun, fun () -> ok end} | Rest]); transaction([]) -> []. @@ -995,7 +1098,7 @@ generate_key() -> Key = public_key:generate_key({rsa, 2048, 65537}), Key1 = Key#'RSAPrivateKey'{version = 'two-prime'}, jose_jwk:from_key(Key1). - %% jose_jwk:generate_key({rsa, 2048}). +%% jose_jwk:generate_key({rsa, 2048}). -else. generate_key() -> ?INFO_MSG("Generate EC key pair~n", []), diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index eaa22aeb4..19c7daa8f 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -46,6 +46,7 @@ import_file/1, import_dir/1, %% Acme get_certificate/1, + renew_certificate/0, list_certificates/1, revoke_certificate/1, %% Purge DB @@ -246,7 +247,6 @@ get_commands_spec() -> args_example = ["/var/lib/ejabberd/jabberd14/"], args = [{file, string}], result = {res, restuple}}, - #ejabberd_commands{name = get_certificate, tags = [acme], desc = "Gets a certificate for the specified domain. Can be used with {old-account|new-account}.", module = ?MODULE, function = get_certificate, @@ -254,6 +254,11 @@ get_commands_spec() -> args_example = ["old-account | new-account"], args = [{option, string}], result = {certificates, string}}, + #ejabberd_commands{name = renew_certificate, tags = [acme], + desc = "Renews all certificates that are close to expiring", + module = ?MODULE, function = renew_certificate, + args = [], + result = {certificates, string}}, #ejabberd_commands{name = list_certificates, tags = [acme], desc = "Lists all curently handled certificates and their respective domains in {plain|verbose} format", module = ?MODULE, function = list_certificates, @@ -579,6 +584,9 @@ get_certificate(UseNewAccount) -> {invalid_option, String} end. +renew_certificate() -> + ejabberd_acme:renew_certificates("http://localhost:4000"). + list_certificates(Verbose) -> case ejabberd_acme:is_valid_verbose_opt(Verbose) of true -> From 97a4d57f2efebb93ded29fcab9b326a76534070b Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 8 Aug 2017 18:00:37 +0300 Subject: [PATCH 46/75] Remove some debugging functions --- src/ejabberd_acme.erl | 212 +----------------------------------------- 1 file changed, 1 insertion(+), 211 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index c9e50c678..9a09211ba 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -10,11 +10,7 @@ is_valid_verbose_opt/1, %% Misc generate_key/0, - to_public/1, - %% Debugging Scenarios - scenario/3, - scenario0/2, - new_user_scenario/2 + to_public/1 ]). -include("ejabberd.hrl"). @@ -945,152 +941,6 @@ get_config_cert_dir() -> CertDir end. -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Transaction Fun -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -transaction([{Fun, Rollback} | Rest]) -> - try - {ok, Result} = Fun(), - [Result | transaction(Rest)] - catch Type:Reason -> - Rollback(), - erlang:raise(Type, Reason, erlang:get_stacktrace()) - end; -transaction([Fun | Rest]) -> - % not every action require cleanup on error - transaction([{Fun, fun () -> ok end} | Rest]); -transaction([]) -> []. - -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -%% -%% Debugging Funcs -- They are only used for the development phase -%% -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - -%% A typical acme workflow -scenario(CAUrl, AccId, PrivateKey) -> - {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), - - {ok, {_TOS, Account}, Nonce1} = - ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce0), - ?INFO_MSG("Account: ~p~n", [Account]), - - Req = - [{<<"identifier">>, - {[{<<"type">>, <<"dns">>}, - {<<"value">>, <<"my-acme-test-ejabberd.com">>}]}}, - {<<"existing">>, <<"accept">>} - ], - {ok, Authz, Nonce2} = ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req, Nonce1), - - {Account, Authz, PrivateKey}. - - -new_user_scenario(CAUrl, HttpDir) -> - PrivateKey = generate_key(), - - {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), - %% ?INFO_MSG("Directories: ~p", [Dirs]), - - Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), - - {_, AccIdInt} = proplists:lookup(<<"id">>, Account), - AccId = integer_to_list(AccIdInt), - {ok, {_TOS, Account1}, Nonce2} = - ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce1), - %% ?INFO_MSG("Old account: ~p~n", [Account1]), - - Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = - ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), - - %% NewKey = generate_key(), - %% KeyChangeUrl = CAUrl ++ "/acme/key-change/", - %% {ok, Account3, Nonce4} = key_roll_over(KeyChangeUrl, AccURL, PrivateKey, NewKey, Nonce3), - %% ?INFO_MSG("Changed key: ~p~n", [Account3]), - - %% {ok, {_TOS, Account4}, Nonce5} = get_account(AccURL, NewKey, Nonce4), - %% ?INFO_MSG("New account:~p~n", [Account4]), - %% {Account4, PrivateKey}. - - AccIdBin = list_to_bitstring(integer_to_list(AccIdInt)), - DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, - Req2 = - [{<<"identifier">>, - {[{<<"type">>, <<"dns">>}, - {<<"value">>, DomainName}]}}, - {<<"existing">>, <<"accept">>} - ], - {ok, {AuthzUrl, Authz}, Nonce4} = - ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req2, Nonce3), - - {ok, AuthzId} = location_to_id(AuthzUrl), - {ok, Authz2, Nonce5} = ejabberd_acme_comm:get_authz({CAUrl, AuthzId}), - ?INFO_MSG("AuthzUrl: ~p~n", [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", []), - - {ok, ChallengeId} = location_to_id(ChallengeUrl), - Req3 = - [ {<<"type">>, <<"http-01">>} - , {<<"keyAuthorization">>, KeyAuthz} - ], - {ok, SolvedChallenge, Nonce6} = ejabberd_acme_comm:complete_challenge( - {CAUrl, AuthzId, ChallengeId}, PrivateKey, Req3, Nonce5), - %% ?INFO_MSG("SolvedChallenge: ~p~n", [SolvedChallenge]), - - %% timer:sleep(2000), - {ok, Authz3, Nonce7} = ejabberd_acme_comm:get_authz_until_valid({CAUrl, AuthzId}), - - #{"new-cert" := NewCert} = Dirs, - CSRSubject = [{commonName, bitstring_to_list(DomainName)}], - {CSR, CSRKey} = make_csr(CSRSubject), - {MegS, Sec, MicS} = erlang:timestamp(), - NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), - NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), - Req4 = - [{<<"csr">>, CSR}, - {<<"notBefore">>, NotBefore}, - {<<"NotAfter">>, NotAfter} - ], - {ok, {CertUrl, Certificate}, Nonce8} = - ejabberd_acme_comm:new_cert(Dirs, PrivateKey, Req4, Nonce7), - ?INFO_MSG("CertUrl: ~p~n", [CertUrl]), - - {ok, CertId} = location_to_id(CertUrl), - {ok, Certificate2, Nonce9} = ejabberd_acme_comm:get_cert({CAUrl, CertId}), - - DecodedCert = public_key:pkix_decode_cert(list_to_binary(Certificate2), plain), - %% ?INFO_MSG("DecodedCert: ~p~n", [DecodedCert]), - PemEntryCert = public_key:pem_entry_encode('Certificate', DecodedCert), - %% ?INFO_MSG("PemEntryCert: ~p~n", [PemEntryCert]), - - {_, CSRKeyKey} = jose_jwk:to_key(CSRKey), - PemEntryKey = public_key:pem_entry_encode('ECPrivateKey', CSRKeyKey), - %% ?INFO_MSG("PemKey: ~p~n", [jose_jwk:to_pem(CSRKey)]), - %% ?INFO_MSG("PemEntryKey: ~p~n", [PemEntryKey]), - - PemCert = public_key:pem_encode([PemEntryKey, PemEntryCert]), - %% ?INFO_MSG("PemCert: ~p~n", [PemCert]), - - ok = file:write_file(HttpDir ++ "/my_server.pem", PemCert), - - Base64Cert = base64url:encode(Certificate2), - Req5 = [{<<"certificate">>, Base64Cert}], - {ok, [], Nonce10} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req5, Nonce9), - - {ok, Certificate3, Nonce11} = ejabberd_acme_comm:get_cert({CAUrl, CertId}), - - {Account2, Authz3, CSR, Certificate, PrivateKey}. -ifdef(GENERATE_RSA_KEY). generate_key() -> @@ -1105,63 +955,3 @@ generate_key() -> jose_jwk:generate_key({ec, secp256r1}). -endif. - -scenario3() -> - CSRSubject = [{commonName, "my-acme-test-ejabberd.com"}, - {organizationName, "Example Corp"}], - {CSR, CSRKey} = make_csr(CSRSubject). - - -%% It doesn't seem to work, The user can get a new authorization even though the account has been deleted -delete_account_scenario(CAUrl) -> - PrivateKey = generate_key(), - - DirURL = CAUrl ++ "/directory", - {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(DirURL), - %% ?INFO_MSG("Directories: ~p", [Dirs]), - - Req0 = [{ <<"contact">>, [<<"mailto:cert-example-admin@example2.com">>]}], - {ok, {TOS, Account}, Nonce1} = ejabberd_acme_comm:new_account(Dirs, PrivateKey, Req0, Nonce0), - - {_, AccIdInt} = proplists:lookup(<<"id">>, Account), - AccId = integer_to_list(AccIdInt), - {ok, {_TOS, Account1}, Nonce2} = - ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce1), - %% ?INFO_MSG("Old account: ~p~n", [Account1]), - - Req1 = [{ <<"agreement">>, list_to_bitstring(TOS)}], - {ok, Account2, Nonce3} = - ejabberd_acme_comm:update_account({CAUrl, AccId}, PrivateKey, Req1, Nonce2), - - %% Delete account - {ok, Account3, Nonce4} = - ejabberd_acme_comm:delete_account({CAUrl, AccId}, PrivateKey, Nonce3), - - timer:sleep(3000), - - {ok, {_TOS, Account4}, Nonce5} = - ejabberd_acme_comm:get_account({CAUrl, AccId}, PrivateKey, Nonce4), - ?INFO_MSG("New account: ~p~n", [Account4]), - - AccIdBin = list_to_bitstring(integer_to_list(AccIdInt)), - DomainName = << <<"my-acme-test-ejabberd">>/binary, AccIdBin/binary, <<".com">>/binary >>, - Req2 = - [{<<"identifier">>, - {[{<<"type">>, <<"dns">>}, - {<<"value">>, DomainName}]}}, - {<<"existing">>, <<"accept">>} - ], - {ok, {AuthzUrl, Authz}, Nonce6} = - ejabberd_acme_comm:new_authz(Dirs, PrivateKey, Req2, Nonce5), - - {ok, Account1, Account3, Authz}. - -%% Just a test -scenario0(KeyFile, HttpDir) -> - PrivateKey = jose_jwk:from_file(KeyFile), - %% scenario("http://localhost:4000", "2", PrivateKey). - %% delete_account_scenario("http://localhost:4000"). - new_user_scenario("http://localhost:4000", HttpDir). - -%% scenario3(). - From 011b7ac3f212a5820b33936e99a44b5e4d17338a Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 10 Aug 2017 15:26:35 +0300 Subject: [PATCH 47/75] Support getting certificates for domains not specified in the configuration file --- include/ejabberd_acme.hrl | 2 +- src/ejabberd_acme.erl | 50 ++++++++++++++++++++++++++------------- src/ejabberd_admin.erl | 26 ++++++++++++-------- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index 4ef3bedbe..e0725af70 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -45,5 +45,5 @@ %% Options -type account_opt() :: string(). -type verbose_opt() :: string(). - +-type domains_opt() :: string(). diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 9a09211ba..4194f9393 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,14 +1,15 @@ -module (ejabberd_acme). -export([%% Ejabberdctl Commands - get_certificates/2, + get_certificates/3, renew_certificates/1, list_certificates/1, revoke_certificate/2, %% Command Options Validity is_valid_account_opt/1, is_valid_verbose_opt/1, - %% Misc + is_valid_domain_opt/1, + %% Key Related generate_key/0, to_public/1 ]). @@ -42,18 +43,27 @@ is_valid_verbose_opt("plain") -> true; is_valid_verbose_opt("verbose") -> true; is_valid_verbose_opt(_) -> false. +%% TODO: Make this check more complicated +-spec is_valid_domain_opt(string()) -> boolean(). +is_valid_domain_opt("all") -> true; +is_valid_domain_opt(DomainString) -> + case parse_domain_string(DomainString) of + [] -> + false; + SeparatedDomains -> + true + end. + %% %% Get Certificate %% -%% Needs a hell lot of cleaning --spec get_certificates(url(), account_opt()) -> string() | {'error', _}. -get_certificates(CAUrl, NewAccountOpt) -> +-spec get_certificates(url(), domains_opt(), account_opt()) -> string() | {'error', _}. +get_certificates(CAUrl, Domains, NewAccountOpt) -> try - ?INFO_MSG("Persistent: ~p~n", [file:read_file_info(persistent_file())]), - get_certificates0(CAUrl, NewAccountOpt) + get_certificates0(CAUrl, Domains, NewAccountOpt) catch throw:Throw -> Throw; @@ -62,24 +72,30 @@ get_certificates(CAUrl, NewAccountOpt) -> {error, get_certificates} end. --spec get_certificates0(url(), account_opt()) -> string(). -get_certificates0(CAUrl, "old-account") -> +-spec get_certificates0(url(), domains_opt(), account_opt()) -> string(). +get_certificates0(CAUrl, Domains, "old-account") -> %% Get the current account {ok, _AccId, PrivateKey} = ensure_account_exists(), - get_certificates1(CAUrl, PrivateKey); + get_certificates1(CAUrl, Domains, PrivateKey); -get_certificates0(CAUrl, "new-account") -> +get_certificates0(CAUrl, Domains, "new-account") -> %% Create a new account and save it to disk {ok, _Id, PrivateKey} = create_save_new_account(CAUrl), - get_certificates1(CAUrl, PrivateKey). + get_certificates1(CAUrl, Domains, PrivateKey). --spec get_certificates1(url(), jose_jwk:key()) -> string(). -get_certificates1(CAUrl, PrivateKey) -> - %% Read Config +-spec get_certificates1(url(), domains_opt(), jose_jwk:key()) -> string(). +get_certificates1(CAUrl, "all", PrivateKey) -> Hosts = get_config_hosts(), + get_certificates2(CAUrl, PrivateKey, Hosts); +get_certificates1(CAUrl, DomainString, PrivateKey) -> + Domains = parse_domain_string(DomainString), + Hosts = [list_to_bitstring(D) || D <- Domains], + get_certificates2(CAUrl, PrivateKey, Hosts). +-spec get_certificates2(url(), jose_jwk:key(), [bitstring()]) -> string(). +get_certificates2(CAUrl, PrivateKey, Hosts) -> %% Get a certificate for each host PemCertKeys = [get_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts], @@ -87,7 +103,6 @@ get_certificates1(CAUrl, PrivateKey) -> SavedCerts = [save_certificate(Cert) || Cert <- PemCertKeys], %% Format the result to send back to ejabberdctl - %% Result format_get_certificates_result(SavedCerts). -spec format_get_certificates_result([{'ok', bitstring(), _} | @@ -704,6 +719,9 @@ utc_string_to_datetime(UtcString) -> throw({error, utc_string_to_datetime}) end. +parse_domain_string(DomainString) -> + string:tokens(DomainString, ";"). + -spec is_error(_) -> boolean(). is_error({error, _}) -> true; is_error({error, _, _}) -> true; diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 19c7daa8f..5a313511f 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -45,7 +45,7 @@ %% Migration jabberd1.4 import_file/1, import_dir/1, %% Acme - get_certificate/1, + get_certificate/2, renew_certificate/0, list_certificates/1, revoke_certificate/1, @@ -248,11 +248,13 @@ get_commands_spec() -> args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = get_certificate, tags = [acme], - desc = "Gets a certificate for the specified domain. Can be used with {old-account|new-account}.", + desc = "Gets a certificate for all or the specified domains {all|domain1;domain2;...}. Can be used with {old-account|new-account}.", module = ?MODULE, function = get_certificate, - args_desc = ["Whether to create a new account or use the existing one"], - args_example = ["old-account | new-account"], - args = [{option, string}], + args_desc = ["Domains for which to acquire a certificate", + "Whether to create a new account or use the existing one"], + args_example = ["all | www.example.com;www.example1.net", + "old-account | new-account"], + args = [{domains, string}, {option, string}], result = {certificates, string}}, #ejabberd_commands{name = renew_certificate, tags = [acme], desc = "Renews all certificates that are close to expiring", @@ -575,13 +577,17 @@ import_dir(Path) -> %%% Acme %%% -get_certificate(UseNewAccount) -> - case ejabberd_acme:is_valid_account_opt(UseNewAccount) of +get_certificate(Domains, UseNewAccount) -> + case ejabberd_acme:is_valid_domain_opt(Domains) of true -> - ejabberd_acme:get_certificates("http://localhost:4000", UseNewAccount); + case ejabberd_acme:is_valid_account_opt(UseNewAccount) of + true -> + ejabberd_acme:get_certificates("http://localhost:4000", Domains, UseNewAccount); + false -> + io_lib:format("Invalid account option: ~p", [UseNewAccount]) + end; false -> - String = io_lib:format("Invalid account option: ~p", [UseNewAccount]), - {invalid_option, String} + String = io_lib:format("Invalid domains: ~p", [Domains]) end. renew_certificate() -> From c20bfb3422be8b67479cc1a1e79fc8b1c7dc1d5d Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 10 Aug 2017 17:23:13 +0300 Subject: [PATCH 48/75] Revoke Certificate: Jose Private Key Instead of signing the jose object with the account private key, it now signs the object using the certificate private key. This is useful in case the user wants to revoke a old certificate whose account key doesn't exist anymore. --- src/ejabberd_acme.erl | 45 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 4194f9393..459b7e3d0 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -474,7 +474,6 @@ revoke_certificate0(CAUrl, Domain) -> BinDomain = list_to_bitstring(Domain), case domain_certificate_exists(BinDomain) of {BinDomain, Certificate} -> - ?INFO_MSG("Certificate: ~p found!!", [Certificate]), ok = revoke_certificate1(CAUrl, Certificate), {ok, deleted}; false -> @@ -483,14 +482,12 @@ revoke_certificate0(CAUrl, Domain) -> -spec revoke_certificate1(url(), data_cert()) -> ok. revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) -> - {ok, _AccId, PrivateKey} = ensure_account_exists(), - - Certificate = prepare_certificate_revoke(PemEncodedCert), + {Certificate, CertPrivateKey} = prepare_certificate_revoke(PemEncodedCert), {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), Req = [{<<"certificate">>, Certificate}], - {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, PrivateKey, Req, Nonce), + {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, CertPrivateKey, Req, Nonce), ok = remove_certificate_persistent(Cert), ok. @@ -501,7 +498,9 @@ prepare_certificate_revoke(PemEncodedCert) -> PemCert = public_key:pem_entry_decode(PemCertEnc), DerCert = public_key:der_encode('Certificate', PemCert), Base64Cert = base64url:encode(DerCert), - Base64Cert. + + Key = find_private_key_in_pem(PemEncodedCert), + {Base64Cert, Key}. -spec domain_certificate_exists(bitstring()) -> {bitstring(), data_cert()} | false. domain_certificate_exists(Domain) -> @@ -719,9 +718,43 @@ utc_string_to_datetime(UtcString) -> throw({error, utc_string_to_datetime}) end. +-spec find_private_key_in_pem(pem()) -> {ok, jose_jwk:key()} | false. +find_private_key_in_pem(Pem) -> + PemList = public_key:pem_decode(Pem), + case find_private_key_in_pem1(private_key_types(), PemList) of + false -> + false; + PemKey -> + Key = public_key:pem_entry_decode(PemKey), + JoseKey = jose_jwk:from_key(Key), + JoseKey + end. + + +-spec find_private_key_in_pem1([public_key:pki_asn1_type()], + [public_key:pem_entry()]) -> + public_key:pem_entry() | false. +find_private_key_in_pem1([], _PemList) -> + false; +find_private_key_in_pem1([Type|Types], PemList) -> + case lists:keyfind(Type, 1, PemList) of + false -> + find_private_key_in_pem1(Types, PemList); + Key -> + Key + end. + + +-spec parse_domain_string(string()) -> [string()]. parse_domain_string(DomainString) -> string:tokens(DomainString, ";"). +-spec private_key_types() -> [public_key:pki_asn1_type()]. +private_key_types() -> + ['RSAPrivateKey', + 'DSAPrivateKey', + 'ECPrivateKey']. + -spec is_error(_) -> boolean(). is_error({error, _}) -> true; is_error({error, _, _}) -> true; From 2b1fea01cd176ddc16c2b8329fc80a28037a959a Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 10 Aug 2017 18:54:26 +0300 Subject: [PATCH 49/75] Renew certificate now renews all saved certificates that are close to expire Before this commit renew_certificate only checked the hosts in the config file and renewd the certificates for those. However the user can request certificates apart from the hosts in the config file so he should be able to also renew them. --- src/ejabberd_acme.erl | 47 ++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 459b7e3d0..a66f6eca2 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -283,50 +283,35 @@ renew_certificates0(CAUrl) -> %% Get the current account {ok, _AccId, PrivateKey} = ensure_account_exists(), - %% Read Config - Hosts = get_config_hosts(), + %% Find all hosts that we have certificates for + Certs = read_certificates_persistent(), %% Get a certificate for each host - PemCertKeys = [renew_certificate(CAUrl, Host, PrivateKey) || Host <- Hosts], + PemCertKeys = [renew_certificate(CAUrl, Cert, PrivateKey) || Cert <- Certs], %% Save Certificates SavedCerts = [save_renewed_certificate(Cert) || Cert <- PemCertKeys], %% Format the result to send back to ejabberdctl - %% Result format_get_certificates_result(SavedCerts). --spec renew_certificate(url(), bitstring(), jose_jwk:key()) -> +-spec renew_certificate(url(), data_cert(), jose_jwk:key()) -> {'ok', bitstring(), _} | {'error', bitstring(), _}. -renew_certificate(CAUrl, DomainName, PrivateKey) -> - case cert_to_expire(DomainName) of +renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) -> + case cert_to_expire(Cert) of true -> get_certificate(CAUrl, DomainName, PrivateKey); - {false, not_found} -> - {ok, DomainName, not_found}; - {false, PemCert} -> - {ok, DomainName, exists} + false -> + {ok, DomainName, no_expire} end. --spec cert_to_expire(bitstring()) -> 'true' | - {'false', pem()} | - {'false', not_found}. -cert_to_expire(DomainName) -> - Certs = read_certificates_persistent(), - case lists:keyfind(DomainName, 1, Certs) of - {DomainName, #data_cert{pem = Pem}} -> - Certificate = pem_to_certificate(Pem), - Validity = get_utc_validity(Certificate), - case close_to_expire(Validity) of - true -> - true; - false -> - {false, Pem} - end; - false -> - {false, not_found} - end. + +-spec cert_to_expire(data_cert()) -> boolean(). +cert_to_expire({DomainName, #data_cert{pem = Pem}}) -> + Certificate = pem_to_certificate(Pem), + Validity = get_utc_validity(Certificate), + close_to_expire(Validity). -spec close_to_expire(string()) -> boolean(). close_to_expire(Validity) -> @@ -933,9 +918,7 @@ save_certificate({ok, DomainName, Cert}) -> {ok, bitstring(), _} | {error, bitstring(), _}. save_renewed_certificate({error, _, _} = Error) -> Error; -save_renewed_certificate({ok, _, not_found} = Cert) -> - Cert; -save_renewed_certificate({ok, _, exists} = Cert) -> +save_renewed_certificate({ok, _, no_expire} = Cert) -> Cert; save_renewed_certificate({ok, DomainName, Cert}) -> save_certificate({ok, DomainName, Cert}). From 7140c8d844468d2b6c0dbe6ae9cac8c7fa73672e Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 11 Aug 2017 13:28:17 +0300 Subject: [PATCH 50/75] Format expired certificates differently in list_certificates --- src/ejabberd_acme.erl | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index a66f6eca2..e84493beb 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -311,16 +311,18 @@ renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) -> cert_to_expire({DomainName, #data_cert{pem = Pem}}) -> Certificate = pem_to_certificate(Pem), Validity = get_utc_validity(Certificate), - close_to_expire(Validity). --spec close_to_expire(string()) -> boolean(). -close_to_expire(Validity) -> + %% 30 days before expiration + close_to_expire(Validity, 30). + +-spec close_to_expire(string(), integer()) -> boolean(). +close_to_expire(Validity, Days) -> {ValidDate, _ValidTime} = utc_string_to_datetime(Validity), ValidDays = calendar:date_to_gregorian_days(ValidDate), {CurrentDate, _CurrentTime} = calendar:universal_time(), CurrentDays = calendar:date_to_gregorian_days(CurrentDate), - CurrentDays > ValidDays - 30. + CurrentDays > ValidDays - Days. @@ -378,20 +380,26 @@ format_certificate(DataCert, Verbose) -> format_certificate_plain(DomainName, NotAfter, Path) -> Result = lists:flatten(io_lib:format( " Domain: ~s~n" - " Valid until: ~s UTC~n" + " ~s~n" " Path: ~s", - [DomainName, NotAfter, Path])), + [DomainName, format_validity(NotAfter), Path])), Result. -spec format_certificate_verbose(bitstring(), string(), bitstring()) -> string(). format_certificate_verbose(DomainName, NotAfter, PemCert) -> Result = lists:flatten(io_lib:format( " Domain: ~s~n" - " Valid until: ~s UTC~n" + " ~s~n" " Certificate In PEM format: ~n~s", - [DomainName, NotAfter, PemCert])), + [DomainName, format_validity(NotAfter), PemCert])), Result. +-spec format_validity({'expired' | 'ok', string()}) -> string(). +format_validity({expired, NotAfter}) -> + io_lib:format("Expired at: ~s UTC", [NotAfter]); +format_validity({ok, NotAfter}) -> + io_lib:format("Valid until: ~s UTC", [NotAfter]). + -spec fail_format_certificate(bitstring()) -> string(). fail_format_certificate(DomainName) -> Result = lists:flatten(io_lib:format( @@ -413,7 +421,7 @@ get_commonName(#'Certificate'{tbsCertificate = TbsCertificate}) -> %% TODO: Remove the length-encoding from the commonName before returning it CommonName. --spec get_notAfter(#'Certificate'{}) -> string(). +-spec get_notAfter(#'Certificate'{}) -> {expired | ok, string()}. get_notAfter(Certificate) -> UtcTime = get_utc_validity(Certificate), %% TODO: Find a library function to decode utc time @@ -426,7 +434,12 @@ get_notAfter(Certificate) -> [YEAR, [MO1,MO2], [D1,D2], [H1,H2], [MI1,MI2], [S1,S2]])), - NotAfter. + case close_to_expire(UtcTime, 0) of + true -> + {expired, NotAfter}; + false -> + {ok, NotAfter} + end. -spec get_utc_validity(#'Certificate'{}) -> string(). get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) -> From 1aadb797b38a08b122f97935ad6061156019b30e Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 11 Aug 2017 14:10:55 +0300 Subject: [PATCH 51/75] Remove the new account option from get certificate. There is no reason for having this --- src/ejabberd_acme.erl | 32 ++++++++++++++++++-------------- src/ejabberd_admin.erl | 21 +++++++-------------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index e84493beb..2370dc906 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,7 +1,7 @@ -module (ejabberd_acme). -export([%% Ejabberdctl Commands - get_certificates/3, + get_certificates/2, renew_certificates/1, list_certificates/1, revoke_certificate/2, @@ -60,10 +60,10 @@ is_valid_domain_opt(DomainString) -> %% Get Certificate %% --spec get_certificates(url(), domains_opt(), account_opt()) -> string() | {'error', _}. -get_certificates(CAUrl, Domains, NewAccountOpt) -> +-spec get_certificates(url(), domains_opt()) -> string() | {'error', _}. +get_certificates(CAUrl, Domains) -> try - get_certificates0(CAUrl, Domains, NewAccountOpt) + get_certificates0(CAUrl, Domains) catch throw:Throw -> Throw; @@ -72,19 +72,23 @@ get_certificates(CAUrl, Domains, NewAccountOpt) -> {error, get_certificates} end. --spec get_certificates0(url(), domains_opt(), account_opt()) -> string(). -get_certificates0(CAUrl, Domains, "old-account") -> - %% Get the current account - {ok, _AccId, PrivateKey} = ensure_account_exists(), - - get_certificates1(CAUrl, Domains, PrivateKey); - -get_certificates0(CAUrl, Domains, "new-account") -> - %% Create a new account and save it to disk - {ok, _Id, PrivateKey} = create_save_new_account(CAUrl), +-spec get_certificates0(url(), domains_opt()) -> string(). +get_certificates0(CAUrl, Domains) -> + %% Check if an account exists or create another one + {ok, _AccId, PrivateKey} = retrieve_or_create_account(CAUrl), get_certificates1(CAUrl, Domains, PrivateKey). + +retrieve_or_create_account(CAUrl) -> + case read_account_persistent() of + none -> + create_save_new_account(CAUrl); + {ok, AccId, PrivateKey} -> + {ok, AccId, PrivateKey} + end. + + -spec get_certificates1(url(), domains_opt(), jose_jwk:key()) -> string(). get_certificates1(CAUrl, "all", PrivateKey) -> Hosts = get_config_hosts(), diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 5a313511f..e41a53994 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -45,7 +45,7 @@ %% Migration jabberd1.4 import_file/1, import_dir/1, %% Acme - get_certificate/2, + get_certificate/1, renew_certificate/0, list_certificates/1, revoke_certificate/1, @@ -248,13 +248,11 @@ get_commands_spec() -> args = [{file, string}], result = {res, restuple}}, #ejabberd_commands{name = get_certificate, tags = [acme], - desc = "Gets a certificate for all or the specified domains {all|domain1;domain2;...}. Can be used with {old-account|new-account}.", + desc = "Gets a certificate for all or the specified domains {all|domain1;domain2;...}.", module = ?MODULE, function = get_certificate, - args_desc = ["Domains for which to acquire a certificate", - "Whether to create a new account or use the existing one"], - args_example = ["all | www.example.com;www.example1.net", - "old-account | new-account"], - args = [{domains, string}, {option, string}], + args_desc = ["Domains for which to acquire a certificate"], + args_example = ["all | www.example.com;www.example1.net"], + args = [{domains, string}], result = {certificates, string}}, #ejabberd_commands{name = renew_certificate, tags = [acme], desc = "Renews all certificates that are close to expiring", @@ -577,15 +575,10 @@ import_dir(Path) -> %%% Acme %%% -get_certificate(Domains, UseNewAccount) -> +get_certificate(Domains) -> case ejabberd_acme:is_valid_domain_opt(Domains) of true -> - case ejabberd_acme:is_valid_account_opt(UseNewAccount) of - true -> - ejabberd_acme:get_certificates("http://localhost:4000", Domains, UseNewAccount); - false -> - io_lib:format("Invalid account option: ~p", [UseNewAccount]) - end; + ejabberd_acme:get_certificates("http://localhost:4000", Domains); false -> String = io_lib:format("Invalid domains: ~p", [Domains]) end. From 73f0b6707a0d1245923b372ba0e0424d17145ba3 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 12 Aug 2017 15:59:54 +0300 Subject: [PATCH 52/75] Move the ca_url to the config file --- ejabberd.yml.example | 4 ++-- src/ejabberd_acme.erl | 34 ++++++++++++++++++++++++---------- src/ejabberd_admin.erl | 6 +++--- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 85754e1bb..ee3dda24c 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -664,11 +664,11 @@ language: "en" ###' ACME ## -## Must contain a contact and a directory that the Http Challenges can be solved at +## Must contain a contact and the ACME CA url ## acme: contact: "mailto:cert-admin-ejabberd@example.com" - http_dir: "/home/konstantinos/Desktop/Programming/test-server-for-acme/" + ca_url: "http://localhost:4000" cert_dir: "/usr/local/var/lib/ejabberd/" diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index e84493beb..541aa2879 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,10 +1,10 @@ -module (ejabberd_acme). -export([%% Ejabberdctl Commands - get_certificates/3, - renew_certificates/1, + get_certificates/2, + renew_certificates/0, list_certificates/1, - revoke_certificate/2, + revoke_certificate/1, %% Command Options Validity is_valid_account_opt/1, is_valid_verbose_opt/1, @@ -60,9 +60,10 @@ is_valid_domain_opt(DomainString) -> %% Get Certificate %% --spec get_certificates(url(), domains_opt(), account_opt()) -> string() | {'error', _}. -get_certificates(CAUrl, Domains, NewAccountOpt) -> +-spec get_certificates(domains_opt(), account_opt()) -> string() | {'error', _}. +get_certificates(Domains, NewAccountOpt) -> try + CAUrl = binary_to_list(get_config_ca_url()), get_certificates0(CAUrl, Domains, NewAccountOpt) catch throw:Throw -> @@ -266,9 +267,10 @@ ensure_account_exists() -> %% %% Renew Certificates %% --spec renew_certificates(url()) -> string() | {'error', _}. -renew_certificates(CAUrl) -> +-spec renew_certificates() -> string() | {'error', _}. +renew_certificates() -> try + CAUrl = binary_to_list(get_config_ca_url()), renew_certificates0(CAUrl) catch throw:Throw -> @@ -454,10 +456,10 @@ get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) -> %% Revoke Certificate %% -%% Add a try-catch to this stub --spec revoke_certificate(url(), string()) -> {ok, deleted} | {error, _}. -revoke_certificate(CAUrl, Domain) -> +-spec revoke_certificate(string()) -> {ok, deleted} | {error, _}. +revoke_certificate(Domain) -> try + CAUrl = binary_to_list(get_config_ca_url()), revoke_certificate0(CAUrl, Domain) catch throw:Throw -> @@ -968,6 +970,18 @@ get_config_contact() -> throw({error, configuration_contact}) end. +-spec get_config_ca_url() -> bitstring(). +get_config_ca_url() -> + Acme = get_config_acme(), + case lists:keyfind(ca_url, 1, Acme) of + {ca_url, CAUrl} -> + CAUrl; + false -> + ?ERROR_MSG("No CA url has been specified", []), + throw({error, configuration_ca_url}) + end. + + -spec get_config_hosts() -> [bitstring()]. get_config_hosts() -> case ejabberd_config:get_option(hosts, undefined) of diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 5a313511f..ceafed567 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -582,7 +582,7 @@ get_certificate(Domains, UseNewAccount) -> true -> case ejabberd_acme:is_valid_account_opt(UseNewAccount) of true -> - ejabberd_acme:get_certificates("http://localhost:4000", Domains, UseNewAccount); + ejabberd_acme:get_certificates(Domains, UseNewAccount); false -> io_lib:format("Invalid account option: ~p", [UseNewAccount]) end; @@ -591,7 +591,7 @@ get_certificate(Domains, UseNewAccount) -> end. renew_certificate() -> - ejabberd_acme:renew_certificates("http://localhost:4000"). + ejabberd_acme:renew_certificates(). list_certificates(Verbose) -> case ejabberd_acme:is_valid_verbose_opt(Verbose) of @@ -603,7 +603,7 @@ list_certificates(Verbose) -> end. revoke_certificate(Domain) -> - ejabberd_acme:revoke_certificate("http://localhost:4000", Domain). + ejabberd_acme:revoke_certificate(Domain). %%% %%% Purge DB From a72a7f830af9bd72a86cbc7aed2f78e46ea0a781 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 12 Aug 2017 17:14:23 +0300 Subject: [PATCH 53/75] Add support to revoke a certificate by providing the pem This is important so that a user can revoke a certificate that is not acquired or logged from our acme client --- src/ejabberd_acme.erl | 52 ++++++++++++++++++++++++++++++++---------- src/ejabberd_admin.erl | 14 ++++++++---- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 541aa2879..dd8f08575 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -9,6 +9,7 @@ is_valid_account_opt/1, is_valid_verbose_opt/1, is_valid_domain_opt/1, + is_valid_revoke_cert/1, %% Key Related generate_key/0, to_public/1 @@ -53,6 +54,11 @@ is_valid_domain_opt(DomainString) -> SeparatedDomains -> true end. + +-spec is_valid_revoke_cert(string()) -> boolean(). +is_valid_revoke_cert(DomainOrFile) -> + lists:prefix("file:", DomainOrFile) orelse + lists:prefix("domain:", DomainOrFile). @@ -457,10 +463,10 @@ get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) -> %% -spec revoke_certificate(string()) -> {ok, deleted} | {error, _}. -revoke_certificate(Domain) -> +revoke_certificate(DomainOrFile) -> try CAUrl = binary_to_list(get_config_ca_url()), - revoke_certificate0(CAUrl, Domain) + revoke_certificate0(CAUrl, DomainOrFile) catch throw:Throw -> Throw; @@ -469,28 +475,50 @@ revoke_certificate(Domain) -> {error, revoke_certificate} end. --spec revoke_certificate0(url(), string()) -> {ok, deleted} | {error, not_found}. -revoke_certificate0(CAUrl, Domain) -> - BinDomain = list_to_bitstring(Domain), - case domain_certificate_exists(BinDomain) of - {BinDomain, Certificate} -> - ok = revoke_certificate1(CAUrl, Certificate), +-spec revoke_certificate0(url(), string()) -> {ok, deleted}. +revoke_certificate0(CAUrl, DomainOrFile) -> + ParsedCert = parse_revoke_cert_argument(DomainOrFile), + revoke_certificate1(CAUrl, ParsedCert). + +-spec revoke_certificate1(url(), {domain, bitstring()} | {file, file:filename()}) -> + {ok, deleted}. +revoke_certificate1(CAUrl, {domain, Domain}) -> + case domain_certificate_exists(Domain) of + {Domain, Cert = #data_cert{pem=PemCert}} -> + ok = revoke_certificate2(CAUrl, PemCert), + ok = remove_certificate_persistent(Cert), {ok, deleted}; false -> - {error, not_found} + ?ERROR_MSG("Certificate for domain: ~p not found", [Domain]), + throw({error, not_found}) + end; +revoke_certificate1(CAUrl, {file, File}) -> + case file:read_file(File) of + {ok, Pem} -> + ok = revoke_certificate2(CAUrl, Pem), + {ok, deleted}; + {error, Reason} -> + ?ERROR_MSG("Error: ~p reading pem certificate-key file: ~p", [Reason, File]), + throw({error, Reason}) end. + --spec revoke_certificate1(url(), data_cert()) -> ok. -revoke_certificate1(CAUrl, Cert = #data_cert{pem=PemEncodedCert}) -> +-spec revoke_certificate2(url(), data_cert()) -> ok. +revoke_certificate2(CAUrl, PemEncodedCert) -> {Certificate, CertPrivateKey} = prepare_certificate_revoke(PemEncodedCert), {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), Req = [{<<"certificate">>, Certificate}], {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, CertPrivateKey, Req, Nonce), - ok = remove_certificate_persistent(Cert), ok. +-spec parse_revoke_cert_argument(string()) -> {domain, bitstring()} | {file, file:filename()}. +parse_revoke_cert_argument([$f, $i, $l, $e, $:|File]) -> + {file, File}; +parse_revoke_cert_argument([$d, $o, $m, $a, $i, $n, $: | Domain]) -> + {domain, list_to_bitstring(Domain)}. + -spec prepare_certificate_revoke(pem()) -> bitstring(). prepare_certificate_revoke(PemEncodedCert) -> PemList = public_key:pem_decode(PemEncodedCert), diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index ceafed567..87ddb9327 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -270,8 +270,8 @@ get_commands_spec() -> #ejabberd_commands{name = revoke_certificate, tags = [acme], desc = "Revokes the selected certificate", module = ?MODULE, function = revoke_certificate, - args_desc = ["The domain of the certificate in question"], - args = [{domain, string}], + args_desc = ["The domain or file (in pem format) of the certificate in question {domain:Domain | file:File}"], + args = [{domain_or_file, string}], result = {res, restuple}}, #ejabberd_commands{name = import_piefxis, tags = [mnesia], @@ -602,8 +602,14 @@ list_certificates(Verbose) -> {invalid_option, String} end. -revoke_certificate(Domain) -> - ejabberd_acme:revoke_certificate(Domain). +revoke_certificate(DomainOrFile) -> + case ejabberd_acme:is_valid_revoke_cert(DomainOrFile) of + true -> + ejabberd_acme:revoke_certificate(DomainOrFile); + false -> + String = io_lib:format("Bad argument: ~s", [DomainOrFile]), + {invalid_argument, String} + end. %%% %%% Purge DB From 3b22efeaeea139015bc631b04029b2af4bdf5295 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 12 Aug 2017 17:26:07 +0300 Subject: [PATCH 54/75] Add throws when http requests fail This was done in order to show the unexpected code in the top level --- src/ejabberd_acme_comm.erl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ejabberd_acme_comm.erl b/src/ejabberd_acme_comm.erl index b66d5f610..99eaff87b 100644 --- a/src/ejabberd_acme_comm.erl +++ b/src/ejabberd_acme_comm.erl @@ -291,7 +291,7 @@ prepare_get_request(Url, HandleRespFun, ResponseType) -> -spec sign_json_jose(jose_jwk:key(), bitstring(), nonce()) -> {_, jws()}. sign_json_jose(Key, Json, Nonce) -> - PubKey = jose_jwk:to_public(Key), + PubKey = ejabberd_acme:to_public(Key), {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), PubKeyJson = jiffy:decode(BinaryPubKey), %% TODO: Ensure this works for all cases @@ -383,11 +383,11 @@ decode(Json) -> %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -spec failed_http_request({ok, _} | {error, _}, url()) -> {error, _}. -failed_http_request({ok, {{_, Code, _}, _Head, Body}}, Url) -> +failed_http_request({ok, {{_, Code, Reason}, _Head, Body}}, Url) -> ?ERROR_MSG("Got unexpected status code from <~s>: ~B, Body: ~s", [Url, Code, Body]), - {error, unexpected_code}; + throw({error, {unexpected_code, Code, Reason}}); failed_http_request({error, Reason}, Url) -> ?ERROR_MSG("Error making a request to <~s>: ~p", [Url, Reason]), - {error, Reason}. + throw({error, Reason}). From 051e2c639ce59a288a15baad6dadffd18669eb53 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 12 Aug 2017 18:00:46 +0300 Subject: [PATCH 55/75] Change some specs --- src/ejabberd_acme.erl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index dd8f08575..749c505e7 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -303,7 +303,7 @@ renew_certificates0(CAUrl) -> %% Format the result to send back to ejabberdctl format_get_certificates_result(SavedCerts). --spec renew_certificate(url(), data_cert(), jose_jwk:key()) -> +-spec renew_certificate(url(), {bitstring(), data_cert()}, jose_jwk:key()) -> {'ok', bitstring(), _} | {'error', bitstring(), _}. renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) -> @@ -315,7 +315,7 @@ renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) -> end. --spec cert_to_expire(data_cert()) -> boolean(). +-spec cert_to_expire({bitstring(), data_cert()}) -> boolean(). cert_to_expire({DomainName, #data_cert{pem = Pem}}) -> Certificate = pem_to_certificate(Pem), Validity = get_utc_validity(Certificate), @@ -384,7 +384,7 @@ format_certificate(DataCert, Verbose) -> fail_format_certificate(DomainName) end. --spec format_certificate_plain(bitstring(), string(), string()) -> string(). +-spec format_certificate_plain(bitstring(), {expired | ok, string()}, string()) -> string(). format_certificate_plain(DomainName, NotAfter, Path) -> Result = lists:flatten(io_lib:format( " Domain: ~s~n" @@ -393,7 +393,7 @@ format_certificate_plain(DomainName, NotAfter, Path) -> [DomainName, format_validity(NotAfter), Path])), Result. --spec format_certificate_verbose(bitstring(), string(), bitstring()) -> string(). +-spec format_certificate_verbose(bitstring(), {expired | ok, string()}, bitstring()) -> string(). format_certificate_verbose(DomainName, NotAfter, PemCert) -> Result = lists:flatten(io_lib:format( " Domain: ~s~n" @@ -503,7 +503,7 @@ revoke_certificate1(CAUrl, {file, File}) -> end. --spec revoke_certificate2(url(), data_cert()) -> ok. +-spec revoke_certificate2(url(), pem()) -> ok. revoke_certificate2(CAUrl, PemEncodedCert) -> {Certificate, CertPrivateKey} = prepare_certificate_revoke(PemEncodedCert), @@ -519,7 +519,7 @@ parse_revoke_cert_argument([$f, $i, $l, $e, $:|File]) -> parse_revoke_cert_argument([$d, $o, $m, $a, $i, $n, $: | Domain]) -> {domain, list_to_bitstring(Domain)}. --spec prepare_certificate_revoke(pem()) -> bitstring(). +-spec prepare_certificate_revoke(pem()) -> {bitstring(), jose_jwk:key()}. prepare_certificate_revoke(PemEncodedCert) -> PemList = public_key:pem_decode(PemEncodedCert), PemCertEnc = lists:keyfind('Certificate', 1, PemList), @@ -527,7 +527,7 @@ prepare_certificate_revoke(PemEncodedCert) -> DerCert = public_key:der_encode('Certificate', PemCert), Base64Cert = base64url:encode(DerCert), - Key = find_private_key_in_pem(PemEncodedCert), + {ok, Key} = find_private_key_in_pem(PemEncodedCert), {Base64Cert, Key}. -spec domain_certificate_exists(bitstring()) -> {bitstring(), data_cert()} | false. @@ -755,7 +755,7 @@ find_private_key_in_pem(Pem) -> PemKey -> Key = public_key:pem_entry_decode(PemKey), JoseKey = jose_jwk:from_key(Key), - JoseKey + {ok, JoseKey} end. From ddfe8742c7dde56f544e0e690926dda358a07663 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 19 Aug 2017 11:35:15 +0300 Subject: [PATCH 56/75] Add behaviour ejabberd_config in ejabberd_acme in order to validate the config --- src/ejabberd_acme.erl | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 749c505e7..fcb399d96 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -22,6 +22,9 @@ -include("ejabberd_acme.hrl"). -include_lib("public_key/include/public_key.hrl"). +-export([opt_type/1]). + +-behavior(ejabberd_config). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -69,7 +72,7 @@ is_valid_revoke_cert(DomainOrFile) -> -spec get_certificates(domains_opt(), account_opt()) -> string() | {'error', _}. get_certificates(Domains, NewAccountOpt) -> try - CAUrl = binary_to_list(get_config_ca_url()), + CAUrl = get_config_ca_url(), get_certificates0(CAUrl, Domains, NewAccountOpt) catch throw:Throw -> @@ -276,7 +279,7 @@ ensure_account_exists() -> -spec renew_certificates() -> string() | {'error', _}. renew_certificates() -> try - CAUrl = binary_to_list(get_config_ca_url()), + CAUrl = get_config_ca_url(), renew_certificates0(CAUrl) catch throw:Throw -> @@ -465,7 +468,7 @@ get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) -> -spec revoke_certificate(string()) -> {ok, deleted} | {error, _}. revoke_certificate(DomainOrFile) -> try - CAUrl = binary_to_list(get_config_ca_url()), + CAUrl = get_config_ca_url(), revoke_certificate0(CAUrl, DomainOrFile) catch throw:Throw -> @@ -998,7 +1001,7 @@ get_config_contact() -> throw({error, configuration_contact}) end. --spec get_config_ca_url() -> bitstring(). +-spec get_config_ca_url() -> string(). get_config_ca_url() -> Acme = get_config_acme(), case lists:keyfind(ca_url, 1, Acme) of @@ -1044,3 +1047,32 @@ generate_key() -> jose_jwk:generate_key({ec, secp256r1}). -endif. +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Option Parsing Code +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +parse_acme_opts(AcmeOpt) -> + [parse_acme_opt(Opt) || Opt <- AcmeOpt]. + + +parse_acme_opt({ca_url, CaUrl}) when is_bitstring(CaUrl) -> + {ca_url, binary_to_list(CaUrl)}; +parse_acme_opt({contact, Contact}) when is_bitstring(Contact) -> + {contact, Contact}. + +parse_cert_dir_opt(Opt) when is_bitstring(Opt) -> + true = filelib:is_dir(Opt), + Opt. + +-spec opt_type(acme) -> fun(([{ca_url, string()} | {contact, bitstring()}]) -> + ([{ca_url, string()} | {contact, bitstring()}])); + (cert_dir) -> fun((bitstring()) -> (bitstring())); + (atom()) -> [atom()]. +opt_type(acme) -> + fun parse_acme_opts/1; +opt_type(cert_dir) -> + fun parse_cert_dir_opt/1; +opt_type(_) -> + [acme, cert_dir]. From 7cc7b74f1e8966c7e92e63bb5c604ee12da93fb5 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 19 Aug 2017 12:50:40 +0300 Subject: [PATCH 57/75] Add acme certificates for all configured hosts in ejabberd_pkix --- src/ejabberd_acme.erl | 21 +++++++++++++++++++++ src/ejabberd_pkix.erl | 27 ++++++++++++++++++--------- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index fcb399d96..62368abee 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -10,6 +10,8 @@ is_valid_verbose_opt/1, is_valid_domain_opt/1, is_valid_revoke_cert/1, + %% Called by ejabberd_pkix + certificate_exists/1, %% Key Related generate_key/0, to_public/1 @@ -539,6 +541,25 @@ domain_certificate_exists(Domain) -> lists:keyfind(Domain, 1, Certs). +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% +%% Called by ejabberd_pkix to check +%% if a certificate exists for a +%% specific host +%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +-spec certificate_exists(bitstring()) -> {true, file:filename()} | false. +certificate_exists(Host) -> + Certificates = read_certificates_persistent(), + case lists:keyfind(Host, 1 , Certificates) of + false -> + false; + {Host, #data_cert{path=Path}} -> + {true, Path} + end. + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% Certificate Request Functions diff --git a/src/ejabberd_pkix.erl b/src/ejabberd_pkix.erl index f9f0472f6..89b33b8aa 100644 --- a/src/ejabberd_pkix.erl +++ b/src/ejabberd_pkix.erl @@ -204,15 +204,24 @@ add_certfiles(State) -> end, State, ejabberd_config:get_myhosts()). add_certfiles(Host, State) -> - lists:foldl( - fun(Opt, AccState) -> - case ejabberd_config:get_option({Opt, Host}) of - undefined -> AccState; - Path -> - {_, NewAccState} = add_certfile(Path, AccState), - NewAccState - end - end, State, [c2s_certfile, s2s_certfile, domain_certfile]). + NewState = + lists:foldl( + fun(Opt, AccState) -> + case ejabberd_config:get_option({Opt, Host}) of + undefined -> AccState; + Path -> + {_, NewAccState} = add_certfile(Path, AccState), + NewAccState + end + end, State, [c2s_certfile, s2s_certfile, domain_certfile]), + %% Add acme certificate if it exists + case ejabberd_acme:certificate_exists(Host) of + {true, Path} -> + {_, FinalState} = add_certfile(Path, NewState), + FinalState; + false -> + NewState + end. add_certfile(Path, State) -> case maps:get(Path, State#state.certs, undefined) of From e45f7ddfec2a7e2dec8f6cf0c6f2b873ee288d70 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 19 Aug 2017 13:32:13 +0300 Subject: [PATCH 58/75] Cleanup some comments: --- src/ejabberd_acme.erl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 62368abee..5151e3fbc 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -568,10 +568,6 @@ certificate_exists(Host) -> %% For now we accept only generating a key of %% specific type for signing the csr -%% TODO: Make this function handle more signing keys -%% 1. Derive oid from Key -%% 2. Derive the whole algo objects from Key -%% TODO: Encode Strings using length using a library function -spec make_csr(proplist()) -> {binary(), jose_jwk:key()}. make_csr(Attributes) -> @@ -722,7 +718,6 @@ get_challenges(Body) -> -spec not_before_not_after() -> {binary(), binary()}. not_before_not_after() -> - %% TODO: Make notBefore and notAfter configurable somewhere {MegS, Sec, MicS} = erlang:timestamp(), NotBefore = xmpp_util:encode_timestamp({MegS, Sec, MicS}), %% The certificate will be valid for 90 Days after today From 15dd88385fccd4880ba0b017ebdb83e05330a9c2 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 19 Aug 2017 16:58:06 +0300 Subject: [PATCH 59/75] Delete a development acme module --- src/acme_challenge.erl | 1 + src/acme_experimental.erl | 607 -------------------------------------- src/ejabberd_acme.erl | 2 +- 3 files changed, 2 insertions(+), 608 deletions(-) delete mode 100644 src/acme_experimental.erl diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index 081e10429..de0df8363 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -87,6 +87,7 @@ solve_challenge1(Challenge, _Key) -> ?INFO_MSG("Challenge: ~p~n", [Challenge]). +%% Old way of solving challenges save_key_authorization(Chal, Tkn, KeyAuthz, HttpDir) -> FileLocation = HttpDir ++ "/.well-known/acme-challenge/" ++ bitstring_to_list(Tkn), case file:write_file(FileLocation, KeyAuthz) of diff --git a/src/acme_experimental.erl b/src/acme_experimental.erl deleted file mode 100644 index 08fdc6ff4..000000000 --- a/src/acme_experimental.erl +++ /dev/null @@ -1,607 +0,0 @@ --module(acme_experimental). - --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 - , new_reg/2 - , update_account/2 - , account_info/2 %% TODO: Maybe change to get_account - , account_key_change/2 - , deactivate_account/2 - %% Orders/Certificates - , new_cert/2 - , 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"). --include_lib("public_key/include/public_key.hrl"). - -% -define(CA_URL, "https://acme-v01.api.letsencrypt.org"). - - - --define(DEFAULT_DIRECTORY, ?CA_URL ++ "/directory"). --define(DEFAULT_NEW_NONCE, ?CA_URL ++ "/acme/new_nonce"). - --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(), - dir_url = ?DEFAULT_DIRECTORY :: list(), - dirs = maps:new(), - nonce = "", - account = none - }). - -%% 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). - -new_reg(Pid, Options) -> - gen_server:call(Pid, ?FUNCTION_NAME). - -update_account(Pid, AccountId) -> - %% TODO: This has to have more info ofcourse - gen_server:call(Pid, {?FUNCTION_NAME, AccountId}). - -account_info(Pid, AccountId) -> - gen_server:call(Pid, {?FUNCTION_NAME, AccountId}). - -account_key_change(Pid, Options) -> - ok. - -deactivate_account(Pid, Options) -> - ok. - -new_cert(Pid, Options) -> - gen_server:call(Pid, ?FUNCTION_NAME). - -new_authz(Pid, Options) -> - gen_server:call(Pid, ?FUNCTION_NAME). - -get_certificate(Pid, Options) -> - ok. - -get_authz(Pid, Options) -> - gen_server:call(Pid, ?FUNCTION_NAME). - -complete_challenge(Pid, Options) -> - gen_server:call(Pid, {?FUNCTION_NAME, Options}). - -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), - - ok = application:start(base64url), - ok = application:start(jose), - - {ok, #state{}}. - -handle_call(directory, _From, S = #state{dir_url=Url, dirs=Dirs}) -> - %% Make the get request - {ok, {_Status, Head, Body}} = httpc:request(get, {Url, []}, [], []), - - %% Decode the json string - {Directories} = jiffy:decode(Body), - StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X,Y} <- Directories], - - % Find and save the replay nonce - % io:format("Directory Head Response: ~p~n", [Head]), - Nonce = get_nonce(Head), - - %% Update the directories in state - NewDirs = maps:from_list(StrDirectories), - % io:format("New directories: ~p~n", [NewDirs]), - - {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, - {ok, {Status, Head, []}} = - httpc:request(head, {Url, []}, [], []), - {reply, {ok, {Status, Head}}, S}; -handle_call(new_reg, _From, S = #state{ca_url = Ca, dirs=Dirs, nonce = Nonce}) -> - %% Get url from all directories - #{"new-reg" := Url} = Dirs, - - %% Make the request body - ReqBody = jiffy:encode({ - [ { <<"contact">>, [<<"mailto:cert-admin@example.com">>]} - , { <<"resource">>, <<"new-reg">>} - ]}), - - %% 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), - - %% 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({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, - - MyCSR = make_csr(), - % file:write_file("myCSR.der", CSR), - % {ok, CSR} = file:read_file("CSR.der"), - % io:format("CSR: ~p~nMy Encoded CSR: ~p~nCorrect Encoded CSR: ~p~n", - % [ public_key:der_decode('CertificationRequest', CSR) - % , MyCSR - % , CSR]), - - CSRbase64 = base64url:encode(MyCSR), - - % io:format("CSR base64: ~p~n", [CSRbase64]), - {MegS, Sec, MicS} = erlang:timestamp(), - NotBefore = xmpp_util:encode_timestamp({MegS-1, Sec, MicS}), - NotAfter = xmpp_util:encode_timestamp({MegS+1, Sec, MicS}), - - %% Make the request body - ReqBody = jiffy:encode({[ - {<<"resource">>, <<"new-cert">>}, - {<<"csr">>, CSRbase64}, - {<<"notBefore">>, NotBefore}, - {<<"NotAfter">>, NotAfter} - ]}), - %% 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/pkix-cert", 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({ - [ { <<"identifier">>, { - [ {<<"type">>, <<"dns">>} - , {<<"value">>, <<"my-acme-test-ejabberd.com">>} - ] }} - , {<<"existing">>, <<"accept">>} - , { <<"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}}; -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), - - %% 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(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}. - -%% Util functions - -final_url(Urls) -> - Joined = lists:join("/", Urls), - lists:flatten(Joined). - -get_nonce(Head) -> - {"replay-nonce", Nonce} = proplists:lookup("replay-nonce", Head), - Nonce. - -get_challenges({Body}) -> - {<<"challenges">>, Challenges} = proplists:lookup(<<"challenges">>, Body), - Challenges. - - -%% Test - -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) -> - % Generate a public key - PubKey = jose_jwk:to_public(Key), - % io:format("Public Key: ~p~n", [PubKey]), - {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), - % io:format("Public Key: ~p~n", [BinaryPubKey]), - PubKeyJson = jiffy:decode(BinaryPubKey), - % io:format("Public Key: ~p~n", [PubKeyJson]), - - % Jws object containing the algorithm - JwsObj = jose_jws:from( - #{ <<"alg">> => <<"ES256">> - %% Im not sure if it is needed - % , <<"b64">> => true - , <<"jwk">> => PubKeyJson - , <<"nonce">> => list_to_bitstring(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)]), - - %% Peek Payload - Payload = jose_jws:peek_payload(Signed), - io:format("Payload: ~p~n", [jiffy:decode(Payload)]), - - %% Verify - % {true, _} = jose_jws:verify(Key, Signed), - % io:format("Verify: ~p~n", [jose_jws:verify(Key, Signed)]), - - Signed. - -make_csr() -> - - SigningKey = jose_jwk:from_pem_file("csr_signing_private_key.key"), - {_, PrivateKey} = jose_jwk:to_key(SigningKey), - % io:format("PrivateKey: ~p~n", [PrivateKey]), - - PubKey = jose_jwk:to_public(SigningKey), - % io:format("Public Key: ~p~n", [PubKey]), - - {_, BinaryPubKey} = jose_jwk:to_binary(PubKey), - % io:format("Public Key: ~p~n", [BinaryPubKey]), - - {_, RawPubKey} = jose_jwk:to_key(PubKey), - % io:format("Raw Public Key: ~p~n", [RawPubKey]), - {{_, RawBinPubKey}, _} = RawPubKey, - % io:format("Encoded Raw Public Key: ~p~n", [RawBinPubKey]), - - %% TODO: Understand how to extract the information below from the key struct - AlgoID = #'CertificationRequestInfo_subjectPKInfo_algorithm'{ - algorithm = {1,2,840,10045,2,1}, %% Very dirty - parameters = {asn1_OPENTYPE,<<6,8,42,134,72,206,61,3,1,7>>} - }, - SubPKInfo = #'CertificationRequestInfo_subjectPKInfo'{ - algorithm = AlgoID, %% Very dirty - subjectPublicKey = RawBinPubKey %% public_key:der_encode('ECPoint', RawPubKey) - }, - - CommonName = #'AttributeTypeAndValue'{ - type = {2,5,4,3}, - % value = list_to_bitstring([12,25] ++ "my-acme-test-ejabberd.com") - value = length_bitstring(<<"my-acme-test-ejabberd.com">>) - }, - CountryName = #'AttributeTypeAndValue'{ - type = {2,5,4,6}, - value = length_bitstring(<<"US">>) - }, - StateOrProvinceName = #'AttributeTypeAndValue'{ - type = {2,5,4,8}, - value = length_bitstring(<<"California">>) - }, - LocalityName = #'AttributeTypeAndValue'{ - type = {2,5,4,7}, - value = length_bitstring(<<"San Jose">>) - }, - OrganizationName = #'AttributeTypeAndValue'{ - type = {2,5,4,10}, - value = length_bitstring(<<"Example">>) - }, - CRI = #'CertificationRequestInfo'{ - version = 0, - % subject = {rdnSequence, [[CommonName]]}, - subject = {rdnSequence, - [ [CommonName] - , [CountryName] - , [StateOrProvinceName] - , [LocalityName] - , [OrganizationName]]}, - subjectPKInfo = SubPKInfo, - attributes = [] - }, - EncodedCRI = public_key:der_encode( - 'CertificationRequestInfo', - CRI), - - SignedCRI = public_key:sign(EncodedCRI, 'sha256', PrivateKey), - - SigningAlgoID = #'CertificationRequest_signatureAlgorithm'{ - algorithm = [1,2,840,10045,4,3,2], %% Very dirty - parameters = asn1_NOVALUE - }, - - CSR = #'CertificationRequest'{ - certificationRequestInfo = CRI, - signatureAlgorithm = SigningAlgoID, - signature = SignedCRI - }, - Result = public_key:der_encode( - 'CertificationRequest', - CSR), - % io:format("My CSR: ~p~n", [CSR]), - - Result. - -%% TODO: Find a correct function to do this -length_bitstring(Bitstring) -> - Size = size(Bitstring), - case Size < 127 of - true -> - <<12, Size, Bitstring/binary>>; - false -> - error(not_implemented) - end. - -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]), - - %% 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 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)]), - - % Get a certification - {ok, {Status4, Head4, Body4}} = - new_cert(Pid, []), - io:format("New Cert~nHead: ~p~nBody: ~p~n", - [{Status4, Head4}, Body4]), - - % make_csr(), - - 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)]). \ No newline at end of file diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 260d994b8..41947ee41 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -1,7 +1,7 @@ -module (ejabberd_acme). -export([%% Ejabberdctl Commands - get_certificates/2, + get_certificates/1, renew_certificates/0, list_certificates/1, revoke_certificate/1, From 9b3e160e1840fa83e0452b351c6d2fbbd1553801 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 19 Aug 2017 17:47:05 +0300 Subject: [PATCH 60/75] Remove some debugging INFO_MSGs --- src/acme_challenge.erl | 6 ++---- src/ejabberd_acme.erl | 3 --- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index de0df8363..0e9e395ee 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -21,8 +21,6 @@ %% TODO: Maybe validate request here?? process(LocalPath, Request) -> Result = ets_get_key_authorization(LocalPath), - ?INFO_MSG("Trying to serve: ~p at: ~p", [Request, LocalPath]), - ?INFO_MSG("Http Response: ~p", [Result]), {200, [{<<"Content-Type">>, <<"text/plain">>}], Result}. @@ -31,7 +29,6 @@ process(LocalPath, Request) -> -spec key_authorization(bitstring(), jose_jwk:key()) -> bitstring(). key_authorization(Token, Key) -> Thumbprint = jose_jwk:thumbprint(Key), - %% ?INFO_MSG("Thumbprint: ~p~n", [Thumbprint]), KeyAuthorization = erlang:iolist_to_binary([Token, <<".">>, Thumbprint]), KeyAuthorization. @@ -84,7 +81,8 @@ solve_challenge1(Chal = #challenge{type = <<"http-01">>, token=Tkn}, Key) -> ets_put_key_authorization(Tkn, KeyAuthz), {ok, Chal#challenge.uri, KeyAuthz}; solve_challenge1(Challenge, _Key) -> - ?INFO_MSG("Challenge: ~p~n", [Challenge]). + ?ERROR_MSG("Unkown Challenge Type: ~p", [Challenge]), + {error, unknown_challenge}. %% Old way of solving challenges diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 41947ee41..c5de986e1 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -155,7 +155,6 @@ format_get_certificate({error, Domain, Reason}) -> {'ok', bitstring(), pem()} | {'error', bitstring(), _}. get_certificate(CAUrl, DomainName, PrivateKey) -> - ?INFO_MSG("Getting a Certificate for domain: ~p~n", [DomainName]), try {ok, _Authz} = create_new_authorization(CAUrl, DomainName, PrivateKey), create_new_certificate(CAUrl, DomainName, PrivateKey) @@ -1056,14 +1055,12 @@ get_config_cert_dir() -> -ifdef(GENERATE_RSA_KEY). generate_key() -> - ?INFO_MSG("Generate RSA key pair~n", []), Key = public_key:generate_key({rsa, 2048, 65537}), Key1 = Key#'RSAPrivateKey'{version = 'two-prime'}, jose_jwk:from_key(Key1). %% jose_jwk:generate_key({rsa, 2048}). -else. generate_key() -> - ?INFO_MSG("Generate EC key pair~n", []), jose_jwk:generate_key({ec, secp256r1}). -endif. From f2876bdad7cc1f151c37feb41d22a9b6285961ff Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 10:12:42 +0300 Subject: [PATCH 61/75] Add certfile when acquired --- src/ejabberd_acme.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index c5de986e1..869ecf03f 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -970,6 +970,7 @@ save_certificate({ok, DomainName, Cert}) -> path = CertificateFile }, add_certificate_persistent(DataCert), + ejabberd_pkix:add_certfile(CertificateFile), {ok, DomainName, saved} catch throw:Throw -> From 10f7b5a548a9a38c9c98e52a99b4af510f794884 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 10:25:37 +0300 Subject: [PATCH 62/75] Remove partial RSA key support --- rebar.config | 2 -- src/ejabberd_acme.erl | 20 -------------------- 2 files changed, 22 deletions(-) diff --git a/rebar.config b/rebar.config index 0383fb084..d8f19042c 100644 --- a/rebar.config +++ b/rebar.config @@ -96,8 +96,6 @@ {if_have_fun, {rand, uniform, 1}, {d, 'RAND_UNIFORM'}}, {if_have_fun, {gb_sets, iterator_from, 2}, {d, 'GB_SETS_ITERATOR_FROM'}}, {if_have_fun, {public_key, short_name_hash, 1}, {d, 'SHORT_NAME_HASH'}}, - %% {if_have_fun, {public_key, generate_key, 1}, {d, 'GENERATE_RSA_KEY'}}, - {if_version_above, "19", {d, 'GENERATE_RSA_KEY'}}, {if_var_true, hipe, native}, {src_dirs, [asn1, src, {if_var_true, tools, tools}, diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 869ecf03f..952a8cb85 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -730,18 +730,6 @@ not_before_not_after() -> -spec to_public(jose_jwk:key()) -> jose_jwk:key(). to_public(PrivateKey) -> jose_jwk:to_public(PrivateKey). -%% case jose_jwk:to_key(PrivateKey) of -%% #'RSAPrivateKey'{modulus = Mod, publicExponent = Exp} -> -%% Public = #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}, -%% jose_jwk:from_key(Public); -%% _ -> -%% jose_jwk:to_public(PrivateKey) -%% end. - -%% to_public(#'RSAPrivateKey'{modulus = Mod, publicExponent = Exp}) -> -%% #'RSAPublicKey'{modulus = Mod, publicExponent = Exp}; -%% to_public(PrivateKey) -> -%% jose_jwk:to_public(PrivateKey). -spec pem_to_certificate(pem()) -> #'Certificate'{}. pem_to_certificate(Pem) -> @@ -1054,16 +1042,8 @@ get_config_cert_dir() -> end. --ifdef(GENERATE_RSA_KEY). -generate_key() -> - Key = public_key:generate_key({rsa, 2048, 65537}), - Key1 = Key#'RSAPrivateKey'{version = 'two-prime'}, - jose_jwk:from_key(Key1). -%% jose_jwk:generate_key({rsa, 2048}). --else. generate_key() -> jose_jwk:generate_key({ec, secp256r1}). --endif. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% From 6f972fa3fee77c53f48f77e5a867610dfeb81f91 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 10:29:12 +0300 Subject: [PATCH 63/75] Clean run_acme testcase --- run_acme.sh | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/run_acme.sh b/run_acme.sh index c4c7df4d9..227751af9 100755 --- a/run_acme.sh +++ b/run_acme.sh @@ -1,9 +1,15 @@ #!/bin/bash -erl -pa ebin \ -deps/jiffy/ebin \ -deps/fast_tls/ebin \ -deps/jose/ebin \ -deps/base64url/ebin \ -deps/xmpp/ebin \ --noshell -s acme_experimental scenario -s erlang halt \ No newline at end of file + +set -v +sudo ejabberdctl stop +set -e +make +sudo make install +sudo ejabberdctl start +sleep 2 +sudo ejabberdctl get_certificate all +sudo ejabberdctl list_certificates plain + +sudo ejabberdctl revoke_certificate domain:my-test-ejabberd-server6.free +sudo ejabberdctl list_certificates verbose From 37a54cd498a0aae4f4bea51e1072388a216e3c71 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 10:38:33 +0300 Subject: [PATCH 64/75] List the possible ca_urls in example config file --- ejabberd.yml.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 72eb0b4b1..9891e0ed2 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -664,7 +664,12 @@ language: "en" ###' ACME ## -## Must contain a contact and the ACME CA url +## A contact with which it will create an ACME account +## The ACME Certificate Authority URL. +## This could either be: +## - https://acme-v01.api.letsencrypt.org - for the production CA +## - https://acme-staging.api.letsencrypt.org - for the staging CA +## - http://localhost:4000 - for a local version of the CA ## acme: contact: "mailto:cert-admin-ejabberd@example.com" From 25ca6e55820fc1983ab9a9e60469bc94a0526d3b Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 13:36:34 +0300 Subject: [PATCH 65/75] Acquire certificates for all subdomains of a host and include them in SAN --- src/ejabberd_acme.erl | 52 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 952a8cb85..b15909032 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -156,8 +156,12 @@ format_get_certificate({error, Domain, Reason}) -> {'error', bitstring(), _}. get_certificate(CAUrl, DomainName, PrivateKey) -> try - {ok, _Authz} = create_new_authorization(CAUrl, DomainName, PrivateKey), - create_new_certificate(CAUrl, DomainName, PrivateKey) + AllSubDomains = find_all_sub_domains(DomainName), + lists:foreach( + fun(Domain) -> + {ok, _Authz} = create_new_authorization(CAUrl, Domain, PrivateKey) + end, [DomainName|AllSubDomains]), + create_new_certificate(CAUrl, {DomainName, AllSubDomains}, PrivateKey) catch throw:Throw -> Throw; @@ -235,13 +239,14 @@ create_new_authorization(CAUrl, DomainName, PrivateKey) -> throw({error, DomainName, authorization}) end. --spec create_new_certificate(url(), bitstring(), jose_jwk:key()) -> +-spec create_new_certificate(url(), {bitstring(), [bitstring()]}, jose_jwk:key()) -> {ok, bitstring(), pem()}. -create_new_certificate(CAUrl, DomainName, PrivateKey) -> +create_new_certificate(CAUrl, {DomainName, AllSubDomains}, PrivateKey) -> try {ok, Dirs, Nonce0} = ejabberd_acme_comm:directory(CAUrl), CSRSubject = [{commonName, bitstring_to_list(DomainName)}], - {CSR, CSRKey} = make_csr(CSRSubject), + SANs = [{dNSName, SAN} || SAN <- AllSubDomains], + {CSR, CSRKey} = make_csr(CSRSubject, SANs), {NotBefore, NotAfter} = not_before_not_after(), Req = [{<<"csr">>, CSR}, @@ -572,8 +577,9 @@ certificate_exists(Host) -> %% For now we accept only generating a key of %% specific type for signing the csr --spec make_csr(proplist()) -> {binary(), jose_jwk:key()}. -make_csr(Attributes) -> +-spec make_csr(proplist(), [{dNSName, bitstring()}]) + -> {binary(), jose_jwk:key()}. +make_csr(Attributes, SANs) -> Key = generate_key(), {_, KeyKey} = jose_jwk:to_key(Key), KeyPub = to_public(Key), @@ -582,7 +588,8 @@ make_csr(Attributes) -> {ok, RawBinPubKey} = raw_binary_public_key(KeyPub), SubPKInfo = subject_pk_info(SubPKInfoAlgo, RawBinPubKey), {ok, Subject} = attributes_from_list(Attributes), - CRI = certificate_request_info(SubPKInfo, Subject), + ExtensionRequest = extension_request(SANs), + CRI = certificate_request_info(SubPKInfo, Subject, ExtensionRequest), {ok, EncodedCRI} = der_encode( 'CertificationRequestInfo', CRI), @@ -617,12 +624,27 @@ subject_pk_info(Algo, RawBinPubKey) -> subjectPublicKey = RawBinPubKey }. -certificate_request_info(SubPKInfo, Subject) -> +extension(SANs) -> + #'Extension'{ + extnID = attribute_oid(subjectAltName), + critical = false, + extnValue = public_key:der_encode('SubjectAltName', SANs)}. + +extension_request(SANs) -> + #'AttributePKCS-10'{ + type = ?'pkcs-9-at-extensionRequest', + values = [{'asn1_OPENTYPE', + public_key:der_encode( + 'ExtensionRequest', + [extension(SANs)])}] + }. + +certificate_request_info(SubPKInfo, Subject, ExtensionRequest) -> #'CertificationRequestInfo'{ version = 0, subject = Subject, subjectPKInfo = SubPKInfo, - attributes = [] + attributes = [ExtensionRequest] }. signature_algo(_Key, _Hash) -> @@ -693,6 +715,7 @@ attribute_oid(countryName) -> ?'id-at-countryName'; attribute_oid(stateOrProvinceName) -> ?'id-at-stateOrProvinceName'; attribute_oid(localityName) -> ?'id-at-localityName'; attribute_oid(organizationName) -> ?'id-at-organizationName'; +attribute_oid(subjectAltName) -> ?'id-ce-subjectAltName'; attribute_oid(_) -> error(bad_attributes). @@ -793,6 +816,15 @@ private_key_types() -> 'DSAPrivateKey', 'ECPrivateKey']. +-spec find_all_sub_domains(bitstring()) -> [bitstring()]. +find_all_sub_domains(DomainName) -> + AllRoutes = ejabberd_router:get_all_routes(), + DomainLen = size(DomainName), + [Route || Route <- AllRoutes, + binary:longest_common_suffix([DomainName, Route]) + =:= DomainLen]. + + -spec is_error(_) -> boolean(). is_error({error, _}) -> true; is_error({error, _, _}) -> true; From 62903155fd28daf474f339fd4bf0233c9c926eee Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 14:44:19 +0300 Subject: [PATCH 66/75] Show SANs in list_Certificates --- src/ejabberd_acme.erl | 46 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index b15909032..b28cb1cdc 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -385,11 +385,14 @@ format_certificate(DataCert, Verbose) -> %% Find the notAfter date NotAfter = get_notAfter(Certificate), + %% Find the subjectAltNames + SANs = get_subjectAltNames(Certificate), + case Verbose of "plain" -> - format_certificate_plain(DomainName, NotAfter, Path); + format_certificate_plain(DomainName, SANs, NotAfter, Path); "verbose" -> - format_certificate_verbose(DomainName, NotAfter, PemCert) + format_certificate_verbose(DomainName, SANs, NotAfter, PemCert) end catch E:R -> @@ -397,22 +400,30 @@ format_certificate(DataCert, Verbose) -> fail_format_certificate(DomainName) end. --spec format_certificate_plain(bitstring(), {expired | ok, string()}, string()) -> string(). -format_certificate_plain(DomainName, NotAfter, Path) -> +-spec format_certificate_plain(bitstring(), [string()], {expired | ok, string()}, string()) + -> string(). +format_certificate_plain(DomainName, SANs, NotAfter, Path) -> Result = lists:flatten(io_lib:format( " Domain: ~s~n" + "~s" " ~s~n" " Path: ~s", - [DomainName, format_validity(NotAfter), Path])), + [DomainName, + lists:flatten([io_lib:format(" SAN: ~s~n", [SAN]) || SAN <- SANs]), + format_validity(NotAfter), Path])), Result. --spec format_certificate_verbose(bitstring(), {expired | ok, string()}, bitstring()) -> string(). -format_certificate_verbose(DomainName, NotAfter, PemCert) -> +-spec format_certificate_verbose(bitstring(), [string()], {expired | ok, string()}, bitstring()) + -> string(). +format_certificate_verbose(DomainName, SANs, NotAfter, PemCert) -> Result = lists:flatten(io_lib:format( - " Domain: ~s~n" + " Domain: ~s~n" + "~s" " ~s~n" " Certificate In PEM format: ~n~s", - [DomainName, format_validity(NotAfter), PemCert])), + [DomainName, + lists:flatten([io_lib:format(" SAN: ~s~n", [SAN]) || SAN <- SANs]), + format_validity(NotAfter), PemCert])), Result. -spec format_validity({'expired' | 'ok', string()}) -> string(). @@ -462,6 +473,23 @@ get_notAfter(Certificate) -> {ok, NotAfter} end. +-spec get_subjectAltNames(#'Certificate'{}) -> [string()]. +get_subjectAltNames(#'Certificate'{tbsCertificate = TbsCertificate}) -> + #'TBSCertificate'{ + extensions = Exts + } = TbsCertificate, + + EncodedSANs = [Val || #'Extension'{extnID = Oid, extnValue = Val} <- Exts, + Oid =:= attribute_oid(subjectAltName)], + + lists:flatmap( + fun(EncSAN) -> + SANs0 = public_key:der_decode('SubjectAltName', EncSAN), + [Name || {dNSName, Name} <- SANs0] + end, EncodedSANs). + + + -spec get_utc_validity(#'Certificate'{}) -> string(). get_utc_validity(#'Certificate'{tbsCertificate = TbsCertificate}) -> #'TBSCertificate'{ From 8c56fbc0d8a7298f6abcfafdee95c45de7822f39 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 14:53:38 +0300 Subject: [PATCH 67/75] Remove debugging script --- run_acme.sh | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100755 run_acme.sh diff --git a/run_acme.sh b/run_acme.sh deleted file mode 100755 index 227751af9..000000000 --- a/run_acme.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - - -set -v -sudo ejabberdctl stop -set -e -make -sudo make install -sudo ejabberdctl start -sleep 2 -sudo ejabberdctl get_certificate all -sudo ejabberdctl list_certificates plain - -sudo ejabberdctl revoke_certificate domain:my-test-ejabberd-server6.free -sudo ejabberdctl list_certificates verbose From 30e729a1502f867ee8deb391d50497f4c870b1c6 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 14:54:23 +0300 Subject: [PATCH 68/75] Whitespace change --- src/ejabberd_admin.erl | 50 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 8d022e606..6db4d4e84 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -799,31 +799,31 @@ mnesia_change_nodename(FromString, ToString, Source, Target) -> end, Convert = fun - ({schema, db_nodes, Nodes}, Acc) -> - io:format(" +++ db_nodes ~p~n", [Nodes]), - {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; - ({schema, version, Version}, Acc) -> - io:format(" +++ version: ~p~n", [Version]), - {[{schema, version, Version}], Acc}; - ({schema, cookie, Cookie}, Acc) -> - io:format(" +++ cookie: ~p~n", [Cookie]), - {[{schema, cookie, Cookie}], Acc}; - ({schema, Tab, CreateList}, Acc) -> - io:format("~n * Checking table: '~p'~n", [Tab]), - Keys = [ram_copies, disc_copies, disc_only_copies], - OptSwitch = - fun({Key, Val}) -> - case lists:member(Key, Keys) of - true -> - io:format(" + Checking key: '~p'~n", [Key]), - {Key, lists:map(Switch, Val)}; - false-> {Key, Val} - end - end, - Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, - Res; - (Other, Acc) -> - {[Other], Acc} +({schema, db_nodes, Nodes}, Acc) -> + io:format(" +++ db_nodes ~p~n", [Nodes]), + {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; +({schema, version, Version}, Acc) -> + io:format(" +++ version: ~p~n", [Version]), + {[{schema, version, Version}], Acc}; +({schema, cookie, Cookie}, Acc) -> + io:format(" +++ cookie: ~p~n", [Cookie]), + {[{schema, cookie, Cookie}], Acc}; +({schema, Tab, CreateList}, Acc) -> + io:format("~n * Checking table: '~p'~n", [Tab]), + Keys = [ram_copies, disc_copies, disc_only_copies], + OptSwitch = + fun({Key, Val}) -> + case lists:member(Key, Keys) of + true -> + io:format(" + Checking key: '~p'~n", [Key]), + {Key, lists:map(Switch, Val)}; + false-> {Key, Val} + end + end, + Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, + Res; +(Other, Acc) -> + {[Other], Acc} end, mnesia:traverse_backup(Source, Target, Convert, switched). From f1ea67817c843dcd981f3361a1e0b6fe9f8790e4 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 22 Aug 2017 14:58:12 +0300 Subject: [PATCH 69/75] More whitespace changes --- src/ejabberd_admin.erl | 74 +++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 6db4d4e84..76cdec45b 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -787,45 +787,45 @@ mnesia_change_nodename(FromString, ToString, Source, Target) -> Switch = fun (Node) when Node == From -> - io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), - To; - (Node) when Node == To -> + io:format(" - Replacing nodename: '~p' with: '~p'~n", [From, To]), + To; + (Node) when Node == To -> %% throw({error, already_exists}); - io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), - Node; - (Node) -> - io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), - Node - end, -Convert = -fun -({schema, db_nodes, Nodes}, Acc) -> - io:format(" +++ db_nodes ~p~n", [Nodes]), - {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; -({schema, version, Version}, Acc) -> - io:format(" +++ version: ~p~n", [Version]), - {[{schema, version, Version}], Acc}; -({schema, cookie, Cookie}, Acc) -> - io:format(" +++ cookie: ~p~n", [Cookie]), - {[{schema, cookie, Cookie}], Acc}; -({schema, Tab, CreateList}, Acc) -> - io:format("~n * Checking table: '~p'~n", [Tab]), - Keys = [ram_copies, disc_copies, disc_only_copies], - OptSwitch = - fun({Key, Val}) -> - case lists:member(Key, Keys) of - true -> - io:format(" + Checking key: '~p'~n", [Key]), - {Key, lists:map(Switch, Val)}; - false-> {Key, Val} - end + io:format(" - Node: '~p' will not be modified (it is already '~p')~n", [Node, To]), + Node; + (Node) -> + io:format(" - Node: '~p' will not be modified (it is not '~p')~n", [Node, From]), + Node end, - Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, - Res; -(Other, Acc) -> - {[Other], Acc} -end, -mnesia:traverse_backup(Source, Target, Convert, switched). + Convert = + fun + ({schema, db_nodes, Nodes}, Acc) -> + io:format(" +++ db_nodes ~p~n", [Nodes]), + {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc}; + ({schema, version, Version}, Acc) -> + io:format(" +++ version: ~p~n", [Version]), + {[{schema, version, Version}], Acc}; + ({schema, cookie, Cookie}, Acc) -> + io:format(" +++ cookie: ~p~n", [Cookie]), + {[{schema, cookie, Cookie}], Acc}; + ({schema, Tab, CreateList}, Acc) -> + io:format("~n * Checking table: '~p'~n", [Tab]), + Keys = [ram_copies, disc_copies, disc_only_copies], + OptSwitch = + fun({Key, Val}) -> + case lists:member(Key, Keys) of + true -> + io:format(" + Checking key: '~p'~n", [Key]), + {Key, lists:map(Switch, Val)}; + false-> {Key, Val} + end + end, + Res = {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc}, + Res; + (Other, Acc) -> + {[Other], Acc} + end, + mnesia:traverse_backup(Source, Target, Convert, switched). clear_cache() -> Nodes = ejabberd_cluster:get_nodes(), From 80b44d8c154b2884d90a4ee156c05ebda1cac6c2 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Fri, 25 Aug 2017 12:08:16 +0300 Subject: [PATCH 70/75] Remove some unused variable warnings, replace lists:join with string join --- src/acme_challenge.erl | 14 ++------------ src/ejabberd_acme.erl | 13 ++++++------- src/ejabberd_admin.erl | 2 +- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index 0e9e395ee..c8491e9c2 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -18,8 +18,9 @@ -include("ejabberd_http.hrl"). -include("ejabberd_acme.hrl"). + %% TODO: Maybe validate request here?? -process(LocalPath, Request) -> +process(LocalPath, _Request) -> Result = ets_get_key_authorization(LocalPath), {200, [{<<"Content-Type">>, <<"text/plain">>}], @@ -85,17 +86,6 @@ solve_challenge1(Challenge, _Key) -> {error, unknown_challenge}. -%% Old way of solving challenges -save_key_authorization(Chal, Tkn, KeyAuthz, HttpDir) -> - FileLocation = HttpDir ++ "/.well-known/acme-challenge/" ++ bitstring_to_list(Tkn), - case file:write_file(FileLocation, KeyAuthz) of - ok -> - {ok, Chal#challenge.uri, KeyAuthz}; - {error, Reason} = Err -> - ?ERROR_MSG("Error writing to file: ~s with reason: ~p~n", [FileLocation, Reason]), - Err - end. - -spec ets_put_key_authorization(bitstring(), bitstring()) -> ok. ets_put_key_authorization(Tkn, KeyAuthz) -> Tab = ets_get_acme_table(), diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index b28cb1cdc..e40ad9ccf 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -56,7 +56,7 @@ is_valid_domain_opt(DomainString) -> case parse_domain_string(DomainString) of [] -> false; - SeparatedDomains -> + _SeparatedDomains -> true end. @@ -128,8 +128,7 @@ format_get_certificates_result(Certs) -> Cond = lists:all(fun(Cert) -> not is_error(Cert) end, Certs), - FormattedCerts = lists:join($\n, - [format_get_certificate(C) || C <- Certs]), + FormattedCerts = string:join([format_get_certificate(C) || C <- Certs], "\n"), case Cond of true -> Result = io_lib:format("Success:~n~s", [FormattedCerts]), @@ -329,7 +328,7 @@ renew_certificate(CAUrl, {DomainName, _} = Cert, PrivateKey) -> -spec cert_to_expire({bitstring(), data_cert()}) -> boolean(). -cert_to_expire({DomainName, #data_cert{pem = Pem}}) -> +cert_to_expire({_DomainName, #data_cert{pem = Pem}}) -> Certificate = pem_to_certificate(Pem), Validity = get_utc_validity(Certificate), @@ -551,7 +550,7 @@ revoke_certificate2(CAUrl, PemEncodedCert) -> {ok, Dirs, Nonce} = ejabberd_acme_comm:directory(CAUrl), Req = [{<<"certificate">>, Certificate}], - {ok, [], Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, CertPrivateKey, Req, Nonce), + {ok, [], _Nonce1} = ejabberd_acme_comm:revoke_cert(Dirs, CertPrivateKey, Req, Nonce), ok. -spec parse_revoke_cert_argument(string()) -> {domain, bitstring()} | {file, file:filename()}. @@ -802,7 +801,7 @@ utc_string_to_datetime(UtcString) -> Second = list_to_integer([S1,S2]), {{Year, Month, Day}, {Hour, Minute, Second}} catch - E:R -> + _:_ -> ?ERROR_MSG("Unable to parse UTC string", []), throw({error, utc_string_to_datetime}) end. @@ -910,7 +909,7 @@ data_add_certificate(Data, DataCert = #data_cert{domain=Domain}) -> data_set_certificates(Data, NewCerts). -spec data_remove_certificate(acme_data(), data_cert()) -> acme_data(). -data_remove_certificate(Data, DataCert = #data_cert{domain=Domain}) -> +data_remove_certificate(Data, _DataCert = #data_cert{domain=Domain}) -> Certs = data_get_certificates(Data), NewCerts = lists:keydelete(Domain, 1, Certs), data_set_certificates(Data, NewCerts). diff --git a/src/ejabberd_admin.erl b/src/ejabberd_admin.erl index 76cdec45b..368c7fe53 100644 --- a/src/ejabberd_admin.erl +++ b/src/ejabberd_admin.erl @@ -580,7 +580,7 @@ get_certificate(Domains) -> true -> ejabberd_acme:get_certificates(Domains); false -> - String = io_lib:format("Invalid domains: ~p", [Domains]) + io_lib:format("Invalid domains: ~p", [Domains]) end. renew_certificate() -> From f55a8d045d637ad4e94753de011ed1d228037072 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 6 Sep 2017 18:10:38 +0300 Subject: [PATCH 71/75] Solve Travis build xref problem Travis build failed on xref because some functions that I used did not exist in OTP versions 17.5, 18.3 Those functions are: ets:take/2, lists:join/2, erlang:timestamp/0. --- src/acme_challenge.erl | 3 ++- src/ejabberd_acme.erl | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index c8491e9c2..2bc6ccbb5 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -96,8 +96,9 @@ ets_put_key_authorization(Tkn, KeyAuthz) -> -spec ets_get_key_authorization([bitstring()]) -> bitstring(). ets_get_key_authorization(Key) -> Tab = ets_get_acme_table(), - case ets:take(Tab, Key) of + case ets:lookup(Tab, Key) of [{Key, KeyAuthz}] -> + ets:delete(Tab, Key), KeyAuthz; _ -> ?ERROR_MSG("Unable to serve key authorization in: ~p", [Key]), diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index e40ad9ccf..4eb4316b3 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -128,13 +128,14 @@ format_get_certificates_result(Certs) -> Cond = lists:all(fun(Cert) -> not is_error(Cert) end, Certs), - FormattedCerts = string:join([format_get_certificate(C) || C <- Certs], "\n"), + %% FormattedCerts = string:join([format_get_certificate(C) || C <- Certs], "\n"), + FormattedCerts = str:join([format_get_certificate(C) || C <- Certs], $\n), case Cond of true -> Result = io_lib:format("Success:~n~s", [FormattedCerts]), lists:flatten(Result); _ -> - Result = io_lib:format("Error with one or more certificates~n~s", [lists:flatten(FormattedCerts)]), + Result = io_lib:format("Error with one or more certificates~n~s", [FormattedCerts]), lists:flatten(Result) end. @@ -771,10 +772,11 @@ get_challenges(Body) -> -spec not_before_not_after() -> {binary(), binary()}. not_before_not_after() -> - {MegS, Sec, MicS} = erlang:timestamp(), - NotBefore = xmpp_util:encode_timestamp({MegS, Sec, MicS}), + {Date, Time} = calendar:universal_time(), + NotBefore = encode_calendar_datetime({Date, Time}), %% The certificate will be valid for 90 Days after today - NotAfter = xmpp_util:encode_timestamp({MegS+7, Sec+776000, MicS}), + AfterDate = add_days_to_date(90, Date), + NotAfter = encode_calendar_datetime({AfterDate, Time}), {NotBefore, NotAfter}. -spec to_public(jose_jwk:key()) -> jose_jwk:key(). @@ -788,6 +790,17 @@ pem_to_certificate(Pem) -> Certificate = public_key:pem_entry_decode(PemEntryCert), Certificate. +-spec add_days_to_date(integer(), calendar:date()) -> calendar:date(). +add_days_to_date(Days, Date) -> + Date1 = calendar:date_to_gregorian_days(Date), + calendar:gregorian_days_to_date(Date1 + Days). + +-spec encode_calendar_datetime(calendar:datetime()) -> binary(). +encode_calendar_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) -> + list_to_binary(io_lib:format("~4..0B-~2..0B-~2..0BT" + "~2..0B:~2..0B:~2..0BZ", + [Year, Month, Day, Hour, Minute, Second])). + %% TODO: Find a better and more robust way to parse the utc string -spec utc_string_to_datetime(string()) -> calendar:datetime(). utc_string_to_datetime(UtcString) -> From 315e330237c32ad123737ec69771c98ee0d742c2 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Wed, 6 Sep 2017 18:35:33 +0300 Subject: [PATCH 72/75] Fix version of jose library --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index d8f19042c..f64e2a6e0 100644 --- a/rebar.config +++ b/rebar.config @@ -30,7 +30,7 @@ {jiffy, ".*", {git, "https://github.com/davisp/jiffy", {tag, "0.14.8"}}}, {p1_oauth2, ".*", {git, "https://github.com/processone/p1_oauth2", {tag, "0.6.2"}}}, {luerl, ".*", {git, "https://github.com/rvirding/luerl", {tag, "v0.2"}}}, - {jose, ".*", {git, "git://github.com/potatosalad/erlang-jose.git", {branch, "master"}}}, + {jose, ".*", {git, "git://github.com/potatosalad/erlang-jose.git", {tag, "1.8.4"}}}, {if_var_true, stun, {stun, ".*", {git, "https://github.com/processone/stun", {tag, "1.0.14"}}}}, {if_var_true, sip, {esip, ".*", {git, "https://github.com/processone/esip", {tag, "1.0.15"}}}}, {if_var_true, mysql, {p1_mysql, ".*", {git, "https://github.com/processone/p1_mysql", From 189d02cee07fa073989d934ff8bed0b4476e3c43 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Thu, 2 Nov 2017 19:05:12 +0200 Subject: [PATCH 73/75] Bug Fix The dictionary returned after the directory call contains a meta key whose value is a JSON dictionary. This is now taken care so that only bitstring values are kept as resource URIs --- src/ejabberd_acme_comm.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ejabberd_acme_comm.erl b/src/ejabberd_acme_comm.erl index 99eaff87b..acd552f7e 100644 --- a/src/ejabberd_acme_comm.erl +++ b/src/ejabberd_acme_comm.erl @@ -151,7 +151,7 @@ revoke_cert(Dirs, PrivateKey, Req, Nonce) -> get_dirs({ok, Head, Return}) -> NewNonce = get_nonce(Head), StrDirectories = [{bitstring_to_list(X), bitstring_to_list(Y)} || - {X, Y} <- Return], + {X, Y} <- Return, is_bitstring(X) andalso is_bitstring(Y)], NewDirs = maps:from_list(StrDirectories), {ok, NewDirs, NewNonce}. From 78f494dd2eba0970b14247f1027f09b6a82efaf0 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Sat, 11 Nov 2017 15:38:47 +0200 Subject: [PATCH 74/75] Configuration file changes Explain the acme configuration options --- ejabberd.yml.example | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index 9891e0ed2..b9e117deb 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -663,18 +663,22 @@ language: "en" ###. ==== ###' ACME -## -## A contact with which it will create an ACME account -## The ACME Certificate Authority URL. -## This could either be: -## - https://acme-v01.api.letsencrypt.org - for the production CA -## - https://acme-staging.api.letsencrypt.org - for the staging CA -## - http://localhost:4000 - for a local version of the CA -## acme: - contact: "mailto:cert-admin-ejabberd@example.com" - ca_url: "http://localhost:4000" + ## A contact mail that the ACME Certificate Authority can contact in case of + ## an authorization issue, such as a server-initiated certificate revocation. + ## It is not mandatory to provide an email address but it is highly suggested. + contact: "mailto:example-admin@example.com" + + + ## The ACME Certificate Authority URL. + ## This could either be: + ## - https://acme-v01.api.letsencrypt.org - (Default) for the production CA + ## - https://acme-staging.api.letsencrypt.org - for the staging CA + ## - http://localhost:4000 - for a local version of the CA + ca_url: "https://acme-v01.api.letsencrypt.org" + +## The directory in which certificates will be saved cert_dir: "/usr/local/var/lib/ejabberd/" ###. ======= From ce99db05954a3170c4c5a5a45b18ea391fd86987 Mon Sep 17 00:00:00 2001 From: Konstantinos Kallas Date: Tue, 14 Nov 2017 14:12:33 +0200 Subject: [PATCH 75/75] Explain what is needed for the acme configuration and other small changes 1. Add a request handler in ejabberd_http and explain how to configure the http listener so that the challenges can be solved. 2. Make acme configuration optional by providing defaults in ejabberd_acme. 3. Save the CA that the account has been created in so that it creates a new account when connecting to a new CA. 4. Small spec change in acme configuration. --- ejabberd.yml.example | 12 +++++- include/ejabberd_acme.hrl | 8 +++- src/acme_challenge.erl | 8 +++- src/ejabberd_acme.erl | 79 +++++++++++++++++++++++---------------- src/ejabberd_http.erl | 3 +- 5 files changed, 72 insertions(+), 38 deletions(-) diff --git a/ejabberd.yml.example b/ejabberd.yml.example index b9e117deb..aa80ef8d2 100644 --- a/ejabberd.yml.example +++ b/ejabberd.yml.example @@ -161,7 +161,6 @@ listen: "/ws": ejabberd_http_ws "/bosh": mod_bosh "/api": mod_http_api - "/.well-known": acme_challenge ## "/pub/archive": mod_http_fileserver web_admin: true ## register: true @@ -662,6 +661,17 @@ language: "en" ###. ==== ###' ACME +## +## In order to use the acme certificate acquiring through "Let's Encrypt" +## an http listener has to be configured to listen to port 80 so that +## the authorization challenges posed by "Let's Encrypt" can be solved. +## +## A simple way of doing this would be to add the following in the listen +## configuration field: +## - +## port: 80 +## ip: "::" +## module: ejabberd_http acme: diff --git a/include/ejabberd_acme.hrl b/include/ejabberd_acme.hrl index e0725af70..f48a6d8b9 100644 --- a/include/ejabberd_acme.hrl +++ b/include/ejabberd_acme.hrl @@ -7,8 +7,9 @@ }). -record(data_acc, { - id :: list(), - key :: jose_jwk:key() + id :: list(), + ca_url :: url(), + key :: jose_jwk:key() }). -type data_acc() :: #data_acc{}. @@ -23,6 +24,9 @@ %% Types %% +%% Acme configuration +-type acme_config() :: [{ca_url, url()} | {contact, bitstring()}]. + %% The main data type that ejabberd_acme keeps -type acme_data() :: proplist(). diff --git a/src/acme_challenge.erl b/src/acme_challenge.erl index 2bc6ccbb5..a65765f74 100644 --- a/src/acme_challenge.erl +++ b/src/acme_challenge.erl @@ -2,8 +2,8 @@ -export ([key_authorization/2, solve_challenge/3, - - process/2 + process/2, + acme_handler/0 ]). %% Challenge Types %% ================ @@ -18,6 +18,10 @@ -include("ejabberd_http.hrl"). -include("ejabberd_acme.hrl"). +%% This is the default endpoint for the http challenge +%% This function is called by the http_listener +acme_handler() -> + {[<<".well-known">>],acme_challenge}. %% TODO: Maybe validate request here?? process(LocalPath, _Request) -> diff --git a/src/ejabberd_acme.erl b/src/ejabberd_acme.erl index 4eb4316b3..6f616a342 100644 --- a/src/ejabberd_acme.erl +++ b/src/ejabberd_acme.erl @@ -28,6 +28,12 @@ -behavior(ejabberd_config). +%% +%% Default ACME configuration +%% + +-define(DEFAULT_CONFIG_CONTACT, <<"mailto:example-admin@example.com">>). +-define(DEFAULT_CONFIG_CA_URL, "https://acme-v01.api.letsencrypt.org"). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @@ -91,13 +97,16 @@ get_certificates0(CAUrl, Domains) -> get_certificates1(CAUrl, Domains, PrivateKey). - +-spec retrieve_or_create_account(url()) -> {'ok', string(), jose_jwk:key()}. retrieve_or_create_account(CAUrl) -> case read_account_persistent() of none -> create_save_new_account(CAUrl); - {ok, AccId, PrivateKey} -> - {ok, AccId, PrivateKey} + + {ok, AccId, CAUrl, PrivateKey} -> + {ok, AccId, PrivateKey}; + {ok, _AccId, _, _PrivateKey} -> + create_save_new_account(CAUrl) end. @@ -182,7 +191,7 @@ create_save_new_account(CAUrl) -> {ok, Id} = create_new_account(CAUrl, Contact, PrivateKey), %% Write Persistent Data - ok = write_account_persistent({Id, PrivateKey}), + ok = write_account_persistent({Id, CAUrl, PrivateKey}), {ok, Id, PrivateKey}. @@ -272,14 +281,17 @@ create_new_certificate(CAUrl, {DomainName, AllSubDomains}, PrivateKey) -> throw({error, DomainName, certificate}) end. --spec ensure_account_exists() -> {ok, string(), jose_jwk:key()}. -ensure_account_exists() -> +-spec ensure_account_exists(url()) -> {ok, string(), jose_jwk:key()}. +ensure_account_exists(CAUrl) -> case read_account_persistent() of none -> ?ERROR_MSG("No existing account", []), throw({error, no_old_account}); - {ok, AccId, PrivateKey} -> - {ok, AccId, PrivateKey} + {ok, AccId, CAUrl, PrivateKey} -> + {ok, AccId, PrivateKey}; + {ok, _AccId, OtherCAUrl, _PrivateKey} -> + ?ERROR_MSG("Account is connected to another CA: ~s", [OtherCAUrl]), + throw({error, account_in_other_CA}) end. @@ -302,7 +314,7 @@ renew_certificates() -> -spec renew_certificates0(url()) -> string(). renew_certificates0(CAUrl) -> %% Get the current account - {ok, _AccId, PrivateKey} = ensure_account_exists(), + {ok, _AccId, PrivateKey} = ensure_account_exists(CAUrl), %% Find all hosts that we have certificates for Certs = read_certificates_persistent(), @@ -883,18 +895,18 @@ data_empty() -> %% Account %% --spec data_get_account(acme_data()) -> {ok, list(), jose_jwk:key()} | none. +-spec data_get_account(acme_data()) -> {ok, list(), url(), jose_jwk:key()} | none. data_get_account(Data) -> case lists:keyfind(account, 1, Data) of - {account, #data_acc{id = AccId, key = PrivateKey}} -> - {ok, AccId, PrivateKey}; + {account, #data_acc{id = AccId, ca_url = CAUrl, key = PrivateKey}} -> + {ok, AccId, CAUrl, PrivateKey}; false -> none end. --spec data_set_account(acme_data(), {list(), jose_jwk:key()}) -> acme_data(). -data_set_account(Data, {AccId, PrivateKey}) -> - NewAcc = {account, #data_acc{id = AccId, key = PrivateKey}}, +-spec data_set_account(acme_data(), {list(), url(), jose_jwk:key()}) -> acme_data(). +data_set_account(Data, {AccId, CAUrl, PrivateKey}) -> + NewAcc = {account, #data_acc{id = AccId, ca_url = CAUrl, key = PrivateKey}}, lists:keystore(account, 1, Data, NewAcc). %% @@ -983,13 +995,13 @@ create_persistent() -> throw({error, Reason}) end. --spec write_account_persistent({list(), jose_jwk:key()}) -> ok | no_return(). -write_account_persistent({AccId, PrivateKey}) -> +-spec write_account_persistent({list(), url(), jose_jwk:key()}) -> ok | no_return(). +write_account_persistent({AccId, CAUrl, PrivateKey}) -> {ok, Data} = read_persistent(), - NewData = data_set_account(Data, {AccId, PrivateKey}), + NewData = data_set_account(Data, {AccId, CAUrl, PrivateKey}), ok = write_persistent(NewData). --spec read_account_persistent() -> {ok, list(), jose_jwk:key()} | none. +-spec read_account_persistent() -> {ok, list(), url(), jose_jwk:key()} | none. read_account_persistent() -> {ok, Data} = read_persistent(), data_get_account(Data). @@ -1060,12 +1072,13 @@ write_cert(CertificateFile, Cert, DomainName) -> throw({error, DomainName, saving}) end. --spec get_config_acme() -> [{atom(), bitstring()}]. +-spec get_config_acme() -> acme_config(). get_config_acme() -> case ejabberd_config:get_option(acme, undefined) of undefined -> - ?ERROR_MSG("No acme configuration has been specified", []), - throw({error, configuration}); + ?WARNING_MSG("No acme configuration has been specified", []), + %% throw({error, configuration}); + []; Acme -> Acme end. @@ -1077,19 +1090,21 @@ get_config_contact() -> {contact, Contact} -> Contact; false -> - ?ERROR_MSG("No contact has been specified", []), - throw({error, configuration_contact}) + ?WARNING_MSG("No contact has been specified in configuration", []), + ?DEFAULT_CONFIG_CONTACT + %% throw({error, configuration_contact}) end. --spec get_config_ca_url() -> string(). +-spec get_config_ca_url() -> url(). get_config_ca_url() -> Acme = get_config_acme(), case lists:keyfind(ca_url, 1, Acme) of {ca_url, CAUrl} -> CAUrl; false -> - ?ERROR_MSG("No CA url has been specified", []), - throw({error, configuration_ca_url}) + ?ERROR_MSG("No CA url has been specified in configuration", []), + ?DEFAULT_CONFIG_CA_URL + %% throw({error, configuration_ca_url}) end. @@ -1097,7 +1112,7 @@ get_config_ca_url() -> get_config_hosts() -> case ejabberd_config:get_option(hosts, undefined) of undefined -> - ?ERROR_MSG("No hosts have been specified", []), + ?ERROR_MSG("No hosts have been specified in configuration", []), throw({error, configuration_hosts}); Hosts -> Hosts @@ -1107,8 +1122,9 @@ get_config_hosts() -> get_config_cert_dir() -> case ejabberd_config:get_option(cert_dir, undefined) of undefined -> - ?ERROR_MSG("No cert_dir configuration has been specified", []), - throw({error, configuration}); + ?WARNING_MSG("No cert_dir configuration has been specified in configuration", []), + mnesia:system_info(directory); + %% throw({error, configuration}); CertDir -> CertDir end. @@ -1136,8 +1152,7 @@ parse_cert_dir_opt(Opt) when is_bitstring(Opt) -> true = filelib:is_dir(Opt), Opt. --spec opt_type(acme) -> fun(([{ca_url, string()} | {contact, bitstring()}]) -> - ([{ca_url, string()} | {contact, bitstring()}])); +-spec opt_type(acme) -> fun((acme_config()) -> (acme_config())); (cert_dir) -> fun((bitstring()) -> (bitstring())); (atom()) -> [atom()]. opt_type(acme) -> diff --git a/src/ejabberd_http.erl b/src/ejabberd_http.erl index 43bbb3f04..3ba316852 100644 --- a/src/ejabberd_http.erl +++ b/src/ejabberd_http.erl @@ -136,8 +136,9 @@ init({SockMod, Socket}, Opts) -> true -> [{[], ejabberd_xmlrpc}]; false -> [] end, + Acme = [acme_challenge:acme_handler()], DefinedHandlers = proplists:get_value(request_handlers, Opts, []), - RequestHandlers = DefinedHandlers ++ Captcha ++ Register ++ + RequestHandlers = Acme ++ DefinedHandlers ++ Captcha ++ Register ++ Admin ++ Bind ++ XMLRPC, ?DEBUG("S: ~p~n", [RequestHandlers]),