RPC Framework
The traditional key exchange like TLS or SSL needs a CA to ensure key exchange run safely. But in DH-RPC I use a DHT to do that. The main idea is removing CA Cert from the whole system by using a DHT for Naming and Key Exchange.
DH-RPC is a secp256k1-ECDH-AES encrypted P2P RPC framework for decentralized applications written in golang.
CovenantSQL is built on DH-RPC, including:
- Byzantine Fault Tolerance consensus protocol Kayak
- Consistent Secure DHT
- DB API
- Metric Collect
- Blocks sync
Usage
Alice Client:
// Init Key Management System
route.InitKMS(PubKeyStoreFile)
// Register Node public key, addr to Tracker
reqA := &proto.PingReq{
Node: AliceNode,
}
respA := new(proto.PingResp)
rpc.NewCaller().CallNode(Tracker.NodeID, "DHT.Ping", reqA, respA)
pc := rpc.NewPersistentCaller(BobNodeID)
respSimple := new(string)
pc.Call("Test.Talk", "Hi there", respSimple)
fmt.Printf("Response msg: %s", *respSimple)
Bob Server:
// RPC logic
// TestService to be register to RPC server
type TestService struct {}
func (s *TestService) Talk(msg string, ret *string) error {
fmt.Println(msg)
resp := fmt.Sprintf("got msg %s", msg)
*ret = resp
return nil
}
// Init Key Management System
route.InitKMS(PubKeyStoreFile)
// Register DHT service
server, err := rpc.NewServerWithService(rpc.ServiceMap{
"Test": &TestService{},
})
// Init RPC server with an empty master key, which is not recommend
server.InitRPCServer("0.0.0.0:2120", PrivateKeyFile, "")
// Start Node RPC server
server.Serve()
Tracker stuff can refer to the Example section below
Features
- 100% compatible with Go net/rpc standard.
- ID based routing and Key exchange built on Secure Enhanced DHT.
- use MessagePack for serialization which support most types without writing
Marshal
andUnmarshal
. - Crypto Schema
- Use Elliptic Curve Secp256k1 for Asymmetric Encryption
- ECDH for Key Exchange
- PKCS#7 for padding
- AES-256-CFB for Symmetric Encryption
- Private key protected by master key
- Anonymous connection is also supported
- DHT persistence layer has 2 implementations:
- SQLite3 based simple traditional DHT
- Kayak based 2PC strong consistent DHT
- Connection pool based on smux, make thousands of connections multiplexed over One TCP connection.
Stack
DH-RPC := TLS - Cert + DHT
Layer | Implementation |
---|---|
RPC | net/rpc |
Naming | Consistent Secure DHT |
Pooling | Session Pool |
Multiplex | smux |
Transport Security | Enhanced TLS |
Network | TCP or KCP for optional later |
How it works
As we all know, Elliptic Curve Public Key is computed form Private Key
ECPubKey := ECPrivKey.Pub()
DH-RPC node is generated by hash of NodePublicKey and an Uint256 Nonce:
NodeID := sha256(blake2b-512(NodePublicKey + Uint256Nonce))
DHT is used to hold the NodeID
:PublicKey
NodeID
:Addr
map. A RPC connection will do ECDH to get shared secret after TCP connection established.
GenECDHSharedSecret(APub, BPriv) == GenECDHSharedSecret(BPub, APriv)
The main procedure is described as sequence chart below:
So anyone tries to fake NodeB by overwriting the address or public key on DHT without the private key of NodeB will be failed to get the correct shared secret.
Example
The example below is 1 tracker and 2 nodes.
Complete code can be found here
Tracker Code
package main
import (
"os"
"github.com/CovenantSQL/CovenantSQL/conf"
"github.com/CovenantSQL/CovenantSQL/consistent"
"github.com/CovenantSQL/CovenantSQL/route"
"github.com/CovenantSQL/CovenantSQL/rpc"
"github.com/CovenantSQL/CovenantSQL/utils/log"
)
func main() {
//log.SetLevel(log.DebugLevel)
conf.GConf, _ = conf.LoadConfig(os.Args[1])
log.Debugf("GConf: %#v", conf.GConf)
// Init Key Management System
route.InitKMS(conf.GConf.PubKeyStoreFile)
// Creating DHT RPC with simple persistence layer
dht, err := route.NewDHTService(conf.GConf.DHTFileName, new(consistent.KMSStorage), true)
if err != nil {
log.Fatalf("init dht failed: %v", err)
}
// Register DHT service
server, err := rpc.NewServerWithService(rpc.ServiceMap{route.DHTRPCName: dht})
if err != nil {
log.Fatal(err)
}
// Init RPC server with an empty master key, which is not recommend
addr := conf.GConf.ListenAddr
masterKey := []byte("")
server.InitRPCServer(addr, conf.GConf.PrivateKeyFile, masterKey)
server.Serve()
}
Node Code
package main
import (
"bufio"
"fmt"
"os"
"strings"
"github.com/CovenantSQL/CovenantSQL/conf"
"github.com/CovenantSQL/CovenantSQL/proto"
"github.com/CovenantSQL/CovenantSQL/route"
"github.com/CovenantSQL/CovenantSQL/rpc"
"github.com/CovenantSQL/CovenantSQL/utils/log"
)
// TestService to be register to RPC server
type TestService struct {
}
func NewTestService() *TestService {
return &TestService{}
}
func (s *TestService) Talk(msg string, ret *string) error {
fmt.Println(msg)
resp := fmt.Sprintf("got %s", msg)
*ret = resp
return nil
}
func main() {
//log.SetLevel(log.DebugLevel)
conf.GConf, _ = conf.LoadConfig(os.Args[1])
log.Debugf("GConf: %#v", conf.GConf)
// Init Key Management System
route.InitKMS(conf.GConf.PubKeyStoreFile)
// Register DHT service
server, err := rpc.NewServerWithService(rpc.ServiceMap{
"Test": NewTestService(),
})
if err != nil {
log.Fatal(err)
}
// Init RPC server with an empty master key, which is not recommend
addr := conf.GConf.ListenAddr
masterKey := []byte("")
server.InitRPCServer(addr, conf.GConf.PrivateKeyFile, masterKey)
// Start Node RPC server
go server.Serve()
// Register Node public key, addr to Tracker(BP)
for _, n := range conf.GConf.KnownNodes {
client := rpc.NewCaller()
reqA := &proto.PingReq{
Node: n,
}
respA := new(proto.PingResp)
err = client.CallNode(conf.GConf.BP.NodeID, "DHT.Ping", reqA, respA)
if err != nil {
log.Fatal(err)
}
log.Debugf("respA: %v", respA)
}
// Read target node and connect to it
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Input target node ID: ")
scanner.Scan()
if scanner.Err() == nil {
target := proto.NodeID(strings.TrimSpace(scanner.Text()))
pc := rpc.NewPersistentCaller(target)
log.Debugf("connecting to %s", scanner.Text())
fmt.Print("Input msg: ")
for scanner.Scan() {
input := scanner.Text()
log.Debugf("get input %s", input)
repSimple := new(string)
err = pc.Call("Test.Talk", input, repSimple)
if err != nil {
log.Fatal(err)
}
log.Infof("resp msg: %s", *repSimple)
}
}
}
Start tracker and node1, node2
$ ./runTracker.sh &
$ ./runNode2.sh &
$ ./runNode1.sh
$ Input target node ID: 000005aa62048f85da4ae9698ed59c14ec0d48a88a07c15a32265634e7e64ade #node2
$ Input msg: abcdefg