123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774letletsencrypt_production_url=Uri.of_string"https://acme-v02.api.letsencrypt.org/directory"letletsencrypt_staging_url=Uri.of_string"https://acme-staging-v02.api.letsencrypt.org/directory"letsha256_and_base64a=Primitives.sha256a|>B64u.urlencodelet(let*)=Result.bindmoduleJ=Yojson.Basictypejson=J.t(* Serialize a json object without having spaces around. Dammit Yojson. *)(* XXX. I didn't pay enough attention on escaping.
* It is possible that this is okay; however, our encodings are nice. *)(* NOTE: hannes thinks that Json.to_string (`String {|foo"bar|}) looks suspicious *)letrecjson_to_string?(comma=",")?(colon=":"):J.t->string=function|`Null->""|`Strings->Printf.sprintf{|"%s"|}(String.escapeds)|`Boolb->ifbthen"true"else"false"|`Floatf->string_of_floatf|`Inti->string_of_inti|`Listl->lets=List.map(json_to_string~comma~colon)lin"["^(String.concatcommas)^"]"|`Assoca->letserialize_pair(key,value)=Printf.sprintf{|"%s"%s%s|}keycolon(json_to_string~comma~colonvalue)inlets=List.mapserialize_pairainPrintf.sprintf{|{%s}|}(String.concatcommas)letof_strings=tryOk(J.from_strings)withYojson.Json_errorstr->Error(`Msgstr)leterr_msgtypnamejson=Error(`Msg(Fmt.str"couldn't find %s %s in %s"typname(J.to_stringjson)))(* decoders *)letstring_valkeyjson=matchJ.Util.memberkeyjsonwith|`Strings->Oks|_->err_msg"string"keyjsonletopt_string_valkeyjson=matchJ.Util.memberkeyjsonwith|`Strings->Ok(Somes)|`Null->OkNone|_->err_msg"opt_string"keyjsonletjson_valmemberjson=matchJ.Util.membermemberjsonwith|`Assocj->Ok(`Assocj)|_->err_msg"json object"memberjsonletb64_z_valmemberjson=let*s=string_valmemberjsoninB64u.urldecodezsletb64_string_valmemberjson=let*s=string_valmemberjsoninB64u.urldecodesletassoc_valkeyjson=matchJ.Util.memberkeyjsonwith|`Assoc_|`Nullasx->Okx|_->err_msg"assoc"keyjsonletlist_valkeyjson=matchJ.Util.memberkeyjsonwith|`Listl->Okl|_->err_msg"list"keyjsonletopt_string_listkeyjson=matchJ.Util.memberkeyjsonwith|`Listl->letxs=List.fold_left(funacc->function`Strings->s::acc|_->acc)[]linOk(Somexs)|`Null->OkNone|_->err_msg"string list"keyjsonletopt_boolkeyjson=matchJ.Util.memberkeyjsonwith|`Boolb->Ok(Someb)|`Null->OkNone|_->err_msg"opt bool"keyjsonletdecode_ptimestr=matchPtime.of_rfc3339strwith|Ok(ts,_,_)->Okts|Error`RFC3339(_,err)->Error(`Msg(Fmt.str"couldn't parse %s as rfc3339 %a"strPtime.pp_rfc3339_errorerr))letmaybef=function|None->OkNone|Somes->let*s'=fsinOk(Somes')moduleJwk=structtypekey=X509.Public_key.tletencode=function|`RSAkey->lete,n=Primitives.pub_to_zkeyin`Assoc["e",`String(B64u.urlencodeze);"kty",`String"RSA";"n",`String(B64u.urlencodezn);]|`P256key->letcs=Mirage_crypto_ec.P256.Dsa.pub_to_octetskeyinletx,y=String.subcs132,String.subcs3332in`Assoc["crv",`String"P-256";"kty",`String"EC";"x",`String(B64u.urlencodex);"y",`String(B64u.urlencodey);]|`P384key->letcs=Mirage_crypto_ec.P384.Dsa.pub_to_octetskeyinletx,y=String.subcs148,String.subcs4948in`Assoc["crv",`String"P-384";"kty",`String"EC";"x",`String(B64u.urlencodex);"y",`String(B64u.urlencodey);]|`P521key->letcs=Mirage_crypto_ec.P521.Dsa.pub_to_octetskeyinletx,y=String.subcs166,String.subcs6766in`Assoc["crv",`String"P-521";"kty",`String"EC";"x",`String(B64u.urlencodex);"y",`String(B64u.urlencodey);]|_->assertfalseletdecode_jsonjson=let*kty=string_val"kty"jsoninmatchktywith|"RSA"->let*e=b64_z_val"e"jsoninlet*n=b64_z_val"n"jsoninlet*pub=Primitives.pub_of_z~e~ninOk(`RSApub)|"EC"->letfour=String.make1'\004'inlet*x=b64_string_val"x"jsoninlet*y=b64_string_val"y"jsoninlet*crv=string_val"crv"jsoninbeginmatchcrvwith|"P-256"->let*pub=Result.map_error(fune->`Msg(Fmt.to_to_stringMirage_crypto_ec.pp_errore))(Mirage_crypto_ec.P256.Dsa.pub_of_octets(String.concat""[four;x;y]))inOk(`P256pub)|"P-384"->let*pub=Result.map_error(fune->`Msg(Fmt.to_to_stringMirage_crypto_ec.pp_errore))(Mirage_crypto_ec.P384.Dsa.pub_of_octets(String.concat""[four;x;y]))inOk(`P384pub)|"P-521"->let*pub=Result.map_error(fune->`Msg(Fmt.to_to_stringMirage_crypto_ec.pp_errore))(Mirage_crypto_ec.P521.Dsa.pub_of_octets(String.concat""[four;x;y]))inOk(`P521pub)|x->Error(`Msg(Fmt.str"unknown EC curve %s"x))end|x->Error(`Msg(Fmt.str"unknown key type %s"x))letdecodedata=let*json=of_stringdataindecode_jsonjsonletthumbprintpub_key=letjwk=json_to_string(encodepub_key)inleth=Primitives.sha256jwkinB64u.urlencodehendmoduleJws=structtypeheader={alg:string;nonce:stringoption;jwk:Jwk.keyoption;}letencode?(protected=[])~data?noncepriv=letalg,hash=matchprivwith|`RSA_->"RS256",`SHA256|`P256_->"ES256",`SHA256|`P384_->"ES384",`SHA384|`P521_->"ES512",`SHA512|_->assertfalseinletprotected=letn=matchnoncewithNone->[]|Somex->["nonce",`Stringx]in`Assoc(("alg",`Stringalg)::protected@n)|>json_to_stringinletprotected=protected|>B64u.urlencodeinletpayload=B64u.urlencodedatainletsignature=letm=protected^"."^payloadinPrimitives.signhashprivm|>B64u.urlencodeinletjson=`Assoc["protected",`Stringprotected;"payload",`Stringpayload;"signature",`Stringsignature]injson_to_string~comma:", "~colon:": "jsonletencode_acme?kid_url~data?nonceurlpriv=letkid_or_jwk=matchkid_urlwith|None->"jwk",Jwk.encode(X509.Private_key.publicpriv)|Someurl->"kid",`String(Uri.to_stringurl)inleturl="url",`String(Uri.to_stringurl)inletprotected=[kid_or_jwk;url]inencode~protected~data?nonceprivletdecode_headerprotected_header=let*protected=of_stringprotected_headerinlet*jwk=matchjson_val"jwk"protectedwith|Okkey->let*k=Jwk.decode_jsonkeyinOk(Somek)|Error_->OkNoneinlet*alg=string_val"alg"protectedinletnonce=Result.to_option(string_val"nonce"protected)inOk{alg;nonce;jwk}letdecode?pubdata=let*jws=of_stringdatainlet*protected64=string_val"protected"jwsinlet*payload64=string_val"payload"jwsinlet*signature=b64_string_val"signature"jwsinlet*protected=B64u.urldecodeprotected64inlet*header=decode_headerprotectedinlet*payload=B64u.urldecodepayload64inlet*pub=matchpub,header.jwkwith|Somepub,_->Okpub|None,Somepub->Okpub|None,None->Error(`Msg"no public key found")inletverifyms=matchheader.algwith|"RS256"->Primitives.verify`SHA256pubms|"ES256"->Primitives.verify`SHA256pubms|"ES384"->Primitives.verify`SHA384pubms|"ES512"->Primitives.verify`SHA512pubms|_->falseinletm=protected64^"."^payload64inifverifymsignaturethenOk(header,payload)elseError(`Msg"signature verification failed")endleturis=Ok(Uri.of_strings)moduleDirectory=structtypemeta={terms_of_service:Uri.toption;website:Uri.toption;caa_identities:stringlistoption;(* external_account_required *)}letpp_metappf{terms_of_service;website;caa_identities}=Fmt.pfppf"terms of service: %a@,website %a@,caa identities %a"Fmt.(option~none:(any"no tos")Uri.pp_hum)terms_of_serviceFmt.(option~none:(any"no website")Uri.pp_hum)websiteFmt.(option~none:(any"no CAA")(list~sep:(any", ")string))caa_identitiesletmeta_of_json=function|`Assoc_asjson->let*terms_of_service=let*tos=opt_string_val"termsOfService"jsoninmaybeuritosinlet*website=let*w=opt_string_val"website"jsoninmaybeuriwinlet*caa_identities=opt_string_list"caaIdentities"jsoninOk(Some{terms_of_service;website;caa_identities})|_->OkNonetypet={new_nonce:Uri.t;new_account:Uri.t;new_order:Uri.t;new_authz:Uri.toption;revoke_cert:Uri.t;key_change:Uri.t;meta:metaoption;}letppppfdir=Fmt.pfppf"new nonce %a@,new account %a@,new order %a@,new authz %a@,revoke cert %a@,key change %a@,meta %a"Uri.pp_humdir.new_nonceUri.pp_humdir.new_accountUri.pp_humdir.new_orderFmt.(option~none:(any"no authz")Uri.pp_hum)dir.new_authzUri.pp_humdir.revoke_certUri.pp_humdir.key_changeFmt.(option~none:(any"no meta")pp_meta)dir.metaletdecodes=let*json=of_stringsinlet*new_nonce=let*nn=string_val"newNonce"jsoninurinninlet*new_account=let*na=string_val"newAccount"jsoninurinainlet*new_order=let*no=string_val"newOrder"jsoninurinoinlet*new_authz=let*na=opt_string_val"newAuthz"jsoninmaybeurinainlet*revoke_cert=let*rc=string_val"revokeCert"jsoninurircinlet*key_change=let*kc=string_val"keyChange"jsoninurikcinlet*meta=let*m=assoc_val"meta"jsoninmeta_of_jsonminOk{new_nonce;new_account;new_order;new_authz;revoke_cert;key_change;meta}endmoduleAccount=structtypet={account_status:[`Valid|`Deactivated|`Revoked];contact:stringlistoption;terms_of_service_agreed:booloption;(* externalAccountBinding *)orders:Uri.toption;initial_ip:stringoption;created_at:Ptime.toption;}letpp_statusppfs=Fmt.stringppf(matchswith|`Valid->"valid"|`Deactivated->"deactivated"|`Revoked->"revoked")letppppfa=Fmt.pfppf"status %a@,contact %a@,terms of service agreed %a@,orders %a@,initial IP %a@,created %a"pp_statusa.account_statusFmt.(option~none:(any"no contact")(list~sep:(any", ")string))a.contactFmt.(option~none:(any"unknown")bool)a.terms_of_service_agreedFmt.(option~none:(any"unknown")Uri.pp_hum)a.ordersFmt.(option~none:(any"unknown")string)a.initial_ipFmt.(option~none:(any"unknown")(Ptime.pp_rfc3339()))a.created_atletstatus_of_string=function|"valid"->Ok`Valid|"deactivated"->Ok`Deactivated|"revoked"->Ok`Revoked|s->Error(`Msg(Fmt.str"unknown account status %s"s))(* "it's fine to not have a 'required' orders array" (in contrast to 8555)
and seen in the wild when creating an account, or retrieving the account url
of a key, or even fetching the account url. all with an account that never
ever did an order... it seems to be a discrepancy from LE servers and
RFC 8555 *)(* https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md
or https://github.com/letsencrypt/boulder/issues/3335 contains more
information *)letdecodestr=let*json=of_stringstrinlet*account_status=let*s=string_val"status"jsoninstatus_of_stringsinlet*contact=opt_string_list"contact"jsoninlet*terms_of_service_agreed=opt_bool"termsOfServiceAgreed"jsoninlet*orders=let*o=opt_string_val"orders"jsoninmaybeurioinlet*initial_ip=opt_string_val"initialIp"jsoninlet*created_at=let*ca=opt_string_val"createdAt"jsoninmaybedecode_ptimecainOk{account_status;contact;terms_of_service_agreed;orders;initial_ip;created_at}endtypeid_type=[`Dns]letpp_id_typeppf=function`Dns->Fmt.stringppf"dns"letpp_id=Fmt.(pair~sep:(any" - ")pp_id_typestring)letid_type_of_string=function|"dns"->Ok`Dns|s->Error(`Msg(Fmt.str"only DNS typ is supported, got %s"s))letdecode_idjson=let*typ=let*t=string_val"type"jsoninid_type_of_stringtinlet*id=string_val"value"jsoninOk(typ,id)letdecode_idsids=List.fold_left(funaccjson_id->let*acc=accinlet*id=decode_idjson_idinOk(id::acc))(Ok[])idsmoduleOrder=structtypet={order_status:[`Pending|`Ready|`Processing|`Valid|`Invalid];expires:Ptime.toption;(* required if order_status = pending | valid *)identifiers:(id_type*string)list;not_before:Ptime.toption;not_after:Ptime.toption;error:jsonoption;(* "structured as problem document, RFC 7807" *)authorizations:Uri.tlist;finalize:Uri.t;certificate:Uri.toption;}letpp_statusppfs=Fmt.stringppf(matchswith|`Pending->"pending"|`Ready->"ready"|`Processing->"processing"|`Valid->"valid"|`Invalid->"invalid")letppppfo=Fmt.pfppf"status %a@,expires %a@,identifiers %a@,not_before %a@,not_after %a@,error %a@,authorizations %a@,finalize %a@,certificate %a"pp_statuso.order_statusFmt.(option~none:(any"no")(Ptime.pp_rfc3339()))o.expiresFmt.(list~sep:(any", ")pp_id)o.identifiersFmt.(option~none:(any"no")(Ptime.pp_rfc3339()))o.not_beforeFmt.(option~none:(any"no")(Ptime.pp_rfc3339()))o.not_afterFmt.(option~none:(any"no error")J.pp)o.errorFmt.(list~sep:(any", ")Uri.pp_hum)o.authorizationsUri.pp_humo.finalizeFmt.(option~none:(any"no")Uri.pp_hum)o.certificateletstatus_of_string=function|"pending"->Ok`Pending|"ready"->Ok`Ready|"processing"->Ok`Processing|"valid"->Ok`Valid|"invalid"->Ok`Invalid|s->Error(`Msg(Fmt.str"unknown order status %s"s))letdecodestr=let*json=of_stringstrinlet*order_status=let*s=string_val"status"jsoninstatus_of_stringsinlet*expires=let*e=opt_string_val"expires"jsoninmaybedecode_ptimeeinlet*identifiers=let*i=list_val"identifiers"jsonindecode_idsiinlet*not_before=let*nb=opt_string_val"notBefore"jsoninmaybedecode_ptimenbinlet*not_after=let*na=opt_string_val"notAfter"jsoninmaybedecode_ptimenainleterror=matchJ.Util.member"error"jsonwith`Null->None|x->Somexinlet*authorizations=let*auths=opt_string_list"authorizations"jsoninlet*auths=Option.to_result~none:(`Msg"no authorizations found in order")authsinOk(List.mapUri.of_stringauths)inlet*finalize=let*f=string_val"finalize"jsoninurifinlet*certificate=let*c=opt_string_val"certificate"jsoninmaybeuricinOk{order_status;expires;identifiers;not_before;not_after;error;authorizations;finalize;certificate}endmoduleChallenge=structtypetyp=[`Dns|`Http|`Alpn]letpp_typppft=Fmt.stringppf(matchtwith`Dns->"DNS"|`Http->"HTTP"|`Alpn->"ALPN")lettyp_of_string=function|"tls-alpn-01"->Ok`Alpn|"http-01"->Ok`Http|"dns-01"->Ok`Dns|s->Error(`Msg(Fmt.str"unknown challenge typ %s"s))(* turns out, the only interesting ones are dns, http, alpn *)(* all share the same style *)typet={challenge_typ:typ;url:Uri.t;challenge_status:[`Pending|`Processing|`Valid|`Invalid];token:string;validated:Ptime.toption;error:jsonoption;}letpp_statusppfs=Fmt.stringppf(matchswith|`Pending->"pending"|`Processing->"processing"|`Valid->"valid"|`Invalid->"invalid")letppppfc=Fmt.pfppf"status %a@,typ %a@,token %s@,url %a@,validated %a@,error %a"pp_statusc.challenge_statuspp_typc.challenge_typc.tokenUri.pp_humc.urlFmt.(option~none:(any"no")(Ptime.pp_rfc3339()))c.validatedFmt.(option~none:(any"no error")J.pp)c.errorletstatus_of_string=function|"pending"->Ok`Pending|"processing"->Ok`Processing|"valid"->Ok`Valid|"invalid"->Ok`Invalid|s->Error(`Msg(Fmt.str"unknown order status %s"s))letdecodejson=let*challenge_typ=let*t=string_val"type"jsonintyp_of_stringtinlet*challenge_status=let*s=string_val"status"jsoninstatus_of_stringsinlet*url=let*u=string_val"url"jsoninuriuin(* in all three challenges, it's b64 url encoded (but the raw value never used) *)(* they MUST >= 128bit entropy, and not have any trailing = *)let*token=string_val"token"jsoninlet*validated=let*v=opt_string_val"validated"jsoninmaybedecode_ptimevinleterror=matchJ.Util.member"error"jsonwith`Null->None|x->SomexinOk{challenge_typ;challenge_status;url;token;validated;error}endmoduleAuthorization=structtypet={identifier:id_type*string;authorization_status:[`Pending|`Valid|`Invalid|`Deactivated|`Expired|`Revoked];expires:Ptime.toption;challenges:Challenge.tlist;wildcard:bool;}letpp_statusppfs=Fmt.stringppf(matchswith|`Pending->"pending"|`Valid->"valid"|`Invalid->"invalid"|`Deactivated->"deactivated"|`Expired->"expired"|`Revoked->"revoked")letppppfa=Fmt.pfppf"status %a@,identifier %a@,expires %a@,challenges %a@,wildcard %a"pp_statusa.authorization_statuspp_ida.identifierFmt.(option~none:(any"no")(Ptime.pp_rfc3339()))a.expiresFmt.(list~sep:(any",")Challenge.pp)a.challengesFmt.boola.wildcardletstatus_of_string=function|"pending"->Ok`Pending|"valid"->Ok`Valid|"invalid"->Ok`Invalid|"deactivated"->Ok`Deactivated|"expired"->Ok`Expired|"revoked"->Ok`Revoked|s->Error(`Msg(Fmt.str"unknown order status %s"s))letdecodestr=let*json=of_stringstrinlet*identifier=let*i=assoc_val"identifier"jsonindecode_idiinlet*authorization_status=let*s=string_val"status"jsoninstatus_of_stringsinlet*expires=let*e=opt_string_val"expires"jsoninmaybedecode_ptimeeinlet*challenges=list_val"challenges"jsoninletchallenges=(* be modest in what you receive - there may be other challenges in the future *)List.fold_left(funaccjson->matchChallenge.decodejsonwith|Error`Msgerr->Logs.warn(funm->m"ignoring challenge %a: parse error %s"J.ppjsonerr);acc|Okc->c::acc)[]challengesin(* TODO "MUST be present and true for orders containing a DNS identifier with wildcard. for others, it MUST be absent" *)let*wildcard=Result.map(Option.value~default:false)(opt_bool"wildcard"json)inOk{identifier;authorization_status;expires;challenges;wildcard}endmoduleError=struct(* from http://www.iana.org/assignments/acme urn registry *)typet={err_typ:[|`Account_does_not_exist|`Already_revoked|`Bad_csr|`Bad_nonce|`Bad_public_key|`Bad_revocation_reason|`Bad_signature_algorithm|`CAA|`Connection|`DNS|`External_account_required|`Incorrect_response|`Invalid_contact|`Malformed|`Order_not_ready|`Rate_limited|`Rejected_identifier|`Server_internal|`TLS|`Unauthorized|`Unsupported_contact|`Unsupported_identifier|`User_action_required];detail:string}leterr_typ_to_string=function|`Account_does_not_exist->"The request specified an account that does not exist"|`Already_revoked->"The request specified a certificate to be revoked that has already been revoked"|`Bad_csr->"The CSR is unacceptable (e.g., due to a short key)"|`Bad_nonce->"The client sent an unacceptable anti-replay nonce"|`Bad_public_key->"The JWS was signed by a public key the server does not support"|`Bad_revocation_reason->"The revocation reason provided is not allowed by the server"|`Bad_signature_algorithm->"The JWS was signed with an algorithm the server does not support"|`CAA->"Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate"(* | `Compound -> "Specific error conditions are indicated in the 'subproblems' array" *)|`Connection->"The server could not connect to validation target"|`DNS->"There was a problem with a DNS query during identifier validation"|`External_account_required->"The request must include a value for the 'externalAccountBinding' field"|`Incorrect_response->"Response received didn't match the challenge's requirements"|`Invalid_contact->"A contact URL for an account was invalid"|`Malformed->"The request message was malformed"|`Order_not_ready->"The request attempted to finalize an order that is not ready to be finalized"|`Rate_limited->"The request exceeds a rate limit"|`Rejected_identifier->"The server will not issue certificates for the identifier"|`Server_internal->"The server experienced an internal error"|`TLS->"The server received a TLS error during validation"|`Unauthorized->"The client lacks sufficient authorization"|`Unsupported_contact->"A contact URL for an account used an unsupported protocol scheme"|`Unsupported_identifier->"An identifier is of an unsupported type"|`User_action_required->"Visit the 'instance' URL and take actions specified there"letppppfe=Fmt.pfppf"%s, detail: %s"(err_typ_to_stringe.err_typ)e.detailleterr_typ_of_stringstr=letprefix="urn:ietf:params:acme:error:"inletplen=String.lengthprefixinleterr=ifString.lengthstr>plen&&String.(equalprefix(substr0plen))thenSome(String.substrplen(String.lengthstr-plen))elseNoneinmatcherrwith|Someerr->(* from https://www.iana.org/assignments/acme/acme.xhtml (20200209) *)beginmatcherrwith|"accountDoesNotExist"->Ok`Account_does_not_exist|"alreadyRevoked"->Ok`Already_revoked|"badCSR"->Ok`Bad_csr|"badNonce"->Ok`Bad_nonce|"badPublicKey"->Ok`Bad_public_key|"badRevocationReason"->Ok`Bad_revocation_reason|"badSignatureAlgorithm"->Ok`Bad_signature_algorithm|"caa"->Ok`CAA(* | "compound" -> Ok `Compound see 'subproblems' array *)|"connection"->Ok`Connection|"dns"->Ok`DNS|"externalAccountRequired"->Ok`External_account_required|"incorrectResponse"->Ok`Incorrect_response|"invalidContact"->Ok`Invalid_contact|"malformed"->Ok`Malformed|"orderNotReady"->Ok`Order_not_ready|"rateLimited"->Ok`Rate_limited|"rejectedIdentifier"->Ok`Rejected_identifier|"serverInternal"->Ok`Server_internal|"tls"->Ok`TLS|"unauthorized"->Ok`Unauthorized|"unsupportedContact"->Ok`Unsupported_contact|"unsupportedIdentifier"->Ok`Unsupported_identifier|"userActionRequired"->Ok`User_action_required|s->Error(`Msg(Fmt.str"unknown acme error typ %s"s))end|None->Error(`Msg(Fmt.str"unknown error type %s"str))letdecodestr=let*json=of_stringstrinlet*err_typ=let*t=string_val"type"jsoninerr_typ_of_stringtinlet*detail=string_val"detail"jsoninOk{err_typ;detail}end