Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source
Page
Library
Module
Module type
Parameter
Class
Class type
Source
acme_client.ml1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529open Acme_common let src = Logs.Src.create "letsencrypt" ~doc:"let's encrypt library" module Log = (val Logs.src_log src : Logs.LOG) let ( let* ) = Result.bind let msgf fmt = Fmt.kstr (fun msg -> `Msg msg) fmt let key token = let pk = X509.Private_key.public key in let pk = Jws.Jwk.of_public_key_exn pk in Fmt.str "%s.%s" token (Jws.Jwk.signature pk) type t = { account_key : X509.Private_key.t; mutable next_nonce : string; d : Directory.t; account_url : string; } type challenge = Challenge.typ = DNS | HTTP | ALPN module type S = sig type 'a t val bind : 'a t -> ('a -> 'b t) -> 'b t val return : 'a -> 'a t end module type Client = sig type 'a t type ctx type error type meth = [ `HEAD | `GET | `POST ] type response = { headers : (string * string) list ; status : int } val request : ?ctx:ctx -> ?meth:meth -> ?headers:(string * string) list -> ?body:string -> string -> (response * string, error) result t end module Solver (S : S) = struct type solver = { challenge : challenge; solve_challenge : token:string -> key_authorization:string -> [`host] Domain_name.t -> (unit, [ `Msg of string]) result S.t; } let http_solver writef = let solve_challenge ~token ~ domain = let prefix = ".well-known/acme-challenge" in writef domain ~prefix ~token ~content:key_authorization in { challenge = HTTP; solve_challenge } let print_http = let solve domain ~prefix ~token ~content = Log.warn (fun f -> f "Setup http://%a/%s/%s to serve %s and press enter to continue" Domain_name.pp domain prefix token content); ignore (read_line ()); S.return (Ok ()) in http_solver solve let alpn_solver ?(key_type = `RSA) ?(bits = 2048) writef = (* on the ID-PE arc (from RFC 5280), 31 *) let id_pe_acme = Asn.OID.(base 1 3 <| 6 <| 1 <| 5 <| 5 <| 7 <| 1 <| 31) and alpn = "acme-tls/1" in (* extension value is an octet_string of the hash *) let encode_val hash = let enc = Asn.(encode (codec der S.octet_string)) in enc hash in let solve_challenge ~token:_ ~ domain = let open X509 in let priv = Private_key.generate ~bits key_type in let solution = Digestif.SHA256.digest_string key_authorization in let solution = Digestif.SHA256.to_raw_string solution in let name = Domain_name.to_string domain in let cn = Distinguished_name.CN name in let dn = [ Distinguished_name.Relative_distinguished_name.singleton cn ] in let extensions = let gn = General_name.(singleton DNS [ name ]) in let full = encode_val solution in Extension.(add Subject_alt_name (false, gn) (singleton (Unsupported id_pe_acme) (true, full))) in let valid_from, valid_until = Ptime.epoch, Ptime.epoch in match let* csr = Signing_request.create dn priv in Result.map_error (fun e -> `Msg (Fmt.to_to_string X509.Validation.pp_signature_error e)) (Signing_request.sign csr ~valid_from ~valid_until ~extensions priv dn) with | Ok cert -> writef domain ~alpn priv cert | Error _ as e -> S.return e in { challenge = ALPN; solve_challenge } let print_alpn = let solve domain ~alpn priv cert = Log.warn (fun f -> f "Setup a TLS server for %a (ALPN %s) to use key %s and certificate %s. Press enter to continue" Domain_name.pp domain alpn (X509.Private_key.encode_pem priv) (X509.Certificate.encode_pem cert)); ignore (read_line ()); S.return (Ok ()) in alpn_solver solve end module Make (S : S) (C : Client with type 'a t = 'a S.t) = struct include Solver (S) let ( let* ) x fn = S.bind x @@ function | Error _ as err -> S.return err | Ok v -> fn v let ( >>= ) = S.bind let error_msgf fmt = Fmt.kstr (fun msg -> S.return (Error (`Msg msg))) fmt let ok v = S.return (Ok v) let guard ~err fn = if fn () then ok () else S.return (Error err) let location r = let fn (k, v) = String.lowercase_ascii k, v in let hdrs = List.map fn r.C.headers in match List.assoc_opt "location" hdrs with | Some url -> ok url | None -> error_msgf "Expected a location header, but couldn't find it" let request ?ctx ?meth ?headers ?body url = C.request ?ctx ?meth ?headers ?body url >>= function | Ok value -> ok value | Error err -> S.return (Error (`HTTP err)) let extract_nonce r = let hdrs = List.map (fun (k, v) -> String.lowercase_ascii k, v) r.C.headers in match List.assoc_opt "replay-nonce" hdrs with | Some nonce -> ok nonce | None -> error_msgf "Nonce not found" let discover ?ctx directory = let* resp, body = request ?ctx ~meth:`GET directory in match resp.C.status with | 200 -> S.return (Directory.decode body) | c -> error_msgf "Impossible to discover your ACME service: status %u - body: %S" c body let get_nonce ?ctx url = let* r, body = request ?ctx ~meth:`HEAD url in match r.C.status with | 200 -> extract_nonce r | c -> error_msgf "Invalid response from HEAD request to %s, status: %u - body %S" url c body let rec post ?ctx ?(with_kid = false) cli data url = let prepare key nonce = let kid = if with_kid then None else Some cli.account_url in let extra = Jws.S.singleton "url" (Jsont.Json.string url) in let key = Jws.Pk.of_private_key_exn key in let data = Jsont_bytesrw.encode_string Jsont.json data |> Result.get_ok in let body = Jws.encode ?kid ~extra ~nonce key data in let headers = [ "Content-Type", "application/jose+json" ; "Content-Length", string_of_int (String.length body) ] in (headers, body) in let headers, body = prepare cli.account_key cli.next_nonce in Log.debug (fun m -> m "HTTP post %s (data %a body %S)" url Jsont.Json.pp data body); let* resp, body = request ?ctx ~meth:`POST ~body ~headers url in Log.debug (fun m -> m "Got code: %3d" resp.C.status); Log.debug (fun m -> m "headers %a" Fmt.(Dump.list (pair string string)) resp.C.headers); Log.debug (fun m -> m "body %S" body); let* () = extract_nonce resp >>= function | Ok nonce -> cli.next_nonce <- nonce; ok () | Error (`Msg msg) -> Log.err (fun m -> m "Couldn't extract nonce: %s" msg); ok () in match resp.C.status with | 400 -> let* err = S.return (Error.decode body) in begin match err.Error.error with | `Bad_nonce -> Log.warn (fun m -> m "received bad nonce %s from server, retrying same request" err.detail); post ?ctx cli data url | _ -> ok (resp, body) end | _ -> ok (resp, body) let create_account ?ctx ?email cli = let url = cli.d.newAccount in let contact = match email with | None -> [] | Some email -> let open Jsont.Json in [ mem (name "contact") (list [string ("mailto:" ^ email)]) ] in let body = let open Jsont.Json in object' (mem (name "termsOfServiceAgreed") (bool true) :: contact) in let* resp, body = post ?ctx ~with_kid:true cli body url in match resp.C.status with | 201 -> let* account = Account.decode body |> S.return in let* () = let err = msgf "Account %a does not have status valid" Account.pp account in guard ~err @@ fun () -> (account.Account.status = Account.Valid) in let* account_url = location resp in ok { cli with account_url } | c -> error_msgf "Invalid response to newAccount, status: %u - body %S" c body let get_account ?ctx cli url = let* resp, body = post ?ctx cli Jsont.Json.(null ()) url in match resp.C.status with | 200 -> (* at least staging doesn't include orders *) let* acc = S.return (Account.decode body) in (* well, here we may encounter some orders which should be processed (or cancelled, considering the lack of a csr)! *) Log.info (fun m -> m "account %a" Account.pp acc); ok () | c -> error_msgf "Invalid response to get_account, status: %u - body %S" c body let find_account_url ?ctx ?email ~nonce key directory = let url = directory.Directory.newAccount in let body = let open Jsont.Json in object' [ mem (name "onlyReturnExisting") (bool true) ] in let cli = { next_nonce = nonce ; account_key = key ; d= directory ; account_url = String.empty } in let* resp, body = post ?ctx ~with_kid:false cli body url in match resp.C.status with | 200 -> (* unclear why this is not an account object, as required in 7.3.0/7.3.1 *) let* account = S.return (Account.decode body) in let* () = let err = msgf "Account %a does not have status valid" Account.pp account in guard ~err @@ fun () -> account.Account.status = Account.Valid in let* account_url = location resp in ok { cli with account_url } | 400 -> let* err = S.return (Error.decode body) in if err.Error.error = `Account_does_not_exist then begin Log.info (fun m -> m "account does not exist, creating an account"); create_account ?ctx ?email cli end else begin Log.err (fun m -> m "error %a in find account url" Error.pp err); error_msgf "newAccount: %s" err.Error.detail end (* according to RFC 8555 7.3.3 there can be a forbidden if ToS were updated, and the client should re-approve them *) | status -> error_msgf "find_account_url: unexpected status %u - body %S" status body let challenge_solved ?ctx cli url = let body = Jsont.Json.(object' []) in (* not entirely clear why this now is {} and not "" *) let* resp, body = post ?ctx cli body url in match resp.C.status with | 200 -> Log.info (fun m -> m "challenge solved POSTed (OK), body %s" body); ok () | 201 -> Log.info (fun m -> m "challenge solved POSTed (CREATE), body %s" body); ok () | status -> error_msgf "challenge solved: status %u - body: %S" status body let process_challenge ?ctx solver cli sleep host challenge = (* overall plan: - solve it (including "provisioning" - for now maybe a sleep 5) - report back to server that it is now solved *) (* good news is that we already ensured that the solver and challenge fit *) match challenge.Challenge.status with | Pending -> (* do some work :) solve it! *) let token = challenge.token in let = key_authorization cli.account_key token in let* () = S.bind (solver.solve_challenge ~token ~key_authorization host) @@ function | (Ok _) as r -> S.return r | Error (`Msg _) as r -> S.return r in challenge_solved ?ctx cli challenge.url | Processing -> (* ehm - relax and wait till the server figured something out? *) (* but there's as well the notion of "Likewise, client requests for retries do not cause a state change." *) (* it looks like in processing after some _client_defined_timeout_, the client may approach to server to re-evaluate *) (* from Section 8.2 *) (* While the server is still trying, the status of the challenge remains "processing"; it is only marked "invalid" once the server has given up. The server MUST provide information about its retry state to the client via the "error" field in the challenge and the Retry-After HTTP header field in response to requests to the challenge resource. The server MUST add an entry to the "error" field in the challenge after each failed validation query. The server SHOULD set the Retry- After header field to a time after the server's next validation query, since the status of the challenge will not change until that time. Clients can explicitly request a retry by re-sending their response to a challenge in a new POST request (with a new nonce, etc.). This allows clients to request a retry when the state has changed (e.g., after firewall rules have been updated). Servers SHOULD retry a request immediately on receiving such a POST request. In order to avoid denial-of-service attacks via client-initiated retries, servers SHOULD rate-limit such requests. *) (* so what shall we do? wait? *) Log.info (fun m -> m "challenge is processing, let's wait a second"); sleep 1 >>= fun () -> ok () | Valid -> (* nothing to do from our side *) ok () | Invalid -> (* we lost *) S.return (Error (`Msg "challenge invalid")) (* yeah, we could parallelize them... but first not do it. *) let ?ctx solver cli sleep url = let body = Jsont.Json.(null ()) in let* resp, body = post ?ctx cli body url in match resp.C.status with | 200 -> let* auth = S.return (Authorization.decode body) in Log.info (fun m -> m "authorization %a" Authorization.pp auth); begin match auth.Authorization.status with | Pending -> (* we need to work on some challenge here! *) let host = Domain_name.(host_exn @@ of_string_exn @@ auth.identifier) in begin match List.filter (fun c -> c.Challenge.typ = solver.challenge) auth.challenges with | [] -> Log.err (fun m -> m "no challenge found for solver"); S.return (Error (`Msg "couldn't find a challenge that matches the provided solver")) | c::cs -> if not (cs = []) then Log.err (fun m -> m "multiple (%d) challenges found for solver, taking head" (succ (List.length cs))); process_challenge ?ctx solver cli sleep host c end | Valid -> (* we can ignore it - some challenge made it *) Log.info (fun m -> m "authorization is valid"); ok () | Invalid -> (* no chance this will ever be good again, or is there? *) Log.err (fun m -> m "authorization is invalid"); S.return (Error (`Msg "invalid")) | Deactivated -> (* client-side deactivated / retracted *) Log.err (fun m -> m "authorization is deactivated"); S.return (Error (`Msg "deactivated")) | Expired -> (* timeout *) Log.err (fun m -> m "authorization is expired"); S.return (Error (`Msg "expired")) | Revoked -> (* server-side deactivated *) Log.err (fun m -> m "authorization is revoked"); S.return (Error (`Msg "revoked")) end | status -> error_msgf "authorization: status %u - body: %S" status body let finalize ?ctx cli csr url = let body = let csr_as_b64 = X509.Signing_request.encode_der csr |> Jws.Base64u.encode in let open Jsont.Json in object' [ mem (name "csr") (string csr_as_b64) ] in let* resp, body = post ?ctx cli body url in match resp.C.status with | 200 -> let* order = S.return (Order.decode body) in ok (resp.C.headers, order) | status -> error_msgf "finalize: status %u - body: %S" status body let dl_certificate ?ctx cli url = let body = Jsont.Json.(null ()) in let* resp, body = post ?ctx cli body url in match resp.C.status with | 200 -> (* body is a certificate chain (no comments), with end-entity certificate being the first *) (* TODO: check order? figure out chain? *) S.return (X509.Certificate.decode_pem_multiple body) | status -> error_msgf "certificate: status %u - body: %S" status body let get_order ?ctx cli url = let body = Jsont.Json.(null ()) in let* resp, body = post ?ctx cli body url in match resp.C.status with | 200 -> let* order = Order.decode body |> S.return in ok (resp.C.headers, order) | status -> error_msgf "getting order: status %u - body: %S" status body (* HTTP defines this header as "either seconds" or "absolute HTTP date" *) let retry_after headers = let hdrs = List.map (fun (k, v) -> String.lowercase_ascii k, v) headers in match List.assoc_opt "retry-after" hdrs with | None -> 1 | Some x -> try int_of_string x with Failure _ -> Log.warn (fun m -> m "retry-after header is not an integer, but %s (using 1 second instead)" x); 1 (* TODO this 'expires' stuff in the order *) (* state machine is slightly unclear, from section 7.4 (page 47 top): "Once the client believes it has fulfilled the server's requirements, it should send a POST request to the order resource's finalize URL" does this mean e.g. retry-after should as well be done to the finalize URL? (rather than the order URL) page 48 says: "A request to finalize an order will result in error if the order is not in the "ready" state. In such cases, the server MUST return a 403 (Forbidden) error with a problem document of type "orderNotReady". The client should then send a POST-as-GET request to the order resource to obtain its current state." and also "If a request to finalize an order is successful, the server will return a 200 (OK) with an updated order object. The status of the order will indicate what action the client should take" so basically the "order" object returned by finalize is only every in "processing" or "pending", or do I misunderstand anything? if it is in a different state, a 403 would've been issued (not telling what is wrong) - with orderNotReady; if the CSR is bad, some unspecified HTTP status is returned, with "badCSR" as error code. how convenient. *) let rec process_order ?ctx solver cli sleep csr order_url headers order = (* as usual, first do the easy stuff ;) *) match order.Order.status with | Invalid -> (* exterminate -- consider the order process abandoned *) Log.err (fun m -> m "order %a is invalid, falling apart" Order.pp order); S.return (Error (`Msg "attempting to process an invalid order")) | Pending -> (* there's still some authorization pending, according to the server! *) Log.warn (fun m -> m "something is pending here... need to work on this"); let rec fold = function | [] -> ok () | a :: rest -> let* () = process_authorization ?ctx solver cli sleep a in fold rest in let* () = fold order.authorizations in let* headers, order = get_order ?ctx cli order_url in process_order ?ctx solver cli sleep csr order_url headers order | Ready -> (* server agrees that requirements are fulfilled, submit a finalization request *) let* headers, order = finalize ?ctx cli csr order.finalize in process_order ?ctx solver cli sleep csr order_url headers order | Processing -> (* sleep Retry-After header field time, and re-get order to hopefully get a certificate url *) let retry_after = retry_after headers in Log.debug (fun m -> m "sleeping for %d seconds" retry_after); sleep retry_after >>= fun () -> let* headers, order = get_order ?ctx cli order_url in process_order ?ctx solver cli sleep csr order_url headers order | Valid -> (* the server has issued the certificate and provisioned its URL in the certificate field of the order *) match order.certificate with | None -> Log.warn (fun m -> m "received valid order %a without certificate URL, should not happen" Order.pp order); S.return (Error (`Msg "valid order without certificate URL")) | Some cert -> let* certs = dl_certificate ?ctx cli cert in Log.info (fun m -> m "retrieved %d certificates" (List.length certs)); List.iter (fun c -> Log.info (fun m -> m "%s" (X509.Certificate.encode_pem c))) certs; ok certs let new_order ?ctx solver cli sleep csr = let hostnames = X509.Host.Set.fold (fun (typ, name) acc -> let pre = match typ with `Strict -> "" | `Wildcard -> "*." in (pre ^ Domain_name.to_string name) :: acc) (X509.Signing_request.hostnames csr) [] in let body = (* TODO this may contain "notBefore" and "notAfter" as RFC3339 encoded timestamps (what the client would like as validity of the certificate) *) let open Jsont.Json in let ids = List.map (fun hostname -> object' [ mem (name "type") (string "dns") ; mem (name "value") (string hostname) ]) hostnames in object' [ mem (name "identifiers") (list ids) ] in let* resp, body = post ?ctx cli body cli.d.Directory.newOrder in match resp.C.status with | 201 -> let* order = S.return (Order.decode body) in (* identifiers (should-be-verified to be the same set as the hostnames above?) *) let* order_url = location resp in process_order ?ctx solver cli sleep csr order_url resp.C.headers order | status -> error_msgf "newOrder: status %u - body: %S" status body let sign_certificate ?ctx solver cli sleep csr = (* send a newOrder request for all the host names in the CSR *) (* but as well need to check that we're able to solve authorizations for the names *) new_order ?ctx solver cli sleep csr let supported_key = function | `RSA _ | `P256 _ | `P384 _ | `P521 _ -> Ok () | _ -> Error (`Msg "unsupported key type") let initialise ?ctx ~endpoint ?email account_key = (* create a new client *) let* () = S.return (supported_key account_key) in let* d = discover ?ctx endpoint in Log.info (fun m -> m "discovered directory %a" Directory.pp d); let* nonce = get_nonce ?ctx d.Directory.newNonce in Log.info (fun m -> m "got nonce %s" nonce); (* now there are two ways forward - register a new account based on account_key - retrieve account URL for account_key (if already registered) let's first try the latter -- the former is done by find_account_url if account does not exist! *) find_account_url ?ctx ?email ~nonce account_key d end