From e4350daac5abeb38da9473fa139c99f208f09393 Mon Sep 17 00:00:00 2001 From: Adin Schmahmann Date: Wed, 4 Sep 2024 15:29:50 -0400 Subject: [PATCH] feat: switch to http peerID auth --- README.md | 15 +--- acme/writer.go | 199 +++++++++----------------------------------- client/acme.go | 2 +- client/challenge.go | 77 +++-------------- cmd/e2e_test.go | 2 +- go.mod | 8 +- go.sum | 4 +- 7 files changed, 63 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 2ddc31e..1c6a66a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ To claim a domain name like `.libp2p.direct` requires: To set an ACME challenge send an HTTP request to the server (for libp2p.direct this is registration.libp2p.direct) ```shell curl -X POST "https://registration.libp2p.direct/v1//_acme-challenge" \ --H "Authorization: Bearer ." +-H "Authorization: libp2p-PeerID bearer=\"\"" -H "Content-Type: application/json" \ -d '{ "value": "your_acme_challenge_token", @@ -66,15 +66,4 @@ curl -X POST "https://registration.libp2p.direct/v1//_acme-challenge" \ }' ``` -Where the signature is a base64 encoding of the signature for a [libp2p signed envelope](https://github.com/libp2p/specs/blob/master/RFC/0002-signed-envelopes.md) -where: -- The domain separation string is "peer-forge-domain-challenge" -- The payload type is the ASCII string "/peer-forge-domain-challenge" -- The payload bytes are the contents of the body of the request - -If the public key is not extractable from the peerID then after the signature add a `.` followed by the base64 encoded -public key in the libp2p public key format. - -Note: Per the [peerID spec](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#peer-ids) the peerIDs with -extractable public keys are those that are encoded as fewer than 42 bytes (i.e. Ed25519 and Secp256k1), which means the -others (i.e. RSA and ECDSA) require the public keys to be in the Authorization header. \ No newline at end of file +Where the bearer token is derived via the [libp2p HTTP PeerID Auth Specification](https://github.com/libp2p/specs/pull/564). \ No newline at end of file diff --git a/acme/writer.go b/acme/writer.go index 5305910..7d2093b 100644 --- a/acme/writer.go +++ b/acme/writer.go @@ -3,14 +3,13 @@ package acme import ( "bytes" "context" + "crypto/rand" "encoding/base64" "encoding/json" - "errors" "fmt" "io" "net" "net/http" - "strings" "time" clog "github.com/coredns/coredns/plugin/pkg/log" @@ -19,12 +18,11 @@ import ( "github.com/gorilla/mux" "github.com/ipfs/go-datastore" - pool "github.com/libp2p/go-buffer-pool" "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + httppeeridauth "github.com/libp2p/go-libp2p/p2p/http/auth" "github.com/multiformats/go-multiaddr" - "github.com/multiformats/go-varint" ) var log = clog.NewWithPlugin(pluginName) @@ -57,144 +55,62 @@ func (c *acmeWriter) OnStartup() error { c.mux = mux.NewRouter() c.nlSetup = true - c.mux.HandleFunc("/v1/{peerID}/_acme-challenge", func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte("must pass an authorization header")) - return - } - - vars := mux.Vars(r) - peerIDStr, peerIDFound := vars["peerID"] - if !peerIDFound { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("peerID not found in request URL")) - return - } - peerID, err := peer.Decode(peerIDStr) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("invalid peer ID")) - return - } - - pk, err := peerID.ExtractPublicKey() - if err != nil && errors.Is(err, peer.ErrNoPublicKey) { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(fmt.Sprintf("unable to extract public key from peer ID: %s", err.Error()))) - return - } - - body, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("error reading body: %s", err))) - return - } - - authComponents := strings.Split(authHeader, ".") - if pk == nil { - if len(authComponents) != 2 { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("must pass both a signature and public key if the public key cannot be extracted from the peerID")) - return - } - base64EncodedPubKey := authComponents[1] - encodedPubKey, err := base64.StdEncoding.DecodeString(base64EncodedPubKey) + sk, _, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + return err + } + authPeer := &httppeeridauth.ServerPeerIDAuth{ + PrivKey: sk, + TokenTTL: time.Hour, + Next: func(peerID peer.ID, w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) if err != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("could not decode public key")) + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(fmt.Sprintf("error reading body: %s", err))) return } - pk, err = crypto.UnmarshalPublicKey(encodedPubKey) - if err != nil { + + typedBody := &requestBody{} + decoder := json.NewDecoder(bytes.NewReader(body)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(typedBody); err != nil { w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("could not unmarshal public key")) + _, _ = w.Write([]byte(fmt.Sprintf("error decoding body: %s", err))) return } - calculatedID, err := peer.IDFromPublicKey(pk) + // Value must be a base64url encoding of a SHA256 digest per https://datatracker.ietf.org/doc/html/rfc8555/#section-8.4 + decodedValue, err := base64.RawURLEncoding.DecodeString(typedBody.Value) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("could not calculate peerID from public key")) + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(fmt.Sprintf("error decoding value as base64url: %s", err))) return } - if calculatedID != peerID { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte("calculated peer ID does not match the passed peerID")) + if len(decodedValue) != 32 { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("value is not a base64url of a SHA256 digest")) return } - } else { - if len(authComponents) != 1 { + + if err := testAddresses(r.Context(), peerID, typedBody.Addresses); err != nil { w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("when the peerID can be extracted from the public key only the signature should be in the authorization header")) + _, _ = w.Write([]byte(fmt.Sprintf("error testing addresses: %s", err))) return } - } - base64EncodedSignature := authComponents[0] - signature, err := base64.StdEncoding.DecodeString(base64EncodedSignature) - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(fmt.Sprintf("error decoding signature: %s", err))) - } - - unsigned := makeUnsigned(signatureDomainString, signaturePayloadType, body) - defer pool.Put(unsigned) - - verified, err := pk.Verify(unsigned, signature) - if !verified { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte("invalid signature")) - return - } - - if err != nil { - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte(fmt.Sprintf("error verifying signature: %s", err))) - return - } - - typedBody := &requestBody{} - decoder := json.NewDecoder(bytes.NewReader(body)) - decoder.DisallowUnknownFields() - if err := decoder.Decode(typedBody); err != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(fmt.Sprintf("error decoding body: %s", err))) - return - } - - // Value must be a base64url encoding of a SHA256 digest per https://datatracker.ietf.org/doc/html/rfc8555/#section-8.4 - decodedValue, err := base64.RawURLEncoding.DecodeString(typedBody.Value) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(fmt.Sprintf("error decoding value as base64url: %s", err))) - return - } - - if len(decodedValue) != 32 { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte("value is not a base64url of a SHA256 digest")) - return - } - - if err := testAddresses(r.Context(), peerID, typedBody.Addresses); err != nil { - w.WriteHeader(http.StatusBadRequest) - _, _ = w.Write([]byte(fmt.Sprintf("error testing addresses: %s", err))) - return - } + const ttl = time.Hour + err = c.Datastore.PutWithTTL(r.Context(), datastore.NewKey(peerID.String()), []byte(typedBody.Value), ttl) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(fmt.Sprintf("error storing value: %s", err))) + return + } + w.WriteHeader(http.StatusOK) + }, + } - const ttl = time.Hour - err = c.Datastore.PutWithTTL(r.Context(), datastore.NewKey(peerID.String()), []byte(typedBody.Value), ttl) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("error storing value: %s", err))) - return - } - w.WriteHeader(http.StatusOK) - }).Methods("POST") + c.mux.Handle("/v1/_acme-challenge", authPeer).Methods("POST") go func() { http.Serve(c.ln, c.mux) }() @@ -239,41 +155,6 @@ type requestBody struct { Addresses []string `json:"addresses"` } -const signatureDomainString = "peer-forge-domain-challenge" - -var signaturePayloadType []byte = []byte("/peer-forge-domain-challenge") - -// makeUnsigned is a helper function that prepares a buffer to sign or verify. -// It returns a byte slice from a pool. The caller MUST return this slice to the -// pool. -func makeUnsigned(domain string, payloadType []byte, payload []byte) []byte { - var ( - fields = [][]byte{[]byte(domain), payloadType, payload} - - // fields are prefixed with their length as an unsigned varint. we - // compute the lengths before allocating the sig buffer, so we know how - // much space to add for the lengths - flen = make([][]byte, len(fields)) - size = 0 - ) - - for i, f := range fields { - l := len(f) - flen[i] = varint.ToUvarint(uint64(l)) - size += l + len(flen[i]) - } - - b := pool.Get(size) - - var s int - for i, f := range fields { - s += copy(b[s:], flen[i]) - s += copy(b[s:], f) - } - - return b[:s] -} - func (c *acmeWriter) OnFinalShutdown() error { if !c.nlSetup { return nil diff --git a/client/acme.go b/client/acme.go index fad4bcc..81a07b4 100644 --- a/client/acme.go +++ b/client/acme.go @@ -408,7 +408,7 @@ func (d *dns01P2PForgeSolver) Wait(ctx context.Context, challenge acme.Challenge } func (d *dns01P2PForgeSolver) Present(ctx context.Context, challenge acme.Challenge) error { - return SendChallenge(ctx, d.forge, d.host.ID(), d.host.Peerstore().PrivKey(d.host.ID()), challenge.DNS01KeyAuthorization(), d.host.Addrs()) + return SendChallenge(ctx, d.forge, d.host.Peerstore().PrivKey(d.host.ID()), challenge.DNS01KeyAuthorization(), d.host.Addrs()) } func (d *dns01P2PForgeSolver) CleanUp(ctx context.Context, challenge acme.Challenge) error { diff --git a/client/challenge.go b/client/challenge.go index 69e644b..f947075 100644 --- a/client/challenge.go +++ b/client/challenge.go @@ -3,69 +3,45 @@ package client import ( "bytes" "context" - "encoding/base64" "encoding/json" - "errors" "fmt" + httppeeridauth "github.com/libp2p/go-libp2p/p2p/http/auth" "io" "net/http" "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/record" - "github.com/libp2p/go-libp2p/core/record/pb" "github.com/multiformats/go-multiaddr" - "google.golang.org/protobuf/proto" ) // SendChallenge submits a challenge to the DNS server for the given peerID. // It requires the corresponding private key and a list of multiaddresses that the peerID is listening on using // publicly reachable IP addresses. -func SendChallenge(ctx context.Context, baseURL string, peerID peer.ID, privKey crypto.PrivKey, challenge string, addrs []multiaddr.Multiaddr) error { +func SendChallenge(ctx context.Context, baseURL string, privKey crypto.PrivKey, challenge string, addrs []multiaddr.Multiaddr) error { maStrs := make([]string, len(addrs)) for i, addr := range addrs { maStrs[i] = addr.String() } - var requestBody = &requestRecord{ + + body, err := json.Marshal(&struct { + Value string `json:"value"` + Addresses []string `json:"addresses"` + }{ Value: challenge, Addresses: maStrs, - } - - env, err := record.Seal(requestBody, privKey) - if err != nil { - return err - } - envBytes, err := env.Marshal() + }) if err != nil { return err } - var pbEnv pb.Envelope - if err := proto.Unmarshal(envBytes, &pbEnv); err != nil { - return err - } - authHeader := base64.StdEncoding.EncodeToString(pbEnv.Signature) - pk, err := peerID.ExtractPublicKey() - if err != nil && errors.Is(err, peer.ErrNoPublicKey) { - return err - } - if pk == nil { - pk = env.PublicKey - pkBytes, err := crypto.MarshalPublicKey(pk) - if err != nil { - return err - } - base64EncodedPk := base64.StdEncoding.EncodeToString(pkBytes) - authHeader += "." + base64EncodedPk - } - - req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/v1/%s/_acme-challenge", baseURL, peerID), bytes.NewReader(pbEnv.Payload)) + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/v1/_acme-challenge", baseURL), bytes.NewReader(body)) if err != nil { return err } - req.Header.Add("Authorization", authHeader) - resp, err := http.DefaultClient.Do(req) + client := &httppeeridauth.ClientPeerIDAuth{PrivKey: privKey} + _, resp, err := client.AuthenticatedDo(http.DefaultClient, func() *http.Request { + return req + }) if err != nil { return err } @@ -75,30 +51,3 @@ func SendChallenge(ctx context.Context, baseURL string, peerID peer.ID, privKey } return nil } - -type requestRecord struct { - Value string `json:"value"` - Addresses []string `json:"addresses"` -} - -func (r *requestRecord) Domain() string { - return "peer-forge-domain-challenge" -} - -func (r *requestRecord) Codec() []byte { - return []byte("/peer-forge-domain-challenge") -} - -func (r *requestRecord) MarshalRecord() ([]byte, error) { - out, err := json.Marshal(r) - if err != nil { - return nil, err - } - return out, nil -} - -func (r *requestRecord) UnmarshalRecord(bytes []byte) error { - return json.Unmarshal(bytes, r) -} - -var _ record.Record = (*requestRecord)(nil) diff --git a/cmd/e2e_test.go b/cmd/e2e_test.go index ff6ad93..79b31e8 100644 --- a/cmd/e2e_test.go +++ b/cmd/e2e_test.go @@ -123,7 +123,7 @@ func TestSetACMEChallenge(t *testing.T) { testDigest := sha256.Sum256([]byte("test")) testChallenge := base64.RawURLEncoding.EncodeToString(testDigest[:]) - if err := client.SendChallenge(ctx, fmt.Sprintf("http://127.0.0.1:%d", httpPort), h.ID(), sk, testChallenge, h.Addrs()); err != nil { + if err := client.SendChallenge(ctx, fmt.Sprintf("http://127.0.0.1:%d", httpPort), sk, testChallenge, h.Addrs()); err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index 1eec860..493b20c 100644 --- a/go.mod +++ b/go.mod @@ -13,15 +13,12 @@ require ( github.com/ipfs/go-ds-dynamodb v0.1.1 github.com/ipfs/go-log/v2 v2.5.1 github.com/letsencrypt/pebble/v2 v2.6.0 - github.com/libp2p/go-buffer-pool v0.1.0 - github.com/libp2p/go-libp2p v0.36.3-0.20240830152458-d55bed5f78fa + github.com/libp2p/go-libp2p v0.36.3-0.20240903225031-fbcede220897 github.com/mholt/acmez/v2 v2.0.1 github.com/miekg/dns v1.1.61 github.com/multiformats/go-multiaddr v0.13.0 github.com/multiformats/go-multiaddr-dns v0.3.1 github.com/multiformats/go-multibase v0.2.0 - github.com/multiformats/go-varint v0.0.7 - google.golang.org/protobuf v1.34.2 ) require ( @@ -113,6 +110,7 @@ require ( github.com/koron/go-ssdp v0.0.4 // indirect github.com/letsencrypt/challtestsrv v1.3.2 // indirect github.com/libdns/libdns v0.2.2 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect @@ -137,6 +135,7 @@ require ( github.com/multiformats/go-multicodec v0.9.0 // indirect github.com/multiformats/go-multihash v0.2.3 // indirect github.com/multiformats/go-multistream v0.5.0 // indirect + github.com/multiformats/go-varint v0.0.7 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/ginkgo/v2 v2.19.1 // indirect github.com/opencontainers/runtime-spec v1.2.0 // indirect @@ -212,6 +211,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/DataDog/dd-trace-go.v1 v1.62.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index ebf920e..d757a93 100644 --- a/go.sum +++ b/go.sum @@ -328,8 +328,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.36.3-0.20240830152458-d55bed5f78fa h1:fpHzZMdrj/X7HrqoD1yPY56YTlIlX7l3d0sSraekjJQ= -github.com/libp2p/go-libp2p v0.36.3-0.20240830152458-d55bed5f78fa/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8= +github.com/libp2p/go-libp2p v0.36.3-0.20240903225031-fbcede220897 h1:OCIXsMBv93Db1pVVOM95wlyr0oUf8XE6pFktd6nhFu0= +github.com/libp2p/go-libp2p v0.36.3-0.20240903225031-fbcede220897/go.mod h1:4Y5vFyCUiJuluEPmpnKYf6WFx5ViKPUYs/ixe9ANFZ8= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA=