Sign and Check ToFU Server Bundle #321

Merged
sarah merged 2 commits from tapir_server into master 2020-10-01 21:17:14 +00:00
7 changed files with 171 additions and 13 deletions

15
model/errors.go Normal file
View File

@ -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")
)

View File

@ -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)

64
model/keyBundle_test.go Normal file
View File

@ -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)
}
}

View File

@ -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
Outdated
Review

we don't record anywhere if we def didnt see the key or exit the function so we will ALWAS fall through to adding, even if it already exists in the bundle?

we don't record anywhere if we def didnt see the key or exit the function so we will ALWAS fall through to adding, even if it already exists in the bundle?
Outdated
Review

We can't do anything with the bundle until we've confirmed it isn't malicious so we first iterate through all the keys and then if that check passes we just publish the update key attributes (which yeah means if the user calls addserver twice on the same bundle it it will do some unnecessary work, but it simplifies the function a lot).

We can't do anything with the bundle until we've confirmed it isn't malicious so we first iterate through all the keys and then if that check passes we just publish the update key attributes (which yeah means if the user calls addserver twice on the same bundle it it will do some unnecessary work, but it simplifies the function a lot).
}
// 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
}

View File

@ -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)
}

View File

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

View File

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