mirror of
https://github.com/vacp2p/nim-libp2p.git
synced 2026-01-09 22:28:27 -05:00
chore(acme): add MockACMEApi for testing (#1457)
This commit is contained in:
@@ -19,18 +19,22 @@ const
|
||||
DefaultRandStringSize = 256
|
||||
ACMEHttpHeaders = [("Content-Type", "application/jose+json")]
|
||||
|
||||
type Nonce = string
|
||||
type Kid = string
|
||||
type Nonce* = string
|
||||
type Kid* = string
|
||||
|
||||
type ACMEDirectory = object
|
||||
newNonce: string
|
||||
newOrder: string
|
||||
newAccount: string
|
||||
type ACMEDirectory* = object
|
||||
newNonce*: string
|
||||
newOrder*: string
|
||||
newAccount*: string
|
||||
|
||||
type ACMEApi* = object
|
||||
type ACMEApi* = ref object of RootObj
|
||||
directory: ACMEDirectory
|
||||
session: HttpSessionRef
|
||||
acmeServerURL: string
|
||||
acmeServerURL*: string
|
||||
|
||||
type HTTPResponse* = object
|
||||
body*: JsonNode
|
||||
headers*: HttpTable
|
||||
|
||||
type JWK = object
|
||||
kty: string
|
||||
@@ -94,10 +98,10 @@ type ACMEChallengeResponseBody = object
|
||||
finalize: string
|
||||
|
||||
type ACMEChallengeResponse* = object
|
||||
status: ACMEChallengeStatus
|
||||
authorizations: seq[string]
|
||||
finalize: string
|
||||
orderURL: string
|
||||
status*: ACMEChallengeStatus
|
||||
authorizations*: seq[string]
|
||||
finalize*: string
|
||||
orderURL*: string
|
||||
|
||||
type ACMEChallengeResponseWrapper* = object
|
||||
finalizeURL*: string
|
||||
@@ -105,7 +109,7 @@ type ACMEChallengeResponseWrapper* = object
|
||||
dns01*: ACMEChallenge
|
||||
|
||||
type ACMEAuthorizationsResponse* = object
|
||||
challenges: seq[ACMEChallenge]
|
||||
challenges*: seq[ACMEChallenge]
|
||||
|
||||
type ACMECompletedResponse* = object
|
||||
checkURL: string
|
||||
@@ -117,7 +121,7 @@ type ACMEOrderStatus* {.pure.} = enum
|
||||
valid = "valid"
|
||||
invalid = "invalid"
|
||||
|
||||
type ACMECheckKind = enum
|
||||
type ACMECheckKind* = enum
|
||||
ACMEOrderCheck
|
||||
ACMEChallengeCheck
|
||||
|
||||
@@ -129,7 +133,8 @@ type ACMECheckResponse* = object
|
||||
chalStatus: ACMEChallengeStatus
|
||||
retryAfter: Duration
|
||||
|
||||
type ACMEFinalizedResponse* = object
|
||||
type ACMEFinalizeResponse* = object
|
||||
status: ACMEOrderStatus
|
||||
|
||||
type ACMEOrderResponse* = object
|
||||
certificate: string
|
||||
@@ -139,7 +144,7 @@ type ACMECertificateResponse* = object
|
||||
rawCertificate: string
|
||||
certificateExpiry: DateTime
|
||||
|
||||
template handleError(msg: string, body: untyped): untyped =
|
||||
template handleError*(msg: string, body: untyped): untyped =
|
||||
try:
|
||||
body
|
||||
except ACMEError as exc:
|
||||
@@ -155,6 +160,18 @@ template handleError(msg: string, body: untyped): untyped =
|
||||
except CatchableError as exc:
|
||||
raise newException(ACMEError, msg & ": Unexpected error", exc)
|
||||
|
||||
method post*(
|
||||
self: ACMEApi, url: string, payload: string
|
||||
): Future[HTTPResponse] {.
|
||||
async: (raises: [ACMEError, HttpError, CancelledError]), base
|
||||
.}
|
||||
|
||||
method get*(
|
||||
self: ACMEApi, url: string
|
||||
): Future[HTTPResponse] {.
|
||||
async: (raises: [ACMEError, HttpError, CancelledError]), base
|
||||
.}
|
||||
|
||||
proc new*(
|
||||
T: typedesc[ACMEApi], acmeServerURL: string = LetsEncryptURL
|
||||
): Future[ACMEApi] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
@@ -167,15 +184,12 @@ proc new*(
|
||||
|
||||
ACMEApi(session: session, directory: directory, acmeServerURL: acmeServerURL)
|
||||
|
||||
proc requestNonce(
|
||||
method requestNonce*(
|
||||
self: ACMEApi
|
||||
): Future[Nonce] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
try:
|
||||
let resp =
|
||||
await HttpClientRequestRef.get(self.session, self.directory.newNonce).get().send()
|
||||
return resp.headers.getString("Replay-Nonce")
|
||||
except HttpError as exc:
|
||||
raise newException(ACMEError, "Failed to request new nonce from ACME server", exc)
|
||||
): Future[Nonce] {.async: (raises: [ACMEError, CancelledError]), base.} =
|
||||
handleError("requestNonce"):
|
||||
let acmeResponse = await self.get(self.directory.newNonce)
|
||||
Nonce(acmeResponse.headers.keyOrError("Replay-Nonce"))
|
||||
|
||||
# TODO: save n and e in account so we don't have to recalculate every time
|
||||
proc acmeHeader(
|
||||
@@ -210,15 +224,26 @@ proc acmeHeader(
|
||||
kid: kid.get(),
|
||||
)
|
||||
|
||||
proc post(
|
||||
method post*(
|
||||
self: ACMEApi, url: string, payload: string
|
||||
): Future[HttpClientResponseRef] {.
|
||||
async: (raises: [ACMEError, HttpError, CancelledError])
|
||||
): Future[HTTPResponse] {.
|
||||
async: (raises: [ACMEError, HttpError, CancelledError]), base
|
||||
.} =
|
||||
await HttpClientRequestRef
|
||||
let rawResponse = await HttpClientRequestRef
|
||||
.post(self.session, url, body = payload, headers = ACMEHttpHeaders)
|
||||
.get()
|
||||
.send()
|
||||
let body = await rawResponse.getResponseBody()
|
||||
HTTPResponse(body: body, headers: rawResponse.headers)
|
||||
|
||||
method get*(
|
||||
self: ACMEApi, url: string
|
||||
): Future[HTTPResponse] {.
|
||||
async: (raises: [ACMEError, HttpError, CancelledError]), base
|
||||
.} =
|
||||
let rawResponse = await HttpClientRequestRef.get(self.session, url).get().send()
|
||||
let body = await rawResponse.getResponseBody()
|
||||
HTTPResponse(body: body, headers: rawResponse.headers)
|
||||
|
||||
proc createSignedAcmeRequest(
|
||||
self: ACMEApi,
|
||||
@@ -247,16 +272,14 @@ proc requestRegister*(
|
||||
let payload = await self.createSignedAcmeRequest(
|
||||
self.directory.newAccount, registerRequest, key, needsJwk = true
|
||||
)
|
||||
let rawResponse = await self.post(self.directory.newAccount, payload)
|
||||
let body = await rawResponse.getResponseBody()
|
||||
let headers = rawResponse.headers
|
||||
let acmeResponseBody = body.to(ACMERegisterResponseBody)
|
||||
let acmeResponse = await self.post(self.directory.newAccount, payload)
|
||||
let acmeResponseBody = acmeResponse.body.to(ACMERegisterResponseBody)
|
||||
|
||||
ACMERegisterResponse(
|
||||
status: acmeResponseBody.status, kid: headers.getString("location")
|
||||
status: acmeResponseBody.status, kid: acmeResponse.headers.keyOrError("location")
|
||||
)
|
||||
|
||||
proc requestNewOrder(
|
||||
proc requestNewOrder*(
|
||||
self: ACMEApi, domains: seq[string], key: KeyPair, kid: Kid
|
||||
): Future[ACMEChallengeResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
# request challenge from ACME server
|
||||
@@ -267,29 +290,25 @@ proc requestNewOrder(
|
||||
let payload = await self.createSignedAcmeRequest(
|
||||
self.directory.newOrder, orderRequest, key, kid = Opt.some(kid)
|
||||
)
|
||||
let rawResponse = await self.post(self.directory.newOrder, payload)
|
||||
let body = await rawResponse.getResponseBody()
|
||||
let headers = rawResponse.headers
|
||||
let acmeResponse = await self.post(self.directory.newOrder, payload)
|
||||
|
||||
let challengeResponseBody = body.to(ACMEChallengeResponseBody)
|
||||
let challengeResponseBody = acmeResponse.body.to(ACMEChallengeResponseBody)
|
||||
if challengeResponseBody.authorizations.len() == 0:
|
||||
raise newException(ACMEError, "Authorizations field is empty")
|
||||
ACMEChallengeResponse(
|
||||
status: challengeResponseBody.status,
|
||||
authorizations: challengeResponseBody.authorizations,
|
||||
finalize: challengeResponseBody.finalize,
|
||||
orderURL: headers.getString("location"),
|
||||
orderURL: acmeResponse.headers.keyOrError("location"),
|
||||
)
|
||||
|
||||
proc requestAuthorizations(
|
||||
proc requestAuthorizations*(
|
||||
self: ACMEApi, authorizations: seq[string], key: KeyPair, kid: Kid
|
||||
): Future[ACMEAuthorizationsResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
handleError("requestAuthorizations"):
|
||||
doAssert authorizations.len > 0
|
||||
let rawResponse =
|
||||
await HttpClientRequestRef.get(self.session, authorizations[0]).get().send()
|
||||
let body = await rawResponse.getResponseBody()
|
||||
body.to(ACMEAuthorizationsResponse)
|
||||
let acmeResponse = await self.get(authorizations[0])
|
||||
acmeResponse.body.to(ACMEAuthorizationsResponse)
|
||||
|
||||
proc requestChallenge*(
|
||||
self: ACMEApi, domains: seq[string], key: KeyPair, kid: Kid
|
||||
@@ -305,17 +324,14 @@ proc requestChallenge*(
|
||||
dns01: authorizationsResponse.challenges.filterIt(it.`type` == "dns-01")[0],
|
||||
)
|
||||
|
||||
proc requestCheck(
|
||||
proc requestCheck*(
|
||||
self: ACMEApi, checkURL: string, checkKind: ACMECheckKind, key: KeyPair, kid: Kid
|
||||
): Future[ACMECheckResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
handleError("requestCheck"):
|
||||
let rawResponse =
|
||||
await HttpClientRequestRef.get(self.session, checkURL).get().send()
|
||||
let body = await rawResponse.getResponseBody()
|
||||
let headers = rawResponse.headers
|
||||
let acmeResponse = await self.get(checkURL)
|
||||
let retryAfter =
|
||||
try:
|
||||
parseInt(rawResponse.headers.getString("Retry-After")).seconds
|
||||
parseInt(acmeResponse.headers.keyOrError("Retry-After")).seconds
|
||||
except ValueError:
|
||||
DefaultChalCompletedRetryTime
|
||||
|
||||
@@ -324,58 +340,70 @@ proc requestCheck(
|
||||
try:
|
||||
ACMECheckResponse(
|
||||
kind: checkKind,
|
||||
orderStatus: parseEnum[ACMEOrderStatus](body["status"].getStr),
|
||||
orderStatus: parseEnum[ACMEOrderStatus](acmeResponse.body["status"].getStr),
|
||||
retryAfter: retryAfter,
|
||||
)
|
||||
except ValueError:
|
||||
raise newException(ACMEError, "Invalid order status: " & body["status"].getStr)
|
||||
raise newException(
|
||||
ACMEError, "Invalid order status: " & acmeResponse.body["status"].getStr
|
||||
)
|
||||
of ACMEChallengeCheck:
|
||||
try:
|
||||
ACMECheckResponse(
|
||||
kind: checkKind,
|
||||
chalStatus: parseEnum[ACMEChallengeStatus](body["status"].getStr),
|
||||
chalStatus: parseEnum[ACMEChallengeStatus](acmeResponse.body["status"].getStr),
|
||||
retryAfter: retryAfter,
|
||||
)
|
||||
except ValueError:
|
||||
raise newException(ACMEError, "Invalid order status: " & body["status"].getStr)
|
||||
raise newException(
|
||||
ACMEError, "Invalid order status: " & acmeResponse.body["status"].getStr
|
||||
)
|
||||
|
||||
proc requestCompleted(
|
||||
proc requestCompleted*(
|
||||
self: ACMEApi, chalURL: string, key: KeyPair, kid: Kid
|
||||
): Future[ACMECompletedResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
handleError("challengeCompleted (send notify)"):
|
||||
handleError("requestCompleted (send notify)"):
|
||||
let payload =
|
||||
await self.createSignedAcmeRequest(chalURL, %*{}, key, kid = Opt.some(kid))
|
||||
let rawResponse = await self.post(chalURL, payload)
|
||||
let body = await rawResponse.getResponseBody()
|
||||
body.to(ACMECompletedResponse)
|
||||
let acmeResponse = await self.post(chalURL, payload)
|
||||
acmeResponse.body.to(ACMECompletedResponse)
|
||||
|
||||
proc challengeCompleted*(
|
||||
proc checkChallengeCompleted*(
|
||||
self: ACMEApi,
|
||||
chalURL: string,
|
||||
checkURL: string,
|
||||
key: KeyPair,
|
||||
kid: Kid,
|
||||
retries: int = DefaultChalCompletedRetries,
|
||||
): Future[void] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let completedResponse = await self.requestCompleted(chalURL, key, kid)
|
||||
# check until acme server is done (poll validation)
|
||||
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
for i in 0 .. retries:
|
||||
let checkResponse =
|
||||
await self.requestCheck(completedResponse.checkURL, ACMEChallengeCheck, key, kid)
|
||||
let checkResponse = await self.requestCheck(checkURL, ACMEChallengeCheck, key, kid)
|
||||
case checkResponse.chalStatus
|
||||
of ACMEChallengeStatus.pending:
|
||||
await sleepAsync(checkResponse.retryAfter) # try again after some delay
|
||||
of ACMEChallengeStatus.valid:
|
||||
return
|
||||
return true
|
||||
else:
|
||||
raise newException(
|
||||
ACMEError,
|
||||
"Failed challenge completion: expected 'valid', got '" &
|
||||
$checkResponse.chalStatus & "'",
|
||||
)
|
||||
return false
|
||||
|
||||
proc completeChallenge*(
|
||||
self: ACMEApi,
|
||||
chalURL: string,
|
||||
key: KeyPair,
|
||||
kid: Kid,
|
||||
retries: int = DefaultChalCompletedRetries,
|
||||
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let completedResponse = await self.requestCompleted(chalURL, key, kid)
|
||||
# check until acme server is done (poll validation)
|
||||
return await self.checkChallengeCompleted(chalURL, key, kid, retries = retries)
|
||||
|
||||
proc requestFinalize*(
|
||||
self: ACMEApi, domain: string, finalizeURL: string, key: KeyPair, kid: Kid
|
||||
): Future[ACMEFinalizedResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
): Future[ACMEFinalizeResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let derCSR = createCSR(domain)
|
||||
let b64CSR = base64.encode(derCSR.toSeq, safe = true)
|
||||
|
||||
@@ -383,11 +411,35 @@ proc requestFinalize*(
|
||||
let payload = await self.createSignedAcmeRequest(
|
||||
finalizeURL, %*{"csr": b64CSR}, key, kid = Opt.some(kid)
|
||||
)
|
||||
let rawResponse = await self.post(finalizeURL, payload)
|
||||
let body = await rawResponse.getResponseBody()
|
||||
body.to(ACMEFinalizedResponse)
|
||||
let acmeResponse = await self.post(finalizeURL, payload)
|
||||
# server responds with updated order response
|
||||
acmeResponse.body.to(ACMEFinalizeResponse)
|
||||
|
||||
proc finalizeCertificate*(
|
||||
proc checkCertFinalized*(
|
||||
self: ACMEApi,
|
||||
orderURL: string,
|
||||
key: KeyPair,
|
||||
kid: Kid,
|
||||
retries: int = DefaultChalCompletedRetries,
|
||||
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
for i in 0 .. retries:
|
||||
let checkResponse = await self.requestCheck(orderURL, ACMEOrderCheck, key, kid)
|
||||
case checkResponse.orderStatus
|
||||
of ACMEOrderStatus.valid:
|
||||
return true
|
||||
of ACMEOrderStatus.processing:
|
||||
await sleepAsync(checkResponse.retryAfter) # try again after some delay
|
||||
else:
|
||||
raise newException(
|
||||
ACMEError,
|
||||
"Failed certificate finalization: expected 'valid', got '" &
|
||||
$checkResponse.orderStatus & "'",
|
||||
)
|
||||
return false
|
||||
|
||||
return false
|
||||
|
||||
proc certificateFinalized*(
|
||||
self: ACMEApi,
|
||||
domain: string,
|
||||
finalizeURL: string,
|
||||
@@ -396,31 +448,16 @@ proc finalizeCertificate*(
|
||||
kid: Kid,
|
||||
retries: int = DefaultFinalizeRetries,
|
||||
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
# call finalize and keep checking order until cert is valid (done)
|
||||
let finalizeResponse = await self.requestFinalize(domain, finalizeURL, key, kid)
|
||||
|
||||
handleError("finalizeCertificate (check finalized)"):
|
||||
var checkResponse: ACMECheckResponse
|
||||
for i in 0 .. retries:
|
||||
let checkResponse = await self.requestCheck(orderURL, ACMEOrderCheck, key, kid)
|
||||
case checkResponse.orderStatus
|
||||
of ACMEOrderStatus.valid:
|
||||
return true
|
||||
of ACMEOrderStatus.processing:
|
||||
await sleepAsync(checkResponse.retryAfter) # try again after some delay
|
||||
else:
|
||||
return false
|
||||
|
||||
return false
|
||||
# keep checking order until cert is valid (done)
|
||||
return await self.checkCertFinalized(orderURL, key, kid, retries = retries)
|
||||
|
||||
proc requestGetOrder*(
|
||||
self: ACMEApi, orderURL: string
|
||||
): Future[ACMEOrderResponse] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
handleError("requestGetOrder"):
|
||||
let rawResponse =
|
||||
await HttpClientRequestRef.get(self.session, orderURL).get().send()
|
||||
let body = await rawResponse.getResponseBody()
|
||||
body.to(ACMEOrderResponse)
|
||||
let acmeResponse = await self.get(orderURL)
|
||||
acmeResponse.body.to(ACMEOrderResponse)
|
||||
|
||||
proc downloadCertificate*(
|
||||
self: ACMEApi, orderURL: string
|
||||
|
||||
37
libp2p/autotls/acme/mockapi.nim
Normal file
37
libp2p/autotls/acme/mockapi.nim
Normal file
@@ -0,0 +1,37 @@
|
||||
import chronos, chronos/apps/http/httpclient, json
|
||||
|
||||
import ./api, ./utils
|
||||
|
||||
export api
|
||||
|
||||
type MockACMEApi* = ref object of ACMEApi
|
||||
parent*: ACMEApi
|
||||
mockedHeaders*: HttpTable
|
||||
mockedBody*: JsonNode
|
||||
|
||||
proc new*(
|
||||
T: typedesc[MockACMEApi]
|
||||
): Future[MockACMEApi] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let directory = ACMEDirectory(
|
||||
newNonce: LetsEncryptURL & "/new-nonce",
|
||||
newOrder: LetsEncryptURL & "/new-order",
|
||||
newAccount: LetsEncryptURL & "/new-account",
|
||||
)
|
||||
MockACMEApi(
|
||||
session: HttpSessionRef.new(), directory: directory, acmeServerURL: LetsEncryptURL
|
||||
)
|
||||
|
||||
method requestNonce*(
|
||||
self: MockACMEApi
|
||||
): Future[Nonce] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
return self.acmeServerURL & "/acme/1234"
|
||||
|
||||
method post*(
|
||||
self: MockACMEApi, url: string, payload: string
|
||||
): Future[HTTPResponse] {.async: (raises: [ACMEError, HttpError, CancelledError]).} =
|
||||
HTTPResponse(body: self.mockedBody, headers: self.mockedHeaders)
|
||||
|
||||
method get*(
|
||||
self: MockACMEApi, url: string
|
||||
): Future[HTTPResponse] {.async: (raises: [ACMEError, HttpError, CancelledError]).} =
|
||||
HTTPResponse(body: self.mockedBody, headers: self.mockedHeaders)
|
||||
@@ -4,6 +4,11 @@ import ../../transports/tls/certificate_ffi
|
||||
|
||||
type ACMEError* = object of LPError
|
||||
|
||||
proc keyOrError*(table: HttpTable, key: string): string {.raises: [ValueError].} =
|
||||
if not table.contains(key):
|
||||
raise newException(ValueError, "key " & key & " not present in headers")
|
||||
table.getString(key)
|
||||
|
||||
proc base64UrlEncode*(data: seq[byte]): string =
|
||||
## Encodes data using base64url (RFC 4648 §5) — no padding, URL-safe
|
||||
var encoded = base64.encode(data, safe = true)
|
||||
|
||||
178
tests/testautotls.nim
Normal file
178
tests/testautotls.nim
Normal file
@@ -0,0 +1,178 @@
|
||||
{.used.}
|
||||
|
||||
# Nim-Libp2p
|
||||
# Copyright (c) 2025 Status Research & Development GmbH
|
||||
# Licensed under either of
|
||||
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
||||
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
||||
# at your option.
|
||||
# This file may not be copied, modified, or distributed except according to
|
||||
# those terms.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import sequtils, json
|
||||
import chronos, chronos/apps/http/httpclient
|
||||
import ../libp2p/[stream/connection, upgrademngrs/upgrade, autotls/acme/mockapi, wire]
|
||||
|
||||
import ./helpers
|
||||
|
||||
suite "AutoTLS ACME Client":
|
||||
var api {.threadvar.}: MockACMEApi
|
||||
var key {.threadvar.}: KeyPair
|
||||
|
||||
asyncTeardown:
|
||||
await api.close()
|
||||
checkTrackers()
|
||||
|
||||
asyncSetup:
|
||||
api = await MockACMEApi.new()
|
||||
api.mockedHeaders = HttpTable.init()
|
||||
key = KeyPair.random(PKScheme.RSA, newRng()[]).get()
|
||||
|
||||
asyncTest "register to acme server":
|
||||
api.mockedBody = %*{"status": "valid"}
|
||||
api.mockedHeaders.add("location", "some-expected-kid")
|
||||
|
||||
let registerResponse = await api.requestRegister(key)
|
||||
check registerResponse.kid == "some-expected-kid"
|
||||
|
||||
asyncTest "request challenge for a domain":
|
||||
api.mockedBody =
|
||||
%*{
|
||||
"status": "pending",
|
||||
"authorizations": ["expected-authorizations-url"],
|
||||
"finalize": "expected-finalize-url",
|
||||
}
|
||||
api.mockedHeaders.set("location", "expected-order-url")
|
||||
|
||||
let challengeResponse =
|
||||
await api.requestNewOrder(@["some.dummy.domain.com"], key, "kid")
|
||||
check challengeResponse.status == ACMEChallengeStatus.pending
|
||||
check challengeResponse.authorizations == ["expected-authorizations-url"]
|
||||
check challengeResponse.finalize == "expected-finalize-url"
|
||||
check challengeResponse.orderURL == "expected-order-url"
|
||||
|
||||
# reset mocked obj for second request
|
||||
api.mockedBody =
|
||||
%*{
|
||||
"challenges": [
|
||||
{
|
||||
"url": "expected-dns01-url",
|
||||
"type": "dns-01",
|
||||
"status": "pending",
|
||||
"token": "expected-dns01-token",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let authorizationsResponse =
|
||||
await api.requestAuthorizations(challengeResponse.authorizations, key, "kid")
|
||||
check authorizationsResponse.challenges.len > 0
|
||||
|
||||
let dns01 = authorizationsResponse.challenges.filterIt(it.`type` == "dns-01")[0]
|
||||
check dns01.url == "expected-dns01-url"
|
||||
check dns01.`type` == "dns-01"
|
||||
check dns01.token == "expected-dns01-token"
|
||||
check dns01.status == ACMEChallengeStatus.pending
|
||||
|
||||
asyncTest "register with unsupported keys":
|
||||
let unsupportedSchemes = [PKScheme.Ed25519, PKScheme.Secp256k1, PKScheme.ECDSA]
|
||||
for scheme in unsupportedSchemes:
|
||||
let unsupportedKey = KeyPair.random(scheme, newRng()[]).get()
|
||||
expect(ACMEError):
|
||||
discard await api.requestRegister(unsupportedKey)
|
||||
|
||||
asyncTest "request challenge with invalid kid":
|
||||
expect(ACMEError):
|
||||
discard await api.requestChallenge(@["domain.com"], key, "invalid_kid_here")
|
||||
|
||||
asyncTest "challenge completed successful":
|
||||
api.mockedBody = %*{"checkURL": "some-check-url"}
|
||||
discard await api.requestCompleted("some-chal-url", key, "kid")
|
||||
|
||||
api.mockedBody = %*{"status": "valid"}
|
||||
api.mockedHeaders.add("Retry-After", "1")
|
||||
let completed = await api.checkChallengeCompleted("some-chal-url", key, "kid")
|
||||
check completed == true
|
||||
|
||||
asyncTest "challenge completed max retries reached":
|
||||
api.mockedBody = %*{"checkURL": "some-check-url"}
|
||||
discard await api.requestCompleted("some-chal-url", key, "kid")
|
||||
|
||||
api.mockedBody = %*{"status": "pending"}
|
||||
api.mockedHeaders.add("Retry-After", "1")
|
||||
let completed =
|
||||
await api.checkChallengeCompleted("some-chal-url", key, "kid", retries = 1)
|
||||
check completed == false
|
||||
|
||||
asyncTest "challenge completed invalid":
|
||||
api.mockedBody = %*{"checkURL": "some-check-url"}
|
||||
discard await api.requestCompleted("some-chal-url", key, "kid")
|
||||
|
||||
api.mockedBody = %*{"status": "invalid"}
|
||||
api.mockedHeaders.add("Retry-After", "1")
|
||||
expect(ACMEError):
|
||||
discard await api.checkChallengeCompleted("some-chal-url", key, "kid")
|
||||
|
||||
asyncTest "finalize certificate successful":
|
||||
api.mockedBody = %*{"status": "valid"}
|
||||
api.mockedHeaders.add("Retry-After", "1")
|
||||
let finalized = await api.certificateFinalized(
|
||||
"some-domain", "some-finalize-url", "some-order-url", key, "kid"
|
||||
)
|
||||
check finalized == true
|
||||
|
||||
asyncTest "finalize certificate max retries reached":
|
||||
api.mockedBody = %*{"status": "processing"}
|
||||
api.mockedHeaders.add("Retry-After", "1")
|
||||
let finalized = await api.certificateFinalized(
|
||||
"some-domain", "some-finalize-url", "some-order-url", key, "kid", retries = 1
|
||||
)
|
||||
check finalized == false
|
||||
|
||||
asyncTest "finalize certificate invalid":
|
||||
api.mockedBody = %*{"status": "invalid"}
|
||||
api.mockedHeaders.add("Retry-After", "1")
|
||||
expect(ACMEError):
|
||||
discard await api.certificateFinalized(
|
||||
"some-domain", "some-finalize-url", "some-order-url", key, "kid"
|
||||
)
|
||||
|
||||
asyncTest "expect error on invalid JSON response":
|
||||
api.mockedBody = %*{"inexistent field": "invalid value"}
|
||||
|
||||
expect(ACMEError):
|
||||
# avoid calling overloaded mock method requestNonce here since we want to test the actual thing
|
||||
discard await procCall requestNonce(ACMEApi(api))
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestRegister(key)
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestNewOrder(@["some-domain"], key, "kid")
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestAuthorizations(@["auth-1", "auth-2"], key, "kid")
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestChallenge(@["domain-1", "domain-2"], key, "kid")
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestCheck(
|
||||
"some-check-url", ACMECheckKind.ACMEOrderCheck, key, "kid"
|
||||
)
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestCheck(
|
||||
"some-check-url", ACMECheckKind.ACMEChallengeCheck, key, "kid"
|
||||
)
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestCompleted("some-chal-url", key, "kid")
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestFinalize("some-domain", "some-finalize-url", key, "kid")
|
||||
|
||||
expect(ACMEError):
|
||||
discard await api.requestGetOrder("some-order-url")
|
||||
@@ -47,14 +47,3 @@ suite "AutoTLS Integration":
|
||||
check challenge.dns01.`type`.len() > 0
|
||||
check challenge.dns01.status == ACMEChallengeStatus.pending
|
||||
check challenge.dns01.token.len() > 0
|
||||
|
||||
asyncTest "test register with unsupported keys":
|
||||
let unsupportedSchemes = [PKScheme.Ed25519, PKScheme.Secp256k1, PKScheme.ECDSA]
|
||||
for scheme in unsupportedSchemes:
|
||||
let unsupportedKey = KeyPair.random(scheme, newRng()[]).get()
|
||||
expect(ACMEError):
|
||||
discard await api.requestRegister(unsupportedKey)
|
||||
|
||||
asyncTest "test request challenge with invalid kid":
|
||||
expect(ACMEError):
|
||||
discard await api.requestChallenge(@["domain.com"], key, "invalid_kid_here")
|
||||
|
||||
@@ -28,7 +28,7 @@ import
|
||||
transports/tls/testcertificate
|
||||
|
||||
import
|
||||
testnameresolve, testmultistream, testbufferstream, testidentify,
|
||||
testautotls, testnameresolve, testmultistream, testbufferstream, testidentify,
|
||||
testobservedaddrmanager, testconnmngr, testswitch, testnoise, testpeerinfo,
|
||||
testpeerstore, testping, testmplex, testrelayv1, testrelayv2, testrendezvous,
|
||||
testdiscovery, testyamux, testautonat, testautonatservice, testautorelay, testdcutr,
|
||||
|
||||
Reference in New Issue
Block a user