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().