Skip to content

Commit

Permalink
Add tests to generate examples for specs
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcoPolo committed Aug 28, 2024
1 parent e521e4d commit fccc2d5
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 15 deletions.
6 changes: 3 additions & 3 deletions p2p/http/auth/internal/handshake/client.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package handshake

import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"

"github.com/libp2p/go-libp2p/core/crypto"
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions p2p/http/auth/internal/handshake/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down
166 changes: 165 additions & 1 deletion p2p/http/auth/internal/handshake/handshake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"testing"
"time"

Expand Down Expand Up @@ -219,7 +223,7 @@ func TestOpaqueStateRoundTrip(t *testing.T) {
ChallengeClient: "foo-bar",
CreatedTime: timeAfterUnmarshal,
IsToken: true,
PeerID: &zeroID,
PeerID: zeroID,
Hostname: "example.com",
}

Expand Down Expand Up @@ -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.")

}
22 changes: 11 additions & 11 deletions p2p/http/auth/internal/handshake/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package handshake

import (
"crypto/hmac"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"net/http"
"time"

Expand All @@ -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"`
}
Expand Down Expand Up @@ -132,15 +132,15 @@ 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
}
encodedChallenge := base64.URLEncoding.AppendEncode(h.buf[challengeLen:challengeLen], h.buf[:challengeLen])
h.opaque = opaqueState{
ChallengeClient: string(encodedChallenge),
Hostname: h.Hostname,
CreatedTime: time.Now(),
CreatedTime: nowFn(),
}
h.hb.writeParam("challenge-client", encodedChallenge)
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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) {
Expand Down

0 comments on commit fccc2d5

Please sign in to comment.