diff --git a/crypto/key.go b/crypto/key.go index 6da7b7487..7e5027750 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -1,10 +1,18 @@ package crypto import ( + "bytes" "errors" + "crypto/elliptic" + "crypto/hmac" "crypto/rand" "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "hash" + "math/big" "code.google.com/p/goprotobuf/proto" ) @@ -19,10 +27,7 @@ type PrivKey interface { // Cryptographically sign the given bytes Sign([]byte) ([]byte, error) - // Decrypt a message encrypted with this keys public key - Decrypt([]byte) ([]byte, error) - - // Return the public key paired with this private key + // Return a public key paired with this private key GetPublic() PubKey // Generate a secret string of bytes @@ -36,13 +41,13 @@ type PubKey interface { // Verify that 'sig' is the signed hash of 'data' Verify(data []byte, sig []byte) (bool, error) - // Encrypt the given data with the public key - Encrypt([]byte) ([]byte, error) - // Bytes returns a serialized, storeable representation of this key Bytes() ([]byte, error) } +// Given a public key, generates the shared key. +type GenSharedKey func([]byte) ([]byte, error) + func GenerateKeyPair(typ, bits int) (PrivKey, PubKey, error) { switch typ { case RSA: @@ -57,6 +62,144 @@ func GenerateKeyPair(typ, bits int) (PrivKey, PubKey, error) { } } +// Generates an ephemeral public key and returns a function that will compute +// the shared secret key. Used in the identify module. +// +// Focuses only on ECDH now, but can be made more general in the future. +func GenerateEKeyPair(curveName string) ([]byte, GenSharedKey, error) { + var curve elliptic.Curve + + switch curveName { + case "P-224": + curve = elliptic.P224() + case "P-256": + curve = elliptic.P256() + case "P-384": + curve = elliptic.P384() + case "P-521": + curve = elliptic.P521() + } + + priv, x, y, err := elliptic.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, nil, err + } + + var pubKey bytes.Buffer + pubKey.Write(x.Bytes()) + pubKey.Write(y.Bytes()) + + done := func(theirPub []byte) ([]byte, error) { + // Verify and unpack node's public key. + curveSize := curve.Params().BitSize + + if len(theirPub) != (curveSize / 4) { + return nil, errors.New("Malformed public key.") + } + + bound := (curveSize / 8) + x := big.NewInt(0) + y := big.NewInt(0) + + x.SetBytes(theirPub[0:bound]) + y.SetBytes(theirPub[bound : bound*2]) + + if !curve.IsOnCurve(x, y) { + return nil, errors.New("Invalid public key.") + } + + // Generate shared secret. + secret, _ := curve.ScalarMult(x, y, priv) + + return secret.Bytes(), nil + } + + return pubKey.Bytes(), done, nil +} + +// Generates a set of keys for each party by stretching the shared key. +// (myIV, theirIV, myCipherKey, theirCipherKey, myMACKey, theirMACKey) +func KeyStretcher(cmp int, cipherType string, hashType string, secret []byte) ([]byte, []byte, []byte, []byte, []byte, []byte) { + var cipherKeySize int + switch cipherType { + case "AES-128": + cipherKeySize = 16 + case "AES-256": + cipherKeySize = 32 + } + + ivSize := 16 + hmacKeySize := 20 + + seed := []byte("key expansion") + + result := make([]byte, 2*(ivSize+cipherKeySize+hmacKeySize)) + + var h func() hash.Hash + + switch hashType { + case "SHA1": + h = sha1.New + case "SHA256": + h = sha256.New + case "SHA512": + h = sha512.New + } + + m := hmac.New(h, secret) + m.Write(seed) + + a := m.Sum(nil) + + j := 0 + for j < len(result) { + m.Reset() + m.Write(a) + m.Write(seed) + b := m.Sum(nil) + + todo := len(b) + + if j+todo > len(result) { + todo = len(result) - j + } + + copy(result[j:j+todo], b) + + j += todo + + m.Reset() + m.Write(a) + a = m.Sum(nil) + } + + myResult := make([]byte, ivSize+cipherKeySize+hmacKeySize) + theirResult := make([]byte, ivSize+cipherKeySize+hmacKeySize) + + half := len(result) / 2 + + if cmp == 1 { + copy(myResult, result[:half]) + copy(theirResult, result[half:]) + } else if cmp == -1 { + copy(myResult, result[half:]) + copy(theirResult, result[:half]) + } else { // Shouldn't happen, but oh well. + copy(myResult, result[half:]) + copy(theirResult, result[half:]) + } + + myIV := myResult[0:ivSize] + myCKey := myResult[ivSize : ivSize+cipherKeySize] + myMKey := myResult[ivSize+cipherKeySize:] + + theirIV := theirResult[0:ivSize] + theirCKey := theirResult[ivSize : ivSize+cipherKeySize] + theirMKey := theirResult[ivSize+cipherKeySize:] + + return myIV, theirIV, myCKey, theirCKey, myMKey, theirMKey +} + func UnmarshalPublicKey(data []byte) (PubKey, error) { pmes := new(PBPublicKey) err := proto.Unmarshal(data, pmes) diff --git a/crypto/rsa.go b/crypto/rsa.go index fda3bcf58..31aa69596 100644 --- a/crypto/rsa.go +++ b/crypto/rsa.go @@ -28,10 +28,6 @@ func (pk *RsaPublicKey) Verify(data, sig []byte) (bool, error) { return true, nil } -func (pk *RsaPublicKey) Encrypt(message []byte) ([]byte, error) { - return rsa.EncryptPKCS1v15(rand.Reader, pk.k, message) -} - func (pk *RsaPublicKey) Bytes() ([]byte, error) { b, err := x509.MarshalPKIXPublicKey(pk.k) if err != nil { @@ -56,10 +52,6 @@ func (sk *RsaPrivateKey) Sign(message []byte) ([]byte, error) { return rsa.SignPKCS1v15(rand.Reader, sk.k, crypto.SHA256, hashed[:]) } -func (sk *RsaPrivateKey) Decrypt(ciphertext []byte) ([]byte, error) { - return rsa.DecryptPKCS1v15(rand.Reader, sk.k, ciphertext) -} - func (sk *RsaPrivateKey) GetPublic() PubKey { return &RsaPublicKey{&sk.k.PublicKey} } diff --git a/identify/identify.go b/identify/identify.go index b145add2e..017b03c0f 100644 --- a/identify/identify.go +++ b/identify/identify.go @@ -5,6 +5,16 @@ package identify import ( "bytes" "errors" + "strings" + + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "hash" proto "code.google.com/p/goprotobuf/proto" ci "github.com/jbenet/go-ipfs/crypto" @@ -12,94 +22,221 @@ import ( u "github.com/jbenet/go-ipfs/util" ) +// List of supported protocols--each section in order of preference. +// Takes the form: ECDH curves : Ciphers : Hashes +var SupportedExchanges = "P-256,P-224,P-384,P-521" +var SupportedCiphers = "AES-256,AES-128" +var SupportedHashes = "SHA256,SHA512,SHA1" + // ErrUnsupportedKeyType is returned when a private key cast/type switch fails. var ErrUnsupportedKeyType = errors.New("unsupported key type") // Perform initial communication with this peer to share node ID's and -// initiate communication -func Handshake(self, remote *peer.Peer, in, out chan []byte) error { - encoded, err := buildHandshake(self) +// initiate communication. (secureIn, secureOut, error) +func Handshake(self, remote *peer.Peer, in, out chan []byte) (chan []byte, chan []byte, error) { + // Generate and send Hello packet. + // Hello = (rand, PublicKey, Supported) + nonce := make([]byte, 16) + rand.Read(nonce) + + hello := new(Hello) + + myPubKey, err := self.PubKey.Bytes() if err != nil { - return err + return nil, nil, err } + + hello.Rand = nonce + hello.Pubkey = myPubKey + hello.Exchanges = &SupportedExchanges + hello.Ciphers = &SupportedCiphers + hello.Hashes = &SupportedHashes + + encoded, err := proto.Marshal(hello) + if err != nil { + return nil, nil, err + } + out <- encoded + + // Parse their Hello packet and generate an Exchange packet. + // Exchange = (EphemeralPubKey, Signature) resp := <-in - pbresp := new(Identify) - err = proto.Unmarshal(resp, pbresp) + helloResp := new(Hello) + err = proto.Unmarshal(resp, helloResp) if err != nil { - return err + return nil, nil, err } - // Verify that the given ID matches their given public key - if verifyErr := verifyID(peer.ID(pbresp.GetId()), pbresp.GetPubkey()); verifyErr != nil { - return verifyErr - } - - pubkey, err := ci.UnmarshalPublicKey(pbresp.GetPubkey()) + remote.PubKey, err = ci.UnmarshalPublicKey(helloResp.GetPubkey()) if err != nil { - return err + return nil, nil, err } - // Challenge peer to ensure they own the given pubkey - secret := self.PrivKey.GenSecret() - encrypted, err := pubkey.Encrypt(secret) + remote.ID, err = IdFromPubKey(remote.PubKey) if err != nil { - //... this is odd - return err + return nil, nil, err } - out <- encrypted - challenge := <-in - - // Decrypt challenge and send plaintext to partner - plain, err := self.PrivKey.Decrypt(challenge) + exchange, err := selectBest(SupportedExchanges, helloResp.GetExchanges()) if err != nil { - return err + return nil, nil, err } - out <- plain - chalResp := <-in - if !bytes.Equal(chalResp, secret) { - return errors.New("Recieved incorrect challenge response!") + cipherType, err := selectBest(SupportedCiphers, helloResp.GetCiphers()) + if err != nil { + return nil, nil, err + } + + hashType, err := selectBest(SupportedHashes, helloResp.GetHashes()) + if err != nil { + return nil, nil, err + } + + epubkey, done, err := ci.GenerateEKeyPair(exchange) // Generate EphemeralPubKey + + var handshake bytes.Buffer // Gather corpus to sign. + handshake.Write(encoded) + handshake.Write(resp) + handshake.Write(epubkey) + + exPacket := new(Exchange) + + exPacket.Epubkey = epubkey + exPacket.Signature, err = self.PrivKey.Sign(handshake.Bytes()) + if err != nil { + return nil, nil, err + } + + exEncoded, err := proto.Marshal(exPacket) + + out <- exEncoded + + // Parse their Exchange packet and generate a Finish packet. + // Finish = E('Finish') + resp1 := <-in + + exchangeResp := new(Exchange) + err = proto.Unmarshal(resp1, exchangeResp) + if err != nil { + return nil, nil, err + } + + var theirHandshake bytes.Buffer + theirHandshake.Write(resp) + theirHandshake.Write(encoded) + theirHandshake.Write(exchangeResp.GetEpubkey()) + + ok, err := remote.PubKey.Verify(theirHandshake.Bytes(), exchangeResp.GetSignature()) + if err != nil { + return nil, nil, err + } + + if !ok { + return nil, nil, errors.New("Bad signature!") + } + + secret, err := done(exchangeResp.GetEpubkey()) + if err != nil { + return nil, nil, err + } + + cmp := bytes.Compare(myPubKey, helloResp.GetPubkey()) + mIV, tIV, mCKey, tCKey, mMKey, tMKey := ci.KeyStretcher(cmp, cipherType, hashType, secret) + + secureIn := make(chan []byte) + secureOut := make(chan []byte) + + go secureInProxy(in, secureIn, hashType, tIV, tCKey, tMKey) + go secureOutProxy(out, secureOut, hashType, mIV, mCKey, mMKey) + + finished := []byte("Finished") + + secureOut <- finished + resp2 := <-secureIn + + if bytes.Compare(resp2, finished) != 0 { + return nil, nil, errors.New("Negotiation failed.") } - remote.ID = peer.ID(pbresp.GetId()) - remote.PubKey = pubkey u.DOut("[%s] identify: Got node id: %s\n", self.ID.Pretty(), remote.ID.Pretty()) - return nil + return secureIn, secureOut, nil } -func buildHandshake(self *peer.Peer) ([]byte, error) { - pkb, err := self.PubKey.Bytes() - if err != nil { - return nil, err +func makeMac(hashType string, key []byte) (hash.Hash, int) { + switch hashType { + case "SHA1": + return hmac.New(sha1.New, key), sha1.Size + case "SHA512": + return hmac.New(sha512.New, key), sha512.Size + default: + return hmac.New(sha256.New, key), sha256.Size } - - pmes := new(Identify) - pmes.Id = []byte(self.ID) - pmes.Pubkey = pkb - - encoded, err := proto.Marshal(pmes) - if err != nil { - return nil, err - } - - return encoded, nil } -func verifyID(id peer.ID, pubkey []byte) error { - hash, err := u.Hash(pubkey) - if err != nil { - return err - } +func secureInProxy(in, secureIn chan []byte, hashType string, tIV, tCKey, tMKey []byte) { + theirBlock, _ := aes.NewCipher(tCKey) + theirCipher := cipher.NewCTR(theirBlock, tIV) - if id.Equal(peer.ID(hash)) { - return nil - } + theirMac, macSize := makeMac(hashType, tMKey) - return errors.New("ID did not match public key!") + for { + data, ok := <-in + if !ok { + return + } + + if len(data) <= macSize { + continue + } + + mark := len(data) - macSize + buff := make([]byte, mark) + + theirCipher.XORKeyStream(buff, data[0:mark]) + + theirMac.Write(data[0:mark]) + expected := theirMac.Sum(nil) + theirMac.Reset() + + hmacOk := hmac.Equal(data[mark:], expected) + + if hmacOk { + secureIn <- buff + } else { + secureIn <- nil + } + } +} + +func secureOutProxy(out, secureOut chan []byte, hashType string, mIV, mCKey, mMKey []byte) { + myBlock, _ := aes.NewCipher(mCKey) + myCipher := cipher.NewCTR(myBlock, mIV) + + myMac, macSize := makeMac(hashType, mMKey) + + for { + data, ok := <-secureOut + if !ok { + return + } + + if len(data) == 0 { + continue + } + + buff := make([]byte, len(data)+macSize) + + myCipher.XORKeyStream(buff, data) + + myMac.Write(buff[0:len(data)]) + copy(buff[len(data):], myMac.Sum(nil)) + myMac.Reset() + + out <- buff + } } func IdFromPubKey(pk ci.PubKey) (peer.ID, error) { @@ -113,3 +250,41 @@ func IdFromPubKey(pk ci.PubKey) (peer.ID, error) { } return peer.ID(hash), nil } + +// Determines which algorithm to use. Note: f(a, b) = f(b, a) +func selectBest(myPrefs, theirPrefs string) (string, error) { + // Person with greatest hash gets first choice. + myHash, err := u.Hash([]byte(myPrefs)) + if err != nil { + return "", err + } + + theirHash, err := u.Hash([]byte(theirPrefs)) + if err != nil { + return "", err + } + + cmp := bytes.Compare(myHash, theirHash) + var firstChoiceArr, secChoiceArr []string + + if cmp == -1 { + firstChoiceArr = strings.Split(theirPrefs, ",") + secChoiceArr = strings.Split(myPrefs, ",") + } else if cmp == 1 { + firstChoiceArr = strings.Split(myPrefs, ",") + secChoiceArr = strings.Split(theirPrefs, ",") + } else { // Exact same preferences. + myPrefsArr := strings.Split(myPrefs, ",") + return myPrefsArr[0], nil + } + + for _, secChoice := range secChoiceArr { + for _, firstChoice := range firstChoiceArr { + if firstChoice == secChoice { + return firstChoice, nil + } + } + } + + return "", errors.New("No algorithms in common!") +} diff --git a/identify/identify_test.go b/identify/identify_test.go index 13cd7dfa0..6ac33039f 100644 --- a/identify/identify_test.go +++ b/identify/identify_test.go @@ -41,13 +41,13 @@ func TestHandshake(t *testing.T) { } go func() { - err := Handshake(pa, pb, cha, chb) + _, _, err := Handshake(pa, pb, cha, chb) if err != nil { t.Fatal(err) } }() - err = Handshake(pb, pa, chb, cha) + _, _, err = Handshake(pb, pa, chb, cha) if err != nil { t.Fatal(err) } diff --git a/identify/message.pb.go b/identify/message.pb.go index 0e5c45627..9313fc1c6 100644 --- a/identify/message.pb.go +++ b/identify/message.pb.go @@ -9,7 +9,8 @@ It is generated from these files: message.proto It has these top-level messages: - Identify + Hello + Exchange */ package identify @@ -20,29 +21,77 @@ import math "math" var _ = proto.Marshal var _ = math.Inf -type Identify struct { - Id []byte `protobuf:"bytes,1,req,name=id" json:"id,omitempty"` - Pubkey []byte `protobuf:"bytes,2,req,name=pubkey" json:"pubkey,omitempty"` - XXX_unrecognized []byte `json:"-"` +type Hello struct { + Rand []byte `protobuf:"bytes,1,req,name=rand" json:"rand,omitempty"` + Pubkey []byte `protobuf:"bytes,2,req,name=pubkey" json:"pubkey,omitempty"` + Exchanges *string `protobuf:"bytes,3,req,name=exchanges" json:"exchanges,omitempty"` + Ciphers *string `protobuf:"bytes,4,req,name=ciphers" json:"ciphers,omitempty"` + Hashes *string `protobuf:"bytes,5,req,name=hashes" json:"hashes,omitempty"` + XXX_unrecognized []byte `json:"-"` } -func (m *Identify) Reset() { *m = Identify{} } -func (m *Identify) String() string { return proto.CompactTextString(m) } -func (*Identify) ProtoMessage() {} +func (m *Hello) Reset() { *m = Hello{} } +func (m *Hello) String() string { return proto.CompactTextString(m) } +func (*Hello) ProtoMessage() {} -func (m *Identify) GetId() []byte { +func (m *Hello) GetRand() []byte { if m != nil { - return m.Id + return m.Rand } return nil } -func (m *Identify) GetPubkey() []byte { +func (m *Hello) GetPubkey() []byte { if m != nil { return m.Pubkey } return nil } +func (m *Hello) GetExchanges() string { + if m != nil && m.Exchanges != nil { + return *m.Exchanges + } + return "" +} + +func (m *Hello) GetCiphers() string { + if m != nil && m.Ciphers != nil { + return *m.Ciphers + } + return "" +} + +func (m *Hello) GetHashes() string { + if m != nil && m.Hashes != nil { + return *m.Hashes + } + return "" +} + +type Exchange struct { + Epubkey []byte `protobuf:"bytes,1,req,name=epubkey" json:"epubkey,omitempty"` + Signature []byte `protobuf:"bytes,2,req,name=signature" json:"signature,omitempty"` + XXX_unrecognized []byte `json:"-"` +} + +func (m *Exchange) Reset() { *m = Exchange{} } +func (m *Exchange) String() string { return proto.CompactTextString(m) } +func (*Exchange) ProtoMessage() {} + +func (m *Exchange) GetEpubkey() []byte { + if m != nil { + return m.Epubkey + } + return nil +} + +func (m *Exchange) GetSignature() []byte { + if m != nil { + return m.Signature + } + return nil +} + func init() { } diff --git a/identify/message.proto b/identify/message.proto index df9ff515f..4c3e032e5 100644 --- a/identify/message.proto +++ b/identify/message.proto @@ -1,6 +1,14 @@ package identify; -message Identify { - required bytes id = 1; +message Hello { + required bytes rand = 1; required bytes pubkey = 2; + required string exchanges = 3; + required string ciphers = 4; + required string hashes = 5; +} + +message Exchange { + required bytes epubkey = 1; + required bytes signature = 2; } diff --git a/swarm/swarm.go b/swarm/swarm.go index fc32ff249..ebf14c180 100644 --- a/swarm/swarm.go +++ b/swarm/swarm.go @@ -172,7 +172,7 @@ func (s *Swarm) handleNewConn(nconn net.Conn) { } newConnChans(conn) - err := ident.Handshake(s.local, p, conn.Incoming.MsgChan, conn.Outgoing.MsgChan) + _, _, err := ident.Handshake(s.local, p, conn.Incoming.MsgChan, conn.Outgoing.MsgChan) if err != nil { u.PErr("%v\n", err.Error()) conn.Close() @@ -420,7 +420,7 @@ func (s *Swarm) GetConnection(id peer.ID, addr *ma.Multiaddr) (*peer.Peer, error // Handle performing a handshake on a new connection and ensuring proper forward communication func (s *Swarm) handleDialedCon(conn *Conn) error { - err := ident.Handshake(s.local, conn.Peer, conn.Incoming.MsgChan, conn.Outgoing.MsgChan) + _, _, err := ident.Handshake(s.local, conn.Peer, conn.Incoming.MsgChan, conn.Outgoing.MsgChan) if err != nil { return err }