tordam

A library for peer discovery inside the Tor network
git clone https://git.parazyd.org/tordam
Log | Files | Refs | README | LICENSE

commit 64ad43723f6b233b1830d22ce62879c37e29ddbc
parent 0ba25233d58c34d9c68993f11c9ca302a959e15c
Author: parazyd <parazyd@dyne.org>
Date:   Tue, 12 Dec 2017 01:56:50 +0100

Refactor handshake validation.

This commit refactors the handshake validation logic. It allows us being
a lot more robust in error checking and overall validation.

Most importantly, in damlib, the ValidateReq function has been split
into two: one handling the first handshake, and the other handling the
second handshake.

Exported redis variables are now located in damlib's redis.go.

We also introduce a config.go file in damlib, which can be a central
place for holding configurable information.

Diffstat:
Mcmd/dam-client/main.go | 37++++++++++++-------------------------
Mcmd/dam-dir/main.go | 194+++++++++++++++----------------------------------------------------------------
Mcmd/dam-dir/main_test.go | 2+-
Apkg/damlib/config.go | 18++++++++++++++++++
Apkg/damlib/redis.go | 13+++++++++++++
Mpkg/damlib/validate.go | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
6 files changed, 284 insertions(+), 222 deletions(-)

diff --git a/cmd/dam-client/main.go b/cmd/dam-client/main.go @@ -16,18 +16,6 @@ import ( lib "github.com/parazyd/tor-dam/pkg/damlib" ) -// Cwd holds the path to the directory where we will Chdir on startup. -var Cwd = os.Getenv("HOME") + "/.dam" - -// RsaBits holds the size of our RSA private key. Tor standard is 1024. -const RsaBits = 1024 - -// Privpath holds the name of where our private key is. -const Privpath = "dam-private.key" - -// Postmsg holds the message we are signing with our private key. -const Postmsg = "I am a DAM node!" - type msgStruct struct { Secret string } @@ -103,33 +91,32 @@ func announce(dir string, vals map[string]string, privkey *rsa.PrivateKey) (bool log.Printf("%s: Success. 2/2 handshake valid.\n", dir) log.Printf("%s: Reply: %s\n", dir, m.Secret) return true, nil - } else { - log.Printf("%s: Fail. Reply: %s\n", dir, m.Secret) - return false, nil } + log.Printf("%s: Fail. Reply: %s\n", dir, m.Secret) + return false, nil } return false, nil } func main() { - if _, err := os.Stat(Cwd); os.IsNotExist(err) { - err := os.Mkdir(Cwd, 0700) + if _, err := os.Stat(lib.Cwd); os.IsNotExist(err) { + err := os.Mkdir(lib.Cwd, 0700) lib.CheckError(err) } - err := os.Chdir(Cwd) + err := os.Chdir(lib.Cwd) lib.CheckError(err) - if _, err := os.Stat(Privpath); os.IsNotExist(err) { - key, err := lib.GenRsa(RsaBits) + if _, err := os.Stat(lib.Privpath); os.IsNotExist(err) { + key, err := lib.GenRsa(lib.RsaBits) lib.CheckError(err) - _, err = lib.SavePrivRsa(Privpath, key) + _, err = lib.SavePrivRsa(lib.Privpath, key) lib.CheckError(err) } // Start up the hidden service log.Println("Starting up the hidden service...") - cmd := exec.Command("damhs.py", Privpath) + cmd := exec.Command("damhs.py", lib.Privpath) stdout, err := cmd.StdoutPipe() lib.CheckError(err) @@ -159,10 +146,10 @@ func main() { } } - key, err := lib.LoadRsaKeyFromFile(Privpath) + key, err := lib.LoadRsaKeyFromFile(lib.Privpath) lib.CheckError(err) - sig, err := lib.SignMsgRsa([]byte(Postmsg), key) + sig, err := lib.SignMsgRsa([]byte(lib.PostMsg), key) lib.CheckError(err) encodedSig := base64.StdEncoding.EncodeToString(sig) @@ -172,7 +159,7 @@ func main() { nodevals := map[string]string{ "nodetype": "node", "address": string(onionAddr), - "message": Postmsg, + "message": lib.PostMsg, "signature": encodedSig, "secret": "", } diff --git a/cmd/dam-dir/main.go b/cmd/dam-dir/main.go @@ -3,7 +3,6 @@ package main // See LICENSE file for copyright and license details. import ( - "encoding/base64" "encoding/json" "log" "net/http" @@ -12,26 +11,12 @@ import ( "sync" "time" - "github.com/go-redis/redis" lib "github.com/parazyd/tor-dam/pkg/damlib" ) -// Cwd holds the path to the directory where we will Chdir on startup. -var Cwd = os.Getenv("HOME") + "/.dam" - // ListenAddress controls where our HTTP API daemon is listening. const ListenAddress = "127.0.0.1:49371" -// RedisAddress points us to our Redis instance. -const RedisAddress = "127.0.0.1:6379" - -// RedisCli is our global Redis client -var RedisCli = redis.NewClient(&redis.Options{ - Addr: RedisAddress, - Password: "", - DB: 0, -}) - type nodeStruct struct { Nodetype string Address string @@ -52,7 +37,7 @@ func startRedis() { time.Sleep(500 * time.Millisecond) - _, err = RedisCli.Ping().Result() + _, err = lib.RedisCli.Ping().Result() lib.CheckError(err) } @@ -81,34 +66,12 @@ func handlePost(rw http.ResponseWriter, request *http.Request) { return } - // Drop out ASAP. + // Bail out as soon as possible. if len(n.Nodetype) == 0 || len(n.Address) == 0 || len(n.Message) == 0 || len(n.Signature) == 0 { return } - // TODO: When a node wants to promote itself from something it already was, - // what to do? - switch n.Nodetype { - case "node": - log.Println("Client of type:", n.Nodetype) - case "directory": - log.Println("Client of type:", n.Nodetype) - default: - log.Println("Invalid nodetype:", n.Nodetype) - ret = map[string]string{"secret": "Invalid nodetype."} - if err := postback(rw, ret, 400); err != nil { - lib.CheckError(err) - } - return - } - - decSig, err := base64.StdEncoding.DecodeString(n.Signature) - if err != nil { - log.Println("Failed decoding signature:", err) - ret = map[string]string{"secret": err.Error()} - if err := postback(rw, ret, 400); err != nil { - lib.CheckError(err) - } + if !(lib.ValidateOnionAddress(n.Address)) { return } @@ -116,139 +79,52 @@ func handlePost(rw http.ResponseWriter, request *http.Request) { "nodetype": n.Nodetype, "address": n.Address, "message": n.Message, - "signature": string(decSig), + "signature": n.Signature, "secret": n.Secret, } - // Check if we have seen this node already. - ex, err := RedisCli.Exists(n.Address).Result() - lib.CheckError(err) - var pub = "" - if ex == 1 { - res, err := RedisCli.HGet(n.Address, "pubkey").Result() - pub = string(res) - lib.CheckError(err) - } - - pkey, valid := lib.ValidateReq(req, pub) - if !(valid) && pkey == nil { - ret := map[string]string{"secret": "Request is not valid."} - if err := postback(rw, ret, 400); err != nil { - lib.CheckError(err) - } - return - } else if !(valid) && pkey != nil { - // We couldn't get a descriptor. - ret := map[string]string{"secret": string(pkey)} - if err := postback(rw, ret, 500); err != nil { - lib.CheckError(err) - } - return - } - - pubkey, err := lib.ParsePubkeyRsa(pkey) - lib.CheckError(err) - - n.Pubkey = string(pkey) - now := time.Now() - n.Firstseen = now.Unix() - n.Lastseen = now.Unix() - - if len(req["secret"]) != 88 { - // Client did not send a decrypted secret. - randString, err := lib.GenRandomASCII(64) - lib.CheckError(err) - - secret, err := lib.EncryptMsgRsa([]byte(randString), pubkey) - lib.CheckError(err) - - encodedSecret := base64.StdEncoding.EncodeToString(secret) - ret := map[string]string{"secret": encodedSecret} - - // Check if we have seen this node already. - ex, err := RedisCli.Exists(n.Address).Result() - lib.CheckError(err) - - // Save the node into redis - info := map[string]interface{}{ - "nodetype": n.Nodetype, - "address": n.Address, - "message": n.Message, - "signature": n.Signature, - "secret": base64.StdEncoding.EncodeToString([]byte(randString)), - "pubkey": n.Pubkey, - "lastseen": n.Lastseen, - } - - if ex != 1 { - info["firstseen"] = n.Firstseen - info["valid"] = 0 // This should be 1 after the node is not considered malicious - } - log.Printf("%s: Writing to Redis\n", n.Address) - redRet, err := RedisCli.HMSet(n.Address, info).Result() - lib.CheckError(err) - - if redRet == "OK" { - log.Println("Returning encrypted secret to", n.Address) + // First handshake + if len(n.Message) != 88 && len(n.Secret) != 88 { + valid, msg := lib.ValidateFirst(req) + ret = map[string]string{"secret": msg} + if valid { + log.Printf("%s: 1/2 handshake valid.\n", n.Address) + log.Println("Sending back encrypted secret.") if err := postback(rw, ret, 200); err != nil { lib.CheckError(err) } return } - } - - if len(req["secret"]) == 88 && len(req["message"]) == 88 { - // Client sent a decrypted secret. - var correct = false - localSec, err := RedisCli.HGet(n.Address, "secret").Result() + log.Printf("%s: 1/2 handshake invalid: %s\n", n.Address, msg) + // Delete it all from redis. + _, err := lib.RedisCli.Del(n.Address).Result() lib.CheckError(err) - - if localSec == req["secret"] && localSec == req["message"] { - log.Println("Secrets match!") - correct = true - } else { - log.Println("Secrets don't match!") - correct = false - } - - if correct { - msg := []byte(req["message"]) - sig := []byte(req["signature"]) - pub, err := lib.ParsePubkeyRsa([]byte(n.Pubkey)) - lib.CheckError(err) - val, err := lib.VerifyMsgRsa(msg, sig, pub) + if err := postback(rw, ret, 400); err != nil { lib.CheckError(err) - if val { - correct = true - } else { - correct = false - } } + return + } - if correct { - // Replace the secret in redis to prevent reuse. - randString, err := lib.GenRandomASCII(64) - lib.CheckError(err) - encoded := base64.StdEncoding.EncodeToString([]byte(randString)) - _, err = RedisCli.HSet(n.Address, "secret", encoded).Result() - lib.CheckError(err) - log.Printf("Welcoming %s to the network\n", n.Address) - ret := map[string]string{"secret": "Welcome to the DAM network!"} + // Second handshake + if len(req["secret"]) == 88 && len(req["message"]) == 88 { + valid, msg := lib.ValidateSecond(req) + ret = map[string]string{"secret": msg} + if valid { + log.Printf("%s: 2/2 handshake valid.\n", n.Address) + log.Println("Sending back welcome message.") if err := postback(rw, ret, 200); err != nil { lib.CheckError(err) } return - } else { - // Delete it all from redis. - _, err := RedisCli.Del(n.Address).Result() + } + log.Printf("%s: 2/2 handshake invalid.\n", n.Address) + // Delete it all from redis. + _, err := lib.RedisCli.Del(n.Address).Result() + lib.CheckError(err) + if err := postback(rw, ret, 400); err != nil { lib.CheckError(err) - log.Printf("Verifying %s failed.\n", n.Address) - ret := map[string]string{"secret": "Verification failed. Bye."} - if err := postback(rw, ret, 400); err != nil { - lib.CheckError(err) - } - return } + return } } @@ -260,14 +136,14 @@ func handleElse(rw http.ResponseWriter, request *http.Request) { func main() { var wg sync.WaitGroup - if _, err := os.Stat(Cwd); os.IsNotExist(err) { - err := os.Mkdir(Cwd, 0700) + if _, err := os.Stat(lib.Cwd); os.IsNotExist(err) { + err := os.Mkdir(lib.Cwd, 0700) lib.CheckError(err) } - err := os.Chdir(Cwd) + err := os.Chdir(lib.Cwd) lib.CheckError(err) - if _, err := RedisCli.Ping().Result(); err != nil { + if _, err := lib.RedisCli.Ping().Result(); err != nil { // We assume redis is not running. Start it up. startRedis() } diff --git a/cmd/dam-dir/main_test.go b/cmd/dam-dir/main_test.go @@ -146,7 +146,7 @@ func TestValidSecondHandshake(t *testing.T) { if err != nil { t.Fatal(err) } - if m.Secret == "Welcome to the DAM network!" { + if m.Secret == lib.WelcomeMsg { t.Log("Server replied:", m.Secret) } else { t.Fatal(m.Secret) diff --git a/pkg/damlib/config.go b/pkg/damlib/config.go @@ -0,0 +1,18 @@ +package damlib + +import "os" + +// Cwd holds the path to the directory where we will Chdir on startup. +var Cwd = os.Getenv("HOME") + "/.dam" + +// RsaBits holds the size of our RSA private key. Tor standard is 1024. +const RsaBits = 1024 + +// Privpath holds the name of where our private key is. +const Privpath = "dam-private.key" + +// PostMsg holds the message we are signing with our private key. +const PostMsg = "I am a DAM node!" + +// WelcomeMsg holds the message we return when welcoming a node. +const WelcomeMsg = "Welcome to the DAM network!" diff --git a/pkg/damlib/redis.go b/pkg/damlib/redis.go @@ -0,0 +1,13 @@ +package damlib + +import "github.com/go-redis/redis" + +// RedisAddress points us to our Redis instance. +const RedisAddress = "127.0.0.1:6379" + +// RedisCli is our global Redis client +var RedisCli = redis.NewClient(&redis.Options{ + Addr: RedisAddress, + Password: "", + DB: 0, +}) diff --git a/pkg/damlib/validate.go b/pkg/damlib/validate.go @@ -3,71 +3,239 @@ package damlib // See LICENSE file for copyright and license details. import ( + "encoding/base64" + "errors" "log" "regexp" "strings" "time" ) -// ValidateReq validates our given request against the logic we are checking. -// The function takes a request data map, and a public key in the form of a -// string. If the public key is an empty string, the function will run an -// external program to fetch the node's public key from a Tor HSDir. -// -// ValidateReq will first validate "nodetype", looking whether the announcer -// is a node or a directory. -// Then, it will validate the onion address using a regular expression. -// Now, if pubkey is empty, it will run the external program to fetch it. If a -// descriptor can't be retrieved, it will retry for 10 times, and fail if those -// are not successful. -// -// Continuing, ValidateReq will verify the RSA signature posted by the -// announcer. -// If any of the above are invalid, the function will return nil and false. -// Otherwise, it will return the pubkey as a slice of bytes, and true. -func ValidateReq(req map[string]string, pubkey string) ([]byte, bool) { - // Validate nodetype. - if req["nodetype"] != "node" { - return nil, false +// ValidateOnionAddress matches a string against a regular expression matching +// a Tor hidden service address. Returns true on success and false on failure. +func ValidateOnionAddress(addr string) bool { + re, _ := regexp.Compile("^[a-z2-7]{16}\\.onion$") + if len(re.FindString(addr)) != 22 { + return false } - // Validate address. - re, err := regexp.Compile("^[a-z2-7]{16}\\.onion$") - CheckError(err) - if len(re.FindString(req["address"])) != 22 { - return nil, false + return true +} + +// sanityCheck performs basic sanity checks against the incoming request. +// Returns a boolean value according to the validity, and a string with an +// according message. +func sanityCheck(req map[string]string, handshake int) (bool, string) { + if !(ValidateOnionAddress(req["address"])) { + return false, "Invalid onion address" + } + if _, err := base64.StdEncoding.DecodeString(req["signature"]); err != nil { + return false, err.Error() + } + // TODO: When a node wants to promote itself from something it already was, + // what to do? + switch req["nodetype"] { + case "node": + log.Printf("%s is a node.", req["address"]) + case "directory": + log.Printf("%s is a directory.", req["address"]) + default: + return false, "Invalid nodetype." } - log.Println(req["address"], "seems valid") - if len(pubkey) == 0 { - // Address is valid, we try to fetch its pubkey from a HSDir + if handshake == 2 { + if _, err := base64.StdEncoding.DecodeString(req["message"]); err != nil { + return false, err.Error() + } + if _, err := base64.StdEncoding.DecodeString(req["secret"]); err != nil { + return false, err.Error() + } + } + return true, "" +} + +// ValidateFirst validates the first incoming handshake. +// It first calls sanityCheck to validate it's actually working with proper +// data. +// Next, it will look if the node is already found in redis. If so, it will +// fetch its public hey from redis, otherwise it will run an external program to +// fetch the node's public key from a Tor HSDir. If that program fails, so will +// the function. +// Once the public key is retrieved, it will validate the received message +// signature against that key. If all is well, we consider the request valid. +// Continuing, a random ASCII string will be generated and encrypted with the +// retrieved public key. All this data will be written into redis, and finally +// the encrypted (and base64 encoded) secret will be returned along with a true +// boolean value. +// On any failure, the function will return false, and produce an according +// string which is to be considered as an error message. +func ValidateFirst(req map[string]string) (bool, string) { + sane, what := sanityCheck(req, 1) + if !(sane) { + return false, what + } + + // Get the public key. + var pub string + // Check if we have seen this node already. + ex, err := RedisCli.Exists(req["address"]).Result() + CheckError(err) + if ex == 1 { + // We saw it so we should have the public key in redis. + // If we do not, that is an internal error. + pub, err = RedisCli.HGet(req["address"], "pubkey").Result() + CheckError(err) + // FIXME: Do a smarter check + if len(pub) < 20 { + CheckError(errors.New("Invalid data fetched from redis when requesting pubkey")) + } + } else { + // We fetch it from a HSDir cnt := 0 for { // We try until we have it. cnt++ if cnt > 10 { // We probably can't get a good HSDir. The client shall retry // later on. - return []byte("Couldn't get a descriptor. Try later."), false + return false, "Could not get a descriptor. Try later." } - pubkey = FetchHSPubkey(req["address"]) - if strings.HasPrefix(pubkey, "-----BEGIN RSA PUBLIC KEY-----") && - strings.HasSuffix(pubkey, "-----END RSA PUBLIC KEY-----") { + pub = FetchHSPubkey(req["address"]) + if strings.HasPrefix(pub, "-----BEGIN RSA PUBLIC KEY-----") && + strings.HasSuffix(pub, "-----END RSA PUBLIC KEY-----") { log.Println("Got descriptor!") break } time.Sleep(2000 * time.Millisecond) } } + // Validate signature. msg := []byte(req["message"]) - sig := []byte(req["signature"]) - pub, err := ParsePubkeyRsa([]byte(pubkey)) + decSig, _ := base64.StdEncoding.DecodeString(req["signature"]) + sig := []byte(decSig) + pubkey, err := ParsePubkeyRsa([]byte(pub)) // pubkey is their public key in *rsa.PublicKey type + CheckError(err) + val, _ := VerifyMsgRsa(msg, sig, pubkey) + if val != true { + log.Println("crypto/rsa: verification failure") + return false, "Signature verification failure." + } + + // The request is valid at this point. + + // Make a random secret for them, and save our node info to redis. + randString, err := GenRandomASCII(64) + CheckError(err) + encodedSecret := base64.StdEncoding.EncodeToString([]byte(randString)) + + var info = map[string]interface{}{ + "nodetype": req["nodetype"], + "address": req["address"], + "message": encodedSecret, + "signature": req["signature"], + "secret": encodedSecret, + "lastseen": time.Now().Unix(), + } // Can not cast, need this for HMSet + if ex != 1 { // We did not have this node in redis. + info["pubkey"] = pub + info["firstseen"] = time.Now().Unix() + info["valid"] = 0 + } + + log.Printf("%s: writing to redis\n", req["address"]) + redRet, err := RedisCli.HMSet(req["address"], info).Result() + CheckError(err) + + if redRet != "OK" { + return false, "Internal server error" + } + + encryptedSecret, err := EncryptMsgRsa([]byte(randString), pubkey) + CheckError(err) + + encryptedEncodedSecret := base64.StdEncoding.EncodeToString(encryptedSecret) + return true, encryptedEncodedSecret +} + +// ValidateSecond validates the second part of the handshake. +// First basic sanity checks are performed to ensure we are working with valid +// data. +// Next, the according public key will be retrieved from redis. If no key is +// found, we will consider the handshake invalid. +// Now the decrypted secret that was sent to us will be compared with what we +// have saved before. Upon proving they are the same, the RSA signature will now +// be validated. If all is well, we consider the request valid. +// Further on, we will generate a new random ASCII string and save it in redis +// to prevent further reuse of the already known string. Upon success, the +// function will return true, and a welcome message. Upon failure, the function +// will return false, and an according string which is to be considered an error +// message. +func ValidateSecond(req map[string]string) (bool, string) { + sane, what := sanityCheck(req, 2) + if !(sane) { + return false, what + } + + // Get the public key. + var pub string + // Check if we have seen this node already. + ex, err := RedisCli.Exists(req["address"]).Result() + CheckError(err) + if ex == 1 { + // We saw it so we should have the public key in redis. + // If we do not, that is an internal error. + pub, err = RedisCli.HGet(req["address"], "pubkey").Result() + CheckError(err) + // FIXME: Do a smarter check + if len(pub) < 20 { + CheckError(errors.New("Invalid data fetched from redis when requesting pubkey")) + } + } else { + return false, "We have not seen you before. Please authenticate properly." + } + + localSec, err := RedisCli.HGet(req["address"], "secret").Result() CheckError(err) - val, _ := VerifyMsgRsa(msg, sig, pub) + if !(localSec == req["secret"] && localSec == req["message"]) { + log.Println("Secrets don't match.") + return false, "Secrets don't match." + } + + // Validate signature. + msg := []byte(req["message"]) + decSig, _ := base64.StdEncoding.DecodeString(req["signature"]) + sig := []byte(decSig) + pubkey, err := ParsePubkeyRsa([]byte(pub)) // pubkey is their public key in *rsa.PublicKey type + CheckError(err) + val, _ := VerifyMsgRsa(msg, sig, pubkey) if val != true { log.Println("crypto/rsa: verification failure") - return nil, false + return false, "Signature verification failure." + } + + // The request is valid at this point. + + // Make a new random secret to prevent reuse. + randString, err := GenRandomASCII(64) + CheckError(err) + encodedSecret := base64.StdEncoding.EncodeToString([]byte(randString)) + + var info = map[string]interface{}{ + "nodetype": req["nodetype"], + "address": req["address"], + "message": encodedSecret, + "signature": req["signature"], + "secret": encodedSecret, + "lastseen": time.Now().Unix(), + } // Can not cast, need this for HMSet + + log.Printf("%s: writing to redis\n", req["address"]) + redRet, err := RedisCli.HMSet(req["address"], info).Result() + CheckError(err) + + if redRet != "OK" { + return false, "Internal server error" } - return []byte(pubkey), true + return true, WelcomeMsg }