From 2e7b82eecf3fef373fe44f0ce3d67b9157142b4b Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Sat, 29 Jun 2024 12:01:14 -0700 Subject: [PATCH 01/31] Initial implementation of http peer id auth --- p2p/http/auth/auth.go | 253 ++++++++++++++++++++++ p2p/http/auth/auth_test.go | 240 +++++++++++++++++++++ p2p/http/auth/client.go | 181 ++++++++++++++++ p2p/http/auth/server.go | 418 +++++++++++++++++++++++++++++++++++++ 4 files changed, 1092 insertions(+) create mode 100644 p2p/http/auth/auth.go create mode 100644 p2p/http/auth/auth_test.go create mode 100644 p2p/http/auth/client.go create mode 100644 p2p/http/auth/server.go diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go new file mode 100644 index 0000000000..3dab102b1f --- /dev/null +++ b/p2p/http/auth/auth.go @@ -0,0 +1,253 @@ +package httppeeridauth + +import ( + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "regexp" + "slices" + "strings" + + logging "github.com/ipfs/go-log/v2" + pool "github.com/libp2p/go-buffer-pool" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +const PeerIDAuthScheme = "libp2p-PeerID" +const BearerAuthScheme = "libp2p-Bearer" +const serverAuthPrefix = PeerIDAuthScheme + " challenge-client=" +const challengeLen = 32 + +var log = logging.Logger("httppeeridauth") + +type authScheme struct { + scheme string + params map[string]string + bearerToken string +} + +const maxSchemes = 4 +const maxParams = 10 + +var paramRegexStr = `([\w-]+)=([\w\d-_=.]+|"[^"]+")` +var paramRegex = regexp.MustCompile(paramRegexStr) + +var authHeaderRegex = regexp.MustCompile(fmt.Sprintf(`(%s\s+[^,\s]+)|(%s+\s+(:?(:?%s)(:?\s*,\s*)?)*)`, BearerAuthScheme, PeerIDAuthScheme, paramRegexStr)) + +func parseAuthHeader(headerVal string) (map[string]authScheme, error) { + if len(headerVal) > maxAuthHeaderSize { + return nil, fmt.Errorf("header too long") + } + schemes := authHeaderRegex.FindAllString(headerVal, maxSchemes+1) + if len(schemes) > maxSchemes { + return nil, fmt.Errorf("too many schemes") + } + + if len(schemes) == 0 { + return nil, nil + } + + out := make([]authScheme, 0, 2) + for _, s := range schemes { + s = strings.TrimSpace(s) + schemeEndIdx := strings.IndexByte(s, ' ') + if schemeEndIdx == -1 { + continue + } + scheme := authScheme{scheme: s[:schemeEndIdx]} + switch scheme.scheme { + case BearerAuthScheme, PeerIDAuthScheme: + default: + // Ignore unknown schemes + continue + } + params := s[schemeEndIdx+1:] + if scheme.scheme == BearerAuthScheme { + scheme.bearerToken = params + out = append(out, scheme) + continue + } + scheme.params = make(map[string]string, 10) + params = strings.TrimSpace(params) + for _, kv := range paramRegex.FindAllStringSubmatch(params, maxParams) { + if len(kv) != 3 { + return nil, fmt.Errorf("invalid param format") + } + scheme.params[kv[1]] = strings.Trim(kv[2], `"`) + } + out = append(out, scheme) + } + if len(out) == 0 { + return nil, nil + } + + outMap := make(map[string]authScheme, len(out)) + for _, s := range out { + outMap[s.scheme] = s + } + return outMap, nil +} + +func verifySig(publicKey crypto.PubKey, prefix string, signedParts []string, sig []byte) error { + b := pool.Get(4096) + defer pool.Put(b) + buf, err := genDataToSign(b[:0], prefix, signedParts) + if err != nil { + return fmt.Errorf("failed to generate signed data: %w", err) + } + ok, err := publicKey.Verify(buf, sig) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("signature verification failed") + } + + return nil +} + +func sign(privKey crypto.PrivKey, prefix string, partsToSign []string) ([]byte, error) { + b := pool.Get(4096) + defer pool.Put(b) + buf, err := genDataToSign(b[:0], prefix, partsToSign) + if err != nil { + return nil, fmt.Errorf("failed to generate data to sign: %w", err) + } + return privKey.Sign(buf) +} + +func genDataToSign(buf []byte, prefix string, parts []string) ([]byte, error) { + // Sort the parts in alphabetical order + slices.Sort(parts) + buf = append(buf, []byte(prefix)...) + for _, p := range parts { + buf = binary.AppendUvarint(buf, uint64(len(p))) + buf = append(buf, p...) + } + return buf, nil +} + +type authFields struct { + origin string + pubKey crypto.PubKey + opaque string + challengeServer []byte + challengeClient []byte + signature []byte +} + +func decodeB64PubKey(b64EncodedPubKey string) (crypto.PubKey, error) { + bLen := base64.URLEncoding.DecodedLen(len(b64EncodedPubKey)) + buf := pool.Get(bLen) + defer pool.Put(buf) + + buf, err := base64.URLEncoding.AppendDecode(buf[:0], []byte(b64EncodedPubKey)) + if err != nil { + return nil, err + } + return crypto.UnmarshalPublicKey(buf) +} + +func parseAuthFields(authHeader string, origin string, isServer bool) (authFields, error) { + if authHeader == "" { + return authFields{}, errMissingAuthHeader + } + if len(authHeader) > maxAuthHeaderSize { + return authFields{}, errors.New("authorization header too large") + } + + schemes, err := parseAuthHeader(authHeader) + if err != nil { + return authFields{}, err + } + + peerIDAuth, ok := schemes[PeerIDAuthScheme] + if !ok { + return authFields{}, errors.New("no peer ID auth scheme found") + } + + if isServer && peerIDAuth.params["sig"] == "" { + return authFields{}, errors.New("no signature found") + } + sig, err := base64.URLEncoding.DecodeString(peerIDAuth.params["sig"]) + if err != nil { + return authFields{}, fmt.Errorf("failed to decode signature: %s", err) + } + + var pubKey crypto.PubKey + var id peer.ID + if peerIDAuth.params["peer-id"] != "" { + id, err = peer.Decode(peerIDAuth.params["peer-id"]) + if err != nil { + return authFields{}, fmt.Errorf("failed to decode peer ID: %s", err) + } + pubKey, err = id.ExtractPublicKey() + if err != nil && err != peer.ErrNoPublicKey { + return authFields{}, err + } + if err == peer.ErrNoPublicKey { + // RSA key perhaps, see if there is a public-key param + encodedPubKey, ok := peerIDAuth.params["public-key"] + if !ok { + return authFields{}, errors.New("no public key found") + } + pubKey, err = decodeB64PubKey(encodedPubKey) + if err != nil { + return authFields{}, fmt.Errorf("failed to unmarshal public key: %s", err) + } + idFromKey, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return authFields{}, fmt.Errorf("failed to get peer ID from public key: %s", err) + } + if id != idFromKey { + return authFields{}, errors.New("peer ID from public key does not match peer ID") + } + } else { + if encodedPubKey, ok := peerIDAuth.params["public-key"]; ok { + // If there's a public key param, it must match the public key from the peer ID + pubKeyFromParam, err := decodeB64PubKey(encodedPubKey) + if err != nil { + return authFields{}, fmt.Errorf("failed to unmarshal public key: %s", err) + } + if !pubKeyFromParam.Equals(pubKey) { + return authFields{}, errors.New("public key from peer ID does not match public key from param") + } + } + } + } + + var challengeServer []byte + if peerIDAuth.params["challenge-server"] != "" { + challengeServer, err = base64.URLEncoding.DecodeString(peerIDAuth.params["challenge-server"]) + if err != nil { + return authFields{}, fmt.Errorf("failed to decode challenge: %s", err) + } + } + + var challengeClient []byte + if !isServer && peerIDAuth.params["challenge-client"] != "" { + // Only parse this for the client. The server should read this from the opaque field + challengeClient, err = base64.URLEncoding.DecodeString(peerIDAuth.params["challenge-client"]) + if err != nil { + return authFields{}, fmt.Errorf("failed to decode challenge: %s", err) + } + } + + return authFields{ + origin: origin, + pubKey: pubKey, + opaque: peerIDAuth.params["opaque"], + challengeServer: challengeServer, + challengeClient: challengeClient, + signature: sig, + }, nil +} + +// TODOs +// - update spec to mention base64 url encoding +// - Use string builder and put them in a pool +// - benchmark allocs +// - mutual auth +// - an expiration time in opaque token diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go new file mode 100644 index 0000000000..ca7753c342 --- /dev/null +++ b/p2p/http/auth/auth_test.go @@ -0,0 +1,240 @@ +package httppeeridauth + +import ( + "bytes" + "context" + "crypto/rand" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" +) + +// TestMutualAuth tests that we can do a mutually authenticated round trip +func TestMutualAuth(t *testing.T) { + logging.SetLogLevel("httppeeridauth", "DEBUG") + + zeroBytes := make([]byte, 64) + serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + require.NoError(t, err) + auth := PeerIDAuth{ + PrivKey: serverKey, + ValidOrigins: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, + } + + ts := httptest.NewServer(&auth) + defer ts.Close() + + type testCase struct { + name string + clientKeyGen func(t *testing.T) crypto.PrivKey + } + + testCases := []testCase{ + { + name: "ED25519", + clientKeyGen: func(t *testing.T) crypto.PrivKey { + clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + return clientKey + }, + }, + { + name: "RSA", + clientKeyGen: func(t *testing.T) crypto.PrivKey { + clientKey, _, err := crypto.GenerateRSAKeyPair(2048, rand.Reader) + require.NoError(t, err) + return clientKey + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + client := &http.Client{} + clientKey := tc.clientKeyGen(t) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + expectedServerID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + + ctx := context.Background() + serverID, err := clientAuth.mutualAuth(ctx, client, ts.URL, "example.com") + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tokenMap["example.com"]) + + // Once more with the auth token + req, err := http.NewRequest("GET", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, err = clientAuth.AddAuthTokenToRequest(req) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + + // Verify that unwrapping our token gives us the client's peer ID + expectedClientPeerID, err := peer.IDFromPrivateKey(clientKey) + require.NoError(t, err) + clientPeerID, err := auth.UnwrapBearerToken(req) + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + // Verify that we can make an authenticated request + resp, err := client.Do(req) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + } +} + +func TestParseAuthHeader(t *testing.T) { + testCases := []struct { + name string + header string + expected map[string]authScheme + err error + }{ + { + name: "empty header", + header: "", + expected: nil, + err: nil, + }, + { + name: "header too long", + header: strings.Repeat("a", maxAuthHeaderSize+1), + expected: nil, + err: fmt.Errorf("header too long"), + }, + { + name: "too many schemes", + header: strings.Repeat("libp2p-Bearer token1, ", maxSchemes+1), + expected: nil, + err: fmt.Errorf("too many schemes"), + }, + { + name: "Valid Bearer scheme", + header: "libp2p-Bearer token123", + expected: map[string]authScheme{"libp2p-Bearer": {bearerToken: "token123", scheme: "libp2p-Bearer"}}, + err: nil, + }, + { + name: "Valid PeerID scheme", + header: "libp2p-PeerID param1=val1, param2=val2", + expected: map[string]authScheme{"libp2p-PeerID": {scheme: "libp2p-PeerID", params: map[string]string{"param1": "val1", "param2": "val2"}}}, + err: nil, + }, + { + name: "Ignore unknown scheme", + header: "Unknown scheme1, libp2p-Bearer token456, libp2p-PeerID param=value", + expected: map[string]authScheme{ + "libp2p-Bearer": { + scheme: "libp2p-Bearer", + bearerToken: "token456"}, + "libp2p-PeerID": {scheme: "libp2p-PeerID", params: map[string]string{"param": "value"}}}, + err: nil, + }, + { + name: "Parse quoted params", + header: `libp2p-PeerID param="value"`, + expected: map[string]authScheme{ + "libp2p-PeerID": {scheme: "libp2p-PeerID", params: map[string]string{"param": "value"}}}, + err: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := parseAuthHeader(tc.header) + if tc.err != nil { + require.Error(t, err, tc.err) + require.Equal(t, tc.err.Error(), err.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tc.expected, actual) + } + }) + } +} + +func FuzzParseAuthHeader(f *testing.F) { + // Just check that we don't panic' + f.Fuzz(func(t *testing.T, data []byte) { + parseAuthHeader(string(data)) + }) +} + +func FuzzServeHTTP(f *testing.F) { + zeroBytes := make([]byte, 64) + serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + require.NoError(f, err) + auth := PeerIDAuth{ + PrivKey: serverKey, + ValidOrigins: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, + } + // Just check that we don't panic' + f.Fuzz(func(t *testing.T, data []byte) { + if len(data) == 0 { + return + } + hostLen := int(data[0]) + data = data[1:] + if hostLen > len(data) { + return + } + host := string(data[:hostLen]) + data = data[hostLen:] + req := httptest.NewRequest("GET", "http://example.com", nil) + req.Host = host + req.Header.Set("Authorization", string(data)) + auth.ServeHTTP(httptest.NewRecorder(), req) + }) +} + +func BenchmarkAuths(b *testing.B) { + zeroBytes := make([]byte, 64) + serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + require.NoError(b, err) + auth := PeerIDAuth{ + PrivKey: serverKey, + ValidOrigins: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, + } + + ts := httptest.NewServer(&auth) + defer ts.Close() + + ctx := context.Background() + client := &http.Client{} + clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(b, err) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + clientID, err := peer.IDFromPrivateKey(clientKey) + require.NoError(b, err) + challengeServer := make([]byte, challengeLen) + clientAuthValue, err := clientAuth.authSelfToServer(ctx, client, clientID, challengeServer, ts.URL, "example.com") + require.NoError(b, err) + + b.ResetTimer() + req, err := http.NewRequest("GET", ts.URL, nil) + require.NoError(b, err) + req.Host = "example.com" + req.Header.Set("Authorization", clientAuthValue) + + for i := 0; i < b.N; i++ { + resp, err := client.Do(req) + if err != nil || resp.StatusCode != http.StatusOK { + b.Fatal(err, resp.StatusCode) + } + } +} diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go new file mode 100644 index 0000000000..834f441641 --- /dev/null +++ b/p2p/http/auth/client.go @@ -0,0 +1,181 @@ +package httppeeridauth + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "net/http" + "sync" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +type ClientPeerIDAuth struct { + PrivKey crypto.PrivKey + tokenMapMu sync.Mutex + tokenMap map[string]tokenInfo +} + +type tokenInfo struct { + peerID peer.ID + token string +} + +var ErrNoAuthToken = errors.New("no auth token found") + +// AddAuthTokenToRequest adds the libp2p-Bearer token to the request. Returns the peer ID of the server. +func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, error) { + a.tokenMapMu.Lock() + defer a.tokenMapMu.Unlock() + if a.tokenMap == nil { + a.tokenMap = make(map[string]tokenInfo) + } + + t, ok := a.tokenMap[req.Host] + if !ok { + return "", ErrNoAuthToken + } + + req.Header.Set("Authorization", BearerAuthScheme+" "+t.token) + return t.peerID, nil +} + +// mutualAuth performs mutual authentication with the server at the given endpoint. Returns the server's peer id. +func (a *ClientPeerIDAuth) mutualAuth(ctx context.Context, client *http.Client, authEndpoint string, origin string) (peer.ID, error) { + if a.PrivKey == nil { + return "", errors.New("no private key set") + } + + myPeerID, err := peer.IDFromPrivateKey(a.PrivKey) + if err != nil { + return "", fmt.Errorf("failed to get peer ID: %w", err) + } + + var challengeServer [challengeLen]byte + _, err = rand.Read(challengeServer[:]) + if err != nil { + return "", fmt.Errorf("failed to generate challenge-server: %w", err) + } + authValue, err := a.authSelfToServer(ctx, client, myPeerID, challengeServer[:], authEndpoint, origin) + if err != nil { + return "", fmt.Errorf("failed to authenticate self to server: %w", err) + } + + authServerReq, err := http.NewRequestWithContext(ctx, "GET", authEndpoint, nil) + authServerReq.Host = origin + if err != nil { + return "", fmt.Errorf("failed to create request to authenticate server: %w", err) + } + authServerReq.Header.Set("Authorization", authValue) + resp, err := client.Do(authServerReq) + if err != nil { + return "", fmt.Errorf("failed to do authenticate server request: %w", err) + } + resp.Body.Close() + + // Verify the server's signature + respAuth, err := parseAuthFields(resp.Header.Get("Authentication-Info"), origin, false) + if err != nil { + return "", fmt.Errorf("failed to parse Authentication-Info header: %w", err) + } + serverID, err := a.verifySigFromServer(respAuth, myPeerID, challengeServer[:]) + if err != nil { + return "", fmt.Errorf("failed to authenticate server: %w", err) + } + + // Auth succeeded, store the token + respAuthSchemes, err := parseAuthHeader(resp.Header.Get("Authorization")) + if err != nil { + return "", fmt.Errorf("failed to parse auth header: %w", err) + } + + if bearer, ok := respAuthSchemes[BearerAuthScheme]; ok { + a.tokenMapMu.Lock() + if a.tokenMap == nil { + a.tokenMap = make(map[string]tokenInfo) + } + a.tokenMap[origin] = tokenInfo{token: bearer.bearerToken, peerID: serverID} + a.tokenMapMu.Unlock() + } + + return serverID, nil +} + +// authSelfToServer performs the initial authentication request to the server. It authenticates the client to the server. +// Returns the Authorization value with libp2p-PeerID scheme to use for subsequent requests. +func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Client, myPeerID peer.ID, challengeServer []byte, authEndpoint string, origin string) (string, error) { + r, err := http.NewRequestWithContext(ctx, "GET", authEndpoint, nil) + r.Host = origin + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + // do the initial auth request + resp, err := client.Do(r) + if err != nil { + return "", fmt.Errorf("failed to do initial auth request: %w", err) + } + if resp.StatusCode != http.StatusUnauthorized { + return "", nil + } + resp.Body.Close() + + authHeader := resp.Header.Get("WWW-Authenticate") + f, err := parseAuthFields(authHeader, origin, false) + if err != nil { + return "", fmt.Errorf("failed to parse our auth header: %w", err) + } + + if len(f.challengeClient) == 0 { + return "", errors.New("missing challenge") + } + + challengeClientb64 := base64.URLEncoding.EncodeToString([]byte(f.challengeClient)) + sig, err := sign(a.PrivKey, PeerIDAuthScheme, []string{ + "challenge-client=" + challengeClientb64, + fmt.Sprintf(`origin="%s"`, origin), + }) + if err != nil { + return "", fmt.Errorf("failed to sign challenge: %w", err) + } + + authValue := fmt.Sprintf( + "%s peer-id=%s, sig=%s, opaque=%s, challenge-server=%s", + PeerIDAuthScheme, + myPeerID.String(), + base64.URLEncoding.EncodeToString(sig), + f.opaque, + base64.URLEncoding.EncodeToString([]byte(challengeServer)), + ) + + // Attempt to read public key from our peer id + _, err = myPeerID.ExtractPublicKey() + if err == peer.ErrNoPublicKey { + // If it fails we need to include the public key explicitly + pubKey := a.PrivKey.GetPublic() + pubKeyBytes, err := crypto.MarshalPublicKey(pubKey) + if err != nil { + return "", fmt.Errorf("failed to marshal public key: %w", err) + } + authValue += ", public-key=" + base64.URLEncoding.EncodeToString(pubKeyBytes) + } else if err != nil { + return "", fmt.Errorf("failed to extract public key: %w", err) + } + return authValue, nil +} + +func (a *ClientPeerIDAuth) verifySigFromServer(r authFields, myPeerID peer.ID, challengeServer []byte) (peer.ID, error) { + partsToVerify := make([]string, 0, 3) + partsToVerify = append(partsToVerify, fmt.Sprintf(`origin="%s"`, r.origin)) + partsToVerify = append(partsToVerify, "challenge-server="+base64.URLEncoding.EncodeToString(challengeServer)) + partsToVerify = append(partsToVerify, "client="+myPeerID.String()) + + err := verifySig(r.pubKey, PeerIDAuthScheme, partsToVerify, r.signature) + if err != nil { + return "", fmt.Errorf("failed to verify signature: %s", err) + } + return peer.IDFromPublicKey(r.pubKey) +} diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go new file mode 100644 index 0000000000..bef419c59e --- /dev/null +++ b/p2p/http/auth/server.go @@ -0,0 +1,418 @@ +package httppeeridauth + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "net/http" + "strings" + "time" + + pool "github.com/libp2p/go-buffer-pool" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +const maxAuthHeaderSize = 8192 + +const challengeTTL = 5 * time.Minute + +type PeerIDAuth struct { + PrivKey crypto.PrivKey + ValidOrigins map[string]struct{} + TokenTTL time.Duration + Next http.Handler +} + +var errMissingAuthHeader = errors.New("missing header") + +// ServeHTTP implements the http.Handler interface for PeerIDAuth. It will +// attempt to authenticate the request using using the libp2p peer ID auth +// scheme. If a Next handler is set, it will be called on authenticated +// requests. +func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Do they have a bearer token? + _, err := a.UnwrapBearerToken(r) + if err != nil { + // No bearer token, let's try peer ID auth + _, ok := a.ValidOrigins[r.Host] + if !ok { + log.Debugf("Unauthorized request from %s: invalid origin", r.Host) + w.WriteHeader(http.StatusBadRequest) + return + } + + f, err := parseAuthFields(r.Header.Get("Authorization"), r.Host, true) + if err != nil { + a.serveAuthReq(w) + return + } + + var id peer.ID + id, err = a.authenticate(f) + if err != nil { + log.Debugf("failed to authenticate: %s", err) + a.serveAuthReq(w) + return + } + + tok := bearerToken{ + peer: id, + origin: f.origin, + createdAt: time.Now(), + } + b := pool.Get(4096) + defer pool.Put(b) + b, err = genBearerTokenBlob(b[:0], a.PrivKey, tok) + if err != nil { + log.Debugf("failed to generate bearer token: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + blobB64 := pool.Get(len(BearerAuthScheme) + 1 + base64.URLEncoding.EncodedLen(len(b))) + defer pool.Put(blobB64) + + blobB64 = blobB64[:0] + blobB64 = append(blobB64, BearerAuthScheme...) + blobB64 = append(blobB64, ' ') + blobB64 = base64.URLEncoding.AppendEncode(blobB64, b) + + w.Header().Set("Authorization", string(blobB64)) + + if len(f.challengeServer) >= challengeLen { + buf, err := a.signChallengeServer(f) + if err != nil { + log.Debugf("failed to sign challenge: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + myId, err := peer.IDFromPublicKey(a.PrivKey.GetPublic()) + if err != nil { + log.Debugf("failed to get peer ID: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + authInfoVal := fmt.Sprintf("%s peer-id=%s, sig=%s", PeerIDAuthScheme, myId.String(), base64.URLEncoding.EncodeToString(buf)) + w.Header().Set("Authentication-Info", authInfoVal) + } + + if a.Next != nil { + // Set the token on the request so the next handler can read it + authHeader := r.Header.Get("Authorization") + if authHeader != "" { + authHeader = string(blobB64) + ", " + authHeader + } else { + authHeader = string(blobB64) + } + r.Header.Set("Authorization", string(blobB64)+", "+authHeader) + } + } + + if a.Next == nil { + // No next handler, just return + w.WriteHeader(http.StatusOK) + return + } + a.Next.ServeHTTP(w, r) +} + +func (a *PeerIDAuth) signChallengeServer(f authFields) ([]byte, error) { + if len(f.challengeServer) == 0 { + return nil, errors.New("missing challenge") + } + challengeb64 := base64.URLEncoding.EncodeToString([]byte(f.challengeServer)) + clientID, err := peer.IDFromPublicKey(f.pubKey) + if err != nil { + return nil, fmt.Errorf("failed to get client ID: %w", err) + } + partsToSign := []string{ + "challenge-server=" + challengeb64, + "client=" + clientID.String(), + fmt.Sprintf(`origin="%s"`, f.origin), + } + sig, err := sign(a.PrivKey, PeerIDAuthScheme, partsToSign) + if err != nil { + return nil, fmt.Errorf("failed to sign challenge: %w", err) + } + return sig, nil +} + +func (a *PeerIDAuth) authenticate(f authFields) (peer.ID, error) { + partsToVerify := make([]string, 0, 2) + o, err := getChallengeFromOpaque(a.PrivKey, []byte(f.opaque)) + if err != nil { + return "", fmt.Errorf("failed to get challenge from opaque: %s", err) + } + if time.Now().After(o.createdTime.Add(challengeTTL)) { + return "", errors.New("challenge expired") + } + challengeClient := o.challenge + if len(challengeClient) > 0 { + partsToVerify = append(partsToVerify, "challenge-client="+base64.URLEncoding.EncodeToString(challengeClient)) + } + partsToVerify = append(partsToVerify, fmt.Sprintf(`origin="%s"`, f.origin)) + + err = verifySig(f.pubKey, PeerIDAuthScheme, partsToVerify, f.signature) + if err != nil { + return "", fmt.Errorf("failed to verify signature: %s", err) + } + return peer.IDFromPublicKey(f.pubKey) +} + +func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request) (peer.ID, error) { + if !strings.Contains(r.Header.Get("Authorization"), BearerAuthScheme) { + return "", errors.New("missing bearer auth scheme") + } + schemes, err := parseAuthHeader(r.Header.Get("Authorization")) + if err != nil { + return "", fmt.Errorf("failed to parse auth header: %w", err) + } + bearerScheme, ok := schemes[BearerAuthScheme] + if !ok { + return "", fmt.Errorf("missing bearer auth scheme") + } + return a.unwrapBearerToken(r.Host, bearerScheme) +} + +func (a *PeerIDAuth) unwrapBearerToken(expectedOrigin string, s authScheme) (peer.ID, error) { + buf := pool.Get(4096) + defer pool.Put(buf) + buf, err := base64.URLEncoding.AppendDecode(buf[:0], []byte(s.bearerToken)) + if err != nil { + return "", fmt.Errorf("failed to decode bearer token: %w", err) + } + parsed, err := parseBearerTokenBlob(a.PrivKey, buf) + if err != nil { + return "", fmt.Errorf("failed to parse bearer token: %w", err) + } + if time.Now().After(parsed.createdAt.Add(a.TokenTTL)) { + return "", fmt.Errorf("bearer token expired") + } + if parsed.origin != expectedOrigin { + return "", fmt.Errorf("bearer token origin mismatch") + } + return parsed.peer, nil +} + +type bearerToken struct { + peer peer.ID + origin string + createdAt time.Time +} + +func genBearerTokenBlob(buf []byte, privKey crypto.PrivKey, t bearerToken) ([]byte, error) { + peerBytes, err := t.peer.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal peer ID: %w", err) + } + createdAtBytes, err := t.createdAt.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal createdAt: %w", err) + } + + // Auth scheme prefix + buf = append(buf, BearerAuthScheme...) + buf = append(buf, ' ') // Space between auth scheme and the token + + // Peer ID + buf = binary.AppendUvarint(buf, uint64(len(peerBytes))) + buf = append(buf, peerBytes...) + + // Origin + buf = binary.AppendUvarint(buf, uint64(len(t.origin))) + buf = append(buf, t.origin...) + + // Created at + buf = binary.AppendUvarint(buf, uint64(len(createdAtBytes))) + buf = append(buf, createdAtBytes...) + + sig, err := privKey.Sign(buf) + if err != nil { + return nil, fmt.Errorf("failed to sign bearer token: %w", err) + } + buf = append(buf, sig...) + + return buf, nil +} + +func parseBearerTokenBlob(privKey crypto.PrivKey, blob []byte) (bearerToken, error) { + originalSlice := blob + + // Auth scheme prefix +1 for space + if len(BearerAuthScheme)+1 > len(blob) { + return bearerToken{}, fmt.Errorf("bearer token too short") + } + hasPrefix := bytes.Equal([]byte(BearerAuthScheme), blob[:len(BearerAuthScheme)]) + if !hasPrefix { + return bearerToken{}, fmt.Errorf("missing bearer token prefix") + } + blob = blob[len(BearerAuthScheme):] + if blob[0] != ' ' { + return bearerToken{}, fmt.Errorf("missing space after auth scheme") + } + blob = blob[1:] + + // Peer ID + peerIDLen, n := binary.Uvarint(blob) + if n <= 0 { + return bearerToken{}, fmt.Errorf("failed to read peer ID length") + } + + blob = blob[n:] + if int(peerIDLen) > len(blob) { + return bearerToken{}, fmt.Errorf("peer ID length is wrong") + } + var peer peer.ID + err := peer.UnmarshalBinary(blob[:peerIDLen]) + if err != nil { + return bearerToken{}, fmt.Errorf("failed to unmarshal peer ID: %w", err) + } + blob = blob[peerIDLen:] + + // Origin + originLen, n := binary.Uvarint(blob) + if n <= 0 { + return bearerToken{}, fmt.Errorf("failed to read origin length") + } + blob = blob[n:] + if int(originLen) > len(blob) { + return bearerToken{}, fmt.Errorf("origin length is wrong") + } + origin := string(blob[:originLen]) + blob = blob[originLen:] + + // Created At + createdAtLen, n := binary.Uvarint(blob) + if n <= 0 { + return bearerToken{}, fmt.Errorf("failed to read created at length") + } + blob = blob[n:] + if int(createdAtLen) > len(blob) { + return bearerToken{}, fmt.Errorf("created at length is wrong") + } + var createdAt time.Time + err = createdAt.UnmarshalBinary(blob[:createdAtLen]) + if err != nil { + return bearerToken{}, fmt.Errorf("failed to unmarshal created at: %w", err) + } + sig := blob[createdAtLen:] + if len(sig) == 0 { + return bearerToken{}, fmt.Errorf("missing signature") + } + + blobWithoutSig := originalSlice[:len(originalSlice)-len(sig)] + ok, err := privKey.GetPublic().Verify(blobWithoutSig, sig) + if err != nil { + return bearerToken{}, fmt.Errorf("failed to verify signature: %w", err) + } + if !ok { + return bearerToken{}, fmt.Errorf("signature verification failed") + } + return bearerToken{peer: peer, origin: origin, createdAt: createdAt}, nil +} + +type opaqueUnwrapped struct { + challenge []byte + createdTime time.Time +} + +func getChallengeFromOpaque(privKey crypto.PrivKey, opaqueB64 []byte) (opaqueUnwrapped, error) { + if len(opaqueB64) == 0 { + return opaqueUnwrapped{}, fmt.Errorf("missing opaque blob") + } + + opaqueBlob := pool.Get(2048) + defer pool.Put(opaqueBlob) + opaqueBlob, err := base64.URLEncoding.AppendDecode(opaqueBlob[:0], opaqueB64) + if err != nil { + return opaqueUnwrapped{}, fmt.Errorf("failed to decode opaque blob: %w", err) + } + if len(opaqueBlob) == 0 { + return opaqueUnwrapped{}, fmt.Errorf("missing opaque blob") + } + if len(opaqueBlob) < challengeLen { + return opaqueUnwrapped{}, fmt.Errorf("opaque blob too short") + } + + // The form of the opaque blob is: + timeBytesLen, n := binary.Uvarint(opaqueBlob) + if n <= 0 { + return opaqueUnwrapped{}, fmt.Errorf("failed to read timeBytesLen") + } + fullPayload := opaqueBlob // Store the full payload so we can verify the signature + opaqueBlob = opaqueBlob[n:] + timeBytes := opaqueBlob[:timeBytesLen] + createdTime := time.Time{} + err = createdTime.UnmarshalBinary(timeBytes) + if err != nil { + return opaqueUnwrapped{}, fmt.Errorf("failed to unmarshal time: %w", err) + } + opaqueBlob = opaqueBlob[timeBytesLen:] + challenge := opaqueBlob[:challengeLen] + opaqueBlob = opaqueBlob[challengeLen:] + sig := opaqueBlob + payloadWithoutSig := fullPayload[:len(fullPayload)-len(opaqueBlob)] + ok, err := privKey.GetPublic().Verify(payloadWithoutSig, sig) + if err != nil { + return opaqueUnwrapped{}, fmt.Errorf("signature verification failed: %w", err) + } + if !ok { + return opaqueUnwrapped{}, fmt.Errorf("signature verification failed") + } + challengeCopy := make([]byte, challengeLen) // Copy the challenge because the underlying buffer will be returned to the pool + copy(challengeCopy, challenge) + return opaqueUnwrapped{ + challenge: challengeCopy, + createdTime: createdTime, + }, nil +} + +func genOpaqueFromChallenge(buf []byte, now time.Time, privKey crypto.PrivKey, challenge []byte) ([]byte, error) { + timeBytes, err := now.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to marshal time: %w", err) + } + buf = binary.AppendUvarint(buf, uint64(len(timeBytes))) + buf = append(buf, timeBytes...) + buf = append(buf, challenge...) + sig, err := privKey.Sign(buf) + if err != nil { + return nil, fmt.Errorf("failed to sign challenge: %w", err) + } + buf = append(buf, sig...) + return buf, nil +} + +func (a *PeerIDAuth) serveAuthReq(w http.ResponseWriter) { + var challenge [challengeLen]byte + _, err := rand.Read(challenge[:]) + if err != nil { + log.Warnf("failed to generate challenge: %s", err) + w.WriteHeader(http.StatusInternalServerError) + } + + tmp := pool.Get(2048) + defer pool.Put(tmp) + opaque, err := genOpaqueFromChallenge(tmp[:0], time.Now(), a.PrivKey, challenge[:]) + if err != nil { + log.Warnf("failed to generate opaque: %s", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + authHeaderVal := pool.Get(2048) + defer pool.Put(authHeaderVal) + authHeaderVal = authHeaderVal[:0] + authHeaderVal = append(authHeaderVal, serverAuthPrefix...) + authHeaderVal = base64.URLEncoding.AppendEncode(authHeaderVal, challenge[:]) + authHeaderVal = append(authHeaderVal, ", opaque="...) + authHeaderVal = base64.URLEncoding.AppendEncode(authHeaderVal, opaque) + + w.Header().Set("WWW-Authenticate", string(authHeaderVal)) + w.WriteHeader(http.StatusUnauthorized) +} From 54c6fa1eef61520e17563ed14e4640176a11c68a Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Sat, 29 Jun 2024 12:25:52 -0700 Subject: [PATCH 02/31] export client mutual auth --- p2p/http/auth/auth_test.go | 2 +- p2p/http/auth/client.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index ca7753c342..f321e3d7cd 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -67,7 +67,7 @@ func TestMutualAuth(t *testing.T) { require.NoError(t, err) ctx := context.Background() - serverID, err := clientAuth.mutualAuth(ctx, client, ts.URL, "example.com") + serverID, err := clientAuth.MutualAuth(ctx, client, ts.URL, "example.com") require.NoError(t, err) require.Equal(t, expectedServerID, serverID) require.NotZero(t, clientAuth.tokenMap["example.com"]) diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 834f441641..ad978aac0e 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -43,8 +43,8 @@ func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, er return t.peerID, nil } -// mutualAuth performs mutual authentication with the server at the given endpoint. Returns the server's peer id. -func (a *ClientPeerIDAuth) mutualAuth(ctx context.Context, client *http.Client, authEndpoint string, origin string) (peer.ID, error) { +// MutualAuth performs mutual authentication with the server at the given endpoint. Returns the server's peer id. +func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, authEndpoint string, origin string) (peer.ID, error) { if a.PrivKey == nil { return "", errors.New("no private key set") } From 30a0ac36c3b8b9219dbc94ea1244f69cf551930c Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 1 Jul 2024 14:32:52 -0700 Subject: [PATCH 03/31] Add test vectors --- p2p/http/auth/auth_test.go | 79 ++++++++++++++++++++++++++++++++++++++ p2p/http/auth/client.go | 14 ++++--- p2p/http/auth/server.go | 22 ++++++----- 3 files changed, 100 insertions(+), 15 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index f321e3d7cd..4be635683c 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -4,9 +4,12 @@ import ( "bytes" "context" "crypto/rand" + "encoding/base64" + "encoding/hex" "fmt" "net/http" "net/http/httptest" + "net/url" "strings" "testing" "time" @@ -238,3 +241,79 @@ func BenchmarkAuths(b *testing.B) { } } } + +// Test Vectors +var zeroBytes = make([]byte, 64) +var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + +// Peer ID derived from the zero key +var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) + +// Result of signing with a zero key and a 32 0 byte challenge with origin "example.com" +var expectedClientSig = `56975c7694351cca10bf1c84fee1d49df86b6e356d8ff3208080b9cb49098d1e437845d87aacd15f908aabc8031ddc769721bb6bb9e4d1f2d2fc85b6d3c99e07` + +// Result of signing with a zero key and a 32 0 byte challenge with origin +// "example.com" and client ID derived from the zero key +var expectedServerSig = `4bc1ac4653cb2fa816b10793c2597da7bb4ab1391cd5e75332b96482a216f9cda197dcfb92727dbbacee9ad6859f3dc9edea5ab43fe6abbfa49c095efaeaa60e` + +type inputToSigning struct { + prefix string + params map[string]string +} + +// 32 0 bytes encoded in base64 +var zeroBytesB64 = base64.URLEncoding.EncodeToString(make([]byte, 32)) +var inputToSigningTestVectors = []struct { + name string + input inputToSigning + percentEncodedOutput string +}{ + { + name: "What the client signs", + input: inputToSigning{ + prefix: PeerIDAuthScheme, + params: map[string]string{"challenge-client": zeroBytesB64, "origin": "example.com"}, + }, + percentEncodedOutput: "libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%12origin=example.com", + }, { + name: "What the server signs", + input: inputToSigning{ + prefix: PeerIDAuthScheme, + params: map[string]string{"challenge-server": zeroBytesB64, "origin": "example.com", "client": zeroID.String()}, + }, + percentEncodedOutput: "libp2p-PeerID=challenge-server=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%3Bclient=12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN%12origin=example.com", + }, +} + +func TestSigningVectors(t *testing.T) { + t.Run("Inputs to signing", func(t *testing.T) { + for _, test := range inputToSigningTestVectors { + t.Run(test.name, func(t *testing.T) { + params := make([]string, 0, len(test.input.params)) + for k, v := range test.input.params { + params = append(params, fmt.Sprintf("%s=%s", k, v)) + } + out, err := genDataToSign(nil, test.input.prefix, params) + require.NoError(t, err) + require.Equal(t, test.percentEncodedOutput, url.PathEscape(string(out))) + }) + } + }) + t.Run("Client sig", func(t *testing.T) { + client := ClientPeerIDAuth{PrivKey: zeroKey} + challengeClient := make([]byte, challengeLen) + origin := "example.com" + sig, err := client.sign(challengeClient, origin) + require.NoError(t, err) + require.Equal(t, expectedClientSig, hex.EncodeToString(sig)) + }) + + t.Run("Server sig", func(t *testing.T) { + server := PeerIDAuth{PrivKey: zeroKey} + challengeServer := make([]byte, challengeLen) + origin := "example.com" + sig, err := server.signChallengeServer(challengeServer, zeroID, origin) + require.NoError(t, err) + require.Equal(t, expectedServerSig, hex.EncodeToString(sig)) + }) +} diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index ad978aac0e..2adcebfb82 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -104,6 +104,14 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, return serverID, nil } +func (a *ClientPeerIDAuth) sign(challengeClient []byte, origin string) ([]byte, error) { + challengeClientb64 := base64.URLEncoding.EncodeToString([]byte(challengeClient)) + return sign(a.PrivKey, PeerIDAuthScheme, []string{ + "challenge-client=" + challengeClientb64, + fmt.Sprintf(`origin="%s"`, origin), + }) +} + // authSelfToServer performs the initial authentication request to the server. It authenticates the client to the server. // Returns the Authorization value with libp2p-PeerID scheme to use for subsequent requests. func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Client, myPeerID peer.ID, challengeServer []byte, authEndpoint string, origin string) (string, error) { @@ -133,11 +141,7 @@ func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Cl return "", errors.New("missing challenge") } - challengeClientb64 := base64.URLEncoding.EncodeToString([]byte(f.challengeClient)) - sig, err := sign(a.PrivKey, PeerIDAuthScheme, []string{ - "challenge-client=" + challengeClientb64, - fmt.Sprintf(`origin="%s"`, origin), - }) + sig, err := a.sign(f.challengeClient, origin) if err != nil { return "", fmt.Errorf("failed to sign challenge: %w", err) } diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index bef419c59e..a9eed3901c 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -83,7 +83,13 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Authorization", string(blobB64)) if len(f.challengeServer) >= challengeLen { - buf, err := a.signChallengeServer(f) + clientID, err := peer.IDFromPublicKey(f.pubKey) + if err != nil { + log.Debugf("failed to get peer ID: %s", err) + w.WriteHeader(http.StatusBadRequest) + return + } + buf, err := a.signChallengeServer(f.challengeServer, clientID, f.origin) if err != nil { log.Debugf("failed to sign challenge: %s", err) w.WriteHeader(http.StatusInternalServerError) @@ -121,19 +127,15 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.Next.ServeHTTP(w, r) } -func (a *PeerIDAuth) signChallengeServer(f authFields) ([]byte, error) { - if len(f.challengeServer) == 0 { +func (a *PeerIDAuth) signChallengeServer(challengeServer []byte, client peer.ID, origin string) ([]byte, error) { + if len(challengeServer) == 0 { return nil, errors.New("missing challenge") } - challengeb64 := base64.URLEncoding.EncodeToString([]byte(f.challengeServer)) - clientID, err := peer.IDFromPublicKey(f.pubKey) - if err != nil { - return nil, fmt.Errorf("failed to get client ID: %w", err) - } + challengeb64 := base64.URLEncoding.EncodeToString([]byte(challengeServer)) partsToSign := []string{ "challenge-server=" + challengeb64, - "client=" + clientID.String(), - fmt.Sprintf(`origin="%s"`, f.origin), + "client=" + client.String(), + fmt.Sprintf(`origin="%s"`, origin), } sig, err := sign(a.PrivKey, PeerIDAuthScheme, partsToSign) if err != nil { From 62d887a90f526a85b12d9b9723c01b2652c8ba6f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 2 Jul 2024 13:57:24 -0700 Subject: [PATCH 04/31] Add test walkthrough for spec --- p2p/http/auth/auth_test.go | 105 +++++++++++++++---------------------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 4be635683c..b738164826 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -249,71 +249,52 @@ var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) // Peer ID derived from the zero key var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) -// Result of signing with a zero key and a 32 0 byte challenge with origin "example.com" -var expectedClientSig = `56975c7694351cca10bf1c84fee1d49df86b6e356d8ff3208080b9cb49098d1e437845d87aacd15f908aabc8031ddc769721bb6bb9e4d1f2d2fc85b6d3c99e07` +func genClientID(t *testing.T) (peer.ID, crypto.PrivKey) { + clientPrivStr, err := hex.DecodeString("080112407e0830617c4a7de83925dfb2694556b12936c477a0e1feb2e148ec9da60fee7d1ed1e8fae2c4a144b8be8fd4b47bf3d3b34b871c3cacf6010f0e42d474fce27e") + require.NoError(t, err) + clientKey, err := crypto.UnmarshalPrivateKey(clientPrivStr) + require.NoError(t, err) + clientID, err := peer.IDFromPrivateKey(clientKey) + require.NoError(t, err) + return clientID, clientKey +} -// Result of signing with a zero key and a 32 0 byte challenge with origin -// "example.com" and client ID derived from the zero key -var expectedServerSig = `4bc1ac4653cb2fa816b10793c2597da7bb4ab1391cd5e75332b96482a216f9cda197dcfb92727dbbacee9ad6859f3dc9edea5ab43fe6abbfa49c095efaeaa60e` +// TestWalkthroughInSpec tests the walkthrough example in libp2p/specs +func TestWalkthroughInSpec(t *testing.T) { + zeroBytes := make([]byte, 32) + clientID, clientKey := genClientID(t) + require.Equal(t, "12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq", clientID.String()) -type inputToSigning struct { - prefix string - params map[string]string -} + challengeClientb64 := base64.URLEncoding.EncodeToString(zeroBytes) + require.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", challengeClientb64) + challengeServer64 := "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" -// 32 0 bytes encoded in base64 -var zeroBytesB64 = base64.URLEncoding.EncodeToString(make([]byte, 32)) -var inputToSigningTestVectors = []struct { - name string - input inputToSigning - percentEncodedOutput string -}{ - { - name: "What the client signs", - input: inputToSigning{ - prefix: PeerIDAuthScheme, - params: map[string]string{"challenge-client": zeroBytesB64, "origin": "example.com"}, - }, - percentEncodedOutput: "libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%12origin=example.com", - }, { - name: "What the server signs", - input: inputToSigning{ - prefix: PeerIDAuthScheme, - params: map[string]string{"challenge-server": zeroBytesB64, "origin": "example.com", "client": zeroID.String()}, - }, - percentEncodedOutput: "libp2p-PeerID=challenge-server=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%3Bclient=12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN%12origin=example.com", - }, -} + origin := "example.com" -func TestSigningVectors(t *testing.T) { - t.Run("Inputs to signing", func(t *testing.T) { - for _, test := range inputToSigningTestVectors { - t.Run(test.name, func(t *testing.T) { - params := make([]string, 0, len(test.input.params)) - for k, v := range test.input.params { - params = append(params, fmt.Sprintf("%s=%s", k, v)) - } - out, err := genDataToSign(nil, test.input.prefix, params) - require.NoError(t, err) - require.Equal(t, test.percentEncodedOutput, url.PathEscape(string(out))) - }) - } - }) - t.Run("Client sig", func(t *testing.T) { - client := ClientPeerIDAuth{PrivKey: zeroKey} - challengeClient := make([]byte, challengeLen) - origin := "example.com" - sig, err := client.sign(challengeClient, origin) - require.NoError(t, err) - require.Equal(t, expectedClientSig, hex.EncodeToString(sig)) - }) + clientParts := []string{ + "challenge-client=" + challengeClientb64, + fmt.Sprintf(`origin="%s"`, origin), + } + toSign, err := genDataToSign(nil, PeerIDAuthScheme, clientParts) + require.NoError(t, err) + require.Equal(t, "libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%14origin=%22example.com%22", url.PathEscape(string(toSign))) + sig, err := sign(clientKey, PeerIDAuthScheme, clientParts) + require.NoError(t, err) + require.Equal(t, "MKoR8Shzr6VmQ675dErKh_gGGUsGaO8zXnZ8Cx8bIKiQlYBhqazUG8w4lG3_Wd5IfSz5P1HLfXtVb_fg_dsxDw==", base64.URLEncoding.EncodeToString(sig)) - t.Run("Server sig", func(t *testing.T) { - server := PeerIDAuth{PrivKey: zeroKey} - challengeServer := make([]byte, challengeLen) - origin := "example.com" - sig, err := server.signChallengeServer(challengeServer, zeroID, origin) - require.NoError(t, err) - require.Equal(t, expectedServerSig, hex.EncodeToString(sig)) - }) + serverID := zeroID + require.Equal(t, "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN", serverID.String()) + + serverParts := []string{ + "challenge-server=" + challengeServer64, + "client=" + clientID.String(), + fmt.Sprintf(`origin="%s"`, origin), + } + toSign, err = genDataToSign(nil, PeerIDAuthScheme, serverParts) + require.NoError(t, err) + require.Equal(t, "libp2p-PeerID=challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=%3Bclient=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq%14origin=%22example.com%22", url.PathEscape(string(toSign))) + + sig, err = sign(zeroKey, PeerIDAuthScheme, serverParts) + require.NoError(t, err) + require.Equal(t, "m0OkSsO9YGcqfZ_XVTbiRwTtM4ds8434D9aod22Mmo3Wm0vBvxHOd71glC-uEez6g5gjA580KkGc9DOIvP47BQ==", base64.URLEncoding.EncodeToString(sig)) } From 5041d970a3d2ba6f212ed3ea3ef14b4dbe6a9c4a Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 2 Jul 2024 14:02:10 -0700 Subject: [PATCH 05/31] Don't bother decoding challenges --- p2p/http/auth/auth.go | 48 ++++++++++++++--------------------------- p2p/http/auth/client.go | 9 ++++---- p2p/http/auth/server.go | 11 +++++----- 3 files changed, 25 insertions(+), 43 deletions(-) diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index 3dab102b1f..3366d52639 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -130,12 +130,12 @@ func genDataToSign(buf []byte, prefix string, parts []string) ([]byte, error) { } type authFields struct { - origin string - pubKey crypto.PubKey - opaque string - challengeServer []byte - challengeClient []byte - signature []byte + origin string + pubKey crypto.PubKey + opaque string + challengeServerB64 string + challengeClientB64 string + signature []byte } func decodeB64PubKey(b64EncodedPubKey string) (crypto.PubKey, error) { @@ -218,36 +218,20 @@ func parseAuthFields(authHeader string, origin string, isServer bool) (authField } } - var challengeServer []byte - if peerIDAuth.params["challenge-server"] != "" { - challengeServer, err = base64.URLEncoding.DecodeString(peerIDAuth.params["challenge-server"]) - if err != nil { - return authFields{}, fmt.Errorf("failed to decode challenge: %s", err) - } - } + challengeServer := peerIDAuth.params["challenge-server"] - var challengeClient []byte - if !isServer && peerIDAuth.params["challenge-client"] != "" { + var challengeClient string + if !isServer { // Only parse this for the client. The server should read this from the opaque field - challengeClient, err = base64.URLEncoding.DecodeString(peerIDAuth.params["challenge-client"]) - if err != nil { - return authFields{}, fmt.Errorf("failed to decode challenge: %s", err) - } + challengeClient = peerIDAuth.params["challenge-client"] } return authFields{ - origin: origin, - pubKey: pubKey, - opaque: peerIDAuth.params["opaque"], - challengeServer: challengeServer, - challengeClient: challengeClient, - signature: sig, + origin: origin, + pubKey: pubKey, + opaque: peerIDAuth.params["opaque"], + challengeServerB64: challengeServer, + challengeClientB64: challengeClient, + signature: sig, }, nil } - -// TODOs -// - update spec to mention base64 url encoding -// - Use string builder and put them in a pool -// - benchmark allocs -// - mutual auth -// - an expiration time in opaque token diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 2adcebfb82..2c99ba7f7c 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -104,10 +104,9 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, return serverID, nil } -func (a *ClientPeerIDAuth) sign(challengeClient []byte, origin string) ([]byte, error) { - challengeClientb64 := base64.URLEncoding.EncodeToString([]byte(challengeClient)) +func (a *ClientPeerIDAuth) sign(challengeClientB64 string, origin string) ([]byte, error) { return sign(a.PrivKey, PeerIDAuthScheme, []string{ - "challenge-client=" + challengeClientb64, + "challenge-client=" + challengeClientB64, fmt.Sprintf(`origin="%s"`, origin), }) } @@ -137,11 +136,11 @@ func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Cl return "", fmt.Errorf("failed to parse our auth header: %w", err) } - if len(f.challengeClient) == 0 { + if len(f.challengeClientB64) == 0 { return "", errors.New("missing challenge") } - sig, err := a.sign(f.challengeClient, origin) + sig, err := a.sign(f.challengeClientB64, origin) if err != nil { return "", fmt.Errorf("failed to sign challenge: %w", err) } diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index a9eed3901c..65f77ad41b 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -82,14 +82,14 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Authorization", string(blobB64)) - if len(f.challengeServer) >= challengeLen { + if base64.URLEncoding.DecodedLen(len(f.challengeServerB64)) >= challengeLen { clientID, err := peer.IDFromPublicKey(f.pubKey) if err != nil { log.Debugf("failed to get peer ID: %s", err) w.WriteHeader(http.StatusBadRequest) return } - buf, err := a.signChallengeServer(f.challengeServer, clientID, f.origin) + buf, err := a.signChallengeServer(f.challengeServerB64, clientID, f.origin) if err != nil { log.Debugf("failed to sign challenge: %s", err) w.WriteHeader(http.StatusInternalServerError) @@ -127,13 +127,12 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.Next.ServeHTTP(w, r) } -func (a *PeerIDAuth) signChallengeServer(challengeServer []byte, client peer.ID, origin string) ([]byte, error) { - if len(challengeServer) == 0 { +func (a *PeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, origin string) ([]byte, error) { + if len(challengeServerB64) == 0 { return nil, errors.New("missing challenge") } - challengeb64 := base64.URLEncoding.EncodeToString([]byte(challengeServer)) partsToSign := []string{ - "challenge-server=" + challengeb64, + "challenge-server=" + challengeServerB64, "client=" + client.String(), fmt.Sprintf(`origin="%s"`, origin), } From 94ba8ae8d28896f3a609b8be4c885b17f3cb7ec9 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 2 Jul 2024 14:09:51 -0700 Subject: [PATCH 06/31] Backport AppendEncode/AppendDecode --- p2p/http/auth/auth.go | 28 +++++++++++++++++++++++++++- p2p/http/auth/server.go | 10 +++++----- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index 3366d52639..af136349b0 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -143,7 +143,7 @@ func decodeB64PubKey(b64EncodedPubKey string) (crypto.PubKey, error) { buf := pool.Get(bLen) defer pool.Put(buf) - buf, err := base64.URLEncoding.AppendDecode(buf[:0], []byte(b64EncodedPubKey)) + buf, err := b64AppendDecode(buf[:0], []byte(b64EncodedPubKey)) if err != nil { return nil, err } @@ -235,3 +235,29 @@ func parseAuthFields(authHeader string, origin string, isServer bool) (authField signature: sig, }, nil } + +// Same as base64.URLEncoding.AppendEncode, but backported for Go 1.21. Once we are on Go 1.23 we can drop this +func b64AppendEncode(dst, src []byte) []byte { + enc := base64.URLEncoding + n := enc.EncodedLen(len(src)) + dst = slices.Grow(dst, n) + enc.Encode(dst[len(dst):][:n], src) + return dst[:len(dst)+n] +} + +// Same as base64.URLEncoding.AppendDecode, but backported for Go 1.21. Once we are on Go 1.23 we can drop this +func b64AppendDecode(dst, src []byte) ([]byte, error) { + enc := base64.URLEncoding + encNoPad := base64.RawURLEncoding + + // Compute the output size without padding to avoid over allocating. + n := len(src) + for n > 0 && rune(src[n-1]) == base64.StdPadding { + n-- + } + n = encNoPad.DecodedLen(n) + + dst = slices.Grow(dst, n) + n, err := enc.Decode(dst[len(dst):][:n], src) + return dst[:len(dst)+n], err +} diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 65f77ad41b..50fc40111f 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -78,7 +78,7 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { blobB64 = blobB64[:0] blobB64 = append(blobB64, BearerAuthScheme...) blobB64 = append(blobB64, ' ') - blobB64 = base64.URLEncoding.AppendEncode(blobB64, b) + blobB64 = b64AppendEncode(blobB64, b) w.Header().Set("Authorization", string(blobB64)) @@ -183,7 +183,7 @@ func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request) (peer.ID, error) { func (a *PeerIDAuth) unwrapBearerToken(expectedOrigin string, s authScheme) (peer.ID, error) { buf := pool.Get(4096) defer pool.Put(buf) - buf, err := base64.URLEncoding.AppendDecode(buf[:0], []byte(s.bearerToken)) + buf, err := b64AppendDecode(buf[:0], []byte(s.bearerToken)) if err != nil { return "", fmt.Errorf("failed to decode bearer token: %w", err) } @@ -329,7 +329,7 @@ func getChallengeFromOpaque(privKey crypto.PrivKey, opaqueB64 []byte) (opaqueUnw opaqueBlob := pool.Get(2048) defer pool.Put(opaqueBlob) - opaqueBlob, err := base64.URLEncoding.AppendDecode(opaqueBlob[:0], opaqueB64) + opaqueBlob, err := b64AppendDecode(opaqueBlob[:0], opaqueB64) if err != nil { return opaqueUnwrapped{}, fmt.Errorf("failed to decode opaque blob: %w", err) } @@ -410,9 +410,9 @@ func (a *PeerIDAuth) serveAuthReq(w http.ResponseWriter) { defer pool.Put(authHeaderVal) authHeaderVal = authHeaderVal[:0] authHeaderVal = append(authHeaderVal, serverAuthPrefix...) - authHeaderVal = base64.URLEncoding.AppendEncode(authHeaderVal, challenge[:]) + authHeaderVal = b64AppendEncode(authHeaderVal, challenge[:]) authHeaderVal = append(authHeaderVal, ", opaque="...) - authHeaderVal = base64.URLEncoding.AppendEncode(authHeaderVal, opaque) + authHeaderVal = b64AppendEncode(authHeaderVal, opaque) w.Header().Set("WWW-Authenticate", string(authHeaderVal)) w.WriteHeader(http.StatusUnauthorized) From ad2ee53e471df25a06d6e46fff6ab368986021ce Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 2 Jul 2024 14:26:50 -0700 Subject: [PATCH 07/31] Rename origin to hostname --- p2p/http/auth/auth.go | 6 ++--- p2p/http/auth/auth_test.go | 32 +++++++++++------------ p2p/http/auth/client.go | 24 +++++++++--------- p2p/http/auth/server.go | 52 +++++++++++++++++++------------------- 4 files changed, 57 insertions(+), 57 deletions(-) diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index af136349b0..4fc297bf72 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -130,7 +130,7 @@ func genDataToSign(buf []byte, prefix string, parts []string) ([]byte, error) { } type authFields struct { - origin string + hostname string pubKey crypto.PubKey opaque string challengeServerB64 string @@ -150,7 +150,7 @@ func decodeB64PubKey(b64EncodedPubKey string) (crypto.PubKey, error) { return crypto.UnmarshalPublicKey(buf) } -func parseAuthFields(authHeader string, origin string, isServer bool) (authFields, error) { +func parseAuthFields(authHeader string, hostname string, isServer bool) (authFields, error) { if authHeader == "" { return authFields{}, errMissingAuthHeader } @@ -227,7 +227,7 @@ func parseAuthFields(authHeader string, origin string, isServer bool) (authField } return authFields{ - origin: origin, + hostname: hostname, pubKey: pubKey, opaque: peerIDAuth.params["opaque"], challengeServerB64: challengeServer, diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index b738164826..806dba6a36 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -28,9 +28,9 @@ func TestMutualAuth(t *testing.T) { serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(t, err) auth := PeerIDAuth{ - PrivKey: serverKey, - ValidOrigins: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, + PrivKey: serverKey, + ValidHostnames: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, } ts := httptest.NewServer(&auth) @@ -181,9 +181,9 @@ func FuzzServeHTTP(f *testing.F) { serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(f, err) auth := PeerIDAuth{ - PrivKey: serverKey, - ValidOrigins: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, + PrivKey: serverKey, + ValidHostnames: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, } // Just check that we don't panic' f.Fuzz(func(t *testing.T, data []byte) { @@ -209,9 +209,9 @@ func BenchmarkAuths(b *testing.B) { serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(b, err) auth := PeerIDAuth{ - PrivKey: serverKey, - ValidOrigins: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, + PrivKey: serverKey, + ValidHostnames: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, } ts := httptest.NewServer(&auth) @@ -269,18 +269,18 @@ func TestWalkthroughInSpec(t *testing.T) { require.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", challengeClientb64) challengeServer64 := "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" - origin := "example.com" + hostname := "example.com" clientParts := []string{ "challenge-client=" + challengeClientb64, - fmt.Sprintf(`origin="%s"`, origin), + fmt.Sprintf(`hostname="%s"`, hostname), } toSign, err := genDataToSign(nil, PeerIDAuthScheme, clientParts) require.NoError(t, err) - require.Equal(t, "libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%14origin=%22example.com%22", url.PathEscape(string(toSign))) + require.Equal(t, "libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%16hostname=%22example.com%22", url.PathEscape(string(toSign))) sig, err := sign(clientKey, PeerIDAuthScheme, clientParts) require.NoError(t, err) - require.Equal(t, "MKoR8Shzr6VmQ675dErKh_gGGUsGaO8zXnZ8Cx8bIKiQlYBhqazUG8w4lG3_Wd5IfSz5P1HLfXtVb_fg_dsxDw==", base64.URLEncoding.EncodeToString(sig)) + require.Equal(t, "F5OBYbbMXoIVJNWrW0UANi7rrbj4GCB6kcEceQjajLTMvC-_jpBF9MFlxiaNYXOEiPQqeo_S56YUSNinwl0ZCQ==", base64.URLEncoding.EncodeToString(sig)) serverID := zeroID require.Equal(t, "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN", serverID.String()) @@ -288,13 +288,13 @@ func TestWalkthroughInSpec(t *testing.T) { serverParts := []string{ "challenge-server=" + challengeServer64, "client=" + clientID.String(), - fmt.Sprintf(`origin="%s"`, origin), + fmt.Sprintf(`hostname="%s"`, hostname), } toSign, err = genDataToSign(nil, PeerIDAuthScheme, serverParts) require.NoError(t, err) - require.Equal(t, "libp2p-PeerID=challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=%3Bclient=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq%14origin=%22example.com%22", url.PathEscape(string(toSign))) + require.Equal(t, "libp2p-PeerID=challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=%3Bclient=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq%16hostname=%22example.com%22", url.PathEscape(string(toSign))) sig, err = sign(zeroKey, PeerIDAuthScheme, serverParts) require.NoError(t, err) - require.Equal(t, "m0OkSsO9YGcqfZ_XVTbiRwTtM4ds8434D9aod22Mmo3Wm0vBvxHOd71glC-uEez6g5gjA580KkGc9DOIvP47BQ==", base64.URLEncoding.EncodeToString(sig)) + require.Equal(t, "btLFqW200aDTQqpkKetJJje7V-iDknXygFqPsfiegNsboXeYDiQ6Rqcpezz1wfr8j9h83QkN9z78cAWzKzV_AQ==", base64.URLEncoding.EncodeToString(sig)) } diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 2c99ba7f7c..19cbce0ac1 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -44,7 +44,7 @@ func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, er } // MutualAuth performs mutual authentication with the server at the given endpoint. Returns the server's peer id. -func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, authEndpoint string, origin string) (peer.ID, error) { +func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, authEndpoint string, hostname string) (peer.ID, error) { if a.PrivKey == nil { return "", errors.New("no private key set") } @@ -59,13 +59,13 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, if err != nil { return "", fmt.Errorf("failed to generate challenge-server: %w", err) } - authValue, err := a.authSelfToServer(ctx, client, myPeerID, challengeServer[:], authEndpoint, origin) + authValue, err := a.authSelfToServer(ctx, client, myPeerID, challengeServer[:], authEndpoint, hostname) if err != nil { return "", fmt.Errorf("failed to authenticate self to server: %w", err) } authServerReq, err := http.NewRequestWithContext(ctx, "GET", authEndpoint, nil) - authServerReq.Host = origin + authServerReq.Host = hostname if err != nil { return "", fmt.Errorf("failed to create request to authenticate server: %w", err) } @@ -77,7 +77,7 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, resp.Body.Close() // Verify the server's signature - respAuth, err := parseAuthFields(resp.Header.Get("Authentication-Info"), origin, false) + respAuth, err := parseAuthFields(resp.Header.Get("Authentication-Info"), hostname, false) if err != nil { return "", fmt.Errorf("failed to parse Authentication-Info header: %w", err) } @@ -97,25 +97,25 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, if a.tokenMap == nil { a.tokenMap = make(map[string]tokenInfo) } - a.tokenMap[origin] = tokenInfo{token: bearer.bearerToken, peerID: serverID} + a.tokenMap[hostname] = tokenInfo{token: bearer.bearerToken, peerID: serverID} a.tokenMapMu.Unlock() } return serverID, nil } -func (a *ClientPeerIDAuth) sign(challengeClientB64 string, origin string) ([]byte, error) { +func (a *ClientPeerIDAuth) sign(challengeClientB64 string, hostname string) ([]byte, error) { return sign(a.PrivKey, PeerIDAuthScheme, []string{ "challenge-client=" + challengeClientB64, - fmt.Sprintf(`origin="%s"`, origin), + fmt.Sprintf(`hostname="%s"`, hostname), }) } // authSelfToServer performs the initial authentication request to the server. It authenticates the client to the server. // Returns the Authorization value with libp2p-PeerID scheme to use for subsequent requests. -func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Client, myPeerID peer.ID, challengeServer []byte, authEndpoint string, origin string) (string, error) { +func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Client, myPeerID peer.ID, challengeServer []byte, authEndpoint string, hostname string) (string, error) { r, err := http.NewRequestWithContext(ctx, "GET", authEndpoint, nil) - r.Host = origin + r.Host = hostname if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } @@ -131,7 +131,7 @@ func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Cl resp.Body.Close() authHeader := resp.Header.Get("WWW-Authenticate") - f, err := parseAuthFields(authHeader, origin, false) + f, err := parseAuthFields(authHeader, hostname, false) if err != nil { return "", fmt.Errorf("failed to parse our auth header: %w", err) } @@ -140,7 +140,7 @@ func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Cl return "", errors.New("missing challenge") } - sig, err := a.sign(f.challengeClientB64, origin) + sig, err := a.sign(f.challengeClientB64, hostname) if err != nil { return "", fmt.Errorf("failed to sign challenge: %w", err) } @@ -172,7 +172,7 @@ func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Cl func (a *ClientPeerIDAuth) verifySigFromServer(r authFields, myPeerID peer.ID, challengeServer []byte) (peer.ID, error) { partsToVerify := make([]string, 0, 3) - partsToVerify = append(partsToVerify, fmt.Sprintf(`origin="%s"`, r.origin)) + partsToVerify = append(partsToVerify, fmt.Sprintf(`hostname="%s"`, r.hostname)) partsToVerify = append(partsToVerify, "challenge-server="+base64.URLEncoding.EncodeToString(challengeServer)) partsToVerify = append(partsToVerify, "client="+myPeerID.String()) diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 50fc40111f..e4540331bb 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -21,10 +21,10 @@ const maxAuthHeaderSize = 8192 const challengeTTL = 5 * time.Minute type PeerIDAuth struct { - PrivKey crypto.PrivKey - ValidOrigins map[string]struct{} - TokenTTL time.Duration - Next http.Handler + PrivKey crypto.PrivKey + ValidHostnames map[string]struct{} + TokenTTL time.Duration + Next http.Handler } var errMissingAuthHeader = errors.New("missing header") @@ -38,9 +38,9 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { _, err := a.UnwrapBearerToken(r) if err != nil { // No bearer token, let's try peer ID auth - _, ok := a.ValidOrigins[r.Host] + _, ok := a.ValidHostnames[r.Host] if !ok { - log.Debugf("Unauthorized request from %s: invalid origin", r.Host) + log.Debugf("Unauthorized request from %s: invalid hostname", r.Host) w.WriteHeader(http.StatusBadRequest) return } @@ -61,7 +61,7 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { tok := bearerToken{ peer: id, - origin: f.origin, + hostname: f.hostname, createdAt: time.Now(), } b := pool.Get(4096) @@ -89,7 +89,7 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) return } - buf, err := a.signChallengeServer(f.challengeServerB64, clientID, f.origin) + buf, err := a.signChallengeServer(f.challengeServerB64, clientID, f.hostname) if err != nil { log.Debugf("failed to sign challenge: %s", err) w.WriteHeader(http.StatusInternalServerError) @@ -127,14 +127,14 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.Next.ServeHTTP(w, r) } -func (a *PeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, origin string) ([]byte, error) { +func (a *PeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, hostname string) ([]byte, error) { if len(challengeServerB64) == 0 { return nil, errors.New("missing challenge") } partsToSign := []string{ "challenge-server=" + challengeServerB64, "client=" + client.String(), - fmt.Sprintf(`origin="%s"`, origin), + fmt.Sprintf(`hostname="%s"`, hostname), } sig, err := sign(a.PrivKey, PeerIDAuthScheme, partsToSign) if err != nil { @@ -156,7 +156,7 @@ func (a *PeerIDAuth) authenticate(f authFields) (peer.ID, error) { if len(challengeClient) > 0 { partsToVerify = append(partsToVerify, "challenge-client="+base64.URLEncoding.EncodeToString(challengeClient)) } - partsToVerify = append(partsToVerify, fmt.Sprintf(`origin="%s"`, f.origin)) + partsToVerify = append(partsToVerify, fmt.Sprintf(`hostname="%s"`, f.hostname)) err = verifySig(f.pubKey, PeerIDAuthScheme, partsToVerify, f.signature) if err != nil { @@ -180,7 +180,7 @@ func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request) (peer.ID, error) { return a.unwrapBearerToken(r.Host, bearerScheme) } -func (a *PeerIDAuth) unwrapBearerToken(expectedOrigin string, s authScheme) (peer.ID, error) { +func (a *PeerIDAuth) unwrapBearerToken(expectedHostname string, s authScheme) (peer.ID, error) { buf := pool.Get(4096) defer pool.Put(buf) buf, err := b64AppendDecode(buf[:0], []byte(s.bearerToken)) @@ -194,15 +194,15 @@ func (a *PeerIDAuth) unwrapBearerToken(expectedOrigin string, s authScheme) (pee if time.Now().After(parsed.createdAt.Add(a.TokenTTL)) { return "", fmt.Errorf("bearer token expired") } - if parsed.origin != expectedOrigin { - return "", fmt.Errorf("bearer token origin mismatch") + if parsed.hostname != expectedHostname { + return "", fmt.Errorf("bearer token hostname mismatch") } return parsed.peer, nil } type bearerToken struct { peer peer.ID - origin string + hostname string createdAt time.Time } @@ -224,9 +224,9 @@ func genBearerTokenBlob(buf []byte, privKey crypto.PrivKey, t bearerToken) ([]by buf = binary.AppendUvarint(buf, uint64(len(peerBytes))) buf = append(buf, peerBytes...) - // Origin - buf = binary.AppendUvarint(buf, uint64(len(t.origin))) - buf = append(buf, t.origin...) + // Hostname + buf = binary.AppendUvarint(buf, uint64(len(t.hostname))) + buf = append(buf, t.hostname...) // Created at buf = binary.AppendUvarint(buf, uint64(len(createdAtBytes))) @@ -275,17 +275,17 @@ func parseBearerTokenBlob(privKey crypto.PrivKey, blob []byte) (bearerToken, err } blob = blob[peerIDLen:] - // Origin - originLen, n := binary.Uvarint(blob) + // Hostname + hostnameLen, n := binary.Uvarint(blob) if n <= 0 { - return bearerToken{}, fmt.Errorf("failed to read origin length") + return bearerToken{}, fmt.Errorf("failed to read hostname length") } blob = blob[n:] - if int(originLen) > len(blob) { - return bearerToken{}, fmt.Errorf("origin length is wrong") + if int(hostnameLen) > len(blob) { + return bearerToken{}, fmt.Errorf("hostname length is wrong") } - origin := string(blob[:originLen]) - blob = blob[originLen:] + hostname := string(blob[:hostnameLen]) + blob = blob[hostnameLen:] // Created At createdAtLen, n := binary.Uvarint(blob) @@ -314,7 +314,7 @@ func parseBearerTokenBlob(privKey crypto.PrivKey, blob []byte) (bearerToken, err if !ok { return bearerToken{}, fmt.Errorf("signature verification failed") } - return bearerToken{peer: peer, origin: origin, createdAt: createdAt}, nil + return bearerToken{peer: peer, hostname: hostname, createdAt: createdAt}, nil } type opaqueUnwrapped struct { From 3c3adf251b2341a520c1ebf0fcad0b74af529041 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 5 Jul 2024 10:23:52 -0700 Subject: [PATCH 08/31] Read hostname from tls session --- p2p/http/auth/auth_test.go | 136 ++++++++++++++++++++++++------------- p2p/http/auth/server.go | 27 ++++++-- 2 files changed, 111 insertions(+), 52 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 806dba6a36..bdc7de6a99 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -27,24 +27,17 @@ func TestMutualAuth(t *testing.T) { zeroBytes := make([]byte, 64) serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(t, err) - auth := PeerIDAuth{ - PrivKey: serverKey, - ValidHostnames: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, - } - ts := httptest.NewServer(&auth) - defer ts.Close() - - type testCase struct { + type clientTestCase struct { name string clientKeyGen func(t *testing.T) crypto.PrivKey } - testCases := []testCase{ + clientTestCases := []clientTestCase{ { name: "ED25519", clientKeyGen: func(t *testing.T) crypto.PrivKey { + t.Helper() clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader) require.NoError(t, err) return clientKey @@ -53,6 +46,7 @@ func TestMutualAuth(t *testing.T) { { name: "RSA", clientKeyGen: func(t *testing.T) crypto.PrivKey { + t.Helper() clientKey, _, err := crypto.GenerateRSAKeyPair(2048, rand.Reader) require.NoError(t, err) return clientKey @@ -60,42 +54,90 @@ func TestMutualAuth(t *testing.T) { }, } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - client := &http.Client{} - clientKey := tc.clientKeyGen(t) - clientAuth := ClientPeerIDAuth{PrivKey: clientKey} - - expectedServerID, err := peer.IDFromPrivateKey(serverKey) - require.NoError(t, err) - - ctx := context.Background() - serverID, err := clientAuth.MutualAuth(ctx, client, ts.URL, "example.com") - require.NoError(t, err) - require.Equal(t, expectedServerID, serverID) - require.NotZero(t, clientAuth.tokenMap["example.com"]) - - // Once more with the auth token - req, err := http.NewRequest("GET", ts.URL, nil) - require.NoError(t, err) - req.Host = "example.com" - serverID, err = clientAuth.AddAuthTokenToRequest(req) - require.NoError(t, err) - require.Equal(t, expectedServerID, serverID) - - // Verify that unwrapping our token gives us the client's peer ID - expectedClientPeerID, err := peer.IDFromPrivateKey(clientKey) - require.NoError(t, err) - clientPeerID, err := auth.UnwrapBearerToken(req) - require.NoError(t, err) - require.Equal(t, expectedClientPeerID, clientPeerID) - - // Verify that we can make an authenticated request - resp, err := client.Do(req) - require.NoError(t, err) - - require.Equal(t, http.StatusOK, resp.StatusCode) - }) + type serverTestCase struct { + name string + serverGen func(t *testing.T) (*httptest.Server, *PeerIDAuth) + } + + serverTestCases := []serverTestCase{ + { + name: "no TLS", + serverGen: func(t *testing.T) (*httptest.Server, *PeerIDAuth) { + t.Helper() + auth := PeerIDAuth{ + PrivKey: serverKey, + ValidHostnames: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, + InsecureNoTLS: true, + } + + ts := httptest.NewServer(&auth) + t.Cleanup(ts.Close) + return ts, &auth + }, + }, + { + name: "TLS", + serverGen: func(t *testing.T) (*httptest.Server, *PeerIDAuth) { + t.Helper() + auth := PeerIDAuth{ + PrivKey: serverKey, + ValidHostnames: map[string]struct{}{"example.com": {}}, + TokenTTL: time.Hour, + } + + ts := httptest.NewTLSServer(&auth) + t.Cleanup(ts.Close) + return ts, &auth + }, + }, + } + + for _, ctc := range clientTestCases { + for _, stc := range serverTestCases { + t.Run(ctc.name+"+"+stc.name, func(t *testing.T) { + ts, serverAuth := stc.serverGen(t) + client := ts.Client() + tlsClientConfig := client.Transport.(*http.Transport).TLSClientConfig + if tlsClientConfig != nil { + // If we're using TLS, we need to set the SNI so that the + // server can verify the request Host matches it. + tlsClientConfig.ServerName = "example.com" + } + clientKey := ctc.clientKeyGen(t) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + expectedServerID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + + ctx := context.Background() + serverID, err := clientAuth.MutualAuth(ctx, client, ts.URL, "example.com") + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tokenMap["example.com"]) + + // Once more with the auth token + req, err := http.NewRequest("GET", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, err = clientAuth.AddAuthTokenToRequest(req) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + + // Verify that unwrapping our token gives us the client's peer ID + expectedClientPeerID, err := peer.IDFromPrivateKey(clientKey) + require.NoError(t, err) + clientPeerID, err := serverAuth.UnwrapBearerToken(req, req.Host) + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + // Verify that we can make an authenticated request + resp, err := client.Do(req) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode) + }) + } } } @@ -184,6 +226,7 @@ func FuzzServeHTTP(f *testing.F) { PrivKey: serverKey, ValidHostnames: map[string]struct{}{"example.com": {}}, TokenTTL: time.Hour, + InsecureNoTLS: true, } // Just check that we don't panic' f.Fuzz(func(t *testing.T, data []byte) { @@ -212,6 +255,7 @@ func BenchmarkAuths(b *testing.B) { PrivKey: serverKey, ValidHostnames: map[string]struct{}{"example.com": {}}, TokenTTL: time.Hour, + InsecureNoTLS: true, } ts := httptest.NewServer(&auth) diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index e4540331bb..2d914ae777 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -25,6 +25,8 @@ type PeerIDAuth struct { ValidHostnames map[string]struct{} TokenTTL time.Duration Next http.Handler + // InsecureNoTLS is a flag that allows the server to accept requests without a TLS ServerName. Used only for testing. + InsecureNoTLS bool } var errMissingAuthHeader = errors.New("missing header") @@ -34,18 +36,31 @@ var errMissingAuthHeader = errors.New("missing header") // scheme. If a Next handler is set, it will be called on authenticated // requests. func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { + hostname := r.Host + if !a.InsecureNoTLS { + if r.TLS == nil { + log.Debugf("No TLS connection, and InsecureNoTLS is false") + w.WriteHeader(http.StatusBadRequest) + return + } + if hostname != r.TLS.ServerName { + log.Debugf("Unauthorized request for host %s: hostname mismatch. Expected %s", hostname, r.TLS.ServerName) + w.WriteHeader(http.StatusBadRequest) + return + } + } // Do they have a bearer token? - _, err := a.UnwrapBearerToken(r) + _, err := a.UnwrapBearerToken(r, hostname) if err != nil { // No bearer token, let's try peer ID auth - _, ok := a.ValidHostnames[r.Host] + _, ok := a.ValidHostnames[hostname] if !ok { - log.Debugf("Unauthorized request from %s: invalid hostname", r.Host) + log.Debugf("Unauthorized request from %s: invalid hostname", hostname) w.WriteHeader(http.StatusBadRequest) return } - f, err := parseAuthFields(r.Header.Get("Authorization"), r.Host, true) + f, err := parseAuthFields(r.Header.Get("Authorization"), hostname, true) if err != nil { a.serveAuthReq(w) return @@ -165,7 +180,7 @@ func (a *PeerIDAuth) authenticate(f authFields) (peer.ID, error) { return peer.IDFromPublicKey(f.pubKey) } -func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request) (peer.ID, error) { +func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname string) (peer.ID, error) { if !strings.Contains(r.Header.Get("Authorization"), BearerAuthScheme) { return "", errors.New("missing bearer auth scheme") } @@ -177,7 +192,7 @@ func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request) (peer.ID, error) { if !ok { return "", fmt.Errorf("missing bearer auth scheme") } - return a.unwrapBearerToken(r.Host, bearerScheme) + return a.unwrapBearerToken(expectedHostname, bearerScheme) } func (a *PeerIDAuth) unwrapBearerToken(expectedHostname string, s authScheme) (peer.ID, error) { From 5cb1f15c3dc3af314180bfb27b18ca8aa3674f81 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 8 Jul 2024 18:07:42 -0700 Subject: [PATCH 09/31] Add protocol ID constant --- p2p/http/auth/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index 4fc297bf72..dbf0deb6c7 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -17,6 +17,7 @@ import ( const PeerIDAuthScheme = "libp2p-PeerID" const BearerAuthScheme = "libp2p-Bearer" +const ProtocolID = "/http-peer-id-auth/1.0.0" const serverAuthPrefix = PeerIDAuthScheme + " challenge-client=" const challengeLen = 32 From 3007fd8c973f9ae46d03d887733e1ea5dbcb00ad Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 8 Jul 2024 18:07:53 -0700 Subject: [PATCH 10/31] Add marshalled zero key in test --- p2p/http/auth/auth_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index bdc7de6a99..8ebfaad4c8 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -305,6 +305,11 @@ func genClientID(t *testing.T) (peer.ID, crypto.PrivKey) { // TestWalkthroughInSpec tests the walkthrough example in libp2p/specs func TestWalkthroughInSpec(t *testing.T) { + marshalledZeroKey, err := crypto.MarshalPrivateKey(zeroKey) + require.NoError(t, err) + // To demonstrate the marshalled version of the zero key. In js-libp2p (maybe others?) it's easier to consume this form. + require.Equal(t, "0801124000000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", hex.EncodeToString(marshalledZeroKey)) + zeroBytes := make([]byte, 32) clientID, clientKey := genClientID(t) require.Equal(t, "12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq", clientID.String()) From 4d038a884e11eaf479e28d28f4e44f283772fb69 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 8 Jul 2024 18:17:45 -0700 Subject: [PATCH 11/31] Rename PeerIDAuth to ServerPeerIDAuth --- p2p/http/auth/auth_test.go | 14 +++++++------- p2p/http/auth/server.go | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 8ebfaad4c8..e439645252 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -56,15 +56,15 @@ func TestMutualAuth(t *testing.T) { type serverTestCase struct { name string - serverGen func(t *testing.T) (*httptest.Server, *PeerIDAuth) + serverGen func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) } serverTestCases := []serverTestCase{ { name: "no TLS", - serverGen: func(t *testing.T) (*httptest.Server, *PeerIDAuth) { + serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) { t.Helper() - auth := PeerIDAuth{ + auth := ServerPeerIDAuth{ PrivKey: serverKey, ValidHostnames: map[string]struct{}{"example.com": {}}, TokenTTL: time.Hour, @@ -78,9 +78,9 @@ func TestMutualAuth(t *testing.T) { }, { name: "TLS", - serverGen: func(t *testing.T) (*httptest.Server, *PeerIDAuth) { + serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) { t.Helper() - auth := PeerIDAuth{ + auth := ServerPeerIDAuth{ PrivKey: serverKey, ValidHostnames: map[string]struct{}{"example.com": {}}, TokenTTL: time.Hour, @@ -222,7 +222,7 @@ func FuzzServeHTTP(f *testing.F) { zeroBytes := make([]byte, 64) serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(f, err) - auth := PeerIDAuth{ + auth := ServerPeerIDAuth{ PrivKey: serverKey, ValidHostnames: map[string]struct{}{"example.com": {}}, TokenTTL: time.Hour, @@ -251,7 +251,7 @@ func BenchmarkAuths(b *testing.B) { zeroBytes := make([]byte, 64) serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(b, err) - auth := PeerIDAuth{ + auth := ServerPeerIDAuth{ PrivKey: serverKey, ValidHostnames: map[string]struct{}{"example.com": {}}, TokenTTL: time.Hour, diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 2d914ae777..75363b5469 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -20,7 +20,7 @@ const maxAuthHeaderSize = 8192 const challengeTTL = 5 * time.Minute -type PeerIDAuth struct { +type ServerPeerIDAuth struct { PrivKey crypto.PrivKey ValidHostnames map[string]struct{} TokenTTL time.Duration @@ -35,7 +35,7 @@ var errMissingAuthHeader = errors.New("missing header") // attempt to authenticate the request using using the libp2p peer ID auth // scheme. If a Next handler is set, it will be called on authenticated // requests. -func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { hostname := r.Host if !a.InsecureNoTLS { if r.TLS == nil { @@ -142,7 +142,7 @@ func (a *PeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.Next.ServeHTTP(w, r) } -func (a *PeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, hostname string) ([]byte, error) { +func (a *ServerPeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, hostname string) ([]byte, error) { if len(challengeServerB64) == 0 { return nil, errors.New("missing challenge") } @@ -158,7 +158,7 @@ func (a *PeerIDAuth) signChallengeServer(challengeServerB64 string, client peer. return sig, nil } -func (a *PeerIDAuth) authenticate(f authFields) (peer.ID, error) { +func (a *ServerPeerIDAuth) authenticate(f authFields) (peer.ID, error) { partsToVerify := make([]string, 0, 2) o, err := getChallengeFromOpaque(a.PrivKey, []byte(f.opaque)) if err != nil { @@ -180,7 +180,7 @@ func (a *PeerIDAuth) authenticate(f authFields) (peer.ID, error) { return peer.IDFromPublicKey(f.pubKey) } -func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname string) (peer.ID, error) { +func (a *ServerPeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname string) (peer.ID, error) { if !strings.Contains(r.Header.Get("Authorization"), BearerAuthScheme) { return "", errors.New("missing bearer auth scheme") } @@ -195,7 +195,7 @@ func (a *PeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname string) return a.unwrapBearerToken(expectedHostname, bearerScheme) } -func (a *PeerIDAuth) unwrapBearerToken(expectedHostname string, s authScheme) (peer.ID, error) { +func (a *ServerPeerIDAuth) unwrapBearerToken(expectedHostname string, s authScheme) (peer.ID, error) { buf := pool.Get(4096) defer pool.Put(buf) buf, err := b64AppendDecode(buf[:0], []byte(s.bearerToken)) @@ -404,7 +404,7 @@ func genOpaqueFromChallenge(buf []byte, now time.Time, privKey crypto.PrivKey, c return buf, nil } -func (a *PeerIDAuth) serveAuthReq(w http.ResponseWriter) { +func (a *ServerPeerIDAuth) serveAuthReq(w http.ResponseWriter) { var challenge [challengeLen]byte _, err := rand.Read(challenge[:]) if err != nil { From 3492299af1d8c8e46f89deefff135eec8fc7c05b Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 19 Jul 2024 14:13:29 -0700 Subject: [PATCH 12/31] PR comments --- p2p/http/auth/server.go | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 75363b5469..3a14cbb422 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -79,23 +79,13 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { hostname: f.hostname, createdAt: time.Now(), } - b := pool.Get(4096) - defer pool.Put(b) - b, err = genBearerTokenBlob(b[:0], a.PrivKey, tok) + b, err := genBearerAuthHeader(a.PrivKey, tok) if err != nil { log.Debugf("failed to generate bearer token: %s", err) w.WriteHeader(http.StatusInternalServerError) return } - blobB64 := pool.Get(len(BearerAuthScheme) + 1 + base64.URLEncoding.EncodedLen(len(b))) - defer pool.Put(blobB64) - - blobB64 = blobB64[:0] - blobB64 = append(blobB64, BearerAuthScheme...) - blobB64 = append(blobB64, ' ') - blobB64 = b64AppendEncode(blobB64, b) - - w.Header().Set("Authorization", string(blobB64)) + w.Header().Set("Authorization", b) if base64.URLEncoding.DecodedLen(len(f.challengeServerB64)) >= challengeLen { clientID, err := peer.IDFromPublicKey(f.pubKey) @@ -120,6 +110,11 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { authInfoVal := fmt.Sprintf("%s peer-id=%s, sig=%s", PeerIDAuthScheme, myId.String(), base64.URLEncoding.EncodeToString(buf)) w.Header().Set("Authentication-Info", authInfoVal) + } else { + // Only supporting mutual auth for now. Fail because the client didn't want to authenticate us. + log.Debugf("Client did not provide challenge-server") + w.WriteHeader(http.StatusBadRequest) + return } if a.Next != nil { @@ -221,6 +216,23 @@ type bearerToken struct { createdAt time.Time } +func genBearerAuthHeader(privKey crypto.PrivKey, t bearerToken) (string, error) { + b := pool.Get(4096) + defer pool.Put(b) + b, err := genBearerTokenBlob(b[:0], privKey, t) + if err != nil { + return "", err + } + blobB64 := pool.Get(len(BearerAuthScheme) + 1 + base64.URLEncoding.EncodedLen(len(b))) + defer pool.Put(blobB64) + + blobB64 = blobB64[:0] + blobB64 = append(blobB64, BearerAuthScheme...) + blobB64 = append(blobB64, ' ') + blobB64 = b64AppendEncode(blobB64, b) + return string(blobB64), nil +} + func genBearerTokenBlob(buf []byte, privKey crypto.PrivKey, t bearerToken) ([]byte, error) { peerBytes, err := t.peer.MarshalBinary() if err != nil { From e19d8b0f816b11439842cb167d3a2c523845837f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 23 Aug 2024 15:28:02 -0700 Subject: [PATCH 13/31] WIP --- p2p/http/auth/alloc_test.go | 21 ++++++++ p2p/http/auth/auth.go | 99 ++++++++++++++++++++++++++++++++++--- p2p/http/auth/auth_test.go | 29 +++++++++++ p2p/http/auth/client.go | 23 ++++----- p2p/http/auth/server.go | 58 +++++++++++++++------- 5 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 p2p/http/auth/alloc_test.go diff --git a/p2p/http/auth/alloc_test.go b/p2p/http/auth/alloc_test.go new file mode 100644 index 0000000000..a84b693fbe --- /dev/null +++ b/p2p/http/auth/alloc_test.go @@ -0,0 +1,21 @@ +//go:build nocover + +package httppeeridauth + +import "testing" + +func TestParsePeerIDAuthSchemeParamsNoAllocNoCover(t *testing.T) { + str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) + paramMap := make(map[string][]byte, 5) + + allocs := testing.AllocsPerRun(1000, func() { + paramMap, err := parsePeerIDAuthSchemeParams(str, paramMap) + if err != nil { + t.Fatal(err) + } + clear(paramMap) + }) + if allocs > 0 { + t.Fatalf("alloc test failed expected 0 received %0.2f", allocs) + } +} diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index dbf0deb6c7..3ae183b347 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -1,12 +1,15 @@ package httppeeridauth import ( + "bufio" + "bytes" "encoding/base64" "encoding/binary" "errors" "fmt" "regexp" "slices" + "sort" "strings" logging "github.com/ipfs/go-log/v2" @@ -16,13 +19,98 @@ import ( ) const PeerIDAuthScheme = "libp2p-PeerID" -const BearerAuthScheme = "libp2p-Bearer" + +var PeerIDAuthSchemeBytes = []byte(PeerIDAuthScheme) + +const bearerTokenPrefix = "bearer=" const ProtocolID = "/http-peer-id-auth/1.0.0" const serverAuthPrefix = PeerIDAuthScheme + " challenge-client=" const challengeLen = 32 var log = logging.Logger("httppeeridauth") +func init() { + sort.Strings(internedParamKeys) +} + +var internedParamKeys []string = []string{ + "bearer", + "challenge-client", + "challenge-server", + "opaque", + "peer-id", + "public-key", + "sig", +} + +const maxHeaderValSize = 2048 + +var errTooBig = errors.New("header value too big") +var errInvalid = errors.New("invalid header value") + +// parsePeerIDAuthSchemeParams parses the parameters of the PeerID auth scheme +// from the header string. zero alloc if the params map is big enough. +func parsePeerIDAuthSchemeParams(headerVal []byte, params map[string][]byte) (map[string][]byte, error) { + if len(headerVal) > maxHeaderValSize { + return nil, errTooBig + } + startIdx := bytes.Index(headerVal, []byte(PeerIDAuthScheme)) + if startIdx == -1 { + return params, nil + } + + headerVal = headerVal[startIdx+len(PeerIDAuthScheme):] + advance, token, err := splitAuthHeaderParams(headerVal, true) + for ; err == nil; advance, token, err = splitAuthHeaderParams(headerVal, true) { + headerVal = headerVal[advance:] + bs := token + splitAt := bytes.Index(bs, []byte("=")) + if splitAt == -1 { + return nil, errInvalid + } + kB := bs[:splitAt] + v := bs[splitAt+1:] + i, ok := sort.Find(len(internedParamKeys), func(i int) int { + return bytes.Compare(kB, []byte(internedParamKeys[i])) + }) + var k string + if ok { + k = internedParamKeys[i] + } else { + // Not an interned key? + k = string(kB) + } + + params[k] = v + } + return params, nil +} + +func splitAuthHeaderParams(data []byte, atEOF bool) (advance int, token []byte, err error) { + if len(data) == 0 && atEOF { + return 0, nil, bufio.ErrFinalToken + } + + start := 0 + for start < len(data) && (data[start] == ' ' || data[start] == ',') { + start++ + } + if start == len(data) { + return len(data), nil, nil + } + end := start + 1 + for end < len(data) && data[end] != ' ' && data[end] != ',' { + end++ + } + token = data[start:end] + if !bytes.ContainsAny(token, "=") { + // This isn't a param. It's likely the next scheme. We're done + return len(data), nil, bufio.ErrFinalToken + } + + return end, token, nil +} + type authScheme struct { scheme string params map[string]string @@ -35,7 +123,7 @@ const maxParams = 10 var paramRegexStr = `([\w-]+)=([\w\d-_=.]+|"[^"]+")` var paramRegex = regexp.MustCompile(paramRegexStr) -var authHeaderRegex = regexp.MustCompile(fmt.Sprintf(`(%s\s+[^,\s]+)|(%s+\s+(:?(:?%s)(:?\s*,\s*)?)*)`, BearerAuthScheme, PeerIDAuthScheme, paramRegexStr)) +var authHeaderRegex = regexp.MustCompile(fmt.Sprintf(`(%s+\s+(:?(:?%s)(:?\s*,\s*)?)*)`, PeerIDAuthScheme, paramRegexStr)) func parseAuthHeader(headerVal string) (map[string]authScheme, error) { if len(headerVal) > maxAuthHeaderSize { @@ -59,17 +147,12 @@ func parseAuthHeader(headerVal string) (map[string]authScheme, error) { } scheme := authScheme{scheme: s[:schemeEndIdx]} switch scheme.scheme { - case BearerAuthScheme, PeerIDAuthScheme: + case PeerIDAuthScheme: default: // Ignore unknown schemes continue } params := s[schemeEndIdx+1:] - if scheme.scheme == BearerAuthScheme { - scheme.bearerToken = params - out = append(out, scheme) - continue - } scheme.params = make(map[string]string, 10) params = strings.TrimSpace(params) for _, kv := range paramRegex.FindAllStringSubmatch(params, maxParams) { diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index e439645252..bb7932fc6b 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -347,3 +347,32 @@ func TestWalkthroughInSpec(t *testing.T) { require.NoError(t, err) require.Equal(t, "btLFqW200aDTQqpkKetJJje7V-iDknXygFqPsfiegNsboXeYDiQ6Rqcpezz1wfr8j9h83QkN9z78cAWzKzV_AQ==", base64.URLEncoding.EncodeToString(sig)) } + +func TestParsePeerIDAuthSchemeParams(t *testing.T) { + str := `libp2p-PeerID peer-id="", sig="", public-key="", bearer=""` + paramMap := make(map[string][]byte, 5) + expectedParamMap := map[string][]byte{ + "peer-id": []byte(`""`), + "sig": []byte(`""`), + "public-key": []byte(`""`), + "bearer": []byte(`""`), + } + paramMap, err := parsePeerIDAuthSchemeParams([]byte(str), paramMap) + require.NoError(t, err) + require.Equal(t, expectedParamMap, paramMap) + +} + +func BenchmarkParsePeerIDAuthSchemeParams(b *testing.B) { + str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) + paramMap := make(map[string][]byte, 5) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + paramMap, err := parsePeerIDAuthSchemeParams(str, paramMap) + if err != nil { + b.Fatal(err) + } + clear(paramMap) + } +} diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 19cbce0ac1..4551b7ab28 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -39,7 +39,7 @@ func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, er return "", ErrNoAuthToken } - req.Header.Set("Authorization", BearerAuthScheme+" "+t.token) + // req.Header.Set("Authorization", BearerAuthScheme+" "+t.token) return t.peerID, nil } @@ -64,7 +64,7 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, return "", fmt.Errorf("failed to authenticate self to server: %w", err) } - authServerReq, err := http.NewRequestWithContext(ctx, "GET", authEndpoint, nil) + authServerReq, err := http.NewRequestWithContext(ctx, "POST", authEndpoint, nil) authServerReq.Host = hostname if err != nil { return "", fmt.Errorf("failed to create request to authenticate server: %w", err) @@ -92,14 +92,15 @@ func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, return "", fmt.Errorf("failed to parse auth header: %w", err) } - if bearer, ok := respAuthSchemes[BearerAuthScheme]; ok { - a.tokenMapMu.Lock() - if a.tokenMap == nil { - a.tokenMap = make(map[string]tokenInfo) - } - a.tokenMap[hostname] = tokenInfo{token: bearer.bearerToken, peerID: serverID} - a.tokenMapMu.Unlock() - } + _ = respAuthSchemes + // if bearer, ok := respAuthSchemes[BearerAuthScheme]; ok { + // a.tokenMapMu.Lock() + // if a.tokenMap == nil { + // a.tokenMap = make(map[string]tokenInfo) + // } + // a.tokenMap[hostname] = tokenInfo{token: bearer.bearerToken, peerID: serverID} + // a.tokenMapMu.Unlock() + // } return serverID, nil } @@ -114,7 +115,7 @@ func (a *ClientPeerIDAuth) sign(challengeClientB64 string, hostname string) ([]b // authSelfToServer performs the initial authentication request to the server. It authenticates the client to the server. // Returns the Authorization value with libp2p-PeerID scheme to use for subsequent requests. func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Client, myPeerID peer.ID, challengeServer []byte, authEndpoint string, hostname string) (string, error) { - r, err := http.NewRequestWithContext(ctx, "GET", authEndpoint, nil) + r, err := http.NewRequestWithContext(ctx, "POST", authEndpoint, nil) r.Host = hostname if err != nil { return "", fmt.Errorf("failed to create request: %w", err) diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 3a14cbb422..3fdf358bb6 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -79,13 +79,12 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { hostname: f.hostname, createdAt: time.Now(), } - b, err := genBearerAuthHeader(a.PrivKey, tok) + bearerToken, err := genBearerAuthHeader(a.PrivKey, tok) if err != nil { log.Debugf("failed to generate bearer token: %s", err) w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("Authorization", b) if base64.URLEncoding.DecodedLen(len(f.challengeServerB64)) >= challengeLen { clientID, err := peer.IDFromPublicKey(f.pubKey) @@ -108,7 +107,7 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - authInfoVal := fmt.Sprintf("%s peer-id=%s, sig=%s", PeerIDAuthScheme, myId.String(), base64.URLEncoding.EncodeToString(buf)) + authInfoVal := fmt.Sprintf("%s peer-id=%s, sig=%s %s", PeerIDAuthScheme, myId.String(), base64.URLEncoding.EncodeToString(buf), bearerToken) w.Header().Set("Authentication-Info", authInfoVal) } else { // Only supporting mutual auth for now. Fail because the client didn't want to authenticate us. @@ -120,12 +119,12 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { if a.Next != nil { // Set the token on the request so the next handler can read it authHeader := r.Header.Get("Authorization") - if authHeader != "" { - authHeader = string(blobB64) + ", " + authHeader + if len(authHeader) == 0 { + authHeader = PeerIDAuthScheme + " " + bearerToken } else { - authHeader = string(blobB64) + authHeader = authHeader + ", " + bearerToken } - r.Header.Set("Authorization", string(blobB64)+", "+authHeader) + r.Header.Set("Authorization", authHeader) } } @@ -137,6 +136,30 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { a.Next.ServeHTTP(w, r) } +type peerIDAuthServerState int + +const ( + peerIDAuthServerStateChallengeClient peerIDAuthServerState = iota + peerIDAuthServerStateVerify + peerIDAuthServerStateDone +) + +type peerIDAuthHandshakeServer struct { + serverPrivKey crypto.PrivKey + // HMACKey is used to authenticate the opaque blobs and token + hmacKey []byte + + state peerIDAuthServerState + + peerID []byte // the string representation of the peer ID (as bytes) + publicKey []byte + + challengeClient []byte + challengeServer []byte + + bearerToken []byte +} + func (a *ServerPeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, hostname string) ([]byte, error) { if len(challengeServerB64) == 0 { return nil, errors.New("missing challenge") @@ -176,14 +199,14 @@ func (a *ServerPeerIDAuth) authenticate(f authFields) (peer.ID, error) { } func (a *ServerPeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname string) (peer.ID, error) { - if !strings.Contains(r.Header.Get("Authorization"), BearerAuthScheme) { + if !strings.Contains(r.Header.Get("Authorization"), PeerIDAuthScheme) { return "", errors.New("missing bearer auth scheme") } schemes, err := parseAuthHeader(r.Header.Get("Authorization")) if err != nil { return "", fmt.Errorf("failed to parse auth header: %w", err) } - bearerScheme, ok := schemes[BearerAuthScheme] + bearerScheme, ok := schemes[PeerIDAuthScheme] if !ok { return "", fmt.Errorf("missing bearer auth scheme") } @@ -193,7 +216,7 @@ func (a *ServerPeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname s func (a *ServerPeerIDAuth) unwrapBearerToken(expectedHostname string, s authScheme) (peer.ID, error) { buf := pool.Get(4096) defer pool.Put(buf) - buf, err := b64AppendDecode(buf[:0], []byte(s.bearerToken)) + buf, err := b64AppendDecode(buf[:0], []byte(s.params["bearer"])) if err != nil { return "", fmt.Errorf("failed to decode bearer token: %w", err) } @@ -223,13 +246,14 @@ func genBearerAuthHeader(privKey crypto.PrivKey, t bearerToken) (string, error) if err != nil { return "", err } - blobB64 := pool.Get(len(BearerAuthScheme) + 1 + base64.URLEncoding.EncodedLen(len(b))) + blobB64 := pool.Get(len(bearerTokenPrefix) + base64.URLEncoding.EncodedLen(len(b))) defer pool.Put(blobB64) blobB64 = blobB64[:0] - blobB64 = append(blobB64, BearerAuthScheme...) - blobB64 = append(blobB64, ' ') + blobB64 = append(blobB64, []byte(bearerTokenPrefix)...) + blobB64 = append(blobB64, byte('"')) blobB64 = b64AppendEncode(blobB64, b) + blobB64 = append(blobB64, byte('"')) return string(blobB64), nil } @@ -244,7 +268,7 @@ func genBearerTokenBlob(buf []byte, privKey crypto.PrivKey, t bearerToken) ([]by } // Auth scheme prefix - buf = append(buf, BearerAuthScheme...) + buf = append(buf, []byte(PeerIDAuthScheme+" bearer")...) buf = append(buf, ' ') // Space between auth scheme and the token // Peer ID @@ -272,14 +296,14 @@ func parseBearerTokenBlob(privKey crypto.PrivKey, blob []byte) (bearerToken, err originalSlice := blob // Auth scheme prefix +1 for space - if len(BearerAuthScheme)+1 > len(blob) { + if len(PeerIDAuthScheme+" bearer")+1 > len(blob) { return bearerToken{}, fmt.Errorf("bearer token too short") } - hasPrefix := bytes.Equal([]byte(BearerAuthScheme), blob[:len(BearerAuthScheme)]) + hasPrefix := bytes.Equal([]byte(PeerIDAuthScheme+" bearer"), blob[:len(PeerIDAuthScheme+" bearer")]) if !hasPrefix { return bearerToken{}, fmt.Errorf("missing bearer token prefix") } - blob = blob[len(BearerAuthScheme):] + blob = blob[len(PeerIDAuthScheme+" bearer"):] if blob[0] != ' ' { return bearerToken{}, fmt.Errorf("missing space after auth scheme") } From a68e63dfad401b704e1678ba768470f7c3065691 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 16:17:15 -0700 Subject: [PATCH 14/31] Refactor handshake --- p2p/http/auth/alloc_test.go | 5 +- p2p/http/auth/auth.go | 148 +++--- p2p/http/auth/auth_test.go | 276 ++---------- p2p/http/auth/client.go | 137 +----- p2p/http/auth/internal/handshake/client.go | 151 +++++++ p2p/http/auth/internal/handshake/handshake.go | 198 ++++++++ .../auth/internal/handshake/handshake_test.go | 307 +++++++++++++ p2p/http/auth/internal/handshake/server.go | 293 ++++++++++++ p2p/http/auth/server.go | 426 ------------------ 9 files changed, 1072 insertions(+), 869 deletions(-) create mode 100644 p2p/http/auth/internal/handshake/client.go create mode 100644 p2p/http/auth/internal/handshake/handshake.go create mode 100644 p2p/http/auth/internal/handshake/handshake_test.go create mode 100644 p2p/http/auth/internal/handshake/server.go diff --git a/p2p/http/auth/alloc_test.go b/p2p/http/auth/alloc_test.go index a84b693fbe..e7083c92cf 100644 --- a/p2p/http/auth/alloc_test.go +++ b/p2p/http/auth/alloc_test.go @@ -6,14 +6,13 @@ import "testing" func TestParsePeerIDAuthSchemeParamsNoAllocNoCover(t *testing.T) { str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) - paramMap := make(map[string][]byte, 5) allocs := testing.AllocsPerRun(1000, func() { - paramMap, err := parsePeerIDAuthSchemeParams(str, paramMap) + p := params{} + err := p.parsePeerIDAuthSchemeParams(str) if err != nil { t.Fatal(err) } - clear(paramMap) }) if allocs > 0 { t.Fatalf("alloc test failed expected 0 received %0.2f", allocs) diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index 3ae183b347..9a47be4408 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -4,12 +4,10 @@ import ( "bufio" "bytes" "encoding/base64" - "encoding/binary" "errors" "fmt" "regexp" "slices" - "sort" "strings" logging "github.com/ipfs/go-log/v2" @@ -29,34 +27,30 @@ const challengeLen = 32 var log = logging.Logger("httppeeridauth") -func init() { - sort.Strings(internedParamKeys) -} - -var internedParamKeys []string = []string{ - "bearer", - "challenge-client", - "challenge-server", - "opaque", - "peer-id", - "public-key", - "sig", -} - const maxHeaderValSize = 2048 var errTooBig = errors.New("header value too big") var errInvalid = errors.New("invalid header value") +// params represent params passed in via headers. All []byte fields to avoid allocations. +type params struct { + bearerTokenB64 []byte + challengeClient []byte + challengeServer []byte + opaqueB64 []byte + publicKeyB64 []byte + sigB64 []byte +} + // parsePeerIDAuthSchemeParams parses the parameters of the PeerID auth scheme -// from the header string. zero alloc if the params map is big enough. -func parsePeerIDAuthSchemeParams(headerVal []byte, params map[string][]byte) (map[string][]byte, error) { +// from the header string. zero alloc. +func (p *params) parsePeerIDAuthSchemeParams(headerVal []byte) error { if len(headerVal) > maxHeaderValSize { - return nil, errTooBig + return errTooBig } startIdx := bytes.Index(headerVal, []byte(PeerIDAuthScheme)) if startIdx == -1 { - return params, nil + return nil } headerVal = headerVal[startIdx+len(PeerIDAuthScheme):] @@ -66,24 +60,75 @@ func parsePeerIDAuthSchemeParams(headerVal []byte, params map[string][]byte) (ma bs := token splitAt := bytes.Index(bs, []byte("=")) if splitAt == -1 { - return nil, errInvalid + return errInvalid } kB := bs[:splitAt] v := bs[splitAt+1:] - i, ok := sort.Find(len(internedParamKeys), func(i int) int { - return bytes.Compare(kB, []byte(internedParamKeys[i])) - }) - var k string - if ok { - k = internedParamKeys[i] - } else { - // Not an interned key? - k = string(kB) + if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' { + return errInvalid + } + v = v[1 : len(v)-1] // drop quotes + switch string(kB) { + case "bearer": + p.bearerTokenB64 = v + case "challenge-client": + p.challengeClient = v + case "challenge-server": + p.challengeServer = v + case "opaque": + p.opaqueB64 = v + case "public-key": + p.publicKeyB64 = v + case "sig": + p.sigB64 = v } + } + return nil +} + +type headerBuilder struct { + b strings.Builder + pastFirstField bool +} + +func (h *headerBuilder) clear() { + h.b.Reset() + h.pastFirstField = false +} + +func (h *headerBuilder) writeScheme(scheme string) { + h.b.WriteString(scheme) + h.b.WriteByte(' ') +} + +func (h *headerBuilder) maybeAddComma() { + if !h.pastFirstField { + h.pastFirstField = true + return + } + h.b.WriteString(", ") +} - params[k] = v +// writeParam writes a key value pair to the header. It first b64 encodes the value. +// It uses buf as a scratch space. +func (h *headerBuilder) writeParamB64(buf []byte, key string, val []byte) { + if buf == nil { + buf = make([]byte, base64.URLEncoding.EncodedLen(len(val))) } - return params, nil + encodedVal := base64.URLEncoding.AppendEncode(buf[:0], val) + h.writeParam(key, encodedVal) +} + +// writeParam writes a key value pair to the header. It writes the val as-is. +func (h *headerBuilder) writeParam(key string, val []byte) { + h.maybeAddComma() + + h.b.Grow(len(key) + len(`="`) + len(val) + 1) + // Not doing fmt.Fprintf here to avoid one allocation + h.b.WriteString(key) + h.b.WriteString(`="`) + h.b.Write(val) + h.b.WriteByte('"') } func splitAuthHeaderParams(data []byte, atEOF bool) (advance int, token []byte, err error) { @@ -174,45 +219,6 @@ func parseAuthHeader(headerVal string) (map[string]authScheme, error) { return outMap, nil } -func verifySig(publicKey crypto.PubKey, prefix string, signedParts []string, sig []byte) error { - b := pool.Get(4096) - defer pool.Put(b) - buf, err := genDataToSign(b[:0], prefix, signedParts) - if err != nil { - return fmt.Errorf("failed to generate signed data: %w", err) - } - ok, err := publicKey.Verify(buf, sig) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("signature verification failed") - } - - return nil -} - -func sign(privKey crypto.PrivKey, prefix string, partsToSign []string) ([]byte, error) { - b := pool.Get(4096) - defer pool.Put(b) - buf, err := genDataToSign(b[:0], prefix, partsToSign) - if err != nil { - return nil, fmt.Errorf("failed to generate data to sign: %w", err) - } - return privKey.Sign(buf) -} - -func genDataToSign(buf []byte, prefix string, parts []string) ([]byte, error) { - // Sort the parts in alphabetical order - slices.Sort(parts) - buf = append(buf, []byte(prefix)...) - for _, p := range parts { - buf = binary.AppendUvarint(buf, uint64(len(p))) - buf = append(buf, p...) - } - return buf, nil -} - type authFields struct { hostname string pubKey crypto.PubKey diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index bb7932fc6b..b3b6a71aae 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -2,15 +2,9 @@ package httppeeridauth import ( "bytes" - "context" "crypto/rand" - "encoding/base64" "encoding/hex" - "fmt" - "net/http" "net/http/httptest" - "net/url" - "strings" "testing" "time" @@ -96,128 +90,51 @@ func TestMutualAuth(t *testing.T) { for _, ctc := range clientTestCases { for _, stc := range serverTestCases { t.Run(ctc.name+"+"+stc.name, func(t *testing.T) { - ts, serverAuth := stc.serverGen(t) - client := ts.Client() - tlsClientConfig := client.Transport.(*http.Transport).TLSClientConfig - if tlsClientConfig != nil { - // If we're using TLS, we need to set the SNI so that the - // server can verify the request Host matches it. - tlsClientConfig.ServerName = "example.com" - } - clientKey := ctc.clientKeyGen(t) - clientAuth := ClientPeerIDAuth{PrivKey: clientKey} - - expectedServerID, err := peer.IDFromPrivateKey(serverKey) - require.NoError(t, err) - - ctx := context.Background() - serverID, err := clientAuth.MutualAuth(ctx, client, ts.URL, "example.com") - require.NoError(t, err) - require.Equal(t, expectedServerID, serverID) - require.NotZero(t, clientAuth.tokenMap["example.com"]) - - // Once more with the auth token - req, err := http.NewRequest("GET", ts.URL, nil) - require.NoError(t, err) - req.Host = "example.com" - serverID, err = clientAuth.AddAuthTokenToRequest(req) - require.NoError(t, err) - require.Equal(t, expectedServerID, serverID) - - // Verify that unwrapping our token gives us the client's peer ID - expectedClientPeerID, err := peer.IDFromPrivateKey(clientKey) - require.NoError(t, err) - clientPeerID, err := serverAuth.UnwrapBearerToken(req, req.Host) - require.NoError(t, err) - require.Equal(t, expectedClientPeerID, clientPeerID) - - // Verify that we can make an authenticated request - resp, err := client.Do(req) - require.NoError(t, err) - - require.Equal(t, http.StatusOK, resp.StatusCode) + // ts, serverAuth := stc.serverGen(t) + // client := ts.Client() + // tlsClientConfig := client.Transport.(*http.Transport).TLSClientConfig + // if tlsClientConfig != nil { + // // If we're using TLS, we need to set the SNI so that the + // // server can verify the request Host matches it. + // tlsClientConfig.ServerName = "example.com" + // } + // clientKey := ctc.clientKeyGen(t) + // clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + // expectedServerID, err := peer.IDFromPrivateKey(serverKey) + // require.NoError(t, err) + + // ctx := context.Background() + // serverID, err := clientAuth.MutualAuth(ctx, client, ts.URL, "example.com") + // require.NoError(t, err) + // require.Equal(t, expectedServerID, serverID) + // require.NotZero(t, clientAuth.tokenMap["example.com"]) + + // // Once more with the auth token + // req, err := http.NewRequest("GET", ts.URL, nil) + // require.NoError(t, err) + // req.Host = "example.com" + // serverID, err = clientAuth.AddAuthTokenToRequest(req) + // require.NoError(t, err) + // require.Equal(t, expectedServerID, serverID) + + // // Verify that unwrapping our token gives us the client's peer ID + // expectedClientPeerID, err := peer.IDFromPrivateKey(clientKey) + // require.NoError(t, err) + // clientPeerID, err := serverAuth.UnwrapBearerToken(req, req.Host) + // require.NoError(t, err) + // require.Equal(t, expectedClientPeerID, clientPeerID) + + // // Verify that we can make an authenticated request + // resp, err := client.Do(req) + // require.NoError(t, err) + + // require.Equal(t, http.StatusOK, resp.StatusCode) }) } } } -func TestParseAuthHeader(t *testing.T) { - testCases := []struct { - name string - header string - expected map[string]authScheme - err error - }{ - { - name: "empty header", - header: "", - expected: nil, - err: nil, - }, - { - name: "header too long", - header: strings.Repeat("a", maxAuthHeaderSize+1), - expected: nil, - err: fmt.Errorf("header too long"), - }, - { - name: "too many schemes", - header: strings.Repeat("libp2p-Bearer token1, ", maxSchemes+1), - expected: nil, - err: fmt.Errorf("too many schemes"), - }, - { - name: "Valid Bearer scheme", - header: "libp2p-Bearer token123", - expected: map[string]authScheme{"libp2p-Bearer": {bearerToken: "token123", scheme: "libp2p-Bearer"}}, - err: nil, - }, - { - name: "Valid PeerID scheme", - header: "libp2p-PeerID param1=val1, param2=val2", - expected: map[string]authScheme{"libp2p-PeerID": {scheme: "libp2p-PeerID", params: map[string]string{"param1": "val1", "param2": "val2"}}}, - err: nil, - }, - { - name: "Ignore unknown scheme", - header: "Unknown scheme1, libp2p-Bearer token456, libp2p-PeerID param=value", - expected: map[string]authScheme{ - "libp2p-Bearer": { - scheme: "libp2p-Bearer", - bearerToken: "token456"}, - "libp2p-PeerID": {scheme: "libp2p-PeerID", params: map[string]string{"param": "value"}}}, - err: nil, - }, - { - name: "Parse quoted params", - header: `libp2p-PeerID param="value"`, - expected: map[string]authScheme{ - "libp2p-PeerID": {scheme: "libp2p-PeerID", params: map[string]string{"param": "value"}}}, - err: nil, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual, err := parseAuthHeader(tc.header) - if tc.err != nil { - require.Error(t, err, tc.err) - require.Equal(t, tc.err.Error(), err.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tc.expected, actual) - } - }) - } -} - -func FuzzParseAuthHeader(f *testing.F) { - // Just check that we don't panic' - f.Fuzz(func(t *testing.T, data []byte) { - parseAuthHeader(string(data)) - }) -} - func FuzzServeHTTP(f *testing.F) { zeroBytes := make([]byte, 64) serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) @@ -247,45 +164,6 @@ func FuzzServeHTTP(f *testing.F) { }) } -func BenchmarkAuths(b *testing.B) { - zeroBytes := make([]byte, 64) - serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) - require.NoError(b, err) - auth := ServerPeerIDAuth{ - PrivKey: serverKey, - ValidHostnames: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, - InsecureNoTLS: true, - } - - ts := httptest.NewServer(&auth) - defer ts.Close() - - ctx := context.Background() - client := &http.Client{} - clientKey, _, err := crypto.GenerateEd25519Key(rand.Reader) - require.NoError(b, err) - clientAuth := ClientPeerIDAuth{PrivKey: clientKey} - clientID, err := peer.IDFromPrivateKey(clientKey) - require.NoError(b, err) - challengeServer := make([]byte, challengeLen) - clientAuthValue, err := clientAuth.authSelfToServer(ctx, client, clientID, challengeServer, ts.URL, "example.com") - require.NoError(b, err) - - b.ResetTimer() - req, err := http.NewRequest("GET", ts.URL, nil) - require.NoError(b, err) - req.Host = "example.com" - req.Header.Set("Authorization", clientAuthValue) - - for i := 0; i < b.N; i++ { - resp, err := client.Do(req) - if err != nil || resp.StatusCode != http.StatusOK { - b.Fatal(err, resp.StatusCode) - } - } -} - // Test Vectors var zeroBytes = make([]byte, 64) var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) @@ -302,77 +180,3 @@ func genClientID(t *testing.T) (peer.ID, crypto.PrivKey) { require.NoError(t, err) return clientID, clientKey } - -// TestWalkthroughInSpec tests the walkthrough example in libp2p/specs -func TestWalkthroughInSpec(t *testing.T) { - marshalledZeroKey, err := crypto.MarshalPrivateKey(zeroKey) - require.NoError(t, err) - // To demonstrate the marshalled version of the zero key. In js-libp2p (maybe others?) it's easier to consume this form. - require.Equal(t, "0801124000000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", hex.EncodeToString(marshalledZeroKey)) - - zeroBytes := make([]byte, 32) - clientID, clientKey := genClientID(t) - require.Equal(t, "12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq", clientID.String()) - - challengeClientb64 := base64.URLEncoding.EncodeToString(zeroBytes) - require.Equal(t, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", challengeClientb64) - challengeServer64 := "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" - - hostname := "example.com" - - clientParts := []string{ - "challenge-client=" + challengeClientb64, - fmt.Sprintf(`hostname="%s"`, hostname), - } - toSign, err := genDataToSign(nil, PeerIDAuthScheme, clientParts) - require.NoError(t, err) - require.Equal(t, "libp2p-PeerID=challenge-client=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=%16hostname=%22example.com%22", url.PathEscape(string(toSign))) - sig, err := sign(clientKey, PeerIDAuthScheme, clientParts) - require.NoError(t, err) - require.Equal(t, "F5OBYbbMXoIVJNWrW0UANi7rrbj4GCB6kcEceQjajLTMvC-_jpBF9MFlxiaNYXOEiPQqeo_S56YUSNinwl0ZCQ==", base64.URLEncoding.EncodeToString(sig)) - - serverID := zeroID - require.Equal(t, "12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN", serverID.String()) - - serverParts := []string{ - "challenge-server=" + challengeServer64, - "client=" + clientID.String(), - fmt.Sprintf(`hostname="%s"`, hostname), - } - toSign, err = genDataToSign(nil, PeerIDAuthScheme, serverParts) - require.NoError(t, err) - require.Equal(t, "libp2p-PeerID=challenge-server=BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=%3Bclient=12D3KooWBtg3aaRMjxwedh83aGiUkwSxDwUZkzuJcfaqUmo7R3pq%16hostname=%22example.com%22", url.PathEscape(string(toSign))) - - sig, err = sign(zeroKey, PeerIDAuthScheme, serverParts) - require.NoError(t, err) - require.Equal(t, "btLFqW200aDTQqpkKetJJje7V-iDknXygFqPsfiegNsboXeYDiQ6Rqcpezz1wfr8j9h83QkN9z78cAWzKzV_AQ==", base64.URLEncoding.EncodeToString(sig)) -} - -func TestParsePeerIDAuthSchemeParams(t *testing.T) { - str := `libp2p-PeerID peer-id="", sig="", public-key="", bearer=""` - paramMap := make(map[string][]byte, 5) - expectedParamMap := map[string][]byte{ - "peer-id": []byte(`""`), - "sig": []byte(`""`), - "public-key": []byte(`""`), - "bearer": []byte(`""`), - } - paramMap, err := parsePeerIDAuthSchemeParams([]byte(str), paramMap) - require.NoError(t, err) - require.Equal(t, expectedParamMap, paramMap) - -} - -func BenchmarkParsePeerIDAuthSchemeParams(b *testing.B) { - str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) - paramMap := make(map[string][]byte, 5) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - paramMap, err := parsePeerIDAuthSchemeParams(str, paramMap) - if err != nil { - b.Fatal(err) - } - clear(paramMap) - } -} diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 4551b7ab28..18ec535469 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -2,114 +2,26 @@ package httppeeridauth import ( "context" - "crypto/rand" - "encoding/base64" "errors" "fmt" "net/http" - "sync" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" ) type ClientPeerIDAuth struct { - PrivKey crypto.PrivKey - tokenMapMu sync.Mutex - tokenMap map[string]tokenInfo + PrivKey crypto.PrivKey } -type tokenInfo struct { - peerID peer.ID - token string -} - -var ErrNoAuthToken = errors.New("no auth token found") - // AddAuthTokenToRequest adds the libp2p-Bearer token to the request. Returns the peer ID of the server. func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, error) { - a.tokenMapMu.Lock() - defer a.tokenMapMu.Unlock() - if a.tokenMap == nil { - a.tokenMap = make(map[string]tokenInfo) - } - - t, ok := a.tokenMap[req.Host] - if !ok { - return "", ErrNoAuthToken - } - - // req.Header.Set("Authorization", BearerAuthScheme+" "+t.token) - return t.peerID, nil + panic("todo") } // MutualAuth performs mutual authentication with the server at the given endpoint. Returns the server's peer id. func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, authEndpoint string, hostname string) (peer.ID, error) { - if a.PrivKey == nil { - return "", errors.New("no private key set") - } - - myPeerID, err := peer.IDFromPrivateKey(a.PrivKey) - if err != nil { - return "", fmt.Errorf("failed to get peer ID: %w", err) - } - - var challengeServer [challengeLen]byte - _, err = rand.Read(challengeServer[:]) - if err != nil { - return "", fmt.Errorf("failed to generate challenge-server: %w", err) - } - authValue, err := a.authSelfToServer(ctx, client, myPeerID, challengeServer[:], authEndpoint, hostname) - if err != nil { - return "", fmt.Errorf("failed to authenticate self to server: %w", err) - } - - authServerReq, err := http.NewRequestWithContext(ctx, "POST", authEndpoint, nil) - authServerReq.Host = hostname - if err != nil { - return "", fmt.Errorf("failed to create request to authenticate server: %w", err) - } - authServerReq.Header.Set("Authorization", authValue) - resp, err := client.Do(authServerReq) - if err != nil { - return "", fmt.Errorf("failed to do authenticate server request: %w", err) - } - resp.Body.Close() - - // Verify the server's signature - respAuth, err := parseAuthFields(resp.Header.Get("Authentication-Info"), hostname, false) - if err != nil { - return "", fmt.Errorf("failed to parse Authentication-Info header: %w", err) - } - serverID, err := a.verifySigFromServer(respAuth, myPeerID, challengeServer[:]) - if err != nil { - return "", fmt.Errorf("failed to authenticate server: %w", err) - } - - // Auth succeeded, store the token - respAuthSchemes, err := parseAuthHeader(resp.Header.Get("Authorization")) - if err != nil { - return "", fmt.Errorf("failed to parse auth header: %w", err) - } - - _ = respAuthSchemes - // if bearer, ok := respAuthSchemes[BearerAuthScheme]; ok { - // a.tokenMapMu.Lock() - // if a.tokenMap == nil { - // a.tokenMap = make(map[string]tokenInfo) - // } - // a.tokenMap[hostname] = tokenInfo{token: bearer.bearerToken, peerID: serverID} - // a.tokenMapMu.Unlock() - // } - - return serverID, nil -} - -func (a *ClientPeerIDAuth) sign(challengeClientB64 string, hostname string) ([]byte, error) { - return sign(a.PrivKey, PeerIDAuthScheme, []string{ - "challenge-client=" + challengeClientB64, - fmt.Sprintf(`hostname="%s"`, hostname), - }) + panic("todo") } // authSelfToServer performs the initial authentication request to the server. It authenticates the client to the server. @@ -140,46 +52,5 @@ func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Cl if len(f.challengeClientB64) == 0 { return "", errors.New("missing challenge") } - - sig, err := a.sign(f.challengeClientB64, hostname) - if err != nil { - return "", fmt.Errorf("failed to sign challenge: %w", err) - } - - authValue := fmt.Sprintf( - "%s peer-id=%s, sig=%s, opaque=%s, challenge-server=%s", - PeerIDAuthScheme, - myPeerID.String(), - base64.URLEncoding.EncodeToString(sig), - f.opaque, - base64.URLEncoding.EncodeToString([]byte(challengeServer)), - ) - - // Attempt to read public key from our peer id - _, err = myPeerID.ExtractPublicKey() - if err == peer.ErrNoPublicKey { - // If it fails we need to include the public key explicitly - pubKey := a.PrivKey.GetPublic() - pubKeyBytes, err := crypto.MarshalPublicKey(pubKey) - if err != nil { - return "", fmt.Errorf("failed to marshal public key: %w", err) - } - authValue += ", public-key=" + base64.URLEncoding.EncodeToString(pubKeyBytes) - } else if err != nil { - return "", fmt.Errorf("failed to extract public key: %w", err) - } - return authValue, nil -} - -func (a *ClientPeerIDAuth) verifySigFromServer(r authFields, myPeerID peer.ID, challengeServer []byte) (peer.ID, error) { - partsToVerify := make([]string, 0, 3) - partsToVerify = append(partsToVerify, fmt.Sprintf(`hostname="%s"`, r.hostname)) - partsToVerify = append(partsToVerify, "challenge-server="+base64.URLEncoding.EncodeToString(challengeServer)) - partsToVerify = append(partsToVerify, "client="+myPeerID.String()) - - err := verifySig(r.pubKey, PeerIDAuthScheme, partsToVerify, r.signature) - if err != nil { - return "", fmt.Errorf("failed to verify signature: %s", err) - } - return peer.IDFromPublicKey(r.pubKey) + panic("todo") } diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go new file mode 100644 index 0000000000..39189cf43c --- /dev/null +++ b/p2p/http/auth/internal/handshake/client.go @@ -0,0 +1,151 @@ +package handshake + +import ( + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "net/http" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +type peerIDAuthClientState int + +const ( + peerIDAuthClientStateSignChallenge peerIDAuthClientState = iota + peerIDAuthClientStateVerifyChallenge + peerIDAuthClientStateDone // We have the bearer token, and there's nothing left to do +) + +type PeerIDAuthHandshakeClient struct { + Hostname string + PrivKey crypto.PrivKey + + serverPeerID peer.ID + ran bool + state peerIDAuthClientState + p params + hb headerBuilder + challengeServer [challengeLen]byte +} + +var errMissingChallenge = errors.New("missing challenge") + +func (h *PeerIDAuthHandshakeClient) ParseHeaderVal(headerVal []byte) error { + if h.state == peerIDAuthClientStateDone { + return nil + } + h.p = params{} + + if len(headerVal) == 0 { + return errMissingChallenge + } + + err := h.p.parsePeerIDAuthSchemeParams(headerVal) + if err != nil { + return err + } + + if h.p.challengeClient != nil { + h.state = peerIDAuthClientStateSignChallenge + return nil + } + + if h.p.sigB64 != nil { + h.state = peerIDAuthClientStateVerifyChallenge + return nil + } + + return errors.New("missing challenge or signature") +} + +func (h *PeerIDAuthHandshakeClient) Run() error { + h.ran = true + clientPubKeyBytes, err := crypto.MarshalPublicKey(h.PrivKey.GetPublic()) + if err != nil { + return err + } + switch h.state { + case peerIDAuthClientStateSignChallenge: + clientSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ + {"challenge-client", h.p.challengeClient}, + {"hostname", []byte(h.Hostname)}, + }) + if err != nil { + return fmt.Errorf("failed to sign challenge: %w", err) + } + _, err = rand.Read(h.challengeServer[:]) + if err != nil { + return err + } + copy(h.challengeServer[:], base64.URLEncoding.AppendEncode(nil, h.challengeServer[:])) + + h.hb.clear() + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) + h.hb.writeParam("opaque", h.p.opaqueB64) + h.hb.writeParam("challenge-server", h.challengeServer[:]) + h.hb.writeParamB64(nil, "sig", clientSig) + return nil + case peerIDAuthClientStateVerifyChallenge: + serverPubKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + sig, err := base64.URLEncoding.AppendDecode(nil, h.p.sigB64) + if err != nil { + return fmt.Errorf("failed to decode signature: %w", err) + } + serverPubKey, err := crypto.UnmarshalPublicKey(serverPubKeyBytes) + if err != nil { + return err + } + err = verifySig(serverPubKey, PeerIDAuthScheme, []sigParam{ + {"challenge-server", h.challengeServer[:]}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }, sig) + if err != nil { + return err + } + h.serverPeerID, err = peer.IDFromPublicKey(serverPubKey) + if err != nil { + return err + } + + h.hb.clear() + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParam("bearer", h.p.bearerTokenB64) + h.state = peerIDAuthClientStateDone + + return nil + case peerIDAuthClientStateDone: + return nil + } + + return errors.New("unhandled state") +} + +// PeerID returns the peer ID of the authenticated client. +func (h *PeerIDAuthHandshakeClient) PeerID() (peer.ID, error) { + if !h.ran { + return "", errNotRan + } + switch h.state { + case peerIDAuthClientStateVerifyChallenge: + case peerIDAuthClientStateDone: + default: + return "", errors.New("not in proper state") + } + + return h.serverPeerID, nil +} + +func (h *PeerIDAuthHandshakeClient) SetHeader(hdr http.Header) { + if !h.ran { + return + } + hdr.Set("Authorization", h.hb.b.String()) +} diff --git a/p2p/http/auth/internal/handshake/handshake.go b/p2p/http/auth/internal/handshake/handshake.go new file mode 100644 index 0000000000..41e87f087d --- /dev/null +++ b/p2p/http/auth/internal/handshake/handshake.go @@ -0,0 +1,198 @@ +package handshake + +import ( + "bufio" + "bytes" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "slices" + "strings" + + "github.com/libp2p/go-libp2p/core/crypto" + + pool "github.com/libp2p/go-buffer-pool" +) + +const PeerIDAuthScheme = "libp2p-PeerID" +const challengeLen = 32 +const maxHeaderSize = 8192 + +var peerIDAuthSchemeBytes = []byte(PeerIDAuthScheme) + +var errTooBig = errors.New("header value too big") +var errInvalid = errors.New("invalid header value") +var errNotRan = errors.New("not ran. call Run() first") + +// params represent params passed in via headers. All []byte fields to avoid allocations. +type params struct { + bearerTokenB64 []byte + challengeClient []byte + challengeServer []byte + opaqueB64 []byte + publicKeyB64 []byte + sigB64 []byte +} + +// parsePeerIDAuthSchemeParams parses the parameters of the PeerID auth scheme +// from the header string. zero alloc. +func (p *params) parsePeerIDAuthSchemeParams(headerVal []byte) error { + if len(headerVal) > maxHeaderSize { + return errTooBig + } + startIdx := bytes.Index(headerVal, []byte(PeerIDAuthScheme)) + if startIdx == -1 { + return nil + } + + headerVal = headerVal[startIdx+len(PeerIDAuthScheme):] + advance, token, err := splitAuthHeaderParams(headerVal, true) + for ; err == nil; advance, token, err = splitAuthHeaderParams(headerVal, true) { + headerVal = headerVal[advance:] + bs := token + splitAt := bytes.Index(bs, []byte("=")) + if splitAt == -1 { + return errInvalid + } + kB := bs[:splitAt] + v := bs[splitAt+1:] + if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' { + return errInvalid + } + v = v[1 : len(v)-1] // drop quotes + switch string(kB) { + case "bearer": + p.bearerTokenB64 = v + case "challenge-client": + p.challengeClient = v + case "challenge-server": + p.challengeServer = v + case "opaque": + p.opaqueB64 = v + case "public-key": + p.publicKeyB64 = v + case "sig": + p.sigB64 = v + } + } + return nil +} + +func splitAuthHeaderParams(data []byte, atEOF bool) (advance int, token []byte, err error) { + if len(data) == 0 && atEOF { + return 0, nil, bufio.ErrFinalToken + } + + start := 0 + for start < len(data) && (data[start] == ' ' || data[start] == ',') { + start++ + } + if start == len(data) { + return len(data), nil, nil + } + end := start + 1 + for end < len(data) && data[end] != ' ' && data[end] != ',' { + end++ + } + token = data[start:end] + if !bytes.ContainsAny(token, "=") { + // This isn't a param. It's likely the next scheme. We're done + return len(data), nil, bufio.ErrFinalToken + } + + return end, token, nil +} + +type headerBuilder struct { + b strings.Builder + pastFirstField bool +} + +func (h *headerBuilder) clear() { + h.b.Reset() + h.pastFirstField = false +} + +func (h *headerBuilder) writeScheme(scheme string) { + h.b.WriteString(scheme) + h.b.WriteByte(' ') +} + +func (h *headerBuilder) maybeAddComma() { + if !h.pastFirstField { + h.pastFirstField = true + return + } + h.b.WriteString(", ") +} + +// writeParam writes a key value pair to the header. It first b64 encodes the value. +// It uses buf as a scratch space. +func (h *headerBuilder) writeParamB64(buf []byte, key string, val []byte) { + if buf == nil { + buf = make([]byte, base64.URLEncoding.EncodedLen(len(val))) + } + encodedVal := base64.URLEncoding.AppendEncode(buf[:0], val) + h.writeParam(key, encodedVal) +} + +// writeParam writes a key value pair to the header. It writes the val as-is. +func (h *headerBuilder) writeParam(key string, val []byte) { + h.maybeAddComma() + + h.b.Grow(len(key) + len(`="`) + len(val) + 1) + // Not doing fmt.Fprintf here to avoid one allocation + h.b.WriteString(key) + h.b.WriteString(`="`) + h.b.Write(val) + h.b.WriteByte('"') +} + +type sigParam struct { + k string + v []byte +} + +func verifySig(publicKey crypto.PubKey, prefix string, signedParts []sigParam, sig []byte) error { + b := pool.Get(4096) + defer pool.Put(b) + buf, err := genDataToSign(b[:0], prefix, signedParts) + if err != nil { + return fmt.Errorf("failed to generate signed data: %w", err) + } + ok, err := publicKey.Verify(buf, sig) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("signature verification failed") + } + + return nil +} + +func sign(privKey crypto.PrivKey, prefix string, partsToSign []sigParam) ([]byte, error) { + b := pool.Get(4096) + defer pool.Put(b) + buf, err := genDataToSign(b[:0], prefix, partsToSign) + if err != nil { + return nil, fmt.Errorf("failed to generate data to sign: %w", err) + } + return privKey.Sign(buf) +} + +func genDataToSign(buf []byte, prefix string, parts []sigParam) ([]byte, error) { + // Sort the parts in lexicographic order + slices.SortFunc(parts, func(a, b sigParam) int { + return strings.Compare(a.k, b.k) + }) + buf = append(buf, []byte(prefix)...) + for _, p := range parts { + buf = binary.AppendUvarint(buf, uint64(len(p.k)+1+len(p.v))) // +1 for '=' + buf = append(buf, p.k...) + buf = append(buf, '=') + buf = append(buf, p.v...) + } + return buf, nil +} diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go new file mode 100644 index 0000000000..b4a4a0e51b --- /dev/null +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -0,0 +1,307 @@ +package handshake + +import ( + "bytes" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" +) + +func TestHandshake(t *testing.T) { + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + + headers := make(http.Header) + + // Start the handshake + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client receives the challenge and signs it. Also sends the challenge server + require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.SetHeader(headers) + + // Server receives the sig and verifies it. Also signs the challenge server + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client verifies sig and sets the bearer token for future requests + require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("Authentication-Info")))) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.SetHeader(headers) + + // Server verifies the bearer token + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + expectedClientPeerID, _ := peer.IDFromPrivateKey(clientPriv) + expectedServerPeerID, _ := peer.IDFromPrivateKey(serverPriv) + clientPeerID, err := serverHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + serverPeerID, err := clientHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedServerPeerID, serverPeerID) +} + +func BenchmarkServerHandshake(b *testing.B) { + clientHeader1 := make(http.Header) + clientHeader2 := make(http.Header) + headers := make(http.Header) + + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + require.NoError(b, serverHandshake.ParseHeaderVal(nil)) + require.NoError(b, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client receives the challenge and signs it. Also sends the challenge server + require.NoError(b, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + clear(headers) + require.NoError(b, clientHandshake.Run()) + clientHandshake.SetHeader(clientHeader1) + + // Server receives the sig and verifies it. Also signs the challenge server + serverHandshake.Reset() + require.NoError(b, serverHandshake.ParseHeaderVal([]byte(clientHeader1.Get("Authorization")))) + clear(headers) + require.NoError(b, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client verifies sig and sets the bearer token for future requests + require.NoError(b, clientHandshake.ParseHeaderVal([]byte(headers.Get("Authentication-Info")))) + clear(headers) + require.NoError(b, clientHandshake.Run()) + clientHandshake.SetHeader(clientHeader2) + + // Server verifies the bearer token + serverHandshake.Reset() + require.NoError(b, serverHandshake.ParseHeaderVal([]byte(clientHeader2.Get("Authorization")))) + clear(headers) + require.NoError(b, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + initialClientAuth := []byte(clientHeader1.Get("Authorization")) + bearerClientAuth := []byte(clientHeader2.Get("Authorization")) + _ = initialClientAuth + _ = bearerClientAuth + + b.ResetTimer() + for i := 0; i < b.N; i++ { + serverHandshake.Reset() + serverHandshake.ParseHeaderVal(nil) + serverHandshake.Run() + + serverHandshake.Reset() + serverHandshake.ParseHeaderVal(initialClientAuth) + serverHandshake.Run() + + serverHandshake.Reset() + serverHandshake.ParseHeaderVal(bearerClientAuth) + serverHandshake.Run() + } + +} + +func TestParsePeerIDAuthSchemeParams(t *testing.T) { + str := `libp2p-PeerID sig="", public-key="", bearer=""` + p := params{} + expectedParam := params{ + sigB64: []byte(``), + publicKeyB64: []byte(``), + bearerTokenB64: []byte(``), + } + err := p.parsePeerIDAuthSchemeParams([]byte(str)) + require.NoError(t, err) + require.Equal(t, expectedParam, p) +} + +func BenchmarkParsePeerIDAuthSchemeParams(b *testing.B) { + str := []byte(`libp2p-PeerID peer-id="", sig="", public-key="", bearer=""`) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + p := params{} + err := p.parsePeerIDAuthSchemeParams(str) + if err != nil { + b.Fatal(err) + } + } +} + +func TestHeaderBuilder(t *testing.T) { + hb := headerBuilder{} + hb.writeScheme(PeerIDAuthScheme) + hb.writeParam("peer-id", []byte("foo")) + hb.writeParam("challenge-client", []byte("something-else")) + hb.writeParam("hostname", []byte("example.com")) + + expected := `libp2p-PeerID peer-id="foo", challenge-client="something-else", hostname="example.com"` + require.Equal(t, expected, hb.b.String()) +} + +func BenchmarkHeaderBuilder(b *testing.B) { + h := headerBuilder{} + scratch := make([]byte, 256) + scratch = scratch[:0] + + b.ResetTimer() + for i := 0; i < b.N; i++ { + h.b.Grow(256) + h.writeParamB64(scratch, "foo", []byte("bar")) + h.clear() + } +} + +// Test Vectors +var zeroBytes = make([]byte, 64) +var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) + +// Peer ID derived from the zero key +var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) + +func TestOpaqueStateRoundTrip(t *testing.T) { + zeroBytes := [32]byte{} + + // To drop the monotonic clock reading + timeAfterUnmarshal := time.Now() + b, err := json.Marshal(timeAfterUnmarshal) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, &timeAfterUnmarshal)) + hmac := hmac.New(sha256.New, zeroBytes[:]) + + o := opaqueState{ + ChallengeClient: "foo-bar", + CreatedTime: timeAfterUnmarshal, + IsToken: true, + PeerID: &zeroID, + Hostname: "example.com", + } + + hmac.Reset() + b, err = o.Marshal(hmac, nil) + require.NoError(t, err) + + o2 := opaqueState{} + + hmac.Reset() + err = o2.Unmarshal(hmac, b) + require.NoError(t, err) + require.EqualValues(t, o, o2) +} + +func FuzzServerHandshakeNoPanic(f *testing.F) { + zeroBytes := [32]byte{} + hmac := hmac.New(sha256.New, zeroBytes[:]) + + f.Fuzz(func(t *testing.T, data []byte) { + hmac.Reset() + h := PeerIDAuthHandshakeServer{ + Hostname: "example.com", + PrivKey: zeroKey, + Hmac: hmac, + } + err := h.ParseHeaderVal(data) + if err != nil { + return + } + err = h.Run() + if err != nil { + return + } + h.PeerID() + }) +} + +func BenchmarkOpaqueStateWrite(b *testing.B) { + zeroBytes := [32]byte{} + hmac := hmac.New(sha256.New, zeroBytes[:]) + o := opaqueState{ + ChallengeClient: "foo-bar", + CreatedTime: time.Now(), + } + d := make([]byte, 512) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hmac.Reset() + _, err := o.Marshal(hmac, d[:0]) + if err != nil { + b.Fatal(err) + } + } +} + +func BenchmarkOpaqueStateRead(b *testing.B) { + zeroBytes := [32]byte{} + hmac := hmac.New(sha256.New, zeroBytes[:]) + o := opaqueState{ + ChallengeClient: "foo-bar", + CreatedTime: time.Now(), + } + d := make([]byte, 256) + d, err := o.Marshal(hmac, d[:0]) + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + hmac.Reset() + err := o.Unmarshal(hmac, d) + if err != nil { + b.Fatal(err) + } + } +} + +func FuzzParsePeerIDAuthSchemeParamsNoPanic(f *testing.F) { + p := params{} + // Just check that we don't panic + f.Fuzz(func(t *testing.T, data []byte) { + p.parsePeerIDAuthSchemeParams(data) + }) +} diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go new file mode 100644 index 0000000000..3777818c47 --- /dev/null +++ b/p2p/http/auth/internal/handshake/server.go @@ -0,0 +1,293 @@ +package handshake + +import ( + "crypto/hmac" + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "hash" + "net/http" + "time" + + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" +) + +const challengeTTL = 5 * time.Minute + +type peerIDAuthServerState int + +const ( + peerIDAuthServerStateChallengeClient peerIDAuthServerState = iota + peerIDAuthServerStateVerifyChallenge + peerIDAuthServerStateVerifyBearer +) + +type opaqueState struct { + IsToken bool `json:"is-token"` + PeerID *peer.ID `json:"peer-id"` + ChallengeClient string `json:"challenge-client"` + Hostname string `json:"hostname"` + CreatedTime time.Time `json:"created-time"` +} + +// Marshal serializes the state by appending it to the byte slice. +func (o *opaqueState) Marshal(hmac hash.Hash, b []byte) ([]byte, error) { + hmac.Reset() + fieldsMarshalled, err := json.Marshal(o) + if err != nil { + return b, err + } + _, err = hmac.Write(fieldsMarshalled) + if err != nil { + return b, err + } + b = hmac.Sum(b) + b = append(b, fieldsMarshalled...) + return b, nil +} + +var errInvalidHMAC = errors.New("invalid HMAC") + +func (o *opaqueState) Unmarshal(hmacImpl hash.Hash, d []byte) error { + hmacImpl.Reset() + if len(d) < hmacImpl.Size() { + return errInvalidHMAC + } + hmacVal := d[:hmacImpl.Size()] + fields := d[hmacImpl.Size():] + _, err := hmacImpl.Write(fields) + if err != nil { + return err + } + expectedHmac := hmacImpl.Sum(nil) + if !hmac.Equal(hmacVal, expectedHmac) { + return errInvalidHMAC + } + + err = json.Unmarshal(fields, &o) + if err != nil { + return err + } + return nil +} + +type PeerIDAuthHandshakeServer struct { + Hostname string + PrivKey crypto.PrivKey + TokenTTL time.Duration + // used to authenticate opaque blobs and tokens + Hmac hash.Hash + + ran bool + buf [1024]byte + + state peerIDAuthServerState + p params + hb headerBuilder + + opaque opaqueState +} + +var errInvalidHeader = errors.New("invalid header") + +func (h *PeerIDAuthHandshakeServer) Reset() { + h.Hmac.Reset() + h.ran = false + clear(h.buf[:]) + h.state = 0 + h.p = params{} + h.hb.clear() + h.opaque = opaqueState{} +} +func (h *PeerIDAuthHandshakeServer) ParseHeaderVal(headerVal []byte) error { + if len(headerVal) == 0 { + // We are in the initial state. Nothing to parse. + return nil + } + err := h.p.parsePeerIDAuthSchemeParams(headerVal) + if err != nil { + return err + } + if h.p.sigB64 != nil && h.p.opaqueB64 != nil { + h.state = peerIDAuthServerStateVerifyChallenge + return nil + } + if h.p.bearerTokenB64 != nil { + h.state = peerIDAuthServerStateVerifyBearer + return nil + } + + return errInvalidHeader +} + +var errExpiredChallenge = errors.New("challenge expired") +var errExpiredToken = errors.New("token expired") + +func (h *PeerIDAuthHandshakeServer) Run() error { + h.ran = true + switch h.state { + case peerIDAuthServerStateChallengeClient: + h.hb.writeScheme(PeerIDAuthScheme) + { + _, err := rand.Read(h.buf[:challengeLen]) + if err != nil { + return err + } + encodedChallenge := base64.URLEncoding.AppendEncode(h.buf[challengeLen:challengeLen], h.buf[:challengeLen]) + h.opaque = opaqueState{ + ChallengeClient: string(encodedChallenge), + Hostname: h.Hostname, + CreatedTime: time.Now(), + } + h.hb.writeParam("challenge-client", encodedChallenge) + } + { + opaqueVal, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[len(opaqueVal):], "opaque", opaqueVal) + } + case peerIDAuthServerStateVerifyChallenge: + { + opaque, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.opaqueB64) + if err != nil { + return err + } + err = h.opaque.Unmarshal(h.Hmac, opaque) + if err != nil { + return err + } + } + if time.Now().After(h.opaque.CreatedTime.Add(challengeTTL)) { + return errExpiredChallenge + } + if h.opaque.IsToken { + return errors.New("expected challenge, got token") + } + + if h.Hostname != h.opaque.Hostname { + return errors.New("hostname in opaque mismatch") + } + + // If we got a public key, check that it matches the peer id + if len(h.p.publicKeyB64) == 0 { + return errors.New("missing public key") + } + publicKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + pubKey, err := crypto.UnmarshalPublicKey(publicKeyBytes) + if err != nil { + return err + } + + { + sig, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.sigB64) + if err != nil { + return fmt.Errorf("failed to decode signature: %w", err) + } + err = verifySig(pubKey, PeerIDAuthScheme, []sigParam{ + {k: "challenge-client", v: []byte(h.opaque.ChallengeClient)}, + {k: "hostname", v: []byte(h.Hostname)}, + }, sig) + if err != nil { + return err + } + } + + // We authenticated the client, now authenticate ourselves + serverSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ + {"challenge-server", h.p.challengeServer}, + {"client-public-key", publicKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }) + if err != nil { + return fmt.Errorf("failed to sign challenge: %w", err) + } + + peerID, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return err + } + + // And create a bearer token for the client + h.opaque = opaqueState{ + IsToken: true, + PeerID: &peerID, + Hostname: h.Hostname, + CreatedTime: time.Now(), + } + serverPubKey := h.PrivKey.GetPublic() + pubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) + if err != nil { + return err + } + + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParamB64(h.buf[:], "sig", serverSig) + { + bearerToken, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[len(bearerToken):], "bearer", bearerToken) + } + h.hb.writeParamB64(h.buf[:], "public-key", pubKeyBytes) + case peerIDAuthServerStateVerifyBearer: + { + bearerToken, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.bearerTokenB64) + if err != nil { + return err + } + err = h.opaque.Unmarshal(h.Hmac, bearerToken) + if err != nil { + return err + } + } + if !h.opaque.IsToken { + return errors.New("expected token, got challenge") + } + + if time.Now().After(h.opaque.CreatedTime.Add(h.TokenTTL)) { + return errExpiredToken + } + + return nil + } + + return nil +} + +// PeerID returns the peer ID of the authenticated client. +func (h *PeerIDAuthHandshakeServer) PeerID() (peer.ID, error) { + if !h.ran { + return "", errNotRan + } + switch h.state { + case peerIDAuthServerStateVerifyChallenge: + case peerIDAuthServerStateVerifyBearer: + default: + return "", errors.New("not in proper state") + } + return *h.opaque.PeerID, nil +} + +func (h *PeerIDAuthHandshakeServer) SetHeader(hdr http.Header) { + if !h.ran { + return + } + defer h.hb.clear() + switch h.state { + case peerIDAuthServerStateChallengeClient: + hdr.Set("WWW-Authenticate", h.hb.b.String()) + case peerIDAuthServerStateVerifyChallenge: + hdr.Set("Authentication-Info", h.hb.b.String()) + case peerIDAuthServerStateVerifyBearer: + // For completeness. Nothing to do + } +} diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 3fdf358bb6..9299d124fc 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -1,19 +1,11 @@ package httppeeridauth import ( - "bytes" - "crypto/rand" - "encoding/base64" - "encoding/binary" "errors" - "fmt" "net/http" - "strings" "time" - pool "github.com/libp2p/go-buffer-pool" "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" ) const maxAuthHeaderSize = 8192 @@ -49,422 +41,4 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } - // Do they have a bearer token? - _, err := a.UnwrapBearerToken(r, hostname) - if err != nil { - // No bearer token, let's try peer ID auth - _, ok := a.ValidHostnames[hostname] - if !ok { - log.Debugf("Unauthorized request from %s: invalid hostname", hostname) - w.WriteHeader(http.StatusBadRequest) - return - } - - f, err := parseAuthFields(r.Header.Get("Authorization"), hostname, true) - if err != nil { - a.serveAuthReq(w) - return - } - - var id peer.ID - id, err = a.authenticate(f) - if err != nil { - log.Debugf("failed to authenticate: %s", err) - a.serveAuthReq(w) - return - } - - tok := bearerToken{ - peer: id, - hostname: f.hostname, - createdAt: time.Now(), - } - bearerToken, err := genBearerAuthHeader(a.PrivKey, tok) - if err != nil { - log.Debugf("failed to generate bearer token: %s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if base64.URLEncoding.DecodedLen(len(f.challengeServerB64)) >= challengeLen { - clientID, err := peer.IDFromPublicKey(f.pubKey) - if err != nil { - log.Debugf("failed to get peer ID: %s", err) - w.WriteHeader(http.StatusBadRequest) - return - } - buf, err := a.signChallengeServer(f.challengeServerB64, clientID, f.hostname) - if err != nil { - log.Debugf("failed to sign challenge: %s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - myId, err := peer.IDFromPublicKey(a.PrivKey.GetPublic()) - if err != nil { - log.Debugf("failed to get peer ID: %s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - authInfoVal := fmt.Sprintf("%s peer-id=%s, sig=%s %s", PeerIDAuthScheme, myId.String(), base64.URLEncoding.EncodeToString(buf), bearerToken) - w.Header().Set("Authentication-Info", authInfoVal) - } else { - // Only supporting mutual auth for now. Fail because the client didn't want to authenticate us. - log.Debugf("Client did not provide challenge-server") - w.WriteHeader(http.StatusBadRequest) - return - } - - if a.Next != nil { - // Set the token on the request so the next handler can read it - authHeader := r.Header.Get("Authorization") - if len(authHeader) == 0 { - authHeader = PeerIDAuthScheme + " " + bearerToken - } else { - authHeader = authHeader + ", " + bearerToken - } - r.Header.Set("Authorization", authHeader) - } - } - - if a.Next == nil { - // No next handler, just return - w.WriteHeader(http.StatusOK) - return - } - a.Next.ServeHTTP(w, r) -} - -type peerIDAuthServerState int - -const ( - peerIDAuthServerStateChallengeClient peerIDAuthServerState = iota - peerIDAuthServerStateVerify - peerIDAuthServerStateDone -) - -type peerIDAuthHandshakeServer struct { - serverPrivKey crypto.PrivKey - // HMACKey is used to authenticate the opaque blobs and token - hmacKey []byte - - state peerIDAuthServerState - - peerID []byte // the string representation of the peer ID (as bytes) - publicKey []byte - - challengeClient []byte - challengeServer []byte - - bearerToken []byte -} - -func (a *ServerPeerIDAuth) signChallengeServer(challengeServerB64 string, client peer.ID, hostname string) ([]byte, error) { - if len(challengeServerB64) == 0 { - return nil, errors.New("missing challenge") - } - partsToSign := []string{ - "challenge-server=" + challengeServerB64, - "client=" + client.String(), - fmt.Sprintf(`hostname="%s"`, hostname), - } - sig, err := sign(a.PrivKey, PeerIDAuthScheme, partsToSign) - if err != nil { - return nil, fmt.Errorf("failed to sign challenge: %w", err) - } - return sig, nil -} - -func (a *ServerPeerIDAuth) authenticate(f authFields) (peer.ID, error) { - partsToVerify := make([]string, 0, 2) - o, err := getChallengeFromOpaque(a.PrivKey, []byte(f.opaque)) - if err != nil { - return "", fmt.Errorf("failed to get challenge from opaque: %s", err) - } - if time.Now().After(o.createdTime.Add(challengeTTL)) { - return "", errors.New("challenge expired") - } - challengeClient := o.challenge - if len(challengeClient) > 0 { - partsToVerify = append(partsToVerify, "challenge-client="+base64.URLEncoding.EncodeToString(challengeClient)) - } - partsToVerify = append(partsToVerify, fmt.Sprintf(`hostname="%s"`, f.hostname)) - - err = verifySig(f.pubKey, PeerIDAuthScheme, partsToVerify, f.signature) - if err != nil { - return "", fmt.Errorf("failed to verify signature: %s", err) - } - return peer.IDFromPublicKey(f.pubKey) -} - -func (a *ServerPeerIDAuth) UnwrapBearerToken(r *http.Request, expectedHostname string) (peer.ID, error) { - if !strings.Contains(r.Header.Get("Authorization"), PeerIDAuthScheme) { - return "", errors.New("missing bearer auth scheme") - } - schemes, err := parseAuthHeader(r.Header.Get("Authorization")) - if err != nil { - return "", fmt.Errorf("failed to parse auth header: %w", err) - } - bearerScheme, ok := schemes[PeerIDAuthScheme] - if !ok { - return "", fmt.Errorf("missing bearer auth scheme") - } - return a.unwrapBearerToken(expectedHostname, bearerScheme) -} - -func (a *ServerPeerIDAuth) unwrapBearerToken(expectedHostname string, s authScheme) (peer.ID, error) { - buf := pool.Get(4096) - defer pool.Put(buf) - buf, err := b64AppendDecode(buf[:0], []byte(s.params["bearer"])) - if err != nil { - return "", fmt.Errorf("failed to decode bearer token: %w", err) - } - parsed, err := parseBearerTokenBlob(a.PrivKey, buf) - if err != nil { - return "", fmt.Errorf("failed to parse bearer token: %w", err) - } - if time.Now().After(parsed.createdAt.Add(a.TokenTTL)) { - return "", fmt.Errorf("bearer token expired") - } - if parsed.hostname != expectedHostname { - return "", fmt.Errorf("bearer token hostname mismatch") - } - return parsed.peer, nil -} - -type bearerToken struct { - peer peer.ID - hostname string - createdAt time.Time -} - -func genBearerAuthHeader(privKey crypto.PrivKey, t bearerToken) (string, error) { - b := pool.Get(4096) - defer pool.Put(b) - b, err := genBearerTokenBlob(b[:0], privKey, t) - if err != nil { - return "", err - } - blobB64 := pool.Get(len(bearerTokenPrefix) + base64.URLEncoding.EncodedLen(len(b))) - defer pool.Put(blobB64) - - blobB64 = blobB64[:0] - blobB64 = append(blobB64, []byte(bearerTokenPrefix)...) - blobB64 = append(blobB64, byte('"')) - blobB64 = b64AppendEncode(blobB64, b) - blobB64 = append(blobB64, byte('"')) - return string(blobB64), nil -} - -func genBearerTokenBlob(buf []byte, privKey crypto.PrivKey, t bearerToken) ([]byte, error) { - peerBytes, err := t.peer.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal peer ID: %w", err) - } - createdAtBytes, err := t.createdAt.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal createdAt: %w", err) - } - - // Auth scheme prefix - buf = append(buf, []byte(PeerIDAuthScheme+" bearer")...) - buf = append(buf, ' ') // Space between auth scheme and the token - - // Peer ID - buf = binary.AppendUvarint(buf, uint64(len(peerBytes))) - buf = append(buf, peerBytes...) - - // Hostname - buf = binary.AppendUvarint(buf, uint64(len(t.hostname))) - buf = append(buf, t.hostname...) - - // Created at - buf = binary.AppendUvarint(buf, uint64(len(createdAtBytes))) - buf = append(buf, createdAtBytes...) - - sig, err := privKey.Sign(buf) - if err != nil { - return nil, fmt.Errorf("failed to sign bearer token: %w", err) - } - buf = append(buf, sig...) - - return buf, nil -} - -func parseBearerTokenBlob(privKey crypto.PrivKey, blob []byte) (bearerToken, error) { - originalSlice := blob - - // Auth scheme prefix +1 for space - if len(PeerIDAuthScheme+" bearer")+1 > len(blob) { - return bearerToken{}, fmt.Errorf("bearer token too short") - } - hasPrefix := bytes.Equal([]byte(PeerIDAuthScheme+" bearer"), blob[:len(PeerIDAuthScheme+" bearer")]) - if !hasPrefix { - return bearerToken{}, fmt.Errorf("missing bearer token prefix") - } - blob = blob[len(PeerIDAuthScheme+" bearer"):] - if blob[0] != ' ' { - return bearerToken{}, fmt.Errorf("missing space after auth scheme") - } - blob = blob[1:] - - // Peer ID - peerIDLen, n := binary.Uvarint(blob) - if n <= 0 { - return bearerToken{}, fmt.Errorf("failed to read peer ID length") - } - - blob = blob[n:] - if int(peerIDLen) > len(blob) { - return bearerToken{}, fmt.Errorf("peer ID length is wrong") - } - var peer peer.ID - err := peer.UnmarshalBinary(blob[:peerIDLen]) - if err != nil { - return bearerToken{}, fmt.Errorf("failed to unmarshal peer ID: %w", err) - } - blob = blob[peerIDLen:] - - // Hostname - hostnameLen, n := binary.Uvarint(blob) - if n <= 0 { - return bearerToken{}, fmt.Errorf("failed to read hostname length") - } - blob = blob[n:] - if int(hostnameLen) > len(blob) { - return bearerToken{}, fmt.Errorf("hostname length is wrong") - } - hostname := string(blob[:hostnameLen]) - blob = blob[hostnameLen:] - - // Created At - createdAtLen, n := binary.Uvarint(blob) - if n <= 0 { - return bearerToken{}, fmt.Errorf("failed to read created at length") - } - blob = blob[n:] - if int(createdAtLen) > len(blob) { - return bearerToken{}, fmt.Errorf("created at length is wrong") - } - var createdAt time.Time - err = createdAt.UnmarshalBinary(blob[:createdAtLen]) - if err != nil { - return bearerToken{}, fmt.Errorf("failed to unmarshal created at: %w", err) - } - sig := blob[createdAtLen:] - if len(sig) == 0 { - return bearerToken{}, fmt.Errorf("missing signature") - } - - blobWithoutSig := originalSlice[:len(originalSlice)-len(sig)] - ok, err := privKey.GetPublic().Verify(blobWithoutSig, sig) - if err != nil { - return bearerToken{}, fmt.Errorf("failed to verify signature: %w", err) - } - if !ok { - return bearerToken{}, fmt.Errorf("signature verification failed") - } - return bearerToken{peer: peer, hostname: hostname, createdAt: createdAt}, nil -} - -type opaqueUnwrapped struct { - challenge []byte - createdTime time.Time -} - -func getChallengeFromOpaque(privKey crypto.PrivKey, opaqueB64 []byte) (opaqueUnwrapped, error) { - if len(opaqueB64) == 0 { - return opaqueUnwrapped{}, fmt.Errorf("missing opaque blob") - } - - opaqueBlob := pool.Get(2048) - defer pool.Put(opaqueBlob) - opaqueBlob, err := b64AppendDecode(opaqueBlob[:0], opaqueB64) - if err != nil { - return opaqueUnwrapped{}, fmt.Errorf("failed to decode opaque blob: %w", err) - } - if len(opaqueBlob) == 0 { - return opaqueUnwrapped{}, fmt.Errorf("missing opaque blob") - } - if len(opaqueBlob) < challengeLen { - return opaqueUnwrapped{}, fmt.Errorf("opaque blob too short") - } - - // The form of the opaque blob is: - timeBytesLen, n := binary.Uvarint(opaqueBlob) - if n <= 0 { - return opaqueUnwrapped{}, fmt.Errorf("failed to read timeBytesLen") - } - fullPayload := opaqueBlob // Store the full payload so we can verify the signature - opaqueBlob = opaqueBlob[n:] - timeBytes := opaqueBlob[:timeBytesLen] - createdTime := time.Time{} - err = createdTime.UnmarshalBinary(timeBytes) - if err != nil { - return opaqueUnwrapped{}, fmt.Errorf("failed to unmarshal time: %w", err) - } - opaqueBlob = opaqueBlob[timeBytesLen:] - challenge := opaqueBlob[:challengeLen] - opaqueBlob = opaqueBlob[challengeLen:] - sig := opaqueBlob - payloadWithoutSig := fullPayload[:len(fullPayload)-len(opaqueBlob)] - ok, err := privKey.GetPublic().Verify(payloadWithoutSig, sig) - if err != nil { - return opaqueUnwrapped{}, fmt.Errorf("signature verification failed: %w", err) - } - if !ok { - return opaqueUnwrapped{}, fmt.Errorf("signature verification failed") - } - challengeCopy := make([]byte, challengeLen) // Copy the challenge because the underlying buffer will be returned to the pool - copy(challengeCopy, challenge) - return opaqueUnwrapped{ - challenge: challengeCopy, - createdTime: createdTime, - }, nil -} - -func genOpaqueFromChallenge(buf []byte, now time.Time, privKey crypto.PrivKey, challenge []byte) ([]byte, error) { - timeBytes, err := now.MarshalBinary() - if err != nil { - return nil, fmt.Errorf("failed to marshal time: %w", err) - } - buf = binary.AppendUvarint(buf, uint64(len(timeBytes))) - buf = append(buf, timeBytes...) - buf = append(buf, challenge...) - sig, err := privKey.Sign(buf) - if err != nil { - return nil, fmt.Errorf("failed to sign challenge: %w", err) - } - buf = append(buf, sig...) - return buf, nil -} - -func (a *ServerPeerIDAuth) serveAuthReq(w http.ResponseWriter) { - var challenge [challengeLen]byte - _, err := rand.Read(challenge[:]) - if err != nil { - log.Warnf("failed to generate challenge: %s", err) - w.WriteHeader(http.StatusInternalServerError) - } - - tmp := pool.Get(2048) - defer pool.Put(tmp) - opaque, err := genOpaqueFromChallenge(tmp[:0], time.Now(), a.PrivKey, challenge[:]) - if err != nil { - log.Warnf("failed to generate opaque: %s", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - authHeaderVal := pool.Get(2048) - defer pool.Put(authHeaderVal) - authHeaderVal = authHeaderVal[:0] - authHeaderVal = append(authHeaderVal, serverAuthPrefix...) - authHeaderVal = b64AppendEncode(authHeaderVal, challenge[:]) - authHeaderVal = append(authHeaderVal, ", opaque="...) - authHeaderVal = b64AppendEncode(authHeaderVal, opaque) - - w.Header().Set("WWW-Authenticate", string(authHeaderVal)) - w.WriteHeader(http.StatusUnauthorized) } From a61a437dff3dbe4d6bbdf26dc2d963d569e9386b Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 17:42:21 -0700 Subject: [PATCH 15/31] Implement public API --- p2p/http/auth/auth.go | 347 +----------------- p2p/http/auth/auth_test.go | 125 +++---- p2p/http/auth/client.go | 86 +++-- .../{ => internal/handshake}/alloc_test.go | 2 +- p2p/http/auth/internal/handshake/client.go | 9 + p2p/http/auth/server.go | 79 +++- 6 files changed, 203 insertions(+), 445 deletions(-) rename p2p/http/auth/{ => internal/handshake}/alloc_test.go (95%) diff --git a/p2p/http/auth/auth.go b/p2p/http/auth/auth.go index 9a47be4408..34fe81bb41 100644 --- a/p2p/http/auth/auth.go +++ b/p2p/http/auth/auth.go @@ -1,353 +1,10 @@ package httppeeridauth import ( - "bufio" - "bytes" - "encoding/base64" - "errors" - "fmt" - "regexp" - "slices" - "strings" - logging "github.com/ipfs/go-log/v2" - pool "github.com/libp2p/go-buffer-pool" - "github.com/libp2p/go-libp2p/core/crypto" - "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake" ) -const PeerIDAuthScheme = "libp2p-PeerID" - -var PeerIDAuthSchemeBytes = []byte(PeerIDAuthScheme) - -const bearerTokenPrefix = "bearer=" -const ProtocolID = "/http-peer-id-auth/1.0.0" -const serverAuthPrefix = PeerIDAuthScheme + " challenge-client=" -const challengeLen = 32 +const PeerIDAuthScheme = handshake.PeerIDAuthScheme var log = logging.Logger("httppeeridauth") - -const maxHeaderValSize = 2048 - -var errTooBig = errors.New("header value too big") -var errInvalid = errors.New("invalid header value") - -// params represent params passed in via headers. All []byte fields to avoid allocations. -type params struct { - bearerTokenB64 []byte - challengeClient []byte - challengeServer []byte - opaqueB64 []byte - publicKeyB64 []byte - sigB64 []byte -} - -// parsePeerIDAuthSchemeParams parses the parameters of the PeerID auth scheme -// from the header string. zero alloc. -func (p *params) parsePeerIDAuthSchemeParams(headerVal []byte) error { - if len(headerVal) > maxHeaderValSize { - return errTooBig - } - startIdx := bytes.Index(headerVal, []byte(PeerIDAuthScheme)) - if startIdx == -1 { - return nil - } - - headerVal = headerVal[startIdx+len(PeerIDAuthScheme):] - advance, token, err := splitAuthHeaderParams(headerVal, true) - for ; err == nil; advance, token, err = splitAuthHeaderParams(headerVal, true) { - headerVal = headerVal[advance:] - bs := token - splitAt := bytes.Index(bs, []byte("=")) - if splitAt == -1 { - return errInvalid - } - kB := bs[:splitAt] - v := bs[splitAt+1:] - if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' { - return errInvalid - } - v = v[1 : len(v)-1] // drop quotes - switch string(kB) { - case "bearer": - p.bearerTokenB64 = v - case "challenge-client": - p.challengeClient = v - case "challenge-server": - p.challengeServer = v - case "opaque": - p.opaqueB64 = v - case "public-key": - p.publicKeyB64 = v - case "sig": - p.sigB64 = v - } - } - return nil -} - -type headerBuilder struct { - b strings.Builder - pastFirstField bool -} - -func (h *headerBuilder) clear() { - h.b.Reset() - h.pastFirstField = false -} - -func (h *headerBuilder) writeScheme(scheme string) { - h.b.WriteString(scheme) - h.b.WriteByte(' ') -} - -func (h *headerBuilder) maybeAddComma() { - if !h.pastFirstField { - h.pastFirstField = true - return - } - h.b.WriteString(", ") -} - -// writeParam writes a key value pair to the header. It first b64 encodes the value. -// It uses buf as a scratch space. -func (h *headerBuilder) writeParamB64(buf []byte, key string, val []byte) { - if buf == nil { - buf = make([]byte, base64.URLEncoding.EncodedLen(len(val))) - } - encodedVal := base64.URLEncoding.AppendEncode(buf[:0], val) - h.writeParam(key, encodedVal) -} - -// writeParam writes a key value pair to the header. It writes the val as-is. -func (h *headerBuilder) writeParam(key string, val []byte) { - h.maybeAddComma() - - h.b.Grow(len(key) + len(`="`) + len(val) + 1) - // Not doing fmt.Fprintf here to avoid one allocation - h.b.WriteString(key) - h.b.WriteString(`="`) - h.b.Write(val) - h.b.WriteByte('"') -} - -func splitAuthHeaderParams(data []byte, atEOF bool) (advance int, token []byte, err error) { - if len(data) == 0 && atEOF { - return 0, nil, bufio.ErrFinalToken - } - - start := 0 - for start < len(data) && (data[start] == ' ' || data[start] == ',') { - start++ - } - if start == len(data) { - return len(data), nil, nil - } - end := start + 1 - for end < len(data) && data[end] != ' ' && data[end] != ',' { - end++ - } - token = data[start:end] - if !bytes.ContainsAny(token, "=") { - // This isn't a param. It's likely the next scheme. We're done - return len(data), nil, bufio.ErrFinalToken - } - - return end, token, nil -} - -type authScheme struct { - scheme string - params map[string]string - bearerToken string -} - -const maxSchemes = 4 -const maxParams = 10 - -var paramRegexStr = `([\w-]+)=([\w\d-_=.]+|"[^"]+")` -var paramRegex = regexp.MustCompile(paramRegexStr) - -var authHeaderRegex = regexp.MustCompile(fmt.Sprintf(`(%s+\s+(:?(:?%s)(:?\s*,\s*)?)*)`, PeerIDAuthScheme, paramRegexStr)) - -func parseAuthHeader(headerVal string) (map[string]authScheme, error) { - if len(headerVal) > maxAuthHeaderSize { - return nil, fmt.Errorf("header too long") - } - schemes := authHeaderRegex.FindAllString(headerVal, maxSchemes+1) - if len(schemes) > maxSchemes { - return nil, fmt.Errorf("too many schemes") - } - - if len(schemes) == 0 { - return nil, nil - } - - out := make([]authScheme, 0, 2) - for _, s := range schemes { - s = strings.TrimSpace(s) - schemeEndIdx := strings.IndexByte(s, ' ') - if schemeEndIdx == -1 { - continue - } - scheme := authScheme{scheme: s[:schemeEndIdx]} - switch scheme.scheme { - case PeerIDAuthScheme: - default: - // Ignore unknown schemes - continue - } - params := s[schemeEndIdx+1:] - scheme.params = make(map[string]string, 10) - params = strings.TrimSpace(params) - for _, kv := range paramRegex.FindAllStringSubmatch(params, maxParams) { - if len(kv) != 3 { - return nil, fmt.Errorf("invalid param format") - } - scheme.params[kv[1]] = strings.Trim(kv[2], `"`) - } - out = append(out, scheme) - } - if len(out) == 0 { - return nil, nil - } - - outMap := make(map[string]authScheme, len(out)) - for _, s := range out { - outMap[s.scheme] = s - } - return outMap, nil -} - -type authFields struct { - hostname string - pubKey crypto.PubKey - opaque string - challengeServerB64 string - challengeClientB64 string - signature []byte -} - -func decodeB64PubKey(b64EncodedPubKey string) (crypto.PubKey, error) { - bLen := base64.URLEncoding.DecodedLen(len(b64EncodedPubKey)) - buf := pool.Get(bLen) - defer pool.Put(buf) - - buf, err := b64AppendDecode(buf[:0], []byte(b64EncodedPubKey)) - if err != nil { - return nil, err - } - return crypto.UnmarshalPublicKey(buf) -} - -func parseAuthFields(authHeader string, hostname string, isServer bool) (authFields, error) { - if authHeader == "" { - return authFields{}, errMissingAuthHeader - } - if len(authHeader) > maxAuthHeaderSize { - return authFields{}, errors.New("authorization header too large") - } - - schemes, err := parseAuthHeader(authHeader) - if err != nil { - return authFields{}, err - } - - peerIDAuth, ok := schemes[PeerIDAuthScheme] - if !ok { - return authFields{}, errors.New("no peer ID auth scheme found") - } - - if isServer && peerIDAuth.params["sig"] == "" { - return authFields{}, errors.New("no signature found") - } - sig, err := base64.URLEncoding.DecodeString(peerIDAuth.params["sig"]) - if err != nil { - return authFields{}, fmt.Errorf("failed to decode signature: %s", err) - } - - var pubKey crypto.PubKey - var id peer.ID - if peerIDAuth.params["peer-id"] != "" { - id, err = peer.Decode(peerIDAuth.params["peer-id"]) - if err != nil { - return authFields{}, fmt.Errorf("failed to decode peer ID: %s", err) - } - pubKey, err = id.ExtractPublicKey() - if err != nil && err != peer.ErrNoPublicKey { - return authFields{}, err - } - if err == peer.ErrNoPublicKey { - // RSA key perhaps, see if there is a public-key param - encodedPubKey, ok := peerIDAuth.params["public-key"] - if !ok { - return authFields{}, errors.New("no public key found") - } - pubKey, err = decodeB64PubKey(encodedPubKey) - if err != nil { - return authFields{}, fmt.Errorf("failed to unmarshal public key: %s", err) - } - idFromKey, err := peer.IDFromPublicKey(pubKey) - if err != nil { - return authFields{}, fmt.Errorf("failed to get peer ID from public key: %s", err) - } - if id != idFromKey { - return authFields{}, errors.New("peer ID from public key does not match peer ID") - } - } else { - if encodedPubKey, ok := peerIDAuth.params["public-key"]; ok { - // If there's a public key param, it must match the public key from the peer ID - pubKeyFromParam, err := decodeB64PubKey(encodedPubKey) - if err != nil { - return authFields{}, fmt.Errorf("failed to unmarshal public key: %s", err) - } - if !pubKeyFromParam.Equals(pubKey) { - return authFields{}, errors.New("public key from peer ID does not match public key from param") - } - } - } - } - - challengeServer := peerIDAuth.params["challenge-server"] - - var challengeClient string - if !isServer { - // Only parse this for the client. The server should read this from the opaque field - challengeClient = peerIDAuth.params["challenge-client"] - } - - return authFields{ - hostname: hostname, - pubKey: pubKey, - opaque: peerIDAuth.params["opaque"], - challengeServerB64: challengeServer, - challengeClientB64: challengeClient, - signature: sig, - }, nil -} - -// Same as base64.URLEncoding.AppendEncode, but backported for Go 1.21. Once we are on Go 1.23 we can drop this -func b64AppendEncode(dst, src []byte) []byte { - enc := base64.URLEncoding - n := enc.EncodedLen(len(src)) - dst = slices.Grow(dst, n) - enc.Encode(dst[len(dst):][:n], src) - return dst[:len(dst)+n] -} - -// Same as base64.URLEncoding.AppendDecode, but backported for Go 1.21. Once we are on Go 1.23 we can drop this -func b64AppendDecode(dst, src []byte) ([]byte, error) { - enc := base64.URLEncoding - encNoPad := base64.RawURLEncoding - - // Compute the output size without padding to avoid over allocating. - n := len(src) - for n > 0 && rune(src[n-1]) == base64.StdPadding { - n-- - } - n = encNoPad.DecodedLen(n) - - dst = slices.Grow(dst, n) - n, err := enc.Decode(dst[len(dst):][:n], src) - return dst[:len(dst)+n], err -} diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index b3b6a71aae..1a74d8b8bf 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -3,7 +3,7 @@ package httppeeridauth import ( "bytes" "crypto/rand" - "encoding/hex" + "net/http" "net/http/httptest" "testing" "time" @@ -59,10 +59,12 @@ func TestMutualAuth(t *testing.T) { serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) { t.Helper() auth := ServerPeerIDAuth{ - PrivKey: serverKey, - ValidHostnames: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, - InsecureNoTLS: true, + PrivKey: serverKey, + ValidHostnameFn: func(s string) bool { + return s == "example.com" + }, + TokenTTL: time.Hour, + InsecureNoTLS: true, } ts := httptest.NewServer(&auth) @@ -75,9 +77,11 @@ func TestMutualAuth(t *testing.T) { serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) { t.Helper() auth := ServerPeerIDAuth{ - PrivKey: serverKey, - ValidHostnames: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, + PrivKey: serverKey, + ValidHostnameFn: func(s string) bool { + return s == "example.com" + }, + TokenTTL: time.Hour, } ts := httptest.NewTLSServer(&auth) @@ -90,46 +94,39 @@ func TestMutualAuth(t *testing.T) { for _, ctc := range clientTestCases { for _, stc := range serverTestCases { t.Run(ctc.name+"+"+stc.name, func(t *testing.T) { - // ts, serverAuth := stc.serverGen(t) - // client := ts.Client() - // tlsClientConfig := client.Transport.(*http.Transport).TLSClientConfig - // if tlsClientConfig != nil { - // // If we're using TLS, we need to set the SNI so that the - // // server can verify the request Host matches it. - // tlsClientConfig.ServerName = "example.com" - // } - // clientKey := ctc.clientKeyGen(t) - // clientAuth := ClientPeerIDAuth{PrivKey: clientKey} - - // expectedServerID, err := peer.IDFromPrivateKey(serverKey) - // require.NoError(t, err) - - // ctx := context.Background() - // serverID, err := clientAuth.MutualAuth(ctx, client, ts.URL, "example.com") - // require.NoError(t, err) - // require.Equal(t, expectedServerID, serverID) - // require.NotZero(t, clientAuth.tokenMap["example.com"]) - - // // Once more with the auth token - // req, err := http.NewRequest("GET", ts.URL, nil) - // require.NoError(t, err) - // req.Host = "example.com" - // serverID, err = clientAuth.AddAuthTokenToRequest(req) - // require.NoError(t, err) - // require.Equal(t, expectedServerID, serverID) - - // // Verify that unwrapping our token gives us the client's peer ID - // expectedClientPeerID, err := peer.IDFromPrivateKey(clientKey) - // require.NoError(t, err) - // clientPeerID, err := serverAuth.UnwrapBearerToken(req, req.Host) - // require.NoError(t, err) - // require.Equal(t, expectedClientPeerID, clientPeerID) - - // // Verify that we can make an authenticated request - // resp, err := client.Do(req) - // require.NoError(t, err) - - // require.Equal(t, http.StatusOK, resp.StatusCode) + ts, _ := stc.serverGen(t) + client := ts.Client() + tlsClientConfig := client.Transport.(*http.Transport).TLSClientConfig + if tlsClientConfig != nil { + // If we're using TLS, we need to set the SNI so that the + // server can verify the request Host matches it. + tlsClientConfig.ServerName = "example.com" + } + clientKey := ctc.clientKeyGen(t) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + expectedServerID, err := peer.IDFromPrivateKey(serverKey) + require.NoError(t, err) + + req, err := http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err := clientAuth.AuthenticatedDo(client, req) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.Equal(t, http.StatusOK, resp.StatusCode) + + // Once more with the auth token + req, err = http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + require.NotEmpty(t, req.Header.Get("Authorization")) + require.NoError(t, err) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.Equal(t, http.StatusOK, resp.StatusCode) }) } } @@ -140,12 +137,14 @@ func FuzzServeHTTP(f *testing.F) { serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) require.NoError(f, err) auth := ServerPeerIDAuth{ - PrivKey: serverKey, - ValidHostnames: map[string]struct{}{"example.com": {}}, - TokenTTL: time.Hour, - InsecureNoTLS: true, + PrivKey: serverKey, + ValidHostnameFn: func(s string) bool { + return s == "example.com" + }, + TokenTTL: time.Hour, + InsecureNoTLS: true, } - // Just check that we don't panic' + // Just check that we don't panic f.Fuzz(func(t *testing.T, data []byte) { if len(data) == 0 { return @@ -164,19 +163,11 @@ func FuzzServeHTTP(f *testing.F) { }) } -// Test Vectors -var zeroBytes = make([]byte, 64) -var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) +// // Test Vectors +// var zeroBytes = make([]byte, 64) +// var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) -// Peer ID derived from the zero key -var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) +// // Peer ID derived from the zero key +// var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) -func genClientID(t *testing.T) (peer.ID, crypto.PrivKey) { - clientPrivStr, err := hex.DecodeString("080112407e0830617c4a7de83925dfb2694556b12936c477a0e1feb2e148ec9da60fee7d1ed1e8fae2c4a144b8be8fd4b47bf3d3b34b871c3cacf6010f0e42d474fce27e") - require.NoError(t, err) - clientKey, err := crypto.UnmarshalPrivateKey(clientPrivStr) - require.NoError(t, err) - clientID, err := peer.IDFromPrivateKey(clientKey) - require.NoError(t, err) - return clientID, clientKey -} +// TODO add generator for specs table & example diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 18ec535469..d3d78f7308 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -1,17 +1,25 @@ package httppeeridauth import ( - "context" - "errors" "fmt" "net/http" + "sync" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake" ) type ClientPeerIDAuth struct { PrivKey crypto.PrivKey + + tokenMapMu sync.Mutex + tokenMap map[string]tokenInfo +} + +type tokenInfo struct { + token string + peerID peer.ID } // AddAuthTokenToRequest adds the libp2p-Bearer token to the request. Returns the peer ID of the server. @@ -19,38 +27,70 @@ func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, er panic("todo") } -// MutualAuth performs mutual authentication with the server at the given endpoint. Returns the server's peer id. -func (a *ClientPeerIDAuth) MutualAuth(ctx context.Context, client *http.Client, authEndpoint string, hostname string) (peer.ID, error) { - panic("todo") -} +// AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth handshake if needed. +func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) { + clonedReq := req.Clone(req.Context()) -// authSelfToServer performs the initial authentication request to the server. It authenticates the client to the server. -// Returns the Authorization value with libp2p-PeerID scheme to use for subsequent requests. -func (a *ClientPeerIDAuth) authSelfToServer(ctx context.Context, client *http.Client, myPeerID peer.ID, challengeServer []byte, authEndpoint string, hostname string) (string, error) { - r, err := http.NewRequestWithContext(ctx, "POST", authEndpoint, nil) - r.Host = hostname - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) + hostname := req.Host + a.tokenMapMu.Lock() + if a.tokenMap == nil { + a.tokenMap = make(map[string]tokenInfo) + } + ti, ok := a.tokenMap[hostname] + a.tokenMapMu.Unlock() + if ok { + req.Header.Set("Authorization", ti.token) } - // do the initial auth request - resp, err := client.Do(r) + resp, err := client.Do(req) if err != nil { - return "", fmt.Errorf("failed to do initial auth request: %w", err) + return "", nil, err } if resp.StatusCode != http.StatusUnauthorized { - return "", nil + // our token is still valid or no auth needed + return ti.peerID, resp, nil } resp.Body.Close() - authHeader := resp.Header.Get("WWW-Authenticate") - f, err := parseAuthFields(authHeader, hostname, false) + handshake := handshake.PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: a.PrivKey, + } + err = handshake.ParseHeaderVal([]byte(resp.Header.Get("WWW-Authenticate"))) + if err != nil { + return "", nil, fmt.Errorf("failed to parse auth header: %w", err) + } + err = handshake.Run() + if err != nil { + return "", nil, fmt.Errorf("failed to run handshake: %w", err) + } + handshake.SetHeader(clonedReq.Header) + + resp, err = client.Do(clonedReq) if err != nil { - return "", fmt.Errorf("failed to parse our auth header: %w", err) + return "", nil, fmt.Errorf("failed to do authenticated request: %w", err) } - if len(f.challengeClientB64) == 0 { - return "", errors.New("missing challenge") + err = handshake.ParseHeaderVal([]byte(resp.Header.Get("Authentication-Info"))) + if err != nil { + resp.Body.Close() + return "", nil, fmt.Errorf("failed to parse auth info header: %w", err) } - panic("todo") + err = handshake.Run() + if err != nil { + resp.Body.Close() + return "", nil, fmt.Errorf("failed to run auth info handshake: %w", err) + } + + serverPeerID, err := handshake.PeerID() + if err != nil { + resp.Body.Close() + return "", nil, fmt.Errorf("failed to get server's peer ID: %w", err) + } + a.tokenMapMu.Lock() + a.tokenMap[hostname] = tokenInfo{handshake.BearerToken(), serverPeerID} + a.tokenMapMu.Unlock() + + return serverPeerID, resp, nil + } diff --git a/p2p/http/auth/alloc_test.go b/p2p/http/auth/internal/handshake/alloc_test.go similarity index 95% rename from p2p/http/auth/alloc_test.go rename to p2p/http/auth/internal/handshake/alloc_test.go index e7083c92cf..333bad4f0d 100644 --- a/p2p/http/auth/alloc_test.go +++ b/p2p/http/auth/internal/handshake/alloc_test.go @@ -1,6 +1,6 @@ //go:build nocover -package httppeeridauth +package handshake import "testing" diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 39189cf43c..8b41ac7dd6 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -149,3 +149,12 @@ func (h *PeerIDAuthHandshakeClient) SetHeader(hdr http.Header) { } hdr.Set("Authorization", h.hb.b.String()) } + +// BearerToken returns the server given bearer token for the client. Set this on +// the Authorization header in the client's request. +func (h *PeerIDAuthHandshakeClient) BearerToken() string { + if h.state != peerIDAuthClientStateDone { + return "" + } + return h.hb.b.String() +} diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 9299d124fc..6a1fbebc8b 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -1,24 +1,31 @@ package httppeeridauth import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" "errors" + "hash" "net/http" + "sync" "time" "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake" ) -const maxAuthHeaderSize = 8192 - -const challengeTTL = 5 * time.Minute - type ServerPeerIDAuth struct { - PrivKey crypto.PrivKey - ValidHostnames map[string]struct{} - TokenTTL time.Duration - Next http.Handler + PrivKey crypto.PrivKey + TokenTTL time.Duration + Next func(peer peer.ID, w http.ResponseWriter, r *http.Request) // InsecureNoTLS is a flag that allows the server to accept requests without a TLS ServerName. Used only for testing. InsecureNoTLS bool + // Only used when InsecureNoTLS is true. If set, the server will only accept requests for the hostnames which return true + ValidHostnameFn func(string) bool + + Hmac hash.Hash + initHmac sync.Once } var errMissingAuthHeader = errors.New("missing header") @@ -28,8 +35,30 @@ var errMissingAuthHeader = errors.New("missing header") // scheme. If a Next handler is set, it will be called on authenticated // requests. func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.initHmac.Do(func() { + if a.Hmac == nil { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + a.Hmac = hmac.New(sha256.New, key) + } + }) + hostname := r.Host - if !a.InsecureNoTLS { + if a.InsecureNoTLS { + if a.ValidHostnameFn == nil { + log.Debugf("No ValidHostnameFn set for InsecureNoTLS") + w.WriteHeader(http.StatusInternalServerError) + return + } + if !a.ValidHostnameFn(hostname) { + log.Debugf("Unauthorized request for host %s: hostname not in valid set", hostname) + w.WriteHeader(http.StatusBadRequest) + return + } + } else { if r.TLS == nil { log.Debugf("No TLS connection, and InsecureNoTLS is false") w.WriteHeader(http.StatusBadRequest) @@ -41,4 +70,36 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } + + handshake := handshake.PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: a.PrivKey, + TokenTTL: a.TokenTTL, + Hmac: a.Hmac, + } + err := handshake.ParseHeaderVal([]byte(r.Header.Get("Authorization"))) + if err != nil { + log.Debugf("Failed to parse header: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + err = handshake.Run() + if err != nil { + log.Debugf("Failed to run handshake: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + handshake.SetHeader(w.Header()) + + peer, err := handshake.PeerID() + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if a.Next == nil { + w.WriteHeader(http.StatusOK) + return + } + a.Next(peer, w, r) } From 185382a474a8a56cf084ef04ca3dd31407529ab7 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 17:44:24 -0700 Subject: [PATCH 16/31] Nits --- p2p/http/auth/auth_test.go | 31 ------------------------------- p2p/http/auth/client.go | 6 ------ p2p/http/auth/server.go | 3 --- 3 files changed, 40 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 1a74d8b8bf..fab82e9580 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -132,37 +132,6 @@ func TestMutualAuth(t *testing.T) { } } -func FuzzServeHTTP(f *testing.F) { - zeroBytes := make([]byte, 64) - serverKey, _, err := crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) - require.NoError(f, err) - auth := ServerPeerIDAuth{ - PrivKey: serverKey, - ValidHostnameFn: func(s string) bool { - return s == "example.com" - }, - TokenTTL: time.Hour, - InsecureNoTLS: true, - } - // Just check that we don't panic - f.Fuzz(func(t *testing.T, data []byte) { - if len(data) == 0 { - return - } - hostLen := int(data[0]) - data = data[1:] - if hostLen > len(data) { - return - } - host := string(data[:hostLen]) - data = data[hostLen:] - req := httptest.NewRequest("GET", "http://example.com", nil) - req.Host = host - req.Header.Set("Authorization", string(data)) - auth.ServeHTTP(httptest.NewRecorder(), req) - }) -} - // // Test Vectors // var zeroBytes = make([]byte, 64) // var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index d3d78f7308..874a4519b7 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -22,11 +22,6 @@ type tokenInfo struct { peerID peer.ID } -// AddAuthTokenToRequest adds the libp2p-Bearer token to the request. Returns the peer ID of the server. -func (a *ClientPeerIDAuth) AddAuthTokenToRequest(req *http.Request) (peer.ID, error) { - panic("todo") -} - // AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth handshake if needed. func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) { clonedReq := req.Clone(req.Context()) @@ -92,5 +87,4 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Reques a.tokenMapMu.Unlock() return serverPeerID, resp, nil - } diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 6a1fbebc8b..3768089f16 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -4,7 +4,6 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha256" - "errors" "hash" "net/http" "sync" @@ -28,8 +27,6 @@ type ServerPeerIDAuth struct { initHmac sync.Once } -var errMissingAuthHeader = errors.New("missing header") - // ServeHTTP implements the http.Handler interface for PeerIDAuth. It will // attempt to authenticate the request using using the libp2p peer ID auth // scheme. If a Next handler is set, it will be called on authenticated From e6019714de4629a978044df8b835c9c26b5b15c3 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 17:47:18 -0700 Subject: [PATCH 17/31] Error if challenge is too short --- p2p/http/auth/internal/handshake/client.go | 3 +++ p2p/http/auth/internal/handshake/server.go | 3 +++ 2 files changed, 6 insertions(+) diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 8b41ac7dd6..21c9ff9242 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -69,6 +69,9 @@ func (h *PeerIDAuthHandshakeClient) Run() error { } switch h.state { case peerIDAuthClientStateSignChallenge: + if len(h.p.challengeClient) < challengeLen { + return errors.New("challenge too short") + } clientSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ {"challenge-client", h.p.challengeClient}, {"hostname", []byte(h.Hostname)}, diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go index 3777818c47..90537dd395 100644 --- a/p2p/http/auth/internal/handshake/server.go +++ b/p2p/http/auth/internal/handshake/server.go @@ -200,6 +200,9 @@ func (h *PeerIDAuthHandshakeServer) Run() error { } } + if len(h.p.challengeServer) < challengeLen { + return errors.New("challenge too short") + } // We authenticated the client, now authenticate ourselves serverSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ {"challenge-server", h.p.challengeServer}, From 6ed98ebbc6b3cc0817310964c0cec01146c160fc Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 17:48:42 -0700 Subject: [PATCH 18/31] nit --- p2p/http/auth/internal/handshake/handshake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/auth/internal/handshake/handshake.go b/p2p/http/auth/internal/handshake/handshake.go index 41e87f087d..0cd2ff4c86 100644 --- a/p2p/http/auth/internal/handshake/handshake.go +++ b/p2p/http/auth/internal/handshake/handshake.go @@ -187,7 +187,7 @@ func genDataToSign(buf []byte, prefix string, parts []sigParam) ([]byte, error) slices.SortFunc(parts, func(a, b sigParam) int { return strings.Compare(a.k, b.k) }) - buf = append(buf, []byte(prefix)...) + buf = append(buf, prefix...) for _, p := range parts { buf = binary.AppendUvarint(buf, uint64(len(p.k)+1+len(p.v))) // +1 for '=' buf = append(buf, p.k...) From 273e8920dd3553e0e24ccfc63589b493db821774 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 17:54:01 -0700 Subject: [PATCH 19/31] Mod tidy --- test-plans/go.mod | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-plans/go.mod b/test-plans/go.mod index 466a55e911..f8c58c4345 100644 --- a/test-plans/go.mod +++ b/test-plans/go.mod @@ -2,6 +2,8 @@ module github.com/libp2p/go-libp2p/test-plans/m/v2 go 1.22 +toolchain go1.22.1 + require ( github.com/go-redis/redis/v8 v8.11.5 github.com/libp2p/go-libp2p v0.0.0 From 212e57224e64d180e6a63460384103aeee880751 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Tue, 27 Aug 2024 17:59:44 -0700 Subject: [PATCH 20/31] nit --- p2p/http/auth/internal/handshake/handshake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/http/auth/internal/handshake/handshake.go b/p2p/http/auth/internal/handshake/handshake.go index 0cd2ff4c86..896d82f3ad 100644 --- a/p2p/http/auth/internal/handshake/handshake.go +++ b/p2p/http/auth/internal/handshake/handshake.go @@ -41,7 +41,7 @@ func (p *params) parsePeerIDAuthSchemeParams(headerVal []byte) error { if len(headerVal) > maxHeaderSize { return errTooBig } - startIdx := bytes.Index(headerVal, []byte(PeerIDAuthScheme)) + startIdx := bytes.Index(headerVal, peerIDAuthSchemeBytes) if startIdx == -1 { return nil } From 4993c9769d0cedefe98268f653032a1afcd1df30 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 28 Aug 2024 11:57:12 -0700 Subject: [PATCH 21/31] Use a newRequest function rather than shallow clone Because otherwise the body is not copied. --- p2p/http/auth/auth_test.go | 30 +++++++++++++++--------------- p2p/http/auth/client.go | 13 ++++++++----- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index fab82e9580..83e133f81a 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -108,21 +108,30 @@ func TestMutualAuth(t *testing.T) { expectedServerID, err := peer.IDFromPrivateKey(serverKey) require.NoError(t, err) - req, err := http.NewRequest("POST", ts.URL, nil) - require.NoError(t, err) - req.Host = "example.com" - serverID, resp, err := clientAuth.AuthenticatedDo(client, req) + newReq := func() *http.Request { + req, err := http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + return req + } + serverID, resp, err := clientAuth.AuthenticatedDo(client, newReq) require.NoError(t, err) require.Equal(t, expectedServerID, serverID) require.NotZero(t, clientAuth.tokenMap["example.com"]) require.Equal(t, http.StatusOK, resp.StatusCode) // Once more with the auth token - req, err = http.NewRequest("POST", ts.URL, nil) + req, err := http.NewRequest("POST", ts.URL, nil) require.NoError(t, err) req.Host = "example.com" - serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + timesCalled := 0 + newReq = func() *http.Request { + timesCalled++ + return req + } + serverID, resp, err = clientAuth.AuthenticatedDo(client, newReq) require.NotEmpty(t, req.Header.Get("Authorization")) + require.Equal(t, 1, timesCalled, "should only call newRequest once since we have a token") require.NoError(t, err) require.Equal(t, expectedServerID, serverID) require.NotZero(t, clientAuth.tokenMap["example.com"]) @@ -131,12 +140,3 @@ func TestMutualAuth(t *testing.T) { } } } - -// // Test Vectors -// var zeroBytes = make([]byte, 64) -// var zeroKey, _, _ = crypto.GenerateEd25519Key(bytes.NewReader(zeroBytes)) - -// // Peer ID derived from the zero key -// var zeroID, _ = peer.IDFromPublicKey(zeroKey.GetPublic()) - -// TODO add generator for specs table & example diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 874a4519b7..cb7c4856de 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -23,9 +23,10 @@ type tokenInfo struct { } // AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth handshake if needed. -func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) { - clonedReq := req.Clone(req.Context()) - +// Takes in a function that creates a new request, so that we can retry the +// request if we need to authenticate. +func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, newRequest func() *http.Request) (peer.ID, *http.Response, error) { + req := newRequest() hostname := req.Host a.tokenMapMu.Lock() if a.tokenMap == nil { @@ -59,9 +60,11 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Reques if err != nil { return "", nil, fmt.Errorf("failed to run handshake: %w", err) } - handshake.SetHeader(clonedReq.Header) - resp, err = client.Do(clonedReq) + req = newRequest() + handshake.SetHeader(req.Header) + + resp, err = client.Do(req) if err != nil { return "", nil, fmt.Errorf("failed to do authenticated request: %w", err) } From fbcede2208979ebe05837a238d8cc9dcb527936f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 28 Aug 2024 13:08:21 -0700 Subject: [PATCH 22/31] Add tests to generate examples for specs --- p2p/http/auth/internal/handshake/client.go | 6 +- p2p/http/auth/internal/handshake/handshake.go | 5 + .../auth/internal/handshake/handshake_test.go | 166 +++++++++++++++++- p2p/http/auth/internal/handshake/server.go | 22 +-- 4 files changed, 184 insertions(+), 15 deletions(-) diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 21c9ff9242..488371bf75 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -1,10 +1,10 @@ package handshake import ( - "crypto/rand" "encoding/base64" "errors" "fmt" + "io" "net/http" "github.com/libp2p/go-libp2p/core/crypto" @@ -79,7 +79,7 @@ func (h *PeerIDAuthHandshakeClient) Run() error { if err != nil { return fmt.Errorf("failed to sign challenge: %w", err) } - _, err = rand.Read(h.challengeServer[:]) + _, err = io.ReadFull(randReader, h.challengeServer[:]) if err != nil { return err } @@ -88,9 +88,9 @@ func (h *PeerIDAuthHandshakeClient) Run() error { h.hb.clear() h.hb.writeScheme(PeerIDAuthScheme) h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) - h.hb.writeParam("opaque", h.p.opaqueB64) h.hb.writeParam("challenge-server", h.challengeServer[:]) h.hb.writeParamB64(nil, "sig", clientSig) + h.hb.writeParam("opaque", h.p.opaqueB64) return nil case peerIDAuthClientStateVerifyChallenge: serverPubKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) diff --git a/p2p/http/auth/internal/handshake/handshake.go b/p2p/http/auth/internal/handshake/handshake.go index 896d82f3ad..75051f6e27 100644 --- a/p2p/http/auth/internal/handshake/handshake.go +++ b/p2p/http/auth/internal/handshake/handshake.go @@ -3,12 +3,14 @@ package handshake import ( "bufio" "bytes" + "crypto/rand" "encoding/base64" "encoding/binary" "errors" "fmt" "slices" "strings" + "time" "github.com/libp2p/go-libp2p/core/crypto" @@ -25,6 +27,9 @@ var errTooBig = errors.New("header value too big") var errInvalid = errors.New("invalid header value") var errNotRan = errors.New("not ran. call Run() first") +var randReader = rand.Reader // A var so it can be changed in tests +var nowFn = time.Now // A var so it can be changed in tests + // params represent params passed in via headers. All []byte fields to avoid allocations. type params struct { bearerTokenB64 []byte diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go index b4a4a0e51b..d0d292454b 100644 --- a/p2p/http/auth/internal/handshake/handshake_test.go +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -5,8 +5,12 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha256" + "encoding/base64" + "encoding/hex" "encoding/json" + "fmt" "net/http" + "net/url" "testing" "time" @@ -219,7 +223,7 @@ func TestOpaqueStateRoundTrip(t *testing.T) { ChallengeClient: "foo-bar", CreatedTime: timeAfterUnmarshal, IsToken: true, - PeerID: &zeroID, + PeerID: zeroID, Hostname: "example.com", } @@ -305,3 +309,163 @@ func FuzzParsePeerIDAuthSchemeParamsNoPanic(f *testing.F) { p.parsePeerIDAuthSchemeParams(data) }) } + +type specsExampleParameters struct { + hostname string + serverPriv crypto.PrivKey + serverHmacKey [32]byte + clientPriv crypto.PrivKey +} + +func TestSpecsExample(t *testing.T) { + originalRandReader := randReader + originalNowFn := nowFn + randReader = bytes.NewReader(append( + bytes.Repeat([]byte{0x11}, 32), + bytes.Repeat([]byte{0x33}, 32)..., + )) + nowFn = func() time.Time { + return time.Unix(0, 0) + } + defer func() { + randReader = originalRandReader + nowFn = originalNowFn + }() + + parameters := specsExampleParameters{ + hostname: "example.com", + } + serverPrivBytes, err := hex.AppendDecode(nil, []byte("0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c")) + require.NoError(t, err) + clientPrivBytes, err := hex.AppendDecode(nil, []byte("0801124002020202020202020202020202020202020202020202020202020202020202028139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394")) + require.NoError(t, err) + + parameters.serverPriv, err = crypto.UnmarshalPrivateKey(serverPrivBytes) + require.NoError(t, err) + + parameters.clientPriv, err = crypto.UnmarshalPrivateKey(clientPrivBytes) + require.NoError(t, err) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: parameters.hostname, + PrivKey: parameters.serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, parameters.serverHmacKey[:]), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: parameters.hostname, + PrivKey: parameters.clientPriv, + } + + headers := make(http.Header) + + // Start the handshake + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + initialWWWAuthenticate := headers.Get("WWW-Authenticate") + + // Client receives the challenge and signs it. Also sends the challenge server + require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.SetHeader(headers) + clientAuthentication := headers.Get("Authorization") + + // Server receives the sig and verifies it. Also signs the challenge server + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + serverAuthentication := headers.Get("Authentication-Info") + + // Client verifies sig and sets the bearer token for future requests + require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("Authentication-Info")))) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.SetHeader(headers) + clientBearerToken := headers.Get("Authorization") + + params := params{} + params.parsePeerIDAuthSchemeParams([]byte(initialWWWAuthenticate)) + challengeClient := params.challengeClient + params.parsePeerIDAuthSchemeParams([]byte(clientAuthentication)) + challengeServer := params.challengeServer + + fmt.Println("### Parameters") + fmt.Println("| Parameter | Value |") + fmt.Println("| --- | --- |") + fmt.Printf("| hostname | %s |\n", parameters.hostname) + fmt.Printf("| Server Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(serverPrivBytes)) + fmt.Printf("| Server HMAC Key (hex) | %s |\n", hex.EncodeToString(parameters.serverHmacKey[:])) + fmt.Printf("| Challenge Client | %s |\n", string(challengeClient)) + fmt.Printf("| Client Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(clientPrivBytes)) + fmt.Printf("| Challenge Server | %s |\n", string(challengeServer)) + fmt.Printf("| \"Now\" time | %s |\n", nowFn()) + fmt.Println() + fmt.Println("### Handshake Diagram") + + fmt.Println("```mermaid") + fmt.Printf(`sequenceDiagram +Client->>Server: Initial request +Server->>Client: WWW-Authenticate=%s +Client->>Server: Authorization=%s +Note left of Server: Server has authenticated Client +Server->>Client: Authentication-Info=%s +Note right of Client: Client has authenticated Server + +Note over Client: Future requests use the bearer token +Client->>Server: Authorization=%s +`, initialWWWAuthenticate, clientAuthentication, serverAuthentication, clientBearerToken) + fmt.Println("```") + +} + +func TestSigningExample(t *testing.T) { + serverPrivBytes, err := hex.AppendDecode(nil, []byte("0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c")) + require.NoError(t, err) + serverPriv, err := crypto.UnmarshalPrivateKey(serverPrivBytes) + require.NoError(t, err) + clientPrivBytes, err := hex.AppendDecode(nil, []byte("0801124002020202020202020202020202020202020202020202020202020202020202028139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394")) + require.NoError(t, err) + clientPriv, err := crypto.UnmarshalPrivateKey(clientPrivBytes) + require.NoError(t, err) + clientPubKeyBytes, err := crypto.MarshalPublicKey(clientPriv.GetPublic()) + require.NoError(t, err) + + require.NoError(t, err) + challenge := "ERERERERERERERERERERERERERERERERERERERERERE=" + + hostname := "example.com" + dataToSign, err := genDataToSign(nil, PeerIDAuthScheme, []sigParam{ + {"challenge-server", []byte(challenge)}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(hostname)}, + }) + require.NoError(t, err) + + sig, err := sign(serverPriv, PeerIDAuthScheme, []sigParam{ + {"challenge-server", []byte(challenge)}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(hostname)}, + }) + require.NoError(t, err) + + fmt.Println("### Signing Example") + + fmt.Println("| Parameter | Value |") + fmt.Println("| --- | --- |") + fmt.Printf("| hostname | %s |\n", hostname) + fmt.Printf("| Server Private Key (pb encoded as hex) | %s |\n", hex.EncodeToString(serverPrivBytes)) + fmt.Printf("| challenge-server | %s |\n", string(challenge)) + fmt.Printf("| Client Public Key (pb encoded as hex) | %s |\n", hex.EncodeToString(clientPubKeyBytes)) + fmt.Printf("| data to sign ([percent encoded](https://datatracker.ietf.org/doc/html/rfc3986#section-2.1)) | %s |\n", url.PathEscape(string(dataToSign))) + fmt.Printf("| data to sign (hex encoded) | %s |\n", hex.EncodeToString(dataToSign)) + fmt.Printf("| signature (base64 encoded) | %s |\n", base64.URLEncoding.EncodeToString(sig)) + fmt.Println() + + fmt.Println("Note that the `=` after the libp2p-PeerID scheme is actually the varint length of the challenge-server parameter.") + +} diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go index 90537dd395..041943c40a 100644 --- a/p2p/http/auth/internal/handshake/server.go +++ b/p2p/http/auth/internal/handshake/server.go @@ -2,12 +2,12 @@ package handshake import ( "crypto/hmac" - "crypto/rand" "encoding/base64" "encoding/json" "errors" "fmt" "hash" + "io" "net/http" "time" @@ -26,9 +26,9 @@ const ( ) type opaqueState struct { - IsToken bool `json:"is-token"` - PeerID *peer.ID `json:"peer-id"` - ChallengeClient string `json:"challenge-client"` + IsToken bool `json:"is-token,omitempty"` + PeerID peer.ID `json:"peer-id,omitempty"` + ChallengeClient string `json:"challenge-client,omitempty"` Hostname string `json:"hostname"` CreatedTime time.Time `json:"created-time"` } @@ -132,7 +132,7 @@ func (h *PeerIDAuthHandshakeServer) Run() error { case peerIDAuthServerStateChallengeClient: h.hb.writeScheme(PeerIDAuthScheme) { - _, err := rand.Read(h.buf[:challengeLen]) + _, err := io.ReadFull(randReader, h.buf[:challengeLen]) if err != nil { return err } @@ -140,7 +140,7 @@ func (h *PeerIDAuthHandshakeServer) Run() error { h.opaque = opaqueState{ ChallengeClient: string(encodedChallenge), Hostname: h.Hostname, - CreatedTime: time.Now(), + CreatedTime: nowFn(), } h.hb.writeParam("challenge-client", encodedChallenge) } @@ -162,7 +162,7 @@ func (h *PeerIDAuthHandshakeServer) Run() error { return err } } - if time.Now().After(h.opaque.CreatedTime.Add(challengeTTL)) { + if nowFn().After(h.opaque.CreatedTime.Add(challengeTTL)) { return errExpiredChallenge } if h.opaque.IsToken { @@ -221,9 +221,9 @@ func (h *PeerIDAuthHandshakeServer) Run() error { // And create a bearer token for the client h.opaque = opaqueState{ IsToken: true, - PeerID: &peerID, + PeerID: peerID, Hostname: h.Hostname, - CreatedTime: time.Now(), + CreatedTime: nowFn(), } serverPubKey := h.PrivKey.GetPublic() pubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) @@ -256,7 +256,7 @@ func (h *PeerIDAuthHandshakeServer) Run() error { return errors.New("expected token, got challenge") } - if time.Now().After(h.opaque.CreatedTime.Add(h.TokenTTL)) { + if nowFn().After(h.opaque.CreatedTime.Add(h.TokenTTL)) { return errExpiredToken } @@ -277,7 +277,7 @@ func (h *PeerIDAuthHandshakeServer) PeerID() (peer.ID, error) { default: return "", errors.New("not in proper state") } - return *h.opaque.PeerID, nil + return h.opaque.PeerID, nil } func (h *PeerIDAuthHandshakeServer) SetHeader(hdr http.Header) { From 7a5faf8bab26cf72fe362a4db7ebebaf410b9490 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 4 Sep 2024 14:38:18 -0700 Subject: [PATCH 23/31] Rename InsecureNoTLS. Update comment --- p2p/http/auth/auth_test.go | 4 ++-- p2p/http/auth/server.go | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 83e133f81a..9718a63f33 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -63,8 +63,8 @@ func TestMutualAuth(t *testing.T) { ValidHostnameFn: func(s string) bool { return s == "example.com" }, - TokenTTL: time.Hour, - InsecureNoTLS: true, + TokenTTL: time.Hour, + NoTLS: true, } ts := httptest.NewServer(&auth) diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 3768089f16..28f4f4f9fe 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -18,10 +18,12 @@ type ServerPeerIDAuth struct { PrivKey crypto.PrivKey TokenTTL time.Duration Next func(peer peer.ID, w http.ResponseWriter, r *http.Request) - // InsecureNoTLS is a flag that allows the server to accept requests without a TLS ServerName. Used only for testing. - InsecureNoTLS bool - // Only used when InsecureNoTLS is true. If set, the server will only accept requests for the hostnames which return true - ValidHostnameFn func(string) bool + // NoTLS is a flag that allows the server to accept requests without a TLS + // ServerName. Used when something else is terminating the TLS connection. + NoTLS bool + // Required when NoTLS is true. The server will only accept requests for + // which the Host header returns true. + ValidHostnameFn func(hostname string) bool Hmac hash.Hash initHmac sync.Once @@ -44,9 +46,9 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { }) hostname := r.Host - if a.InsecureNoTLS { + if a.NoTLS { if a.ValidHostnameFn == nil { - log.Debugf("No ValidHostnameFn set for InsecureNoTLS") + log.Error("No ValidHostnameFn set. Required for NoTLS") w.WriteHeader(http.StatusInternalServerError) return } @@ -57,7 +59,7 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } else { if r.TLS == nil { - log.Debugf("No TLS connection, and InsecureNoTLS is false") + log.Warn("No TLS connection, and NoTLS is false") w.WriteHeader(http.StatusBadRequest) return } From 96f02caea49784455f32a062fcdf4325ff516579 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Thu, 5 Sep 2024 17:57:56 -0700 Subject: [PATCH 24/31] Add support for client-initiated handshake --- p2p/http/auth/internal/handshake/client.go | 162 +++++++++----- .../auth/internal/handshake/handshake_test.go | 127 ++++++----- p2p/http/auth/internal/handshake/server.go | 208 ++++++++++++------ 3 files changed, 314 insertions(+), 183 deletions(-) diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 488371bf75..6b6bbc55ee 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -17,6 +17,11 @@ const ( peerIDAuthClientStateSignChallenge peerIDAuthClientState = iota peerIDAuthClientStateVerifyChallenge peerIDAuthClientStateDone // We have the bearer token, and there's nothing left to do + + // Client initiated handshake + peerIDAuthClientInitiateChallenge + peerIDAuthClientStateVerifyAndSignChallenge + peerIDAuthClientStateWaitingForBearer ) type PeerIDAuthHandshakeClient struct { @@ -24,7 +29,7 @@ type PeerIDAuthHandshakeClient struct { PrivKey crypto.PrivKey serverPeerID peer.ID - ran bool + serverPubKey crypto.PubKey state peerIDAuthClientState p params hb headerBuilder @@ -34,7 +39,7 @@ type PeerIDAuthHandshakeClient struct { var errMissingChallenge = errors.New("missing challenge") func (h *PeerIDAuthHandshakeClient) ParseHeaderVal(headerVal []byte) error { - if h.state == peerIDAuthClientStateDone { + if h.state == peerIDAuthClientStateDone || h.state == peerIDAuthClientInitiateChallenge { return nil } h.p = params{} @@ -48,108 +53,147 @@ func (h *PeerIDAuthHandshakeClient) ParseHeaderVal(headerVal []byte) error { return err } - if h.p.challengeClient != nil { - h.state = peerIDAuthClientStateSignChallenge - return nil - } - - if h.p.sigB64 != nil { - h.state = peerIDAuthClientStateVerifyChallenge - return nil + if h.serverPubKey == nil && len(h.p.publicKeyB64) > 0 { + serverPubKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + h.serverPubKey, err = crypto.UnmarshalPublicKey(serverPubKeyBytes) + if err != nil { + return err + } + h.serverPeerID, err = peer.IDFromPublicKey(h.serverPubKey) + if err != nil { + return err + } } - return errors.New("missing challenge or signature") + return err } func (h *PeerIDAuthHandshakeClient) Run() error { - h.ran = true + if h.state == peerIDAuthClientStateDone { + return nil + } + + h.hb.clear() clientPubKeyBytes, err := crypto.MarshalPublicKey(h.PrivKey.GetPublic()) if err != nil { return err } switch h.state { - case peerIDAuthClientStateSignChallenge: - if len(h.p.challengeClient) < challengeLen { - return errors.New("challenge too short") - } - clientSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ - {"challenge-client", h.p.challengeClient}, - {"hostname", []byte(h.Hostname)}, - }) - if err != nil { - return fmt.Errorf("failed to sign challenge: %w", err) - } - _, err = io.ReadFull(randReader, h.challengeServer[:]) - if err != nil { + case peerIDAuthClientInitiateChallenge: + h.hb.writeScheme(PeerIDAuthScheme) + h.addChallengeServerParam() + h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) + h.state = peerIDAuthClientStateVerifyAndSignChallenge + return nil + case peerIDAuthClientStateVerifyAndSignChallenge: + if err := h.verifySig(clientPubKeyBytes); err != nil { return err } - copy(h.challengeServer[:], base64.URLEncoding.AppendEncode(nil, h.challengeServer[:])) - h.hb.clear() h.hb.writeScheme(PeerIDAuthScheme) - h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) - h.hb.writeParam("challenge-server", h.challengeServer[:]) - h.hb.writeParamB64(nil, "sig", clientSig) h.hb.writeParam("opaque", h.p.opaqueB64) + h.addSigParam() + h.state = peerIDAuthClientStateWaitingForBearer return nil - case peerIDAuthClientStateVerifyChallenge: - serverPubKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) - if err != nil { - return err - } - sig, err := base64.URLEncoding.AppendDecode(nil, h.p.sigB64) - if err != nil { - return fmt.Errorf("failed to decode signature: %w", err) + + case peerIDAuthClientStateWaitingForBearer: + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParam("bearer", h.p.bearerTokenB64) + h.state = peerIDAuthClientStateDone + return nil + + case peerIDAuthClientStateSignChallenge: + if len(h.p.challengeClient) < challengeLen { + return errors.New("challenge too short") } - serverPubKey, err := crypto.UnmarshalPublicKey(serverPubKeyBytes) - if err != nil { + + h.hb.writeScheme(PeerIDAuthScheme) + h.hb.writeParamB64(nil, "public-key", clientPubKeyBytes) + if err := h.addChallengeServerParam(); err != nil { return err } - err = verifySig(serverPubKey, PeerIDAuthScheme, []sigParam{ - {"challenge-server", h.challengeServer[:]}, - {"client-public-key", clientPubKeyBytes}, - {"hostname", []byte(h.Hostname)}, - }, sig) - if err != nil { + if err := h.addSigParam(); err != nil { return err } - h.serverPeerID, err = peer.IDFromPublicKey(serverPubKey) - if err != nil { + h.hb.writeParam("opaque", h.p.opaqueB64) + + h.state = peerIDAuthClientStateVerifyChallenge + return nil + case peerIDAuthClientStateVerifyChallenge: + if err := h.verifySig(clientPubKeyBytes); err != nil { return err } - h.hb.clear() h.hb.writeScheme(PeerIDAuthScheme) h.hb.writeParam("bearer", h.p.bearerTokenB64) h.state = peerIDAuthClientStateDone - return nil - case peerIDAuthClientStateDone: return nil } return errors.New("unhandled state") } +func (h *PeerIDAuthHandshakeClient) addChallengeServerParam() error { + _, err := io.ReadFull(randReader, h.challengeServer[:]) + if err != nil { + return err + } + copy(h.challengeServer[:], base64.URLEncoding.AppendEncode(nil, h.challengeServer[:])) + h.hb.writeParam("challenge-server", h.challengeServer[:]) + return nil +} + +func (h *PeerIDAuthHandshakeClient) verifySig(clientPubKeyBytes []byte) error { + sig, err := base64.URLEncoding.AppendDecode(nil, h.p.sigB64) + if err != nil { + return fmt.Errorf("failed to decode signature: %w", err) + } + err = verifySig(h.serverPubKey, PeerIDAuthScheme, []sigParam{ + {"challenge-server", h.challengeServer[:]}, + {"client-public-key", clientPubKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }, sig) + return err +} + +func (h *PeerIDAuthHandshakeClient) addSigParam() error { + if h.serverPubKey == nil { + return errors.New("server public key not set") + } + serverPubKeyBytes, err := crypto.MarshalPublicKey(h.serverPubKey) + if err != nil { + return err + } + clientSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ + {"challenge-client", h.p.challengeClient}, + {"server-public-key", serverPubKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }) + if err != nil { + return fmt.Errorf("failed to sign challenge: %w", err) + } + h.hb.writeParamB64(nil, "sig", clientSig) + return nil + +} + // PeerID returns the peer ID of the authenticated client. func (h *PeerIDAuthHandshakeClient) PeerID() (peer.ID, error) { - if !h.ran { - return "", errNotRan - } switch h.state { - case peerIDAuthClientStateVerifyChallenge: case peerIDAuthClientStateDone: + case peerIDAuthClientStateWaitingForBearer: default: - return "", errors.New("not in proper state") + return "", errors.New("server not authenticated yet") } return h.serverPeerID, nil } func (h *PeerIDAuthHandshakeClient) SetHeader(hdr http.Header) { - if !h.ran { - return - } hdr.Set("Authorization", h.hb.b.String()) } diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go index d0d292454b..edfb01032d 100644 --- a/p2p/http/auth/internal/handshake/handshake_test.go +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -20,64 +20,77 @@ import ( ) func TestHandshake(t *testing.T) { - hostname := "example.com" - serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) - clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) - - serverHandshake := PeerIDAuthHandshakeServer{ - Hostname: hostname, - PrivKey: serverPriv, - TokenTTL: time.Hour, - Hmac: hmac.New(sha256.New, make([]byte, 32)), - } - - clientHandshake := PeerIDAuthHandshakeClient{ - Hostname: hostname, - PrivKey: clientPriv, + for _, clientInitiated := range []bool{true, false} { + t.Run(fmt.Sprintf("clientInitiated=%t", clientInitiated), func(t *testing.T) { + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + if clientInitiated { + clientHandshake.state = peerIDAuthClientInitiateChallenge + } + + headers := make(http.Header) + + // Start the handshake + if !clientInitiated { + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + } + + // Client receives the challenge and signs it. Also sends the challenge server + require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.SetHeader(headers) + + // Server receives the sig and verifies it. Also signs the challenge server + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client verifies sig and sets the bearer token for future requests + headerVal := []byte(headers.Get("Authentication-Info")) + if clientInitiated { + headerVal = []byte(headers.Get("WWW-Authenticate")) + } + require.NoError(t, clientHandshake.ParseHeaderVal(headerVal)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.SetHeader(headers) + + // Server verifies the bearer token + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + expectedClientPeerID, _ := peer.IDFromPrivateKey(clientPriv) + expectedServerPeerID, _ := peer.IDFromPrivateKey(serverPriv) + clientPeerID, err := serverHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + serverPeerID, err := clientHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedServerPeerID, serverPeerID) + }) } - - headers := make(http.Header) - - // Start the handshake - require.NoError(t, serverHandshake.ParseHeaderVal(nil)) - require.NoError(t, serverHandshake.Run()) - serverHandshake.SetHeader(headers) - - // Client receives the challenge and signs it. Also sends the challenge server - require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) - clear(headers) - require.NoError(t, clientHandshake.Run()) - clientHandshake.SetHeader(headers) - - // Server receives the sig and verifies it. Also signs the challenge server - serverHandshake.Reset() - require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) - clear(headers) - require.NoError(t, serverHandshake.Run()) - serverHandshake.SetHeader(headers) - - // Client verifies sig and sets the bearer token for future requests - require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("Authentication-Info")))) - clear(headers) - require.NoError(t, clientHandshake.Run()) - clientHandshake.SetHeader(headers) - - // Server verifies the bearer token - serverHandshake.Reset() - require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) - clear(headers) - require.NoError(t, serverHandshake.Run()) - serverHandshake.SetHeader(headers) - - expectedClientPeerID, _ := peer.IDFromPrivateKey(clientPriv) - expectedServerPeerID, _ := peer.IDFromPrivateKey(serverPriv) - clientPeerID, err := serverHandshake.PeerID() - require.NoError(t, err) - require.Equal(t, expectedClientPeerID, clientPeerID) - - serverPeerID, err := clientHandshake.PeerID() - require.NoError(t, err) - require.Equal(t, expectedServerPeerID, serverPeerID) } func BenchmarkServerHandshake(b *testing.B) { diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go index 041943c40a..5f50f459ec 100644 --- a/p2p/http/auth/internal/handshake/server.go +++ b/p2p/http/auth/internal/handshake/server.go @@ -20,13 +20,18 @@ const challengeTTL = 5 * time.Minute type peerIDAuthServerState int const ( + // Server initiated peerIDAuthServerStateChallengeClient peerIDAuthServerState = iota peerIDAuthServerStateVerifyChallenge peerIDAuthServerStateVerifyBearer + + // Client initiated + peerIDAuthServerStateSignChallenge ) type opaqueState struct { IsToken bool `json:"is-token,omitempty"` + ClientPublicKey []byte `json:"client-public-key,omitempty"` PeerID peer.ID `json:"peer-id,omitempty"` ChallengeClient string `json:"challenge-client,omitempty"` Hostname string `json:"hostname"` @@ -112,15 +117,19 @@ func (h *PeerIDAuthHandshakeServer) ParseHeaderVal(headerVal []byte) error { return err } if h.p.sigB64 != nil && h.p.opaqueB64 != nil { - h.state = peerIDAuthServerStateVerifyChallenge - return nil } - if h.p.bearerTokenB64 != nil { + switch { + case h.p.sigB64 != nil && h.p.opaqueB64 != nil: + h.state = peerIDAuthServerStateVerifyChallenge + case h.p.bearerTokenB64 != nil: h.state = peerIDAuthServerStateVerifyBearer - return nil - } + case h.p.challengeServer != nil && h.p.publicKeyB64 != nil: + h.state = peerIDAuthServerStateSignChallenge + default: + return errInvalidHeader - return errInvalidHeader + } + return nil } var errExpiredChallenge = errors.New("challenge expired") @@ -129,27 +138,36 @@ var errExpiredToken = errors.New("token expired") func (h *PeerIDAuthHandshakeServer) Run() error { h.ran = true switch h.state { + case peerIDAuthServerStateSignChallenge: + h.hb.writeScheme(PeerIDAuthScheme) + if err := h.addChallengeClientParam(); err != nil { + return err + } + if err := h.addPublicKeyParam(); err != nil { + return err + } + + publicKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) + if err != nil { + return err + } + h.opaque.ClientPublicKey = publicKeyBytes + if err := h.addServerSigParam(publicKeyBytes); err != nil { + return err + } + if err := h.addOpaqueParam(); err != nil { + return err + } case peerIDAuthServerStateChallengeClient: h.hb.writeScheme(PeerIDAuthScheme) - { - _, err := io.ReadFull(randReader, h.buf[:challengeLen]) - if err != nil { - return err - } - encodedChallenge := base64.URLEncoding.AppendEncode(h.buf[challengeLen:challengeLen], h.buf[:challengeLen]) - h.opaque = opaqueState{ - ChallengeClient: string(encodedChallenge), - Hostname: h.Hostname, - CreatedTime: nowFn(), - } - h.hb.writeParam("challenge-client", encodedChallenge) + if err := h.addChallengeClientParam(); err != nil { + return err } - { - opaqueVal, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) - if err != nil { - return err - } - h.hb.writeParamB64(h.buf[len(opaqueVal):], "opaque", opaqueVal) + if err := h.addPublicKeyParam(); err != nil { + return err + } + if err := h.addOpaqueParam(); err != nil { + return err } case peerIDAuthServerStateVerifyChallenge: { @@ -173,44 +191,27 @@ func (h *PeerIDAuthHandshakeServer) Run() error { return errors.New("hostname in opaque mismatch") } - // If we got a public key, check that it matches the peer id - if len(h.p.publicKeyB64) == 0 { - return errors.New("missing public key") - } - publicKeyBytes, err := base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) - if err != nil { - return err - } - pubKey, err := crypto.UnmarshalPublicKey(publicKeyBytes) - if err != nil { - return err - } + var publicKeyBytes []byte + clientInitiatedHandshake := h.opaque.ClientPublicKey != nil - { - sig, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.sigB64) - if err != nil { - return fmt.Errorf("failed to decode signature: %w", err) + if clientInitiatedHandshake { + publicKeyBytes = h.opaque.ClientPublicKey + } else { + if len(h.p.publicKeyB64) == 0 { + return errors.New("missing public key") } - err = verifySig(pubKey, PeerIDAuthScheme, []sigParam{ - {k: "challenge-client", v: []byte(h.opaque.ChallengeClient)}, - {k: "hostname", v: []byte(h.Hostname)}, - }, sig) + var err error + publicKeyBytes, err = base64.URLEncoding.AppendDecode(nil, h.p.publicKeyB64) if err != nil { return err } } - - if len(h.p.challengeServer) < challengeLen { - return errors.New("challenge too short") - } - // We authenticated the client, now authenticate ourselves - serverSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ - {"challenge-server", h.p.challengeServer}, - {"client-public-key", publicKeyBytes}, - {"hostname", []byte(h.Hostname)}, - }) + pubKey, err := crypto.UnmarshalPublicKey(publicKeyBytes) if err != nil { - return fmt.Errorf("failed to sign challenge: %w", err) + return err + } + if err := h.verifySig(pubKey); err != nil { + return err } peerID, err := peer.IDFromPublicKey(pubKey) @@ -225,22 +226,17 @@ func (h *PeerIDAuthHandshakeServer) Run() error { Hostname: h.Hostname, CreatedTime: nowFn(), } - serverPubKey := h.PrivKey.GetPublic() - pubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) - if err != nil { - return err - } h.hb.writeScheme(PeerIDAuthScheme) - h.hb.writeParamB64(h.buf[:], "sig", serverSig) - { - bearerToken, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) - if err != nil { + + if !clientInitiatedHandshake { + if err := h.addServerSigParam(publicKeyBytes); err != nil { return err } - h.hb.writeParamB64(h.buf[len(bearerToken):], "bearer", bearerToken) } - h.hb.writeParamB64(h.buf[:], "public-key", pubKeyBytes) + if err := h.addBearerParam(); err != nil { + return err + } case peerIDAuthServerStateVerifyBearer: { bearerToken, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.bearerTokenB64) @@ -266,6 +262,84 @@ func (h *PeerIDAuthHandshakeServer) Run() error { return nil } +func (h *PeerIDAuthHandshakeServer) addChallengeClientParam() error { + _, err := io.ReadFull(randReader, h.buf[:challengeLen]) + if err != nil { + return err + } + encodedChallenge := base64.URLEncoding.AppendEncode(h.buf[challengeLen:challengeLen], h.buf[:challengeLen]) + h.opaque.ChallengeClient = string(encodedChallenge) + h.opaque.Hostname = h.Hostname + h.opaque.CreatedTime = nowFn() + h.hb.writeParam("challenge-client", encodedChallenge) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addOpaqueParam() error { + opaqueVal, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[len(opaqueVal):], "opaque", opaqueVal) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addServerSigParam(clientPublicKeyBytes []byte) error { + if len(h.p.challengeServer) < challengeLen { + return errors.New("challenge too short") + } + serverSig, err := sign(h.PrivKey, PeerIDAuthScheme, []sigParam{ + {"challenge-server", h.p.challengeServer}, + {"client-public-key", clientPublicKeyBytes}, + {"hostname", []byte(h.Hostname)}, + }) + if err != nil { + return fmt.Errorf("failed to sign challenge: %w", err) + } + h.hb.writeParamB64(h.buf[:], "sig", serverSig) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addBearerParam() error { + bearerToken, err := h.opaque.Marshal(h.Hmac, h.buf[:0]) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[len(bearerToken):], "bearer", bearerToken) + return nil +} + +func (h *PeerIDAuthHandshakeServer) addPublicKeyParam() error { + serverPubKey := h.PrivKey.GetPublic() + pubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) + if err != nil { + return err + } + h.hb.writeParamB64(h.buf[:], "public-key", pubKeyBytes) + return nil +} + +func (h *PeerIDAuthHandshakeServer) verifySig(clientPubKey crypto.PubKey) error { + serverPubKey := h.PrivKey.GetPublic() + serverPubKeyBytes, err := crypto.MarshalPublicKey(serverPubKey) + if err != nil { + return err + } + sig, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.sigB64) + if err != nil { + return fmt.Errorf("failed to decode signature: %w", err) + } + err = verifySig(clientPubKey, PeerIDAuthScheme, []sigParam{ + {k: "challenge-client", v: []byte(h.opaque.ChallengeClient)}, + {k: "server-public-key", v: serverPubKeyBytes}, + {k: "hostname", v: []byte(h.Hostname)}, + }, sig) + if err != nil { + return err + } + return nil +} + // PeerID returns the peer ID of the authenticated client. func (h *PeerIDAuthHandshakeServer) PeerID() (peer.ID, error) { if !h.ran { @@ -286,7 +360,7 @@ func (h *PeerIDAuthHandshakeServer) SetHeader(hdr http.Header) { } defer h.hb.clear() switch h.state { - case peerIDAuthServerStateChallengeClient: + case peerIDAuthServerStateChallengeClient, peerIDAuthServerStateSignChallenge: hdr.Set("WWW-Authenticate", h.hb.b.String()) case peerIDAuthServerStateVerifyChallenge: hdr.Set("Authentication-Info", h.hb.b.String()) From 80a856949984b9d09d9dd78066b7384937fc4e88 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 6 Sep 2024 14:53:28 -0700 Subject: [PATCH 25/31] Change handshake api a bit --- p2p/http/auth/internal/handshake/client.go | 14 +++++++++++++- .../auth/internal/handshake/handshake_test.go | 16 ++++++---------- p2p/http/auth/internal/handshake/server.go | 19 ++++++++++--------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 6b6bbc55ee..47b089653b 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -38,12 +38,24 @@ type PeerIDAuthHandshakeClient struct { var errMissingChallenge = errors.New("missing challenge") -func (h *PeerIDAuthHandshakeClient) ParseHeaderVal(headerVal []byte) error { +func (h *PeerIDAuthHandshakeClient) SetInitiateChallenge() { + h.state = peerIDAuthClientInitiateChallenge +} + +func (h *PeerIDAuthHandshakeClient) ParseHeader(header http.Header) error { if h.state == peerIDAuthClientStateDone || h.state == peerIDAuthClientInitiateChallenge { return nil } h.p = params{} + var headerVal []byte + switch h.state { + case peerIDAuthClientStateSignChallenge, peerIDAuthClientStateVerifyAndSignChallenge: + headerVal = []byte(header.Get("WWW-Authenticate")) + case peerIDAuthClientStateVerifyChallenge, peerIDAuthClientStateWaitingForBearer: + headerVal = []byte(header.Get("Authentication-Info")) + } + if len(headerVal) == 0 { return errMissingChallenge } diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go index edfb01032d..89704171bf 100644 --- a/p2p/http/auth/internal/handshake/handshake_test.go +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -51,7 +51,7 @@ func TestHandshake(t *testing.T) { } // Client receives the challenge and signs it. Also sends the challenge server - require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) clientHandshake.SetHeader(headers) @@ -64,11 +64,7 @@ func TestHandshake(t *testing.T) { serverHandshake.SetHeader(headers) // Client verifies sig and sets the bearer token for future requests - headerVal := []byte(headers.Get("Authentication-Info")) - if clientInitiated { - headerVal = []byte(headers.Get("WWW-Authenticate")) - } - require.NoError(t, clientHandshake.ParseHeaderVal(headerVal)) + require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) clientHandshake.SetHeader(headers) @@ -118,7 +114,7 @@ func BenchmarkServerHandshake(b *testing.B) { serverHandshake.SetHeader(headers) // Client receives the challenge and signs it. Also sends the challenge server - require.NoError(b, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + require.NoError(b, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(b, clientHandshake.Run()) clientHandshake.SetHeader(clientHeader1) @@ -131,7 +127,7 @@ func BenchmarkServerHandshake(b *testing.B) { serverHandshake.SetHeader(headers) // Client verifies sig and sets the bearer token for future requests - require.NoError(b, clientHandshake.ParseHeaderVal([]byte(headers.Get("Authentication-Info")))) + require.NoError(b, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(b, clientHandshake.Run()) clientHandshake.SetHeader(clientHeader2) @@ -380,7 +376,7 @@ func TestSpecsExample(t *testing.T) { initialWWWAuthenticate := headers.Get("WWW-Authenticate") // Client receives the challenge and signs it. Also sends the challenge server - require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("WWW-Authenticate")))) + require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) clientHandshake.SetHeader(headers) @@ -395,7 +391,7 @@ func TestSpecsExample(t *testing.T) { serverAuthentication := headers.Get("Authentication-Info") // Client verifies sig and sets the bearer token for future requests - require.NoError(t, clientHandshake.ParseHeaderVal([]byte(headers.Get("Authentication-Info")))) + require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) clientHandshake.SetHeader(headers) diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go index 5f50f459ec..c7e68615d4 100644 --- a/p2p/http/auth/internal/handshake/server.go +++ b/p2p/http/auth/internal/handshake/server.go @@ -15,6 +15,12 @@ import ( "github.com/libp2p/go-libp2p/core/peer" ) +var ( + ErrExpiredChallenge = errors.New("challenge expired") + ErrExpiredToken = errors.New("token expired") + ErrInvalidHMAC = errors.New("invalid HMAC") +) + const challengeTTL = 5 * time.Minute type peerIDAuthServerState int @@ -54,12 +60,10 @@ func (o *opaqueState) Marshal(hmac hash.Hash, b []byte) ([]byte, error) { return b, nil } -var errInvalidHMAC = errors.New("invalid HMAC") - func (o *opaqueState) Unmarshal(hmacImpl hash.Hash, d []byte) error { hmacImpl.Reset() if len(d) < hmacImpl.Size() { - return errInvalidHMAC + return ErrInvalidHMAC } hmacVal := d[:hmacImpl.Size()] fields := d[hmacImpl.Size():] @@ -69,7 +73,7 @@ func (o *opaqueState) Unmarshal(hmacImpl hash.Hash, d []byte) error { } expectedHmac := hmacImpl.Sum(nil) if !hmac.Equal(hmacVal, expectedHmac) { - return errInvalidHMAC + return ErrInvalidHMAC } err = json.Unmarshal(fields, &o) @@ -132,9 +136,6 @@ func (h *PeerIDAuthHandshakeServer) ParseHeaderVal(headerVal []byte) error { return nil } -var errExpiredChallenge = errors.New("challenge expired") -var errExpiredToken = errors.New("token expired") - func (h *PeerIDAuthHandshakeServer) Run() error { h.ran = true switch h.state { @@ -181,7 +182,7 @@ func (h *PeerIDAuthHandshakeServer) Run() error { } } if nowFn().After(h.opaque.CreatedTime.Add(challengeTTL)) { - return errExpiredChallenge + return ErrExpiredChallenge } if h.opaque.IsToken { return errors.New("expected challenge, got token") @@ -253,7 +254,7 @@ func (h *PeerIDAuthHandshakeServer) Run() error { } if nowFn().After(h.opaque.CreatedTime.Add(h.TokenTTL)) { - return errExpiredToken + return ErrExpiredToken } return nil From 83e5a1f3b4e498fa94374ea0b54f7bf28b74464c Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 6 Sep 2024 15:02:13 -0700 Subject: [PATCH 26/31] Add Client Initiated handshake to API --- p2p/http/auth/auth_test.go | 105 +++++++++++++++++++++++++++------ p2p/http/auth/client.go | 118 +++++++++++++++++++++++++++++-------- p2p/http/auth/server.go | 29 +++++++-- 3 files changed, 207 insertions(+), 45 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 9718a63f33..463cd79082 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -2,9 +2,15 @@ package httppeeridauth import ( "bytes" + "crypto/hmac" "crypto/rand" + "crypto/sha256" + "crypto/tls" + "hash" + "io" "net/http" "net/http/httptest" + "sync" "testing" "time" @@ -94,9 +100,16 @@ func TestMutualAuth(t *testing.T) { for _, ctc := range clientTestCases { for _, stc := range serverTestCases { t.Run(ctc.name+"+"+stc.name, func(t *testing.T) { - ts, _ := stc.serverGen(t) + ts, server := stc.serverGen(t) client := ts.Client() - tlsClientConfig := client.Transport.(*http.Transport).TLSClientConfig + roundTripper := instrumentedRoundTripper{client.Transport, 0} + client.Transport = &roundTripper + requestsSent := func() int { + defer func() { roundTripper.timesRoundtripped = 0 }() + return roundTripper.timesRoundtripped + } + + tlsClientConfig := roundTripper.TLSClientConfig() if tlsClientConfig != nil { // If we're using TLS, we need to set the SNI so that the // server can verify the request Host matches it. @@ -108,35 +121,93 @@ func TestMutualAuth(t *testing.T) { expectedServerID, err := peer.IDFromPrivateKey(serverKey) require.NoError(t, err) - newReq := func() *http.Request { - req, err := http.NewRequest("POST", ts.URL, nil) - require.NoError(t, err) - req.Host = "example.com" - return req - } - serverID, resp, err := clientAuth.AuthenticatedDo(client, newReq) + req, err := http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err := clientAuth.AuthenticatedDo(client, req) require.NoError(t, err) require.Equal(t, expectedServerID, serverID) require.NotZero(t, clientAuth.tokenMap["example.com"]) require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 2, requestsSent()) // Once more with the auth token - req, err := http.NewRequest("POST", ts.URL, nil) + req, err = http.NewRequest("POST", ts.URL, nil) require.NoError(t, err) req.Host = "example.com" - timesCalled := 0 - newReq = func() *http.Request { - timesCalled++ - return req - } - serverID, resp, err = clientAuth.AuthenticatedDo(client, newReq) + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) require.NotEmpty(t, req.Header.Get("Authorization")) - require.Equal(t, 1, timesCalled, "should only call newRequest once since we have a token") require.NoError(t, err) require.Equal(t, expectedServerID, serverID) require.NotZero(t, clientAuth.tokenMap["example.com"]) require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, 1, requestsSent(), "should only call newRequest once since we have a token") + + t.Run("Tokens Expired", func(t *testing.T) { + // Clear the auth token on the server side + server.TokenTTL = 1 // Small TTL + time.Sleep(100 * time.Millisecond) + resetServerTokenTTL := sync.OnceFunc(func() { + server.TokenTTL = time.Hour + }) + + req, err := http.NewRequest("POST", ts.URL, nil) + require.NoError(t, err) + req.Host = "example.com" + req.GetBody = func() (io.ReadCloser, error) { + resetServerTokenTTL() + return nil, nil + } + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + require.NoError(t, err) + require.NotEmpty(t, req.Header.Get("Authorization")) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.Equal(t, 3, requestsSent(), "should call newRequest 3x since our token expired") + }) + + t.Run("Tokens Invalidated", func(t *testing.T) { + // Clear the auth token on the server side + server.Hmac = func() hash.Hash { + key := make([]byte, 32) + _, err := rand.Read(key) + if err != nil { + panic(err) + } + return hmac.New(sha256.New, key) + }() + + req, err := http.NewRequest("POST", ts.URL, nil) + req.GetBody = func() (io.ReadCloser, error) { + return nil, nil + } + require.NoError(t, err) + req.Host = "example.com" + serverID, resp, err = clientAuth.AuthenticatedDo(client, req) + require.NoError(t, err) + require.NotEmpty(t, req.Header.Get("Authorization")) + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, expectedServerID, serverID) + require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.Equal(t, 3, requestsSent(), "should call newRequest 3x since our token expired") + }) + }) } } } + +type instrumentedRoundTripper struct { + http.RoundTripper + timesRoundtripped int +} + +func (irt *instrumentedRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + irt.timesRoundtripped++ + return irt.RoundTripper.RoundTrip(req) +} + +func (irt *instrumentedRoundTripper) TLSClientConfig() *tls.Config { + return irt.RoundTripper.(*http.Transport).TLSClientConfig +} diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index cb7c4856de..73bf028407 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -1,9 +1,11 @@ package httppeeridauth import ( + "errors" "fmt" "net/http" "sync" + "time" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" @@ -11,48 +13,89 @@ import ( ) type ClientPeerIDAuth struct { - PrivKey crypto.PrivKey + PrivKey crypto.PrivKey + TokenTTL time.Duration tokenMapMu sync.Mutex tokenMap map[string]tokenInfo } type tokenInfo struct { - token string - peerID peer.ID + token string + insertedAt time.Time + peerID peer.ID } -// AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth handshake if needed. -// Takes in a function that creates a new request, so that we can retry the -// request if we need to authenticate. -func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, newRequest func() *http.Request) (peer.ID, *http.Response, error) { - req := newRequest() +// AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth +// handshake if needed. +// +// It is recommended to pass in an http.Request with `GetBody` set, so that this +// method can retry sending the request in case a previously used token has +// expired. +func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) { hostname := req.Host a.tokenMapMu.Lock() if a.tokenMap == nil { a.tokenMap = make(map[string]tokenInfo) } - ti, ok := a.tokenMap[hostname] + ti, hasToken := a.tokenMap[hostname] + if hasToken && a.TokenTTL != 0 && time.Since(ti.insertedAt) > a.TokenTTL { + hasToken = false + delete(a.tokenMap, hostname) + } a.tokenMapMu.Unlock() - if ok { + + clientIntiatesHandshake := !hasToken + handshake := handshake.PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: a.PrivKey, + } + if clientIntiatesHandshake { + handshake.SetInitiateChallenge() + } + + if hasToken { + // Try to make the request with the token req.Header.Set("Authorization", ti.token) + resp, err := client.Do(req) + if err != nil { + return "", nil, err + } + if resp.StatusCode != http.StatusUnauthorized { + // our token is still valid + return ti.peerID, resp, nil + } + if req.GetBody == nil { + // We can't retry this request even if we wanted to. + // Return the response and an error + return "", resp, errors.New("expired token. Couldn't run handshake because req.GetBody is nil") + } + resp.Body.Close() + + // Token didn't work, we need to re-authenticate. + // Run the server-initiated handshake + req = req.Clone(req.Context()) + req.Body, err = req.GetBody() + if err != nil { + return "", nil, err + } + + handshake.ParseHeader(resp.Header) } + originalBody := req.Body + handshake.Run() + handshake.SetHeader(req.Header) + + // Don't send the body before we've authenticated the server + req.Body = nil resp, err := client.Do(req) if err != nil { return "", nil, err } - if resp.StatusCode != http.StatusUnauthorized { - // our token is still valid or no auth needed - return ti.peerID, resp, nil - } resp.Body.Close() - handshake := handshake.PeerIDAuthHandshakeClient{ - Hostname: hostname, - PrivKey: a.PrivKey, - } - err = handshake.ParseHeaderVal([]byte(resp.Header.Get("WWW-Authenticate"))) + err = handshake.ParseHeader(resp.Header) if err != nil { return "", nil, fmt.Errorf("failed to parse auth header: %w", err) } @@ -61,15 +104,26 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, newRequest func( return "", nil, fmt.Errorf("failed to run handshake: %w", err) } - req = newRequest() - handshake.SetHeader(req.Header) + serverWasAuthenticated := false + _, err = handshake.PeerID() + if err == nil { + serverWasAuthenticated = true + } + req = req.Clone(req.Context()) + if serverWasAuthenticated { + req.Body = originalBody + } else { + // Don't send the body before we've authenticated the server + req.Body = nil + } + handshake.SetHeader(req.Header) resp, err = client.Do(req) if err != nil { return "", nil, fmt.Errorf("failed to do authenticated request: %w", err) } - err = handshake.ParseHeaderVal([]byte(resp.Header.Get("Authentication-Info"))) + err = handshake.ParseHeader(resp.Header) if err != nil { resp.Body.Close() return "", nil, fmt.Errorf("failed to parse auth info header: %w", err) @@ -86,8 +140,26 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, newRequest func( return "", nil, fmt.Errorf("failed to get server's peer ID: %w", err) } a.tokenMapMu.Lock() - a.tokenMap[hostname] = tokenInfo{handshake.BearerToken(), serverPeerID} + a.tokenMap[hostname] = tokenInfo{ + token: handshake.BearerToken(), + insertedAt: time.Now(), + peerID: serverPeerID, + } a.tokenMapMu.Unlock() + if serverWasAuthenticated { + return serverPeerID, resp, nil + } + + // Server wasn't authenticated earlier. + // We need to make one final request with the body now that we authenticated + // the server. + req = req.Clone(req.Context()) + req.Body = originalBody + handshake.SetHeader(req.Header) + resp, err = client.Do(req) + if err != nil { + return "", nil, fmt.Errorf("failed to do authenticated request: %w", err) + } return serverPeerID, resp, nil } diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 28f4f4f9fe..4e625caf0a 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -4,6 +4,7 @@ import ( "crypto/hmac" "crypto/rand" "crypto/sha256" + "errors" "hash" "net/http" "sync" @@ -70,27 +71,45 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } - handshake := handshake.PeerIDAuthHandshakeServer{ + hs := handshake.PeerIDAuthHandshakeServer{ Hostname: hostname, PrivKey: a.PrivKey, TokenTTL: a.TokenTTL, Hmac: a.Hmac, } - err := handshake.ParseHeaderVal([]byte(r.Header.Get("Authorization"))) + err := hs.ParseHeaderVal([]byte(r.Header.Get("Authorization"))) if err != nil { log.Debugf("Failed to parse header: %v", err) w.WriteHeader(http.StatusBadRequest) return } - err = handshake.Run() + err = hs.Run() if err != nil { + switch { + case errors.Is(err, handshake.ErrInvalidHMAC), + errors.Is(err, handshake.ErrExpiredChallenge), + errors.Is(err, handshake.ErrExpiredToken): + + hs := handshake.PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: a.PrivKey, + TokenTTL: a.TokenTTL, + Hmac: a.Hmac, + } + hs.Run() + hs.SetHeader(w.Header()) + w.WriteHeader(http.StatusUnauthorized) + + return + } + log.Debugf("Failed to run handshake: %v", err) w.WriteHeader(http.StatusBadRequest) return } - handshake.SetHeader(w.Header()) + hs.SetHeader(w.Header()) - peer, err := handshake.PeerID() + peer, err := hs.PeerID() if err != nil { w.WriteHeader(http.StatusUnauthorized) return From 5e07918eea1e48ff06cb73208ae14455a31d8827 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 6 Sep 2024 15:10:43 -0700 Subject: [PATCH 27/31] Use ValidHostnameFn even with TLS set --- p2p/http/auth/server.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/p2p/http/auth/server.go b/p2p/http/auth/server.go index 4e625caf0a..3ee4f96dc8 100644 --- a/p2p/http/auth/server.go +++ b/p2p/http/auth/server.go @@ -54,7 +54,7 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if !a.ValidHostnameFn(hostname) { - log.Debugf("Unauthorized request for host %s: hostname not in valid set", hostname) + log.Debugf("Unauthorized request for host %s: hostname returned false for ValidHostnameFn", hostname) w.WriteHeader(http.StatusBadRequest) return } @@ -69,6 +69,11 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) return } + if a.ValidHostnameFn != nil && !a.ValidHostnameFn(hostname) { + log.Debugf("Unauthorized request for host %s: hostname returned false for ValidHostnameFn", hostname) + w.WriteHeader(http.StatusBadRequest) + return + } } hs := handshake.PeerIDAuthHandshakeServer{ From e0e261faa33faf4b2a2811d3a52233189f693aa1 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 9 Sep 2024 12:47:13 -0700 Subject: [PATCH 28/31] Couple of improvments in internal handshake package --- p2p/http/auth/internal/handshake/client.go | 20 +++++++++- p2p/http/auth/internal/handshake/handshake.go | 12 +++++- .../auth/internal/handshake/handshake_test.go | 12 +++--- p2p/http/auth/internal/handshake/server.go | 37 +++++++++---------- 4 files changed, 53 insertions(+), 28 deletions(-) diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 47b089653b..6800ec8104 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -160,6 +160,9 @@ func (h *PeerIDAuthHandshakeClient) addChallengeServerParam() error { } func (h *PeerIDAuthHandshakeClient) verifySig(clientPubKeyBytes []byte) error { + if len(h.p.sigB64) == 0 { + return errors.New("signature not set") + } sig, err := base64.URLEncoding.AppendDecode(nil, h.p.sigB64) if err != nil { return fmt.Errorf("failed to decode signature: %w", err) @@ -205,7 +208,7 @@ func (h *PeerIDAuthHandshakeClient) PeerID() (peer.ID, error) { return h.serverPeerID, nil } -func (h *PeerIDAuthHandshakeClient) SetHeader(hdr http.Header) { +func (h *PeerIDAuthHandshakeClient) AddHeader(hdr http.Header) { hdr.Set("Authorization", h.hb.b.String()) } @@ -217,3 +220,18 @@ func (h *PeerIDAuthHandshakeClient) BearerToken() string { } return h.hb.b.String() } + +func (h *PeerIDAuthHandshakeClient) ServerAuthenticated() bool { + switch h.state { + case peerIDAuthClientStateDone: + case peerIDAuthClientStateWaitingForBearer: + default: + return false + } + + return h.serverPeerID != "" +} + +func (h *PeerIDAuthHandshakeClient) HandshakeDone() bool { + return h.state == peerIDAuthClientStateDone +} diff --git a/p2p/http/auth/internal/handshake/handshake.go b/p2p/http/auth/internal/handshake/handshake.go index 75051f6e27..e45ad7ef1f 100644 --- a/p2p/http/auth/internal/handshake/handshake.go +++ b/p2p/http/auth/internal/handshake/handshake.go @@ -19,7 +19,7 @@ import ( const PeerIDAuthScheme = "libp2p-PeerID" const challengeLen = 32 -const maxHeaderSize = 8192 +const maxHeaderSize = 2048 var peerIDAuthSchemeBytes = []byte(PeerIDAuthScheme) @@ -144,6 +144,9 @@ func (h *headerBuilder) writeParamB64(buf []byte, key string, val []byte) { // writeParam writes a key value pair to the header. It writes the val as-is. func (h *headerBuilder) writeParam(key string, val []byte) { + if len(val) == 0 { + return + } h.maybeAddComma() h.b.Grow(len(key) + len(`="`) + len(val) + 1) @@ -160,6 +163,10 @@ type sigParam struct { } func verifySig(publicKey crypto.PubKey, prefix string, signedParts []sigParam, sig []byte) error { + if publicKey == nil { + return fmt.Errorf("no public key to verify signature") + } + b := pool.Get(4096) defer pool.Put(b) buf, err := genDataToSign(b[:0], prefix, signedParts) @@ -178,6 +185,9 @@ func verifySig(publicKey crypto.PubKey, prefix string, signedParts []sigParam, s } func sign(privKey crypto.PrivKey, prefix string, partsToSign []sigParam) ([]byte, error) { + if privKey == nil { + return nil, fmt.Errorf("no private key available to sign") + } b := pool.Get(4096) defer pool.Put(b) buf, err := genDataToSign(b[:0], prefix, partsToSign) diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go index 89704171bf..85c09a7e88 100644 --- a/p2p/http/auth/internal/handshake/handshake_test.go +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -54,7 +54,7 @@ func TestHandshake(t *testing.T) { require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) - clientHandshake.SetHeader(headers) + clientHandshake.AddHeader(headers) // Server receives the sig and verifies it. Also signs the challenge server serverHandshake.Reset() @@ -67,7 +67,7 @@ func TestHandshake(t *testing.T) { require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) - clientHandshake.SetHeader(headers) + clientHandshake.AddHeader(headers) // Server verifies the bearer token serverHandshake.Reset() @@ -117,7 +117,7 @@ func BenchmarkServerHandshake(b *testing.B) { require.NoError(b, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(b, clientHandshake.Run()) - clientHandshake.SetHeader(clientHeader1) + clientHandshake.AddHeader(clientHeader1) // Server receives the sig and verifies it. Also signs the challenge server serverHandshake.Reset() @@ -130,7 +130,7 @@ func BenchmarkServerHandshake(b *testing.B) { require.NoError(b, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(b, clientHandshake.Run()) - clientHandshake.SetHeader(clientHeader2) + clientHandshake.AddHeader(clientHeader2) // Server verifies the bearer token serverHandshake.Reset() @@ -379,7 +379,7 @@ func TestSpecsExample(t *testing.T) { require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) - clientHandshake.SetHeader(headers) + clientHandshake.AddHeader(headers) clientAuthentication := headers.Get("Authorization") // Server receives the sig and verifies it. Also signs the challenge server @@ -394,7 +394,7 @@ func TestSpecsExample(t *testing.T) { require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) - clientHandshake.SetHeader(headers) + clientHandshake.AddHeader(headers) clientBearerToken := headers.Get("Authorization") params := params{} diff --git a/p2p/http/auth/internal/handshake/server.go b/p2p/http/auth/internal/handshake/server.go index c7e68615d4..eacf5a7c91 100644 --- a/p2p/http/auth/internal/handshake/server.go +++ b/p2p/http/auth/internal/handshake/server.go @@ -111,6 +111,7 @@ func (h *PeerIDAuthHandshakeServer) Reset() { h.hb.clear() h.opaque = opaqueState{} } + func (h *PeerIDAuthHandshakeServer) ParseHeaderVal(headerVal []byte) error { if len(headerVal) == 0 { // We are in the initial state. Nothing to parse. @@ -120,8 +121,6 @@ func (h *PeerIDAuthHandshakeServer) ParseHeaderVal(headerVal []byte) error { if err != nil { return err } - if h.p.sigB64 != nil && h.p.opaqueB64 != nil { - } switch { case h.p.sigB64 != nil && h.p.opaqueB64 != nil: h.state = peerIDAuthServerStateVerifyChallenge @@ -171,16 +170,15 @@ func (h *PeerIDAuthHandshakeServer) Run() error { return err } case peerIDAuthServerStateVerifyChallenge: - { - opaque, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.opaqueB64) - if err != nil { - return err - } - err = h.opaque.Unmarshal(h.Hmac, opaque) - if err != nil { - return err - } + opaque, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.opaqueB64) + if err != nil { + return err + } + err = h.opaque.Unmarshal(h.Hmac, opaque) + if err != nil { + return err } + if nowFn().After(h.opaque.CreatedTime.Add(challengeTTL)) { return ErrExpiredChallenge } @@ -239,16 +237,15 @@ func (h *PeerIDAuthHandshakeServer) Run() error { return err } case peerIDAuthServerStateVerifyBearer: - { - bearerToken, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.bearerTokenB64) - if err != nil { - return err - } - err = h.opaque.Unmarshal(h.Hmac, bearerToken) - if err != nil { - return err - } + bearerToken, err := base64.URLEncoding.AppendDecode(h.buf[:0], h.p.bearerTokenB64) + if err != nil { + return err + } + err = h.opaque.Unmarshal(h.Hmac, bearerToken) + if err != nil { + return err } + if !h.opaque.IsToken { return errors.New("expected token, got challenge") } From fbc0ac8f743ca596f1182e8d5a279664d13c3513 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 9 Sep 2024 12:58:32 -0700 Subject: [PATCH 29/31] Clear GetBody as well; simply running handshake --- p2p/http/auth/auth_test.go | 32 +++++- p2p/http/auth/client.go | 221 +++++++++++++++++++++---------------- 2 files changed, 156 insertions(+), 97 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index 463cd79082..ad7daec155 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "sync" "testing" "time" @@ -17,6 +18,7 @@ import ( logging "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -190,7 +192,7 @@ func TestMutualAuth(t *testing.T) { require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, expectedServerID, serverID) require.NotZero(t, clientAuth.tokenMap["example.com"]) - require.Equal(t, 3, requestsSent(), "should call newRequest 3x since our token expired") + require.Equal(t, 3, requestsSent(), "should call have sent 3 reqs since our token expired") }) }) @@ -198,6 +200,34 @@ func TestMutualAuth(t *testing.T) { } } +func TestBodyNotSentDuringRedirect(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.Empty(t, string(b)) + if r.URL.Path != "/redirected" { + w.Header().Set("Location", "/redirected") + w.WriteHeader(http.StatusTemporaryRedirect) + return + } + })) + t.Cleanup(ts.Close) + client := ts.Client() + clientKey, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientAuth := ClientPeerIDAuth{PrivKey: clientKey} + + req, err := + http.NewRequest( + "POST", + ts.URL, + strings.NewReader("Only for authenticated servers"), + ) + req.Host = "example.com" + require.NoError(t, err) + _, _, err = clientAuth.AuthenticatedDo(client, req) + require.ErrorContains(t, err, "signature not set") // server doesn't actually handshake +} + type instrumentedRoundTripper struct { http.RoundTripper timesRoundtripped int diff --git a/p2p/http/auth/client.go b/p2p/http/auth/client.go index 73bf028407..a6bdece61a 100644 --- a/p2p/http/auth/client.go +++ b/p2p/http/auth/client.go @@ -3,6 +3,7 @@ package httppeeridauth import ( "errors" "fmt" + "io" "net/http" "sync" "time" @@ -16,14 +17,7 @@ type ClientPeerIDAuth struct { PrivKey crypto.PrivKey TokenTTL time.Duration - tokenMapMu sync.Mutex - tokenMap map[string]tokenInfo -} - -type tokenInfo struct { - token string - insertedAt time.Time - peerID peer.ID + tm tokenMap } // AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth @@ -34,43 +28,24 @@ type tokenInfo struct { // expired. func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) { hostname := req.Host - a.tokenMapMu.Lock() - if a.tokenMap == nil { - a.tokenMap = make(map[string]tokenInfo) - } - ti, hasToken := a.tokenMap[hostname] - if hasToken && a.TokenTTL != 0 && time.Since(ti.insertedAt) > a.TokenTTL { - hasToken = false - delete(a.tokenMap, hostname) - } - a.tokenMapMu.Unlock() - - clientIntiatesHandshake := !hasToken + ti, hasToken := a.tm.get(hostname, a.TokenTTL) handshake := handshake.PeerIDAuthHandshakeClient{ Hostname: hostname, PrivKey: a.PrivKey, } - if clientIntiatesHandshake { - handshake.SetInitiateChallenge() - } if hasToken { - // Try to make the request with the token - req.Header.Set("Authorization", ti.token) - resp, err := client.Do(req) - if err != nil { + // We have a token. Attempt to use that, but fallback to server initiated challenge if it fails. + peer, resp, err := a.doWithToken(client, req, ti) + switch { + case err == nil: + return peer, resp, nil + case errors.Is(err, errTokenRejected): + // Token was rejected, we need to re-authenticate + break + default: return "", nil, err } - if resp.StatusCode != http.StatusUnauthorized { - // our token is still valid - return ti.peerID, resp, nil - } - if req.GetBody == nil { - // We can't retry this request even if we wanted to. - // Return the response and an error - return "", resp, errors.New("expired token. Couldn't run handshake because req.GetBody is nil") - } - resp.Body.Close() // Token didn't work, we need to re-authenticate. // Run the server-initiated handshake @@ -81,85 +56,139 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Reques } handshake.ParseHeader(resp.Header) + } else { + // We didn't have a handshake token, so we initiate the handshake. + // If our token was rejected, the server initiates the handshake. + handshake.SetInitiateChallenge() } - originalBody := req.Body - - handshake.Run() - handshake.SetHeader(req.Header) - - // Don't send the body before we've authenticated the server - req.Body = nil - resp, err := client.Do(req) - if err != nil { - return "", nil, err - } - resp.Body.Close() - err = handshake.ParseHeader(resp.Header) - if err != nil { - return "", nil, fmt.Errorf("failed to parse auth header: %w", err) - } - err = handshake.Run() + serverPeerID, resp, err := a.runHandshake(client, req, clearBody(req), &handshake) if err != nil { return "", nil, fmt.Errorf("failed to run handshake: %w", err) } + a.tm.set(hostname, tokenInfo{ + token: handshake.BearerToken(), + insertedAt: time.Now(), + peerID: serverPeerID, + }) + return serverPeerID, resp, nil +} - serverWasAuthenticated := false - _, err = handshake.PeerID() - if err == nil { - serverWasAuthenticated = true - } +func (a *ClientPeerIDAuth) runHandshake(client *http.Client, req *http.Request, b bodyMeta, hs *handshake.PeerIDAuthHandshakeClient) (peer.ID, *http.Response, error) { + maxSteps := 5 // Avoid infinite loops in case of buggy handshake. Shouldn't happen. + var resp *http.Response - req = req.Clone(req.Context()) - if serverWasAuthenticated { - req.Body = originalBody - } else { - // Don't send the body before we've authenticated the server - req.Body = nil - } - handshake.SetHeader(req.Header) - resp, err = client.Do(req) + err := hs.Run() if err != nil { - return "", nil, fmt.Errorf("failed to do authenticated request: %w", err) + return "", nil, err } - err = handshake.ParseHeader(resp.Header) - if err != nil { - resp.Body.Close() - return "", nil, fmt.Errorf("failed to parse auth info header: %w", err) + sentBody := false + for !hs.HandshakeDone() || !sentBody { + req = req.Clone(req.Context()) + hs.AddHeader(req.Header) + if hs.ServerAuthenticated() { + sentBody = true + b.setBody(req) + } + + resp, err = client.Do(req) + if err != nil { + return "", nil, err + } + + hs.ParseHeader(resp.Header) + err = hs.Run() + if err != nil { + resp.Body.Close() + return "", nil, err + } + + if maxSteps--; maxSteps == 0 { + return "", nil, errors.New("handshake took too many steps") + } } - err = handshake.Run() + + p, err := hs.PeerID() if err != nil { resp.Body.Close() - return "", nil, fmt.Errorf("failed to run auth info handshake: %w", err) + return "", nil, err } + return p, resp, nil +} - serverPeerID, err := handshake.PeerID() +var errTokenRejected = errors.New("token rejected") + +func (a *ClientPeerIDAuth) doWithToken(client *http.Client, req *http.Request, ti tokenInfo) (peer.ID, *http.Response, error) { + // Try to make the request with the token + req.Header.Set("Authorization", ti.token) + resp, err := client.Do(req) if err != nil { - resp.Body.Close() - return "", nil, fmt.Errorf("failed to get server's peer ID: %w", err) + return "", nil, err } - a.tokenMapMu.Lock() - a.tokenMap[hostname] = tokenInfo{ - token: handshake.BearerToken(), - insertedAt: time.Now(), - peerID: serverPeerID, + if resp.StatusCode != http.StatusUnauthorized { + // our token is still valid + return ti.peerID, resp, nil + } + if req.GetBody == nil { + // We can't retry this request even if we wanted to. + // Return the response and an error + return "", resp, errors.New("expired token. Couldn't run handshake because req.GetBody is nil") } - a.tokenMapMu.Unlock() + resp.Body.Close() + + return "", resp, errTokenRejected +} - if serverWasAuthenticated { - return serverPeerID, resp, nil +type bodyMeta struct { + body io.ReadCloser + contentLength int64 + getBody func() (io.ReadCloser, error) +} + +func clearBody(req *http.Request) bodyMeta { + defer func() { + req.Body = nil + req.ContentLength = 0 + req.GetBody = nil + }() + return bodyMeta{body: req.Body, contentLength: req.ContentLength, getBody: req.GetBody} +} + +func (b *bodyMeta) setBody(req *http.Request) { + req.Body = b.body + req.ContentLength = b.contentLength + req.GetBody = b.getBody +} + +type tokenInfo struct { + token string + insertedAt time.Time + peerID peer.ID +} + +type tokenMap struct { + tokenMapMu sync.Mutex + tokenMap map[string]tokenInfo +} + +func (tm *tokenMap) get(hostname string, ttl time.Duration) (tokenInfo, bool) { + tm.tokenMapMu.Lock() + defer tm.tokenMapMu.Unlock() + + ti, ok := tm.tokenMap[hostname] + if ok && ttl != 0 && time.Since(ti.insertedAt) > ttl { + delete(tm.tokenMap, hostname) + return tokenInfo{}, false } + return ti, ok +} - // Server wasn't authenticated earlier. - // We need to make one final request with the body now that we authenticated - // the server. - req = req.Clone(req.Context()) - req.Body = originalBody - handshake.SetHeader(req.Header) - resp, err = client.Do(req) - if err != nil { - return "", nil, fmt.Errorf("failed to do authenticated request: %w", err) +func (tm *tokenMap) set(hostname string, ti tokenInfo) { + tm.tokenMapMu.Lock() + defer tm.tokenMapMu.Unlock() + if tm.tokenMap == nil { + tm.tokenMap = make(map[string]tokenInfo) } - return serverPeerID, resp, nil + tm.tokenMap[hostname] = ti } From 35d6843e94b4e63d1ac38018f35294de9eca5a23 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 9 Sep 2024 20:17:00 -0700 Subject: [PATCH 30/31] Handle case where server refuses client-initiated handshake --- p2p/http/auth/internal/handshake/client.go | 5 ++ .../auth/internal/handshake/handshake_test.go | 74 ++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/p2p/http/auth/internal/handshake/client.go b/p2p/http/auth/internal/handshake/client.go index 6800ec8104..f15ed00c6d 100644 --- a/p2p/http/auth/internal/handshake/client.go +++ b/p2p/http/auth/internal/handshake/client.go @@ -101,6 +101,11 @@ func (h *PeerIDAuthHandshakeClient) Run() error { h.state = peerIDAuthClientStateVerifyAndSignChallenge return nil case peerIDAuthClientStateVerifyAndSignChallenge: + if len(h.p.sigB64) == 0 && len(h.p.challengeClient) != 0 { + // The server refused a client initiated handshake, so we need run the server initiated handshake + h.state = peerIDAuthClientStateSignChallenge + return h.Run() + } if err := h.verifySig(clientPubKeyBytes); err != nil { return err } diff --git a/p2p/http/auth/internal/handshake/handshake_test.go b/p2p/http/auth/internal/handshake/handshake_test.go index 85c09a7e88..eb313b7c44 100644 --- a/p2p/http/auth/internal/handshake/handshake_test.go +++ b/p2p/http/auth/internal/handshake/handshake_test.go @@ -50,26 +50,31 @@ func TestHandshake(t *testing.T) { serverHandshake.SetHeader(headers) } - // Client receives the challenge and signs it. Also sends the challenge server + // Server Inititated: Client receives the challenge and signs it. Also sends the challenge server + // Client Inititated: Client forms the challenge and sends it require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) clientHandshake.AddHeader(headers) - // Server receives the sig and verifies it. Also signs the challenge server + // Server Inititated: Server receives the sig and verifies it. Also signs the challenge-server (client authenticated) + // Client Inititated: Server receives the challenge and signs it. Also sends the challenge-client serverHandshake.Reset() require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) clear(headers) require.NoError(t, serverHandshake.Run()) serverHandshake.SetHeader(headers) - // Client verifies sig and sets the bearer token for future requests + // Server Inititated: Client verifies sig and sets the bearer token for future requests (server authenticated) + // Client Inititated: Client verifies sig, and signs challenge. Sends it along with any application data (server authenticated) require.NoError(t, clientHandshake.ParseHeader(headers)) clear(headers) require.NoError(t, clientHandshake.Run()) clientHandshake.AddHeader(headers) - // Server verifies the bearer token + // Server Inititated: Server verifies the bearer token + // Client Inititated: Server verifies the sig, sets the bearer token (client authenticated) + // and processes any application data serverHandshake.Reset() require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) clear(headers) @@ -89,6 +94,67 @@ func TestHandshake(t *testing.T) { } } +func TestServerRefusesClientInitiatedHandshake(t *testing.T) { + hostname := "example.com" + serverPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + clientPriv, _, _ := crypto.GenerateEd25519Key(rand.Reader) + + serverHandshake := PeerIDAuthHandshakeServer{ + Hostname: hostname, + PrivKey: serverPriv, + TokenTTL: time.Hour, + Hmac: hmac.New(sha256.New, make([]byte, 32)), + } + + clientHandshake := PeerIDAuthHandshakeClient{ + Hostname: hostname, + PrivKey: clientPriv, + } + clientHandshake.SetInitiateChallenge() + + headers := make(http.Header) + // Client initiates the handshake + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + // Server receives the challenge-server, but chooses to reject it (simulating this by not passing the challenge) + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal(nil)) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client now runs the server-initiated handshake. Signs challenge-client; sends challenge-server + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + // Server verifies the challenge-client and signs the challenge-server + serverHandshake.Reset() + require.NoError(t, serverHandshake.ParseHeaderVal([]byte(headers.Get("Authorization")))) + clear(headers) + require.NoError(t, serverHandshake.Run()) + serverHandshake.SetHeader(headers) + + // Client verifies the challenge-server and sets the bearer token + require.NoError(t, clientHandshake.ParseHeader(headers)) + clear(headers) + require.NoError(t, clientHandshake.Run()) + clientHandshake.AddHeader(headers) + + expectedClientPeerID, _ := peer.IDFromPrivateKey(clientPriv) + expectedServerPeerID, _ := peer.IDFromPrivateKey(serverPriv) + clientPeerID, err := serverHandshake.PeerID() + require.NoError(t, err) + require.Equal(t, expectedClientPeerID, clientPeerID) + + serverPeerID, err := clientHandshake.PeerID() + require.NoError(t, err) + require.True(t, clientHandshake.HandshakeDone()) + require.Equal(t, expectedServerPeerID, serverPeerID) +} + func BenchmarkServerHandshake(b *testing.B) { clientHeader1 := make(http.Header) clientHeader2 := make(http.Header) From dbd59dcd11ee03528a2ead881a5a5d9c0e16fc38 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Mon, 9 Sep 2024 20:19:10 -0700 Subject: [PATCH 31/31] Fix reference in test --- p2p/http/auth/auth_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/p2p/http/auth/auth_test.go b/p2p/http/auth/auth_test.go index ad7daec155..d080b19511 100644 --- a/p2p/http/auth/auth_test.go +++ b/p2p/http/auth/auth_test.go @@ -129,7 +129,7 @@ func TestMutualAuth(t *testing.T) { serverID, resp, err := clientAuth.AuthenticatedDo(client, req) require.NoError(t, err) require.Equal(t, expectedServerID, serverID) - require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, 2, requestsSent()) @@ -141,7 +141,7 @@ func TestMutualAuth(t *testing.T) { require.NotEmpty(t, req.Header.Get("Authorization")) require.NoError(t, err) require.Equal(t, expectedServerID, serverID) - require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, 1, requestsSent(), "should only call newRequest once since we have a token") @@ -165,7 +165,7 @@ func TestMutualAuth(t *testing.T) { require.NotEmpty(t, req.Header.Get("Authorization")) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, expectedServerID, serverID) - require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) require.Equal(t, 3, requestsSent(), "should call newRequest 3x since our token expired") }) @@ -191,7 +191,7 @@ func TestMutualAuth(t *testing.T) { require.NotEmpty(t, req.Header.Get("Authorization")) require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, expectedServerID, serverID) - require.NotZero(t, clientAuth.tokenMap["example.com"]) + require.NotZero(t, clientAuth.tm.tokenMap["example.com"]) require.Equal(t, 3, requestsSent(), "should call have sent 3 reqs since our token expired") })