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