From 662a99a666be6719a78dab1c74cb0894fa183702 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Wed, 30 Apr 2025 11:18:10 -0300 Subject: [PATCH 1/9] add autoTLS spec --- tls/autotls.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tls/autotls.md diff --git a/tls/autotls.md b/tls/autotls.md new file mode 100644 index 0000000..170e9f1 --- /dev/null +++ b/tls/autotls.md @@ -0,0 +1,88 @@ +# libp2p AutoTLS + +| Lifecycle Stage | Maturity | Status | Latest Revision | +|-----------------|----------------|--------|-----------------| +| ?? | ?????????????? | Active | r0, 2025-04-30 | + +Authors: [@gmelodie] + +Interest Group: [@??], [@???] + +[@gmelodie]: https://github.com/gmelodie +[@??]: https://github.com/Stebalien +[@???]: https://github.com/jacobheun + + +See the [lifecycle document][lifecycle-spec] for context about the maturity level +and spec status. + +[lifecycle-spec]: https://github.com/libp2p/specs/blob/master/00-framework-01-spec-lifecycle.md + +## Table of Contents + +- [Introduction](#introduction) +- [General Flow](#general-flow) + +## Introduction +TODO + +## General Flow +1. Start libp2p client with public IPv4 (or IPv6) and support for `identify` protocol (standard for `nim-libp2p`) +2. Get `PeerID` as a base36 of the CID of the multihash with the `libp2p-key` (`0x72`) multicodec: + 1. Transform PeerID into a multihash `mh` + 2. Transform `mh` into a `v1` CID with the `0x72` multicodec (`libp2p-key`) + 3. Base36 encode the `cid.data.buffer` (not regular base36! this one needs [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 but doesn't trims leading zeroes and starts with a `k` or `K`) to get `b36peerid` +3. Generate an RSA key `mykey` +4. Register an account on the ACME server ([production server for Let's Encrypt](https://acme-v02.api.letsencrypt.org) or just the [staging server](https://acme-staging-v02.api.letsencrypt.org) for testing) + 1. Send a GET request to the `directory` endpoint, and extract the `newAccount` value from the JSON response, which will be the registration URL we'll use + 2. Signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}`. The actual POST body is signed using JWT with an `mykey` and `nonce` (gotten from `directory["newNonce"]`), so the final body of any ACME request should look like: + ```json + { + "payload": token.claims.toBase64, + "protected": token.header.toBase64, + "signature": base64UrlEncode(token.signature), + } + ``` + Obs: the response to the account registration contains a `kid` in the `location` field that should be saved and used in following requests to ACME server +5. Request a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and another new `nonce` from `directory["newNonce"]`) but with `kid` instead of `jwk` field and the following payload: + ```json + { + "type": "dns", + "value": "*.{b36peerid}.libp2p.direct" + } + ``` +6. From the ACME server response, get the entry with `"type"` of `"dns-01"` and derive the `Key Authorization` for it: + - `sha256.digest((dns01Challenge["token"] + "." + thumbprint(key))` + - [JWK thumbprint](https://www.rfc-editor.org/rfc/rfc7638): `base64encode(sha256.digest({"e": key.e, "kty": "RSA", "n": key.n}))`, but you can use other key types too +7. Send challenge to AutoTLS broker/server https://registration.libp2p.direct/, which requires a [PeerID Auth](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) scheme: + 1. Send GET request to the `v1/_acme-challenge` endpoint and get `www-authenticate` field from the response header, and extract the values of three strings that it contains: `challenge-client`, `public-key` and `opaque` + 2. Generate random string with around 42 characters as a `challengeServer` of our own + 3. Get `peer-pubkey` and `peer-privkey` keys of our libp2p `peer`, which are not necessarily the same keys we're using to talk to ACME server + 4. `sig = ` (obs: `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of the `key=value` string) + ``` + sig = base64URL( + peer-privkey.sign( + bytes(varint + "challenge-client={challenge-client}") + + bytes(varint + "hostname={hostname}") + + bytes(varint + "server-public-key={publicKey}") + ) + ) + ``` + 5. `headers =` + ```json + { + "Content-Type": "application/json", + "User-Agent": "nim-libp2p", + "authorization": "libp2p-PeerID public-key=\"{clientPublicKeyB64}\", opaque=\"{opaque}\", challenge-server=\"{challengeServer}\", sig=\"{sig}\"" + } + ``` + 6. Send POST to `v1/_acme-challenge` endpoint using `payload` as body and `headers` + 7. Get the `bearer` token from the `authentication-info` header of the response, which should be used for following requests from this client. +8. Check that the AutoTLS server has added the `_acme-challenge.{b36peerid}.libp2p.direct` `TXT` and the `dashed-public-ip-address.{b36peerid}.libp2p.direct` `A` DNS resource records. +9. Notify ACME server of challenge completion so it can lookup the DNS resource records. + 1. Get URL from `dns01challenge["url"]` + 2. Send an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the ACME registration step and get the response from the server (`completedResponse`). + 3. From `completedResponse`, the `url` field from the JSON body by `GET`ting it, again with `kid` signing. +10. Wait for ACME server to finish testing the domain. + - The response from the polling will contain a `status` field that will be `pending` while ACME is still testing the challenge, and `valid` or `invalid` when it's done. +11. Download certificate from ACME server. From fed5332b912bb2db943d836536e5d23f5a79128d Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Wed, 21 May 2025 12:15:46 -0300 Subject: [PATCH 2/9] add overview section --- tls/autotls.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tls/autotls.md b/tls/autotls.md index 170e9f1..bab8d1c 100644 --- a/tls/autotls.md +++ b/tls/autotls.md @@ -23,8 +23,14 @@ and spec status. - [Introduction](#introduction) - [General Flow](#general-flow) -## Introduction -TODO +## 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 1. Start libp2p client with public IPv4 (or IPv6) and support for `identify` protocol (standard for `nim-libp2p`) From f28b24606c84851a13e2039447d39caadc2c4f17 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Wed, 21 May 2025 12:52:26 -0300 Subject: [PATCH 3/9] fix minor imprecisions --- tls/autotls.md | 65 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/tls/autotls.md b/tls/autotls.md index bab8d1c..dacb534 100644 --- a/tls/autotls.md +++ b/tls/autotls.md @@ -33,38 +33,63 @@ AutoTLS addresses this problem by introducing an AutoTLS broker — a server tha This mechanism allows libp2p peers to obtain CA-issued certificates without needing to possess or manage their own domain names. ## General Flow -1. Start libp2p client with public IPv4 (or IPv6) and support for `identify` protocol (standard for `nim-libp2p`) -2. Get `PeerID` as a base36 of the CID of the multihash with the `libp2p-key` (`0x72`) multicodec: +1. Start libp2p client with public IPv4 (or IPv6) and support for `identify` protocol +2. Get `PeerID` as a base36 of the CID of the multihash with the `libp2p-key` (`0x72`) multicodec: 1. Transform PeerID into a multihash `mh` - 2. Transform `mh` into a `v1` CID with the `0x72` multicodec (`libp2p-key`) - 3. Base36 encode the `cid.data.buffer` (not regular base36! this one needs [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 but doesn't trims leading zeroes and starts with a `k` or `K`) to get `b36peerid` -3. Generate an RSA key `mykey` -4. Register an account on the ACME server ([production server for Let's Encrypt](https://acme-v02.api.letsencrypt.org) or just the [staging server](https://acme-staging-v02.api.letsencrypt.org) for testing) + 2. Transform `mh` into a CIDv1 with the `libp2p-key` multicodec (which is the `0x72` multicodec) + 3. Encode `cid.data.buffer` to [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 but does not trim leading zeroes and starts either with `k` or `K`) to get `b36peerid` +3. Generate a key as specified in [RFC7518](https://www.rfc-editor.org/rfc/rfc7518#section-6), here we'll use an RSA key `myrsakey` +4. Register an account on the ACME server ([production server for Let's Encrypt](https://acme-v02.api.letsencrypt.org) or just the [staging server](https://acme-staging-v02.api.letsencrypt.org) for testing, but any other ACME server would work) 1. Send a GET request to the `directory` endpoint, and extract the `newAccount` value from the JSON response, which will be the registration URL we'll use - 2. Signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}`. The actual POST body is signed using JWT with an `mykey` and `nonce` (gotten from `directory["newNonce"]`), so the final body of any ACME request should look like: + 2. Send JWT-signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}` (a `contact` field containing a list of `mailto:bob@example.org` contact information strings can also be optionally specified in the payload). The POST body is signed using JWT with `myrsakey` and `nonce` (`nonce` is a number returned by GETting the ACME server at the URL specified in `directory["newNonce"]`). The JSON payload before JWT-signing should look like: ```json { - "payload": token.claims.toBase64, - "protected": token.header.toBase64, - "signature": base64UrlEncode(token.signature), + "header": { + "alg": "RS256", + "typ": "JWT", + "nonce": "`nonce`", + "url": "`url`", + "jwk": { + "kty": "RSA", + "n": "`myrsakey.n`", + "e": "`myrsakey.e`" + } + }, + "claims": { + "payload": { + "termsOfServiceAgreed": true, + "contact": [ + "mailto:alice@example.com", + "mailto:bob@example.com" + ] + } + } } ``` - Obs: the response to the account registration contains a `kid` in the `location` field that should be saved and used in following requests to ACME server -5. Request a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and another new `nonce` from `directory["newNonce"]`) but with `kid` instead of `jwk` field and the following payload: + The final body of any ACME request should look like: + ```json + { + "payload": "`token.claims.toBase64`", + "protected": "`token.header.toBase64`", + "signature": "`base64UrlEncode(token.signature)`" + } + ``` + Obs: the response to the account registration contains `kid` string in the `location` header that SHOULD be saved and used in following requests to ACME server +5. Request a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and a new `nonce`) but with `kid` instead of `jwk` field and the following payload: ```json { "type": "dns", "value": "*.{b36peerid}.libp2p.direct" } ``` -6. From the ACME server response, get the entry with `"type"` of `"dns-01"` and derive the `Key Authorization` for it: - - `sha256.digest((dns01Challenge["token"] + "." + thumbprint(key))` - - [JWK thumbprint](https://www.rfc-editor.org/rfc/rfc7638): `base64encode(sha256.digest({"e": key.e, "kty": "RSA", "n": key.n}))`, but you can use other key types too -7. Send challenge to AutoTLS broker/server https://registration.libp2p.direct/, which requires a [PeerID Auth](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) scheme: - 1. Send GET request to the `v1/_acme-challenge` endpoint and get `www-authenticate` field from the response header, and extract the values of three strings that it contains: `challenge-client`, `public-key` and `opaque` - 2. Generate random string with around 42 characters as a `challengeServer` of our own - 3. Get `peer-pubkey` and `peer-privkey` keys of our libp2p `peer`, which are not necessarily the same keys we're using to talk to ACME server - 4. `sig = ` (obs: `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of the `key=value` string) +6. From the ACME server response, get the entry with `"type"` of `"dns-01"` (called `dns01Challenge` here) and derive the `Key Authorization` for it: + - `sha256.digest((dns01Challenge["token"] + "." + thumbprint(myrsakey))` + - [JWK thumbprint](https://www.rfc-editor.org/rfc/rfc7638): `base64encode(sha256.digest({"e": myrsakey.e, "kty": "RSA", "n": myrsakey.n}))` +7. Send challenge to AutoTLS broker (e.g. `registration.libp2p.direct`). This requires a [PeerID Authentication](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md): + 1. Send GET request to the AutoTLS broker's `v1/_acme-challenge` endpoint and get `www-authenticate` header from the response. Extract the values of three substrings that `www-authenticate` contains: `challenge-client`, `public-key` and `opaque` + 2. Generate random string with around 42 characters to be sent as a `challengeServer` + 3. Get the private key of the requesting libp2p peer as `peer-privkey`. This is not necessarily the same key used to communicate with ACME server + 4. `sig = ` (obs: `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string) ``` sig = base64URL( peer-privkey.sign( From cbce849991cc4680cb56278105907bf090d9879c Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Wed, 21 May 2025 15:44:24 -0300 Subject: [PATCH 4/9] address wording, format --- tls/autotls.md | 123 +++++++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 54 deletions(-) diff --git a/tls/autotls.md b/tls/autotls.md index dacb534..d06017b 100644 --- a/tls/autotls.md +++ b/tls/autotls.md @@ -1,24 +1,16 @@ -# libp2p AutoTLS +# libp2p AutoTLS | Lifecycle Stage | Maturity | Status | Latest Revision | |-----------------|----------------|--------|-----------------| -| ?? | ?????????????? | Active | r0, 2025-04-30 | +| 1A | Working Draft | Active | r1, 2025-05-21 | -Authors: [@gmelodie] +Authors: @gmelodie -Interest Group: [@??], [@???] +Interest Group: TBD [@gmelodie]: https://github.com/gmelodie -[@??]: https://github.com/Stebalien -[@???]: https://github.com/jacobheun - -See the [lifecycle document][lifecycle-spec] for context about the maturity level -and spec status. - -[lifecycle-spec]: https://github.com/libp2p/specs/blob/master/00-framework-01-spec-lifecycle.md - -## Table of Contents +## Table of Contents - [Introduction](#introduction) - [General Flow](#general-flow) @@ -28,20 +20,33 @@ Most modern web browsers only establish TLS connections with peers that present 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. +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 fulfil 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 -1. Start libp2p client with public IPv4 (or IPv6) and support for `identify` protocol -2. Get `PeerID` as a base36 of the CID of the multihash with the `libp2p-key` (`0x72`) multicodec: - 1. Transform PeerID into a multihash `mh` - 2. Transform `mh` into a CIDv1 with the `libp2p-key` multicodec (which is the `0x72` multicodec) - 3. Encode `cid.data.buffer` to [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 but does not trim leading zeroes and starts either with `k` or `K`) to get `b36peerid` -3. Generate a key as specified in [RFC7518](https://www.rfc-editor.org/rfc/rfc7518#section-6), here we'll use an RSA key `myrsakey` -4. Register an account on the ACME server ([production server for Let's Encrypt](https://acme-v02.api.letsencrypt.org) or just the [staging server](https://acme-staging-v02.api.letsencrypt.org) for testing, but any other ACME server would work) - 1. Send a GET request to the `directory` endpoint, and extract the `newAccount` value from the JSON response, which will be the registration URL we'll use - 2. Send JWT-signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}` (a `contact` field containing a list of `mailto:bob@example.org` contact information strings can also be optionally specified in the payload). The POST body is signed using JWT with `myrsakey` and `nonce` (`nonce` is a number returned by GETting the ACME server at the URL specified in `directory["newNonce"]`). The JSON payload before JWT-signing should look like: +The following is the general flow of a successful certificate request and subsequent issuance using AutoTLS. Here, "client" refers to the machine running a libp2p peer and requesting the challenge, while "broker" and "AutoTLS broker", which are used interchangeably, is the server that will fulfil the ACME challenge on behalf of the client. + +1. Client requests a challenge from the ACME server. +2. Client sends the challenge to the broker. +3. Broker tests client and sets DNS record (fulfilling challenge). +4. Client waits until the broker fulfils the challenge. +5. Client signals to ACME server that challenge is fulfilled. +6. ACME server checks challenge in broker. +7. Client finalizes certificate request (creates and sends CSR to ACME server). +8. Client waits until certificate is ready for download. +9. Client downloads certificate. + +## Requesting challenge from ACME server +1. The client starts a libp2p peer with public IPv4 and support for `identify` protocol. +2. The client encodes its `PeerID` as [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md) of the CIDv1 of the multihash with the `libp2p-key` (`0x72`) multicodec: + 1. Transform PeerID into a multihash `mh`. + 2. Encode `mh` using [CIDv1](https://github.com/multiformats/cid?tab=readme-ov-file#cidv1) with the `libp2p-key` [multicodec](https://github.com/multiformats/multicodec)(`0x72`). + 3. Encode the CID data (if `cid` is the CID, then `cid.data.buffer` should be encoded) using [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 without trimming leading zeroes and including a leading `k` or `K`) to get `b36peerid`. +3. The client generates a key `mykey` as specified in [RFC7518](https://www.rfc-editor.org/rfc/rfc7518#section-6). +4. The client registers an account on the ACME server (e.g. [production](https://acme-v02.api.letsencrypt.org) or [staging](https://acme-staging-v02.api.letsencrypt.org) servers for Let's Encrypt). + 1. Send a GET request to the `/directory` endpoint of the ACME server, and extract the `newAccount` value from the JSON response, which will be the registration URL we'll use. + 2. Send [JWT](https://www.rfc-editor.org/rfc/rfc7519)-signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}` (a `contact` field containing a list of `mailto:bob@example.org` contact information strings can also be optionally specified in the payload). The POST body is signed using JWT with `mykey` and `nonce` (`nonce` is a number returned by sending a GET request to the ACME server at the URL specified in `directory["newNonce"]`). The JSON payload using an RSA-256 key before JWT-signing should look like: ```json { "header": { @@ -51,8 +56,8 @@ This mechanism allows libp2p peers to obtain CA-issued certificates without need "url": "`url`", "jwk": { "kty": "RSA", - "n": "`myrsakey.n`", - "e": "`myrsakey.e`" + "n": "`mykey.n`", + "e": "`mykey.e`" } }, "claims": { @@ -69,51 +74,61 @@ This mechanism allows libp2p peers to obtain CA-issued certificates without need The final body of any ACME request should look like: ```json { - "payload": "`token.claims.toBase64`", - "protected": "`token.header.toBase64`", - "signature": "`base64UrlEncode(token.signature)`" + "payload": "`claims.toBase64`", + "protected": "`header.toBase64`", + "signature": "`base64UrlEncode(signature)`" } ``` - Obs: the response to the account registration contains `kid` string in the `location` header that SHOULD be saved and used in following requests to ACME server -5. Request a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and a new `nonce`) but with `kid` instead of `jwk` field and the following payload: +5. The client MUST save the `kid` present in the `location` header of the ACME server's response for in future requests to ACME server. +6. The client requests a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and a new `nonce`) but using the `kid` field instead of the `jwk` field and containing the following JSON payload: ```json { "type": "dns", "value": "*.{b36peerid}.libp2p.direct" } ``` -6. From the ACME server response, get the entry with `"type"` of `"dns-01"` (called `dns01Challenge` here) and derive the `Key Authorization` for it: - - `sha256.digest((dns01Challenge["token"] + "." + thumbprint(myrsakey))` - - [JWK thumbprint](https://www.rfc-editor.org/rfc/rfc7638): `base64encode(sha256.digest({"e": myrsakey.e, "kty": "RSA", "n": myrsakey.n}))` -7. Send challenge to AutoTLS broker (e.g. `registration.libp2p.direct`). This requires a [PeerID Authentication](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md): - 1. Send GET request to the AutoTLS broker's `v1/_acme-challenge` endpoint and get `www-authenticate` header from the response. Extract the values of three substrings that `www-authenticate` contains: `challenge-client`, `public-key` and `opaque` - 2. Generate random string with around 42 characters to be sent as a `challengeServer` - 3. Get the private key of the requesting libp2p peer as `peer-privkey`. This is not necessarily the same key used to communicate with ACME server - 4. `sig = ` (obs: `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string) +7. From the ACME server response, the client MUST save the entry with `"type"` of `"dns-01"` and derive the [`Key Authorization`](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1) from that. + +## Sending challenge to AutoTLS broker +1. The client sends the `key authorization` to the AutoTLS broker (e.g. `registration.libp2p.direct`). This requires a [PeerID Authentication](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) between client and broker: + 1. Client sends GET request to the AutoTLS broker's `/v1/_acme-challenge` endpoint and extracts `challenge-client`, `public-key` and `opaque` from the `www-authenticate` response header. + 2. Client generates 32-character-long random string to be sent as a `challengeServer`. At the time of writing the PeerID Authentication specification does not contain recommendations about challenge length, but the official [`go-libp2p` implementation uses 32 characters](https://github.com/libp2p/go-libp2p/blob/master/p2p/http/auth/internal/handshake/handshake.go#L21). + 3. Client generates `sig`, `headers` and `payload` as follows, where `peer-privkey` is the private key of the client's libp2p peer and `multiaddrs` is a list of string representations of the libp2p peer's multiaddresses: ``` sig = base64URL( peer-privkey.sign( bytes(varint + "challenge-client={challenge-client}") + bytes(varint + "hostname={hostname}") + - bytes(varint + "server-public-key={publicKey}") + bytes(varint + "server-public-key={public-key}") ) ) - ``` - 5. `headers =` - ```json - { + + headers = { "Content-Type": "application/json", - "User-Agent": "nim-libp2p", + "User-Agent": "some-user-agent", "authorization": "libp2p-PeerID public-key=\"{clientPublicKeyB64}\", opaque=\"{opaque}\", challenge-server=\"{challengeServer}\", sig=\"{sig}\"" } - ``` - 6. Send POST to `v1/_acme-challenge` endpoint using `payload` as body and `headers` - 7. Get the `bearer` token from the `authentication-info` header of the response, which should be used for following requests from this client. -8. Check that the AutoTLS server has added the `_acme-challenge.{b36peerid}.libp2p.direct` `TXT` and the `dashed-public-ip-address.{b36peerid}.libp2p.direct` `A` DNS resource records. -9. Notify ACME server of challenge completion so it can lookup the DNS resource records. - 1. Get URL from `dns01challenge["url"]` - 2. Send an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the ACME registration step and get the response from the server (`completedResponse`). - 3. From `completedResponse`, the `url` field from the JSON body by `GET`ting it, again with `kid` signing. -10. Wait for ACME server to finish testing the domain. - - The response from the polling will contain a `status` field that will be `pending` while ACME is still testing the challenge, and `valid` or `invalid` when it's done. + + payload = { + "value": keyAuthorization, + "addresses": multiaddrs + } + ``` + **Note:** `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string. + **Note:** the AutoTLS broker MUST NOT dial multiaddresses containing private IPv4 addresses, thus the client SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. + 4. Client sends a POST request to `/v1/_acme-challenge` endpoint using `payload` as body and `headers` as headers. + 6. Client SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. +3. Client SHOULD query DNS records (`TXT _acme-challenge.{b36peerid}.libp2p.direct` and `A dashed-public-ip-address.{b36peerid}.libp2p.direct`) until they are set by the AutoTLS broker. +4. Client notifies the ACME server about challenge completion so that the ACME server can lookup the DNS resource records. The notification is done in the form of a POST request with an empty JSON payload (`{}`) as body sent to the `url` field returned by the ACME server when it responded to client's initial challenge request. + 1. Client sends an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the initial ACME registration and gets the response from the server (`completedResponse`). + 2. Client extracts `url` field from `completedResponse`'s JSON body ting it, again with `kid` signing. The extracted URL is named `checkUrl` in this document. +5. The client polls the ACME server by sending an empty bodied, `kid` signed GET request to `checkUrl` until it receives a response with `status: valid` or `status: invalid` field, meaning that the challenge checking was successful or not, respectively. + +## Signalling challenge completion to ACME server 11. Download certificate from ACME server. +## Finalizing challenge request +### CSR generation +## Downloading certificate from ACME server + + +## Complete certificate issuance example From 6c713580b2fbc1aa383df671886c0eb3b6e1c9b8 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Thu, 22 May 2025 10:28:32 -0300 Subject: [PATCH 5/9] finalized clarifications --- tls/autotls-client.md | 166 ++++++++++++++++++++++++++++++++++++++++++ tls/autotls.md | 134 ---------------------------------- 2 files changed, 166 insertions(+), 134 deletions(-) create mode 100644 tls/autotls-client.md delete mode 100644 tls/autotls.md diff --git a/tls/autotls-client.md b/tls/autotls-client.md new file mode 100644 index 0000000..187a2c9 --- /dev/null +++ b/tls/autotls-client.md @@ -0,0 +1,166 @@ +# libp2p AutoTLS Client + +| Lifecycle Stage | Maturity | Status | Latest Revision | +|-----------------|----------------|--------|-----------------| +| 1A | Working Draft | Active | r1, 2025-05-21 | + +Authors: @gmelodie + +Interest Group: TBD + +[@gmelodie]: https://github.com/gmelodie + +## Table of Contents + +- [Overview](#overview) +- [General Flow](#general-flow) +- [Requesting challenge from ACME server](#requesting-challenge-from-acme-server) +- [Sending challenge to AutoTLS broker](#sending-challenge-to-autotls-broker) +- [Signalling challenge completion to ACME server](#signalling-challenge-completion-to-acme-server) +- [Downloading certificate](#downloading-certificate) +- [Complete certificate issuance example](#complete-certificate-issuance-example) +- [References](#references) + + + +## 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 fulfil 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 +The following is the general flow of a successful certificate request and subsequent issuance using AutoTLS. Here, "node" refers to the machine running a libp2p peer and requesting the challenge, while "broker" and "AutoTLS broker", which are used interchangeably, is the server that will fulfil the ACME challenge on behalf of the node. + +1. Node requests a challenge from the ACME server. +2. Node sends the challenge to the broker. +3. Broker tests node and sets DNS record (fulfilling challenge). +4. Node queries DNS until it sees that the broker has fulfilled the challenge. +5. Node signals to ACME server that challenge is fulfilled. +6. ACME server checks challenge in broker. +7. Node sends CSR to finalize certificate request. +8. Node polls ACME server until certificate is ready for download. +9. Node downloads certificate. + +## Requesting challenge from ACME server +1. The node starts a libp2p peer with public IPv4 and support for the [`identify`](https://github.com/libp2p/specs/blob/master/identify/README.md) protocol. +2. The node encodes its `PeerID` as [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md) of the CIDv1 of the multihash with the `libp2p-key` (`0x72`) multicodec: + 1. Transform PeerID into a multihash `mh`. + 2. Encode `mh` using [CIDv1](https://github.com/multiformats/cid?tab=readme-ov-file#cidv1) with the `libp2p-key` [multicodec](https://github.com/multiformats/multicodec)(`0x72`). + 3. Encode the CID data using [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 without trimming leading zeroes and including a leading `k` or `K`) to get `b36peerid`. + **Note:** "CID data" are the raw bytes that compose the CID, not richer CID objects that contain more information. +3. The node generates a key `mykey` as specified in [RFC7518](https://www.rfc-editor.org/rfc/rfc7518#section-6). +4. The node registers an account on the ACME server (e.g. [production](https://acme-v02.api.letsencrypt.org) or [staging](https://acme-staging-v02.api.letsencrypt.org) servers for Let's Encrypt). + 1. Send a GET request to the `/directory` endpoint of the ACME server, and extract the `newAccount` value from the JSON response, which will be the registration URL we'll use. + 2. Send [JWT](https://www.rfc-editor.org/rfc/rfc7519)-signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}` (a `contact` field containing a list of `mailto:bob@example.org` contact information strings can also be optionally specified in the payload). The POST body is signed using JWT with `mykey` and `nonce` (`nonce` is a number returned by sending a GET request to the ACME server at the URL specified in `directory["newNonce"]`). The JSON payload using an RSA-256 key before JWT-signing should look like: + ```json + { + "header": { + "alg": "RS256", + "typ": "JWT", + "nonce": "`nonce`", + "url": "`url`", + "jwk": { + "kty": "RSA", + "n": "`mykey.n`", + "e": "`mykey.e`" + } + }, + "claims": { + "payload": { + "termsOfServiceAgreed": true, + "contact": [ + "mailto:alice@example.com", + "mailto:bob@example.com" + ] + } + } + } + ``` + The final body of any ACME request should look like: + ```json + { + "payload": "`claims.toBase64`", + "protected": "`header.toBase64`", + "signature": "`base64UrlEncode(signature)`" + } + ``` +5. The node MUST save the `kid` present in the `location` header of the ACME server's response for in future requests to ACME server. +6. The node requests a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and a new `nonce`) but using the `kid` field instead of the `jwk` field and containing the following JSON payload: + ```json + { + "identifiers": [ + { + "type": "dns", + "value": "*.{b36peerid}.libp2p.direct" + } + ] + } + ``` +7. From the ACME server response, the node MUST save the entry with `type` of `dns-01` and derive [`keyAuthorization`](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1) from that. +8. From the ACME server response's `dns-01` field, the node MUST also save the value on the `url` field of the JSON body, here called `chalUrl`. This is used in the ACME signalling phase. +9. From the ACME server response's, the node MUST also save the value on the `location` header, here called `orderUrl`. This is used in the ACME signalling phase. +10. From the ACME server response's, the node MUST also save the value on the `finalize` field of the JSON body, here called `finalizeUrl`. This is used in the ACME signalling phase. + + + + +## Sending challenge to AutoTLS broker +1. The node sends `keyAuthorization` to the AutoTLS broker (e.g. `registration.libp2p.direct`). This requires a [peer ID authentication](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) between node and broker: + 1. Node sends GET request to the AutoTLS broker's `/v1/_acme-challenge` endpoint and extracts `challenge-node`, `public-key` and `opaque` from the `www-authenticate` response header. + 2. Node generates 32-character-long random string to be sent as a `challengeServer`. + **Note:** At the time of writing the PeerID Authentication specification does not contain recommendations about challenge length, but the official [`go-libp2p` implementation uses 32 characters](https://github.com/libp2p/go-libp2p/blob/master/p2p/http/auth/internal/handshake/handshake.go#L21). + 3. Node generates `sig`, `headers` and `payload` as follows, where `peer-privkey` is the private key of the node's libp2p peer and `multiaddrs` is a list of string representations of the libp2p peer's multiaddresses: + ``` + sig = base64URL( + peer-privkey.sign( + bytes(varint + "challenge-node={challenge-node}") + + bytes(varint + "hostname={hostname}") + + bytes(varint + "server-public-key={public-key}") + ) + ) + + headers = { + "Content-Type": "application/json", + "User-Agent": "some-user-agent", + "authorization": "libp2p-PeerID public-key=\"{nodePublicKeyB64}\", opaque=\"{opaque}\", challenge-server=\"{challengeServer}\", sig=\"{sig}\"" + } + + payload = { + "value": keyAuthorization, + "addresses": multiaddrs + } + ``` + **Note:** `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string. + **Note:** the AutoTLS broker MUST NOT dial multiaddresses containing private IPv4 addresses. The node SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. + 4. Node sends a POST request to `/v1/_acme-challenge` endpoint using `payload` as HTTP body and `headers` as HTTP headers. + 6. Node SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. + + + +## Signalling challenge completion to ACME server +1. Node SHOULD query DNS records (`TXT _acme-challenge.{b36peerid}.libp2p.direct` and `A dashed-public-ip-address.{b36peerid}.libp2p.direct`) until they are set by the AutoTLS broker. +**Note:** here, `dashed-public-ip-address` is the public IPv4 address of the node in which the node received the confirmation dial from the broker. For example, if the node has two public IPv4 addresses `1.1.1.1` and `8.8.8.8`, and the broker dialed it through `1.1.1.1`, then the node SHOULD query the `A 1-1-1-1.{b36peerid}.libp2p.direct`. +2. Node notifies the ACME server about challenge completion so that the ACME server can lookup the DNS resource records that the AutoTLS broker has set. The notification is done in the form of a POST request to `chalUrl` with an empty HTTP body (`{}`). + 1. Node sends an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the initial ACME registration and gets the response from the server (`completedResponse`). + 2. Node extracts `url` field from `completedResponse`'s JSON body ting it, again with `kid` signing. The extracted URL is named `checkUrl` in this document. +3. The node polls the ACME server by sending a GET HTTP request to `checkUrl` with an empty body, and sign using the `kid` of the registered account. The node MUST poll the ACME server until it receives a response with `status: valid` or `status: invalid` field, meaning that the challenge checking was successful or not, respectively. + + + +## Downloading certificate +1. Node finalizes the certificate request: + 1. Generate CSR for the `*.{b36peerid}.libp2p.direct` domain. + 2. Encode the CSR with URL safe base 64 (`b64CSR`). + 3. Send a `kid` signed POST request to `finalizeUrl` with JSON HTTP body of `{"csr": b64CSR}`. +2. Node MUST poll ACME server by sending GET requests to `orderUrl` until the ACME server's response contains a `status` field with a value different than `processing`. +3. Node downloads finalized certificate by sending a GET request to `certDownloadUrl`. `certDownloadUrl` is found in the `certificate` field of the JSON HTTP body of a response to a GET request to `orderUrl`. + + +## Complete certificate issuance example + +## References +- [Announcing AutoTLS: Bridging the Gap Between libp2p and the Web](https://blog.libp2p.io/autotls/) diff --git a/tls/autotls.md b/tls/autotls.md deleted file mode 100644 index d06017b..0000000 --- a/tls/autotls.md +++ /dev/null @@ -1,134 +0,0 @@ -# libp2p AutoTLS - -| Lifecycle Stage | Maturity | Status | Latest Revision | -|-----------------|----------------|--------|-----------------| -| 1A | Working Draft | Active | r1, 2025-05-21 | - -Authors: @gmelodie - -Interest Group: TBD - -[@gmelodie]: https://github.com/gmelodie - -## Table of Contents - -- [Introduction](#introduction) -- [General Flow](#general-flow) - -## 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 fulfil 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 -The following is the general flow of a successful certificate request and subsequent issuance using AutoTLS. Here, "client" refers to the machine running a libp2p peer and requesting the challenge, while "broker" and "AutoTLS broker", which are used interchangeably, is the server that will fulfil the ACME challenge on behalf of the client. - -1. Client requests a challenge from the ACME server. -2. Client sends the challenge to the broker. -3. Broker tests client and sets DNS record (fulfilling challenge). -4. Client waits until the broker fulfils the challenge. -5. Client signals to ACME server that challenge is fulfilled. -6. ACME server checks challenge in broker. -7. Client finalizes certificate request (creates and sends CSR to ACME server). -8. Client waits until certificate is ready for download. -9. Client downloads certificate. - -## Requesting challenge from ACME server -1. The client starts a libp2p peer with public IPv4 and support for `identify` protocol. -2. The client encodes its `PeerID` as [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md) of the CIDv1 of the multihash with the `libp2p-key` (`0x72`) multicodec: - 1. Transform PeerID into a multihash `mh`. - 2. Encode `mh` using [CIDv1](https://github.com/multiformats/cid?tab=readme-ov-file#cidv1) with the `libp2p-key` [multicodec](https://github.com/multiformats/multicodec)(`0x72`). - 3. Encode the CID data (if `cid` is the CID, then `cid.data.buffer` should be encoded) using [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 without trimming leading zeroes and including a leading `k` or `K`) to get `b36peerid`. -3. The client generates a key `mykey` as specified in [RFC7518](https://www.rfc-editor.org/rfc/rfc7518#section-6). -4. The client registers an account on the ACME server (e.g. [production](https://acme-v02.api.letsencrypt.org) or [staging](https://acme-staging-v02.api.letsencrypt.org) servers for Let's Encrypt). - 1. Send a GET request to the `/directory` endpoint of the ACME server, and extract the `newAccount` value from the JSON response, which will be the registration URL we'll use. - 2. Send [JWT](https://www.rfc-editor.org/rfc/rfc7519)-signed POST request to registration URL with the following `payload`: `{"termsOfServiceAgreed": true}` (a `contact` field containing a list of `mailto:bob@example.org` contact information strings can also be optionally specified in the payload). The POST body is signed using JWT with `mykey` and `nonce` (`nonce` is a number returned by sending a GET request to the ACME server at the URL specified in `directory["newNonce"]`). The JSON payload using an RSA-256 key before JWT-signing should look like: - ```json - { - "header": { - "alg": "RS256", - "typ": "JWT", - "nonce": "`nonce`", - "url": "`url`", - "jwk": { - "kty": "RSA", - "n": "`mykey.n`", - "e": "`mykey.e`" - } - }, - "claims": { - "payload": { - "termsOfServiceAgreed": true, - "contact": [ - "mailto:alice@example.com", - "mailto:bob@example.com" - ] - } - } - } - ``` - The final body of any ACME request should look like: - ```json - { - "payload": "`claims.toBase64`", - "protected": "`header.toBase64`", - "signature": "`base64UrlEncode(signature)`" - } - ``` -5. The client MUST save the `kid` present in the `location` header of the ACME server's response for in future requests to ACME server. -6. The client requests a certificate for the `*.{b36peerid}.libp2p.direct` domain from the ACME server by issuing a POST request using the same JWT signature scheme (and a new `nonce`) but using the `kid` field instead of the `jwk` field and containing the following JSON payload: - ```json - { - "type": "dns", - "value": "*.{b36peerid}.libp2p.direct" - } - ``` -7. From the ACME server response, the client MUST save the entry with `"type"` of `"dns-01"` and derive the [`Key Authorization`](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1) from that. - -## Sending challenge to AutoTLS broker -1. The client sends the `key authorization` to the AutoTLS broker (e.g. `registration.libp2p.direct`). This requires a [PeerID Authentication](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) between client and broker: - 1. Client sends GET request to the AutoTLS broker's `/v1/_acme-challenge` endpoint and extracts `challenge-client`, `public-key` and `opaque` from the `www-authenticate` response header. - 2. Client generates 32-character-long random string to be sent as a `challengeServer`. At the time of writing the PeerID Authentication specification does not contain recommendations about challenge length, but the official [`go-libp2p` implementation uses 32 characters](https://github.com/libp2p/go-libp2p/blob/master/p2p/http/auth/internal/handshake/handshake.go#L21). - 3. Client generates `sig`, `headers` and `payload` as follows, where `peer-privkey` is the private key of the client's libp2p peer and `multiaddrs` is a list of string representations of the libp2p peer's multiaddresses: - ``` - sig = base64URL( - peer-privkey.sign( - bytes(varint + "challenge-client={challenge-client}") + - bytes(varint + "hostname={hostname}") + - bytes(varint + "server-public-key={public-key}") - ) - ) - - headers = { - "Content-Type": "application/json", - "User-Agent": "some-user-agent", - "authorization": "libp2p-PeerID public-key=\"{clientPublicKeyB64}\", opaque=\"{opaque}\", challenge-server=\"{challengeServer}\", sig=\"{sig}\"" - } - - payload = { - "value": keyAuthorization, - "addresses": multiaddrs - } - ``` - **Note:** `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string. - **Note:** the AutoTLS broker MUST NOT dial multiaddresses containing private IPv4 addresses, thus the client SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. - 4. Client sends a POST request to `/v1/_acme-challenge` endpoint using `payload` as body and `headers` as headers. - 6. Client SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. -3. Client SHOULD query DNS records (`TXT _acme-challenge.{b36peerid}.libp2p.direct` and `A dashed-public-ip-address.{b36peerid}.libp2p.direct`) until they are set by the AutoTLS broker. -4. Client notifies the ACME server about challenge completion so that the ACME server can lookup the DNS resource records. The notification is done in the form of a POST request with an empty JSON payload (`{}`) as body sent to the `url` field returned by the ACME server when it responded to client's initial challenge request. - 1. Client sends an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the initial ACME registration and gets the response from the server (`completedResponse`). - 2. Client extracts `url` field from `completedResponse`'s JSON body ting it, again with `kid` signing. The extracted URL is named `checkUrl` in this document. -5. The client polls the ACME server by sending an empty bodied, `kid` signed GET request to `checkUrl` until it receives a response with `status: valid` or `status: invalid` field, meaning that the challenge checking was successful or not, respectively. - -## Signalling challenge completion to ACME server -11. Download certificate from ACME server. -## Finalizing challenge request -### CSR generation -## Downloading certificate from ACME server - - -## Complete certificate issuance example From 6ff0101c378cb75e6134e7b46202dcb10f27fff7 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Thu, 22 May 2025 11:36:22 -0300 Subject: [PATCH 6/9] add autotls client example --- tls/autotls-client.md | 190 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/tls/autotls-client.md b/tls/autotls-client.md index 187a2c9..c63c3db 100644 --- a/tls/autotls-client.md +++ b/tls/autotls-client.md @@ -161,6 +161,196 @@ The following is the general flow of a successful certificate request and subseq ## Complete certificate issuance example +In this example the node at `142.93.194.175` and with peer ID `12D3KooWATZi2wFwQxQ14Z3q24TDNWKap6f8W5ryLE6Da4RMfsxy`. + +1. Node encodes its peer ID to multicode base 36: `k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6` + +2. Node generates an RSA key and registers with ACME server by issuing a POST HTTP request to `https://acme-staging-v02.api.letsencrypt.org` with the following JSON body: + ```json + { + "header": { + "alg": "RS256", + "typ": "JWT", + "nonce": "1jOOXM0FEc7tL_RH3PbFWf5Ml4QXTrtG-d2faH_j68L9K27-768", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct", + "jwk": { + "kty": "RSA", + "n": "xImaldcbdCDn_IHSa2qeYjOhQ5PLuqIWLXEwQvvzD6eUIC8MteHvSM9Yj4tUGUzQ6Vic7j5j3npZhrmXMv3FiwIpQgsqDEiXSyriST7zYSPtQUcZr17gEqk9Rxjewl77HKkTej34IQ7JLaHzx5owJVtNsfBI36NPQiBCDaEBMht0E5zyMa83fTlNqnVnyMAqOR7CxctsxmYkoyyYeA_hV0gJfOBzUHls_ENHP67dQ2eVYGJ0gU7ldaK7lsWw10ieNCEDjbDT9E50HAdQt4UO1c_6rD8jzD0UjS2xtO6wrJpkmUnkt71WoQXWIWjoTvhl15dqLwyx_jeW-C6ISpwh1eWdrcM0z0TZpOZQEODg1IJppOEQZsBYeSZg4El5rt1IKcllp6euWlHPopreFNcEUrYZ76uQQLuRyQ2AM_caUITFi6e0ZgTea2COuy4vof2ZJTBZP8uE4aHUdXOMYrDO6TVnXYA7mYJ6jkyp-X9OjzGSst6yRY5Qm-uCmBEuVtoN", + "e": "AQAB" + } + }, + "claims": { + "termsOfServiceAgreed": true + } + } + ``` + Which, after JWT signing, becomes: + ```json + { + "payload": "eyJpZGVudGlmaWVycyI6W3sidHlwZSI6ImRucyIsInZhbHVlIjoiKi5rNTFxemk1dXF1NWRnZjUxM3hicmZqbDRzbWdvMmVoMXg4cDh5NmdyenNmMW96MHJlaXk1NnA2NXRkczNzNi5saWJwMnAuZGlyZWN0In1dfQ", + "protected": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIm5vbmNlIjoiWW8xc2xCY2RhV3FZREoyanFPYkhIb0dUZkZkQlJubzJzM2pzRGxQN2VNdXo5T1lDNWpBIiwidXJsIjoiaHR0cHM6Ly9hY21lLXN0YWdpbmctdjAyLmFwaS5sZXRzZW5jcnlwdC5vcmcvYWNtZS9uZXctb3JkZXIiLCJraWQiOiJodHRwczovL2FjbWUtc3RhZ2luZy12MDIuYXBpLmxldHNlbmNyeXB0Lm9yZy9hY21lL2FjY3QvMjAxNDI3MzQ0In0", + "signature": "f2oPdLiFYwvv7_KLaiSlBgJJh3brXDA9_wPfw52UB_GTU0eL_9y9-oX8WJJcEU87juUWUuML3eEOT4zjUY1EK2ri-rR_8AO2QngpxpWbo86wUM-XwiXk35uGelpW0QvCQw_x16AWK6xr0Rm1gSbnVkxOMrMBl-2xQYyXILwLmEuTq76C2vt2ZzrLhcV-6BKUla2lkgaZKPK3dpTYL0_i0pEybb28Ree5SERHpxihxFTO1ggvLJosbdlGOtAvCc7x-aZhTcuwhjlCRNLi0rnsFRNh3PJc-_Kz5B2Uv_OoTktWg_0vrUU_OFBuf4lHl5lb82cl5NxRH9ieX673rsh9in9l9Nr-Gt3g8SdiY29LTMwOy37MmhhNcL7MjUcseI05FOhLFxyc3dUxsG92VSDwJ_1JQIQH7EGJ6vP_dDPustMlvzNX_qHV2TjN6XpAv2tECmK5enU7qfnhTXbPihvz7MY1_PAlxJSWmBq-ui_sovN85YNJWBZ-tIPOPtqPMZDT" + } + ``` + +3. Node requests a certificate for `*.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` by issuing a POST request to `https://acme-staging-v02.api.letsencrypt.org/acme/new-order` with the following JSON body: + ```json + { + "header": { + "alg": "RS256", + "typ": "JWT", + "nonce": "Yo1slBcdaWqYDJ2jqObHHoGTfFdBRno2s3jsDlP7eMuz9OYC5jA", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/new-order", + "kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/201427344" + }, + "claims": { + "identifiers": [ + { + "type": "dns", + "value": "*.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct" + } + ] + } + } + ``` + +4. From the ACME server's response, node saves `orderUrl`, `chalUrl` and `finalizeUrl`: + ``` + orderUrl: https://acme-staging-v02.api.letsencrypt.org/acme/order/201427344/24815752984 + chalUrl: https://acme-staging-v02.api.letsencrypt.org/acme/chall/201427344/17523856954/k8-vYA + finalizeUrl: https://acme-staging-v02.api.letsencrypt.org/acme/finalize/201427344/24815752984 + ``` + +5. Node generates `keyAuthorization` (`jP5hwrZwCbP_qeeET_qAa9pgG0YulNaR0ivruESzCrE`) from the following `dns-01` object: + ```json + { + "type": "dns-01", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall/201427344/17523856954/k8-vYA", + "status": "pending", + "token": "nE2YGvFXzAy6UsFjYxqYOyA6rxZ7VJeQppsQ72hHyPM" + } + ``` + +6. Node authenticates with AutoTLS broker at `"https://registration.libp2p.direct/v1/_acme-challenge"` (refer to the [peer ID authentication spec](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) for guidance and examples) and sends the following JSON body: + ```json + { + "value": "jP5hwrZwCbP_qeeET_qAa9pgG0YulNaR0ivruESzCrE", + "addresses": [ + "/ip4/142.93.194.175/tcp/49309" + ] + } + ``` +**Note:** the node's multiaddresses are `/ip4/127.0.0.1/tcp/49309`, `/ip4/142.93.194.175/tcp/49309`, `/ip4/10.17.0.5/tcp/49309`, and `/ip4/10.108.0.2/tcp/49309`, but only `/ip4/142.93.194.175/tcp/49309` contains a public IPv4 address, so node SHOULD only send that. + +7. Node saves the bearer token (`bJNzn30OvOSIPsd0UtMygo4ccjUMXkwHONRHc46oyTx7ImlzLXRva2VuIjp0cnVlLCJwZWVyLWlkIjoiMTJEM0tvb1dBVFppMndGd1F4UTE0WjNxMjRURE5XS2FwNmY4VzVyeUxFNkRhNFJNZnN4eSIsImhvc3RuYW1lIjoicmVnaXN0cmF0aW9uLmxpYnAycC5kaXJlY3QiLCJjcmVhdGVkLXRpbWUiOiIyMDI1LTA1LTIyVDE0OjAxOjU4LjY1NzAyMDQ4OFoifQ==`) from the broker's `authentication-info` response header: + ``` + Authentication-Info: libp2p-PeerID sig="hysWRh0SAQX6MkhNIwf0rgyjqbV9wkjMDhNobVhHybBE3CygrOAfEPTkvgrrePX5XTGt1FO-4--VBbJas8BtCQ==", bearer="bJNzn30OvOSIPsd0UtMygo4ccjUMXkwHONRHc46oyTx7ImlzLXRva2VuIjp0cnVlLCJwZWVyLWlkIjoiMTJEM0tvb1dBVFppMndGd1F4UTE0WjNxMjRURE5XS2FwNmY4VzVyeUxFNkRhNFJNZnN4eSIsImhvc3RuYW1lIjoicmVnaXN0cmF0aW9uLmxpYnAycC5kaXJlY3QiLCJjcmVhdGVkLXRpbWUiOiIyMDI1LTA1LTIyVDE0OjAxOjU4LjY1NzAyMDQ4OFoifQ==" + ``` +8. Node queries DNS records: `TXT _acme-challenge.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` and `A 142-93-194-175.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` until there's a non-empty response from DNS servers. + +9. Node notifies ACME server about challenge completion by issuing an empty POST request to `chalUrl` with `kid` JWT signing: + ```json + { + "header": { + "alg": "RS256", + "typ": "JWT", + "nonce": "Yo1slBcd5jWarMN9llbXOVII-htMZpSZBumnZAaKdyzqjyNfREg", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall/201427344/17523856954/k8-vYA", + "kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/201427344" + }, + "claims": {} + } + ``` + Node extracts `checkUrl` (`https://acme-staging-v02.api.letsencrypt.org/acme/chall/201427344/17523856954/k8-vYA`) from `url` field from ACME server's response body: + ```json + { + "type": "dns-01", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall/201427344/17523856954/k8-vYA", + "status": "pending", + "token": "nE2YGvFXzAy6UsFjYxqYOyA6rxZ7VJeQppsQ72hHyPM" + } + ``` + +10. Node polls the ACME server by sending a GET HTTP request to `checkUrl` until it receives a response with `status: valid`: + ```json + { + "type": "dns-01", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall/201427344/17523856954/k8-vYA", + "status": "valid", + "validated": "2025-05-22T14:01:59Z", + "token": "nE2YGvFXzAy6UsFjYxqYOyA6rxZ7VJeQppsQ72hHyPM", + "validationRecord": [ + { + "hostname": "k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct" + } + ] + } + ``` + +11. Node creates the CSR: + ``` + MIIBJzCBzgIBADAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-xJPmWkXNCxikFNfjX-YpNpVUlwVE75FqiLzQKqz0oSRGChnxEiCPXSYW6XYfy7RyYtoaMKJB8oYjpuoZau2U6BsMGoGCSqGSIb3DQEJDjFdMFswWQYDVR0RBFIwUIJOKi5rNTFxemk1dXF1NWRnZjUxM3hicmZqbDRzbWdvMmVoMXg4cDh5NmdyenNmMW96MHJlaXk1NnA2NXRkczNzNi5saWJwMnAuZGlyZWN0MAoGCCqGSM49BAMCA0gAMEUCIA_wWAa07lkYDlXVs8QBxX9XI7ATyMT8KIWirx2dBwyVAiEA9anNGq3BssBdMKW-QHKdOPqcv7lzaB64vTjpfciyfr4=" + ``` + And sends it to `finalizeUrl`: + ```json + { + "header": { + "alg": "RS256", + "typ": "JWT", + "nonce": "Yo1slBcdhW7xgUkJ0DzYeH1otfpMMbjbXD3xlf7TM1lneccMLHI", + "url": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/201427344/24815752984", + "kid": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/201427344" + }, + "claims": { + "csr": "MIIBJzCBzgIBADAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE-xJPmWkXNCxikFNfjX-YpNpVUlwVE75FqiLzQKqz0oSRGChnxEiCPXSYW6XYfy7RyYtoaMKJB8oYjpuoZau2U6BsMGoGCSqGSIb3DQEJDjFdMFswWQYDVR0RBFIwUIJOKi5rNTFxemk1dXF1NWRnZjUxM3hicmZqbDRzbWdvMmVoMXg4cDh5NmdyenNmMW96MHJlaXk1NnA2NXRkczNzNi5saWJwMnAuZGlyZWN0MAoGCCqGSM49BAMCA0gAMEUCIA_wWAa07lkYDlXVs8QBxX9XI7ATyMT8KIWirx2dBwyVAiEA9anNGq3BssBdMKW-QHKdOPqcv7lzaB64vTjpfciyfr4=" + } + } + ``` +12. Node polls `orderUrl` until the ACME server's response contains a `status` field with value different than `processing`: + ```json + { + "status": "valid", + "expires": "2025-05-29T14:01:58Z", + "identifiers": [ + { + "type": "dns", + "value": "*.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct" + } + ], + "authorizations": [ + "https://acme-staging-v02.api.letsencrypt.org/acme/authz/201427344/17523856954" + ], + "finalize": "https://acme-staging-v02.api.letsencrypt.org/acme/finalize/201427344/24815752984", + "certificate": "https://acme-staging-v02.api.letsencrypt.org/acme/cert/2cd1c21b2b77127e4d394eb16eb073f9248d" + } + ``` + +13. Node downloads the certificate by sending a GET request to `certDownloadUrl` (`https://acme-staging-v02.api.letsencrypt.org/acme/cert/2cd1c21b2b77127e4d394eb16eb073f9248d`), which is the `certificate` field of the finalize request's response: + ``` + -----BEGIN CERTIFICATE----- + MIID3zCCA2SgAwIBAgISLNHCGyt3En5NOU6xbrBz+SSNMAoGCCqGSM49BAMDMFMx + CzAJBgNVBAYTAlVTMSAwHgYDVQQKExcoU1RBR0lORykgTGV0J3MgRW5jcnlwdDEi + MCAGA1UEAxMZKFNUQUdJTkcpIEZhbHNlIEZlbm5lbCBFNjAeFw0yNTA1MjIxMzAz + MzJaFw0yNTA4MjAxMzAzMzFaMAAwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT7 + Ek+ZaRc0LGKQU1+Nf5ik2lVSXBUTvkWqIvNAqrPShJEYKGfESII9dJhbpdh/LtHJ + i2howokHyhiOm6hlq7ZTo4ICaTCCAmUwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQW + MBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBT/ + wwgAeNwsd900zIITjOK2+Zjt7TAfBgNVHSMEGDAWgBShdBoGbVC3hi1KLMF+tI2I + SWzNFjA2BggrBgEFBQcBAQQqMCgwJgYIKwYBBQUHMAKGGmh0dHA6Ly9zdGctZTYu + aS5sZW5jci5vcmcvMFwGA1UdEQEB/wRSMFCCTiouazUxcXppNXVxdTVkZ2Y1MTN4 + YnJmamw0c21nbzJlaDF4OHA4eTZncnpzZjFvejByZWl5NTZwNjV0ZHMzczYubGli + cDJwLmRpcmVjdDATBgNVHSAEDDAKMAgGBmeBDAECATAxBgNVHR8EKjAoMCagJKAi + hiBodHRwOi8vc3RnLWU2LmMubGVuY3Iub3JnLzE0LmNybDCCAQYGCisGAQQB1nkC + BAIEgfcEgfQA8gB3AN2ZNPyl5ySAyVZofYE0mQhJskn3tWnYx7yrP1zB825kAAAB + lvhNETkAAAQDAEgwRgIhAOlwytcyMH7HcggxOYMhRdZ8LIoKt2T/VqS/bsMupnmK + AiEA8ed29c8/BGQ2Qzoezp7zc1gm7g6F7VyrzRlj29bpYTQAdwCwzIPlpfl9a698 + CcwoSQSHKsfoixMsY1C3xv0m4WxsdwAAAZb4TREnAAAEAwBIMEYCIQDtuHcbHonG + cEuwgT8r73zcRyLJQOWpRpLAqYtFy0idfwIhAM9zywGUgthnkAilzw1LQYQOmKEf + fquKAmPYn0UU8duIMAoGCCqGSM49BAMDA2kAMGYCMQD+CVoiLqEpSreQua2uzmHr + 0DAoQycGtGfPcBsMdUGxSN7y+VyuYLnSG4PqgPa3nqsCMQDQY9jPJzUjLwwg11Z2 + +ZhDTPiLY3NoLGxa4dh5/LWKaRL6Sz77brYwebRXEnNQKAo= + -----END CERTIFICATE----- + ``` ## References - [Announcing AutoTLS: Bridging the Gap Between libp2p and the Web](https://blog.libp2p.io/autotls/) From fc238d155824d941d17d1e845d8be78c6dce0574 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Mon, 26 May 2025 14:29:11 -0300 Subject: [PATCH 7/9] fix line breaks --- tls/autotls-client.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tls/autotls-client.md b/tls/autotls-client.md index c63c3db..b971e35 100644 --- a/tls/autotls-client.md +++ b/tls/autotls-client.md @@ -51,6 +51,7 @@ The following is the general flow of a successful certificate request and subseq 1. Transform PeerID into a multihash `mh`. 2. Encode `mh` using [CIDv1](https://github.com/multiformats/cid?tab=readme-ov-file#cidv1) with the `libp2p-key` [multicodec](https://github.com/multiformats/multicodec)(`0x72`). 3. Encode the CID data using [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md), which is the same as regular base36 without trimming leading zeroes and including a leading `k` or `K`) to get `b36peerid`. + **Note:** "CID data" are the raw bytes that compose the CID, not richer CID objects that contain more information. 3. The node generates a key `mykey` as specified in [RFC7518](https://www.rfc-editor.org/rfc/rfc7518#section-6). 4. The node registers an account on the ACME server (e.g. [production](https://acme-v02.api.letsencrypt.org) or [staging](https://acme-staging-v02.api.letsencrypt.org) servers for Let's Encrypt). @@ -112,6 +113,7 @@ The following is the general flow of a successful certificate request and subseq 1. The node sends `keyAuthorization` to the AutoTLS broker (e.g. `registration.libp2p.direct`). This requires a [peer ID authentication](https://github.com/libp2p/specs/blob/master/http/peer-id-auth.md) between node and broker: 1. Node sends GET request to the AutoTLS broker's `/v1/_acme-challenge` endpoint and extracts `challenge-node`, `public-key` and `opaque` from the `www-authenticate` response header. 2. Node generates 32-character-long random string to be sent as a `challengeServer`. + **Note:** At the time of writing the PeerID Authentication specification does not contain recommendations about challenge length, but the official [`go-libp2p` implementation uses 32 characters](https://github.com/libp2p/go-libp2p/blob/master/p2p/http/auth/internal/handshake/handshake.go#L21). 3. Node generates `sig`, `headers` and `payload` as follows, where `peer-privkey` is the private key of the node's libp2p peer and `multiaddrs` is a list of string representations of the libp2p peer's multiaddresses: ``` @@ -134,7 +136,9 @@ The following is the general flow of a successful certificate request and subseq "addresses": multiaddrs } ``` + **Note:** `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string. + **Note:** the AutoTLS broker MUST NOT dial multiaddresses containing private IPv4 addresses. The node SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. 4. Node sends a POST request to `/v1/_acme-challenge` endpoint using `payload` as HTTP body and `headers` as HTTP headers. 6. Node SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. @@ -143,7 +147,9 @@ The following is the general flow of a successful certificate request and subseq ## Signalling challenge completion to ACME server 1. Node SHOULD query DNS records (`TXT _acme-challenge.{b36peerid}.libp2p.direct` and `A dashed-public-ip-address.{b36peerid}.libp2p.direct`) until they are set by the AutoTLS broker. + **Note:** here, `dashed-public-ip-address` is the public IPv4 address of the node in which the node received the confirmation dial from the broker. For example, if the node has two public IPv4 addresses `1.1.1.1` and `8.8.8.8`, and the broker dialed it through `1.1.1.1`, then the node SHOULD query the `A 1-1-1-1.{b36peerid}.libp2p.direct`. + 2. Node notifies the ACME server about challenge completion so that the ACME server can lookup the DNS resource records that the AutoTLS broker has set. The notification is done in the form of a POST request to `chalUrl` with an empty HTTP body (`{}`). 1. Node sends an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the initial ACME registration and gets the response from the server (`completedResponse`). 2. Node extracts `url` field from `completedResponse`'s JSON body ting it, again with `kid` signing. The extracted URL is named `checkUrl` in this document. @@ -240,6 +246,7 @@ In this example the node at `142.93.194.175` and with peer ID `12D3KooWATZi2wFwQ ] } ``` + **Note:** the node's multiaddresses are `/ip4/127.0.0.1/tcp/49309`, `/ip4/142.93.194.175/tcp/49309`, `/ip4/10.17.0.5/tcp/49309`, and `/ip4/10.108.0.2/tcp/49309`, but only `/ip4/142.93.194.175/tcp/49309` contains a public IPv4 address, so node SHOULD only send that. 7. Node saves the bearer token (`bJNzn30OvOSIPsd0UtMygo4ccjUMXkwHONRHc46oyTx7ImlzLXRva2VuIjp0cnVlLCJwZWVyLWlkIjoiMTJEM0tvb1dBVFppMndGd1F4UTE0WjNxMjRURE5XS2FwNmY4VzVyeUxFNkRhNFJNZnN4eSIsImhvc3RuYW1lIjoicmVnaXN0cmF0aW9uLmxpYnAycC5kaXJlY3QiLCJjcmVhdGVkLXRpbWUiOiIyMDI1LTA1LTIyVDE0OjAxOjU4LjY1NzAyMDQ4OFoifQ==`) from the broker's `authentication-info` response header: From b5bc4aff99761c3077c7b6eebed1eb39a898522b Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Mon, 26 May 2025 14:39:22 -0300 Subject: [PATCH 8/9] writing fixes --- tls/autotls-client.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tls/autotls-client.md b/tls/autotls-client.md index b971e35..d099a5d 100644 --- a/tls/autotls-client.md +++ b/tls/autotls-client.md @@ -28,7 +28,7 @@ Most modern web browsers only establish TLS connections with peers that present 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 fulfil 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. +[AutoTLS](https://blog.libp2p.io/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 fulfil 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. @@ -141,7 +141,7 @@ The following is the general flow of a successful certificate request and subseq **Note:** the AutoTLS broker MUST NOT dial multiaddresses containing private IPv4 addresses. The node SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. 4. Node sends a POST request to `/v1/_acme-challenge` endpoint using `payload` as HTTP body and `headers` as HTTP headers. - 6. Node SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. + 5. Node SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. @@ -152,7 +152,7 @@ The following is the general flow of a successful certificate request and subseq 2. Node notifies the ACME server about challenge completion so that the ACME server can lookup the DNS resource records that the AutoTLS broker has set. The notification is done in the form of a POST request to `chalUrl` with an empty HTTP body (`{}`). 1. Node sends an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the initial ACME registration and gets the response from the server (`completedResponse`). - 2. Node extracts `url` field from `completedResponse`'s JSON body ting it, again with `kid` signing. The extracted URL is named `checkUrl` in this document. + 2. Node extracts `url` field from `completedResponse`'s JSON body. The extracted URL is named `checkUrl` in this document. 3. The node polls the ACME server by sending a GET HTTP request to `checkUrl` with an empty body, and sign using the `kid` of the registered account. The node MUST poll the ACME server until it receives a response with `status: valid` or `status: invalid` field, meaning that the challenge checking was successful or not, respectively. From d7d4ff32afc7371868310ee24c5cab0fde401d88 Mon Sep 17 00:00:00 2001 From: Gabriel Cruz Date: Mon, 26 May 2025 15:41:49 -0300 Subject: [PATCH 9/9] add timeout and max_retries --- tls/autotls-client.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tls/autotls-client.md b/tls/autotls-client.md index d099a5d..a5328b7 100644 --- a/tls/autotls-client.md +++ b/tls/autotls-client.md @@ -45,6 +45,15 @@ The following is the general flow of a successful certificate request and subseq 8. Node polls ACME server until certificate is ready for download. 9. Node downloads certificate. +## Paramenters + +| Parameter | Description | Reasonable Default | +|--------------------------|------------------------------------------------------------------|--------------| +| `max_dns_retries` | The maximum number of DNS queries that the node SHOULD make before giving up | ??? | +| `max_dns_timeout` | The maximum number of seconds a node SHOULD wait for DNS records to be set | ??? | +| `max_acme_poll_retries` | The maximum number of GET requests that the node SHOULD issue to ACME server before giving up | ??? | +| `max_acme_timeout` | The maximum number of seconds a node SHOULD wait for an ACME resource status to change | ??? | + ## Requesting challenge from ACME server 1. The node starts a libp2p peer with public IPv4 and support for the [`identify`](https://github.com/libp2p/specs/blob/master/identify/README.md) protocol. 2. The node encodes its `PeerID` as [multibase base36](https://github.com/multiformats/multibase/blob/f378d3427fe125057facdbac936c4215cc777920/rfcs/Base36.md) of the CIDv1 of the multihash with the `libp2p-key` (`0x72`) multicodec: @@ -139,7 +148,7 @@ The following is the general flow of a successful certificate request and subseq **Note:** `varint` is a protobuf [varint](https://protobuf.dev/programming-guides/encoding/#varints) field that encodes the length of each of the `key=value` string. - **Note:** the AutoTLS broker MUST NOT dial multiaddresses containing private IPv4 addresses. The node SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. + **Note:** The node SHOULD only include multiaddresses that contain public IPv4 addresses in `multiaddrs`. 4. Node sends a POST request to `/v1/_acme-challenge` endpoint using `payload` as HTTP body and `headers` as HTTP headers. 5. Node SHOULD save the `bearer` token from the `authentication-info` response header, and use it for following requests to the AutoTLS broker. @@ -148,13 +157,17 @@ The following is the general flow of a successful certificate request and subseq ## Signalling challenge completion to ACME server 1. Node SHOULD query DNS records (`TXT _acme-challenge.{b36peerid}.libp2p.direct` and `A dashed-public-ip-address.{b36peerid}.libp2p.direct`) until they are set by the AutoTLS broker. -**Note:** here, `dashed-public-ip-address` is the public IPv4 address of the node in which the node received the confirmation dial from the broker. For example, if the node has two public IPv4 addresses `1.1.1.1` and `8.8.8.8`, and the broker dialed it through `1.1.1.1`, then the node SHOULD query the `A 1-1-1-1.{b36peerid}.libp2p.direct`. +**Note:** Here, `dashed-public-ip-address` is the public IPv4 address of the node in which the node received the confirmation dial from the broker. For example, if the node has two public IPv4 addresses `1.1.1.1` and `8.8.8.8`, and the broker dialed it through `1.1.1.1`, then the node SHOULD query the `A 1-1-1-1.{b36peerid}.libp2p.direct`. + +**Note:** The node SHOULD NOT send more than `max_dns_retries` DNS requests. After `max_dns_timeout`, the communication is considered failed. What to do after `max_dns_timeout` has passed is left as an implementation decision. 2. Node notifies the ACME server about challenge completion so that the ACME server can lookup the DNS resource records that the AutoTLS broker has set. The notification is done in the form of a POST request to `chalUrl` with an empty HTTP body (`{}`). 1. Node sends an empty signed JSON payload (`{}`) to the ACME server using the `kid` obtained from the initial ACME registration and gets the response from the server (`completedResponse`). 2. Node extracts `url` field from `completedResponse`'s JSON body. The extracted URL is named `checkUrl` in this document. 3. The node polls the ACME server by sending a GET HTTP request to `checkUrl` with an empty body, and sign using the `kid` of the registered account. The node MUST poll the ACME server until it receives a response with `status: valid` or `status: invalid` field, meaning that the challenge checking was successful or not, respectively. +**Note:** The node SHOULD NOT send more than `max_acme_poll_retries` poll requests to the ACME server. After `max_acme_timeout`, the communication has failed. What to do after `max_acme_timeout` has passed is left as an implementation decision. + ## Downloading certificate @@ -163,6 +176,9 @@ The following is the general flow of a successful certificate request and subseq 2. Encode the CSR with URL safe base 64 (`b64CSR`). 3. Send a `kid` signed POST request to `finalizeUrl` with JSON HTTP body of `{"csr": b64CSR}`. 2. Node MUST poll ACME server by sending GET requests to `orderUrl` until the ACME server's response contains a `status` field with a value different than `processing`. + +**Note:** The node SHOULD NOT send more than `max_acme_poll_retries` poll requests to the ACME server. After `max_acme_timeout`, the communication has failed. What to do after `max_acme_timeout` has passed is left as an implementation decision. + 3. Node downloads finalized certificate by sending a GET request to `certDownloadUrl`. `certDownloadUrl` is found in the `certificate` field of the JSON HTTP body of a response to a GET request to `orderUrl`. @@ -253,7 +269,7 @@ In this example the node at `142.93.194.175` and with peer ID `12D3KooWATZi2wFwQ ``` Authentication-Info: libp2p-PeerID sig="hysWRh0SAQX6MkhNIwf0rgyjqbV9wkjMDhNobVhHybBE3CygrOAfEPTkvgrrePX5XTGt1FO-4--VBbJas8BtCQ==", bearer="bJNzn30OvOSIPsd0UtMygo4ccjUMXkwHONRHc46oyTx7ImlzLXRva2VuIjp0cnVlLCJwZWVyLWlkIjoiMTJEM0tvb1dBVFppMndGd1F4UTE0WjNxMjRURE5XS2FwNmY4VzVyeUxFNkRhNFJNZnN4eSIsImhvc3RuYW1lIjoicmVnaXN0cmF0aW9uLmxpYnAycC5kaXJlY3QiLCJjcmVhdGVkLXRpbWUiOiIyMDI1LTA1LTIyVDE0OjAxOjU4LjY1NzAyMDQ4OFoifQ==" ``` -8. Node queries DNS records: `TXT _acme-challenge.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` and `A 142-93-194-175.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` until there's a non-empty response from DNS servers. +8. Node queries DNS records: `TXT _acme-challenge.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` and `A 142-93-194-175.k51qzi5uqu5dgf513xbrfjl4smgo2eh1x8p8y6grzsf1oz0reiy56p65tds3s6.libp2p.direct` until it receives a non-empty response from DNS servers. 9. Node notifies ACME server about challenge completion by issuing an empty POST request to `chalUrl` with `kid` JWT signing: ```json @@ -278,7 +294,7 @@ In this example the node at `142.93.194.175` and with peer ID `12D3KooWATZi2wFwQ } ``` -10. Node polls the ACME server by sending a GET HTTP request to `checkUrl` until it receives a response with `status: valid`: +10. Node polls the ACME server by sending GET HTTP requests to `checkUrl` until it receives a response with `status: valid`: ```json { "type": "dns-01",