urbit/app/acme.hoon

1404 lines
38 KiB
Plaintext

/- asn1, hall
/+ base64, der, primitive-rsa, *pkcs, *jose
=, eyre
=* rsa primitive-rsa
::
|%
:: +en-base64url: url-safe base64 encoding, without padding
::
++ en-base64url
~(en base64 | &)
:: +de-base64url: url-safe base64 decoding, without padding
::
++ de-base64url
~(de base64 | &)
:: +join-turf
::
++ join-turf
|= hot=(list turf)
^- cord
%+ rap 3
%- (bake join ,[cord wain])
[', ' (turn hot en-turf:html)]
:: |octn: encode/decode unsigned atoms as big-endian octet stream
::
++ octn
|%
++ en |=(a=@u `octs`[(met 3 a) (swp 3 a)])
++ de |=(a=octs `@u`(rev 3 p.a q.a))
--
:: |body: acme api response body types
::
++ body
|%
+$ acct [wen=@t sas=@t]
::
+$ order
$: exp=@t
sas=@t
aut=(list purl)
fin=(unit purl)
cer=(unit purl)
==
::
+$ auth
$: dom=turf
sas=@t
exp=@t
cal=challenge
==
::
+$ challenge [typ=@t sas=@t url=purl tok=@t err=(unit error)]
::
+$ error [type=@t detail=@t]
--
::
:: |grab: acme api response json reparsers
::
++ grab
=, dejs:format
|%
:: +json-purl: parse url
::
++ json-purl (su auri:de-purl:html)
:: +json-date: parse iso-8601
::
:: XX actually parse
::
++ json-date so
:: +directory: parse ACME service directory
::
++ directory
%- ot
:~ 'newAccount'^json-purl
'newNonce'^json-purl
'newOrder'^json-purl
'revokeCert'^json-purl
'keyChange'^json-purl
==
:: +acct: parse ACME service account
::
++ acct
^- $-(json acct:body)
:: ignoring key, contact, initialIp
::
(ot 'createdAt'^json-date 'status'^so ~)
:: +order: parse certificate order
::
++ order
^- $-(json order:body)
%- ou
:~ 'expires'^(un json-date)
'status'^(un so)
'authorizations'^(uf ~ (ar json-purl))
'finalize'^(uf ~ (mu json-purl))
'certificate'^(uf ~ (mu json-purl))
==
:: +auth: parse authorization
::
++ auth
=> |%
:: +iden: extract +turf from service identifier
::
++ iden
|= [typ=@t hot=host]
^- turf
?>(&(?=(%dns typ) ?=([%& *] hot)) p.hot)
:: +http-trial: extract %http-01 challenge
::
++ trial
|= a=(list challenge:body)
^- challenge:body
=/ b (skim a |=([typ=@t *] ?=(%http-01 typ)))
?>(?=(^ b) i.b)
--
^- $-(json auth:body)
%- ot
:~ 'identifier'^(cu iden (ot type+so value+(su thos:de-purl:html) ~))
'status'^so
'expires'^json-date
'challenges'^(cu trial (ar challenge))
==
:: +challenge: parse domain validation challenge
::
++ challenge
^- $-(json challenge:body)
%- ou
:~ 'type'^(un so)
'status'^(un so)
'url'^(un json-purl)
'token'^(un so)
'error'^(uf ~ (mu error))
==
:: +error: parse ACME service error response
::
++ error
^- $-(json error:body)
(ot type+so detail+so ~)
--
--
::
:::: acme state
::
|%
:: +move: output effect
::
+$ move [bone card]
:: +card: output effect payload
::
+$ card
$% [%connect wire =binding:http-server app=term]
[%http-response =http-event:http]
[%poke wire dock poke]
[%request wire request:http outbound-config:http-client]
[%rule wire %cert (unit [wain wain])]
[%wait wire @da]
==
:: +poke: outgoing app pokes
::
+$ poke
$% [%hall-action %phrase audience:hall (list speech:hall)]
==
:: +nonce-next: next effect to emit upon receiving nonce
::
+$ nonce-next
$? %register
%new-order
%finalize-order
%finalize-trial
==
:: +acct: an ACME service account
::
+$ acct
$: :: key: account keypair
::
key=key:rsa
:: reg: account registration
::
:: XX wen=@da once parser is fixed
::
reg=(unit [wen=@t kid=@t])
==
:: +config: finalized configuration
::
+$ config
$: :: dom: domains
::
dom=(set turf)
:: key: certificate keypair
::
key=key:rsa
:: cer: signed certificate
::
cer=wain
:: exp: expiration date
::
exp=@da
:: dor: source ACME service order URL
::
dor=purl
==
:: +trial: domain validation challenge
::
+$ trial
$% :: %http only for now
::
$: %http
:: ego: ACME service challenge url
::
ego=purl
:: tok: challenge token
::
tok=@t
:: sas: challenge status
::
sas=?(%recv %pend %auth)
== ==
:: +auth: domain authorization
::
+$ auth
$: :: ego: ACME service authorization url
::
ego=purl
:: dom: domain under authorization
::
dom=turf
:: cal: domain validation challenge
::
cal=trial
==
:: +order-auth: domain authorization state for order processing
::
+$ order-auth
$: :: pending: remote authorization urls
::
pending=(list purl)
:: active: authorization in progress
::
active=(unit [idx=@ auth])
:: done: finalized authorizations (XX or failed?)
::
done=(list auth)
==
:: +order: ACME certificate order
::
+$ order
$: :: dom: domains
::
dom=(set turf)
:: try: attempt number
::
try=@ud
:: sas: order state
::
sas=$@(%wake [%rest wen=@da])
:: exp: expiration date
::
:: XX @da once ISO-8601 parser
::
exp=@t
:: ego: ACME service order url
::
ego=purl
:: fin: ACME service order finalization url
::
fin=purl
:: key: certificate keypair
::
key=key:rsa
:: csr: DER-encoded PKCS10 certificate signing request
::
csr=@ux
:: aut: authorizations required by this order
::
aut=order-auth
==
:: +history: archive of past ACME service interactions
::
+$ history
$: :: act: list of revoked account keypairs
::
act=(list acct)
:: fig: list of expired configurations
::
fig=(list config)
:: fal: list of failed order attempts
::
fal=(list order)
==
:: +directory: ACME v2 service directory
::
+$ directory
$: :: register: registration url (newAccount)
::
register=purl
:: nonce: nonce creation url (newNonce)
::
nonce=purl
:: new-order: order creation url (newOrder)
::
new-order=purl
:: revoke: certificate revocation url (revokeCert)
::
revoke=purl
:: rekey: account key revocation url (keyChange)
::
rekey=purl
==
:: +acme: complete app state
::
+$ acme
$: :: dir: ACME service directory
::
dir=directory
:: act: ACME service account
::
act=acct
:: liv: active, live configuration
::
liv=(unit config)
:: hit: ACME account history
::
hit=history
:: nonces: list of unused nonces
::
nonces=(list @t)
:: rod: active, in-progress order
::
rod=(unit order)
:: next-order: queued domains for validation
::
next-order=(unit (map turf [idx=@ud valid=?]))
:: cey: certificate key XX move?
::
cey=key:rsa
==
--
::
:::: acme app
::
:: directory-base: LetsEncrypt service directory url
::
=/ directory-base=purl
=- (need (de-purl:html -))
'https://acme-v02.api.letsencrypt.org/directory'
:: mov: list of outgoing moves for the current transaction
::
=| mov=(list move)
::
|_ [bow=bowl:gall acme]
:: +this: self
::
:: XX Should be a +* core alias, see urbit/arvo#712
::
++ this .
:: +emit: emit a move
::
++ emit
|= car=card
this(mov [[ost.bow car] mov])
:: +abet: finalize transaction
::
++ abet
^- (quip move _this)
[(flop mov) this(mov ~)]
:: +backoff: calculate exponential backoff
::
++ backoff
|= try=@ud
^- @dr
?: =(0 try) ~s0
%+ add
(mul ~s1 (bex (dec try)))
(mul ~s0..0001 (~(rad og eny.bow) 1.000))
:: +acme-wire: create :acme http-request wire
::
++ acme-wire
|= [try=@ud act=@tas =wire]
^- ^wire
(weld /acme/try/(scot %ud try)/[act] wire)
:: +notify: send :hall notification
::
++ notify
|= [=cord =tang]
^- card
=/ msg=speech:hall
:+ %app dap.bow
=/ line [%lin & cord]
?~(tang line [%fat [%tank tang] line])
=/ act
[%phrase (sy [our.bow %inbox] ~) [msg ~]]
[%poke / [our.bow %hall] %hall-action act]
:: +request: unauthenticated http request
::
++ request
|= [wir=wire req=hiss]
^- card
[%request wir (hiss-to-request:html req) *outbound-config:http-client]
:: +signed-request: JWS JSON POST
::
++ signed-request
|= [url=purl non=@t bod=json]
^- hiss
:^ url %post
(my content-type+['application/jose+json' ~] ~)
:- ~
^- octs
=; pro=json
(as-octt:mimes:html (en-json:html (sign:jws key.act pro bod)))
:- %o %- my :~
nonce+s+non
url+s+(crip (en-purl:html url))
?^ reg.act
kid+s+kid.u.reg.act
jwk+(pass:en:jwk key.act)
==
:: +stateful-request: emit signed, nonce'd request
::
++ stateful-request
|= [[try=@ud act=@tas =wire] =purl =json]
^+ this
?~ nonces
(nonce:effect [act wire])
%- emit(nonces t.nonces)
%+ request (acme-wire try act wire)
(signed-request purl i.nonces json)
:: +bad-nonce: check if an http response is a badNonce error
::
++ bad-nonce
|= rep=httr
^- ?
:: XX always 400?
::
?. =(400 p.rep) |
?~ r.rep |
=/ jon=(unit json) (de-json:html q.u.r.rep)
?~ jon |
=('urn:ietf:params:acme:error:badNonce' type:(error:grab u.jon))
:: +rate-limited: handle Acme service rate-limits
::
++ rate-limited
|= [try=@ud act=@tas spur=wire bod=(unit octs)]
^+ this
=/ jon=(unit json)
?~(bod ~ (de-json:html q.u.bod))
?~ jon
:: no details, back way off
:: XX specifically based on wire
::
(retry:effect try act spur (min ~d1 (backoff (add 10 try))))
=/ err (error:grab u.jon)
?. =('params:acme:error:rateLimited' type.err)
:: incorrect 429 status? backoff normally
::
(retry:effect try act spur (min ~h1 (backoff try)))
=/ detail (trip detail.err)
:: too many certificates for these domains
::
?: ?=(^ (find "already issued for exact" detail))
=. ..this (retry:effect try act spur ~d7)
=/ msg=cord
%+ rap 3
:~ 'rate limit exceeded: '
' too many certificates issued for '
?~ rod
:: XX shouldn't happen
::
(en-turf:html /network/arvo/(crip +:(scow %p our.bow)))
(join-turf ~(tap in dom.u.rod))
'. retrying in ~d7.'
==
(emit (notify msg ~))
:: too many certificates for top-level-domain
::
?: ?=(^ (find "too many certificates already" detail))
=. ..this (retry:effect try act spur ~d7)
=/ lul=@dr
(add ~d7 (mul ~m1 (~(rad og eny.bow) (bex 10))))
=/ msg=cord
%+ rap 3
:~ 'rate limit exceeded: '
' too many certificates issued for '
:: XX get from detail
::
(en-turf:html /network/arvo)
'. retrying in '
(scot %dr lul) '.'
==
(emit (notify msg ~))
:: XX match more rate-limit conditions
:: or backoff by wire
::
:: - "too many registrations for this IP"
:: - "too many registrations for this IP range"
:: - "too many currently pending authorizations"
:: - "too many failed authorizations recently"
:: - "too many new orders recently"
::
(retry:effect try act spur (min ~d1 (backoff (add 10 try))))
:: +failure-message: generic http failure message
::
++ failure-message
|= =purl
^- cord
%+ rap 3
:~ 'unable to reach '
(crip (en-purl:html purl)) '. '
'please confirm your urbit has network connectivity.'
==
:: |effect: send moves to advance
::
++ effect
|_ try-count=(unit @ud)
:: +try: this effect attempt number
::
++ try (fall try-count 1)
:: +validate-domain: confirm that a pending domain resolves to us
::
++ validate-domain
|= idx=@ud
^+ this
~| %validate-domain-effect-fail
?. ?=(^ next-order) ~|(%no-next-order !!)
=/ pending
(skip ~(tap by u.next-order) |=([turf @ud valid=?] valid))
?: =(~ pending)
new-order:effect
=/ next=[=turf idx=@ud valid=?]
~| [%no-next-domain idx=idx]
(head (skim pending |=([turf idx=@ud ?] =(idx ^idx))))
:: XX should confirm that :turf points to us
:: confirms that domain exists (and an urbit is on :80)
::
=/ =purl
:- [sec=| por=~ host=[%& turf.next]]
[[ext=`~.udon path=/static] query=~]
=/ =wire
(acme-wire try %validate-domain /idx/(scot %ud idx.next))
(emit (request wire purl %get ~ ~))
:: +directory: get ACME service directory
::
++ directory
^+ this
:: XX now in wire?
::
(emit (request (acme-wire try %directory /) directory-base %get ~ ~))
:: +nonce: get a new nonce for the next request
::
++ nonce
|= nex=wire
~| [%bad-nonce-next nex]
?> ?& ?=(^ nex)
?=(nonce-next i.nex)
==
^+ this
:: XX now in wire?
::
=/ =wire
(acme-wire try %nonce [%next nex])
(emit (request wire nonce.dir %get ~ ~))
:: +register: create ACME service account
::
:: Note: accepts services ToS.
:: XX add rekey mechanism
::
++ register
^+ this
?. =(~ reg.act)
?: =(~ next-order)
this
(validate-domain:effect 0)
=/ =json [%o (my [['termsOfServiceAgreed' b+&] ~])]
:: XX date in wire?
::
=/ wire-params [try %register /]
(stateful-request wire-params register.dir json)
:: +renew: renew certificate
::
++ renew
^+ this
~| %renew-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ liv) ~|(%no-live-config !!)
=< new-order:effect
(queue-next-order & dom.u.liv)
:: +new-order: create a new certificate order
::
++ new-order
^+ this
~| %new-order-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=([~ ^] next-order) ~|(%no-domains !!)
=/ =json
:- %o %- my :~
:- %identifiers
:- %a
%+ turn
~(tap in ~(key by `(map turf *)`u.next-order))
|=(a=turf [%o (my type+s+'dns' value+s+(en-turf:html a) ~)])
==
=/ wire-params [try %new-order /(scot %da now.bow)]
(stateful-request wire-params new-order.dir json)
:: +cancel-order: cancel failed order, set retry timer
::
++ cancel-order
^+ this
~| %cancel-order-effect-fail
:: ?> ?=(^ rod)
:: XX get failure reason
::
=/ try=@ud ?~(rod 1 try.u.rod)
:: backoff faster than usual
::
=/ lul=@dr (min ~h1 (backoff (add 4 try)))
=/ msg=cord
(cat 3 'retrying certificate request in ' (scot %dr lul))
=. ..this (emit (notify msg ~))
=. ..this (retry:effect try %new-order / lul)
:: domains might already be validated
::
=. ..this (queue-next-order & ?>(?=(^ rod) dom.u.rod))
cancel-current-order
:: +finalize-order: finalize completed order
::
++ finalize-order
^+ this
~| %finalize-order-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ rod) ~|(%no-active-order !!)
?. ?=(~ pending.aut.u.rod) ~|(%pending-authz !!)
?. ?=(~ active.aut.u.rod) ~|(%active-authz !!)
:: XX revisit wrt rate limits
::
?> ?=(%wake sas.u.rod)
=/ =json
[%o (my csr+s+(en-base64url (met 3 csr.u.rod) `@`csr.u.rod) ~)]
=/ wire-params [try %finalize-order /(scot %da now.bow)]
(stateful-request wire-params fin.u.rod json)
:: +check-order: check completed order for certificate availability
::
++ check-order
^+ this
~| %check-order-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ rod) ~|(%no-active-order !!)
?. ?=(~ pending.aut.u.rod) ~|(%pending-authz !!)
?. ?=(~ active.aut.u.rod) ~|(%active-authz !!)
:: XX revisit wrt rate limits
::
?> ?=(%wake sas.u.rod)
=/ =wire
(acme-wire try %check-order /(scot %da now.bow))
(emit (request wire ego.u.rod %get ~ ~))
:: +certificate: download PEM-encoded certificate
::
++ certificate
|= url=purl
^+ this
~| %certificate-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ rod) ~|(%no-active-order !!)
=/ hed (my accept+['applicate/x-pem-file' ~] ~)
=/ =wire
(acme-wire try %certificate /(scot %da now.bow))
(emit (request wire url %get hed ~))
:: +install: tell %eyre about our certificate
::
++ install
^+ this
~| %install-effect-fail
?> ?=(^ liv)
=/ key=wain (ring:en:pem:pkcs8 key.u.liv)
(emit %rule /install %cert `[key `wain`cer.u.liv])
:: +get-authz: get next ACME service domain authorization object
::
++ get-authz
^+ this
~| %get-authz-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ rod) ~|(%no-active-order !!)
?. ?=(^ pending.aut.u.rod) ~|(%no-pending-authz !!)
:: XX revisit wrt rate limits
::
?> ?=(%wake sas.u.rod)
=/ =wire
(acme-wire try %get-authz /(scot %da now.bow))
(emit (request wire i.pending.aut.u.rod %get ~ ~))
:: XX check/finalize-authz ??
::
:: +test-trial: confirm that ACME domain validation challenge is available
::
++ test-trial
^+ this
~| %test-trial-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ rod) ~|(%no-active-order !!)
?. ?=(^ active.aut.u.rod) ~|(%no-active-authz !!)
:: XX revisit wrt rate limits
::
?> ?=(%wake sas.u.rod)
=* aut u.active.aut.u.rod
=/ pat=path /'.well-known'/acme-challenge/[tok.cal.aut]
:: note: requires port 80, just as the ACME service will
::
=/ url=purl [[sec=| por=~ hos=[%& dom.aut]] [ext=~ pat] hed=~]
:: =/ url=purl [[sec=| por=`8.081 hos=[%& /localhost]] [ext=~ pat] hed=~]
:: XX idx in wire?
::
=/ =wire
(acme-wire try %test-trial /(scot %da now.bow))
(emit (request wire url %get ~ ~))
:: +finalize-trial: notify ACME service that challenge is ready
::
++ finalize-trial
^+ this
~| %finalize-trial-effect-fail
?. ?=(^ reg.act) ~|(%no-account !!)
?. ?=(^ rod) ~|(%no-active-order !!)
?. ?=(^ active.aut.u.rod) ~|(%no-active-authz !!)
:: XX revisit wrt rate limits
::
?> ?=(%wake sas.u.rod)
=* aut u.active.aut.u.rod
:: empty object included for signature
:: XX include index in wire?
::
=/ wire-params [try %finalize-trial /(scot %da now.bow)]
(stateful-request wire-params ego.cal.aut [%o ~])
:: XX delete-trial?
::
:: +retry: retry effect after timeout
::
++ retry
|= [try=@ud act=@tas =wire lull=@dr]
:: XX validate wire
::
(emit %wait (acme-wire +(try) act wire) (add now.bow lull))
--
:: |event: accept event, emit next effect(s)
::
:: XX should these next effects be triggered at call sites instead?
::
++ event
|_ try=@ud
:: +validate-domain: accept a pending domain confirmation response
::
++ validate-domain
|= [=wire rep=httr]
^+ this
?> ?=([%idx @ *] wire)
?. ?=(^ next-order)
this
=/ idx (slav %ud i.t.wire)
=/ valid |(=(200 p.rep) =(307 p.rep))
=/ item=(list [=turf idx=@ud valid=?])
(skim ~(tap by u.next-order) |=([turf idx=@ud ?] =(^idx idx)))
?. ?& ?=([^ ~] item)
!valid.i.item
==
this
=. u.next-order
(~(put by u.next-order) turf.i.item [idx valid])
?. valid
?: (lth try 10)
=/ lul=@dr (min ~h1 (backoff try))
(retry:effect try %validate-domain /idx/(scot %ud idx) lul)
:: XX remove next-order, cancel pending requests
:: XX include suggestion to fix
::
=/ msg=cord
%+ rap 3
:~ 'unable to reach ' (scot %p our.bow)
' via http at ' (en-turf:html turf.i.item) ':80'
==
(emit(next-order ~) (notify msg [(sell !>(rep)) ~]))
?: ?=(~ (skip ~(val by u.next-order) |=([@ud valid=?] valid)))
new-order:effect
(validate-domain:effect +(idx))
:: +directory: accept ACME service directory, trigger registration
::
++ directory
|= [wir=wire rep=httr]
^+ this
?. =(200 p.rep)
?: (lth try 10)
(retry:effect try %directory / (min ~m30 (backoff try)))
(emit (notify (failure-message directory-base) [(sell !>(rep)) ~]))
=. dir (directory:grab (need (de-json:html q:(need r.rep))))
?~(reg.act register:effect this)
:: +nonce: accept new nonce and trigger next effect
::
:: Nonce has already been saved in +http-response. The next effect
:: is specified in the wire.
::
++ nonce
|= [=wire rep=httr]
^+ this
~| [%unrecognized-nonce-wire wire]
?> &(?=(^ wire) ?=([%next ^] wire))
=* nex i.t.wire
~| [%unknown-nonce-next nex]
?> ?=(nonce-next nex)
?. =(204 p.rep)
?: (lth try 10)
(retry:effect try %nonce t.wire (min ~m30 (backoff try)))
(emit (notify (failure-message nonce.dir) [(sell !>(rep)) ~]))
?- nex
%register register:effect
%new-order new-order:effect
%finalize-order finalize-order:effect
%finalize-trial finalize-trial:effect
==
:: +register: accept ACME service registration
::
++ register
|= [wir=wire rep=httr]
^+ this
?. |(=(200 p.rep) =(201 p.rep))
:: XX possible 204?
::
?: (lth try 10)
(retry:effect try %register / (min ~h1 (backoff try)))
(emit (notify (failure-message register.dir) [(sell !>(rep)) ~]))
=/ loc=@t
q:(head (skim q.rep |=((pair @t @t) ?=(%location p))))
:: XX @da once parser is fixed
::
=/ wen=@t
?~ r.rep
(scot %da now.bow)
=/ bod=acct:body
(acct:grab (need (de-json:html q.u.r.rep)))
?> ?=(%valid sas.bod)
wen.bod
=. reg.act `[wen loc]
?: =(~ next-order)
this
(validate-domain:effect 0)
:: XX rekey
::
:: +new-order: order created, begin processing authorizations
::
++ new-order
|= [wir=wire rep=httr]
^+ this
?. =(201 p.rep)
:: XX possible 204?
::
?: (lth try 10)
(retry:effect try %new-order / (min ~h1 (backoff try)))
:: XX next steps, retrying in ??
::
(emit (notify (failure-message register.dir) [(sell !>(rep)) ~]))
?> ?=(^ next-order)
=/ loc=@t
q:(head (skim q.rep |=((pair @t @t) ?=(%location p))))
=/ ego=purl (need (de-purl:html loc))
:: XX parse identifiers, confirm equal to pending domains
:: XX check status
::
=/ bod=order:body
(order:grab (need (de-json:html q:(need r.rep))))
=/ dom=(set turf) ~(key by u.next-order)
:: XX maybe generate key here?
::
=/ csr=@ux +:(en:der:pkcs10 cey ~(tap in dom))
=/ dor=order
[dom try=1 sas=%wake exp.bod ego (need fin.bod) cey csr [aut.bod ~ ~]]
get-authz:effect(rod `dor, next-order ~)
:: +finalize-order: order finalized, poll for certificate
::
++ finalize-order
|= [wir=wire rep=httr]
^+ this
?: =(504 p.rep)
:: retry timeouts frequently
::
(retry:effect try %finalize-order / (min ~m10 (backoff try)))
:: check-order regardless of status code
::
check-order:effect
:: +check-order: check order status, dispatch appropriately
::
++ check-order
|= [wir=wire rep=httr]
^+ this
~| [%strange-check-order wir]
?> ?=(^ rod)
?. =(200 p.rep)
?: (lth try 10)
(retry:effect try %check-order / (min ~m10 (backoff try)))
:: XX next steps, retrying in, delete order ??
::
(emit (notify (failure-message ego.u.rod) [(sell !>(rep)) ~]))
=/ bod=order:body
(order:grab (need (de-json:html q:(need r.rep))))
?+ sas.bod
~& [%check-order-status-unknown sas.bod]
this
:: order failed (at any stage)
::
%invalid
~& [%check-order-fail %invalid wir rep]
:: XX check authz, get the failure reason
:: XX possible to retry any reasons?
::
=< cancel-order:effect
(emit (notify 'certificate order failed' [(sell !>(rep)) ~]))
:: initial order state
::
%pending
check-order:effect
:: validations completed
::
%ready
finalize-order:effect
:: finalization requested
::
%processing
check-order:effect
:: certificate issued
::
%valid
:: XX update order state
:: XX =< delete-trial
::
~| impossible-order+[wir rep bod]
(certificate:effect (need cer.bod))
==
::
:: +certificate: accept PEM-encoded certificate
::
++ certificate
|= [wir=wire rep=httr]
^+ this
~| [%strange-certificate-response wir]
?> ?=(^ rod)
?. =(200 p.rep)
:: will re-attempt certificate download per order status
::
?: (lth try 10)
(retry:effect try %check-order / (min ~m10 (backoff try)))
:: XX next steps, retrying in, get url somehow ??
::
=/ msg=cord
%+ rap 3
:~ 'unable to download certificate. '
'please confirm that your urbit has network connectivity.'
==
(emit (notify msg [(sell !>(rep)) ~]))
=/ cer=wain (to-wain:format q:(need r.rep))
=/ fig=config
:: XX expiration date
::
[dom.u.rod key.u.rod cer (add now.bow ~d90) ego.u.rod]
:: archive live config
::
=? fig.hit ?=(^ liv) [u.liv fig.hit]
:: save new live config, clear active order
::
=> .(liv (some fig), rod ~)
?> ?=(^ liv)
:: notify :hall
::
=> =/ msg=cord
%+ rap 3
:~ 'received https certificate for '
(join-turf ~(tap in dom.u.liv))
==
(emit (notify msg ~))
:: set renewal timer, install certificate in %eyre
::
:: Certificates expire after ~d90. We want time for retries and
:: to work around rate limits, so our renewal timer is for ~d60.
:: Renewals count towards weekly rate limits, but are allowed to
:: continue past rate limits, so fudge the timer towards the end
:: of the week nearest ~d60.
::
=< install:effect
=; lul=@dr
(retry:effect 0 %renew / lul)
%+ add
(mul ~m1 (~(rad og eny.bow) (bex 8)))
=/ weekday (daws:chrono:userlib (yore now.bow))
?: (gth weekday 4)
(sub ~d60 (mul ~d1 (sub weekday 4)))
(add ~d60 (mul ~d1 (sub 4 weekday)))
:: +get-authz: accept ACME service authorization object
::
++ get-authz
|= [wir=wire rep=httr]
^+ this
~| [%strange-authorization wir]
?> ?=(^ rod)
?> ?=(^ pending.aut.u.rod)
?. =(200 p.rep)
?: (lth try 10)
(retry:effect try %get-authz / (min ~m10 (backoff try)))
:: XX next steps, retrying in ??
::
(emit (notify (failure-message i.pending.aut.u.rod) [(sell !>(rep)) ~]))
=/ bod=auth:body
(auth:grab (need (de-json:html q:(need r.rep))))
=/ cal=trial
:: XX parse token to verify url-safe base64?
::
[%http url.cal.bod tok.cal.bod %recv]
:: XX check that URLs are the same
::
=/ tau=auth [i.pending.aut.u.rod dom.bod cal]
:: XX get idx from wire instead?
::
=/ idx=@ud +((lent done.aut.u.rod))
=/ rod-aut=order-auth
%= aut.u.rod
pending t.pending.aut.u.rod
active `[idx tau]
==
test-trial:effect(aut.u.rod rod-aut)
:: XX check/finalize-authz ??
::
:: +test-trial: accept response from challenge test
::
++ test-trial
|= [wir=wire rep=httr]
~| [%strange-test-trial wir]
?> ?=(^ rod)
?> ?=(^ active.aut.u.rod)
=* aut u.active.aut.u.rod
^+ this
?. =(200 p.rep)
?: (lth try 10)
(retry:effect try %test-trial / (min ~m10 (backoff try)))
:: XX next steps, check connectivity, etc. ??
::
=< cancel-order:effect
=/ msg=cord
%+ rap 3
:~ 'unable to retrieve self-hosted domain validation token '
'via ' (en-turf:html dom.aut) '. '
'please confirm your urbit has network connectivity.'
==
(emit (notify msg [(sell !>(rep)) ~]))
=/ bod
%- as-octs:mimes:html
(rap 3 [tok.cal.aut '.' (pass:thumb:jwk key.act) ~])
?. ?& ?=(^ r.rep)
=(bod u.r.rep)
==
:: XX probably a DNS misconfiguration
::
=/ =tang
:~ ?~(r.rep leaf+"~" (sell !>(u.r.rep)))
leaf+"actual:"
(sell !>((some bod)))
leaf+"expected:"
==
(emit (notify 'domain validation value is wrong' tang))
finalize-trial:effect
:: +finalize-trial:
::
++ finalize-trial
|= [wir=wire rep=httr]
^+ this
~| [%strange-finalize-trial wir]
?> ?=(^ rod)
?> ?=(^ active.aut.u.rod)
=* aut u.active.aut.u.rod
?. =(200 p.rep)
:: XX possible 204? assume pending?
:: XX handle "challenge is not pending"
::
?: =(504 p.rep)
:: retry timeouts frequently
::
?: (lth try 10)
(retry:effect try %finalize-trial / (min ~m10 (backoff try)))
:: XX next steps, check connectivity, etc. ??
::
(emit (notify (failure-message ego.cal.aut) [(sell !>(rep)) ~]))
:: XX get challenge, confirm urn:ietf:params:acme:error:connection
::
:: =/ err=error:body
:: (error:grab (need (de-json:html q:(need r.rep))))
:: ?: =('urn:ietf:params:acme:error:malformed' status.err)
::
=< cancel-order:effect
=/ msg=cord
'unable to finalize domain validation challenge'
(emit (notify msg [(sell !>(rep)) ~]))
=/ bod=challenge:body
(challenge:grab (need (de-json:html q:(need r.rep))))
:: XX check for other possible values in 200 response
:: note: may have already been validated
::
?> ?=(?(%pending %valid) sas.bod)
=/ rod-aut=order-auth
aut.u.rod(active ~, done [+.aut(sas.cal %pend) done.aut.u.rod])
?~ pending.aut.u.rod
check-order:effect(aut.u.rod rod-aut)
get-authz:effect(aut.u.rod rod-aut)
:: XX delete-trial?
::
:: +retry: retry effect after timeout
::
++ retry
|= =wire
^+ this
?> ?=([%try @ @tas *] wire)
=/ try (slav %ud i.t.wire)
=* fec ~(. effect (some +(try)))
=* act i.t.t.wire
=* spur t.t.t.wire
?+ act
~&([%unknown-retry act] this)
%validate-domain
?> ?=([%idx @ ~] spur)
(validate-domain:fec (slav %ud i.t.spur))
%directory directory:fec
%nonce ?> ?=(^ spur)
(nonce:fec t.spur)
%register register:fec
%renew renew:fec
%new-order new-order:fec
%finalize-order finalize-order:fec
%check-order check-order:fec
%certificate check-order:fec :: intentional
%get-authz get-authz:fec
%test-trial test-trial:fec
%finalize-trial finalize-trial:fec
==
--
++ http-response
|= [=wire response=client-response:http-client]
^- (quip move _this)
:: ignore progress reports
::
?: ?=(%progress -.response)
[~ this]
::
?> ?=([%acme ^] wire)
=< abet
::
?: ?=(%cancel -.response)
(retry:event t.wire)
::
=/ rep=httr (to-httr:http-client +.response)
:: add nonce to pool, if present
::
=/ nonhed (skim q.rep |=((pair @t @t) ?=(%replay-nonce p)))
=? nonces ?=(^ nonhed) [q.i.nonhed nonces]
::
?> ?=([%try @ @tas *] t.wire)
=/ try (slav %ud i.t.t.wire)
=* ven ~(. event try)
=* act i.t.t.t.wire
=* spur t.t.t.t.wire
:: backoff if rate-limited
::
?: =(429 p.rep)
(rate-limited try act spur r.rep)
:: request nonce if expired-invalid
::
?: (bad-nonce rep)
(nonce:effect [act spur])
:: XX replace with :hall notification
::
~| [%http-response-fail wire]
%. [spur rep]
?+ act
~&([%unknown-http-response act] !!)
%validate-domain
validate-domain:ven
%directory directory:ven
%nonce nonce:ven
%register register:ven
:: XX rekey
::
%new-order new-order:ven
%finalize-order finalize-order:ven
%check-order check-order:ven
%certificate certificate:ven
%get-authz get-authz:ven
:: XX check/finalize-authz ??
::
%test-trial test-trial:ven
%finalize-trial finalize-trial:ven
:: XX delete-trial?
::
==
:: +poke-handle-http-request: receive incoming http request
::
:: Used to serve the domain validation challenge
::
++ poke-handle-http-request
|= =inbound-request:http-server
^- (quip move _this)
~& [%handle-http +<]
=/ url=(unit (pair pork:eyre quay:eyre))
%+ rush
url.request.inbound-request
;~(plug ;~(pose apat:de-purl:html (easy *pork:eyre)) yque:de-purl:html)
::
?. ?=(^ url)
~| [%invalid-url url.request.inbound-request] !!
?. ?=([%'.well-known' %acme-challenge @ ~] q.p.u.url)
~| [%unknown-url url.request.inbound-request] !!
::
:: XX these crashes should be restored
:: but %rver doesn't get an error notification from %gall
::
:: ?. ?=(^ reg.act) ~|(%no-account !!)
:: ?. ?=(^ rod) ~|(%no-active-order !!)
:: ?. ?=(^ active.aut.u.rod) ~|(%no-active-authz !!)
?. ?& ?=(^ reg.act)
?=(^ rod)
?=(^ active.aut.u.rod)
==
=/ =move [ost.bow %http-response %start [%500 ~] ~ %.y]
[[move ~] this]
::
=/ challenge i.t.t.q.p.u.url
=* aut u.active.aut.u.rod
?. =(tok.cal.aut challenge)
=/ =move [ost.bow %http-response %start [%404 ~] ~ %.y]
[[move ~] this]
=/ =move
=/ hed ['content-type' '/text/plain']~
=/ bod
%- some %- as-octs:mimes:html
(rap 3 [tok.cal.aut '.' (pass:thumb:jwk key.act) ~])
[ost.bow %http-response %start [%200 hed] bod %.y]
[[move ~] this]
:: +wake: timer wakeup event
::
++ wake
|= [wir=wire error=(unit tang)]
^- (quip move _this)
?^ error
%- (slog u.error)
abet
?> ?=([%acme *] wir)
abet:(retry:event t.wir)
:: +poke-acme-order: create new order for a set of domains
::
++ poke-acme-order
|= a=(set turf)
abet:(add-order a)
:: +poke-noun: for debugging
::
++ poke-noun
|= a=*
^- (quip move _this)
=< abet
?+ a
this
::
%dbug-account
~& registered=reg.act
~& [%public (pass:en:pem:pkcs1 key.act)]
~? !=(~ sek.key.act)
[%private (ring:en:pem:pkcs1 key.act)]
this
::
%dbug-certificate
?~ liv ~&(~ this)
~& [%key (ring:en:pem:pkcs8 key.u.liv)]
~& [%cert `wain`cer.u.liv]
~& [%expires exp.u.liv]
~& :- %domains
(join-turf ~(tap in dom.u.liv))
this
::
%dbug-history
~& [%account-history act.hit]
~& [%config-history fig.hit]
~& [%failed-order-history fal.hit]
this
::
:: install privkey and cert .pem from /=home=/acme, ignores app state
::TODO refactor this out of %acme, see also arvo#1151
::
%install-from-clay
=/ bas=path /(scot %p our.bow)/home/(scot %da now.bow)/acme
=/ key=wain .^(wain %cx (weld bas /privkey/pem))
=/ cer=wain .^(wain %cx (weld bas /cert/pem))
(emit %rule /install %cert `[key cer])
::
%init
init
::
%register
register:effect
::
%poll
check-order:effect
::
%retry
(add-order (sy /network/arvo/(crip +:(scow %p our.bow)) ~))
==
:: +poke-path: for debugging
::
++ poke-path
|=(a=path abet:(add-order (sy a ~)))
:: +prep: initialize and adapt state
::
++ prep
|= old=(unit acme)
^- (quip move _this)
?~ old
=/ =move
[ost.bow %connect /acme [~ /'.well-known'/acme-challenge] %acme]
[[move ~] this]
[~ this(+<+ u.old)]
:: +bound: response to %connect binding request
::
++ bound
|= [=wire accepted=? =binding:http-server]
?: accepted
[~ this]
:: XX better error message
::
~& [%acme-http-path-binding-failed +<]
[~ this]
:: +rekey: create new 2.048 bit RSA key
::
:: XX do something about this iteration
::
++ rekey
|= eny=@
=| i=@
|- ^- key:rsa
=/ k (new-key:rsa 2.048 eny)
=/ m (met 0 n.pub.k)
:: ?: =(0 (mod m 8)) k
?: =(2.048 m) k
~& [%key iter=i width=m]
$(i +(i), eny +(eny))
:: +init: initialize :acme state
::
:: We defer the initial request for independence from the causal event,
:: which is necessary to init on the boot event. Which we no longer do,
:: but we're preserving the pattern for future flexibility.
::
++ init
=< (retry:effect 0 %directory / `@dr`1)
%= this
act [(rekey eny.bow) ~]
cey (rekey (mix eny.bow (shaz now.bow)))
==
:: +queue-next-order: enqueue domains for validation
::
++ queue-next-order
|= [valid=? dom=(set turf)]
^+ this
%= this next-order
:- ~
%+ roll
~(tap in dom)
|= [=turf state=(map turf [idx=@ud valid=?])]
(~(put by state) turf [~(wyt by state) valid])
==
:: +cancel-current-order: and archive failure for future autopsy
::
:: XX we may have pending moves out for this order
:: put dates in wires, check against order creation date?
:: or re-use order-id?
::
++ cancel-current-order
^+ this
?~ rod this
%= this
rod ~
fal.hit [u.rod fal.hit]
==
:: +add-order: add new certificate order
::
++ add-order
|= dom=(set turf)
^+ this
?: =(~ dom)
~|(%acme-empty-certificate-order !!)
?: ?=(?(%earl %pawn) (clan:title our.bow))
this
=. ..this (queue-next-order | dom)
=. ..this cancel-current-order
:: notify :hall
::
=. ..this
=/ msg=cord
%+ rap 3
:~ 'requesting an https certificate for '
(join-turf ~(tap in dom))
==
(emit (notify msg ~))
:: if registered, create order
::
?^ reg.act
(validate-domain:effect 0)
:: if initialized, defer
::
?.(=(act *acct) this init)
--