tordam

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

commit 592f384bacf4c4d8c88f2d0be97ece8b00b5dec1
parent 8c81d3a726ed9475be1e68d20ad565c5f99eaaf9
Author: parazyd <parazyd@dyne.org>
Date:   Thu,  1 Nov 2018 12:40:54 +0100

Merge branch 'ed25519'

This merge ports tor-dam to use elliptic curve cryptography instead of
RSA. We keep no backwards compatibility with the state of the code
before this branch is merged. The Tor hidden services are now switched
to v3 and also use the ed25519 curve.

The merge also slightly changes the protocol, which is documented in
the protocol.md file.

Diffstat:
MREADME.md | 12------------
Mcmd/dam-client/main.go | 308++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mcmd/dam-dir/main.go | 49++++++++++++++++---------------------------------
Dcmd/dam-dir/main_test.go | 310-------------------------------------------------------------------------------
Mpkg/damlib/config.go | 14+++++---------
Dpkg/damlib/config_test.go | 31-------------------------------
Mpkg/damlib/crypto_25519.go | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Dpkg/damlib/crypto_25519_test.go | 82-------------------------------------------------------------------------------
Dpkg/damlib/crypto_common.go | 46----------------------------------------------
Dpkg/damlib/crypto_common_test.go | 38--------------------------------------
Dpkg/damlib/crypto_rsa.go | 197-------------------------------------------------------------------------------
Mpkg/damlib/helpers.go | 26+++++++++++++++++++++++++-
Apkg/damlib/helpers_test.go | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpkg/damlib/net_test.go | 51---------------------------------------------------
Mpkg/damlib/redis.go | 27+++++++++++++++++++++++++++
Dpkg/damlib/tor.go | 49-------------------------------------------------
Dpkg/damlib/tor_test.go | 36------------------------------------
Mpkg/damlib/validate.go | 122++++++++++++++++++++++++++-----------------------------------------------------
Apkg/damlib/validate_test.go | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpkg/damlib/zenroom.go | 54------------------------------------------------------
Dpkg/damlib/zenroom.h | 103-------------------------------------------------------------------------------
Dpkg/damlib/zenroom_test.go | 49-------------------------------------------------
Mprotocol.md | 92+++++++++++++++++++++++++++++++++++--------------------------------------------
Mpython/Makefile | 4+---
Dpython/damauth.py | 33---------------------------------
Mpython/damhs.py | 5+----
26 files changed, 597 insertions(+), 1460 deletions(-)

