Skip to content

Commit 9dd0a3f

Browse files
committed
chore(acme): use Uri when possible
1 parent 6acc40c commit 9dd0a3f

File tree

6 files changed

+218
-118
lines changed

6 files changed

+218
-118
lines changed

libp2p/autotls/acme/api.nim

Lines changed: 76 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import options, base64, sequtils, strutils, json
1+
import options, base64, sequtils, strutils, json, uri
22
from times import DateTime, parse
33
import chronos/apps/http/httpclient, jwt, results, bearssl/pem
44

@@ -32,7 +32,7 @@ type ACMEDirectory* = object
3232
type ACMEApi* = ref object of RootObj
3333
directory: ACMEDirectory
3434
session: HttpSessionRef
35-
acmeServerURL*: string
35+
acmeServerURL*: Uri
3636

3737
type HTTPResponse* = object
3838
body*: JsonNode
@@ -51,22 +51,24 @@ type ACMERequestType = enum
5151
type ACMERequestHeader = object
5252
alg: string
5353
typ: string
54-
nonce: string
54+
nonce: Nonce
5555
url: string
5656
case kind: ACMERequestType
5757
of ACMEJwkRequest:
5858
jwk: JWK
5959
of ACMEKidRequest:
6060
kid: Kid
6161

62+
type Email = string
63+
6264
type ACMERegisterRequest* = object
6365
termsOfServiceAgreed: bool
64-
contact: seq[string]
66+
contact: seq[Email]
6567

6668
type ACMEAccountStatus = enum
67-
valid
68-
deactivated
69-
revoked
69+
valid = "valid"
70+
deactivated = "deactivated"
71+
revoked = "revoked"
7072

7173
type ACMERegisterResponseBody = object
7274
status*: ACMEAccountStatus
@@ -81,11 +83,18 @@ type ACMEChallengeStatus* {.pure.} = enum
8183
valid = "valid"
8284
invalid = "invalid"
8385

86+
type ACMEChallengeType* {.pure.} = enum
87+
dns01 = "dns-01"
88+
http01 = "http-01"
89+
tlsalpn01 = "tls-alpn-01"
90+
91+
type ACMEChallengeToken* = string
92+
8493
type ACMEChallenge = object
8594
url*: string
86-
`type`*: string
95+
`type`*: ACMEChallengeType
8796
status*: ACMEChallengeStatus
88-
token*: string
97+
token*: ACMEChallengeToken
8998

9099
type ACMEChallengeIdentifier = object
91100
`type`: string
@@ -96,18 +105,18 @@ type ACMEChallengeRequest = object
96105

97106
type ACMEChallengeResponseBody = object
98107
status: ACMEChallengeStatus
99-
authorizations: seq[string]
108+
authorizations: seq[Authorization]
100109
finalize: string
101110

102111
type ACMEChallengeResponse* = object
103112
status*: ACMEChallengeStatus
104-
authorizations*: seq[string]
113+
authorizations*: seq[Authorization]
105114
finalize*: string
106-
orderURL*: string
115+
order*: string
107116

108117
type ACMEChallengeResponseWrapper* = object
109-
finalizeURL*: string
110-
orderURL*: string
118+
finalize*: string
119+
order*: string
111120
dns01*: ACMEChallenge
112121

113122
type ACMEAuthorizationsResponse* = object
@@ -163,24 +172,26 @@ template handleError*(msg: string, body: untyped): untyped =
163172
raise newException(ACMEError, msg & ": Unexpected error", exc)
164173

165174
method post*(
166-
self: ACMEApi, url: string, payload: string
175+
self: ACMEApi, uri: Uri, payload: string
167176
): Future[HTTPResponse] {.
168177
async: (raises: [ACMEError, HttpError, CancelledError]), base
169178
.}
170179

171180
method get*(
172-
self: ACMEApi, url: string
181+
self: ACMEApi, uri: Uri
173182
): Future[HTTPResponse] {.
174183
async: (raises: [ACMEError, HttpError, CancelledError]), base
175184
.}
176185

177186
proc new*(
178-
T: typedesc[ACMEApi], acmeServerURL: string = LetsEncryptURL
187+
T: typedesc[ACMEApi], acmeServerURL: Uri = parseUri(LetsEncryptURL)
179188
): Future[ACMEApi] {.async: (raises: [ACMEError, CancelledError]).} =
180189
let session = HttpSessionRef.new()
181190
let directory = handleError("new API"):
182-
let rawResponse =
183-
await HttpClientRequestRef.get(session, acmeServerURL & "/directory").get().send()
191+
let rawResponse = await HttpClientRequestRef
192+
.get(session, $(acmeServerURL / "directory"))
193+
.get()
194+
.send()
184195
let body = await rawResponse.getResponseBody()
185196
body.to(ACMEDirectory)
186197

