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