From 19732daaf59a35b252cd9401361e7329109e7df9 Mon Sep 17 00:00:00 2001 From: William Petit Date: Fri, 22 Feb 2019 17:35:49 +0100 Subject: [PATCH] Redesign authentication protocol --- .gitignore | 4 +- Makefile | 4 +- chi/mount.go | 8 +- chi/mount_test.go | 8 +- client/client.go | 49 +++++++-- client/client_test.go | 44 ++++++++ client/option.go | 21 ++-- cmd/keygen/README.md | 33 ++++++ cmd/keygen/create_key.go | 23 +++++ cmd/keygen/create_token.go | 21 ++++ cmd/keygen/get_public_key.go | 19 ++++ cmd/keygen/main.go | 54 ++++++++++ cmd/keygen/util.go | 96 ++++++++++++++++++ crypto/pem.go | 56 +++++++++- crypto/rsa.go | 34 +++++++ doc/sequence-diagram/advertise.seq | 2 +- doc/sequence-diagram/advertise.svg | 37 +++---- doc/sequence-diagram/ping.seq | 2 +- doc/sequence-diagram/ping.svg | 37 +++---- doc/sequence-diagram/update.seq | 2 +- doc/sequence-diagram/update.svg | 39 +++---- go.mod | 3 + go.sum | 6 ++ modd.conf | 1 + request.go | 8 +- server/advertise_test.go | 158 ----------------------------- server/handler.go | 41 +++++--- server/middleware.go | 152 ++++++++++++++++----------- server/option.go | 16 ++- test/advertise_test.go | 33 ++++-- test/ping_test.go | 58 +++++++++-- test/update_test.go | 77 ++++++++++---- test/util_test.go | 58 +++-------- 33 files changed, 791 insertions(+), 413 deletions(-) create mode 100644 client/client_test.go create mode 100644 cmd/keygen/README.md create mode 100644 cmd/keygen/create_key.go create mode 100644 cmd/keygen/create_token.go create mode 100644 cmd/keygen/get_public_key.go create mode 100644 cmd/keygen/main.go create mode 100644 cmd/keygen/util.go create mode 100644 crypto/rsa.go delete mode 100644 server/advertise_test.go diff --git a/.gitignore b/.gitignore index d4662e1..3061650 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /coverage -/vendor \ No newline at end of file +/vendor +/bin +/testdata \ No newline at end of file diff --git a/Makefile b/Makefile index 4aa5ca5..eb00721 100644 --- a/Makefile +++ b/Makefile @@ -26,5 +26,7 @@ doc: @echo "open your browser to http://localhost:6060/pkg/forge.cadoles.com/wpetit/go-http-peering to see the documentation" godoc -http=:6060 +bin/keygen: + go build -o bin/keygen ./cmd/keygen -.PHONY: test lint doc sequence-diagram \ No newline at end of file +.PHONY: test lint doc sequence-diagram bin/keygen \ No newline at end of file diff --git a/chi/mount.go b/chi/mount.go index 7751c6a..ecdca97 100644 --- a/chi/mount.go +++ b/chi/mount.go @@ -1,15 +1,17 @@ package chi import ( + "crypto/rsa" + peering "forge.cadoles.com/wpetit/go-http-peering" "forge.cadoles.com/wpetit/go-http-peering/server" "github.com/go-chi/chi" ) -func Mount(r chi.Router, store peering.Store, funcs ...server.OptionFunc) { - r.Post(peering.AdvertisePath, server.AdvertiseHandler(store, funcs...)) +func Mount(r chi.Router, store peering.Store, key *rsa.PublicKey, funcs ...server.OptionFunc) { + r.Post(peering.AdvertisePath, server.AdvertiseHandler(store, key, funcs...)) r.Group(func(r chi.Router) { - r.Use(server.Authenticate(store, funcs...)) + r.Use(server.Authenticate(store, key, funcs...)) r.Post(peering.UpdatePath, server.UpdateHandler(store, funcs...)) r.Post(peering.PingPath, server.PingHandler(store, funcs...)) }) diff --git a/chi/mount_test.go b/chi/mount_test.go index f153b8e..d07497f 100644 --- a/chi/mount_test.go +++ b/chi/mount_test.go @@ -4,6 +4,7 @@ import ( "testing" peering "forge.cadoles.com/wpetit/go-http-peering" + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" "forge.cadoles.com/wpetit/go-http-peering/memory" "github.com/go-chi/chi" ) @@ -13,7 +14,12 @@ func TestMount(t *testing.T) { r := chi.NewRouter() store := memory.NewStore() - Mount(r, store) + pk, err := peeringCrypto.CreateRSAKey(1024) + if err != nil { + t.Fatal(err) + } + + Mount(r, store, &pk.PublicKey) routes := r.Routes() diff --git a/client/client.go b/client/client.go index 339d18f..4bceba9 100644 --- a/client/client.go +++ b/client/client.go @@ -2,10 +2,10 @@ package client import ( "bytes" + "crypto/rsa" "crypto/sha256" "encoding/json" "errors" - "fmt" "net/http" "time" @@ -25,6 +25,7 @@ var ( ErrUnexpectedResponse = errors.New("unexpected response") ErrUnauthorized = errors.New("unauthorized") ErrRejected = errors.New("rejected") + ErrInvalidServerToken = errors.New("invalid server token") ) type Client struct { @@ -40,18 +41,15 @@ func (c *Client) Advertise(attrs peering.PeerAttributes) error { } data := &peering.AdvertisingRequest{ - ID: c.options.PeerID, Attributes: attrs, PublicKey: publicKey, } - req, _, err := c.createPostRequest(url, data) + res, err := c.Post(url, data) if err != nil { return err } - res, err := c.options.HTTPClient.Do(req) - switch res.StatusCode { case http.StatusCreated: return nil @@ -111,7 +109,7 @@ func (c *Client) Get(url string) (*http.Response, error) { if err != nil { return nil, err } - if err := c.signRequest(req, nil); err != nil { + if err := c.addClientToken(req, nil); err != nil { return nil, err } return c.options.HTTPClient.Do(req) @@ -122,7 +120,7 @@ func (c *Client) Post(url string, data interface{}) (*http.Response, error) { if err != nil { return nil, err } - if err := c.signRequest(req, body); err != nil { + if err := c.addClientToken(req, body); err != nil { return nil, err } return c.options.HTTPClient.Do(req) @@ -144,16 +142,22 @@ func (c *Client) createPostRequest(url string, data interface{}) (*http.Request, return req, body, nil } -func (c *Client) signRequest(r *http.Request, body []byte) error { +func (c *Client) addServerToken(r *http.Request) { + r.Header.Set( + server.ServerTokenHeader, + c.options.ServerToken, + ) +} + +func (c *Client) addClientToken(r *http.Request, body []byte) error { bodySum, err := c.createBodySum(body) if err != nil { return err } - token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.PeerClaims{ + token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.ClientTokenClaims{ StandardClaims: jwt.StandardClaims{ NotBefore: time.Now().Unix(), - Issuer: string(c.options.PeerID), ExpiresAt: time.Now().Add(time.Minute * 10).Unix(), }, BodySum: bodySum, @@ -164,7 +168,9 @@ func (c *Client) signRequest(r *http.Request, body []byte) error { return err } - r.Header.Set("Authorization", fmt.Sprintf("%s %s", server.AuthorizationType, tokenStr)) + r.Header.Set(server.ClientTokenHeader, tokenStr) + + c.addServerToken(r) return nil } @@ -181,6 +187,27 @@ func (c *Client) createBodySum(body []byte) ([]byte, error) { return sha.Sum(nil), nil } +func (c *Client) PeerID(serverPublicKey *rsa.PublicKey) (peering.PeerID, error) { + token, err := jwt.ParseWithClaims( + c.options.ServerToken, + &peering.ServerTokenClaims{}, + func(token *jwt.Token) (interface{}, error) { + return serverPublicKey, nil + }, + ) + if err != nil { + return "", err + } + if !token.Valid { + return "", ErrInvalidServerToken + } + serverClaims, ok := token.Claims.(*peering.ServerTokenClaims) + if !ok { + return "", ErrInvalidServerToken + } + return serverClaims.PeerID, nil +} + func New(funcs ...OptionFunc) *Client { options := createOptions(funcs...) return &Client{options} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..89edb09 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,44 @@ +package client + +import ( + "crypto/rand" + "crypto/rsa" + "testing" + + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" + + peering "forge.cadoles.com/wpetit/go-http-peering" +) + +func TestClientPeerID(t *testing.T) { + + serverPK := mustGeneratePrivateKey() + peerID := peering.NewPeerID() + + serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID) + if err != nil { + t.Fatal(err) + } + + client := New( + WithServerToken(serverToken), + ) + + clientPeerID, err := client.PeerID(&serverPK.PublicKey) + if err != nil { + t.Fatal(err) + } + + if g, e := clientPeerID, peerID; g != e { + t.Errorf("client.PeerID(): got '%v', expected '%v'", g, e) + } + +} + +func mustGeneratePrivateKey() *rsa.PrivateKey { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } + return privateKey +} diff --git a/client/option.go b/client/option.go index 3961a43..e517830 100644 --- a/client/option.go +++ b/client/option.go @@ -3,22 +3,24 @@ package client import ( "crypto/rsa" "net/http" - - peering "forge.cadoles.com/wpetit/go-http-peering" ) +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + type Options struct { - PeerID peering.PeerID - HTTPClient *http.Client - BaseURL string - PrivateKey *rsa.PrivateKey + HTTPClient HTTPClient + BaseURL string + PrivateKey *rsa.PrivateKey + ServerToken string } type OptionFunc func(*Options) -func WithPeerID(id peering.PeerID) OptionFunc { +func WithServerToken(token string) OptionFunc { return func(opts *Options) { - opts.PeerID = id + opts.ServerToken = token } } @@ -28,7 +30,7 @@ func WithPrivateKey(pk *rsa.PrivateKey) OptionFunc { } } -func WithHTTPClient(client *http.Client) OptionFunc { +func WithHTTPClient(client HTTPClient) OptionFunc { return func(opts *Options) { opts.HTTPClient = client } @@ -43,7 +45,6 @@ func WithBaseURL(url string) OptionFunc { func defaultOptions() *Options { return &Options{ HTTPClient: http.DefaultClient, - PeerID: peering.NewPeerID(), } } diff --git a/cmd/keygen/README.md b/cmd/keygen/README.md new file mode 100644 index 0000000..2237581 --- /dev/null +++ b/cmd/keygen/README.md @@ -0,0 +1,33 @@ +# keygen + +Utilitaire de génération de jetons d'authentifications. + +## Usage + +### Créer une nouvelle clé privée + +``` +bin/keygen -create-key +``` + +### Récupérer la clé publique associée à une clé privée précedemment créée + +``` +bin/keygen -get-public-key -key chemin/vers/clé/privée +``` + +### Générer un jeton d'authentification à partir d'une clé privée + +``` +bin/keygen -create-token -key chemin/vers/clé/privée +``` + +### Afficher l'aide + +``` +bin/keygen -help +``` + +## Mode sans interaction + +Les commandes nécessitant l'entrée d'une phrase de passe peuvent utiliser la variable d'environnement `KEY_PASSPHRASE` pour fonctionner sans interaction. \ No newline at end of file diff --git a/cmd/keygen/create_key.go b/cmd/keygen/create_key.go new file mode 100644 index 0000000..73bcbd5 --- /dev/null +++ b/cmd/keygen/create_key.go @@ -0,0 +1,23 @@ +package main + +import ( + "fmt" + + "forge.cadoles.com/wpetit/go-http-peering/crypto" +) + +func createKey() { + passphrase, err := getPassphrase() + if err != nil { + handleError(err) + } + key, err := crypto.CreateRSAKey(keySize) + if err != nil { + handleError(err) + } + privatePEM, err := crypto.EncodePrivateKeyToEncryptedPEM(key, passphrase) + if err != nil { + handleError(err) + } + fmt.Print(string(privatePEM)) +} diff --git a/cmd/keygen/create_token.go b/cmd/keygen/create_token.go new file mode 100644 index 0000000..e7aff07 --- /dev/null +++ b/cmd/keygen/create_token.go @@ -0,0 +1,21 @@ +package main + +import ( + "fmt" + + "forge.cadoles.com/wpetit/go-http-peering/crypto" + + peering "forge.cadoles.com/wpetit/go-http-peering" +) + +func createToken() { + privateKey, err := loadPrivateKey() + if err != nil { + handleError(err) + } + token, err := crypto.CreateServerToken(privateKey, tokenIssuer, peering.PeerID(tokenPeerID)) + if err != nil { + handleError(err) + } + fmt.Println(token) +} diff --git a/cmd/keygen/get_public_key.go b/cmd/keygen/get_public_key.go new file mode 100644 index 0000000..eecbf89 --- /dev/null +++ b/cmd/keygen/get_public_key.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + + "forge.cadoles.com/wpetit/go-http-peering/crypto" +) + +func getPublicKey() { + privateKey, err := loadPrivateKey() + if err != nil { + handleError(err) + } + publicPEM, err := crypto.EncodePublicKeyToPEM(privateKey.Public()) + if err != nil { + handleError(err) + } + fmt.Print(string(publicPEM)) +} diff --git a/cmd/keygen/main.go b/cmd/keygen/main.go new file mode 100644 index 0000000..8e629da --- /dev/null +++ b/cmd/keygen/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "flag" + + "github.com/pborman/uuid" +) + +// nolint:gochecknoglobals +var ( + createKeyCmd = false + getPublicKeyCmd = false + createTokenCmd = false + debug = false + keyFile string + tokenIssuer string + tokenPeerID = uuid.New() + keySize = 2048 +) + +// nolint:gochecknoinits +func init() { + flag.BoolVar( + &createKeyCmd, "create-key", createKeyCmd, + "Create a new encrypted PEM private key to sign authentication tokens", + ) + flag.BoolVar( + &createTokenCmd, "create-token", createTokenCmd, + "Create a new signed authentication token", + ) + flag.BoolVar( + &getPublicKeyCmd, "get-public-key", getPublicKeyCmd, + "Get the PEM encoded public key associated with the private key", + ) + flag.BoolVar(&debug, "debug", debug, "Debug mode") + flag.StringVar(&keyFile, "key", keyFile, "Path to the encrypted PEM encoded key") + flag.StringVar(&tokenIssuer, "token-issuer", tokenIssuer, "Token issuer") + flag.StringVar(&tokenPeerID, "token-peer-id", tokenPeerID, "Token peer ID") + flag.IntVar(&keySize, "key-size", keySize, "Size of the private key") +} + +func main() { + flag.Parse() + switch { + case createKeyCmd: + createKey() + case getPublicKeyCmd: + getPublicKey() + case createTokenCmd: + createToken() + default: + flag.Usage() + } +} diff --git a/cmd/keygen/util.go b/cmd/keygen/util.go new file mode 100644 index 0000000..49f19e5 --- /dev/null +++ b/cmd/keygen/util.go @@ -0,0 +1,96 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "os" + "syscall" + + "forge.cadoles.com/wpetit/go-http-peering/crypto" + + "golang.org/x/crypto/ssh/terminal" +) + +func getPassphrase() ([]byte, error) { + passphrase := os.Getenv("KEY_PASSPHRASE") + if passphrase != "" { + return []byte(passphrase), nil + } + return askPassphrase() +} + +func askPassphrase() ([]byte, error) { + fmt.Print("Passphrase: ") + passphrase, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return nil, err + } + fmt.Println() + + fmt.Print("Confirm passphrase: ") + passphraseConfirmation, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return nil, err + } + fmt.Println() + + if !bytes.Equal(passphrase, passphraseConfirmation) { + return nil, errors.New("passphrases does not match") + } + + return passphrase, nil +} + +func privateKeyToEncryptedPEM(key *rsa.PrivateKey, passphrase []byte) ([]byte, error) { + + if passphrase == nil { + return nil, errors.New("passphrase cannot be empty") + } + + // Convert it to pem + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + block, err := x509.EncryptPEMBlock(rand.Reader, block.Type, block.Bytes, passphrase, x509.PEMCipherAES256) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(block), nil +} + +func loadPrivateKey() (*rsa.PrivateKey, error) { + if keyFile == "" { + return nil, errors.New("you must specify a key file to load") + } + pem, err := ioutil.ReadFile(keyFile) + if err != nil { + return nil, err + } + passphrase, err := getPassphrase() + if err != nil { + return nil, err + } + privateKey, err := crypto.DecodePEMEncryptedPrivateKey(pem, passphrase) + if err != nil { + return nil, err + } + return privateKey, nil +} + +func handleError(err error) { + if !debug { + fmt.Println(err) + } else { + panic(err) + } + os.Exit(1) +} diff --git a/crypto/pem.go b/crypto/pem.go index eeb398d..3ee19c2 100644 --- a/crypto/pem.go +++ b/crypto/pem.go @@ -1,14 +1,17 @@ package crypto import ( + "crypto" + "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" jwt "github.com/dgrijalva/jwt-go" ) -func EncodePublicKeyToPEM(key interface{}) ([]byte, error) { +func EncodePublicKeyToPEM(key crypto.PublicKey) ([]byte, error) { pub, err := x509.MarshalPKIXPublicKey(key) if err != nil { return nil, err @@ -20,6 +23,55 @@ func EncodePublicKeyToPEM(key interface{}) ([]byte, error) { return data, nil } -func DecodePEMToPublicKey(pem []byte) (*rsa.PublicKey, error) { +func DecodePEMToPublicKey(pem []byte) (crypto.PublicKey, error) { return jwt.ParseRSAPublicKeyFromPEM(pem) } + +func DecodePEMEncryptedPrivateKey(key []byte, passphrase []byte) (*rsa.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, errors.New("invalid PEM block") + } + + decryptedBlock, err := x509.DecryptPEMBlock(block, passphrase) + if err != nil { + return nil, err + } + + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS1PrivateKey(decryptedBlock); err != nil { + return nil, err + } + + var privateKey *rsa.PrivateKey + var ok bool + if privateKey, ok = parsedKey.(*rsa.PrivateKey); !ok { + return nil, errors.New("invalid RSA private key") + } + + return privateKey, nil +} + +func EncodePrivateKeyToEncryptedPEM(key *rsa.PrivateKey, passphrase []byte) ([]byte, error) { + if passphrase == nil { + return nil, errors.New("passphrase cannot be empty") + } + + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + block, err := x509.EncryptPEMBlock( + rand.Reader, block.Type, + block.Bytes, passphrase, x509.PEMCipherAES256, + ) + if err != nil { + return nil, err + } + + return pem.EncodeToMemory(block), nil +} diff --git a/crypto/rsa.go b/crypto/rsa.go new file mode 100644 index 0000000..ff2184d --- /dev/null +++ b/crypto/rsa.go @@ -0,0 +1,34 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" + "time" + + peering "forge.cadoles.com/wpetit/go-http-peering" + + jwt "github.com/dgrijalva/jwt-go" +) + +func CreateRSAKey(bits int) (*rsa.PrivateKey, error) { + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, err + } + return key, nil +} + +func CreateServerToken(privateKey *rsa.PrivateKey, issuer string, peerID peering.PeerID) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodRS256, peering.ServerTokenClaims{ + StandardClaims: jwt.StandardClaims{ + NotBefore: time.Now().Unix(), + Issuer: issuer, + }, + PeerID: peerID, + }) + tokenStr, err := token.SignedString(privateKey) + if err != nil { + return "", err + } + return tokenStr, nil +} diff --git a/doc/sequence-diagram/advertise.seq b/doc/sequence-diagram/advertise.seq index 0c52219..1f276b5 100644 --- a/doc/sequence-diagram/advertise.seq +++ b/doc/sequence-diagram/advertise.seq @@ -1,2 +1,2 @@ -Client -> Server: POST /advertise\n\n{"ID": , "Attributes": , "PublicKey": } +Client -> Server: POST /advertise\nX-Server-Token: \n\n{"Attributes": , "PublicKey": } Server -> Client: 201 Created \ No newline at end of file diff --git a/doc/sequence-diagram/advertise.svg b/doc/sequence-diagram/advertise.svg index ae856ae..afc422e 100644 --- a/doc/sequence-diagram/advertise.svg +++ b/doc/sequence-diagram/advertise.svg @@ -1,6 +1,6 @@ - @@ -13,23 +13,24 @@ } - + Client - -Client - - -Server - -Server - -POST /advertise -{"ID": <PEER_ID>, "Attributes": <PEER_ATTRIBUTES>, "PublicKey": <PUBLIC_KEY> } - - - -201 Created - - + +Client + + +Server + +Server + +POST /advertise +X-Server-Token: <JWT_TOKEN> +{"Attributes": <PEER_ATTRIBUTES>, "PublicKey": <PUBLIC_KEY> } + + + +201 Created + + diff --git a/doc/sequence-diagram/ping.seq b/doc/sequence-diagram/ping.seq index 22e7153..facd0d3 100644 --- a/doc/sequence-diagram/ping.seq +++ b/doc/sequence-diagram/ping.seq @@ -1,2 +1,2 @@ -Client -> Server: POST /ping\nAuthorization: Bearer +Client -> Server: POST /ping\nX-Server-Token: \nX-Client-Token: Server -> Client: 204 No Content \ No newline at end of file diff --git a/doc/sequence-diagram/ping.svg b/doc/sequence-diagram/ping.svg index 6c01e44..c8de985 100644 --- a/doc/sequence-diagram/ping.svg +++ b/doc/sequence-diagram/ping.svg @@ -1,6 +1,6 @@ - @@ -13,23 +13,24 @@ } - + Client - -Client - - -Server - -Server - -POST /ping -Authorization: Bearer <JWT_SIGNING_TOKEN> - - - -204 No Content - - + +Client + + +Server + +Server + +POST /ping +X-Server-Token: <JWT_TOKEN> +X-Client-Token: <JWT_TOKEN> + + + +204 No Content + + diff --git a/doc/sequence-diagram/update.seq b/doc/sequence-diagram/update.seq index 652f0af..e882977 100644 --- a/doc/sequence-diagram/update.seq +++ b/doc/sequence-diagram/update.seq @@ -1,2 +1,2 @@ -Client -> Server: POST /update\nAuthorization: Bearer \n\n{"Attributes": } +Client -> Server: POST /update\nX-Server-Token: \nX-Client-Token: \n\n{"Attributes": } Server -> Client: 204 No Content \ No newline at end of file diff --git a/doc/sequence-diagram/update.svg b/doc/sequence-diagram/update.svg index 7f29b61..e2cedfd 100644 --- a/doc/sequence-diagram/update.svg +++ b/doc/sequence-diagram/update.svg @@ -1,6 +1,6 @@ - @@ -13,24 +13,25 @@ } - + Client - -Client - - -Server - -Server - -POST /update -Authorization: Bearer <JWT_SIGNING_TOKEN> -{"Attributes": <PEER_ATTRIBUTES>} - - - -204 No Content - - + +Client + + +Server + +Server + +POST /update +X-Server-Token: <JWT_TOKEN> +X-Client-Token: <JWT_TOKEN> +{"Attributes": <PEER_ATTRIBUTES>} + + + +204 No Content + + diff --git a/go.mod b/go.mod index 7cc9f9d..ce2a4ed 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,10 @@ module forge.cadoles.com/wpetit/go-http-peering require ( + github.com/davecgh/go-spew v1.1.1 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-chi/chi v3.3.3+incompatible github.com/pborman/uuid v1.2.0 + golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 + golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 // indirect ) diff --git a/go.sum b/go.sum index f1b73c1..36239e5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8= @@ -6,3 +8,7 @@ github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2 h1:NwxKRvbkH5MsNkvOtPZi3/3kmI8CAzs3mtv+GLQMkNo= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/modd.conf b/modd.conf index 5702665..f9dc4d4 100644 --- a/modd.conf +++ b/modd.conf @@ -1,6 +1,7 @@ **/*.go !vendor/**.go { prep: make test + prep: make bin/keygen } doc/sequence-diagram/*.seq { diff --git a/request.go b/request.go index a1a9cd9..c914c54 100644 --- a/request.go +++ b/request.go @@ -9,7 +9,6 @@ const ( ) type AdvertisingRequest struct { - ID PeerID Attributes PeerAttributes PublicKey []byte } @@ -18,7 +17,12 @@ type UpdateRequest struct { Attributes PeerAttributes } -type PeerClaims struct { +type ClientTokenClaims struct { jwt.StandardClaims BodySum []byte `json:"bodySum"` } + +type ServerTokenClaims struct { + jwt.StandardClaims + PeerID PeerID `json:"peerID"` +} diff --git a/server/advertise_test.go b/server/advertise_test.go deleted file mode 100644 index 46de11c..0000000 --- a/server/advertise_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "forge.cadoles.com/wpetit/go-http-peering/crypto" - - peering "forge.cadoles.com/wpetit/go-http-peering" - "forge.cadoles.com/wpetit/go-http-peering/memory" -) - -func TestAdvertiseHandlerBadRequest(t *testing.T) { - store := memory.NewStore() - handler := AdvertiseHandler(store) - - req := httptest.NewRequest("POST", peering.AdvertisePath, nil) - w := httptest.NewRecorder() - - handler(w, req) - - res := w.Result() - - if g, e := res.StatusCode, http.StatusBadRequest; g != e { - t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e) - } - - peers, err := store.List() - if err != nil { - t.Fatal(err) - } - - if g, e := len(peers), 0; g != e { - t.Errorf("len(peers): got '%v', expected '%v'", g, e) - } -} - -func TestAdvertiseHandlerInvalidPublicKeyFormat(t *testing.T) { - store := memory.NewStore() - handler := AdvertiseHandler(store) - - advertising := &peering.AdvertisingRequest{ - ID: peering.NewPeerID(), - PublicKey: []byte("Test"), - } - - body, err := json.Marshal(advertising) - if err != nil { - t.Fatal(err) - } - - req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body)) - w := httptest.NewRecorder() - - handler(w, req) - - res := w.Result() - - if g, e := res.StatusCode, http.StatusBadRequest; g != e { - t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e) - } - - peers, err := store.List() - if err != nil { - t.Fatal(err) - } - - if g, e := len(peers), 0; g != e { - t.Errorf("len(peers): got '%v', expected '%v'", g, e) - } -} - -func TestAdvertiseHandlerExistingPeer(t *testing.T) { - store := memory.NewStore() - handler := AdvertiseHandler(store) - - pk := mustGeneratePrivateKey() - pem, err := crypto.EncodePublicKeyToPEM(pk.Public()) - if err != nil { - t.Fatal(err) - } - - peerID := peering.NewPeerID() - - advertising := &peering.AdvertisingRequest{ - ID: peerID, - PublicKey: pem, - } - - body, err := json.Marshal(advertising) - if err != nil { - t.Fatal(err) - } - - req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body)) - w := httptest.NewRecorder() - - handler(w, req) - - req = httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body)) - w = httptest.NewRecorder() - - handler(w, req) - - res := w.Result() - - if g, e := res.StatusCode, http.StatusConflict; g != e { - t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e) - } - -} - -func TestAdvertiseHandlerValidRequest(t *testing.T) { - store := memory.NewStore() - handler := AdvertiseHandler(store) - - pk := mustGeneratePrivateKey() - pem, err := crypto.EncodePublicKeyToPEM(pk.Public()) - if err != nil { - t.Fatal(err) - } - - peerID := peering.NewPeerID() - - advertising := &peering.AdvertisingRequest{ - ID: peerID, - PublicKey: pem, - } - - body, err := json.Marshal(advertising) - if err != nil { - t.Fatal(err) - } - - req := httptest.NewRequest("POST", peering.AdvertisePath, bytes.NewReader(body)) - w := httptest.NewRecorder() - - handler(w, req) - - res := w.Result() - - if g, e := res.StatusCode, http.StatusCreated; g != e { - t.Errorf("res.StatusCode: got '%v', expected '%v'", g, e) - } - - peer, err := store.Get(peerID) - if err != nil { - t.Fatal(err) - } - - if g, e := peer.PublicKey, advertising.PublicKey; !bytes.Equal(peer.PublicKey, advertising.PublicKey) { - t.Errorf("peer.PublicKey: got '%v', expected '%v'", g, e) - } - -} diff --git a/server/handler.go b/server/handler.go index b07d50a..bf1cd79 100644 --- a/server/handler.go +++ b/server/handler.go @@ -1,13 +1,14 @@ package server import ( + "crypto/rsa" "encoding/json" "errors" "net/http" "time" peering "forge.cadoles.com/wpetit/go-http-peering" - "forge.cadoles.com/wpetit/go-http-peering/crypto" + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" ) var ( @@ -18,12 +19,26 @@ var ( ErrUnauthorized = errors.New("unauthorized") ) -func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc { +func AdvertiseHandler(store peering.Store, key *rsa.PublicKey, funcs ...OptionFunc) http.HandlerFunc { options := createOptions(funcs...) logger := options.Logger handler := func(w http.ResponseWriter, r *http.Request) { + + serverToken := r.Header.Get(ServerTokenHeader) + if serverToken == "" { + options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest) + return + } + + serverClaims, err := assertServerToken(key, serverToken) + if err != nil { + logger.Printf("[ERROR] %s", err) + sendError(w, http.StatusUnauthorized) + return + } + advertising := &peering.AdvertisingRequest{} decoder := json.NewDecoder(r.Body) @@ -33,19 +48,13 @@ func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc return } - if !options.PeerIDValidator(advertising.ID) { - logger.Printf("[ERROR] %s", ErrInvalidAdvertisingRequest) - options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest) - return - } - - if _, err := crypto.DecodePEMToPublicKey(advertising.PublicKey); err != nil { + if _, err := peeringCrypto.DecodePEMToPublicKey(advertising.PublicKey); err != nil { logger.Printf("[ERROR] %s", err) options.ErrorHandler(w, r, ErrInvalidAdvertisingRequest) return } - peer, err := store.Get(advertising.ID) + peer, err := store.Get(serverClaims.PeerID) if err == nil { logger.Printf("[ERROR] %s", ErrPeerIDAlreadyInUse) @@ -61,7 +70,7 @@ func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc attrs := filterAttributes(options.PeerAttributes, advertising.Attributes) - peer, err = store.Create(advertising.ID, attrs) + peer, err = store.Create(serverClaims.PeerID, attrs) if err != nil { logger.Printf("[ERROR] %s", err) options.ErrorHandler(w, r, err) @@ -74,6 +83,12 @@ func AdvertiseHandler(store peering.Store, funcs ...OptionFunc) http.HandlerFunc return } + if err := store.UpdateLastContact(peer.ID, r.RemoteAddr, time.Now()); err != nil { + logger.Printf("[ERROR] %s", err) + options.ErrorHandler(w, r, err) + return + } + if err := store.UpdatePublicKey(peer.ID, advertising.PublicKey); err != nil { logger.Printf("[ERROR] %s", err) options.ErrorHandler(w, r, err) @@ -212,10 +227,6 @@ func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) { } } -func DefaultPeerIDValidator(id peering.PeerID) bool { - return string(id) != "" -} - func filterAttributes(filters []string, attrs peering.PeerAttributes) peering.PeerAttributes { filtered := peering.PeerAttributes{} for _, key := range filters { diff --git a/server/middleware.go b/server/middleware.go index 68a8f6f..9b85bf1 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -3,13 +3,14 @@ package server import ( "bytes" "context" + "crypto/rsa" "crypto/sha256" "errors" + "io" "io/ioutil" - "strings" "time" - "forge.cadoles.com/wpetit/go-http-peering/crypto" + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" peering "forge.cadoles.com/wpetit/go-http-peering" jwt "github.com/dgrijalva/jwt-go" @@ -18,8 +19,9 @@ import ( ) const ( - AuthorizationType = "Bearer" - KeyPeerID ContextKey = "peerID" + ServerTokenHeader = "X-Server-Token" // nolint: gosec + ClientTokenHeader = "X-Client-Token" + KeyPeerID ContextKey = "PeerID" ) var ( @@ -30,100 +32,63 @@ var ( type ContextKey string -func Authenticate(store peering.Store, funcs ...OptionFunc) func(http.Handler) http.Handler { +func Authenticate(store peering.Store, key *rsa.PublicKey, funcs ...OptionFunc) func(http.Handler) http.Handler { options := createOptions(funcs...) logger := options.Logger middleware := func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - authorization := r.Header.Get("Authorization") - if authorization == "" { + serverToken := r.Header.Get(ServerTokenHeader) + if serverToken == "" { sendError(w, http.StatusUnauthorized) return } - parts := strings.SplitN(authorization, " ", 2) - - if len(parts) != 2 || parts[0] != AuthorizationType { + clientToken := r.Header.Get(ClientTokenHeader) + if clientToken == "" { sendError(w, http.StatusUnauthorized) return } - token, err := jwt.ParseWithClaims(parts[1], &peering.PeerClaims{}, func(token *jwt.Token) (interface{}, error) { - claims, ok := token.Claims.(*peering.PeerClaims) - if !ok { - return nil, ErrInvalidClaims - } - peerID := peering.PeerID(claims.Issuer) - peer, err := store.Get(peerID) - if err != nil { - return nil, err - } - if peer.Status == peering.StatusRejected { - return nil, ErrPeerRejected - } - if peer.Status != peering.StatusPeered { - return nil, ErrNotPeered - } - publicKey, err := crypto.DecodePEMToPublicKey(peer.PublicKey) - if err != nil { - return nil, err - } - return publicKey, nil - }) - if err != nil || !token.Valid { + serverClaims, err := assertServerToken(key, serverToken) + if err != nil { logger.Printf("[ERROR] %s", err) - if err == ErrPeerRejected { - sendError(w, http.StatusForbidden) - } else { + sendError(w, http.StatusUnauthorized) + return + } + + clientClaims, err := assertClientToken(serverClaims.PeerID, store, clientToken) + if err != nil { + logger.Printf("[ERROR] %s", err) + if err == peering.ErrPeerNotFound { sendError(w, http.StatusUnauthorized) + } else { + sendError(w, http.StatusInternalServerError) } return } - claims, ok := token.Claims.(*peering.PeerClaims) - if !ok { - logger.Printf("[ERROR] %s", ErrInvalidClaims) - sendError(w, http.StatusUnauthorized) - return - } - - body, err := ioutil.ReadAll(r.Body) + match, body, err := assertBodySum(r.Body, clientClaims.BodySum) if err != nil { logger.Printf("[ERROR] %s", err) sendError(w, http.StatusInternalServerError) return } - if err := r.Body.Close(); err != nil { - logger.Printf("[ERROR] %s", err) - sendError(w, http.StatusInternalServerError) - return - } - - match, err := compareChecksum(body, claims.BodySum) - if err != nil { - logger.Printf("[ERROR] %s", err) - sendError(w, http.StatusUnauthorized) - return - } - if !match { logger.Printf("[ERROR] %s", ErrInvalidChecksum) sendError(w, http.StatusBadRequest) return } - peerID := peering.PeerID(claims.Issuer) - - if err := store.UpdateLastContact(peerID, r.RemoteAddr, time.Now()); err != nil { + if err := store.UpdateLastContact(serverClaims.PeerID, r.RemoteAddr, time.Now()); err != nil { logger.Printf("[ERROR] %s", err) sendError(w, http.StatusInternalServerError) return } - ctx := context.WithValue(r.Context(), KeyPeerID, peerID) + ctx := context.WithValue(r.Context(), KeyPeerID, serverClaims.PeerID) r = r.WithContext(ctx) r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) @@ -143,6 +108,71 @@ func GetPeerID(r *http.Request) (peering.PeerID, error) { return peerID, nil } +func assertServerToken(key *rsa.PublicKey, serverToken string) (*peering.ServerTokenClaims, error) { + fn := func(token *jwt.Token) (interface{}, error) { + return key, nil + } + token, err := jwt.ParseWithClaims(serverToken, &peering.ServerTokenClaims{}, fn) + if err != nil { + return nil, err + } + if !token.Valid { + return nil, ErrInvalidClaims + } + claims, ok := token.Claims.(*peering.ServerTokenClaims) + if !ok { + return nil, ErrInvalidClaims + } + return claims, nil +} + +func assertClientToken(peerID peering.PeerID, store peering.Store, clientToken string) (*peering.ClientTokenClaims, error) { + fn := func(token *jwt.Token) (interface{}, error) { + peer, err := store.Get(peerID) + if err != nil { + return nil, err + } + if peer.Status == peering.StatusRejected { + return nil, ErrPeerRejected + } + if peer.Status != peering.StatusPeered { + return nil, ErrNotPeered + } + publicKey, err := peeringCrypto.DecodePEMToPublicKey(peer.PublicKey) + if err != nil { + return nil, err + } + return publicKey, nil + } + token, err := jwt.ParseWithClaims(clientToken, &peering.ClientTokenClaims{}, fn) + if err != nil { + return nil, err + } + if !token.Valid { + return nil, ErrInvalidClaims + } + claims, ok := token.Claims.(*peering.ClientTokenClaims) + if !ok { + return nil, ErrInvalidClaims + } + return claims, nil +} + +func assertBodySum(rc io.ReadCloser, bodySum []byte) (bool, []byte, error) { + body, err := ioutil.ReadAll(rc) + if err != nil { + return false, nil, err + } + if err := rc.Close(); err != nil { + return false, nil, err + } + match, err := compareChecksum(body, bodySum) + if err != nil { + return false, nil, err + } + return match, body, nil +} + func sendError(w http.ResponseWriter, status int) { http.Error(w, http.StatusText(status), status) } diff --git a/server/option.go b/server/option.go index 376cb10..0fa3934 100644 --- a/server/option.go +++ b/server/option.go @@ -4,8 +4,6 @@ import ( "log" "net/http" "os" - - peering "forge.cadoles.com/wpetit/go-http-peering" ) type Logger interface { @@ -13,10 +11,9 @@ type Logger interface { } type Options struct { - PeerAttributes []string - ErrorHandler ErrorHandler - PeerIDValidator func(peering.PeerID) bool - Logger Logger + PeerAttributes []string + ErrorHandler ErrorHandler + Logger Logger } type OptionFunc func(*Options) @@ -44,10 +41,9 @@ func WithErrorHandler(handler ErrorHandler) OptionFunc { func defaultOptions() *Options { logger := log.New(os.Stdout, "[go-http-peering] ", log.LstdFlags|log.Lshortfile) return &Options{ - PeerAttributes: []string{"Label"}, - ErrorHandler: DefaultErrorHandler, - PeerIDValidator: DefaultPeerIDValidator, - Logger: logger, + PeerAttributes: []string{"Label"}, + ErrorHandler: DefaultErrorHandler, + Logger: logger, } } diff --git a/test/advertise_test.go b/test/advertise_test.go index c24c3ea..05c6dc1 100644 --- a/test/advertise_test.go +++ b/test/advertise_test.go @@ -5,6 +5,11 @@ import ( "testing" "time" + "forge.cadoles.com/wpetit/go-http-peering/client" + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" + "forge.cadoles.com/wpetit/go-http-peering/memory" + "forge.cadoles.com/wpetit/go-http-peering/server" + peering "forge.cadoles.com/wpetit/go-http-peering" "forge.cadoles.com/wpetit/go-http-peering/crypto" ) @@ -15,19 +20,35 @@ func TestAdvertise(t *testing.T) { t.SkipNow() } - id, pk, client, store := setup(t) + store := memory.NewStore() + serverPK := mustGeneratePrivateKey() + clientPK := mustGeneratePrivateKey() + peerID := peering.NewPeerID() + + serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID) + if err != nil { + t.Fatal(err) + } + + advertise := server.AdvertiseHandler(store, &serverPK.PublicKey) + + client := client.New( + client.WithHTTPClient(NewHTTPClientMock(advertise)), + client.WithPrivateKey(clientPK), + client.WithServerToken(serverToken), + ) attrs := peering.PeerAttributes{} if err := client.Advertise(attrs); err != nil { t.Fatal(err) } - peer, err := store.Get(id) + peer, err := store.Get(peerID) if err != nil { t.Error(err) } - if g, e := peer.ID, id; g != e { + if g, e := peer.ID, peerID; g != e { t.Errorf("peer.ID: got '%v', expected '%v'", g, e) } @@ -40,11 +61,7 @@ func TestAdvertise(t *testing.T) { t.Error("peer.LastContact should not be time.Time zero value") } - if peer.LastAddress == "" { - t.Error("peer.LastAddress should not be empty") - } - - pem, err := crypto.EncodePublicKeyToPEM(pk.Public()) + pem, err := crypto.EncodePublicKeyToPEM(clientPK.Public()) if err != nil { t.Fatal(err) } diff --git a/test/ping_test.go b/test/ping_test.go index 5a86f45..069714d 100644 --- a/test/ping_test.go +++ b/test/ping_test.go @@ -4,6 +4,10 @@ import ( "testing" peering "forge.cadoles.com/wpetit/go-http-peering" + "forge.cadoles.com/wpetit/go-http-peering/client" + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" + "forge.cadoles.com/wpetit/go-http-peering/memory" + "forge.cadoles.com/wpetit/go-http-peering/server" ) func TestPing(t *testing.T) { @@ -12,33 +16,67 @@ func TestPing(t *testing.T) { t.SkipNow() } - id, _, client, store := setup(t) + store := memory.NewStore() + serverPK := mustGeneratePrivateKey() + clientPK := mustGeneratePrivateKey() + peerID := peering.NewPeerID() - attrs := peering.PeerAttributes{} - if err := client.Advertise(attrs); err != nil { - t.Fatal(err) - } - - peer, err := store.Get(id) + // Generate a server token for the peer client + serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID) if err != nil { t.Fatal(err) } + advertise := server.AdvertiseHandler(store, &serverPK.PublicKey) + // Create advertise client + c := client.New( + client.WithHTTPClient(NewHTTPClientMock(advertise)), + client.WithPrivateKey(clientPK), + client.WithServerToken(serverToken), + ) + + // Advertise client with empty peer attributes + attrs := peering.PeerAttributes{} + if err := c.Advertise(attrs); err != nil { + t.Fatal(err) + } + + // Retrieve peer from store + peer, err := store.Get(peerID) + if err != nil { + t.Fatal(err) + } + + // Store last contact after advertising lastContact := peer.LastContact - if err := store.Accept(id); err != nil { + // Accept peer + if err := store.Accept(peerID); err != nil { t.Error(err) } - if err := client.Ping(); err != nil { + // Create ping authenticated handler + ping := server.Authenticate(store, &serverPK.PublicKey)(server.PingHandler(store)) + + // Create client + c = client.New( + client.WithHTTPClient(NewHTTPClientMock(ping)), + client.WithPrivateKey(clientPK), + client.WithServerToken(serverToken), + ) + + // Do ping + if err := c.Ping(); err != nil { t.Fatal(err) } - peer, err = store.Get(id) + // Retrieve peer + peer, err = store.Get(peerID) if err != nil { t.Fatal(err) } + // Assert that last contact has changed after ping if peer.LastContact == lastContact { t.Error("peer.LastContact should have been updated") } diff --git a/test/update_test.go b/test/update_test.go index e940edb..9c21195 100644 --- a/test/update_test.go +++ b/test/update_test.go @@ -6,7 +6,11 @@ import ( "time" peering "forge.cadoles.com/wpetit/go-http-peering" + "forge.cadoles.com/wpetit/go-http-peering/client" "forge.cadoles.com/wpetit/go-http-peering/crypto" + peeringCrypto "forge.cadoles.com/wpetit/go-http-peering/crypto" + "forge.cadoles.com/wpetit/go-http-peering/memory" + "forge.cadoles.com/wpetit/go-http-peering/server" ) func TestUpdate(t *testing.T) { @@ -15,42 +19,77 @@ func TestUpdate(t *testing.T) { t.SkipNow() } - id, pk, client, store := setup(t) + store := memory.NewStore() + serverPK := mustGeneratePrivateKey() + clientPK := mustGeneratePrivateKey() + peerID := peering.NewPeerID() - attrs := peering.PeerAttributes{} - - if err := client.Advertise(attrs); err != nil { - t.Fatal(err) - } - - if err := store.Accept(id); err != nil { - t.Error(err) - } - - attrs["Label"] = "Foo Bar" - if err := client.UpdateAttributes(attrs); err != nil { - t.Fatal(err) - } - - peer, err := store.Get(id) + // Generate a server token for the peer client + serverToken, err := peeringCrypto.CreateServerToken(serverPK, "test", peerID) if err != nil { t.Fatal(err) } - if g, e := peer.ID, id; g != e { + advertise := server.AdvertiseHandler(store, &serverPK.PublicKey) + // Create advertise client + c := client.New( + client.WithHTTPClient(NewHTTPClientMock(advertise)), + client.WithPrivateKey(clientPK), + client.WithServerToken(serverToken), + ) + + // Advertise client with empty peer attributes + attrs := peering.PeerAttributes{} + if err := c.Advertise(attrs); err != nil { + t.Fatal(err) + } + + // Accept peer + if err := store.Accept(peerID); err != nil { + t.Error(err) + } + + // Create authenticated update handler + update := server.Authenticate(store, &serverPK.PublicKey)(server.UpdateHandler(store)) + + // Create update client + c = client.New( + client.WithHTTPClient(NewHTTPClientMock(update)), + client.WithPrivateKey(clientPK), + client.WithServerToken(serverToken), + ) + + // Update local attributes + attrs["Label"] = "Foo Bar" + + // Update attributes + if err := c.UpdateAttributes(attrs); err != nil { + t.Fatal(err) + } + + // Retrieve peer from store + peer, err := store.Get(peerID) + if err != nil { + t.Fatal(err) + } + + // Assert that peer's ID did not change + if g, e := peer.ID, peerID; g != e { t.Errorf("peer.ID: got '%v', expected '%v'", g, e) } + // Assert that stored attributes are the same as the local ones if g, e := peer.Attributes, attrs; !reflect.DeepEqual(g, e) { t.Errorf("peer.Attributes: got '%v', expected '%v'", g, e) } + // Assert that lastContact has changed var defaultTime time.Time if peer.LastContact == defaultTime { t.Error("peer.LastContact should not be time.Time zero value") } - pem, err := crypto.EncodePublicKeyToPEM(pk.Public()) + pem, err := crypto.EncodePublicKeyToPEM(clientPK.Public()) if err != nil { t.Fatal(err) } diff --git a/test/util_test.go b/test/util_test.go index dfd7978..90e09de 100644 --- a/test/util_test.go +++ b/test/util_test.go @@ -3,15 +3,8 @@ package test import ( "crypto/rand" "crypto/rsa" - "fmt" - "net" "net/http" - "testing" - - peering "forge.cadoles.com/wpetit/go-http-peering" - "forge.cadoles.com/wpetit/go-http-peering/client" - "forge.cadoles.com/wpetit/go-http-peering/memory" - "forge.cadoles.com/wpetit/go-http-peering/server" + "net/http/httptest" ) func mustGeneratePrivateKey() *rsa.PrivateKey { @@ -22,43 +15,22 @@ func mustGeneratePrivateKey() *rsa.PrivateKey { return privateKey } -func startServer(store peering.Store) (int, error) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - return -1, err - } - mux := createServerMux(store) - go http.Serve(listener, mux) - port := listener.Addr().(*net.TCPAddr).Port - return port, nil +type HTTPClientMock struct { + handler http.Handler + recorder *httptest.ResponseRecorder } -func createServerMux(store peering.Store) *http.ServeMux { - mux := http.NewServeMux() - mux.HandleFunc(peering.AdvertisePath, server.AdvertiseHandler(store)) - update := server.Authenticate(store)(server.UpdateHandler(store)) - mux.Handle(peering.UpdatePath, update) - ping := server.Authenticate(store)(server.PingHandler(store)) - mux.Handle(peering.PingPath, ping) - return mux +func (c *HTTPClientMock) Do(r *http.Request) (*http.Response, error) { + w := httptest.NewRecorder() + c.recorder = w + c.handler.ServeHTTP(w, r) + return w.Result(), nil } -func setup(t *testing.T) (peering.PeerID, *rsa.PrivateKey, *client.Client, peering.Store) { - store := memory.NewStore() - - port, err := startServer(store) - if err != nil { - t.Fatal(err) - } - - pk := mustGeneratePrivateKey() - id := peering.NewPeerID() - - c := client.New( - client.WithBaseURL(fmt.Sprintf("http://127.0.0.1:%d", port)), - client.WithPrivateKey(pk), - client.WithPeerID(id), - ) - - return id, pk, c, store +func (c *HTTPClientMock) Recorder() *httptest.ResponseRecorder { + return c.recorder +} + +func NewHTTPClientMock(h http.Handler) *HTTPClientMock { + return &HTTPClientMock{h, nil} }