Current section
Files
Jump to
Current section
Files
src/acumen@order.erl
-module(acumen@order).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/acumen/order.gleam").
-export([to_ec_csr/2, to_rsa_csr/2, decoder/1, parse_order_response/2]).
-export_type([csr_error/0, order/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(
" Order types and CSR generation for ACME certificate requests.\n"
"\n"
" An order represents a request for a certificate covering one or more\n"
" identifiers (domain names or IP addresses). This module also provides\n"
" helpers to generate Certificate Signing Requests (CSRs) from an order.\n"
).
-type csr_error() :: no_identifiers | invalid_identifier | signing_failed.
-type order() :: {order,
acumen@url:url(),
status(),
list(acumen:identifier_()),
list(acumen@url:url()),
acumen@url:url(),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(gleam@time@timestamp:timestamp()),
gleam@option:option(binary()),
gleam@option:option(acumen:acme_error())}.
-type status() :: pending |
ready |
processing |
{valid, acumen@url:url()} |
invalid.
-file("src/acumen/order.gleam", 114).
-spec first_dns_name(acumen:identifier_()) -> {ok, binary()} | {error, nil}.
first_dns_name(Identifier) ->
case Identifier of
{dns_identifier, Name} ->
{ok, Name};
{ip_identifier, _} ->
{error, nil}
end.
-file("src/acumen/order.gleam", 93).
-spec build_csr_builder(order()) -> {ok, kryptos@x509@csr:builder()} |
{error, csr_error()}.
build_csr_builder(Order) ->
case erlang:element(4, Order) of
[] ->
{error, no_identifiers};
Identifiers ->
Subject = case gleam@list:find_map(
Identifiers,
fun first_dns_name/1
) of
{ok, Name} ->
kryptos@x509:name([kryptos@x509:cn(Name)]);
{error, _} ->
kryptos@x509:name([])
end,
_pipe = gleam@list:try_fold(
Identifiers,
kryptos@x509@csr:new(),
fun(Builder, Identifier) -> case Identifier of
{dns_identifier, Name@1} ->
kryptos@x509@csr:with_dns_name(Builder, Name@1);
{ip_identifier, Ip} ->
kryptos@x509@csr:with_ip(Builder, Ip)
end end
),
_pipe@1 = gleam@result:map(
_pipe,
fun(_capture) ->
kryptos@x509@csr:with_subject(_capture, Subject)
end
),
gleam@result:replace_error(_pipe@1, invalid_identifier)
end.
-file("src/acumen/order.gleam", 121).
-spec hash_for_ec_curve(kryptos@ec:curve()) -> kryptos@hash:hash_algorithm().
hash_for_ec_curve(Curve) ->
case Curve of
p256 ->
sha256;
secp256k1 ->
sha256;
p384 ->
sha384;
p521 ->
sha512
end.
-file("src/acumen/order.gleam", 85).
?DOC(
" Generates a CSR from an order using an EC key.\n"
"\n"
" Uses the order's identifiers as Subject Alternative Names, with the first\n"
" DNS identifier as the Common Name. Returns the CSR in DER format.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let #(private_key, _public_key) = ec.generate_key_pair(ec.P256)\n"
" let assert Ok(csr_der) = order.to_ec_csr(ready_order, private_key)\n"
" ```\n"
).
-spec to_ec_csr(order(), kryptos@ec:private_key()) -> {ok, bitstring()} |
{error, csr_error()}.
to_ec_csr(Order, Key) ->
gleam@result:'try'(
build_csr_builder(Order),
fun(Builder) ->
Hash_algorithm = hash_for_ec_curve(
kryptos_ffi:ec_private_key_curve(Key)
),
_pipe = kryptos@x509@csr:sign_with_ecdsa(
Builder,
Key,
Hash_algorithm
),
_pipe@1 = gleam@result:map(_pipe, fun kryptos@x509@csr:to_der/1),
gleam@result:replace_error(_pipe@1, signing_failed)
end
).
-file("src/acumen/order.gleam", 154).
-spec hash_for_rsa_key(kryptos@rsa:private_key()) -> kryptos@hash:hash_algorithm().
hash_for_rsa_key(Key) ->
case kryptos_ffi:rsa_private_key_modulus_bits(Key) of
Bits when Bits >= 4096 ->
sha512;
Bits@1 when Bits@1 >= 3072 ->
sha384;
_ ->
sha256
end.
-file("src/acumen/order.gleam", 143).
?DOC(
" Generates a CSR from an order using an RSA key.\n"
"\n"
" Uses the order's identifiers as Subject Alternative Names, with the first\n"
" DNS identifier as the Common Name. Returns the CSR in DER format.\n"
"\n"
" Hash algorithm is selected by key size: SHA-512 for 4096+ bits,\n"
" SHA-384 for 3072+, SHA-256 otherwise.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(#(private_key, _public_key)) = rsa.generate_key_pair(2048)\n"
" let assert Ok(csr_der) = order.to_rsa_csr(ready_order, private_key)\n"
" ```\n"
).
-spec to_rsa_csr(order(), kryptos@rsa:private_key()) -> {ok, bitstring()} |
{error, csr_error()}.
to_rsa_csr(Order, Key) ->
gleam@result:'try'(
build_csr_builder(Order),
fun(Builder) ->
Hash_algorithm = hash_for_rsa_key(Key),
_pipe = kryptos@x509@csr:sign_with_rsa(Builder, Key, Hash_algorithm),
_pipe@1 = gleam@result:map(_pipe, fun kryptos@x509@csr:to_der/1),
gleam@result:replace_error(_pipe@1, signing_failed)
end
).
-file("src/acumen/order.gleam", 213).
-spec status_decoder() -> gleam@dynamic@decode:decoder(status()).
status_decoder() ->
gleam@dynamic@decode:field(
<<"status"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Status_string) ->
gleam@dynamic@decode:optional_field(
<<"certificate"/utf8>>,
none,
gleam@dynamic@decode:optional(acumen@url:decoder()),
fun(Certificate_url) -> case {Status_string, Certificate_url} of
{<<"pending"/utf8>>, _} ->
gleam@dynamic@decode:success(pending);
{<<"ready"/utf8>>, _} ->
gleam@dynamic@decode:success(ready);
{<<"processing"/utf8>>, _} ->
gleam@dynamic@decode:success(processing);
{<<"valid"/utf8>>, {some, Url}} ->
gleam@dynamic@decode:success({valid, Url});
{<<"valid"/utf8>>, none} ->
gleam@dynamic@decode:failure(
pending,
<<"OrderStatus(valid requires certificate URL)"/utf8>>
);
{<<"invalid"/utf8>>, _} ->
gleam@dynamic@decode:success(invalid);
{_, _} ->
gleam@dynamic@decode:failure(
pending,
<<<<"OrderStatus(unknown: "/utf8,
Status_string/binary>>/binary,
")"/utf8>>
)
end end
)
end
).
-file("src/acumen/order.gleam", 163).
?DOC(false).
-spec decoder(acumen@url:url()) -> gleam@dynamic@decode:decoder(order()).
decoder(Url) ->
gleam@dynamic@decode:then(
status_decoder(),
fun(Status) ->
gleam@dynamic@decode:field(
<<"identifiers"/utf8>>,
gleam@dynamic@decode:list(acumen:identifier_decoder()),
fun(Identifiers) ->
gleam@dynamic@decode:field(
<<"authorizations"/utf8>>,
gleam@dynamic@decode:list(acumen@url:decoder()),
fun(Authorizations) ->
gleam@dynamic@decode:field(
<<"finalize"/utf8>>,
acumen@url:decoder(),
fun(Finalize_url) ->
gleam@dynamic@decode:optional_field(
<<"expires"/utf8>>,
none,
gleam@dynamic@decode:optional(
acumen@internal@utils:timestamp_decoder(
)
),
fun(Expires) ->
gleam@dynamic@decode:optional_field(
<<"notBefore"/utf8>>,
none,
gleam@dynamic@decode:optional(
acumen@internal@utils:timestamp_decoder(
)
),
fun(Not_before) ->
gleam@dynamic@decode:optional_field(
<<"notAfter"/utf8>>,
none,
gleam@dynamic@decode:optional(
acumen@internal@utils:timestamp_decoder(
)
),
fun(Not_after) ->
gleam@dynamic@decode:optional_field(
<<"profile"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder,
fun gleam@dynamic@decode:decode_string/1}
),
fun(Profile) ->
gleam@dynamic@decode:optional_field(
<<"error"/utf8>>,
none,
acumen@challenge:error_decoder(
),
fun(
Error
) ->
gleam@dynamic@decode:success(
{order,
Url,
Status,
Identifiers,
Authorizations,
Finalize_url,
Expires,
Not_before,
Not_after,
Profile,
Error}
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/acumen/order.gleam", 234).
?DOC(false).
-spec parse_order_response(
gleam@http@response:response(binary()),
acumen@url:url()
) -> {ok, order()} | {error, acumen:acme_error()}.
parse_order_response(Resp, Order_url) ->
_pipe = gleam@json:parse(erlang:element(4, Resp), decoder(Order_url)),
gleam@result:map_error(
_pipe,
fun(Error) ->
{json_parse_error,
acumen@internal@utils:json_parse_error_message(
<<"order"/utf8>>,
Error
)}
end
).