Skip to content

Commit

Permalink
feat: switch to http peerID auth
Browse files Browse the repository at this point in the history
  • Loading branch information
aschmahmann committed Sep 4, 2024
1 parent 999ba3c commit 6f12d69
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 204 deletions.
15 changes: 2 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,23 +58,12 @@ To claim a domain name like `<peerID>.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/<peerID>/_acme-challenge" \
-H "Authorization: Bearer <signature>.<public_key>"
-H "Authorization: libp2p-PeerID bearer=\"<base64-encoded-opaque-blob>\""
-H "Content-Type: application/json" \
-d '{
"value": "your_acme_challenge_token",
"addresses": "[your_multiaddrs, comma_separated]"
}'
```

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.
Where the bearer token is derived via the [libp2p HTTP PeerID Auth Specification](https://github.com/libp2p/specs/pull/564).
162 changes: 40 additions & 122 deletions acme/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -23,6 +22,7 @@ import (
"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"
)
Expand Down Expand Up @@ -57,144 +57,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) }()

Expand Down
2 changes: 1 addition & 1 deletion client/acme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
77 changes: 13 additions & 64 deletions client/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
2 changes: 1 addition & 1 deletion cmd/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit 6f12d69

Please sign in to comment.