Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libp2phttp: HTTP Peer ID Authentication #2854

Merged
merged 37 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
18b77b5
Initial implementation of http peer id auth
MarcoPolo Jun 29, 2024
3600784
export client mutual auth
MarcoPolo Jun 29, 2024
2f30269
Add test vectors
MarcoPolo Jul 1, 2024
ef91911
Add test walkthrough for spec
MarcoPolo Jul 2, 2024
425fd92
Don't bother decoding challenges
MarcoPolo Jul 2, 2024
7bf466c
Backport AppendEncode/AppendDecode
MarcoPolo Jul 2, 2024
9f1d418
Rename origin to hostname
MarcoPolo Jul 2, 2024
cda858d
Read hostname from tls session
MarcoPolo Jul 5, 2024
00ad283
Add protocol ID constant
MarcoPolo Jul 9, 2024
e2e85c0
Add marshalled zero key in test
MarcoPolo Jul 9, 2024
1dcf866
Rename PeerIDAuth to ServerPeerIDAuth
MarcoPolo Jul 9, 2024
6e7f554
PR comments
MarcoPolo Jul 19, 2024
3e4198c
WIP
MarcoPolo Aug 23, 2024
cffbec4
Refactor handshake
MarcoPolo Aug 27, 2024
d166a0b
Implement public API
MarcoPolo Aug 28, 2024
80feebc
Nits
MarcoPolo Aug 28, 2024
7ecc2a1
Error if challenge is too short
MarcoPolo Aug 28, 2024
2d8a24f
nit
MarcoPolo Aug 28, 2024
6d8c28c
Mod tidy
MarcoPolo Aug 28, 2024
8e18916
nit
MarcoPolo Aug 28, 2024
8e3f98e
Use a newRequest function rather than shallow clone
MarcoPolo Aug 28, 2024
6bd3799
Add tests to generate examples for specs
MarcoPolo Aug 28, 2024
2fd3b24
Rename InsecureNoTLS. Update comment
MarcoPolo Sep 4, 2024
0da88ec
Add support for client-initiated handshake
MarcoPolo Sep 6, 2024
376128b
Change handshake api a bit
MarcoPolo Sep 6, 2024
aafa743
Add Client Initiated handshake to API
MarcoPolo Sep 6, 2024
1b1163e
Use ValidHostnameFn even with TLS set
MarcoPolo Sep 6, 2024
5c01497
Couple of improvments in internal handshake package
MarcoPolo Sep 9, 2024
68c0253
Clear GetBody as well; simply running handshake
MarcoPolo Sep 9, 2024
ce5529e
Handle case where server refuses client-initiated handshake
MarcoPolo Sep 10, 2024
50a65d0
Fix reference in test
MarcoPolo Sep 10, 2024
889ad26
PR comments
MarcoPolo Sep 10, 2024
f5495ad
Add example for client initated handshake
MarcoPolo Sep 10, 2024
a341297
Export ProtocolID
MarcoPolo Sep 11, 2024
37cb110
Small nits
MarcoPolo Oct 8, 2024
dbca6a3
Check for ErrFinalToken
MarcoPolo Oct 8, 2024
a1aef28
Tweak
MarcoPolo Oct 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions p2p/http/auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package httppeeridauth

import (
logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-libp2p/p2p/http/auth/internal/handshake"
)

const PeerIDAuthScheme = handshake.PeerIDAuthScheme
const ProtocolID = "/http-peer-id-auth/1.0.0"

var log = logging.Logger("http-peer-id-auth")
243 changes: 243 additions & 0 deletions p2p/http/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
package httppeeridauth

import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"hash"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"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/assert"
"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)

type clientTestCase struct {
name string
clientKeyGen func(t *testing.T) crypto.PrivKey
}

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
},
},
{
name: "RSA",
clientKeyGen: func(t *testing.T) crypto.PrivKey {
t.Helper()
clientKey, _, err := crypto.GenerateRSAKeyPair(2048, rand.Reader)
require.NoError(t, err)
return clientKey
},
},
}

type serverTestCase struct {
name string
serverGen func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth)
}

serverTestCases := []serverTestCase{
{
name: "no TLS",
serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) {
t.Helper()
auth := ServerPeerIDAuth{
PrivKey: serverKey,
ValidHostnameFn: func(s string) bool {
return s == "example.com"
},
TokenTTL: time.Hour,
NoTLS: true,
}

ts := httptest.NewServer(&auth)
t.Cleanup(ts.Close)
return ts, &auth
},
},
{
name: "TLS",
serverGen: func(t *testing.T) (*httptest.Server, *ServerPeerIDAuth) {
t.Helper()
auth := ServerPeerIDAuth{
PrivKey: serverKey,
ValidHostnameFn: func(s string) bool {
return s == "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, server := stc.serverGen(t)
client := ts.Client()
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.
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.tm.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)
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.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")

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.tm.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.tm.tokenMap["example.com"])
require.Equal(t, 3, requestsSent(), "should call have sent 3 reqs since our token expired")
})

})
}
}
}

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
}

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
}
Loading
Loading