Files
specs/tls/autotls.md
2025-04-30 11:18:10 -03:00

5.3 KiB

libp2p AutoTLS

Lifecycle Stage Maturity Status Latest Revision
?? ?????????????? Active r0, 2025-04-30

Authors: @gmelodie

Interest Group: @??, @???

See the lifecycle document for context about the maturity level and spec status.

Table of Contents

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, 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 or just the staging server 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:
      {
        "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:
    {
    	"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: 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 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 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}")
    		)
    	)
    
    1. headers =
      {
      	"Content-Type": "application/json",
      	"User-Agent": "nim-libp2p",
      	"authorization": "libp2p-PeerID public-key=\"{clientPublicKeyB64}\", opaque=\"{opaque}\", challenge-server=\"{challengeServer}\", sig=\"{sig}\""
      }
      
    2. Send POST to v1/_acme-challenge endpoint using payload as body and headers
    3. 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 GETting 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.