mte/unikernel/duniverse/ocaml-dns/app/ocertify.ml
2025-11-11 02:07:51 +01:00

205 lines
7.5 KiB
OCaml

(* (c) 2018 Hannes Mehnert, all rights reserved *)
let ( let* ) = Result.bind
let find_or_generate_key key_filename keytype keydata seed bits =
let* f_exists = Bos.OS.File.exists key_filename in
if f_exists then
let* data = Bos.OS.File.read key_filename in
X509.Private_key.decode_pem data
else
let* key =
match keydata with
| None -> Ok (X509.Private_key.generate ?seed ~bits keytype)
| Some s ->
let* s = Base64.decode s in
X509.Private_key.of_octets s keytype
in
let pem = X509.Private_key.encode_pem key in
let* () = Bos.OS.File.write ~mode:0o600 key_filename pem in
Ok key
let query_certificate sock fqdn csr =
match Dns_certify.query Mirage_crypto_rng.generate (Ptime_clock.now ()) fqdn csr with
| Error e -> Error e
| Ok (out, cb) ->
Dns_cli.send_tcp sock out;
let data = Dns_cli.recv_tcp sock in
cb data
let nsupdate_csr sock host keyname zone dnskey csr =
match Dns_certify.nsupdate Mirage_crypto_rng.generate Ptime_clock.now ~host ~keyname ~zone dnskey csr with
| Error s -> Error s
| Ok (out, cb) ->
Dns_cli.send_tcp sock out;
let data = Dns_cli.recv_tcp sock in
match cb data with
| Ok () -> Ok ()
| Error e -> Error (`Msg (Fmt.str "nsupdate reply error %a" Dns_certify.pp_u_err e))
let jump _ server_ip port hostname more_hostnames dns_key_opt csr key keytype keydata seed bits cert force =
Mirage_crypto_rng_unix.use_default ();
let fn suffix = function
| None -> Fpath.(v (Domain_name.to_string hostname) + suffix)
| Some x -> Fpath.v x
in
let csr_filename = fn "req" csr
and key_filename = fn "key" key
and cert_filename = fn "pem" cert
in
let* csr =
let* f_exists = Bos.OS.File.exists csr_filename in
if f_exists then
let* data = Bos.OS.File.read csr_filename in
X509.Signing_request.decode_pem data
else
let* key = find_or_generate_key key_filename keytype keydata seed bits in
let* csr = Dns_certify.signing_request hostname ~more_hostnames key in
let pem = X509.Signing_request.encode_pem csr in
let* () = Bos.OS.File.write csr_filename pem in
Ok csr
in
(* before doing anything, let's check whether cert_filename is present,
the public key matches, and the certificate is still valid *)
let now = Ptime_clock.now () in
let tomorrow =
let (d, ps) = Ptime.Span.to_d_ps (Ptime.to_span now) in
Ptime.v (succ d, ps)
in
let* cert =
let* f_exists = Bos.OS.File.exists cert_filename in
if f_exists then
let* data = Bos.OS.File.read cert_filename in
let* certs = X509.Certificate.decode_pem_multiple data in
match List.filter (fun c -> X509.Certificate.supports_hostname c hostname) certs with
| [] -> Ok None
| [ cert ] -> Ok (Some cert)
| _ -> Error (`Msg "multiple certificates that match the hostname")
else
Ok None
in
let* () =
match cert with
| Some cert ->
if not force && Dns_certify.cert_matches_csr ~until:tomorrow now csr cert then
Error (`Msg "valid certificate with matching key already present")
else
Ok ()
| None -> Ok ()
in
(* strategy: unless force is provided, we can request DNS, and if a
certificate is present, compare its public key with csr public key *)
let write_certificate certs =
let data = X509.Certificate.encode_pem_multiple certs in
let* () = Bos.OS.File.delete cert_filename in
Bos.OS.File.write cert_filename data
in
let sock = Dns_cli.connect_tcp server_ip port in
let* should_update =
if force then
Ok true
else match query_certificate sock hostname csr with
| Ok (server, chain) ->
Logs.app (fun m -> m "found cached certificate in DNS");
let* () = write_certificate (server :: chain) in
Ok false
| Error `No_tlsa ->
Logs.debug (fun m -> m "no TLSA found, sending update");
Ok true
| Error (`Msg m) -> Error (`Msg m)
| Error ((`Decode _ | `Bad_reply _ | `Unexpected_reply _) as e) ->
Error (`Msg (Fmt.str "error %a while parsing TLSA reply"
Dns_certify.pp_q_err e))
in
if not should_update then
Ok ()
else
let* () =
match dns_key_opt with
| None -> Error (`Msg "no dnskey provided, but required for uploading CSR")
| Some (keyname, zone, dnskey) ->
let* () = nsupdate_csr sock hostname keyname zone dnskey csr in
let rec request retries =
match query_certificate sock hostname csr with
| Error (`Msg msg) -> Error (`Msg msg)
| Error #Dns_certify.q_err when retries = 0 ->
Error (`Msg "failed to retrieve certificate (tried 10 times)")
| Error `No_tlsa ->
Logs.warn (fun m -> m "still no tlsa, sleeping two more seconds");
Unix.sleep 2;
request (pred retries)
| Error (#Dns_certify.q_err as e) ->
Logs.err (fun m -> m "error %a while handling TLSA reply (retrying)"
Dns_certify.pp_q_err e);
request (pred retries)
| Ok (server, chain) -> write_certificate (server :: chain)
in
request 10
in
Logs.app (fun m -> m "success! your certificate is stored in %a (private key %a, csr %a)"
Fpath.pp cert_filename Fpath.pp key_filename Fpath.pp csr_filename);
Ok ()
open Cmdliner
let dns_server =
let doc = "DNS server IP" in
Arg.(required & pos 0 (some Dns_cli.ip_c) None & info [] ~doc ~docv:"IP")
let port =
let doc = "Port to connect to" in
Arg.(value & opt int 53 & info [ "port" ] ~doc)
let dns_key =
let doc = "nsupdate key (name:alg:b64key, where name is YYY._update.zone)" in
Arg.(value & opt (some Dns_cli.namekey_c) None & info [ "dns-key" ] ~doc ~docv:"KEY")
let hostname =
let doc = "Hostname (FQDN) to issue a certificate for" in
Arg.(required & pos 1 (some Dns_cli.name_c) None & info [] ~doc ~docv:"HOSTNAME")
let more_hostnames =
let doc = "Additional hostnames to be included in the certificate as SubjectAlternativeName extension" in
Arg.(value & opt_all Dns_cli.domain_name_c [] & info ["additional"] ~doc ~docv:"HOSTNAME")
let csr =
let doc = "certificate signing request filename (defaults to hostname.req)" in
Arg.(value & opt (some string) None & info [ "csr" ] ~doc)
let key =
let doc = "private key filename (default to hostname.key)" in
Arg.(value & opt (some string) None & info [ "key" ] ~doc)
let seed =
let doc = "private key seed (or full private key if keytype is a EC key)" in
Arg.(value & opt (some string) None & info [ "seed" ] ~doc)
let bits =
let doc = "private key bits" in
Arg.(value & opt int 4096 & info [ "bits" ] ~doc)
let keydata =
let doc = "private key (base64 encoded)" in
Arg.(value & opt (some string) None & info [ "data" ] ~doc)
let keytype =
let doc = "keytype to generate" in
Arg.(value & opt (enum X509.Key_type.strings) `RSA & info [ "type" ] ~doc)
let cert =
let doc = "certificate filename (defaults to hostname.pem)" in
Arg.(value & opt (some string) None & info [ "certificate" ] ~doc)
let force =
let doc = "force signing request to DNS" in
Arg.(value & flag & info [ "force" ] ~doc)
let ocertify =
let doc = "ocertify requests a signed certificate" in
let man = [ `S "BUGS"; `P "Submit bugs to me";] in
let term =
Term.(term_result (const jump $ Dns_cli.setup_log $ dns_server $ port $ hostname $ more_hostnames $ dns_key $ csr $ key $ keytype $ keydata $ seed $ bits $ cert $ force))
and info = Cmd.info "ocertify" ~version:"10.2.2" ~doc ~man
in
Cmd.v info term
let () = exit (Cmd.eval ocertify)