diff --git a/README.md b/README.md @@ -47,15 +47,3 @@ to create hidden services and retrieve hidden service descriptors. Redis is our storage backend where information about nodes is held. Working configurations are provided in the `contrib` directory. - - -#### Zenroom - -``` -https://github.com/decodeproject/zenroom -``` - -The shared library can be acquired by cloning the above repository and -running `make linux-go`, which will compile a `.so` located in -`build/go/libzenroomgo.so`. You will then need to install it on your -system to a place such as `/usr/lib/`. diff --git a/cmd/dam-client/main.go b/cmd/dam-client/main.go @@ -25,7 +25,6 @@ import ( "bytes" "compress/gzip" "crypto/rand" - "crypto/rsa" "encoding/base64" "encoding/json" "flag" @@ -39,42 +38,118 @@ import ( "time" lib "github.com/parazyd/tor-dam/pkg/damlib" + "golang.org/x/crypto/ed25519" ) type msgStruct struct { Secret string } -func clientInit(gen bool) { - err := os.Chmod(lib.PrivKeyPath, 0600) - lib.CheckError(err) +func clientInit(gen bool) error { + pub, priv, err := lib.GenEd25519() + if err != nil { + return err + } + if err := lib.SavePrivEd25519(lib.PrivKeyPath, priv); err != nil { + return err + } + if err := lib.SaveSeedEd25519(lib.SeedPath, priv.Seed()); err != nil { + return err + } + if err := os.Chmod(lib.PrivKeyPath, 0600); err != nil { + return err + } + if err := os.Chmod(lib.SeedPath, 0600); err != nil { + return err + } + onionaddr := lib.OnionFromPubkeyEd25519(pub) + if err := ioutil.WriteFile("hostname", onionaddr, 0600); err != nil { + return err + } + if gen { + log.Println("Our hostname is:", string(onionaddr)) + os.Exit(0) + } + return nil +} - key, err := lib.GenRsa(lib.RsaBits) - lib.CheckError(err) +func fetchNodeList(epLists []string, noremote bool) ([]string, error) { + var nodeslice, nodelist []string - err = lib.SavePrivRsa(lib.PrivKeyPath, key) - lib.CheckError(err) + log.Println("Fetching a list of nodes.") - onionaddr, err := lib.OnionFromPubkeyRsa(key.PublicKey) - lib.CheckError(err) + // Remote network entrypoints + if !(noremote) { + for _, i := range epLists { + log.Println("Fetching", i) + n, err := lib.HTTPDownload(i) + if err != nil { + return nil, err + } + nodeslice = lib.ParseDirs(nodeslice, n) + } + } - err = ioutil.WriteFile("hostname", onionaddr, 0644) - lib.CheckError(err) + // Local ~/.dam/directories.txt + if _, err := os.Stat("directories.txt"); err == nil { + ln, err := ioutil.ReadFile("directories.txt") + if err != nil { + return nil, err + } + nodeslice = lib.ParseDirs(nodeslice, ln) + } - log.Printf("Our hostname is: %s\n", string(onionaddr)) - if gen { - os.Exit(0) + // Local nodes known to Redis + nodes, _ := lib.RedisCli.Keys(".onion").Result() + for _, i := range nodes { + valid, err := lib.RedisCli.HGet(i, "valid").Result() + if err != nil { + // Possible RedisCli bug, possible Redis bug. To be investigated. + // Sometimes it returns err, but it's nil and does not say what's + // happening exactly. + continue + } + if valid == "1" { + nodeslice = append(nodeslice, i) + } + } + + // Remove possible duplicates. Duplicates can cause race conditions and are + // redundant to the entire logic. + encounter := map[string]bool{} + for i := range nodeslice { + encounter[nodeslice[i]] = true + } + nodeslice = []string{} + for key := range encounter { + nodeslice = append(nodeslice, key) } + + if len(nodeslice) < 1 { + log.Fatalln("Couldn't fetch any nodes to announce to. Exiting.") + } else if len(nodeslice) <= 6 { + log.Printf("Found only %d nodes.\n", len(nodeslice)) + nodelist = nodeslice + } else { + log.Println("Found enough directories. Picking out 6 random ones.") + for i := 0; i <= 5; i++ { + n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(nodeslice)))) + nodelist = append(nodelist, nodeslice[n.Int64()]) + nodeslice[n.Int64()] = nodeslice[len(nodeslice)-1] + nodeslice = nodeslice[:len(nodeslice)-1] + } + } + return nodelist, nil } -func announce(dir string, vals map[string]string, privkey *rsa.PrivateKey) (bool, error) { +func announce(node string, vals map[string]string, privkey ed25519.PrivateKey) (bool, error) { msg, err := json.Marshal(vals) if err != nil { return false, err } - log.Println("Announcing keypair to:", dir) - resp, err := lib.HTTPPost("http://"+dir+"/announce", msg) + log.Println("Announcing keypair to:", node) + resp, err := lib.HTTPPost("http://"+node+"/announce", msg) if err != nil { return false, err } @@ -82,59 +157,49 @@ func announce(dir string, vals map[string]string, privkey *rsa.PrivateKey) (bool // Parse server's reply var m msgStruct decoder := json.NewDecoder(resp.Body) - err = decoder.Decode(&m) - if err != nil { + if err := decoder.Decode(&m); err != nil { return false, err } if resp.StatusCode == 400 { - log.Printf("%s: Fail. Reply: %s\n", dir, m.Secret) + log.Printf("%s fail. Reply: %s\n", node, m.Secret) return false, nil } if resp.StatusCode == 200 { - log.Printf("%s: Success. 1/2 handshake valid.\n", dir) - decodedSecret, err := base64.StdEncoding.DecodeString(m.Secret) - if err != nil { - return false, err - } + log.Printf("%s success. 1/2 handshake valid.", node) - decrypted, err := lib.DecryptMsgRsa(decodedSecret, privkey) + sig, err := lib.SignMsgEd25519([]byte(m.Secret), privkey) if err != nil { return false, err } - - decryptedEncode := base64.StdEncoding.EncodeToString(decrypted) - - sig, err := lib.SignMsgRsa([]byte(decryptedEncode), privkey) - lib.CheckError(err) encodedSig := base64.StdEncoding.EncodeToString(sig) - vals["secret"] = decryptedEncode - vals["message"] = decryptedEncode + vals["secret"] = m.Secret + vals["message"] = m.Secret vals["signature"] = encodedSig + msg, err := json.Marshal(vals) if err != nil { return false, err } - log.Printf("%s: Success. Sending back decrypted secret\n", dir) - resp, err := lib.HTTPPost("http://"+dir+"/announce", msg) + log.Printf("%s: success. Sending back signed secret.\n", node) + resp, err := lib.HTTPPost("http://"+node+"/announce", msg) if err != nil { return false, err } decoder = json.NewDecoder(resp.Body) - if err = decoder.Decode(&m); err != nil { + if err := decoder.Decode(&m); err != nil { return false, err } if resp.StatusCode == 200 { - log.Printf("%s: Success. 2/2 handshake valid.\n", dir) - // TODO: To TOFU or not to TOFU? + log.Printf("%s success. 2/2 handshake valid.\n", node) data, err := base64.StdEncoding.DecodeString(m.Secret) if err != nil { // Not a list of nodes. - log.Printf("%s: Reply: %s\n", dir, m.Secret) + log.Printf("%s replied: %s\n", node, m.Secret) return true, nil } log.Println("Got node data. Processing...") @@ -146,7 +211,7 @@ func announce(dir string, vals map[string]string, privkey *rsa.PrivateKey) (bool return false, err } for k, v := range nodes { - log.Printf("Adding %s to redis\n", k) + log.Printf("Adding %s to Redis.\n", k) redRet, err := lib.RedisCli.HMSet(k, v).Result() lib.CheckError(err) if redRet != "OK" { @@ -155,115 +220,43 @@ func announce(dir string, vals map[string]string, privkey *rsa.PrivateKey) (bool } return true, nil } - log.Printf("%s: Fail. Reply: %s\n", dir, m.Secret) + log.Printf("%s fail. Reply: %s\n", node, m.Secret) return false, nil } return false, nil } -func fetchDirlist(locations []string) ([]string, error) { - var dirSlice, dirlist []string - log.Println("Grabbing a list of directories.") - - // Remote network entry points - if !(lib.Noremote) { - for _, i := range locations { - log.Println("Fetching", i) - dirs, err := lib.HTTPDownload(i) - if err != nil { - return nil, err - } - dirSlice = lib.ParseDirs(dirSlice, dirs) - } - } - - // Local ~/.dam/directories.txt - if _, err := os.Stat("directories.txt"); err == nil { - dirs, err := ioutil.ReadFile("directories.txt") - lib.CheckError(err) - dirSlice = lib.ParseDirs(dirSlice, dirs) - } - - // Local nodes known to redis - nodes, err := lib.RedisCli.Keys("*.onion").Result() - lib.CheckError(err) - for _, i := range nodes { - valid, err := lib.RedisCli.HGet(i, "valid").Result() - if err != nil { - // Possible RedisCli bug, possible Redis bug. To be investigated. - // Sometimes it returns err, but it's nil and does not say what's - // happening exactly. - continue - } - if valid == "1" { - dirSlice = append(dirSlice, i) - } - } - - // Remove possible duplicats. Dupes can cause race conditions and are - // redundant to the whole logic. - encounter := map[string]bool{} - for j := range dirSlice { - encounter[dirSlice[j]] = true - } - dirSlice = []string{} - for key := range encounter { - dirSlice = append(dirSlice, key) - } - - if len(dirSlice) < 1 { - log.Fatalln("Couldn't get any directories. Exiting.") - } else if len(dirSlice) <= 6 { - log.Printf("Found only %d directories.\n", len(dirSlice)) - dirlist = dirSlice - } else { - log.Println("Found enough directories. Picking out 6 random ones.") - // Pick out 6 random directories from the retrieved list. - for k := 0; k <= 5; k++ { - n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(dirSlice)))) - dirlist = append(dirlist, dirSlice[n.Int64()]) - dirSlice[n.Int64()] = dirSlice[len(dirSlice)-1] - dirSlice = dirSlice[:len(dirSlice)-1] - } - } - return dirlist, nil -} - func main() { - var d, gen bool + var noremote, gen bool var ai int var dh string - var dirHosts []string - flag.BoolVar(&d, "d", false, "Don't fetch remote entry points.") + flag.BoolVar(&noremote, "d", false, "Don't fetch remote entrypoints.") flag.BoolVar(&gen, "gen", false, "Only (re)generate keypairs and exit cleanly.") flag.IntVar(&ai, "ai", 5, "Announce interval in minutes.") flag.StringVar(&dh, "dh", "https://dam.decodeproject.eu/dirs.txt", - "A remote list of entry points/directories. (comma-separated)") + "Remote lists of entrypoints. (comma-separated)") flag.Parse() - if d { - lib.Noremote = true - } - - // Network entry points. These files hold the lists of directories we can - // announce to. Format is "DIR:22mobp7vrb7a4gt2.onion", other lines are ignored. - dirHosts = strings.Split(dh, ",") + // Network entrypoints. These files hold the lists of nodes we can announce + // to initially. Format is "DIR:unlikelynamefora.onion", other lines are + // ignored and can be used as comments or similar. + epLists := strings.Split(dh, ",") - if _, err := os.Stat(lib.Cwd); os.IsNotExist(err) { - err := os.Mkdir(lib.Cwd, 0700) + if _, err := os.Stat(lib.Workdir); os.IsNotExist(err) { + err := os.Mkdir(lib.Workdir, 0700) lib.CheckError(err) } - err := os.Chdir(lib.Cwd) + err := os.Chdir(lib.Workdir) lib.CheckError(err) if _, err = os.Stat(lib.PrivKeyPath); os.IsNotExist(err) || gen { - clientInit(gen) + err = clientInit(gen) + lib.CheckError(err) } - // Start up the hidden service - log.Println("Starting up the hidden service...") + log.Println("Starting up the hidden service.") cmd := exec.Command("damhs.py", lib.PrivKeyPath, lib.TorPortMap) stdout, err := cmd.StdoutPipe() lib.CheckError(err) @@ -274,14 +267,14 @@ func main() { scanner := bufio.NewScanner(stdout) ok := false go func() { - // If we do not manage to publish our descriptor, we will exit. + // If we do not manage to publish our descriptor, we shall exit. t1 := time.Now().Unix() for !(ok) { t2 := time.Now().Unix() if t2-t1 > 90 { err := cmd.Process.Kill() lib.CheckError(err) - log.Fatalln("Too much time passed. Exiting.") + log.Fatalln("Too much time has passed for publishing descriptor.") } time.Sleep(1000 * time.Millisecond) } @@ -290,44 +283,50 @@ func main() { scanner.Scan() status := scanner.Text() if status == "OK" { - log.Println("Hidden service is now running") + log.Println("Hidden service is now running.") ok = true } } + onionaddr, err := ioutil.ReadFile("hostname") + lib.CheckError(err) + log.Println("Our hostname is:", string(onionaddr)) + for { - key, err := lib.LoadRsaKeyFromFile(lib.PrivKeyPath) - lib.CheckError(err) + log.Println("Announcing to nodes...") + var ann = 0 // Track of successful authentications. + var wg sync.WaitGroup + nodes, err := fetchNodeList(epLists, noremote) + if err != nil { + // No route to host, or failed download. Try later. + log.Println("Failed to fetch any nodes. Retrying in a minute.") + time.Sleep(60 * time.Second) + continue + } - sig, err := lib.SignMsgRsa([]byte(lib.PostMsg), key) + privkey, err := lib.LoadEd25519KeyFromSeed(lib.SeedPath) lib.CheckError(err) - encodedSig := base64.StdEncoding.EncodeToString(sig) - onionAddr, err := lib.OnionFromPubkeyRsa(key.PublicKey) + pubkey := privkey.Public().(ed25519.PublicKey) + onionaddr := lib.OnionFromPubkeyEd25519(pubkey) + encodedPub := base64.StdEncoding.EncodeToString([]byte(pubkey)) + + sig, err := lib.SignMsgEd25519([]byte(lib.PostMsg), privkey) lib.CheckError(err) + encodedSig := base64.StdEncoding.EncodeToString(sig) nodevals := map[string]string{ - "nodetype": "node", - "address": string(onionAddr), + "address": string(onionaddr), + "pubkey": encodedPub, "message": lib.PostMsg, "signature": encodedSig, "secret": "", } - log.Println("Announcing to directories...") - var ann = 0 // Track of how many successful authentications - var wg sync.WaitGroup - dirlist, err := fetchDirlist(dirHosts) - if err != nil { - // No route to host, or failed dl. Try later. - log.Println("Failed to fetch directory list. Retrying in a minute.") - time.Sleep(60 * time.Second) - continue - } - for _, i := range dirlist { + for _, i := range nodes { wg.Add(1) go func(x string) { - valid, err := announce(x, nodevals, key) + valid, err := announce(x, nodevals, privkey) if err != nil { log.Printf("%s: %s\n", x, err.Error()) } @@ -339,15 +338,8 @@ func main() { } wg.Wait() - if ann < 1 { - log.Println("No successful authentications.") - } else { - log.Printf("Successfully authenticated with %d nodes.\n", ann) - } - log.Printf("Waiting %d min. before next announce.\n", ai) + log.Printf("%d successful authentications.\n", ann) + log.Printf("Waiting %d min before next announce.\n", ai) time.Sleep(time.Duration(ai) * time.Minute) } - - //err = cmd.Wait() // Hidden service Python daemon - //lib.CheckError(err) } diff --git a/cmd/dam-dir/main.go b/cmd/dam-dir/main.go @@ -26,7 +26,6 @@ import ( "log" "net/http" "os" - "os/exec" "strconv" "sync" "time" @@ -38,7 +37,6 @@ import ( const ListenAddress = "127.0.0.1:49371" type nodeStruct struct { - Nodetype string Address string Message string Signature string @@ -49,23 +47,6 @@ type nodeStruct struct { Valid int64 } -func startRedis() { - log.Println("Starting up redis-server...") - cmd := exec.Command("redis-server", "/usr/local/share/tor-dam/redis.conf") - err := cmd.Start() - lib.CheckError(err) - - time.Sleep(500 * time.Millisecond) - - _, err = lib.RedisCli.Ping().Result() - lib.CheckError(err) - - PubSub := lib.RedisCli.Subscribe(lib.PubSubChan) - _, err = PubSub.Receive() - lib.CheckError(err) - log.Printf("Created \"%s\" channel in redis\n", lib.PubSubChan) -} - func postback(rw http.ResponseWriter, data map[string]string, retCode int) error { jsonVal, err := json.Marshal(data) if err != nil { @@ -96,8 +77,7 @@ func handlePost(rw http.ResponseWriter, request *http.Request) { } // Bail out as soon as possible. - if len(n.Nodetype) == 0 || len(n.Address) == 0 || - len(n.Message) == 0 || len(n.Signature) == 0 { + if len(n.Address) == 0 || len(n.Message) == 0 || len(n.Signature) == 0 { ret = map[string]string{"secret": "Invalid request format."} if err := postback(rw, ret, 400); err != nil { lib.CheckError(err) @@ -114,9 +94,9 @@ func handlePost(rw http.ResponseWriter, request *http.Request) { } req := map[string]string{ - "nodetype": n.Nodetype, "address": n.Address, "message": n.Message, + "pubkey": n.Pubkey, "signature": n.Signature, "secret": n.Secret, } @@ -127,7 +107,7 @@ func handlePost(rw http.ResponseWriter, request *http.Request) { ret = map[string]string{"secret": msg} if valid { log.Printf("%s: 1/2 handshake valid.\n", n.Address) - log.Println("Sending back encrypted secret.") + log.Println("Sending nonce.") if err := postback(rw, ret, 200); err != nil { lib.CheckError(err) } @@ -240,28 +220,31 @@ func handleElse(rw http.ResponseWriter, request *http.Request) {} func main() { var wg sync.WaitGroup - var t bool var ttl int64 + var redconf string - flag.BoolVar(&t, "t", false, "Mark all new nodes valid initially") + flag.BoolVar(&lib.Testnet, "t", false, "Mark all new nodes valid initially") flag.Int64Var(&ttl, "ttl", 0, "Set expiry time in minutes (TTL) for nodes") + flag.StringVar(&redconf, "redconf", "/usr/local/share/tor-dam/redis.conf", + "Path to redis' redis.conf.") flag.Parse() - if t { - lib.Testnet = true - } - // Chdir to our working directory. - if _, err := os.Stat(lib.Cwd); os.IsNotExist(err) { - err := os.Mkdir(lib.Cwd, 0700) + if _, err := os.Stat(lib.Workdir); os.IsNotExist(err) { + err := os.Mkdir(lib.Workdir, 0700) lib.CheckError(err) } - err := os.Chdir(lib.Cwd) + err := os.Chdir(lib.Workdir) lib.CheckError(err) if _, err := lib.RedisCli.Ping().Result(); err != nil { // We assume redis is not running. Start it up. - startRedis() + _, err := lib.StartRedis(redconf) + lib.CheckError(err) + } + + if lib.Testnet { + log.Println("Will mark all nodes valid by default.") } mux := http.NewServeMux() diff --git a/cmd/dam-dir/main_test.go b/cmd/dam-dir/main_test.go @@ -1,310 +0,0 @@ -package main - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "encoding/base64" - "encoding/json" - "net/http" - "os" - //"os/exec" - "strings" - //"syscall" - "testing" - //"time" - - lib "github.com/parazyd/tor-dam/pkg/damlib" -) - -type msgStruct struct { - Secret string -} - -var ValidFirst = map[string]string{ - "nodetype": "node", - "address": "22mobp7vrb7a4gt2.onion", - "message": "I am a DAM node!", - "signature": "BuB/Dv8E44CLzUX88K2Ab0lUNS9A0GSkHPtrFNNWZMihPMWN0ORhwMZBRnMJ8woPO3wSONBvEvaCXA2hvsVrUJTa+hnevQNyQXCRhdTVVuVXEpjyFzkMamxb6InrGqbsGGkEUqGMSr9aaQ85N02MMrM6T6JuyqSSssFg2xuO+P4=", - "secret": "", -} - -func postReq(data map[string]string) (*http.Response, error) { - msg, err := json.Marshal(data) - if err != nil { - return nil, err - } - resp, err := lib.HTTPPost("http://localhost:49371/announce", msg) - if err != nil { - return nil, err - } - return resp, nil -} - -func getRespText(resp *http.Response) (msgStruct, error) { - var m msgStruct - - decoder := json.NewDecoder(resp.Body) - err := decoder.Decode(&m) - return m, err -} - -func firstAnnValid() (*http.Response, error) { - resp, err := postReq(ValidFirst) - if err != nil { - return nil, err - } - return resp, nil -} - -func TestValidFirstHandshake(t *testing.T) { - //t.SkipNow() - resp, err := firstAnnValid() - if err != nil { - t.Fatal(err) - } - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret == "Could not get a descriptor. Try later." { - t.Skipf("Server replied: %s\n", m.Secret) - } - - decodedSecret, err := base64.StdEncoding.DecodeString(m.Secret) - if err != nil { - t.Fatal(err) - } - if len(decodedSecret) != 128 { - t.Fatal("decodedSecret is not of correct length.") - } - if resp.StatusCode != 200 { - t.Log(resp.StatusCode) - t.Fatal("Server did not respond with HTTP 200") - } - t.Log("Server replied:", m.Secret) -} - -func TestValidSecondHandshake(t *testing.T) { - //t.SkipNow() - resp, err := firstAnnValid() - if err != nil { - t.Fatal(err) - } - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret == "Could not get a descriptor. Try later." { - t.Skipf("Server replied: %s\n", m.Secret) - } - if resp.StatusCode != 200 { - t.Log(resp.StatusCode) - t.Fatal("Server did not respond with HTTP 200") - } - decodedSecret, err := base64.StdEncoding.DecodeString(m.Secret) - if err != nil { - t.Fatal(err) - } - if len(decodedSecret) != 128 { - t.Fatal("decodedSecret is not of correct length.") - } - - // Second handshake starts here. - privkey, err := lib.LoadRsaKeyFromFile("./dam-private.key") - if err != nil { - t.Fatal(err) - } - decrypted, err := lib.DecryptMsgRsa([]byte(decodedSecret), privkey) - if err != nil { - t.Fatal(err) - } - decryptedEncode := base64.StdEncoding.EncodeToString(decrypted) - sig, _ := lib.SignMsgRsa([]byte(decryptedEncode), privkey) - encodedSig := base64.StdEncoding.EncodeToString(sig) - - vals := ValidFirst - vals["secret"] = decryptedEncode - vals["message"] = decryptedEncode - vals["signature"] = encodedSig - - resp, err = postReq(vals) - if err != nil { - t.Fatal(err) - } - m, err = getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret != lib.WelcomeMsg { - t.Fatal(m.Secret) - } - t.Log("Server replied:", m.Secret) -} - -func TestInvalidNodetypeFirst(t *testing.T) { - //t.SkipNow() - var vals = map[string]string{ - "nodetype": "foobar", // Invalid. - "address": "22mobp7vrb7a4gt2.onion", - "message": "I am a DAM node!", - "signature": "BuB/Dv8E44CLzUX88K2Ab0lUNS9A0GSkHPtrFNNWZMihPMWN0ORhwMZBRnMJ8woPO3wSONBvEvaCXA2hvsVrUJTa+hnevQNyQXCRhdTVVuVXEpjyFzkMamxb6InrGqbsGGkEUqGMSr9aaQ85N02MMrM6T6JuyqSSssFg2xuO+P4=", - "secret": "", - } - resp, err := postReq(vals) - if err != nil { - t.Fatal(err) - } - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret != "Invalid nodetype." { - t.Fatal("Server replied:", m.Secret) - } - if resp.StatusCode != 400 { - t.Fatal("Server did not respond with HTTP 400") - } - t.Log("Server replied:", m.Secret) -} - -func TestInvalidAddressFirst(t *testing.T) { - //t.SkipNow() - var vals = map[string]string{ - "nodetype": "node", - "address": "foobar.onion", // Invalid. - "message": "I am a DAM node!", - "signature": "BuB/Dv8E44CLzUX88K2Ab0lUNS9A0GSkHPtrFNNWZMihPMWN0ORhwMZBRnMJ8woPO3wSONBvEvaCXA2hvsVrUJTa+hnevQNyQXCRhdTVVuVXEpjyFzkMamxb6InrGqbsGGkEUqGMSr9aaQ85N02MMrM6T6JuyqSSssFg2xuO+P4=", - "secret": "", - } - resp, err := postReq(vals) - if err != nil { - t.Fatal(err) - } - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret != "Invalid onion address." { - t.Fatal("Server replied:", m.Secret) - } - if resp.StatusCode != 400 { - t.Fatal("Server did not respond with HTTP 400") - } - t.Log("Server replied:", m.Secret) -} - -func TestInvalidMessageFirst(t *testing.T) { - //t.SkipNow() - // Valid message and signature, but the signature did not sign this message. - var vals = map[string]string{ - "nodetype": "node", - "address": "22mobp7vrb7a4gt2.onion", - "message": "I am a MAD node!", // Not matching the below signature. - "signature": "BuB/Dv8E44CLzUX88K2Ab0lUNS9A0GSkHPtrFNNWZMihPMWN0ORhwMZBRnMJ8woPO3wSONBvEvaCXA2hvsVrUJTa+hnevQNyQXCRhdTVVuVXEpjyFzkMamxb6InrGqbsGGkEUqGMSr9aaQ85N02MMrM6T6JuyqSSssFg2xuO+P4=", - "secret": "", - } - resp, err := postReq(vals) - if err != nil { - t.Fatal(err) - } - - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret == "Could not get a descriptor. Try later." { - t.Skipf("Server replied: %s\n", m.Secret) - } - if m.Secret != "Signature verification failure." { - t.Fatal("Server replied:", m.Secret) - } - if resp.StatusCode != 400 { - t.Fatal("Server did not respond with HTTP 400") - } - t.Log("Server replied:", m.Secret) -} - -func TestInvalidSignatureFirst(t *testing.T) { - //t.SkipNow() - // Invalid signature format. - var vals = map[string]string{ - "nodetype": "node", - "address": "22mobp7vrb7a4gt2.onion", - "message": "I am a DAM node!", - "signature": "ThisIsnotbasE64==", // Invalid. - "secret": "", - } - resp, err := postReq(vals) - if err != nil { - t.Fatal(err) - } - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if !(strings.HasPrefix(m.Secret, "illegal base64 data at input byte ")) { - t.Fatal("Server replied:", m.Secret) - } - if resp.StatusCode != 400 { - t.Fatal("Server did not respond with HTTP 400") - } - t.Log("Server replied:", m.Secret) -} - -func TestInvalidSecond(t *testing.T) { - //t.SkipNow() - // Try to jump in the second handshake without doing the first. - // The values below are a valid second handshake, but here we test it - // without doing the first one.. - var vals = map[string]string{ - "nodetype": "node", - "address": "22mobp7vrb7a4gt2.onion", - "message": "ZShhYHYsRGNLOTZ6YUwwP3ZXPnxhQiR9UFVWfmk5TG56TEtLb04vMms+OTIrLlQ7aS4rflR3V041RG5Je0tnYw==", - "signature": "L1N+VEi3T3aZaYksAy1+0UMoYn7B3Gapfk0dJzOUxUtUYVhj84TgfYeDnADNYrt5UK9hN/lCTIhsM6zPO7mSjQI43l3dKvMIikqQDwNey/XaokyPI4/oKrMoGQnu8E8UmHmI1pFvwdO5EQQaKbi90qWNj93KB/NlTwqD9Ir4blY=", - "secret": "ZShhYHYsRGNLOTZ6YUwwP3ZXPnxhQiR9UFVWfmk5TG56TEtLb04vMms+OTIrLlQ7aS4rflR3V041RG5Je0tnYw==", - } - resp, err := postReq(vals) - if err != nil { - t.Fatal(err) - } - m, err := getRespText(resp) - if err != nil { - t.Fatal(err) - } - if m.Secret != "We have not seen you before. Please authenticate properly." { - t.Fatal("Server replied:", m.Secret) - } - if resp.StatusCode != 400 { - t.Fatal("Server did not respond with HTTP 400") - } - t.Log("Server replied:", m.Secret) -} - -func TestMain(m *testing.M) { - //cmd := exec.Command("./dam-dir") - //cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - //cmd.Start() - //time.Sleep(1000 * time.Millisecond) - - ex := m.Run() - //syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - os.Exit(ex) -} diff --git a/pkg/damlib/config.go b/pkg/damlib/config.go @@ -22,15 +22,15 @@ 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 +// Workdir holds the path to the directory where we will Chdir on startup. +var Workdir = os.Getenv("HOME") + "/.dam" // PrivKeyPath holds the name of where our private key is. const PrivKeyPath = "dam-private.key" +// SeedPath holds the name of where our private key seed is. +const SeedPath = "dam-private.seed" + // PubSubChan is the name of the pub/sub channel we're publishing to in Redis. const PubSubChan = "tordam" @@ -53,7 +53,3 @@ const DirPort = 49371 // Testnet is flipped with a flag in dam-dir and represents if all new // nodes are initially marked valid or not. var Testnet = false - -// Noremote is flipped with a flag in dam-client and disables fetching -// remote entry points (directories) if enabled. -var Noremote = false diff --git a/pkg/damlib/config_test.go b/pkg/damlib/config_test.go @@ -1,31 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "os" - "testing" -) - -func TestMain(m *testing.M) { - ex := m.Run() - os.Exit(ex) -} diff --git a/pkg/damlib/crypto_25519.go b/pkg/damlib/crypto_25519.go @@ -21,8 +21,11 @@ package damlib */ import ( + "crypto" "crypto/rand" + "crypto/sha512" "encoding/base32" + "encoding/base64" "io/ioutil" "log" "strings" @@ -34,34 +37,66 @@ import ( // GenEd25519 generates an ed25519 keypair. Returns error on failure. func GenEd25519() (ed25519.PublicKey, ed25519.PrivateKey, error) { log.Println("Generating ed25519 keypair...") - rng := rand.Reader - pk, sk, err := ed25519.GenerateKey(rng) + + pk, sk, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, nil, err } return pk, sk, nil } -// SavePubEd25519 writes a ed25519.PublicKey type to a given string filename. -// Returns error upon failure. -func SavePubEd25519(filename string, key ed25519.PublicKey) error { - log.Println("Writing ed25519 public key to", filename) - const pkprefix = "== ed25519v1-public: type0 ==" - var pub []byte - pub = append(pub, []byte(pkprefix)...) - pub = append(pub, []byte(key)...) - return ioutil.WriteFile(filename, pub, 0600) -} - // SavePrivEd25519 writes a ed25519.PrivateKey type to a given string filename. -// Returns error upon failure. +// Expands ed25519.PrivateKey to (a || RH) form, writing base64. Returns error +// upon failure. func SavePrivEd25519(filename string, key ed25519.PrivateKey) error { log.Println("Writing ed25519 private key to", filename) - const skprefix = "== ed25519v1-secret: type0 ==" - var sec []byte - sec = append(sec, []byte(skprefix)...) - sec = append(sec, []byte(key)...) - return ioutil.WriteFile(filename, sec, 0600) + + h := sha512.Sum512(key[:32]) + // Set bits so that h[:32] is a private scalar "a". + h[0] &= 248 + h[31] &= 127 + h[31] |= 64 + // Since h[32:] is RH, h is now (a || RH) + encoded := base64.StdEncoding.EncodeToString(h[:]) + return ioutil.WriteFile(filename, []byte(encoded), 0600) +} + +// SaveSeedEd25519 saves the ed25519 private key seed to a given string filename +// for later reuse. Returns error upon failure. +func SaveSeedEd25519(filename string, key ed25519.PrivateKey) error { + log.Println("Writing ed25519 private key seed to", filename) + + encoded := base64.StdEncoding.EncodeToString(key.Seed()) + return ioutil.WriteFile(filename, []byte(encoded), 0600) +} + +// LoadEd25519KeyFromSeed loads a key from a given seed file and returns +// ed25519.PrivateKey. Otherwise, on failure, it returns error. +func LoadEd25519KeyFromSeed(filename string) (ed25519.PrivateKey, error) { + log.Println("Loading ed25519 private key from seed in", filename) + + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + decoded, err := base64.StdEncoding.DecodeString(string(data)) + if err != nil { + return nil, err + } + return ed25519.NewKeyFromSeed(decoded), nil +} + +// SignMsgEd25519 signs a message using ed25519. Returns the signature in the +// form of []byte, or returns an error if it fails. +func SignMsgEd25519(message []byte, key ed25519.PrivateKey) ([]byte, error) { + log.Println("Signing message...") + + sig, err := key.Sign(rand.Reader, message, crypto.Hash(0)) + if err != nil { + return nil, err + } + return sig, nil } // OnionFromPubkeyEd25519 generates a valid onion address from a given ed25519 @@ -83,21 +118,19 @@ func SavePrivEd25519(filename string, key ed25519.PrivateKey) error { // - ".onion checksum" is a constant string // - CHECKSUM is truncated to two bytes before inserting it in onion_address func OnionFromPubkeyEd25519(pubkey ed25519.PublicKey) []byte { - const hashConst = ".onion checksum" - const versConst = '\x03' + const salt = ".onion checksum" + const version = byte(0x03) - var h []byte - h = append(h, []byte(hashConst)...) - h = append(h, []byte(pubkey)...) - h = append(h, byte(versConst)) + h := []byte(salt) + h = append(h, pubkey...) + h = append(h, version) csum := sha3.Sum256(h) checksum := csum[:2] - var enc []byte - enc = append(enc, []byte(pubkey)...) + enc := pubkey[:] enc = append(enc, checksum...) - enc = append(enc, byte(versConst)) + enc = append(enc, version) encoded := base32.StdEncoding.EncodeToString(enc) return []byte(strings.ToLower(encoded) + ".onion") diff --git a/pkg/damlib/crypto_25519_test.go b/pkg/damlib/crypto_25519_test.go @@ -1,82 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "os" - "testing" -) - -func TestGenEd25519(t *testing.T) { - _, _, err := GenEd25519() - if err != nil { - t.Fatal("Failed generating ed25519 key:", err.Error()) - } - - t.Log("Successfully generated ed25519 keypair.") -} - -func TestSavePubEd25519(t *testing.T) { - pk, _, err := GenEd25519() - if err != nil { - t.Fatal("Failed generating ed25519 key:", err.Error()) - } - - err = SavePubEd25519("/tmp/ed25519pub.test", pk) - if err != nil { - t.Fatal("Failed saving pubkey:", err.Error()) - } - - os.Remove("/tmp/ed25519pub.test") - t.Log("Success saving ed25519 pubkey") -} - -func TestSavePrivEd25519(t *testing.T) { - _, sk, err := GenEd25519() - if err != nil { - t.Fatal("Failed generating ed25519 key:", err.Error()) - } - - err = SavePrivEd25519("/tmp/ed25519priv.test", sk) - if err != nil { - t.Fatal("Failed saving privkey:", err.Error()) - } - - os.Remove("/tmp/ed25519priv.test") - t.Log("Success saving ed25519 privkey") -} - -func TestOnionFromPubkeyEd25519(t *testing.T) { - pk, _, err := GenEd25519() - if err != nil { - t.Fatal("Failed generating ed25519 key:", err.Error()) - } - - res := OnionFromPubkeyEd25519(pk) - valid := ValidateOnionAddress(string(res)) - - t.Log("Got:", string(res)) - - if !valid { - t.Fatal("Address is invalid.") - } - t.Log("Address is valid") -} diff --git a/pkg/damlib/crypto_common.go b/pkg/damlib/crypto_common.go @@ -1,46 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "crypto/rand" - "math/big" -) - -// GenRandomASCII generates a random ASCII string of a given length. -// Takes length int as argument, and returns a string of that length on success -// and error on failure. -func GenRandomASCII(length int) (string, error) { - var res string - for { - if len(res) >= length { - return res, nil - } - num, err := rand.Int(rand.Reader, big.NewInt(int64(127))) - if err != nil { - return "", err - } - n := num.Int64() - if n > 32 && n < 127 { - res += string(n) - } - } -} diff --git a/pkg/damlib/crypto_common_test.go b/pkg/damlib/crypto_common_test.go @@ -1,38 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "testing" -) - -func TestGenRandomASCII(t *testing.T) { - res, err := GenRandomASCII(18) - if err != nil { - t.Fatal("Failed making random string:", err.Error()) - } - - if len(res) != 18 { - t.Fatal("String is of incorrect length: 18 !=", len(res)) - } - - t.Log("Got:", res) -} diff --git a/pkg/damlib/crypto_rsa.go b/pkg/damlib/crypto_rsa.go @@ -1,197 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha1" - "crypto/sha512" - "crypto/x509" - "encoding/asn1" - "encoding/base32" - "encoding/pem" - "errors" - "io/ioutil" - "log" - "os" - "strings" -) - -// GenRsa generates a private RSA keypair of a given bitSize int and returns it -// as rsa.PrivateKey. -func GenRsa(bitSize int) (*rsa.PrivateKey, error) { - log.Printf("Generating %d-bit RSA keypair...\n", bitSize) - rng := rand.Reader - key, err := rsa.GenerateKey(rng, bitSize) - if err != nil { - return nil, err - } - return key, nil -} - -// SavePubRsa saves a given RSA public key to a given filename. -// SavePubRsa takes the filename to write as a string, and the key as -// rsa.PublicKey. It returns an error on failure, otherwise: nil -func SavePubRsa(filename string, pubkey rsa.PublicKey) error { - log.Println("Writing RSA pubkey to", filename) - outfile, err := os.Create(filename) - if err != nil { - return err - } - defer outfile.Close() - asn1Bytes, err := asn1.Marshal(pubkey) - if err != nil { - return err - } - var pemkey = &pem.Block{ - Type: "RSA PUBLIC KEY", - Bytes: asn1Bytes, - } - if err = pem.Encode(outfile, pemkey); err != nil { - return err - } - return outfile.Chmod(0400) -} - -// SavePrivRsa saves a given RSA private key to a given filename. -// SavePrivRsa takes the filename to write as a string, and the key as -// *rsa.PrivateKey. It returns an error on failure, otherwise: nil -func SavePrivRsa(filename string, privkey *rsa.PrivateKey) error { - log.Printf("Writing private key to %s\n", filename) - outfile, err := os.Create(filename) - if err != nil { - return err - } - defer outfile.Close() - var pemkey = &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: x509.MarshalPKCS1PrivateKey(privkey), - } - if err = pem.Encode(outfile, pemkey); err != nil { - return err - } - return outfile.Chmod(0400) -} - -// LoadRsaKeyFromFile loads a RSA private key from a given filename. -// LoadRsaKeyFromFile takes a string filename and tries to read from it, parsing -// the private RSA key. It will return a *rsa.PrivateKey on success, and error -// on fail. -func LoadRsaKeyFromFile(filename string) (*rsa.PrivateKey, error) { - log.Println("Loading RSA private key from", filename) - dat, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - block, _ := pem.Decode(dat) - if block == nil { - return nil, errors.New("failed to parse PEM block containing the key") - } - priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, err - } - return priv, nil -} - -// SignMsgRsa signs a given []byte message using a given RSA private key. -// It will return the signature as a slice of bytes on success, and error on -// failure. -func SignMsgRsa(message []byte, privkey *rsa.PrivateKey) ([]byte, error) { - log.Println("Signing message...") - rng := rand.Reader - hashed := sha512.Sum512(message) - sig, err := rsa.SignPKCS1v15(rng, privkey, crypto.SHA512, hashed[:]) - if err != nil { - return nil, err - } - return sig, nil -} - -// EncryptMsgRsa encrypts a given []byte message using a given RSA public key. -// Returns the encrypted message as a slice of bytes on success, and error on -// failure. -func EncryptMsgRsa(message []byte, pubkey *rsa.PublicKey) ([]byte, error) { - log.Println("Encrypting message...") - rng := rand.Reader - msg, err := rsa.EncryptPKCS1v15(rng, pubkey, message) - if err != nil { - return nil, err - } - return msg, nil -} - -// DecryptMsgRsa decrypts a given []byte message using a given RSA private key. -// Returns the decrypted message as a slice of bytes on success, and error on -// failure. -func DecryptMsgRsa(message []byte, privkey *rsa.PrivateKey) ([]byte, error) { - log.Println("Decrypting message...") - rng := rand.Reader - msg, err := rsa.DecryptPKCS1v15(rng, privkey, message) - if err != nil { - return nil, err - } - return msg, nil -} - -// VerifyMsgRsa verifies a message and signature against a given RSA pubkey. -// Returns a boolean value and error depending on whether it has failed or not. -func VerifyMsgRsa(message []byte, signature []byte, pubkey *rsa.PublicKey) (bool, error) { - log.Println("Verifying message signature") - hashed := sha512.Sum512(message) - if err := rsa.VerifyPKCS1v15(pubkey, crypto.SHA512, hashed[:], signature); err != nil { - return false, err - } - return true, nil -} - -// OnionFromPubkeyRsa generates a valid onion address from a given RSA pubkey. -// Returns the onion address as a slice of bytes on success and error on -// failure. -func OnionFromPubkeyRsa(pubkey rsa.PublicKey) ([]byte, error) { - asn1Bytes, err := asn1.Marshal(pubkey) - if err != nil { - return nil, err - } - hashed := sha1.New() - if _, err = hashed.Write(asn1Bytes); err != nil { - return nil, err - } - encoded := strings.ToLower(base32.StdEncoding.EncodeToString(hashed.Sum(nil)))[:16] - encoded += ".onion" - - return []byte(encoded), nil -} - -// ParsePubkeyRsa parses a []byte form of a RSA public key and returns it as -// *rsa.PublicKey on success. Otherwise, error. -func ParsePubkeyRsa(pubkey []byte) (*rsa.PublicKey, error) { - var pub rsa.PublicKey - var ret *rsa.PublicKey - block, _ := pem.Decode(pubkey) - if _, err := asn1.Unmarshal(block.Bytes, &pub); err != nil { - return nil, err - } - ret = &pub - return ret, nil -} diff --git a/pkg/damlib/helpers.go b/pkg/damlib/helpers.go @@ -23,8 +23,10 @@ package damlib import ( "bytes" "compress/gzip" + "crypto/rand" "encoding/base64" "log" + "math/big" "strings" ) @@ -73,9 +75,31 @@ func ParseDirs(sl []string, data []byte) []string { if strings.HasPrefix(j, "DIR:") { t := strings.Split(j, "DIR:") if !(StringInSlice(t[1], sl)) { - sl = append(sl, t[1]) + if ValidateOnionAddress(t[1]) { + sl = append(sl, t[1]) + } } } } return sl } + +// GenRandomASCII generates a random ASCII string of a given length. +// Takes length int as argument, and returns a string of that length on success +// and error on failure. +func GenRandomASCII(length int) (string, error) { + var res string + for { + if len(res) >= length { + return res, nil + } + num, err := rand.Int(rand.Reader, big.NewInt(int64(127))) + if err != nil { + return "", err + } + n := num.Int64() + if n > 32 && n < 127 { + res += string(n) + } + } +} diff --git a/pkg/damlib/helpers_test.go b/pkg/damlib/helpers_test.go @@ -0,0 +1,65 @@ +package damlib + +/* + * Copyright (c) 2018 Dyne.org Foundation + * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> + * + * This file is part of tor-dam + * + * This source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code. If not, see <http://www.gnu.org/licenses/>. + */ + +import ( + "testing" +) + +func TestStringInSlice(t *testing.T) { + sl := []string{"foo", "bar", "baz"} + if !(StringInSlice("bar", sl)) { + t.Fatal("\"bar\" should be in the slice.") + } + if StringInSlice("kek", sl) { + t.Fatal("\"kek\" should not be in the slice.") + } +} + +func TestGzipEncode(t *testing.T) { + data := "Compress this string" + if _, err := GzipEncode([]byte(data)); err != nil { + t.Fatal(err) + } +} + +func TestParseDirs(t *testing.T) { + var sl []string + data := `DIR:gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion +# Some random data +DIR:vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion` + + sl = ParseDirs(sl, []byte(data)) + + if len(sl) != 2 { + t.Fatal("Length of slice is not 2.") + } +} + +func TestGenRandomASCII(t *testing.T) { + res, err := GenRandomASCII(64) + if err != nil { + t.Fatal(err) + } + if len(res) != 64 { + t.Fatal("Length of ASCII string is not 64.") + } +} diff --git a/pkg/damlib/net_test.go b/pkg/damlib/net_test.go @@ -1,51 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "io/ioutil" - "testing" -) - -func TestHTTPPost(t *testing.T) { - data := []byte("foobar") - - resp, err := HTTPPost("https://requestb.in/ykdug2yk", data) - if err != nil { - t.Fatal("Unable to HTTPPost:", err.Error()) - } - - res, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal("Unable to read response:", err.Error()) - } - - t.Log("Got:", string(res)) -} - -func TestHTTPDownload(t *testing.T) { - data, err := HTTPDownload("https://requestb.in/ykdug2yk") - if err != nil { - t.Fatal("Unable to HTTPDownload:", err.Error()) - } - - t.Log("Got:", string(data)) -} diff --git a/pkg/damlib/redis.go b/pkg/damlib/redis.go @@ -22,6 +22,9 @@ package damlib import ( "fmt" + "log" + "os/exec" + "time" "github.com/go-redis/redis" ) @@ -36,6 +39,30 @@ var RedisCli = redis.NewClient(&redis.Options{ DB: 0, }) +// StartRedis is the function that will start up the Redis server. Takes the +// path to a configuration file as an argument and returns error upon failure. +func StartRedis(conf string) (*exec.Cmd, error) { + log.Println("Starting up redis-server...") + cmd := exec.Command("redis-server", conf) + err := cmd.Start() + if err != nil { + return cmd, err + } + + time.Sleep(500 * time.Millisecond) + if _, err := RedisCli.Ping().Result(); err != nil { + return cmd, err + } + + PubSub := RedisCli.Subscribe(PubSubChan) + if _, err := PubSub.Receive(); err != nil { + return cmd, err + } + + log.Printf("Created \"%s\" channel in Redis.\n", PubSubChan) + return cmd, nil +} + // PublishToRedis is a function that publishes a node's status to Redis. // This is used for Gource visualization. func PublishToRedis(mt, address string) { diff --git a/pkg/damlib/tor.go b/pkg/damlib/tor.go @@ -1,49 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "bytes" - "log" - "os/exec" -) - -// FetchHSPubkey fetches a hidden service's RSA pubkey by running an external -// program, giving it an onion address. It returns the retrieved public key as a -// string. -func FetchHSPubkey(addr string) string { - var outb, errb bytes.Buffer - - log.Println("Fetching pubkey for:", addr) - - cmd := exec.Command("damauth.py", addr) - cmd.Stdout = &outb - cmd.Stderr = &errb - err := cmd.Start() - CheckError(err) - - if err = cmd.Wait(); err != nil { - log.Println("Could not fetch descriptor:", err) - return "" - } - - return outb.String() -} diff --git a/pkg/damlib/tor_test.go b/pkg/damlib/tor_test.go @@ -1,36 +0,0 @@ -package damlib - -/* - * Copyright (c) 2017-2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "strings" - "testing" -) - -func TestFetchHSPubkey(t *testing.T) { - pubkey := FetchHSPubkey("facebookcorewwwi.onion") - - if !strings.HasPrefix(pubkey, "-----BEGIN") { - t.Fatal("Did not get a public key.") - } - - t.Log("Got:", pubkey) -} diff --git a/pkg/damlib/validate.go b/pkg/damlib/validate.go @@ -22,21 +22,18 @@ package damlib import ( "encoding/base64" - "errors" "log" "regexp" - "strings" "time" + + "golang.org/x/crypto/ed25519" ) // 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](?:.{55}|.{15})\.onion`) - if len(re.FindString(addr)) == 22 || len(re.FindString(addr)) == 62 { - return true - } - return false + re, _ := regexp.Compile(`^[a-z2-7](?:.{55})\.onion`) + return len(re.FindString(addr)) == 62 } // sanityCheck performs basic sanity checks against the incoming request. @@ -50,17 +47,6 @@ func sanityCheck(req map[string]string, handshake int) (bool, string) { 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." - } - if handshake == 2 { if _, err := base64.StdEncoding.DecodeString(req["message"]); err != nil { return false, err.Error() @@ -72,69 +58,49 @@ func sanityCheck(req map[string]string, handshake int) (bool, string) { return true, "" } -// ValidateFirstHandshake 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. +// ValidateFirstHandshake 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 take it from the initial request from the +// incoming node. 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 ValidateFirstHandshake(req map[string]string) (bool, string) { if sane, what := sanityCheck(req, 1); !(sane) { return false, what } // Get the public key. - var pub string + var pubstr string + var pubkey ed25519.PublicKey // 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() + pubstr, 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 false, "Could not get a descriptor. Try later." - } - 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) - } + // We take it from the announce. + pubstr = req["pubkey"] } // Validate signature. msg := []byte(req["message"]) - decSig, _ := base64.StdEncoding.DecodeString(req["signature"]) - sig := decSig - pubkey, err := ParsePubkeyRsa([]byte(pub)) // pubkey is their public key in *rsa.PublicKey type + sig, _ := base64.StdEncoding.DecodeString(req["signature"]) + deckey, err := base64.StdEncoding.DecodeString(pubstr) CheckError(err) - if val, _ := VerifyMsgRsa(msg, sig, pubkey); !(val) { - log.Println("crypto/rsa: verification failure") - return false, "Signature verification failure." + pubkey = ed25519.PublicKey(deckey) + + if !(ed25519.Verify(pubkey, msg, sig)) { + log.Println("crypto/ed25519: Signature verification failure.") + return false, "Signature verification vailure." } // The request is valid at this point. @@ -145,7 +111,6 @@ func ValidateFirstHandshake(req map[string]string) (bool, string) { encodedSecret := base64.StdEncoding.EncodeToString([]byte(randString)) var info = map[string]interface{}{ - "nodetype": req["nodetype"], "address": req["address"], "message": encodedSecret, "signature": req["signature"], @@ -153,7 +118,7 @@ func ValidateFirstHandshake(req map[string]string) (bool, string) { "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["pubkey"] = pubstr info["firstseen"] = time.Now().Unix() if Testnet { info["valid"] = 1 @@ -170,11 +135,7 @@ func ValidateFirstHandshake(req map[string]string) (bool, string) { return false, "Internal server error" } - encryptedSecret, err := EncryptMsgRsa([]byte(randString), pubkey) - CheckError(err) - - encryptedEncodedSecret := base64.StdEncoding.EncodeToString(encryptedSecret) - return true, encryptedEncodedSecret + return true, encodedSecret } // ValidateSecondHandshake validates the second part of the handshake. @@ -183,7 +144,7 @@ func ValidateFirstHandshake(req map[string]string) (bool, string) { // 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 +// have saved before. Upon proving they are the same, the 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 @@ -196,19 +157,16 @@ func ValidateSecondHandshake(req map[string]string) (bool, string) { } // Get the public key. - var pub string + var pubstr string + var pubkey ed25519.PublicKey // 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() + pubstr, 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 { log.Printf("%s tried to jump in 2/2 handshake before doing the first.\n", req["address"]) return false, "We have not seen you before. Please authenticate properly." @@ -224,12 +182,13 @@ func ValidateSecondHandshake(req map[string]string) (bool, string) { // Validate signature. msg := []byte(req["message"]) - decSig, _ := base64.StdEncoding.DecodeString(req["signature"]) - sig := decSig - pubkey, err := ParsePubkeyRsa([]byte(pub)) // pubkey is their public key in *rsa.PublicKey type + sig, _ := base64.StdEncoding.DecodeString(req["signature"]) + deckey, err := base64.StdEncoding.DecodeString(pubstr) CheckError(err) - if val, _ := VerifyMsgRsa(msg, sig, pubkey); !(val) { - log.Printf("%s: Signature verification failure\n", req["address"]) + pubkey = ed25519.PublicKey(deckey) + + if !(ed25519.Verify(pubkey, msg, sig)) { + log.Println("crypto/ed25519: Signature verification failure") return false, "Signature verification failure." } @@ -241,7 +200,6 @@ func ValidateSecondHandshake(req map[string]string) (bool, string) { encodedSecret := base64.StdEncoding.EncodeToString([]byte(randString)) var info = map[string]interface{}{ - "nodetype": req["nodetype"], "address": req["address"], "message": encodedSecret, "signature": req["signature"], diff --git a/pkg/damlib/validate_test.go b/pkg/damlib/validate_test.go @@ -0,0 +1,165 @@ +package damlib + +/* + * Copyright (c) 2018 Dyne.org Foundation + * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> + * + * This file is part of tor-dam + * + * This source code is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this source code. If not, see <http://www.gnu.org/licenses/>. + */ + +import ( + "encoding/base64" + "testing" +) + +func TestValidateOnionAddress(t *testing.T) { + if !(ValidateOnionAddress("gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion")) { + t.Fatal("Validating a valid address failed.") + } + if ValidateOnionAddress("gphjf5g3d5ywe1wd.onion") { + t.Fatal("Validating an invalid address succeeded.") + } +} + +func TestValidValidateFirstHandshake(t *testing.T) { + req := map[string]string{ + "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion", + "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=", + "message": "I am a DAM node!", + "signature": "CWqptO9ZRIvYMIHd3XHXaVny+W23P8FGkfbn5lvUqeJbDcY3G8+B4G8iCCIQiZkxkMofe6RbstHn3L1x88c3AA==", + "secret": "", + } + + cmd, _ := StartRedis("../../contrib/redis.conf") + valid, _ := ValidateFirstHandshake(req) + if !(valid) { + t.Fatal("Failed to validate first handshake.") + } + cmd.Process.Kill() +} + +func TestInvalidValidateFirstHandshake(t *testing.T) { + // Invalid message for this signature. + req := map[string]string{ + "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion", + "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=", + "message": "I am a bad DAM node!", + "signature": "CWqptO9ZRIvYMIHd3XHXaVny+W23P8FGkfbn5lvUqeJbDcY3G8+B4G8iCCIQiZkxkMofe6RbstHn3L1x88c3AA==", + "secret": "", + } + + cmd, _ := StartRedis("../../contrib/redis.conf") + valid, _ := ValidateFirstHandshake(req) + if valid { + t.Fatal("Invalid request passed as valid.") + } + cmd.Process.Kill() +} + +func TestValidValidateSecondHandshake(t *testing.T) { + cmd, _ := StartRedis("../../contrib/redis.conf") + + pk, sk, _ := GenEd25519() + onionaddr := OnionFromPubkeyEd25519(pk) + + sig, err := SignMsgEd25519([]byte("I am a DAM node!"), sk) + if err != nil { + t.Fatal(err) + } + encodedSig := base64.StdEncoding.EncodeToString(sig) + encodedPub := base64.StdEncoding.EncodeToString([]byte(pk)) + + req := map[string]string{ + "address": string(onionaddr), + "pubkey": encodedPub, + "message": "I am a DAM node!", + "signature": encodedSig, + "secret": "", + } + + valid, secret := ValidateFirstHandshake(req) + if !(valid) { + t.Fatal("Failed on first handshake.") + } + + sig, err = SignMsgEd25519([]byte(secret), sk) + if err != nil { + t.Fatal(err) + } + encodedSig = base64.StdEncoding.EncodeToString(sig) + req = map[string]string{ + "address": string(onionaddr), + "pubkey": encodedPub, + "message": secret, + "signature": encodedSig, + "secret": secret, + } + + valid, _ = ValidateSecondHandshake(req) + if !(valid) { + t.Fatal("Failed to validate second handshake.") + } + cmd.Process.Kill() +} + +func TestInValidValidateSecondHandshake(t *testing.T) { + cmd, _ := StartRedis("../../contrib/redis.conf") + + pk, sk, _ := GenEd25519() + onionaddr := OnionFromPubkeyEd25519(pk) + + sig, err := SignMsgEd25519([]byte("I am a DAM node!"), sk) + if err != nil { + t.Fatal(err) + } + encodedSig := base64.StdEncoding.EncodeToString(sig) + encodedPub := base64.StdEncoding.EncodeToString([]byte(pk)) + + req := map[string]string{ + "address": string(onionaddr), + "pubkey": encodedPub, + "message": "I am a DAM node!", + "signature": encodedSig, + "secret": "", + } + + valid, secret := ValidateFirstHandshake(req) + if !(valid) { + t.Fatal("Failed on first handshake.") + } + + sig, err = SignMsgEd25519([]byte(secret), sk) + if err != nil { + t.Fatal(err) + } + encodedSig = base64.StdEncoding.EncodeToString(sig) + + secret = "We're malicious!" + + req = map[string]string{ + "address": string(onionaddr), + "pubkey": encodedPub, + "message": secret, + "signature": encodedSig, + "secret": secret, + } + + valid, _ = ValidateSecondHandshake(req) + if !(valid) { + t.Fatal("Failed to validate second handshake.") + } + cmd.Process.Kill() +} diff --git a/pkg/damlib/zenroom.go b/pkg/damlib/zenroom.go @@ -1,54 +0,0 @@ -package damlib - -/* - * Copyright (c) 2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -// #cgo LDFLAGS: -lzenroomgo -// -// #include <stddef.h> -// #include "zenroom.h" -import "C" - -import ( - "bytes" - "unsafe" -) - -// ZenroomExec is Zenroom's simple API call. -func ZenroomExec(script, conf, keys, data string, verbosity int) int { - return int(C.zenroom_exec(C.CString(script), C.CString(conf), - C.CString(keys), C.CString(data), C.int(verbosity))) -} - -// ZenroomExecToBuf is Zenroom's simple API call with buffers. It will return -// stdout and stderr. -func ZenroomExecToBuf(script, conf, keys, data string, verbosity int) (int, []byte, []byte) { - //var bufsize = 1024 * 8 - var bufsize = 1024 - - outbuf := make([]byte, bufsize) - errbuf := make([]byte, bufsize) - - return int(C.zenroom_exec_tobuf(C.CString(script), C.CString(conf), - C.CString(keys), C.CString(data), C.int(verbosity), - (*C.char)(unsafe.Pointer(&outbuf[0])), C.size_t(bufsize), - (*C.char)(unsafe.Pointer(&errbuf[0])), C.size_t(bufsize))), - bytes.Trim(outbuf, "\x00"), bytes.Trim(errbuf, "\x00") -} diff --git a/pkg/damlib/zenroom.h b/pkg/damlib/zenroom.h @@ -1,103 +0,0 @@ -/* Zenroom (DECODE project) - * - * (c) Copyright 2017-2018 Dyne.org foundation - * designed, written and maintained by Denis Roio <jaromil@dyne.org> - * - * This source code is free software; you can redistribute it and/or - * modify it under the terms of the GNU Public License as published - * by the Free Software Foundation; either version 3 of the License, - * or (at your option) any later version. - * - * This source code is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * Please refer to the GNU Public License for more details. - * - * You should have received a copy of the GNU Public License along - * with this source code; if not, write to: Free Software Foundation, - * Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - */ - -#ifndef __ZENROOM_H__ -#define __ZENROOM_H__ - -///////////////////////////////////////// -// high level api: one simple call - -int zenroom_exec(char *script, char *conf, char *keys, - char *data, int verbosity); - -// in case buffers should be used instead of stdout/err file -// descriptors, this call defines where to print out the output and -// the maximum sizes allowed for it. Output is NULL terminated. -int zenroom_exec_tobuf(char *script, char *conf, char *keys, - char *data, int verbosity, - char *stdout_buf, size_t stdout_len, - char *stderr_buf, size_t stderr_len); - -// to obtain the Abstract Syntax Tree (AST) of a script -// (output is in metalua formatted as JSON) -int zenroom_parse_ast(char *script, int verbosity, - char *stdout_buf, size_t stdout_len, - char *stderr_buf, size_t stderr_len); - -void set_debug(int lev); - -//////////////////////////////////////// - - -// lower level api: init (exec_line*) teardown - -// heap initialised by the memory manager -typedef struct { - void* (*malloc)(size_t size); - void* (*realloc)(void *ptr, size_t size); - void (*free)(void *ptr); - void* (*sys_malloc)(size_t size); - void* (*sys_realloc)(void *ptr, size_t size); - void (*sys_free)(void *ptr); - char *heap; - size_t heap_size; -} zen_mem_t; - -// zenroom context, also available as "_Z" global in lua space -// contents are opaque in lua and available only as lightuserdata -typedef struct { - void *lua; // (lua_State*) - zen_mem_t *mem; // memory manager heap - - char *stdout_buf; - size_t stdout_len; - size_t stdout_pos; - char *stderr_buf; - size_t stderr_len; - size_t stderr_pos; - - int errorlevel; - void *userdata; // anything passed at init (reserved for caller) -} zenroom_t; - - -zenroom_t *zen_init(const char *conf, char *keys, char *data); -int zen_exec_script(zenroom_t *Z, const char *script); -void zen_teardown(zenroom_t *zenroom); - -#define UMM_HEAP (64*1024) // 64KiB (masked with 0x7fff) -#define MAX_FILE (64*1024) // load max 64KiB files -#ifndef MAX_STRING -#define MAX_STRING 4097 // max 4KiB strings -#endif -#define MAX_OCTET 2049 // max 2KiB octets - -#define LUA_BASELIBNAME "_G" - -#define ZEN_BITS 8 -#ifndef SIZE_MAX -#if ZEN_BITS == 32 -#define SIZE_MAX 4294967296 -#elif ZEN_BITS == 8 -#define SIZE_MAX 65536 -#endif -#endif - -#endif diff --git a/pkg/damlib/zenroom_test.go b/pkg/damlib/zenroom_test.go @@ -1,49 +0,0 @@ -package damlib - -/* - * Copyright (c) 2018 Dyne.org Foundation - * tor-dam is written and maintained by Ivan J. <parazyd@dyne.org> - * - * This file is part of tor-dam - * - * This source code is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this source code. If not, see <http://www.gnu.org/licenses/>. - */ - -import ( - "testing" -) - -func TestZenroomExec(t *testing.T) { - ret := ZenroomExec(`print ("hello")`, "", "", "", 1) - if ret != 0 { - t.Fatal("ZenroomExec returned:", ret) - } - - t.Log("ZenroomExec returned:", ret) -} - -func TestZenroomExecToBuf(t *testing.T) { - ret, stdout, _ := ZenroomExecToBuf(`print ("hello")`, "", "", "", 1) - if ret != 0 { - t.Fatal("ZenroomExec returned:", ret) - } - - if string(stdout) != "hello" { - t.Log("ZenroomExecToBuf stdout is not 'hello'") - t.Log("Stdout is rather", string(stdout)) - t.Fatal("ZenroomExecToBuf returned:", ret) - } - - t.Log("ZenroomExecToBuf returned:", ret) -} diff --git a/protocol.md b/protocol.md @@ -7,33 +7,32 @@ Abstract * Every node has a HTTP API allowing to list other nodes and announce new ones. * They keep propagating to all valid nodes they know. -* Announcing implies the need of knowledge of at least one or two nodes. +* Announcing implies the need of knowledge of at least one node. * It is possible to make this random enough once there are at least 6 nodes in the network. * A node announces itself to others by sending a JSON-formatted HTTP POST request to one or more active node. * Once the POST request is received, the node will validate the - request and return a secret encrypted with the requester's public - key. - * The requester will try to decrypt this secret, and return it plain - back to the node it's announcing to, along with a cryptographic - signature, so the node can confirm the requester is in actual - possession of the private key. + request and return a random string (nonce) back to the requester for + them to sign with their cryptographic key. + * The requester will try to sign this nonce and return it back to the + node it's announcing to, so the node can confirm the requester is in + actual posession of the private key. * Tor DAM **does not validate** if a node is malicious or not. This is a - layer that has to be established on top. Tor DAM is just the entry - point into the network. + layer that has to be established with external software. Protocol -------- A node announcing itself has to do a JSON-formatted HTTP POST request to -one or more active nodes with the format explained below. N.B. The +one or more active nodes with the format explained below. **N.B.** The strings shown in this document might not be valid, but they represent a correct example. -* `type` reflects the type of the node -* `address` holds the address of the Tor hidden service +* `address` holds the address of the Tor hidden service. +* `pubkey` is the base64 encoded ed25519 public key of the Tor hidden + service. * `message` is the message that has to be signed using the private key of this same hidden service. * `signature` is the base64 encoded signature of the above message. @@ -43,58 +42,48 @@ correct example. ``` { - "type": "node", - "address": "22mobp7vrb7a4gt2.onion", - "message": "I am a DAM node!", - "signature": "BuB/Dv8E44CLzUX88K2Ab0lUNS9A0GSkHPtrFNNWZMihPMWN0ORhwMZBRnMJ8woPO3wSONBvEvaCXA2hvsVrUJTa+hnevQNyQXCRhdTVVuVXEpjyFzkMamxb6InrGqbsGGkEUqGMSr9aaQ85N02MMrM6T6JuyqSSssFg2xuO+P4=", + "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion", + "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=", + "message" "I am a DAM node!", + "signature": "CWqptO9ZRIvYMIHd3XHXaVny+W23P8FGkfbn5lvUqeJbDcY3G8+B4G8iCCIQiZkxkMofe6RbstHn3L1x88c3AA==", "secret": "" } ``` -Sending this as a POST request to a node will make it ask for the public -key of the given address from a HSDir in the Tor network. It will -retrieve the public key and try to validate the signature that was made. -Validating this, we assume that the requester is in possession of the -private key. - -Following up, the node shall generate a cryptographically secure random -string and encrypt it using the before acquired public key. It will then -be encoded using base64 and sent back to the client: +Sending this as a POST request to a node will make it verify the +signature, and following that, the node will generate a +cryptographically secure random string, encode it using base64 and +return it back to the client for them to sign: ``` { - "secret": "eP07xSZWlDdK4+AL0WUkIA3OnVTc3sEgu4MUqGr43TUXaJLfAILvWxKihPxytumBmdJ4LC45LsrdDuhmUSmZZMJxxiLmB4Gf3zoWa1DmStdc147VsGpexY05jaJUZlbmG0kkTFdPmdcKNbis5xfRn8Duo1e5bOPj41lIopwiil0=" + "secret": "NmtDOEsrLGI8eCk1TyxOfXcwRV5lI0Y5fnhbXlAhV1dGfTl8K2JAYEQrU2lAJ2UuJ2kjQF15Q30+SWVXTkFnXw==" } ``` -The client will try to decode and decrypt this secret, and send it back -to the node to complete its part of the handshake. The POST request this -time will contain the following data: -* `type` reflects the type of the node -* `address` holds the address of the Tor hidden service -* `message` is the decrypted and base64 encoded secret that the server - had just sent us. -* `signature` is the base64 encoded signature of the above secret. -* `secret` is a copy of `message` here. +The client will try to decode and sign this secret. Then it will be +reencoded using base64 and sent back for verification to complete its +part of the handshake. The POST request this time will contain the +following data: ``` { - "type": "node", - "address": "22mobp7vrb7a4gt2.onion", - "message": "ZShhYHYsRGNLOTZ6YUwwP3ZXPnxhQiR9UFVWfmk5TG56TEtLb04vMms+OTIrLlQ7aS4rflR3V041RG5Je0tnYw==", - "signature": "L1N+VEi3T3aZaYksAy1+0UMoYn7B3Gapfk0dJzOUxUtUYVhj84TgfYeDnADNYrt5UK9hN/lCTIhsM6zPO7mSjQI43l3dKvMIikqQDwNey/XaokyPI4/oKrMoGQnu8E8UmHmI1pFvwdO5EQQaKbi90qWNj93KB/NlTwqD9Ir4blY=", - "secret": "ZShhYHYsRGNLOTZ6YUwwP3ZXPnxhQiR9UFVWfmk5TG56TEtLb04vMms+OTIrLlQ7aS4rflR3V041RG5Je0tnYw==" + "address": "gphjf5g3d5ywehwrd7cv3czymtdc6ha67bqplxwbspx7tioxt7gxqiid.onion", + "pubkey": "M86S9NsfcWIe0R/FXYs4ZMYvHB74YPXewZPv+aHXn80=", + "message": "NFU5PXU4LT4xVy5NW303IWo1SD0odSohOHEvPThoemM3LHdcW3NVcm1TU3RAPGM8Pi1UUXpKIipWWnlTUk5kIg==", + "signature": "1cocZey3KpuRDfRrKcI3tc4hhJpwfXU3BC3o3VE8wkkCpCFJ5Xl3wl58GLSVS4BdbDAFrf+KFpjtDLhOuSMYAw==", + "secret": "NFU5PXU4LT4xVy5NW303IWo1SD0odSohOHEvPThoemM3LHdcW3NVcm1TU3RAPGM8Pi1UUXpKIipWWnlTUk5kIg==" } ``` -The node will verify the received plain secret against what it has -encrypted to validate. If the comparison yields no errors, we assume -that the requester is actually in possession of the private key. If the -node is not valid in our database, we will complete the handshake by -welcoming the client into the network: +The node will verify the received secret against the public key it has +archived already. If the verification yields no errors, we assume that +the requester is actually in possession of the private key. If the node +is not valid in our database, we will complete the handshake by +welcoming the client into the network: ``` { @@ -102,13 +91,14 @@ welcoming the client into the network: } ``` -Further on, the node will append useful metadata to the struct. We will + +Further on, the node will append useful metadata to the struct. We will add the encoded public key, timestamps of when the client was first seen -and last seen, and a field to indicate if the node is valid. The latter -is not to be handled by Tor DAM, but rather the upper layer, which +and last seen, and a field to indicate if the node is valid. The latter +is not to be handled by Tor DAM, but rather an upper layer, which actually has consensus handling. -If the node is valid in another node's database, the remote node will -then propagate back all the valid nodes it knows (including itself) back +If a requesting/announcing node is valid in another node's database, the +remote node will then propagate back all the valid nodes it knows back to the client in a gzipped and base64 encoded JSON struct. The client -will then handle this and update its own database accordingly. +will then process this and update its own database accordingly. diff --git a/python/Makefile b/python/Makefile @@ -2,9 +2,7 @@ PREFIX ?= /usr/local -BIN =\ - damhs.py \ - damauth.py +BIN = damhs.py all: @echo 'Run "make install" to install to $(DESTDIR)$(PREFIX)/bin' diff --git a/python/damauth.py b/python/damauth.py @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# Copyright (c) 2017-2018 Dyne.org Foundation -# tor-dam is writen and maintained by Ivan J. <parazyd@dyne.org> -# -# This file is part of tor-dam -# -# This source code is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this source code. If not, see <http://www.gnu.org/licenses/>. - -""" -Retrieves and prints a hidden service's public key to stdout. - -Usage: damauth.py <foo.onion> -""" - -from sys import argv, stdout -from stem.control import Controller - - -with Controller.from_port() as ctl: - ctl.authenticate(password='topkek') - stdout.write(ctl.get_hidden_service_descriptor(argv[1]).permanent_key) - stdout.flush() diff --git a/python/damhs.py b/python/damhs.py @@ -54,11 +54,8 @@ def main(): portmap[int(tup[0])] = int(tup[1]) keyfile = argv[1] - ktype = 'RSA1024' # ED25519-V3 + ktype = 'ED25519-V3' kcont = open(keyfile).read() - kcont = kcont.replace('\n', '') - kcont = kcont.replace('-----BEGIN RSA PRIVATE KEY-----', '') - kcont = kcont.replace('-----END RSA PRIVATE KEY-----', '') service = start_hs(ctl=ctl, ktype=ktype, kcont=kcont, portmap=portmap)