From b8d308763ca7f4aae1529255feecf4cd81e80089 Mon Sep 17 00:00:00 2001 From: Sarah Jamie Lewis Date: Thu, 1 Oct 2020 10:13:45 -0700 Subject: [PATCH] Sign and Check ToFU Server Bundle --- model/errors.go | 15 +++++ model/keyBundle.go | 54 +++++++++++++++- model/keyBundle_test.go | 64 +++++++++++++++++++ peer/cwtch_peer.go | 35 ++++++++-- server/app/main.go | 3 +- server/server.go | 9 +-- testing/cwtch_peer_server_integration_test.go | 4 +- 7 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 model/errors.go create mode 100644 model/keyBundle_test.go diff --git a/model/errors.go b/model/errors.go new file mode 100644 index 0000000..28b230d --- /dev/null +++ b/model/errors.go @@ -0,0 +1,15 @@ +package model + +// Error models some common errors that need to be handled by applications that use Cwtch +type Error string + +// Error is the error interface +func (e Error) Error() string { + return string(e) +} + +// Error definitions +const ( + InvalidEd25519PublicKey = Error("InvalidEd25519PublicKey") + InconsistentKeyBundleError = Error("InconsistentKeyBundleError") +) diff --git a/model/keyBundle.go b/model/keyBundle.go index d5b7765..ef62276 100644 --- a/model/keyBundle.go +++ b/model/keyBundle.go @@ -1,6 +1,13 @@ package model -import "errors" +import ( + "crypto/ed25519" + "cwtch.im/tapir/primitives" + "encoding/base32" + "encoding/json" + "errors" + "strings" +) // KeyType provides a wrapper for a generic public key type identifier (could be an onion address, a zcash address etc.) type KeyType string @@ -21,7 +28,15 @@ type Key string // KeyBundle manages a collection of related keys for various different services. type KeyBundle struct { - Keys map[KeyType]Key + Keys map[KeyType]Key + Signature []byte +} + +// NewKeyBundle creates a new KeyBundle initialized with no keys. +func NewKeyBundle() *KeyBundle { + keyBundle := new(KeyBundle) + keyBundle.Keys = make(map[KeyType]Key) + return keyBundle } // HasKeyType returns true if the bundle has a public key of a given type. @@ -39,6 +54,41 @@ func (kb *KeyBundle) GetKey(keytype KeyType) (Key, error) { return "", errors.New("no such key") } +// Serialize produces a json encoded byte array. +func (kb KeyBundle) Serialize() []byte { + // json.Marshal sorts map keys + bundle, _ := json.Marshal(kb) + return bundle +} + +// Sign allows a server to authenticate a key bundle by signing it (this uses the tapir identity interface) +func (kb *KeyBundle) Sign(identity primitives.Identity) { + kb.Signature = identity.Sign(kb.Serialize()) +} + +// DeserializeAndVerify takes in a json formatted bundle and only returns a valid key bundle +// if it has been signed by the server. +func DeserializeAndVerify(bundle []byte) (*KeyBundle, error) { + keyBundle := new(KeyBundle) + err := json.Unmarshal(bundle, &keyBundle) + if err == nil { + signature := keyBundle.Signature + keyBundle.Signature = nil + serverKey, _ := keyBundle.GetKey(KeyTypeServerOnion) + + // We have to do convert the encoded key to a format that can be used to verify the signature + var decodedPub []byte + decodedPub, err = base32.StdEncoding.DecodeString(strings.ToUpper(string(serverKey))) + if err == nil && len(decodedPub) == 35 { + if ed25519.Verify(decodedPub[:32], keyBundle.Serialize(), signature) == true { + return keyBundle, nil + } + } + err = InvalidEd25519PublicKey + } + return nil, err +} + // AttributeBundle returns a map that can be used as part of a peer attribute bundle func (kb *KeyBundle) AttributeBundle() map[string]string { ab := make(map[string]string) diff --git a/model/keyBundle_test.go b/model/keyBundle_test.go new file mode 100644 index 0000000..83cd4c4 --- /dev/null +++ b/model/keyBundle_test.go @@ -0,0 +1,64 @@ +package model + +import ( + "cwtch.im/tapir/primitives" + "testing" +) + +func TestDeserializeAndVerify(t *testing.T) { + server, _ := primitives.InitializeEphemeralIdentity() + + serverKeyBundle := NewKeyBundle() + + serverKeyBundle.Keys[KeyTypeServerOnion] = Key(server.Hostname()) + serverKeyBundle.Keys[KeyTypePrivacyPass] = Key("random 1") + serverKeyBundle.Keys[KeyTypeTokenOnion] = Key("random 2") + serverKeyBundle.Sign(server) + + //eyeball keys are sorted + t.Logf("%s", serverKeyBundle.Serialize()) + serialize := serverKeyBundle.Serialize() + + newKeyBundle, err := DeserializeAndVerify(serialize) + if err != nil { + t.Fatalf("Key Bundle did not Deserialize %v", err) + } + + if newKeyBundle.Keys[KeyTypeServerOnion] != Key(server.Hostname()) { + t.Fatalf("Key Bundle did not Serialize Correctly Actual: %v Expected: %v", newKeyBundle, serverKeyBundle) + } +} + +func TestDeserializeAndVerifyMaliciousSignShouldFail(t *testing.T) { + server, _ := primitives.InitializeEphemeralIdentity() + maliciousServer, _ := primitives.InitializeEphemeralIdentity() + serverKeyBundle := NewKeyBundle() + + serverKeyBundle.Keys[KeyTypeServerOnion] = Key(server.Hostname()) + + // This time we sign with a malicious server + serverKeyBundle.Sign(maliciousServer) + serialize := serverKeyBundle.Serialize() + + newKeyBundle, err := DeserializeAndVerify(serialize) + if err == nil { + t.Fatalf("Key Bundle did Deserialize (it should have failed): %v", newKeyBundle) + } +} + +func TestDeserializeAndVerifyUnsignedShouldFail(t *testing.T) { + server, _ := primitives.InitializeEphemeralIdentity() + + serverKeyBundle := NewKeyBundle() + + serverKeyBundle.Keys[KeyTypeServerOnion] = Key(server.Hostname()) + + // This time we don't sign + // serverKeyBundle.Sign(server) + serialize := serverKeyBundle.Serialize() + + newKeyBundle, err := DeserializeAndVerify(serialize) + if err == nil { + t.Fatalf("Key Bundle did Deserialize (it should have failed): %v", newKeyBundle) + } +} diff --git a/peer/cwtch_peer.go b/peer/cwtch_peer.go index bc5df1e..109bc1c 100644 --- a/peer/cwtch_peer.go +++ b/peer/cwtch_peer.go @@ -209,8 +209,11 @@ func (cp *cwtchPeer) AddContact(nick, onion string, authorization model.Authoriz // server assuming there are no errors and the contact doesn't already exist. // TODO in the future this function should also integrate with a trust provider to validate the key bundle. func (cp *cwtchPeer) AddServer(serverSpecification string) error { - keyBundle := new(model.KeyBundle) - err := json.Unmarshal([]byte(serverSpecification), &keyBundle) + // This confirms that the server did at least sign the bundle + keyBundle, err := model.DeserializeAndVerify([]byte(serverSpecification)) + if err != nil { + return err + } log.Debugf("Got new key bundle %v", keyBundle) if keyBundle.HasKeyType(model.KeyTypeServerOnion) { onionKey, _ := keyBundle.GetKey(model.KeyTypeServerOnion) @@ -238,8 +241,32 @@ func (cp *cwtchPeer) AddServer(serverSpecification string) error { cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, event.SaveHistoryKey, event.DeleteHistoryDefault)) return nil } - // Server Already Exists - return errors.New("a contact already exists with the given onion address") + + // We have already seen this server and so some additional checks are needed (and we don't need to create the + // peer). + server := cp.GetContact(onion) + ab := keyBundle.AttributeBundle() + + // Check server bundle for consistency + for k, v := range ab { + val, exists := server.GetAttribute(k) + if exists { + if val != v { + // this is inconsistent! + return model.InconsistentKeyBundleError + } + } + // we haven't seen this key associated with the server before + } + + // If we have gotten to this point we can assume this is a safe key bundle signed by the + // server with no conflicting keys. So we are going to publish all the keys + for k, v := range ab { + log.Debugf("Server (%v) has %v key %v", onion, k, v) + cp.eventBus.Publish(event.NewEventList(event.SetPeerAttribute, event.RemotePeer, onion, k, v)) + } + + return nil } return err } diff --git a/server/app/main.go b/server/app/main.go index 5994fd0..6e8dd4a 100644 --- a/server/app/main.go +++ b/server/app/main.go @@ -5,7 +5,6 @@ import ( cwtchserver "cwtch.im/cwtch/server" "cwtch.im/tapir/primitives" "encoding/base64" - "encoding/json" "fmt" "git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/log" @@ -64,7 +63,7 @@ func main() { } fmt.Printf("%v", "torv3"+base64.StdEncoding.EncodeToString(invite)) - bundle, _ := json.Marshal(server.KeyBundle()) + bundle := server.KeyBundle().Serialize() log.Infof("Server Config: server:%s", base64.StdEncoding.EncodeToString(bundle)) server.Run(acn) } diff --git a/server/server.go b/server/server.go index 5bb6777..d78a6ce 100644 --- a/server/server.go +++ b/server/server.go @@ -86,13 +86,14 @@ func (s *Server) Run(acn connectivity.ACN) { } } -// KeyBundle provides the keybundle of the server (mostly used for testing) -func (s *Server) KeyBundle() model.KeyBundle { - kb := model.KeyBundle{Keys: make(map[model.KeyType]model.Key)} +// KeyBundle provides the signed keybundle of the server +func (s *Server) KeyBundle() *model.KeyBundle { + kb := model.NewKeyBundle() identity := s.config.Identity() kb.Keys[model.KeyTypeServerOnion] = model.Key(identity.Hostname()) - kb.Keys[model.KeyTypeTokenOnion] = model.Key(tor.GetTorV3Hostname(s.tokenService.PublicKey())) + kb.Keys[model.KeyTypeTokenOnion] = model.Key(s.tokenService.Hostname()) kb.Keys[model.KeyTypePrivacyPass] = model.Key(s.tokenServer.Y.String()) + kb.Sign(identity) return kb } diff --git a/testing/cwtch_peer_server_integration_test.go b/testing/cwtch_peer_server_integration_test.go index 4f5dc65..7c99b94 100644 --- a/testing/cwtch_peer_server_integration_test.go +++ b/testing/cwtch_peer_server_integration_test.go @@ -204,7 +204,9 @@ func TestCwtchPeerIntegration(t *testing.T) { // ***** Peering, server joining, group creation / invite ***** fmt.Println("Alice joining server...") - alice.AddServer(string(serverKeyBundle)) + if err := alice.AddServer(string(serverKeyBundle)); err != nil { + t.Fatalf("Failed to Add Server Bundle %v", err) + } alice.JoinServer(serverAddr) fmt.Println("Alice peering with Bob...")