6.6 KiB
libp2p AutoTLS
| Lifecycle Stage | Maturity | Status | Latest Revision |
|---|---|---|---|
| ?? | ?????????????? | Active | r0, 2025-04-30 |
Authors: @gmelodie
See the lifecycle document for context about the maturity level and spec status.
Table of Contents
Overview
Most modern web browsers only establish TLS connections with peers that present certificates issued by a recognized Certificate Authority (CA). Self-signed certificates are generally not accepted. To obtain a CA-issued certificate, a requester must complete an ACME (Automatic Certificate Management Environment) challenge. This typically involves provisioning a DNS TXT record on a domain the requester controls.
However, most libp2p peers do not own or control domain names, making it impractical for them to complete DNS-based ACME challenges and, by extension, to obtain trusted TLS certificates. This limitation hinders direct communication between libp2p peers and standard web browsers.
AutoTLS addresses this problem by introducing an AutoTLS broker — a server that controls a domain and facilitates ACME challenges on behalf of libp2p peers. A peer can request the AutoTLS broker to fulfill an ACME DNS challenge on its behalf. Once the broker sets the appropriate DNS record, the requesting peer proceeds to notify the ACME server. The ACME server validates the challenge against the broker's domain, and if successful, issues a valid certificate.
This mechanism allows libp2p peers to obtain CA-issued certificates without needing to possess or manage their own domain names.
General Flow
- Start libp2p client with public IPv4 (or IPv6) and support for
identifyprotocol (standard fornim-libp2p) - Get
PeerIDas a base36 of the CID of the multihash with thelibp2p-key(0x72) multicodec:- Transform PeerID into a multihash
mh - Transform
mhinto av1CID with the0x72multicodec (libp2p-key) - Base36 encode the
cid.data.buffer(not regular base36! this one needs multibase base36, which is the same as regular base36 but doesn't trims leading zeroes and starts with akorK) to getb36peerid
- Transform PeerID into a multihash
- Generate an RSA key
mykey - Register an account on the ACME server (production server for Let's Encrypt or just the staging server for testing)
- Send a GET request to the
directoryendpoint, and extract thenewAccountvalue from the JSON response, which will be the registration URL we'll use - Signed POST request to registration URL with the following
payload:{"termsOfServiceAgreed": true}. The actual POST body is signed using JWT with anmykeyandnonce(gotten fromdirectory["newNonce"]), so the final body of any ACME request should look like:Obs: the response to the account registration contains a{ "payload": token.claims.toBase64, "protected": token.header.toBase64, "signature": base64UrlEncode(token.signature), }kidin thelocationfield that should be saved and used in following requests to ACME server
- Send a GET request to the
- Request a certificate for the
*.{b36peerid}.libp2p.directdomain from the ACME server by issuing a POST request using the same JWT signature scheme (and another newnoncefromdirectory["newNonce"]) but withkidinstead ofjwkfield and the following payload:{ "type": "dns", "value": "*.{b36peerid}.libp2p.direct" } - From the ACME server response, get the entry with
"type"of"dns-01"and derive theKey Authorizationfor it:sha256.digest((dns01Challenge["token"] + "." + thumbprint(key))- JWK thumbprint:
base64encode(sha256.digest({"e": key.e, "kty": "RSA", "n": key.n})), but you can use other key types too
- JWK thumbprint:
- Send challenge to AutoTLS broker/server https://registration.libp2p.direct/, which requires a PeerID Auth scheme:
- Send GET request to the
v1/_acme-challengeendpoint and getwww-authenticatefield from the response header, and extract the values of three strings that it contains:challenge-client,public-keyandopaque - Generate random string with around 42 characters as a
challengeServerof our own - Get
peer-pubkeyandpeer-privkeykeys of our libp2ppeer, which are not necessarily the same keys we're using to talk to ACME server sig =(obs:varintis a protobuf varint field that encodes the length of thekey=valuestring)
sig = base64URL( peer-privkey.sign( bytes(varint + "challenge-client={challenge-client}") + bytes(varint + "hostname={hostname}") + bytes(varint + "server-public-key={publicKey}") ) )headers ={ "Content-Type": "application/json", "User-Agent": "nim-libp2p", "authorization": "libp2p-PeerID public-key=\"{clientPublicKeyB64}\", opaque=\"{opaque}\", challenge-server=\"{challengeServer}\", sig=\"{sig}\"" }- Send POST to
v1/_acme-challengeendpoint usingpayloadas body andheaders - Get the
bearertoken from theauthentication-infoheader of the response, which should be used for following requests from this client.
- Send GET request to the
- Check that the AutoTLS server has added the
_acme-challenge.{b36peerid}.libp2p.directTXTand thedashed-public-ip-address.{b36peerid}.libp2p.directADNS resource records. - Notify ACME server of challenge completion so it can lookup the DNS resource records.
- Get URL from
dns01challenge["url"] - Send an empty signed JSON payload (
{}) to the ACME server using thekidobtained from the ACME registration step and get the response from the server (completedResponse). - From
completedResponse, theurlfield from the JSON body byGETting it, again withkidsigning.
- Get URL from
- Wait for ACME server to finish testing the domain.
- The response from the polling will contain a
statusfield that will bependingwhile ACME is still testing the challenge, andvalidorinvalidwhen it's done.
- The response from the polling will contain a
- Download certificate from ACME server.