mirror of
https://github.com/vacp2p/nim-libp2p.git
synced 2026-01-10 12:17:56 -05:00
Compare commits
27 Commits
fix-quic-a
...
autotls-re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a2a39838a | ||
|
|
26d6605427 | ||
|
|
8d8a6b25cb | ||
|
|
b38fcf8703 | ||
|
|
4cfad9cf95 | ||
|
|
f0adbdf837 | ||
|
|
11fec4dc15 | ||
|
|
425ccf8266 | ||
|
|
ccfac50393 | ||
|
|
65a8fa4760 | ||
|
|
7d0a1af0af | ||
|
|
506014a623 | ||
|
|
441868a86b | ||
|
|
b219239693 | ||
|
|
e4f0363c75 | ||
|
|
f87706ee41 | ||
|
|
51ab4bfff1 | ||
|
|
4fd6fc44e0 | ||
|
|
5541ff4f64 | ||
|
|
7222b3300a | ||
|
|
7f3e29e110 | ||
|
|
ad6b3e901d | ||
|
|
f994c499e8 | ||
|
|
1534dcf807 | ||
|
|
b38f1cde15 | ||
|
|
f1141b1287 | ||
|
|
0edd96c642 |
3
.pinned
3
.pinned
@@ -17,3 +17,6 @@ testutils;https://github.com/status-im/nim-testutils@#9e842bd58420d23044bc55e160
|
||||
unittest2;https://github.com/status-im/nim-unittest2@#8b51e99b4a57fcfb31689230e75595f024543024
|
||||
websock;https://github.com/status-im/nim-websock@#d5cd89062cd2d168ef35193c7d29d2102921d97e
|
||||
zlib;https://github.com/status-im/nim-zlib@#daa8723fd32299d4ca621c837430c29a5a11e19a
|
||||
jwt;https://github.com/vacp2p/nim-jwt@#18f8378de52b241f321c1f9ea905456e89b95c6f
|
||||
bearssl_pkey_decoder;https://github.com/vacp2p/bearssl_pkey_decoder@#21dd3710df9345ed2ad8bf8f882761e07863b8e0
|
||||
bio;https://github.com/xzeshen/bio@#0f5ed58b31c678920b6b4f7c1783984e6660be97
|
||||
|
||||
@@ -10,7 +10,8 @@ skipDirs = @["tests", "examples", "Nim", "tools", "scripts", "docs"]
|
||||
requires "nim >= 1.6.0",
|
||||
"nimcrypto >= 0.6.0 & < 0.7.0", "dnsclient >= 0.3.0 & < 0.4.0", "bearssl >= 0.2.5",
|
||||
"chronicles >= 0.10.3 & < 0.11.0", "chronos >= 4.0.4", "metrics", "secp256k1",
|
||||
"stew >= 0.4.0", "websock >= 0.2.0", "unittest2", "results", "quic >= 0.2.7"
|
||||
"stew >= 0.4.0", "websock >= 0.2.0", "unittest2", "results", "quic >= 0.2.7", "bio",
|
||||
"https://github.com/vacp2p/nim-jwt.git#18f8378de52b241f321c1f9ea905456e89b95c6f"
|
||||
|
||||
let nimc = getEnv("NIMC", "nim") # Which nim compiler to use
|
||||
let lang = getEnv("NIMLANG", "c") # Which backend (c/cpp/js)
|
||||
|
||||
327
libp2p/autotls/acme.nim
Normal file
327
libp2p/autotls/acme.nim
Normal file
@@ -0,0 +1,327 @@
|
||||
import options, base64, sequtils, serialization, json_serialization
|
||||
from times import DateTime, parse
|
||||
import chronos/apps/http/httpclient, jwt, results, bearssl/pem
|
||||
from std/json import
|
||||
JsonNode, `%*`, `%`, `[]`, `[]=`, `$`, parseJson, getStr, items, newJObject
|
||||
|
||||
import ./utils
|
||||
import ../crypto/crypto
|
||||
import ../crypto/rsa
|
||||
import ../transports/tls/certificate_ffi
|
||||
import ../transports/tls/certificate
|
||||
|
||||
const
|
||||
LetsEncryptURL* = "https://acme-v02.api.letsencrypt.org"
|
||||
LetsEncryptURLStaging* = "https://acme-staging-v02.api.letsencrypt.org"
|
||||
Alg = "RS256"
|
||||
DefaultChalCompletedRetries = 10
|
||||
DefaultChalCompletedRetryTime = 1.seconds
|
||||
DefaultFinalizeRetries = 10
|
||||
DefaultFinalizeRetryTime = 1.seconds
|
||||
DefaultRandStringSize = 256
|
||||
|
||||
type ACMEDirectory = object
|
||||
newNonce: string
|
||||
newOrder: string
|
||||
newAccount: string
|
||||
|
||||
type ACMEAccount* = object
|
||||
status*: Opt[string]
|
||||
contact*: Opt[seq[string]]
|
||||
key*: KeyPair
|
||||
session*: HttpSessionRef
|
||||
kid*: Opt[string]
|
||||
directory: ACMEDirectory
|
||||
acmeServerURL: string
|
||||
|
||||
proc new*(
|
||||
T: typedesc[ACMEAccount],
|
||||
key: KeyPair,
|
||||
status: Opt[string] = Opt.none(string),
|
||||
contact: Opt[seq[string]] = Opt.none(seq[string]),
|
||||
kid: Opt[string] = Opt.none(string),
|
||||
acmeServerURL: string = LetsEncryptURL,
|
||||
): Future[ref ACMEAccount] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let session = HttpSessionRef.new()
|
||||
var directory: ACMEDirectory
|
||||
try:
|
||||
let directoryResponse =
|
||||
await HttpClientRequestRef.get(session, acmeServerURL & "/directory").get().send()
|
||||
directory = (
|
||||
bytesToString(await directoryResponse.getBodyBytes()).checkedParseJson()
|
||||
).to(ACMEDirectory)
|
||||
except HttpError as exc:
|
||||
raise newException(ACMEError, "Failed to connect to ACME server", exc)
|
||||
except ValueError as exc:
|
||||
raise newException(ACMEError, "Unable to parse JSON", exc)
|
||||
except OSError as exc:
|
||||
raise newException(ACMEError, "Unable to parse JSON", exc)
|
||||
except IOError as exc:
|
||||
raise newException(ACMEError, "Unable to parse JSON", exc)
|
||||
|
||||
let acc = new(ACMEAccount)
|
||||
acc.status = status
|
||||
acc.contact = contact
|
||||
acc.kid = kid
|
||||
acc.key = key
|
||||
acc.session = session
|
||||
acc.directory = directory
|
||||
acc.acmeServerURL = acmeServerURL
|
||||
return acc
|
||||
|
||||
proc newNonce(
|
||||
self: ref ACMEAccount
|
||||
): Future[string] {.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)
|
||||
|
||||
# TODO: save n and e in account so we don't have to recalculate every time
|
||||
proc acmeHeader(
|
||||
self: ref ACMEAccount, url: string, needsJwk: bool, kid: string = ""
|
||||
): Future[JsonNode] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
# TODO: check if scheme is RSA
|
||||
let pubkey = self.key.pubkey.rsakey
|
||||
let nArray = @(getArray(pubkey.buffer, pubkey.key.n, pubkey.key.nlen))
|
||||
let eArray = @(getArray(pubkey.buffer, pubkey.key.e, pubkey.key.elen))
|
||||
let n = base64UrlEncode(nArray)
|
||||
let e = base64UrlEncode(eArray)
|
||||
|
||||
let newNonce = await self.newNonce()
|
||||
var header = %*{"alg": Alg, "typ": "JWT", "nonce": newNonce, "url": url}
|
||||
if needsJwk:
|
||||
header["jwk"] = %*{"kty": "RSA", "n": n, "e": e}
|
||||
else:
|
||||
if self.kid.isNone:
|
||||
raise newException(ACMEError, "no kid registered for account")
|
||||
header["kid"] = %*(self.kid.get)
|
||||
return header
|
||||
|
||||
proc signedAcmeRequest(
|
||||
self: ref ACMEAccount, url: string, payload: JsonNode, needsJwk: bool = false
|
||||
): Future[HttpClientResponseRef] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let acmeHeader = await self.acmeHeader(url, needsJwk)
|
||||
var token: JWT
|
||||
var body: JsonNode
|
||||
try:
|
||||
token = toJWT(%*{"header": acmeHeader, "claims": payload})
|
||||
let derPrivKey = self.key.seckey.rsakey.getBytes.get
|
||||
let pemPrivKey: string = pemEncode(derPrivKey, "PRIVATE KEY")
|
||||
token.sign(pemPrivKey)
|
||||
body = token.toFlattenedJson()
|
||||
except CatchableError as exc:
|
||||
raise newException(ACMEError, "Failed to create JWT", exc)
|
||||
try:
|
||||
return await HttpClientRequestRef
|
||||
.post(
|
||||
self.session,
|
||||
url,
|
||||
body = $body,
|
||||
headers = [("Content-Type", "application/jose+json")],
|
||||
)
|
||||
.get()
|
||||
.send()
|
||||
except HttpError as exc:
|
||||
raise newException(ACMEError, "Failed to send HTTP request to the ACME server", exc)
|
||||
|
||||
proc register*(self: ref ACMEAccount) {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
if self.kid.isSome:
|
||||
# already registered
|
||||
return
|
||||
|
||||
let payload = %*{"termsOfServiceAgreed": true}
|
||||
|
||||
let response =
|
||||
await self.signedAcmeRequest(self.directory.newAccount, payload, needsJwk = true)
|
||||
let jsonResponseBody = await response.getParsedResponseBody()
|
||||
if response.status != HttpCreated:
|
||||
raise newException(
|
||||
ACMEError, "Unable to register with ACME server: " & $jsonResponseBody
|
||||
)
|
||||
self.kid = Opt.some(response.headers.getString("location"))
|
||||
self.status = Opt.some(jsonResponseBody.getJSONField("status").getStr)
|
||||
|
||||
proc requestChallenge*(
|
||||
self: ref ACMEAccount, domains: seq[string]
|
||||
): Future[(JsonNode, string, string)] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
# request challenge from ACME server
|
||||
var identifiers: seq[JsonNode] = domains.mapIt(%*{"type": "dns", "value": it})
|
||||
|
||||
let orderPayload = %*{"identifiers": identifiers}
|
||||
|
||||
let challengeResponse =
|
||||
await self.signedAcmeRequest(self.directory.newOrder, orderPayload)
|
||||
let challengeResponseBody = await challengeResponse.getParsedResponseBody()
|
||||
let orderURL = challengeResponse.headers.getString("location")
|
||||
if orderURL == "":
|
||||
raise newException(ACMEError, "'location' header not found in ACME response")
|
||||
let finalizeURL = challengeResponseBody.getJSONField("finalize").getStr
|
||||
|
||||
# get challenges
|
||||
let authzURL = challengeResponseBody.getJSONField("authorizations")[0].getStr
|
||||
let authzResponseBody =
|
||||
try:
|
||||
let authzResponse =
|
||||
await HttpClientRequestRef.get(self.session, authzURL).get().send()
|
||||
await authzResponse.getParsedResponseBody()
|
||||
except CatchableError as exc:
|
||||
raise newException(ACMEError, "Failed to request challenge", exc)
|
||||
|
||||
let challenges = authzResponseBody.getJSONField("challenges")
|
||||
var dns01: JsonNode = nil
|
||||
for item in challenges:
|
||||
if item.getJSONField("type").getStr == "dns-01":
|
||||
dns01 = item
|
||||
break
|
||||
if dns01.isNil:
|
||||
raise newException(ACMEError, "DNS01 challenge not found in ACME response")
|
||||
|
||||
return (dns01, finalizeURL, orderURL)
|
||||
|
||||
proc notifyChallengeCompleted*(
|
||||
self: ref ACMEAccount, chalURL: string, retries: int = DefaultChalCompletedRetries
|
||||
): Future[void] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
let emptyPayload = newJObject()
|
||||
let completedResponse = await self.signedAcmeRequest(chalURL, emptyPayload)
|
||||
if completedResponse.status != HttpOk:
|
||||
raise newException(
|
||||
ACMEError,
|
||||
"Failed got HTTP status code " & $completedResponse.status &
|
||||
" while sending completed message to ACME server",
|
||||
)
|
||||
|
||||
var completedResponseBody: JsonNode
|
||||
try:
|
||||
completedResponseBody =
|
||||
bytesToString(await completedResponse.getBodyBytes()).checkedParseJson()
|
||||
except HttpError as exc:
|
||||
raise newException(ACMEError, "Failed to connect to ACME server", exc)
|
||||
except ValueError as exc:
|
||||
raise newException(
|
||||
ACMEError, "Unexpected error while signaling challenge completion", exc
|
||||
)
|
||||
|
||||
let checkURL = completedResponseBody.getJSONField("url").getStr
|
||||
# check until acme server is done (poll validation)
|
||||
for _ in 0 .. retries:
|
||||
var checkResponse: HttpClientResponseRef
|
||||
var checkResponseBody: JsonNode
|
||||
try:
|
||||
checkResponse =
|
||||
await HttpClientRequestRef.get(self.session, checkURL).get().send()
|
||||
checkResponseBody =
|
||||
bytesToString(await checkResponse.getBodyBytes()).checkedParseJson()
|
||||
except HttpError as exc:
|
||||
raise newException(ACMEError, "Failed to connect to ACME server", exc)
|
||||
except ValueError as exc:
|
||||
raise newException(
|
||||
ACMEError, "Unexpected error while signaling challenge completion", exc
|
||||
)
|
||||
let status = checkResponseBody.getJSONField("status").getStr
|
||||
case status
|
||||
of "pending":
|
||||
var retryAfter: Duration
|
||||
try:
|
||||
retryAfter = parseInt(checkResponse.headers.getString("Retry-After")).seconds
|
||||
except ValueError:
|
||||
retryAfter = DefaultChalCompletedRetryTime
|
||||
await sleepAsync(retryAfter) # try again after some delay
|
||||
of "valid":
|
||||
return
|
||||
else:
|
||||
raise newException(
|
||||
ACMEError, "Failed challenge completion: expected 'valid', got '" & status & "'"
|
||||
)
|
||||
|
||||
proc finalizeCertificate*(
|
||||
self: ref ACMEAccount,
|
||||
domain: string,
|
||||
finalizeURL: string,
|
||||
orderURL: string,
|
||||
retries: int = DefaultFinalizeRetries,
|
||||
): Future[bool] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
var certKey: cert_key_t
|
||||
var certCtx: cert_context_t
|
||||
var derCSR: ptr cert_buffer = nil
|
||||
|
||||
let personalizationStr = "libp2p_autotls"
|
||||
if cert_init_drbg(
|
||||
personalizationStr.cstring, personalizationStr.len.csize_t, certCtx.addr
|
||||
) != CERT_SUCCESS:
|
||||
raise newException(ACMEError, "Failed to initialize certCtx")
|
||||
if cert_generate_key(certCtx, certKey.addr) != CERT_SUCCESS:
|
||||
raise newException(ACMEError, "Failed to generate cert key")
|
||||
|
||||
if cert_signing_req(domain.cstring, certKey, derCSR.addr) != CERT_SUCCESS:
|
||||
raise newException(ACMEError, "Failed to create CSR")
|
||||
|
||||
let b64CSR = base64.encode(derCSR.toSeq, safe = true)
|
||||
let payload = %*{"csr": b64CSR}
|
||||
|
||||
# send finalize request
|
||||
let finalizedResponse = await self.signedAcmeRequest(finalizeURL, payload)
|
||||
if finalizedResponse.status != HttpOk:
|
||||
raise newException(ACMEError, "Failed to request cert finalization")
|
||||
|
||||
# keep checking order until it's finalized
|
||||
var checkResponse: HttpClientResponseRef
|
||||
var checkResponseBody: JsonNode
|
||||
for _ in 0 .. retries:
|
||||
let finalizedResponse = await self.signedAcmeRequest(finalizeURL, payload)
|
||||
try:
|
||||
checkResponse =
|
||||
await HttpClientRequestRef.get(self.session, orderURL).get().send()
|
||||
checkResponseBody =
|
||||
bytesToString(await checkResponse.getBodyBytes()).checkedParseJson()
|
||||
except ValueError as exc:
|
||||
raise
|
||||
newException(ACMEError, "JSON parsing error while finalizing certificate", exc)
|
||||
except CatchableError as exc:
|
||||
raise
|
||||
newException(ACMEError, "Unexpected error while finalizing certificate", exc)
|
||||
|
||||
let status = checkResponseBody.getJSONField("status").getStr
|
||||
case status
|
||||
of "valid":
|
||||
return true
|
||||
of "processing":
|
||||
var retryAfter: Duration
|
||||
try:
|
||||
retryAfter = parseInt(checkResponse.headers.getString("Retry-After")).seconds
|
||||
except ValueError:
|
||||
retryAfter = DefaultFinalizeRetryTime
|
||||
await sleepAsync(retryAfter) # try again after some delay
|
||||
else:
|
||||
return false
|
||||
|
||||
return false
|
||||
|
||||
proc downloadCertificate*(
|
||||
self: ref ACMEAccount, orderURL: string
|
||||
): Future[(string, DateTime)] {.async: (raises: [ACMEError, CancelledError]).} =
|
||||
try:
|
||||
let downloadResponse =
|
||||
await HttpClientRequestRef.get(self.session, orderURL).get().send()
|
||||
|
||||
if downloadResponse.status != HttpOk:
|
||||
raise newException(ACMEError, "Failed to download certificate")
|
||||
|
||||
let certificateInfoBody =
|
||||
bytesToString(await downloadResponse.getBodyBytes()).checkedParseJson()
|
||||
|
||||
let certificateDownloadURL = certificateInfoBody.getJSONField("certificate").getStr
|
||||
let certificateExpiry = parse(
|
||||
certificateInfoBody.getJSONField("expires").getStr, "yyyy-MM-dd'T'HH:mm:ss'Z'"
|
||||
)
|
||||
|
||||
let certificateResponse =
|
||||
await HttpClientRequestRef.get(self.session, certificateDownloadURL).get().send()
|
||||
let rawCertificate = bytesToString(await certificateResponse.getBodyBytes())
|
||||
return (rawCertificate, certificateExpiry)
|
||||
except HttpError:
|
||||
raise newException(ACMEError, "Failed to connect to ACME server")
|
||||
except ValueError as exc:
|
||||
raise newException(ACMEError, "Unexpected error while downloading certificate", exc)
|
||||
271
libp2p/autotls/autotls.nim
Normal file
271
libp2p/autotls/autotls.nim
Normal file
@@ -0,0 +1,271 @@
|
||||
# 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: [].}
|
||||
{.push public.}
|
||||
|
||||
import
|
||||
net,
|
||||
results,
|
||||
chronos,
|
||||
chronicles,
|
||||
bearssl/rand,
|
||||
bio,
|
||||
json,
|
||||
sequtils,
|
||||
chronos/apps/http/httpclient
|
||||
|
||||
import ./acme
|
||||
import ./peeridauth
|
||||
import ./utils
|
||||
import ../nameresolving/dnsresolver
|
||||
import ../wire
|
||||
import ../crypto/crypto
|
||||
import ../peerinfo
|
||||
import ../utils/heartbeat
|
||||
|
||||
logScope:
|
||||
topics = "libp2p autotls"
|
||||
|
||||
const
|
||||
DefaultDnsServers =
|
||||
@[
|
||||
initTAddress("1.1.1.1:53"),
|
||||
initTAddress("1.0.0.1:53"),
|
||||
initTAddress("[2606:4700:4700::1111]:53"),
|
||||
]
|
||||
DefaultRenewCheckTime = 1.hours
|
||||
DefaultRenewBufferTime = 1.hours
|
||||
DefaultDnsRetries = 10
|
||||
DefaultDnsRetryTime = 1.seconds
|
||||
|
||||
type SigParam = object
|
||||
k: string
|
||||
v: seq[byte]
|
||||
|
||||
type AutoTLSManager* = ref object
|
||||
rng: ref HmacDrbgContext
|
||||
managerFut: Future[void]
|
||||
cert*: Opt[TLSCertificate]
|
||||
certExpiry*: Opt[Moment]
|
||||
certReady*: AsyncEvent
|
||||
acmeAccount*: ref ACMEAccount
|
||||
httpSession: HttpSessionRef
|
||||
dnsResolver*: DnsResolver
|
||||
peerInfo*: Opt[PeerInfo]
|
||||
bearerToken*: Opt[string]
|
||||
renewCheckTime*: Duration
|
||||
renewBufferTime*: Duration
|
||||
acmeServerURL: string
|
||||
keyAuthorization*: Opt[string]
|
||||
ipAddress: Opt[IpAddress]
|
||||
|
||||
proc new*(
|
||||
T: typedesc[AutoTLSManager],
|
||||
rng: ref HmacDrbgContext = newRng(),
|
||||
acmeAccount: ref ACMEAccount = nil,
|
||||
dnsResolver: DnsResolver = DnsResolver.new(DefaultDnsServers),
|
||||
acmeServerURL: string = LetsEncryptURL,
|
||||
ipAddress: Opt[IpAddress] = Opt.none(IpAddress),
|
||||
): AutoTLSManager =
|
||||
T(
|
||||
rng: rng,
|
||||
managerFut: nil,
|
||||
cert: Opt.none(TLSCertificate),
|
||||
certExpiry: Opt.none(Moment),
|
||||
certReady: newAsyncEvent(),
|
||||
acmeAccount: acmeAccount,
|
||||
httpSession: HttpSessionRef.new(),
|
||||
dnsResolver: dnsResolver,
|
||||
peerInfo: Opt.none(PeerInfo),
|
||||
bearerToken: Opt.none(string),
|
||||
renewCheckTime: DefaultRenewCheckTime,
|
||||
renewBufferTime: DefaultRenewBufferTime,
|
||||
acmeServerURL: acmeServerURL,
|
||||
keyAuthorization: Opt.none(string),
|
||||
ipAddress: ipAddress,
|
||||
)
|
||||
|
||||
proc checkDNSRecords(
|
||||
self: AutoTLSManager,
|
||||
ip4Domain: string,
|
||||
acmeChalDomain: string,
|
||||
retries: int = DefaultDnsRetries,
|
||||
): Future[bool] {.async: (raises: [AutoTLSError, CancelledError]).} =
|
||||
var txt: seq[string]
|
||||
var ip4: seq[TransportAddress]
|
||||
|
||||
for _ in 0 .. retries:
|
||||
txt = await self.dnsResolver.resolveTxt(acmeChalDomain)
|
||||
try:
|
||||
ip4 = await self.dnsResolver.resolveIp(ip4Domain, 0.Port)
|
||||
except CatchableError as exc:
|
||||
error "Failed to resolve IP", description = exc.msg # retry
|
||||
if txt.len > 0 and self.keyAuthorization.isSome and
|
||||
txt[0] == self.keyAuthorization.get() and ip4.len > 0:
|
||||
return true
|
||||
await sleepAsync(DefaultDnsRetryTime)
|
||||
|
||||
return false
|
||||
|
||||
method issueCertificate(
|
||||
self: AutoTLSManager
|
||||
): Future[void] {.base, async: (raises: [AutoTLSError, CancelledError]).} =
|
||||
trace "Issuing new certificate"
|
||||
let peerInfo = self.peerInfo.valueOr:
|
||||
raise newException(AutoTLSError, "Cannot issue new certificate: peerInfo not set")
|
||||
|
||||
# generate autotls domain string: "*.{peerID}.libp2p.direct"
|
||||
let base36PeerId = encodePeerId(peerInfo.peerId)
|
||||
let baseDomain = base36PeerId & "." & AutoTLSDNSServer
|
||||
let domain = "*." & baseDomain
|
||||
|
||||
trace "Requesting ACME challenge"
|
||||
let (dns01Challenge, finalizeURL, orderURL) =
|
||||
await self.acmeAccount.requestChallenge(@[domain])
|
||||
|
||||
self.keyAuthorization = Opt.some(
|
||||
base64UrlEncode(
|
||||
@(
|
||||
sha256.digest(
|
||||
(
|
||||
dns01Challenge.getJSONField("token").getStr & "." &
|
||||
thumbprint(self.acmeAccount.key)
|
||||
).toByteSeq
|
||||
).data
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let strMultiaddresses: seq[string] = peerInfo.addrs.mapIt($it)
|
||||
|
||||
let payload = %*{"value": self.keyAuthorization.get(), "addresses": strMultiaddresses}
|
||||
let registrationURL = "https://" & AutoTLSBroker & "/v1/_acme-challenge"
|
||||
|
||||
trace "Sending challenge to AutoTLS broker"
|
||||
var response: HttpClientResponseRef
|
||||
var bearerToken: string
|
||||
if self.bearerToken.isSome:
|
||||
(bearerToken, response) = await peerIdAuthSend(
|
||||
registrationURL,
|
||||
self.httpSession,
|
||||
peerInfo,
|
||||
payload,
|
||||
self.rng,
|
||||
bearerToken = self.bearerToken,
|
||||
)
|
||||
else:
|
||||
# authenticate, send challenge and save bearerToken for future requests
|
||||
(bearerToken, response) = await peerIdAuthSend(
|
||||
registrationURL, self.httpSession, peerInfo, payload, self.rng
|
||||
)
|
||||
self.bearerToken = Opt.some(bearerToken)
|
||||
if response.status != HttpOk:
|
||||
raise newException(
|
||||
AutoTLSError, "Failed to authenticate with AutoTLS Broker at " & AutoTLSBroker
|
||||
)
|
||||
|
||||
# no need to do anything from this point forward if there are not public ip addresses on host
|
||||
let hostPrimaryIP: IpAddress =
|
||||
try:
|
||||
let ip = self.ipAddress.valueOr:
|
||||
checkedGetPrimaryIPAddr()
|
||||
if not isPublicIPv4(ip):
|
||||
raise newException(AutoTLSError, "Host does not have a public IPv4 address")
|
||||
ip
|
||||
except GetPrimaryIPError as exc:
|
||||
raise newException(AutoTLSError, "Failed to get primary IP address for host", exc)
|
||||
except CatchableError as exc:
|
||||
raise newException(
|
||||
AutoTLSError, "Unexpected error while getting primary IP address for host", exc
|
||||
)
|
||||
|
||||
debug "Waiting for DNS record to be set"
|
||||
|
||||
# if my ip address is 100.10.10.3 then the ip4Domain will be:
|
||||
# 100-10-10-3.{peerIdBase36}.libp2p.direct
|
||||
# and acme challenge TXT domain will be:
|
||||
# _acme-challenge.{peerIdBase36}.libp2p.direct
|
||||
let dashedIpAddr = ($hostPrimaryIP).replace(".", "-")
|
||||
let acmeChalDomain = "_acme-challenge." & baseDomain
|
||||
let ip4Domain = dashedIpAddr & "." & baseDomain
|
||||
if not await self.checkDNSRecords(ip4Domain, acmeChalDomain):
|
||||
raise newException(AutoTLSError, "DNS records not set")
|
||||
|
||||
debug "Notifying challenge completion to ACME server"
|
||||
let chalURL = dns01Challenge.getJSONField("url").getStr
|
||||
await self.acmeAccount.notifyChallengeCompleted(chalURL)
|
||||
|
||||
debug "Finalize cert request with CSR"
|
||||
if not await self.acmeAccount.finalizeCertificate(domain, finalizeURL, orderURL):
|
||||
raise newException(AutoTLSError, "ACME certificate finalization request failed")
|
||||
|
||||
debug "Downloading certificate"
|
||||
let (rawCert, expiry) = await self.acmeAccount.downloadCertificate(orderURL)
|
||||
|
||||
trace "Installing certificate"
|
||||
try:
|
||||
self.cert = Opt.some(TLSCertificate.init(rawCert))
|
||||
self.certExpiry = Opt.some(asMoment(expiry))
|
||||
except TLSStreamProtocolError:
|
||||
raise newException(AutoTLSError, "Could not parse downloaded certificates")
|
||||
self.certReady.fire()
|
||||
|
||||
proc manageCertificate(
|
||||
self: AutoTLSManager
|
||||
): Future[void] {.async: (raises: [AutoTLSError, CancelledError]).} =
|
||||
trace "Starting AutoTLS manager"
|
||||
|
||||
debug "Registering ACME account"
|
||||
if self.acmeAccount.isNil:
|
||||
let accountKey = KeyPair.random(PKScheme.RSA, self.rng[]).get()
|
||||
self.acmeAccount =
|
||||
(await ACMEAccount.new(accountKey, acmeServerURL = self.acmeServerURL))
|
||||
await self.acmeAccount.register()
|
||||
|
||||
heartbeat "Certificate Management", self.renewCheckTime:
|
||||
if self.cert.isNone or self.certExpiry.isNone:
|
||||
try:
|
||||
await self.issueCertificate()
|
||||
except CatchableError as exc:
|
||||
error "Failed to issue certificate", err = exc.msg
|
||||
break
|
||||
|
||||
# AutoTLSManager will renew the cert 1h before it expires
|
||||
let expiry = self.certExpiry.get
|
||||
let waitTime: Duration = expiry - Moment.now - self.renewBufferTime
|
||||
if waitTime <= self.renewBufferTime:
|
||||
try:
|
||||
await self.issueCertificate()
|
||||
except CatchableError as exc:
|
||||
error "Failed to renew certificate", err = exc.msg
|
||||
break
|
||||
|
||||
method start*(
|
||||
self: AutoTLSManager, peerInfo: PeerInfo
|
||||
): Future[void] {.base, async: (raises: [CancelledError]).} =
|
||||
if not self.managerFut.isNil:
|
||||
warn "Starting AutoTLS twice"
|
||||
return
|
||||
|
||||
self.peerInfo = Opt.some(peerInfo)
|
||||
self.managerFut = self.manageCertificate()
|
||||
|
||||
method stop*(self: AutoTLSManager): Future[void] {.base, async: (raises: []).} =
|
||||
trace "AutoTLS stop"
|
||||
if self.managerFut.isNil:
|
||||
warn "Stopping AutoTLS without starting it"
|
||||
return
|
||||
|
||||
await self.managerFut.cancelAndWait()
|
||||
self.managerFut = nil
|
||||
if not self.acmeAccount.isNil:
|
||||
await self.acmeAccount.session.closeWait()
|
||||
if not self.httpSession.isNil:
|
||||
await self.httpSession.closeWait()
|
||||
229
libp2p/autotls/peeridauth.nim
Normal file
229
libp2p/autotls/peeridauth.nim
Normal file
@@ -0,0 +1,229 @@
|
||||
import base64, json, strutils
|
||||
import chronos/apps/http/httpclient, results, chronicles, bio
|
||||
import ./utils, ../peerinfo, ../crypto/crypto
|
||||
|
||||
logScope:
|
||||
topics = "libp2p peerid auth"
|
||||
|
||||
const PeerIDAuthPrefix = "libp2p-PeerID"
|
||||
const ChallengeCharset =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
const ChallengeDefaultLen = 48
|
||||
|
||||
type SigParam = object
|
||||
k: string
|
||||
v: seq[byte]
|
||||
|
||||
proc randomChallenge(
|
||||
rng: ref HmacDrbgContext, challengeLen: int = ChallengeDefaultLen
|
||||
): string =
|
||||
var rng = rng[]
|
||||
var challenge = ""
|
||||
for _ in 0 ..< challengeLen:
|
||||
challenge.add(rng.sampleChar(ChallengeCharset))
|
||||
challenge
|
||||
|
||||
proc extractField(data, key: string): string {.raises: [PeerIDAuthError].} =
|
||||
# Helper to extract quoted value from key
|
||||
for segment in data.split(","):
|
||||
if key in segment:
|
||||
return segment.split("=", 1)[1].strip(chars = {' ', '"'})
|
||||
raise newException(PeerIDAuthError, "Failed to find " & key & " in " & data)
|
||||
|
||||
proc encodeVarint(n: int): seq[byte] =
|
||||
var varInt: seq[byte] = @[]
|
||||
var x = uint64(n)
|
||||
while x != 0:
|
||||
var byteVal = byte(x and 0x7F)
|
||||
x = x shr 7
|
||||
if x != 0:
|
||||
byteVal = byteVal or 0x80
|
||||
varInt.add(byteVal)
|
||||
return varInt
|
||||
|
||||
proc genDataToSign(prefix: string, parts: seq[SigParam]): seq[byte] =
|
||||
var buf: seq[byte] = prefix.toByteSeq()
|
||||
for p in parts:
|
||||
buf.add encodeVarint(p.k.len + p.v.len + 1)
|
||||
buf.add (p.k & "=").toByteSeq()
|
||||
buf.add p.v
|
||||
return buf
|
||||
|
||||
proc getSigParams(
|
||||
clientSender: bool, hostname: string, challenge: string, publicKey: PublicKey
|
||||
): seq[SigParam] =
|
||||
if clientSender:
|
||||
@[
|
||||
SigParam(k: "challenge-client", v: challenge.toByteSeq()),
|
||||
SigParam(k: "hostname", v: hostname.toByteSeq()),
|
||||
SigParam(k: "server-public-key", v: publicKey.getBytes().get()),
|
||||
]
|
||||
else:
|
||||
@[
|
||||
SigParam(k: "challenge-server", v: challenge.toByteSeq()),
|
||||
SigParam(k: "client-public-key", v: publicKey.getBytes().get()),
|
||||
SigParam(k: "hostname", v: hostname.toByteSeq()),
|
||||
]
|
||||
|
||||
proc peerIdSign(
|
||||
privateKey: PrivateKey,
|
||||
challenge: string,
|
||||
publicKey: PublicKey,
|
||||
hostname: string,
|
||||
clientSender: bool = true,
|
||||
): string =
|
||||
let parts = getSigParams(clientSender, hostname, challenge, publicKey)
|
||||
let bytesToSign = genDataToSign(PeerIDAuthPrefix, parts)
|
||||
return base64.encode(privateKey.sign(bytesToSign).get().getBytes(), safe = true)
|
||||
|
||||
proc checkSignature(
|
||||
serverSig: string,
|
||||
serverPublicKey: PublicKey,
|
||||
challengeServer: string,
|
||||
clientPublicKey: PublicKey,
|
||||
hostname: string,
|
||||
): bool {.raises: [PeerIDAuthError].} =
|
||||
let parts = getSigParams(false, hostname, challengeServer, clientPublicKey)
|
||||
let signedBytes = genDataToSign(PeerIDAuthPrefix, parts)
|
||||
var serverSignature: Signature
|
||||
try:
|
||||
if not serverSignature.init(base64.decode(serverSig).toByteSeq()):
|
||||
raise newException(
|
||||
PeerIDAuthError, "Failed to initialize Signature from base64 encoded sig"
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise newException(PeerIDAuthError, "Failed to decode server's signature", exc)
|
||||
return serverSignature.verify(
|
||||
signedBytes.toOpenArray(0, signedBytes.len - 1), serverPublicKey
|
||||
)
|
||||
|
||||
proc peerIdAuthenticate(
|
||||
url: string,
|
||||
session: HttpSessionRef,
|
||||
peerInfo: PeerInfo,
|
||||
payload: JsonNode,
|
||||
rng: ref HmacDrbgContext,
|
||||
): Future[(string, HttpClientResponseRef)] {.
|
||||
async: (raises: [AutoTLSError, PeerIDAuthError, CancelledError])
|
||||
.} =
|
||||
# Authenticate in three ways as per the PeerID Auth spec
|
||||
# https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md
|
||||
|
||||
# request authentication
|
||||
let base36PeerId = encodePeerId(peerInfo.peerId)
|
||||
var authStartResponse: HttpClientResponseRef
|
||||
try:
|
||||
authStartResponse = await HttpClientRequestRef.get(session, url).get().send()
|
||||
except HttpError as exc:
|
||||
raise newException(PeerIDAuthError, "Failed to start PeerID Auth", exc)
|
||||
|
||||
# www-authenticate
|
||||
let wwwAuthenticate = authStartResponse.headers.getString("WWW-Authenticate")
|
||||
if wwwAuthenticate == "":
|
||||
raise newException(PeerIDAuthError, "WWW-authenticate not present in response")
|
||||
let challengeClient = extractField(wwwAuthenticate, "challenge-client")
|
||||
|
||||
let serverPublicKey: PublicKey =
|
||||
try:
|
||||
PublicKey.init(decode(extractField(wwwAuthenticate, "public-key")).toByteSeq()).valueOr:
|
||||
raise newException(PeerIDAuthError, "Failed to initialize public-key")
|
||||
except ValueError as exc:
|
||||
raise newException(PeerIDAuthError, "Failed to decode public-key", exc)
|
||||
|
||||
let opaque = extractField(wwwAuthenticate, "opaque")
|
||||
|
||||
let publicKeyBytes: seq[byte] =
|
||||
try:
|
||||
peerInfo.publicKey.getBytes().valueOr:
|
||||
raise
|
||||
newException(PeerIDAuthError, "Failed to get bytes from PeerInfo's publicKey")
|
||||
except ValueError as exc:
|
||||
raise newException(
|
||||
PeerIDAuthError, "Failed to get bytes from PeerInfo's publicKey", exc
|
||||
)
|
||||
|
||||
let hostname = AutoTLSBroker # registration.libp2p.direct
|
||||
|
||||
let clientPubKeyB64 = base64.encode(publicKeyBytes, safe = true)
|
||||
let challengeServer: string =
|
||||
try:
|
||||
randomChallenge(rng)
|
||||
except ValueError as exc:
|
||||
raise newException(PeerIDAuthError, "Failed to generate challenge", exc)
|
||||
|
||||
let sig = peerIdSign(peerInfo.privateKey, challengeClient, serverPublicKey, hostname)
|
||||
let authHeader =
|
||||
PeerIDAuthPrefix & " public-key=\"" & clientPubKeyB64 & "\"" & ", opaque=\"" & opaque &
|
||||
"\"" & ", challenge-server=\"" & challengeServer & "\"" & ", sig=\"" & sig & "\""
|
||||
|
||||
# Authorization
|
||||
let authorizationResponse: HttpClientResponseRef =
|
||||
try:
|
||||
await HttpClientRequestRef
|
||||
.post(
|
||||
session,
|
||||
url,
|
||||
body = $payload,
|
||||
headers = [
|
||||
("Content-Type", "application/json"),
|
||||
("User-Agent", "nim-libp2p"),
|
||||
("authorization", authHeader),
|
||||
],
|
||||
)
|
||||
.get()
|
||||
.send()
|
||||
except HttpError as exc:
|
||||
raise newException(
|
||||
PeerIDAuthError, "Failed to send Authorization for PeerID Auth", exc
|
||||
)
|
||||
|
||||
# check server's signature
|
||||
let serverSig =
|
||||
extractField(authorizationResponse.headers.getString("authentication-info"), "sig")
|
||||
if not checkSignature(
|
||||
serverSig, serverPublicKey, challengeServer, peerInfo.publicKey, hostname
|
||||
):
|
||||
raise newException(PeerIDAuthError, "Failed to validate server's signature")
|
||||
|
||||
# Bearer token
|
||||
return (
|
||||
extractField(
|
||||
authorizationResponse.headers.getString("authentication-info"), "bearer"
|
||||
),
|
||||
authorizationResponse,
|
||||
)
|
||||
|
||||
proc peerIdAuthSend*(
|
||||
url: string,
|
||||
session: HttpSessionRef,
|
||||
peerInfo: PeerInfo,
|
||||
payload: JsonNode,
|
||||
rng: ref HmacDrbgContext,
|
||||
bearerToken: Opt[string] = Opt.none(string),
|
||||
): Future[(string, HttpClientResponseRef)] {.
|
||||
async: (raises: [AutoTLSError, PeerIDAuthError, CancelledError])
|
||||
.} =
|
||||
if bearerToken.isNone:
|
||||
return await peerIdAuthenticate(url, session, peerInfo, payload, rng)
|
||||
|
||||
let authHeader = PeerIDAuthPrefix & " bearer=\"" & bearerToken.get & "\""
|
||||
var response: HttpClientResponseRef
|
||||
try:
|
||||
response = await HttpClientRequestRef
|
||||
.post(
|
||||
session,
|
||||
url,
|
||||
body = $payload,
|
||||
headers = [
|
||||
("Content-Type", "application/json"),
|
||||
("User-Agent", "nim-libp2p"),
|
||||
("Authorization", authHeader),
|
||||
],
|
||||
)
|
||||
.get()
|
||||
.send()
|
||||
except HttpError as exc:
|
||||
raise newException(
|
||||
PeerIDAuthError, "Failed to send request with bearer token for PeerID Auth", exc
|
||||
)
|
||||
return (bearerToken.get, response)
|
||||
109
libp2p/autotls/utils.nim
Normal file
109
libp2p/autotls/utils.nim
Normal file
@@ -0,0 +1,109 @@
|
||||
import
|
||||
base64, strutils, stew/base36, chronos/apps/http/httpclient, chronos, json, net, times
|
||||
import
|
||||
../errors, ../peerid, ../multihash, ../cid, ../multicodec, ../crypto/[crypto, rsa]
|
||||
|
||||
type
|
||||
GetPrimaryIPError* = object of LPError
|
||||
AutoTLSError* = object of LPError
|
||||
ACMEError* = object of AutoTLSError
|
||||
PeerIDAuthError* = object of AutoTLSError
|
||||
|
||||
const
|
||||
AutoTLSBroker* = "registration.libp2p.direct"
|
||||
AutoTLSDNSServer* = "libp2p.direct"
|
||||
HttpOk* = 200
|
||||
HttpCreated* = 201
|
||||
|
||||
proc sampleChar*(ctx: var HmacDrbgContext, choices: string): char =
|
||||
## Samples a random character from the input string using the DRBG context
|
||||
if choices.len == 0:
|
||||
raise newException(ValueError, "Cannot sample from an empty string")
|
||||
var idx: uint32
|
||||
ctx.generate(idx)
|
||||
return choices[uint32(idx mod uint32(choices.len))]
|
||||
|
||||
proc base64UrlEncode*(data: seq[byte]): string =
|
||||
## Encodes data using base64url (RFC 4648 §5) — no padding, URL-safe
|
||||
var encoded = base64.encode(data, safe = true)
|
||||
encoded.removeSuffix("=")
|
||||
encoded.removeSuffix("=")
|
||||
return encoded
|
||||
|
||||
proc isPublicIPv4*(ip: IpAddress): bool =
|
||||
if ip.family != IpAddressFamily.IPv4:
|
||||
return false
|
||||
let ip = $ip
|
||||
return
|
||||
not (
|
||||
ip.startsWith("10.") or
|
||||
(ip.startsWith("172.") and parseInt(ip.split(".")[1]) in 16 .. 31) or
|
||||
ip.startsWith("192.168.") or ip.startsWith("127.") or ip.startsWith("169.254.")
|
||||
)
|
||||
|
||||
proc asMoment*(dt: DateTime): Moment =
|
||||
let unixTime: int64 = dt.toTime.toUnix
|
||||
return Moment.init(unixTime, Second)
|
||||
|
||||
proc encodePeerId*(peerId: PeerId): string {.raises: [AutoTLSError].} =
|
||||
var mh: MultiHash
|
||||
let decodeResult = MultiHash.decode(peerId.data, mh)
|
||||
if decodeResult.isErr or decodeResult.get() == -1:
|
||||
raise
|
||||
newException(AutoTLSError, "Failed to decode PeerId: invalid multihash format")
|
||||
|
||||
let cidResult = Cid.init(CIDv1, multiCodec("libp2p-key"), mh)
|
||||
if cidResult.isErr:
|
||||
raise newException(AutoTLSError, "Failed to initialize CID from multihash")
|
||||
|
||||
return Base36.encode(cidResult.get().data.buffer)
|
||||
|
||||
proc checkedParseJson*(bodyBytes: string): JsonNode {.raises: [ValueError].} =
|
||||
# This is so that we don't need to catch Exceptions directly
|
||||
# since we support 1.6.16 and parseJson before nim 2 didn't have explicit .raises. pragmas
|
||||
try:
|
||||
return parseJson(bodyBytes)
|
||||
except Exception as exc:
|
||||
raise newException(ValueError, "Error while parsing JSON", exc)
|
||||
|
||||
proc checkedGetPrimaryIPAddr*(): IpAddress {.raises: [GetPrimaryIPError].} =
|
||||
# This is so that we don't need to catch Exceptions directly
|
||||
# since we support 1.6.16 and getPrimaryIPAddr before nim 2 didn't have explicit .raises. pragmas
|
||||
try:
|
||||
return getPrimaryIPAddr()
|
||||
except Exception as exc:
|
||||
raise newException(GetPrimaryIPError, "Error while getting primary IP address", exc)
|
||||
|
||||
proc getJSONField*(node: JsonNode, field: string): JsonNode {.raises: [ACMEError].} =
|
||||
try:
|
||||
return node[field]
|
||||
except CatchableError:
|
||||
raise newException(ACMEError, "'" & field & "' field not found in JSON")
|
||||
|
||||
proc getParsedResponseBody*(
|
||||
response: HttpClientResponseRef
|
||||
): Future[JsonNode] {.async: (raises: [ACMEError]).} =
|
||||
try:
|
||||
let responseBody = bytesToString(await response.getBodyBytes()).checkedParseJson()
|
||||
return responseBody
|
||||
except ValueError as exc:
|
||||
raise newException(ACMEError, "Unable to parse JSON body", exc)
|
||||
except OSError as exc:
|
||||
raise newException(ACMEError, "Unable to parse JSON body", exc)
|
||||
except IOError as exc:
|
||||
raise newException(ACMEError, "Unable to parse JSON body", exc)
|
||||
except CatchableError as exc:
|
||||
raise
|
||||
newException(ACMEError, "Unexpected error occurred while getting body bytes", exc)
|
||||
|
||||
proc thumbprint*(key: KeyPair): string =
|
||||
doAssert key.seckey.scheme == PKScheme.RSA, "unsupported keytype"
|
||||
let pubkey = key.pubkey.rsakey
|
||||
let nArray = @(getArray(pubkey.buffer, pubkey.key.n, pubkey.key.nlen))
|
||||
let eArray = @(getArray(pubkey.buffer, pubkey.key.e, pubkey.key.elen))
|
||||
|
||||
let n = base64UrlEncode(nArray)
|
||||
let e = base64UrlEncode(eArray)
|
||||
let keyJson = %*{"e": e, "kty": "RSA", "n": n}
|
||||
let digest = sha256.digest($keyJson)
|
||||
return base64UrlEncode(@(digest.data))
|
||||
@@ -30,6 +30,8 @@ import
|
||||
connmanager,
|
||||
upgrademngrs/muxedupgrade,
|
||||
observedaddrmanager,
|
||||
autotls/acme,
|
||||
autotls/autotls,
|
||||
nameresolving/nameresolver,
|
||||
errors,
|
||||
utility
|
||||
@@ -63,6 +65,7 @@ type
|
||||
nameResolver: NameResolver
|
||||
peerStoreCapacity: Opt[int]
|
||||
autonat: bool
|
||||
autoTLSMgr: AutoTLSManager
|
||||
circuitRelay: Relay
|
||||
rdv: RendezVous
|
||||
services: seq[Service]
|
||||
@@ -184,6 +187,13 @@ proc withMemoryTransport*(b: SwitchBuilder): SwitchBuilder {.public.} =
|
||||
MemoryTransport.new(upgr)
|
||||
)
|
||||
|
||||
# proc withAutoTLSManager*(
|
||||
# b: SwitchBuilder, acmeServerURL = LetsEncryptURL, ipAddress = Opt.none(IpAddress)
|
||||
# ): SwitchBuilder {.public.} =
|
||||
# b.autoTLSMgr =
|
||||
# AutoTLSManager.new(b.rng, acmeServerURL = acmeServerURL, ipAddress = ipAddress)
|
||||
# b
|
||||
|
||||
proc withRng*(b: SwitchBuilder, rng: ref HmacDrbgContext): SwitchBuilder {.public.} =
|
||||
b.rng = rng
|
||||
b
|
||||
@@ -316,6 +326,7 @@ proc build*(b: SwitchBuilder): Switch {.raises: [LPError], public.} =
|
||||
secureManagers = secureManagerInstances,
|
||||
connManager = connManager,
|
||||
ms = ms,
|
||||
autoTLSMgr = b.autoTLSMgr,
|
||||
nameResolver = b.nameResolver,
|
||||
peerStore = peerStore,
|
||||
services = b.services,
|
||||
|
||||
@@ -20,6 +20,7 @@ import chronos, chronicles, metrics
|
||||
import
|
||||
stream/connection,
|
||||
transports/transport,
|
||||
autotls/autotls,
|
||||
upgrademngrs/upgrade,
|
||||
multistream,
|
||||
multiaddress,
|
||||
@@ -58,6 +59,7 @@ type
|
||||
acceptFuts: seq[Future[void]]
|
||||
dialer*: Dial
|
||||
peerStore*: PeerStore
|
||||
autoTLSMgr*: AutoTLSManager
|
||||
nameResolver*: NameResolver
|
||||
started: bool
|
||||
services*: seq[Service]
|
||||
@@ -331,6 +333,9 @@ proc stop*(s: Switch) {.public, async: (raises: [CancelledError]).} =
|
||||
except CatchableError as exc:
|
||||
warn "error cleaning up transports", description = exc.msg
|
||||
|
||||
if not s.autoTLSMgr.isNil:
|
||||
await s.autoTLSMgr.stop()
|
||||
|
||||
await s.ms.stop()
|
||||
|
||||
trace "Switch stopped"
|
||||
@@ -369,6 +374,8 @@ proc start*(s: Switch) {.public, async: (raises: [CancelledError, LPError]).} =
|
||||
|
||||
await s.peerInfo.update()
|
||||
await s.ms.start()
|
||||
if not s.autoTLSMgr.isNil:
|
||||
await s.autoTLSMgr.start(s.peerInfo)
|
||||
s.started = true
|
||||
|
||||
debug "Started libp2p node", peer = s.peerInfo
|
||||
@@ -380,6 +387,7 @@ proc newSwitch*(
|
||||
connManager: ConnManager,
|
||||
ms: MultistreamSelect,
|
||||
peerStore: PeerStore,
|
||||
autoTLSMgr: AutoTLSManager = nil,
|
||||
nameResolver: NameResolver = nil,
|
||||
services = newSeq[Service](),
|
||||
): Switch {.raises: [LPError].} =
|
||||
@@ -394,6 +402,7 @@ proc newSwitch*(
|
||||
peerStore: peerStore,
|
||||
dialer:
|
||||
Dialer.new(peerInfo.peerId, connManager, peerStore, transports, nameResolver),
|
||||
autoTLSMgr: autoTLSMgr,
|
||||
nameResolver: nameResolver,
|
||||
services: services,
|
||||
)
|
||||
|
||||
135
tests/testautotls.nim
Normal file
135
tests/testautotls.nim
Normal file
@@ -0,0 +1,135 @@
|
||||
{.used.}
|
||||
|
||||
# Nim-Libp2p
|
||||
# Copyright (c) 2023 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 std/[strformat, net] # uncomment after re-enabling AutoTLSManager
|
||||
import chronos
|
||||
import chronos/apps/http/httpclient
|
||||
import
|
||||
../libp2p/[
|
||||
stream/connection,
|
||||
transports/tcptransport,
|
||||
upgrademngrs/upgrade,
|
||||
multiaddress,
|
||||
switch,
|
||||
builders,
|
||||
# autotls/autotls, # uncomment after re-enabling AutoTLSManager
|
||||
autotls/acme,
|
||||
autotls/utils,
|
||||
# nameresolving/dnsresolver, # uncomment after re-enabling AutoTLSManager
|
||||
wire,
|
||||
]
|
||||
|
||||
import ./helpers
|
||||
|
||||
suite "AutoTLS":
|
||||
teardown:
|
||||
checkTrackers()
|
||||
|
||||
asyncTest "test ACME":
|
||||
let acc = await ACMEAccount.new(
|
||||
KeyPair.random(PKScheme.RSA, newRng()[]).get(),
|
||||
acmeServerURL = LetsEncryptURLStaging,
|
||||
)
|
||||
|
||||
await acc.register()
|
||||
# account was registered (kid set)
|
||||
check acc.kid.isSome
|
||||
|
||||
# challenge requested
|
||||
let (dns01Challenge, finalizeURL, orderURL) =
|
||||
await acc.requestChallenge(@["some.dummy.domain.com"])
|
||||
check dns01Challenge.isNil == false
|
||||
check finalizeURL.len > 0
|
||||
check orderURL.len > 0
|
||||
await noCancel(acc.session.closeWait())
|
||||
|
||||
# enable when withAutoTLSManager is re-enabled
|
||||
# asyncTest "test AutoTLSManager":
|
||||
# let switch = SwitchBuilder
|
||||
# .new()
|
||||
# .withRng(newRng())
|
||||
# .withAddress(MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet())
|
||||
# .withTcpTransport()
|
||||
# .withAutoTLSManager(acmeServerURL = LetsEncryptURLStaging)
|
||||
# .withYamux()
|
||||
# .withNoise()
|
||||
# .build()
|
||||
|
||||
# try:
|
||||
# let hostPrimaryIP = getPrimaryIPAddr()
|
||||
# if not isPublicIPv4(hostPrimaryIP):
|
||||
# skip() # host doesn't have public IPv4 address
|
||||
# return
|
||||
# except Exception:
|
||||
# skip() # can't get primary IPv4 address from host
|
||||
# return
|
||||
|
||||
# # this is so that we can check renewal afterwards
|
||||
# switch.autoTLSMgr.renewCheckTime = 3.seconds
|
||||
|
||||
# await switch.start()
|
||||
|
||||
# # wait for cert to be ready
|
||||
# await switch.autoTLSMgr.certReady.wait()
|
||||
# # clear since we'll use it again
|
||||
# switch.autoTLSMgr.certReady.clear()
|
||||
|
||||
# # challenge was sent (bearer token from peer id auth was set)
|
||||
# check switch.autoTLSMgr.bearerToken.isSome
|
||||
|
||||
# let dnsResolver = DnsResolver.new(
|
||||
# @[
|
||||
# initTAddress("1.1.1.1:53"),
|
||||
# initTAddress("1.0.0.1:53"),
|
||||
# initTAddress("[2606:4700:4700::1111]:53"),
|
||||
# ]
|
||||
# )
|
||||
# let base36PeerId = encodePeerId(switch.peerInfo.peerId)
|
||||
# let dnsTXTRecord = (
|
||||
# await dnsResolver.resolveTxt(
|
||||
# fmt"_acme-challenge.{base36PeerId}.{AutoTLSDNSServer}"
|
||||
# )
|
||||
# )[0]
|
||||
|
||||
# # DNS TXT record is set
|
||||
# let keyAuthorization = switch.autoTLSMgr.keyAuthorization.valueOr:
|
||||
# raiseAssert "keyAuthorization not found"
|
||||
# check dnsTXTRecord == keyAuthorization
|
||||
|
||||
# # certificate was downloaded and parsed
|
||||
# let cert = switch.autoTLSMgr.cert.valueOr:
|
||||
# raiseAssert "certificate not found"
|
||||
# let certBefore = cert
|
||||
|
||||
# # invalidate certificate
|
||||
# switch.autoTLSMgr.certExpiry = Opt.some(Moment.now - 2.hours)
|
||||
|
||||
# # cert was invalidated correctly
|
||||
# check switch.autoTLSMgr.certExpiry.get < Moment.now
|
||||
|
||||
# # wait for cert to be renewed
|
||||
# await switch.autoTLSMgr.certReady.wait()
|
||||
|
||||
# # certificate was indeed renewed
|
||||
# let certAfter = switch.autoTLSMgr.cert.valueOr:
|
||||
# raiseAssert "certificate not found"
|
||||
|
||||
# check certBefore != certAfter
|
||||
|
||||
# let certExpiry = switch.autoTLSMgr.certExpiry.valueOr:
|
||||
# raiseAssert "certificate expiry not found"
|
||||
|
||||
# # cert is valid
|
||||
# check certExpiry > Moment.now
|
||||
|
||||
# await switch.stop()
|
||||
@@ -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