From fbcede2208979ebe05837a238d8cc9dcb527936f Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Wed, 28 Aug 2024 13:08:21 -0700 Subject: [PATCH] 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) {