@@ -190,12 +201,12 @@ method requestNonce*(
190201
self: ACMEApi
191202
): Future[Nonce] {.async: (raises: [ACMEError, CancelledError]), base.} =
192203
handleError("requestNonce"):
193-
let acmeResponse = await self.get(self.directory.newNonce)
204+
let acmeResponse = await self.get(parseUri(self.directory.newNonce))
194205
Nonce(acmeResponse.headers.keyOrError("Replay-Nonce"))
195206

196207
# TODO: save n and e in account so we don't have to recalculate every time
197208
proc acmeHeader(
198-
self: ACMEApi, url: string, key: KeyPair, needsJwk: bool, kid: Opt[Kid]
209+
self: ACMEApi, uri: Uri, key: KeyPair, needsJwk: bool, kid: Opt[Kid]
199210
): Future[ACMERequestHeader] {.async: (raises: [ACMEError, CancelledError]).} =
200211
if not needsJwk and kid.isNone:
201212
raise newException(ACMEError, "kid not set")
@@ -213,7 +224,7 @@ proc acmeHeader(
213224
alg: Alg,
214225
typ: "JWT",
215226
nonce: newNonce,
216-
url: url,
227+
url: $uri,
217228
jwk: JWK(kty: "RSA", n: base64UrlEncode(nArray), e: base64UrlEncode(eArray)),
218229
)
219230
else:
@@ -222,34 +233,34 @@ proc acmeHeader(
222233
alg: Alg,
223234
typ: "JWT",
224235
nonce: newNonce,
225-
url: url,
236+
url: $uri,
226237
kid: kid.get(),
227238
)
228239

229240
method post*(
230-
self: ACMEApi, url: string, payload: string
241+
self: ACMEApi, uri: Uri, payload: string
231242
): Future[HTTPResponse] {.
232243
async: (raises: [ACMEError, HttpError, CancelledError]), base
233244
.} =
234245
let rawResponse = await HttpClientRequestRef
235-
.post(self.session, url, body = payload, headers = ACMEHttpHeaders)
246+
.post(self.session, $uri, body = payload, headers = ACMEHttpHeaders)
236247
.get()
237248
.send()
238249
let body = await rawResponse.getResponseBody()
239250
HTTPResponse(body: body, headers: rawResponse.headers)
240251

241252
method get*(
242-
self: ACMEApi, url: string
253+
self: ACMEApi, uri: Uri
243254
): Future[HTTPResponse] {.
244255
async: (raises: [ACMEError, HttpError, CancelledError]), base
245256
.} =
246-
let rawResponse = await HttpClientRequestRef.get(self.session, url).get().send()
257+
let rawResponse = await HttpClientRequestRef.get(self.session, $uri).get().send()
247258
let body = await rawResponse.getResponseBody()
248259
HTTPResponse(body: body, headers: rawResponse.headers)
249260

