1
- import options, base64, sequtils, strutils, json
1
+ import options, base64, sequtils, strutils, json, uri
2
2
from times import DateTime, parse
3
3
import chronos/ apps/ http/ httpclient, jwt, results, bearssl/ pem
4
4
@@ -32,7 +32,7 @@ type ACMEDirectory* = object
32
32
type ACMEApi* = ref object of RootObj
33
33
directory: ACMEDirectory
34
34
session: HttpSessionRef
35
- acmeServerURL* : string
35
+ acmeServerURL* : Uri
36
36
37
37
type HTTPResponse* = object
38
38
body* : JsonNode
@@ -51,22 +51,24 @@ type ACMERequestType = enum
51
51
type ACMERequestHeader = object
52
52
alg: string
53
53
typ: string
54
- nonce: string
54
+ nonce: Nonce
55
55
url: string
56
56
case kind: ACMERequestType
57
57
of ACMEJwkRequest:
58
58
jwk: JWK
59
59
of ACMEKidRequest:
60
60
kid: Kid
61
61
62
+ type Email = string
63
+
62
64
type ACMERegisterRequest* = object
63
65
termsOfServiceAgreed: bool
64
- contact: seq [string ]
66
+ contact: seq [Email ]
65
67
66
68
type ACMEAccountStatus = enum
67
- valid
68
- deactivated
69
- revoked
69
+ valid = " valid "
70
+ deactivated = " deactivated "
71
+ revoked = " revoked "
70
72
71
73
type ACMERegisterResponseBody = object
72
74
status* : ACMEAccountStatus
@@ -81,11 +83,18 @@ type ACMEChallengeStatus* {.pure.} = enum
81
83
valid = " valid"
82
84
invalid = " invalid"
83
85
86
+ type ACMEChallengeType* {.pure.} = enum
87
+ dns01 = " dns-01"
88
+ http01 = " http-01"
89
+ tlsalpn01 = " tls-alpn-01"
90
+
91
+ type ACMEChallengeToken* = string
92
+
84
93
type ACMEChallenge = object
85
94
url* : string
86
- `type`* : string
95
+ `type`* : ACMEChallengeType
87
96
status* : ACMEChallengeStatus
88
- token* : string
97
+ token* : ACMEChallengeToken
89
98
90
99
type ACMEChallengeIdentifier = object
91
100
`type`: string
@@ -96,18 +105,18 @@ type ACMEChallengeRequest = object
96
105
97
106
type ACMEChallengeResponseBody = object
98
107
status: ACMEChallengeStatus
99
- authorizations: seq [string ]
108
+ authorizations: seq [Authorization ]
100
109
finalize: string
101
110
102
111
type ACMEChallengeResponse* = object
103
112
status* : ACMEChallengeStatus
104
- authorizations* : seq [string ]
113
+ authorizations* : seq [Authorization ]
105
114
finalize* : string
106
- orderURL * : string
115
+ order * : string
107
116
108
117
type ACMEChallengeResponseWrapper* = object
109
- finalizeURL * : string
110
- orderURL * : string
118
+ finalize * : string
119
+ order * : string
111
120
dns01* : ACMEChallenge
112
121
113
122
type ACMEAuthorizationsResponse* = object
@@ -163,24 +172,26 @@ template handleError*(msg: string, body: untyped): untyped =
163
172
raise newException(ACMEError, msg & " : Unexpected error" , exc)
164
173
165
174
method post* (
166
- self: ACMEApi, url: string , payload: string
175
+ self: ACMEApi, uri: Uri , payload: string
167
176
): Future[HTTPResponse] {.
168
177
async: (raises: [ACMEError, HttpError, CancelledError]) , base
169
178
.}
170
179
171
180
method get* (
172
- self: ACMEApi, url: string
181
+ self: ACMEApi, uri: Uri
173
182
): Future[HTTPResponse] {.
174
183
async: (raises: [ACMEError, HttpError, CancelledError]) , base
175
184
.}
176
185
177
186
proc new* (
178
- T: typedesc [ACMEApi], acmeServerURL: string = LetsEncryptURL
187
+ T: typedesc [ACMEApi], acmeServerURL: Uri = parseUri( LetsEncryptURL)
179
188
) : Future[ACMEApi] {.async: (raises: [ACMEError, CancelledError]) .} =
180
189
let session = HttpSessionRef.new()
181
190
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()
184
195
let body = await rawResponse.getResponseBody()
185
196
body.to(ACMEDirectory)
186
197
@@ -190,12 +201,12 @@ method requestNonce*(
190
201
self: ACMEApi
191
202
): Future[Nonce] {.async: (raises: [ACMEError, CancelledError]) , base.} =
192
203
handleError(" requestNonce" ):
193
- let acmeResponse = await self.get(self.directory.newNonce)
204
+ let acmeResponse = await self.get(parseUri( self.directory.newNonce) )
194
205
Nonce(acmeResponse.headers.keyOrError(" Replay-Nonce" ))
195
206
196
207
# TODO : save n and e in account so we don't have to recalculate every time
197
208
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]
199
210
) : Future[ACMERequestHeader] {.async: (raises: [ACMEError, CancelledError]) .} =
200
211
if not needsJwk and kid.isNone:
201
212
raise newException(ACMEError, " kid not set" )
@@ -213,7 +224,7 @@ proc acmeHeader(
213
224
alg: Alg,
214
225
typ: " JWT" ,
215
226
nonce: newNonce,
216
- url: url ,
227
+ url: $ uri ,
217
228
jwk: JWK(kty: " RSA" , n: base64UrlEncode(nArray), e: base64UrlEncode(eArray)),
218
229
)
219
230
else :
@@ -222,34 +233,34 @@ proc acmeHeader(
222
233
alg: Alg,
223
234
typ: " JWT" ,
224
235
nonce: newNonce,
225
- url: url ,
236
+ url: $ uri ,
226
237
kid: kid.get(),
227
238
)
228
239
229
240
method post* (
230
- self: ACMEApi, url: string , payload: string
241
+ self: ACMEApi, uri: Uri , payload: string
231
242
): Future[HTTPResponse] {.
232
243
async: (raises: [ACMEError, HttpError, CancelledError]) , base
233
244
.} =
234
245
let rawResponse = await HttpClientRequestRef
235
- .post(self.session, url , body = payload, headers = ACMEHttpHeaders)
246
+ .post(self.session, $ uri , body = payload, headers = ACMEHttpHeaders)
236
247
.get()
237
248
.send()
238
249
let body = await rawResponse.getResponseBody()
239
250
HTTPResponse(body: body, headers: rawResponse.headers)
240
251
241
252
method get* (
242
- self: ACMEApi, url: string
253
+ self: ACMEApi, uri: Uri
243
254
): Future[HTTPResponse] {.
244
255
async: (raises: [ACMEError, HttpError, CancelledError]) , base
245
256
.} =
246
- let rawResponse = await HttpClientRequestRef.get(self.session, url ).get().send()
257
+ let rawResponse = await HttpClientRequestRef.get(self.session, $ uri ).get().send()
247
258
let body = await rawResponse.getResponseBody()
248
259
HTTPResponse(body: body, headers: rawResponse.headers)
249
260
250
261
proc createSignedAcmeRequest(
251
262
self: ACMEApi,
252
- url: string ,
263
+ uri: Uri ,
253
264
payload: auto ,
254
265
key: KeyPair,
255
266
needsJwk: bool = false ,
@@ -258,7 +269,7 @@ proc createSignedAcmeRequest(
258
269
if key.pubkey.scheme != PKScheme.RSA or key.seckey.scheme != PKScheme.RSA:
259
270
raise newException(ACMEError, " Unsupported signing key type" )
260
271
261
- let acmeHeader = await self.acmeHeader(url , key, needsJwk, kid)
272
+ let acmeHeader = await self.acmeHeader(uri , key, needsJwk, kid)
262
273
handleError(" createSignedAcmeRequest" ):
263
274
var token = toJWT(%* {" header" : acmeHeader, " claims" : payload})
264
275
let derPrivKey = key.seckey.rsakey.getBytes.get
@@ -272,62 +283,69 @@ proc requestRegister*(
272
283
let registerRequest = ACMERegisterRequest(termsOfServiceAgreed: true )
273
284
handleError(" acmeRegister" ):
274
285
let payload = await self.createSignedAcmeRequest(
275
- self.directory.newAccount, registerRequest, key, needsJwk = true
286
+ parseUri( self.directory.newAccount) , registerRequest, key, needsJwk = true
276
287
)
277
- let acmeResponse = await self.post(self.directory.newAccount, payload)
288
+ let acmeResponse = await self.post(parseUri( self.directory.newAccount) , payload)
278
289
let acmeResponseBody = acmeResponse.body.to(ACMERegisterResponseBody)
279
290
280
291
ACMERegisterResponse(
281
292
status: acmeResponseBody.status, kid: acmeResponse.headers.keyOrError(" location" )
282
293
)
283
294
284
295
proc requestNewOrder* (
285
- self: ACMEApi, domains: seq [string ], key: KeyPair, kid: Kid
296
+ self: ACMEApi, domains: seq [Domain ], key: KeyPair, kid: Kid
286
297
) : Future[ACMEChallengeResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
287
298
# request challenge from ACME server
288
299
let orderRequest = ACMEChallengeRequest(
289
300
identifiers: domains.mapIt(ACMEChallengeIdentifier(`type`: " dns" , value: it))
290
301
)
291
302
handleError(" requestNewOrder" ):
292
303
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)
294
305
)
295
- let acmeResponse = await self.post(self.directory.newOrder, payload)
296
-
306
+ let acmeResponse = await self.post(parseUri(self.directory.newOrder), payload)
297
307
let challengeResponseBody = acmeResponse.body.to(ACMEChallengeResponseBody)
298
308
if challengeResponseBody.authorizations.len() == 0 :
299
309
raise newException(ACMEError, " Authorizations field is empty" )
300
310
ACMEChallengeResponse(
301
311
status: challengeResponseBody.status,
302
312
authorizations: challengeResponseBody.authorizations,
303
313
finalize: challengeResponseBody.finalize,
304
- orderURL : acmeResponse.headers.keyOrError(" location" ),
314
+ order : acmeResponse.headers.keyOrError(" location" ),
305
315
)
306
316
307
317
proc requestAuthorizations* (
308
318
self: ACMEApi, authorizations: seq [Authorization], key: KeyPair, kid: Kid
309
319
) : Future[ACMEAuthorizationsResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
310
320
handleError(" requestAuthorizations" ):
311
321
doAssert authorizations.len > 0
312
- let acmeResponse = await self.get(authorizations[0 ])
322
+ let acmeResponse = await self.get(parseUri( authorizations[0 ]) )
313
323
acmeResponse.body.to(ACMEAuthorizationsResponse)
314
324
315
325
proc requestChallenge* (
316
326
self: ACMEApi, domains: seq [Domain], key: KeyPair, kid: Kid
317
327
) : Future[ACMEChallengeResponseWrapper] {.async: (raises: [ACMEError, CancelledError]) .} =
318
328
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
+ )
319
333
320
334
let authorizationsResponse =
321
335
await self.requestAuthorizations(challengeResponse.authorizations, key, kid)
336
+ if authorizationsResponse.challenges.len == 0 :
337
+ raise newException(ACMEError, " No challenges received" )
322
338
323
339
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 ],
327
345
)
328
346
329
347
proc requestCheck* (
330
- self: ACMEApi, checkURL: string , checkKind: ACMECheckKind, key: KeyPair, kid: Kid
348
+ self: ACMEApi, checkURL: Uri , checkKind: ACMECheckKind, key: KeyPair, kid: Kid
331
349
): Future[ACMECheckResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
332
350
handleError(" requestCheck" ):
333
351
let acmeResponse = await self.get(checkURL)
@@ -362,7 +380,7 @@ proc requestCheck*(
362
380
)
363
381
364
382
proc requestCompleted* (
365
- self: ACMEApi, chalURL: string , key: KeyPair, kid: Kid
383
+ self: ACMEApi, chalURL: Uri , key: KeyPair, kid: Kid
366
384
): Future[ACMECompletedResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
367
385
handleError(" requestCompleted (send notify)" ):
368
386
let payload =
@@ -372,7 +390,7 @@ proc requestCompleted*(
372
390
373
391
proc checkChallengeCompleted* (
374
392
self: ACMEApi,
375
- checkURL: string ,
393
+ checkURL: Uri ,
376
394
key: KeyPair,
377
395
kid: Kid,
378
396
retries: int = DefaultChalCompletedRetries,
@@ -394,7 +412,7 @@ proc checkChallengeCompleted*(
394
412
395
413
proc completeChallenge* (
396
414
self: ACMEApi,
397
- chalURL: string ,
415
+ chalURL: Uri ,
398
416
key: KeyPair,
399
417
kid: Kid,
400
418
retries: int = DefaultChalCompletedRetries,
@@ -404,28 +422,28 @@ proc completeChallenge*(
404
422
return await self.checkChallengeCompleted(chalURL, key, kid, retries = retries)
405
423
406
424
proc requestFinalize* (
407
- self: ACMEApi, domain: string , finalizeURL: string , key: KeyPair, kid: Kid
425
+ self: ACMEApi, domain: Domain, finalize: Uri , key: KeyPair, kid: Kid
408
426
): Future[ACMEFinalizeResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
409
427
let derCSR = createCSR(domain)
410
428
let b64CSR = base64.encode(derCSR.toSeq, safe = true )
411
429
412
430
handleError(" requestFinalize" ):
413
431
let payload = await self.createSignedAcmeRequest(
414
- finalizeURL , %* {" csr" : b64CSR}, key, kid = Opt.some(kid)
432
+ finalize , %* {" csr" : b64CSR}, key, kid = Opt.some(kid)
415
433
)
416
- let acmeResponse = await self.post(finalizeURL , payload)
434
+ let acmeResponse = await self.post(finalize , payload)
417
435
# server responds with updated order response
418
436
acmeResponse.body.to(ACMEFinalizeResponse)
419
437
420
438
proc checkCertFinalized* (
421
439
self: ACMEApi,
422
- orderURL: string ,
440
+ order: Uri ,
423
441
key: KeyPair,
424
442
kid: Kid,
425
443
retries: int = DefaultChalCompletedRetries,
426
444
): Future[bool ] {.async: (raises: [ACMEError, CancelledError]) .} =
427
445
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)
429
447
case checkResponse.orderStatus
430
448
of ACMEOrderStatus.valid:
431
449
return true
@@ -443,28 +461,28 @@ proc checkCertFinalized*(
443
461
444
462
proc certificateFinalized* (
445
463
self: ACMEApi,
446
- domain: string ,
447
- finalizeURL: string ,
448
- orderURL: string ,
464
+ domain: Domain ,
465
+ finalize: Uri ,
466
+ order: Uri ,
449
467
key: KeyPair,
450
468
kid: Kid,
451
469
retries: int = DefaultFinalizeRetries,
452
470
): 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)
454
472
# 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)
456
474
457
475
proc requestGetOrder* (
458
- self: ACMEApi, orderURL: string
476
+ self: ACMEApi, order: Uri
459
477
): Future[ACMEOrderResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
460
478
handleError(" requestGetOrder" ):
461
- let acmeResponse = await self.get(orderURL )
479
+ let acmeResponse = await self.get(order )
462
480
acmeResponse.body.to(ACMEOrderResponse)
463
481
464
482
proc downloadCertificate* (
465
- self: ACMEApi, orderURL: string
483
+ self: ACMEApi, order: Uri
466
484
): Future[ACMECertificateResponse] {.async: (raises: [ACMEError, CancelledError]) .} =
467
- let orderResponse = await self.requestGetOrder(orderURL )
485
+ let orderResponse = await self.requestGetOrder(order )
468
486
469
487
handleError(" downloadCertificate" ):
470
488
let rawResponse = await HttpClientRequestRef
0 commit comments