Packages

Gleam library for interacting with Automatic Certificate Management Environment (ACME) servers like Let's Encrypt

Current section

Files

Jump to
acumen src acumen@challenge.erl
Raw

src/acumen@challenge.erl

-module(acumen@challenge).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/acumen/challenge.gleam").
-export([dns01_txt_record/1, issuer_domain_names/1, status/1, token/1, key_authorization/2, url/1, dns_persist01_txt_record/2, challenge_type_of/1, find_by_type/2, dns_account_label/1, dns_account01_txt_record/3, error_decoder/0, decoder/0, optional_decoder/0]).
-export_type([challenge/0, challenge_type/0, status/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Challenge types for ACME domain validation.\n"
"\n"
" After creating an order, complete challenges to prove control over the\n"
" requested identifiers.\n"
).
-type challenge() :: {http01_challenge,
acumen@url:url(),
status(),
binary(),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(acumen:acme_error())} |
{dns01_challenge,
acumen@url:url(),
status(),
binary(),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(acumen:acme_error())} |
{tls_alpn01_challenge,
acumen@url:url(),
status(),
binary(),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(acumen:acme_error())} |
{dns_account01_challenge,
acumen@url:url(),
status(),
binary(),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(acumen:acme_error())} |
{dns_persist01_challenge,
acumen@url:url(),
status(),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(acumen:acme_error()),
list(binary())}.
-type challenge_type() :: http01 |
dns01 |
tls_alpn01 |
dns_persist01 |
dns_account01.
-type status() :: pending | processing | valid | invalid.
-file("src/acumen/challenge.gleam", 103).
?DOC(
" Computes the DNS-01 TXT record value.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(key_auth) = challenge.key_authorization(dns_challenge, registered_key)\n"
" let assert Ok(txt_value) = challenge.dns01_txt_record(key_auth)\n"
" // Create TXT record: _acme-challenge.example.com -> txt_value\n"
" ```\n"
).
-spec dns01_txt_record(binary()) -> {ok, binary()} |
{error, acumen:acme_error()}.
dns01_txt_record(Key_authorization) ->
_pipe = kryptos@crypto:hash(
sha256,
gleam_stdlib:identity(Key_authorization)
),
_pipe@1 = gleam@result:map(
_pipe,
fun(Digest) -> gleam@bit_array:base64_url_encode(Digest, false) end
),
gleam@result:replace_error(
_pipe@1,
{crypto_error, <<"SHA-256 hash failed"/utf8>>}
).
-file("src/acumen/challenge.gleam", 175).
?DOC(
" Returns the issuer domain names for a DNS-Persist-01 challenge.\n"
"\n"
" Returns `Error(Nil)` for other challenge types.\n"
).
-spec issuer_domain_names(challenge()) -> {ok, list(binary())} | {error, nil}.
issuer_domain_names(Challenge) ->
case Challenge of
{dns_persist01_challenge, _, _, _, _, Issuer_domain_names} ->
{ok, Issuer_domain_names};
{http01_challenge, _, _, _, _, _} ->
{error, nil};
{dns01_challenge, _, _, _, _, _} ->
{error, nil};
{tls_alpn01_challenge, _, _, _, _, _} ->
{error, nil};
{dns_account01_challenge, _, _, _, _, _} ->
{error, nil}
end.
-file("src/acumen/challenge.gleam", 215).
?DOC(" Returns the status of a challenge.\n").
-spec status(challenge()) -> status().
status(Challenge) ->
erlang:element(3, Challenge).
-file("src/acumen/challenge.gleam", 222).
?DOC(
" Returns the token for a challenge.\n"
"\n"
" Returns `Error(Nil)` for challenge types without a token (e.g. DNS-Persist-01).\n"
).
-spec token(challenge()) -> {ok, binary()} | {error, nil}.
token(Challenge) ->
case Challenge of
{http01_challenge, _, _, Token, _, _} ->
{ok, Token};
{dns01_challenge, _, _, Token@1, _, _} ->
{ok, Token@1};
{tls_alpn01_challenge, _, _, Token@2, _, _} ->
{ok, Token@2};
{dns_account01_challenge, _, _, Token@3, _, _} ->
{ok, Token@3};
{dns_persist01_challenge, _, _, _, _, _} ->
{error, nil}
end.
-file("src/acumen/challenge.gleam", 196).
?DOC(
" Computes the key authorization string for a challenge.\n"
"\n"
" Returns `Error(acumen.InvalidChallenge(_))` for challenge types that\n"
" don't have a token (e.g., `DnsPersist01Challenge`).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(key_auth) = challenge.key_authorization(http_challenge, registered_key)\n"
" // Deploy: GET /.well-known/acme-challenge/{token} -> key_auth\n"
" ```\n"
).
-spec key_authorization(challenge(), acumen:registered_key()) -> {ok, binary()} |
{error, acumen:acme_error()}.
key_authorization(Challenge, Key) ->
gleam@result:'try'(
begin
_pipe = token(Challenge),
gleam@result:replace_error(
_pipe,
{invalid_challenge,
<<"Challenge type does not have a token"/utf8>>}
)
end,
fun(Tkn) ->
_pipe@1 = gose@jwk:thumbprint(erlang:element(2, Key), sha256),
_pipe@2 = gleam@result:map_error(
_pipe@1,
fun(Err) ->
{crypto_error,
acumen@internal@utils:gose_error_to_string(Err)}
end
),
gleam@result:map(
_pipe@2,
fun(Thumbprint) ->
<<<<Tkn/binary, "."/utf8>>/binary, Thumbprint/binary>>
end
)
end
).
-file("src/acumen/challenge.gleam", 233).
?DOC(" Returns the URL for a challenge.\n").
-spec url(challenge()) -> acumen@url:url().
url(Challenge) ->
erlang:element(2, Challenge).
-file("src/acumen/challenge.gleam", 150).
?DOC(
" Builds a DNS-Persist-01 TXT record value for a single issuer.\n"
"\n"
" A DNS-Persist-01 challenge may include multiple issuer domain names.\n"
" Callers should create a separate TXT record for each issuer, all on the\n"
" same `_validation-persist.{domain}` DNS name.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(issuers) = challenge.issuer_domain_names(dns_persist_challenge)\n"
" let txt_records = list.map(issuers, fn(issuer) {\n"
" challenge.dns_persist01_txt_record(issuer:, account_url: registered_key.kid)\n"
" })\n"
" // Create a TXT record at _validation-persist.example.com for each value\n"
" ```\n"
).
-spec dns_persist01_txt_record(binary(), acumen@url:url()) -> binary().
dns_persist01_txt_record(Issuer_domain_name, Account_url) ->
<<<<Issuer_domain_name/binary, "; accounturi="/utf8>>/binary,
(acumen@url:to_string(Account_url))/binary>>.
-file("src/acumen/challenge.gleam", 238).
?DOC(false).
-spec challenge_type_of(challenge()) -> challenge_type().
challenge_type_of(Challenge) ->
case Challenge of
{http01_challenge, _, _, _, _, _} ->
http01;
{dns01_challenge, _, _, _, _, _} ->
dns01;
{tls_alpn01_challenge, _, _, _, _, _} ->
tls_alpn01;
{dns_account01_challenge, _, _, _, _, _} ->
dns_account01;
{dns_persist01_challenge, _, _, _, _, _} ->
dns_persist01
end.
-file("src/acumen/challenge.gleam", 165).
?DOC(
" Finds the first challenge matching the given type, or `Error(Nil)` if none.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(http_challenge) =\n"
" challenge.find_by_type(authorization.challenges, of: challenge.Http01)\n"
" ```\n"
).
-spec find_by_type(list(challenge()), challenge_type()) -> {ok, challenge()} |
{error, nil}.
find_by_type(Challenges, Type_) ->
gleam@list:find(Challenges, fun(C) -> challenge_type_of(C) =:= Type_ end).
-file("src/acumen/challenge.gleam", 341).
?DOC(false).
-spec dns_account_label(binary()) -> {ok, binary()} |
{error, acumen:acme_error()}.
dns_account_label(Account_url) ->
case kryptos@crypto:hash(sha256, gleam_stdlib:identity(Account_url)) of
{error, _} ->
{error, {crypto_error, <<"SHA-256 hash failed"/utf8>>}};
{ok, <<Prefix:10/binary, _/bitstring>>} ->
Label = begin
_pipe = Prefix,
_pipe@1 = thirtytwo:encode(_pipe, false),
string:lowercase(_pipe@1)
end,
{ok, Label};
{ok, _} ->
{error, {crypto_error, <<"unexpected hash digest length"/utf8>>}}
end.
-file("src/acumen/challenge.gleam", 124).
?DOC(
" Computes the DNS-Account-01 TXT record name and value.\n"
"\n"
" Returns `#(name, value)` where `name` is the full record name and\n"
" `value` is the record content.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(key_auth) = challenge.key_authorization(dns_acct_challenge, registered_key)\n"
" let assert Ok(#(record_name, record_value)) =\n"
" challenge.dns_account01_txt_record(\"example.com\", registered_key.kid, key_auth)\n"
" // Create TXT record: record_name -> record_value\n"
" ```\n"
).
-spec dns_account01_txt_record(binary(), acumen@url:url(), binary()) -> {ok,
{binary(), binary()}} |
{error, acumen:acme_error()}.
dns_account01_txt_record(Domain, Account_url, Key_authorization) ->
gleam@result:'try'(
dns_account_label(acumen@url:to_string(Account_url)),
fun(Label) ->
gleam@result:'try'(
dns01_txt_record(Key_authorization),
fun(Value) ->
Name = <<<<<<"_acme-challenge_"/utf8, Label/binary>>/binary,
"."/utf8>>/binary,
Domain/binary>>,
{ok, {Name, Value}}
end
)
end
).
-file("src/acumen/challenge.gleam", 358).
?DOC(false).
-spec error_decoder() -> gleam@dynamic@decode:decoder(gleam@option:option(acumen:acme_error())).
error_decoder() ->
gleam@dynamic@decode:optional(
begin
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Type_) ->
gleam@dynamic@decode:optional_field(
<<"detail"/utf8>>,
<<""/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Detail) ->
gleam@dynamic@decode:optional_field(
<<"instance"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Instance) ->
gleam@dynamic@decode:optional_field(
<<"subproblems"/utf8>>,
[],
gleam@dynamic@decode:list(
acumen:subproblem_decoder()
),
fun(Subproblems) ->
gleam@dynamic@decode:success(
acumen:acme_error_from_type(
Type_,
Detail,
Instance,
Subproblems,
none
)
)
end
)
end
)
end
)
end
)
end
).
-file("src/acumen/challenge.gleam", 382).
-spec status_decoder() -> gleam@dynamic@decode:decoder(status()).
status_decoder() ->
gleam@dynamic@decode:then(
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(String) -> case String of
<<"pending"/utf8>> ->
gleam@dynamic@decode:success(pending);
<<"processing"/utf8>> ->
gleam@dynamic@decode:success(processing);
<<"valid"/utf8>> ->
gleam@dynamic@decode:success(valid);
<<"invalid"/utf8>> ->
gleam@dynamic@decode:success(invalid);
_ ->
gleam@dynamic@decode:failure(
pending,
<<"ChallengeStatus"/utf8>>
)
end end
).
-file("src/acumen/challenge.gleam", 292).
-spec dns_persist_challenge_decoder() -> gleam@dynamic@decode:decoder(challenge()).
dns_persist_challenge_decoder() ->
gleam@dynamic@decode:field(
<<"url"/utf8>>,
acumen@url:decoder(),
fun(Url) ->
gleam@dynamic@decode:field(
<<"status"/utf8>>,
status_decoder(),
fun(Status) ->
gleam@dynamic@decode:optional_field(
<<"validated"/utf8>>,
none,
gleam@dynamic@decode:optional(
acumen@internal@utils:timestamp_decoder()
),
fun(Validated) ->
gleam@dynamic@decode:optional_field(
<<"error"/utf8>>,
none,
error_decoder(),
fun(Error) ->
gleam@dynamic@decode:field(
<<"issuer-domain-names"/utf8>>,
gleam@dynamic@decode:list(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Issuer_domain_names) ->
gleam@dynamic@decode:success(
{dns_persist01_challenge,
Url,
Status,
Validated,
Error,
Issuer_domain_names}
)
end
)
end
)
end
)
end
)
end
).
-file("src/acumen/challenge.gleam", 314).
-spec standard_challenge_decoder(
fun((acumen@url:url(), status(), binary(), gleam@option:option(gleam@time@timestamp:timestamp()), gleam@option:option(acumen:acme_error())) -> challenge())
) -> gleam@dynamic@decode:decoder(challenge()).
standard_challenge_decoder(Constructor) ->
gleam@dynamic@decode:field(
<<"url"/utf8>>,
acumen@url:decoder(),
fun(Url) ->
gleam@dynamic@decode:field(
<<"status"/utf8>>,
status_decoder(),
fun(Status) ->
gleam@dynamic@decode:field(
<<"token"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Token) ->
gleam@dynamic@decode:optional_field(
<<"validated"/utf8>>,
none,
gleam@dynamic@decode:optional(
acumen@internal@utils:timestamp_decoder()
),
fun(Validated) ->
gleam@dynamic@decode:optional_field(
<<"error"/utf8>>,
none,
error_decoder(),
fun(Error) ->
gleam@dynamic@decode:success(
Constructor(
Url,
Status,
Token,
Validated,
Error
)
)
end
)
end
)
end
)
end
)
end
).
-file("src/acumen/challenge.gleam", 249).
?DOC(false).
-spec decoder() -> gleam@dynamic@decode:decoder(challenge()).
decoder() ->
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Type_) -> case Type_ of
<<"http-01"/utf8>> ->
standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {http01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
);
<<"dns-01"/utf8>> ->
standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
);
<<"tls-alpn-01"/utf8>> ->
standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {tls_alpn01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
);
<<"dns-persist-01"/utf8>> ->
dns_persist_challenge_decoder();
<<"dns-account-01"/utf8>> ->
standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns_account01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
);
_ ->
Placeholder@1 = case acumen@url:from_string(
<<"https://placeholder"/utf8>>
) of
{ok, Placeholder} -> Placeholder;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"acumen/challenge"/utf8>>,
function => <<"decoder"/utf8>>,
line => 258,
value => _assert_fail,
start => 7814,
'end' => 7881,
pattern_start => 7825,
pattern_end => 7840})
end,
gleam@dynamic@decode:failure(
{http01_challenge,
Placeholder@1,
pending,
<<""/utf8>>,
none,
none},
<<"Challenge"/utf8>>
)
end end
).
-file("src/acumen/challenge.gleam", 274).
?DOC(false).
-spec optional_decoder() -> gleam@dynamic@decode:decoder(gleam@option:option(challenge())).
optional_decoder() ->
gleam@dynamic@decode:field(
<<"type"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Type_) -> case Type_ of
<<"http-01"/utf8>> ->
_pipe = standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {http01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
),
gleam@dynamic@decode:map(
_pipe,
fun(Field@0) -> {some, Field@0} end
);
<<"dns-01"/utf8>> ->
_pipe@1 = standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
),
gleam@dynamic@decode:map(
_pipe@1,
fun(Field@0) -> {some, Field@0} end
);
<<"tls-alpn-01"/utf8>> ->
_pipe@2 = standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {tls_alpn01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
),
gleam@dynamic@decode:map(
_pipe@2,
fun(Field@0) -> {some, Field@0} end
);
<<"dns-persist-01"/utf8>> ->
_pipe@3 = dns_persist_challenge_decoder(),
gleam@dynamic@decode:map(
_pipe@3,
fun(Field@0) -> {some, Field@0} end
);
<<"dns-account-01"/utf8>> ->
_pipe@4 = standard_challenge_decoder(
fun(Field@0, Field@1, Field@2, Field@3, Field@4) -> {dns_account01_challenge, Field@0, Field@1, Field@2, Field@3, Field@4} end
),
gleam@dynamic@decode:map(
_pipe@4,
fun(Field@0) -> {some, Field@0} end
);
_ ->
gleam@dynamic@decode:success(none)
end end
).