250261
proc createSignedAcmeRequest(
251262
self: ACMEApi,
252-
url: string,
263+
uri: Uri,
253264
payload: auto,
254265
key: KeyPair,
255266
needsJwk: bool = false,
@@ -258,7 +269,7 @@ proc createSignedAcmeRequest(
258269
if key.pubkey.scheme != PKScheme.RSA or key.seckey.scheme != PKScheme.RSA:
259270
raise newException(ACMEError, "Unsupported signing key type")
260271

261-
let acmeHeader = await self.acmeHeader(url, key, needsJwk, kid)
272+
let acmeHeader = await self.acmeHeader(uri, key, needsJwk, kid)
262273
handleError("createSignedAcmeRequest"):
263274
var token = toJWT(%*{"header": acmeHeader, "claims": payload})
264275
let derPrivKey = key.seckey.rsakey.getBytes.get
@@ -272,62 +283,69 @@ proc requestRegister*(
272283
let registerRequest = ACMERegisterRequest(termsOfServiceAgreed: true)
273284
handleError("acmeRegister"):
274285
let payload = await self.createSignedAcmeRequest(
275-
self.directory.newAccount, registerRequest, key, needsJwk = true
286+
parseUri(self.directory.newAccount), registerRequest, key, needsJwk = true
276287
)
277-
let acmeResponse = await self.post(self.directory.newAccount, payload)
288+
let acmeResponse = await self.post(parseUri(self.directory.newAccount), payload)
278289
let acmeResponseBody = acmeResponse.body.to(ACMERegisterResponseBody)
279290

280291
ACMERegisterResponse(
281292
status: acmeResponseBody.status, kid: acmeResponse.headers.keyOrError("location")
282293
)
283294

284295
proc requestNewOrder*(
285-
self: ACMEApi, domains: seq[string], key: KeyPair, kid: Kid
296+
self: ACMEApi, domains: seq[Domain], key: KeyPair, kid: Kid
286297
): Future[ACMEChallengeResponse] {.async: (raises: [ACMEError, CancelledError]).} =
287298
# request challenge from ACME server
288299
let orderRequest = ACMEChallengeRequest(
289300
identifiers: domains.mapIt(ACMEChallengeIdentifier(`type`: "dns", value: it))
290301
)
291302
handleError("requestNewOrder"):
292303
let payload = await self.createSignedAcmeRequest(
293-
self.directory.newOrder, orderRequest, key, kid = Opt.some(kid)
304+
parseUri(self.directory.newOrder), orderRequest, key, kid = Opt.some(kid)
294305
)
295-
let acmeResponse = await self.post(self.directory.newOrder, payload)
296-
306+
let acmeResponse = await self.post(parseUri(self.directory.newOrder), payload)
297307
let challengeResponseBody = acmeResponse.body.to(ACMEChallengeResponseBody)
298308
if challengeResponseBody.authorizations.len() == 0:
299309
raise newException(ACMEError, "Authorizations field is empty")
300310
ACMEChallengeResponse(
301311
status: challengeResponseBody.status,
302312
authorizations: challengeResponseBody.authorizations,
303313
finalize: challengeResponseBody.finalize,
304-
orderURL: acmeResponse.headers.keyOrError("location"),
314+
order: acmeResponse.headers.keyOrError("location"),
305315
)
306316

307317
proc requestAuthorizations*(
308318
self: ACMEApi, authorizations: seq[Authorization], key: KeyPair, kid: Kid
309319
): Future[ACMEAuthorizationsResponse] {.async: (raises: [ACMEError, CancelledError]).} =
310320
handleError("requestAuthorizations"):
311321
doAssert authorizations.len > 0
312-
let acmeResponse = await self.get(authorizations[0])
322+
let acmeResponse = await self.get(parseUri(authorizations[0]))
313323
acmeResponse.body.to(ACMEAuthorizationsResponse)
314324

315325
proc requestChallenge*(
316326
self: ACMEApi, domains: seq[Domain], key: KeyPair, kid: Kid
317327
): Future[ACMEChallengeResponseWrapper] {.async: (raises: [ACMEError, CancelledError]).} =
318328
let challengeResponse = await self.requestNewOrder(domains, key, kid)
329+
if challengeResponse.status != ACMEChallengeStatus.pending:
330+
raise newException(
331+
ACMEError, "Invalid new challenge status: " & $challengeResponse.status
332+
)
319333

320334
let authorizationsResponse =
321335
await self.requestAuthorizations(challengeResponse.authorizations, key, kid)
336+
if authorizationsResponse.challenges.len == 0:
337+
raise newException(ACMEError, "No challenges received")
322338

323339
return ACMEChallengeResponseWrapper(
324-
finalizeURL: challengeResponse.finalize,
325-
orderURL: challengeResponse.orderURL,
326-
dns01: authorizationsResponse.challenges.filterIt(it.`type` == "dns-01")[0],
340+
finalize: challengeResponse.finalize,
341+
order: challengeResponse.order,
342+
dns01: authorizationsResponse.challenges.filterIt(
343+
it.`type` == ACMEChallengeType.dns01
344+
)[0],
327345
)
328346

329347
proc requestCheck*(
330-
self: ACMEApi, checkURL: string, checkKind: ACMECheckKind, key: KeyPair, kid: Kid
348+
self: ACMEApi, checkURL: Uri, checkKind: ACMECheckKind, key: KeyPair, kid: Kid
331349
): Future[ACMECheckResponse] {.async: (raises: [ACMEError, CancelledError]).} =
332350
handleError("requestCheck"):
333351
let acmeResponse = await self.get(checkURL)
@@ -362,7 +380,7 @@ proc requestCheck*(
362380
)
363381

364382
proc requestCompleted*(
365-
self: ACMEApi, chalURL: string, key: KeyPair, kid: Kid
383+
self: ACMEApi, chalURL: Uri, key: KeyPair, kid: Kid
366384
): Future[ACMECompletedResponse] {.async: (raises: [ACMEError, CancelledError]).} =
367385
handleError("requestCompleted (send notify)"):
368386
let payload =
@@ -372,7 +390,7 @@ proc requestCompleted*(
372390

373391
proc checkChallengeCompleted*(
374392
self: ACMEApi,
375-
checkURL: string,
393+
checkURL: Uri,
376394
key: KeyPair,
377395
kid: Kid,
378396
retries: int = DefaultChalCompletedRetries,
@@ -394,7 +412,7 @@ proc checkChallengeCompleted*(
394412

395413
proc completeChallenge*(
396414
self: ACMEApi,
397-
chalURL: string,
415+
chalURL: Uri,
398416
key: KeyPair,
399417
kid: Kid,
400418
retries: int = DefaultChalCompletedRetries,
@@ -404,28 +422,28 @@ proc completeChallenge*(
404422
return await self.checkChallengeCompleted(chalURL, key, kid, retries = retries)
405423

406424
proc requestFinalize*(
407-
self: ACMEApi, domain: string, finalizeURL: string, key: KeyPair, kid: Kid
425+
self: ACMEApi, domain: Domain, finalize: Uri, key: KeyPair, kid: Kid
408426
): Future[ACMEFinalizeResponse] {.async: (raises: [ACMEError, CancelledError]).} =
409427
let derCSR = createCSR(domain)
410428
let b64CSR = base64.encode(derCSR.toSeq, safe = true)
411429

412430
handleError("requestFinalize"):
413431
let payload = await self.createSignedAcmeRequest(
414-
finalizeURL, %*{"csr": b64CSR}, key, kid = Opt.some(kid)
432+
finalize, %*{"csr": b64CSR}, key, kid = Opt.some(kid)
415433
)
416-
let acmeResponse = await self.post(finalizeURL, payload)
434+
let acmeResponse = await self.post(finalize, payload)
417435
# server responds with updated order response
418436
acmeResponse.body.to(ACMEFinalizeResponse)
419437

420438
proc checkCertFinalized*(
421439
self: ACMEApi,
422-
orderURL: string,
440+
order: Uri,
423441
key: KeyPair,
424442
kid: Kid,
425443
retries: int = DefaultChalCompletedRetries,
426444
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
427445
for i in 0 .. retries:
428-
let checkResponse = await self.requestCheck(orderURL, ACMEOrderCheck, key, kid)
446+
let checkResponse = await self.requestCheck(order, ACMEOrderCheck, key, kid)
429447
case checkResponse.orderStatus
430448
of ACMEOrderStatus.valid:
431449
return true
@@ -443,28 +461,28 @@ proc checkCertFinalized*(
443461

444462
proc certificateFinalized*(
445463
self: ACMEApi,
446-
domain: string,
447-
finalizeURL: string,
448-
orderURL: string,
464+
domain: Domain,
465+
finalize: Uri,
466+
order: Uri,
449467
key: KeyPair,
450468
kid: Kid,
451469
retries: int = DefaultFinalizeRetries,
452470
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
453-
let finalizeResponse = await self.requestFinalize(domain, finalizeURL, key, kid)
471+
let finalizeResponse = await self.requestFinalize(domain, finalize, key, kid)
454472
# keep checking order until cert is valid (done)
455-
return await self.checkCertFinalized(orderURL, key, kid, retries = retries)
473+
return await self.checkCertFinalized(order, key, kid, retries = retries)
456474

457475
proc requestGetOrder*(
458-
self: ACMEApi, orderURL: string
476+
self: ACMEApi, order: Uri
459477
): Future[ACMEOrderResponse] {.async: (raises: [ACMEError, CancelledError]).} =
460478
handleError("requestGetOrder"):
461-
let acmeResponse = await self.get(orderURL)
479+
let acmeResponse = await self.get(order)
462480
acmeResponse.body.to(ACMEOrderResponse)
463481

464482
proc downloadCertificate*(
465-
self: ACMEApi, orderURL: string
483+
self: ACMEApi, order: Uri
466484
): Future[ACMECertificateResponse] {.async: (raises: [ACMEError, CancelledError]).} =
467-
let orderResponse = await self.requestGetOrder(orderURL)
485+
let orderResponse = await self.requestGetOrder(order)
468486

469487
handleError("downloadCertificate"):
470488
let rawResponse = await HttpClientRequestRef

0 commit comments

Comments
 (0)