├── docker ├── quorum-crux │ ├── passwords.txt │ ├── start.sh │ ├── scripts │ │ ├── test_transaction.sh │ │ └── simpleContract.js │ ├── istanbul-init.sh │ ├── bootstrap.sh │ ├── crux-start.sh │ ├── istanbul-start.sh │ ├── Dockerfile │ ├── istanbul-genesis.json │ ├── docker-compose.yaml │ └── docker-compose-local.yaml └── crux │ ├── start.sh │ ├── Dockerfile │ └── docker-compose.yml ├── enclave ├── testdata │ ├── key.pub │ ├── rcpt1.pub │ ├── rcpt2.pub │ ├── key │ ├── rcpt1 │ └── cert │ │ ├── server.csr │ │ ├── server.crt │ │ └── server.key ├── enclave_test.go └── enclave.go ├── test ├── test1 │ └── testdata │ │ ├── key.pub │ │ └── key ├── test2 │ └── testdata │ │ ├── rcpt1.pub │ │ └── rcpt1 └── client_test.go ├── docs ├── quorum-architecture.png ├── read-tx.mermaid ├── new-tx.mermaid ├── quorum-architecture.xml ├── read-tx.svg └── new-tx.svg ├── .gitignore ├── utils ├── math.go ├── hash.go ├── http.go ├── url.go ├── file_test.go ├── math_test.go ├── key.go ├── file.go └── url_test.go ├── storage ├── datastore.go ├── leveldb.go └── berkleydb.go ├── api ├── internal_test.go ├── client.go ├── encoding_test.go ├── encoding.go └── internal.go ├── CHANGELOG.md ├── .travis.yml ├── .gitlab-ci.yml ├── Gopkg.toml ├── config ├── config_test.go ├── config.go └── config_testdata.conf ├── crux.go ├── server ├── proto_server.go ├── server_handler.go ├── server.go └── server_test.go ├── Makefile ├── README.md ├── Gopkg.lock └── LICENSE /docker/quorum-crux/passwords.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /enclave/testdata/key.pub: -------------------------------------------------------------------------------- 1 | zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg= -------------------------------------------------------------------------------- /enclave/testdata/rcpt1.pub: -------------------------------------------------------------------------------- 1 | I/EbshW61ykJ+qTivXPaKyQ5WmQDUFfMNGEBj2E2uUs= -------------------------------------------------------------------------------- /enclave/testdata/rcpt2.pub: -------------------------------------------------------------------------------- 1 | evN1e8V503XDjXOovu7YeD3eD4+/0GiSvbxQPJutQiw= -------------------------------------------------------------------------------- /test/test1/testdata/key.pub: -------------------------------------------------------------------------------- 1 | zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg= -------------------------------------------------------------------------------- /test/test2/testdata/rcpt1.pub: -------------------------------------------------------------------------------- 1 | I/EbshW61ykJ+qTivXPaKyQ5WmQDUFfMNGEBj2E2uUs= -------------------------------------------------------------------------------- /enclave/testdata/key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"W1n0C+NfjcU/cUBXsP5FQ/frU+qpvKQ7Pi/Mu5Hf/Ic="},"type":"unlocked"} -------------------------------------------------------------------------------- /docs/quorum-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3labs/crux/HEAD/docs/quorum-architecture.png -------------------------------------------------------------------------------- /enclave/testdata/rcpt1: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"9e0UrkhdfGY0kBYuk3Nv3g4FlYXjTHpXORWO2r1An/A="},"type":"unlocked"} -------------------------------------------------------------------------------- /test/test1/testdata/key: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"W1n0C+NfjcU/cUBXsP5FQ/frU+qpvKQ7Pi/Mu5Hf/Ic="},"type":"unlocked"} -------------------------------------------------------------------------------- /test/test2/testdata/rcpt1: -------------------------------------------------------------------------------- 1 | {"data":{"bytes":"9e0UrkhdfGY0kBYuk3Nv3g4FlYXjTHpXORWO2r1An/A="},"type":"unlocked"} -------------------------------------------------------------------------------- /docker/quorum-crux/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./istanbul-init.sh 4 | ./istanbul-start.sh 5 | 6 | tail -f /dev/null -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | *.iml 3 | .idea/ 4 | 5 | # OS X 6 | .DS_Store 7 | 8 | /.GOPATH 9 | /bin 10 | /vendor 11 | /test/test1 12 | /test/test2 13 | 14 | -------------------------------------------------------------------------------- /utils/math.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func NextPowerOf2(v int) int { 4 | v-- 5 | v |= v >> 1 6 | v |= v >> 2 7 | v |= v >> 4 8 | v |= v >> 8 9 | v |= v >> 16 10 | v++ 11 | return v 12 | } 13 | -------------------------------------------------------------------------------- /docker/quorum-crux/scripts/test_transaction.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Sending private transaction" 3 | PRIVATE_CONFIG=qdata/c/tm.ipc geth --exec "loadScript(\"simpleContract.js\")" attach ipc:qdata/dd/geth.ipc 4 | -------------------------------------------------------------------------------- /utils/hash.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "golang.org/x/crypto/sha3" 4 | 5 | func Sha3Hash(payload []byte) []byte { 6 | sha3Hash := sha3.New512() 7 | sha3Hash.Write(payload) 8 | return sha3Hash.Sum(nil) 9 | } 10 | -------------------------------------------------------------------------------- /utils/http.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/http" 4 | 5 | // HttpClient is an interface for sending synchronous HTTP requests. 6 | type HttpClient interface { 7 | Do(req *http.Request) (*http.Response, error) 8 | } 9 | -------------------------------------------------------------------------------- /docker/crux/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo $CRUX_PUB >> key.pub 4 | echo $CRUX_PRIV >> key.priv 5 | CMD="./bin/crux --url=http://$OWN_URL:$PORT/ --port=$PORT --networkinterface 0.0.0.0 --publickeys=key.pub --privatekeys=key.priv --othernodes=$OTHER_NODES --verbosity=3" 6 | $CMD >> "crux.log" 2>&1 -------------------------------------------------------------------------------- /docs/read-tx.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 3 | participant Quorum 4 | participant Crux 5 | 6 | Client->>Quorum: getQuorumPayload(transactionHash, to) 7 | Quorum->>Crux: receive(transactionHash, to) 8 | Crux-->>Quorum: receiveResponse(transaction) 9 | Quorum->>Client: transaction 10 | -------------------------------------------------------------------------------- /docs/new-tx.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 3 | participant Quorum 4 | participant Crux 5 | participant Remote Crux 6 | 7 | Client->>Quorum: send(transaction, privateFor) 8 | Quorum->>Crux: send(transaction, privateFor) 9 | Crux->>Remote Crux: push(encryptedPayload) 10 | Crux-->>Quorum: sendResponse(transactionHash) 11 | -------------------------------------------------------------------------------- /storage/datastore.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | // DataStore is an interface that facilitates operations with an underlying persistent data store. 4 | type DataStore interface { 5 | Write(key *[]byte, value *[]byte) error 6 | Read(key *[]byte) (*[]byte, error) 7 | ReadAll(f func(key, value *[]byte)) error 8 | Delete(key *[]byte) error 9 | Close() error 10 | } 11 | -------------------------------------------------------------------------------- /utils/url.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "net/url" 4 | 5 | func BuildUrl(rawUrl, rawPath string) (string, error) { 6 | baseUrl, err := url.Parse(rawUrl) 7 | if err != nil { 8 | return "", err 9 | } 10 | 11 | path, err := url.Parse(rawPath) 12 | 13 | if err != nil { 14 | return "", err 15 | } 16 | 17 | return baseUrl.ResolveReference(path).String(), nil 18 | } 19 | -------------------------------------------------------------------------------- /utils/file_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | ) 7 | 8 | func TestCreateIpcSocket(t *testing.T) { 9 | dbPath, err := ioutil.TempDir("", "TestCreateIpcSocket") 10 | if err != nil { 11 | t.Error(err) 12 | } 13 | 14 | listener, err := CreateIpcSocket(dbPath) 15 | 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | 20 | if listener == nil { 21 | t.Errorf("Listener not initialised") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docker/quorum-crux/istanbul-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | echo "[*] Cleaning up temporary data directories" 6 | rm -rf qdata 7 | mkdir -p qdata/logs 8 | 9 | echo "[*] Configuring node" 10 | mkdir -p qdata/dd/{keystore,geth} 11 | echo $PERMISSIONED_NODES >> qdata/dd/static-nodes.json 12 | echo $PERMISSIONED_NODES >> qdata/dd/permissioned-nodes.json 13 | echo $GETH_KEY >> qdata/dd/keystore/key 14 | geth --datadir qdata/dd init istanbul-genesis.json 15 | -------------------------------------------------------------------------------- /docker/quorum-crux/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu -o pipefail 3 | 4 | # make/install quorum 5 | git clone https://github.com/ConsenSys/quorum.git 6 | pushd quorum >/dev/null 7 | git checkout tags/v2.0.3-grpc 8 | make all 9 | cp build/bin/geth /usr/local/bin 10 | cp build/bin/bootnode /usr/local/bin 11 | rm -r build 12 | popd >/dev/null 13 | 14 | # make/install crux 15 | git clone https://github.com/blk-io/crux.git 16 | cd crux 17 | git checkout tags/v1.0.3 18 | make setup && make 19 | cp bin/crux /usr/local/bin 20 | rm -r bin 21 | -------------------------------------------------------------------------------- /utils/math_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestNextPowerOf2(t *testing.T) { 6 | 7 | values := map[int]int{ 8 | 0: 0, 9 | 1: 1, 10 | 2: 2, 11 | 3: 4, 12 | 4: 4, 13 | 5: 8, 14 | 7: 8, 15 | 8: 8, 16 | 9: 16, 17 | 16: 16, 18 | 17: 32, 19 | 1023: 1024, 20 | 1024: 1024, 21 | 1025: 2048, 22 | } 23 | 24 | for value, expected := range values { 25 | if NextPowerOf2(value) != expected { 26 | t.Errorf("Next power of 2 for %d, does not equal %d", value, expected) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /utils/key.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "github.com/kevinburke/nacl" 7 | ) 8 | 9 | func ToKey(src []byte) (nacl.Key, error) { 10 | if len(src) != nacl.KeySize { 11 | return nil, fmt.Errorf("nacl: incorrect key length: %d", len(src)) 12 | } 13 | key := new([nacl.KeySize]byte) 14 | copy(key[:], src) 15 | return key, nil 16 | } 17 | 18 | func LoadBase64Key(key string) (nacl.Key, error) { 19 | src, err := base64.StdEncoding.DecodeString(key) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return ToKey(src) 25 | } 26 | -------------------------------------------------------------------------------- /utils/file.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | func CreateIpcSocket(path string) (net.Listener, error) { 10 | err := CreateDirForFile(path) 11 | if err != nil { 12 | return nil, err 13 | } 14 | os.Remove(path) 15 | 16 | var listener net.Listener 17 | listener, err = net.Listen("unix", path) 18 | if err != nil { 19 | return nil, err 20 | } 21 | os.Chmod(path, 0600) 22 | 23 | return listener, nil 24 | } 25 | 26 | func CreateDirForFile(path string) error { 27 | return os.MkdirAll(filepath.Dir(path), os.FileMode(0755)) 28 | } 29 | -------------------------------------------------------------------------------- /docker/quorum-crux/crux-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | DDIR="qdata/c" 6 | mkdir -p $DDIR 7 | mkdir -p qdata/logs 8 | echo $CRUX_PUB >> "$DDIR/tm.pub" 9 | echo $CRUX_PRIV >> "$DDIR/tm.key" 10 | rm -f "$DDIR/tm.ipc" 11 | CMD="crux --url=http://$OWN_URL:$PORT/ --port=$PORT --networkinterface 0.0.0.0 --workdir=$DDIR --socket=tm.ipc --publickeys=tm.pub --privatekeys=tm.key --othernodes=$OTHER_NODES --verbosity=3" 12 | $CMD >> "qdata/logs/crux.log" 2>&1 & 13 | 14 | DOWN=true 15 | while $DOWN; do 16 | sleep 0.1 17 | DOWN=false 18 | if [ ! -S "qdata/c/tm.ipc" ]; then 19 | DOWN=true 20 | fi 21 | done 22 | -------------------------------------------------------------------------------- /api/internal_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/kevinburke/nacl" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestRegisterPublicKeys(t *testing.T) { 10 | key := []nacl.Key{nacl.NewKey()} 11 | 12 | pi := CreatePartyInfo( 13 | "http://localhost:9000", 14 | []string{"http://localhost:9001"}, 15 | key, 16 | http.DefaultClient) 17 | 18 | expKey := []nacl.Key{nacl.NewKey()} 19 | expUrl := "http://localhost:9000" 20 | 21 | pi.RegisterPublicKeys(expKey) 22 | 23 | url, ok := pi.GetRecipient(expKey[0]) 24 | if !ok || url != expUrl { 25 | t.Errorf("Url is %s whereas %s is expected", url, expUrl) 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.3 - 2018-10-17 2 | ### Added 3 | - Network interface paramater to configuration 4 | ### Changed 5 | - Fix formatting of entire project using `go fmt` 6 | - Fix "apk WARNING Ignoring APKINDEX" 7 | - Fix sending payload to multiple recipients 8 | 9 | ## 1.0.2 - 2018-09-06 10 | ### Added 11 | - Delete and Resend API from Chimera included. 12 | 13 | ## 1.0.1 - 2018-08-22 14 | ### Changed 15 | - Fix issue with config file load 16 | 17 | ## 1.0.0 - 2018-07-05 18 | ### Added 19 | - Crux, a secure enclave for Quorum written in Golang 20 | - Protobuf and gRPC support. 21 | - TLS support 22 | - Docker images 23 | -------------------------------------------------------------------------------- /docker/quorum-crux/istanbul-start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | mkdir -p qdata/logs 6 | echo "[*] Starting Crux nodes" 7 | ./crux-start.sh 8 | 9 | echo "[*] Starting Ethereum nodes" 10 | set -v 11 | ARGS="--syncmode full --mine --rpc --rpcaddr 0.0.0.0 --rpcapi admin,db,eth,debug,miner,net,shh,txpool,personal,web3,quorum,istanbul " 12 | ARGS=$ARGS"--txpool.globalslots 20000 --txpool.globalqueue 20000 --istanbul.blockperiod 1 " 13 | # ARGS=$ARGS"--cache 4096 --trie-cache-gens 1000 " 14 | PRIVATE_CONFIG=qdata/c/tm.ipc nohup geth --datadir qdata/dd $ARGS --rpcport $GETH_RPC_PORT --port $GETH_PORT --nodekeyhex $NODE_KEY --unlock 0 --password passwords.txt --verbosity=6 2>>qdata/logs/node.log & 15 | set +v 16 | 17 | 18 | -------------------------------------------------------------------------------- /utils/url_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestBuildUrl(t *testing.T) { 6 | runUrlTest(t, "http://localhost:9001/", "/endpoint", "http://localhost:9001/endpoint") 7 | runUrlTest(t, "http://localhost:9001", "/endpoint", "http://localhost:9001/endpoint") 8 | runUrlTest(t, "http://localhost:9001", "endpoint", "http://localhost:9001/endpoint") 9 | runUrlTest(t, "http://localhost:9001//", "/endpoint", "http://localhost:9001/endpoint") 10 | } 11 | 12 | func runUrlTest(t *testing.T, baseUrl, path, expected string) { 13 | url, err := BuildUrl(baseUrl, path) 14 | 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | 19 | if url != expected { 20 | t.Errorf("Url created: %s, does not match expected: %s", url, expected) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docker/crux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | 3 | RUN apk update --no-cache && \ 4 | # Update and then install dependencies 5 | apk add unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ 6 | apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add leveldb && \ 7 | apk add build-base cmake boost-dev git 8 | 9 | ENV CRUX_PUB="" 10 | ENV CRUX_PRIV="" 11 | ENV OWN_URL="" 12 | ENV OTHER_NODES="" 13 | ENV PORT="" 14 | 15 | RUN git clone https://github.com/blk-io/crux.git 16 | 17 | WORKDIR /crux 18 | 19 | RUN make setup && \ 20 | make build && \ 21 | apk del sed make git cmake build-base gcc g++ musl-dev curl-dev boost-dev 22 | # fails https://github.com/golang/go/issues/14481 23 | # RUN make test 24 | 25 | EXPOSE 9000 26 | 27 | COPY start.sh start.sh 28 | RUN chmod +x start.sh 29 | 30 | ENTRYPOINT ["./start.sh"] -------------------------------------------------------------------------------- /docker/crux/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | node1: 4 | image: blk.io/quorum/crux 5 | build: 6 | context: . 7 | container_name: crux1 8 | ports: 9 | - 9001:9000 10 | environment: 11 | - CRUX_PUB=BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= 12 | - CRUX_PRIV={"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} 13 | - OTHER_NODES=http://node2:9000/ 14 | - OWN_URL=node1 15 | - PORT=9000 16 | 17 | node2: 18 | image: blk.io/quorum/crux 19 | build: 20 | context: . 21 | container_name: crux2 22 | ports: 23 | - 9002:9000 24 | environment: 25 | - CRUX_PUB=QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= 26 | - CRUX_PRIV={"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} 27 | - OTHER_NODES=http://node1:9000/ 28 | - OWN_URL=node2 29 | - PORT=9000 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10.x" 5 | - tip 6 | 7 | env: 8 | REPO_NAME: github.com/blk-io/crux 9 | 10 | before_install: 11 | - sudo apt-get update -qq && sudo apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. 12 | - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) 13 | - ln -svf $TRAVIS_BUILD_DIR $GOPATH/src/$REPO_NAME 14 | - cd $GOPATH/src/$REPO_NAME 15 | - curl -OL https://github.com/google/protobuf/releases/download/v3.5.1/protoc-3.5.1-linux-x86_64.zip 16 | - unzip protoc-3.5.1-linux-x86_64.zip -d protoc3 17 | - sudo mv protoc3/bin/* /usr/local/bin/ 18 | - sudo mv protoc3/include/* /usr/local/include/ 19 | - rm -r protoc3 20 | - rm protoc-3.5.1-linux-x86_64.zip 21 | - go get -u github.com/golang/protobuf/protoc-gen-go 22 | - go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway 23 | - make setup 24 | 25 | script: 26 | - make build 27 | - make test 28 | - make cover 29 | 30 | after_success: 31 | - bash <(curl -s https://codecov.io/bash) 32 | -------------------------------------------------------------------------------- /enclave/testdata/cert/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICsTCCAZkCAQAwVDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0G 3 | A1UEBwwGTG9uZG9uMQ8wDQYDVQQKDAZibGstaW8xEjAQBgNVBAMMCTEyNy4wLjAu 4 | MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALSS/JHKnE9f9pU7jw3T 5 | Zint6aaj7jSEzV5BArjBs0GzDk/R+55AvE+428iFTywiAdeUBO3xxxEzCZ8OOLkf 6 | fJ95Vza0giWcR3/+F9na+1kCOuc2H/fiIvB8DMzGQxjukVLdIaCjl/F5Uyn7gKYK 7 | b7Trcryf/HkaaT3eA4TahC1dOhhq4q/WmkaWariM9QlFyJDfvYiL1XkmOuiiC6vD 8 | +yvd71dBtugDpIcNgmAZ+rVn4ZoC3Tc16OzeqrvxquF1YO0QakGqiGskZA6NTm+V 9 | L/GcubnFweKXkrvisTcqUleoGQM9TibbVQf/A0DamAVrauRs6rxva5JqW7Gh4o5d 10 | BUcCAwEAAaAYMBYGCSqGSIb3DQEJBzEJDAdibGtjcnV4MA0GCSqGSIb3DQEBCwUA 11 | A4IBAQBYRPFuhVoNhYBVoO4qaNet7W4JAZ2YkpTdE/SdECyBAt4lX4OPV+lFYrUD 12 | AjPf98IjwVwNijz6DboqEx+1rUKzLEV/Jv8k0O8noJDR3fRkFU3cyIDcW/N9Wwcc 13 | p3qHwRuSvv+QmDoikJeCR8iN61jeo6WG2KKB4GUETK35SMDS31ExkWHNa/WIFzrV 14 | MTB/Vy2yH0xgxhDRa8sEbsGrvf/XnhKKGNbffmTp46dBItBkDKeiHh2FGI6F8CBv 15 | bkhB0/7fC8Hi4aoBMSdT88L8Y7+mVMNfybmCuSr1foK+gMSS7UweLn2oPEMohFZW 16 | 4mkS8TD1bUkavlCrhVuExCSLVXLu 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /storage/leveldb.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "github.com/syndtr/goleveldb/leveldb" 5 | ) 6 | 7 | type levelDb struct { 8 | dbPath string 9 | conn *leveldb.DB 10 | } 11 | 12 | func InitLevelDb(dbPath string) (*levelDb, error) { 13 | db := new(levelDb) 14 | db.dbPath = dbPath 15 | var err error 16 | db.conn, err = leveldb.OpenFile(dbPath, nil) 17 | return db, err 18 | } 19 | 20 | func (db *levelDb) Write(key *[]byte, value *[]byte) error { 21 | return db.conn.Put(*key, *value, nil) 22 | } 23 | 24 | func (db *levelDb) Read(key *[]byte) (*[]byte, error) { 25 | value, err := db.conn.Get(*key, nil) 26 | if err == nil { 27 | return &value, err 28 | } else { 29 | return nil, err 30 | } 31 | } 32 | 33 | func (db *levelDb) ReadAll(f func(key, value *[]byte)) error { 34 | iter := db.conn.NewIterator(nil, nil) 35 | for iter.Next() { 36 | key, value := iter.Key(), iter.Value() 37 | f(&key, &value) 38 | } 39 | iter.Release() 40 | return iter.Error() 41 | } 42 | 43 | func (db *levelDb) Delete(key *[]byte) error { 44 | return db.conn.Delete(*key, nil) 45 | } 46 | 47 | func (db *levelDb) Close() error { 48 | return db.conn.Close() 49 | } 50 | -------------------------------------------------------------------------------- /enclave/testdata/cert/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDJDCCAgwCCQDmw4WXCHzRUTANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJV 3 | SzEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xDzANBgNVBAoMBmJs 4 | ay1pbzESMBAGA1UEAwwJMTI3LjAuMC4xMB4XDTE4MDYxNDE0Mzg1NFoXDTI4MDYx 5 | MTE0Mzg1NFowVDELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UE 6 | BwwGTG9uZG9uMQ8wDQYDVQQKDAZibGstaW8xEjAQBgNVBAMMCTEyNy4wLjAuMTCC 7 | ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALSS/JHKnE9f9pU7jw3TZint 8 | 6aaj7jSEzV5BArjBs0GzDk/R+55AvE+428iFTywiAdeUBO3xxxEzCZ8OOLkffJ95 9 | Vza0giWcR3/+F9na+1kCOuc2H/fiIvB8DMzGQxjukVLdIaCjl/F5Uyn7gKYKb7Tr 10 | cryf/HkaaT3eA4TahC1dOhhq4q/WmkaWariM9QlFyJDfvYiL1XkmOuiiC6vD+yvd 11 | 71dBtugDpIcNgmAZ+rVn4ZoC3Tc16OzeqrvxquF1YO0QakGqiGskZA6NTm+VL/Gc 12 | ubnFweKXkrvisTcqUleoGQM9TibbVQf/A0DamAVrauRs6rxva5JqW7Gh4o5dBUcC 13 | AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAYHeXraxdqSm5PTYrWqVks+5ctTv/tO4w 14 | cEtleZHy86qnmtu8gqHH+tAKVjn/JNGvYNX/M0oMPjxn0LffGCH6MhC9072RDA10 15 | XC+yUQx2tm5Q8RRVjb/S+6GczAqXVE6Qc59zgTaFtE8xc/J/wW6XPhJ99Jly2oba 16 | y/6UMvPfCLtf7pA99V4lXat0oRYtVbFkyxIGVl0Gx2COJx65UUtUs+Grf41CQCYr 17 | VXSUTl6wTNj6llAMDkgyFobmfwWzr0DWJJSDycHuC5oxdIe6nhLRq1EgU5DHwyGD 18 | Mz9tLPJHGpJPX8ml4U/XuiYHNDt5hJYZ0veJ6fFM3iSBMCfBIh4Npg== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /docker/quorum-crux/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.8 2 | 3 | RUN apk update --no-cache && \ 4 | # Update and then install dependencies 5 | apk add unzip db zlib wrk wget libsodium-dev go bash libpthread-stubs db-dev && \ 6 | apk -X http://dl-cdn.alpinelinux.org/alpine/edge/testing add leveldb && \ 7 | apk add build-base cmake boost-dev git 8 | 9 | ENV PORT="" 10 | ENV NODE_KEY="" 11 | ENV CRUX_PUB="" 12 | ENV GETH_KEY="" 13 | ENV OWN_URL="" 14 | ENV CRUX_PRIV="" 15 | ENV OTHER_NODES="" 16 | ENV GETH_RPC_PORT="" 17 | ENV GETH_PORT="" 18 | 19 | WORKDIR /quorum 20 | 21 | COPY bootstrap.sh bootstrap.sh 22 | COPY istanbul-genesis.json istanbul-genesis.json 23 | COPY passwords.txt passwords.txt 24 | COPY istanbul-init.sh istanbul-init.sh 25 | COPY crux-start.sh crux-start.sh 26 | COPY istanbul-start.sh istanbul-start.sh 27 | COPY start.sh start.sh 28 | COPY scripts/simpleContract.js simpleContract.js 29 | COPY scripts/test_transaction.sh test_transaction.sh 30 | 31 | RUN chmod +x start.sh crux-start.sh istanbul-start.sh istanbul-init.sh && \ 32 | chmod +x test_transaction.sh && \ 33 | chmod +x bootstrap.sh && \ 34 | ./bootstrap.sh && \ 35 | apk del sed make git cmake build-base gcc g++ musl-dev curl-dev boost-dev 36 | 37 | EXPOSE 9000 21000 22000 38 | 39 | # Entrypoint for container 40 | ENTRYPOINT ["./start.sh"] 41 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: golang:latest 2 | 3 | variables: 4 | REPO_NAME: github.com/blk-io/crux 5 | 6 | # The problem is that to be able to use go get, one needs to put 7 | # the repository in the $GOPATH. So for example if your gitlab domain 8 | # is gitlab.com, and that your repository is namespace/project, and 9 | # the default GOPATH being /go, then you'd need to have your 10 | # repository in /go/src/gitlab.com/namespace/project 11 | # Thus, making a symbolic link corrects this. 12 | before_script: 13 | - apt-get update -qq && apt-get install -y -qq libdb-dev libpthread-stubs0-dev # This is hopefully temporary until we completely remove BerkeleyDB. 14 | - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) 15 | - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME 16 | - cd $GOPATH/src/$REPO_NAME 17 | - go get ./... 18 | - make setup 19 | 20 | stages: 21 | - build 22 | - test 23 | 24 | format: 25 | stage: test 26 | script: 27 | - go fmt $(go list ./... | grep -v /vendor/) 28 | - go vet $(go list ./... | grep -v /vendor/) 29 | - make test 30 | 31 | compile: 32 | stage: build 33 | script: 34 | - make build 35 | artifacts: 36 | paths: 37 | - crux 38 | 39 | code_coverage: 40 | stage: test 41 | script: 42 | - rm -f $GOPATH/cover/*.out $GOPATH/cover/all.merged 43 | - mkdir -p $GOPATH/cover 44 | - make cover 45 | 46 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/jsimonetti/berkeleydb" 31 | 32 | [[constraint]] 33 | name = "github.com/kevinburke/nacl" 34 | version = "0.5.0" 35 | 36 | [[constraint]] 37 | name = "github.com/sirupsen/logrus" 38 | version = "1.0.5" 39 | 40 | [[constraint]] 41 | name = "github.com/spf13/pflag" 42 | version = "1.0.0" 43 | 44 | [[constraint]] 45 | name = "github.com/spf13/viper" 46 | version = "1.0.2" 47 | 48 | [[constraint]] 49 | branch = "master" 50 | name = "github.com/syndtr/goleveldb" 51 | 52 | [[constraint]] 53 | branch = "master" 54 | name = "golang.org/x/crypto" 55 | 56 | [prune] 57 | go-tests = true 58 | 59 | [[constraint]] 60 | name = "github.com/grpc-ecosystem/grpc-gateway" 61 | version = "1.4.0" 62 | 63 | [[constraint]] 64 | name = "github.com/blk-io/chimera-api" 65 | revision = "ebd4db90873296427420c2fe2acec18c127b401d" 66 | -------------------------------------------------------------------------------- /enclave/testdata/cert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAtJL8kcqcT1/2lTuPDdNmKe3ppqPuNITNXkECuMGzQbMOT9H7 3 | nkC8T7jbyIVPLCIB15QE7fHHETMJnw44uR98n3lXNrSCJZxHf/4X2dr7WQI65zYf 4 | 9+Ii8HwMzMZDGO6RUt0hoKOX8XlTKfuApgpvtOtyvJ/8eRppPd4DhNqELV06GGri 5 | r9aaRpZquIz1CUXIkN+9iIvVeSY66KILq8P7K93vV0G26AOkhw2CYBn6tWfhmgLd 6 | NzXo7N6qu/Gq4XVg7RBqQaqIayRkDo1Ob5Uv8Zy5ucXB4peSu+KxNypSV6gZAz1O 7 | JttVB/8DQNqYBWtq5GzqvG9rkmpbsaHijl0FRwIDAQABAoIBAFx/iteXxRg33RU2 8 | zCHV71h9IZoWEThf6t9kR+OifZOjCCrFMrIvEQd5d0QxXYwK44ytqxTELCfYUA5k 9 | 95OE2I7MVmuUbbKcalfbLhaPwP8oUEoOBLQy55juwpPG36oO0uxyj/48ruGoN3yi 10 | 85SadfxkO4L6JRdX+x8Q8haE5P5jDUZt/lol1neQR8moEpKwy+KhAEPBe3c+MlXl 11 | wHWYk/j8GHnfgcDCLITpu7dHGUDye7l0dbM4EjbU9R3vcsTfT18eekUqm6MER0Gd 12 | wA4bcrAhlNQjNyLocmSMgzD3NjeXzbbA4NXO32X6OmH+I+sGPEJHcZU0Kew0Depe 13 | yhkmXKkCgYEA5pRIhouqF3RzbGf+yMMdo9bQc7U4iUhTcZ9Vm/z/Nxs0d1pCHy5L 14 | n3VAgOSsIb8AP/DmhpRGyBAGodKnkpXrvMG03y7Pf2KjOxh/A2thTDeB4dcelEvu 15 | Q9Uj1s/BctyVzelrL0DZjBhwxBuT56lqq56E8Yb/jn7yZZ8g7A7vetMCgYEAyHtk 16 | qKfh3d4oF5/mro9KmWsmjB/jfSwXJzu9UsdmLFlRhsRZBXLYxwLxtAFzcM+y+ac3 17 | 18dowvBD2p3tvEwKVF0ncxn0vDNDF8n+sYlVsBrLuFccfw1bgl3jmmAhuXX6NZO8 18 | HcO/S6PjrnSKYd4WXRjHqiv5ufMZFQ1OKjz1mz0CgYEAmOi7E+ao3LcQGFL65p5m 19 | GJHLWQBTxs6c75uvhSuJAD1dVM0ZTl5ALjXumcuLzzE/9CdIaPUJ34CpNUVidVZQ 20 | p7N5xAvh9OMvxm/fQyBBvO6OhntHPyb/kiJViw3phsd73Lqvpv2Fh19p4NM9CYMT 21 | R05vcVCKRzAuhW+6wHDDJZsCgYEAxptXEK2f2Efot960jGFvqaS4v0AoASzYkwlf 22 | eM4Irg6d8UA6YGdx0VVdVNHLJwrbZu79J0powhV7YuvpRAygfwr5tdEU3gx6fuqg 23 | 4ggHVzp0bt39YPA+o1uXyqtJPY1eng0I4wO0Up69Q2o4XNPCm9+cjTybXFczleNk 24 | d/uD5JECgYBcwAPEFNU2+NcU+mYWFHIvNK4hVLD/Bfr6EmkJcOLYE+BZPdaWaPGv 25 | ZE9ZgLpf6bB9cmQGUifWdSHDXwQH7kpYKS5pJdGsPbuFys5HU4xCbCEErsRnPKlE 26 | K/hcgeDuKNJ6/nV1ypUsOXpO6ukhLnTvj1R9MQVipprTaQbxRBxx8g== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /docker/quorum-crux/scripts/simpleContract.js: -------------------------------------------------------------------------------- 1 | a = eth.accounts[0] 2 | web3.eth.defaultAccount = a; 3 | 4 | // abi and bytecode generated from simplestorage.sol: 5 | // > solcjs --bin --abi simplestorage.sol 6 | var abi = [{"constant":true,"inputs":[],"name":"storedData","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"x","type":"uint256"}],"name":"set","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"get","outputs":[{"name":"retVal","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[{"name":"initVal","type":"uint256"}],"payable":false,"type":"constructor"}]; 7 | 8 | var bytecode = "0x6060604052341561000f57600080fd5b604051602080610149833981016040528080519060200190919050505b806000819055505b505b610104806100456000396000f30060606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a1afcd914605157806360fe47b11460775780636d4ce63c146097575b600080fd5b3415605b57600080fd5b606160bd565b6040518082815260200191505060405180910390f35b3415608157600080fd5b6095600480803590602001909190505060c3565b005b341560a157600080fd5b60a760ce565b6040518082815260200191505060405180910390f35b60005481565b806000819055505b50565b6000805490505b905600a165627a7a72305820d5851baab720bba574474de3d09dbeaabc674a15f4dd93b974908476542c23f00029"; 9 | 10 | var simpleContract = web3.eth.contract(abi); 11 | var simple = simpleContract.new(42, {from:web3.eth.accounts[0], data: bytecode, gas: 0x47b760, privateFor: ["QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="]}, function(e, contract) { 12 | if (e) { 13 | console.log("err creating contract", e); 14 | } else { 15 | if (!contract.address) { 16 | console.log("Contract transaction send: TransactionHash: " + contract.transactionHash + " waiting to be mined..."); 17 | } else { 18 | console.log("Contract mined! Address: " + contract.address); 19 | console.log(contract); 20 | } 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /storage/berkleydb.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/jsimonetti/berkeleydb" 6 | ) 7 | 8 | type berkleyDb struct { 9 | dbPath string 10 | conn *berkeleydb.Db 11 | } 12 | 13 | func InitBerkeleyDb(dbPath string) (*berkleyDb, error) { 14 | bdb := &berkleyDb{dbPath: dbPath} 15 | 16 | db, err := berkeleydb.NewDB() 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | err = db.Open( 22 | dbPath, berkeleydb.DbHash, berkeleydb.DbCreate) 23 | 24 | return bdb, err 25 | } 26 | 27 | func (db *berkleyDb) Write(key *[]byte, value *[]byte) error { 28 | 29 | b64Key := base64.StdEncoding.EncodeToString(*key) 30 | b64Value := base64.StdEncoding.EncodeToString(*value) 31 | 32 | err := db.conn.Put(b64Key, b64Value) 33 | if err != nil { 34 | return err 35 | } else { 36 | return nil 37 | } 38 | } 39 | 40 | func (db *berkleyDb) Read(key *[]byte) (*[]byte, error) { 41 | 42 | b64Key := base64.StdEncoding.EncodeToString(*key) 43 | 44 | value, err := db.conn.Get(b64Key) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | var decoded []byte 50 | decoded, err = base64.StdEncoding.DecodeString(value) 51 | return &decoded, err 52 | } 53 | 54 | func (db *berkleyDb) ReadAll(f func(key, value *[]byte)) error { 55 | iter, err := db.conn.Cursor() 56 | if err != nil { 57 | return err 58 | } 59 | 60 | var b64Key, b64Value string 61 | for { 62 | b64Key, b64Value, err = iter.GetNext() 63 | if err != nil { 64 | break 65 | } 66 | key, err := base64.StdEncoding.DecodeString(b64Key) 67 | if err != nil { 68 | break 69 | } 70 | value, err := base64.StdEncoding.DecodeString(b64Value) 71 | if err != nil { 72 | break 73 | } 74 | f(&key, &value) 75 | } 76 | 77 | return err 78 | } 79 | 80 | func (db *berkleyDb) Delete(key *[]byte) error { 81 | b64Key := base64.StdEncoding.EncodeToString(*key) 82 | return db.conn.Delete(b64Key) 83 | } 84 | 85 | func (db *berkleyDb) Close() error { 86 | return db.conn.Close() 87 | } 88 | -------------------------------------------------------------------------------- /docs/quorum-architecture.xml: -------------------------------------------------------------------------------- 1 | 7Ztbb+MoFMc/TaTuPPne9DFJm7bSdJSdVFvtI7FJgorBwuS2n37BxqlvaUjjOu2uOw+DD2Ab/j/D4UB69ijc3jMQLZ9oAHHPMoJtz77tWZbZ927Ef9KyUxbDvE4tC4YCZXszTNE/MCuorCsUwLhQkFOKOYqKRp8SAn1esAHG6KZYbE5x8akRWMCKYeoDXLW+oIAvldVzrbeMB4gWy+zRZtbkGfBfF4yuiHpgz7LnyV+aHYLsZqql8RIEdJMz2Xc9e8Qo5Wkq3I4glr2b9Vtab3wgd//iDBKuU8F20xprgFcwe+Xkxfgu642kOVBWMHr2cLNEHE4j4MvcjQBA2JY8xOLKFMl9g2TZBQZxrNIBiJfJTWQpn4bIVxkYzCAe7vttRDFlIotQAuX9OKOvMDOK7vSSv31Opo8tLHOEca7k3JX/pJ0SPgYhwpLHB4jXkCMfqAxFn9UX1wCjBREXGM5F7w2rnan6dw0Zh9ucSXXuPaQh5GwniqhcV4GffQmO0n2Tw8p1lXGZQ8rzlBEolhf7e7/JKRJK0QM4OBV176F47lkKn6iXkfxp6ZV+KJp6uVW9pCyiIB4oM6dRVkm1teb7OFnSfr8oqedWJL22ahR1rQYUtWs+Vw/Lxs9EYiETgyjCohc4okSUHCICkrd/JByyuRTVMq4Gw8c/sorikfu6rZLh+X04m+uQEQDYn/uaZJhejgwfymY38y3bRlF5y6gqb9Yp329A+OqXXBH+BWAMeSfribI63gVlrZt+S7I+odivF9UDoZSKzOIovU7KBGidFbkSDlJMhejWSDofiCCySNMojCgTrIzhVibyg0GufsfNO9x41gW58Y5zM8TUf/WXAJED9FRoSWrEKSCcARIDX84iqaUMmxGLLhU8fQV0Am/muXo+4Xxu+eehk74bZQFk5fc65Pvm3d0GyDOdInm21SJ518fJm3LAoS50A98XEnOFnRiwBHliSSeTATxAHqdMrMD+d+R9gg/TKjr94+jc/fWkC84jEX2+8pWfC7fQX8l0R8X5LlCrVNwcp2IkJiFI4lWsy8aEvnQcnO/StMqBaR4HYbojh3zhCgPjlby9nFTmIOZpKiYgipdUXWHZgK/ASd+a2XoxrcCF/cC5NCd2KQbiOG1yYh3n5BfkG8pedVHJbHEESGYM4DqyohSU3z8n2zwn+XIdPmc7IW5NCO3z8NEIod0CDmYg1vZhf8I1xLfDbig53/Vol4Vq+GXEVttWBbsbeMMawUyj1kn4DgFyIddNQdO6ELmd7Zg1HiI3qyvUP1eUrcJWZf3vbXsYx/c9bO+z9j1MjbXjw/PzRBQZTB67GPipn6xt2wV1Ww1mmhpLwFOn5B/dnHxGfDE7iHGJSTm777thIuJjsNaGwWcwiUhKXx4Sn+0iufURwCTVEdLAeNHqCtCqRgp+1C3hhpC9Yri7PbhVVlrKycM7QmDxcjimUvxVJDfHpKnEgugmXhS8KKnSOa+dMh2Zu0MUBPIhtXwVCWz6pI0WUDcN8WOVvAnvusqP49R5E03woxFBeH7b/xIlnwABC/HxaI43U0iCdLRh0IdorYaeiNEILJKRyODbbtxpYNxpNUL5zumbgoAllLLVx2F2ygSUbvB7Mur82g/xcnPJPXqdwx0n72z8Hoyf09FkMhw/d2PIB8YQ45J7oDonNyYMrdNZ4qSd9O+2d/7dwOlf8tiF1ub5VrSVAJwAEEai25NzFd2ccZLMVmnOuNaV2WlCZq3dcAa/lsTf7Ut2nQtKbGvEN/4WVcTaTjYzksuOryT2dzvUUN6sbldsjaVm595/PFB5Qffe1loNXk3oVPnrdNB5XeefSXJ0Nbab0FjDW2/grIlcBXLx5OxHO2Is6I6bfFoQoNVYta1xXvpEgn4lizsDkTntBpSPxJ7d9na3xOXbj3mTvNxvpu27fwE= -------------------------------------------------------------------------------- /docker/quorum-crux/istanbul-genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "chainId": 2017, 4 | "homesteadBlock": 1, 5 | "eip150Block": 2, 6 | "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", 7 | "eip155Block": 3, 8 | "eip158Block": 3, 9 | "istanbul": { 10 | "epoch": 30000, 11 | "policy": 0 12 | } 13 | }, 14 | "nonce": "0x0", 15 | "timestamp": "0x5b68584d", 16 | "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000f89af85494c816ef5f432f9aff47b74120a9d6b41ea9a3bb1b946e98452e06d5158133490981a66f626538ec2c44947785902068b03c3820f8168af31845ffb313bdcd94c5263ce01b7c1fc0bc81d348a84df9f7bca54492b8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0", 17 | "gasLimit": "0x1312D00", 18 | "difficulty": "0x1", 19 | "mixHash": "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", 20 | "coinbase": "0x0000000000000000000000000000000000000000", 21 | "alloc": { 22 | "6e98452e06d5158133490981a66f626538ec2c44": { 23 | "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" 24 | }, 25 | "7785902068b03c3820f8168af31845ffb313bdcd": { 26 | "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" 27 | }, 28 | "c5263ce01b7c1fc0bc81d348a84df9f7bca54492": { 29 | "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" 30 | }, 31 | "c816ef5f432f9aff47b74120a9d6b41ea9a3bb1b": { 32 | "balance": "0x446c3b15f9926687d2c40534fdb564000000000000" 33 | }, 34 | "ed9d02e382b34818e88b88a309c7fe71e65f419d": { 35 | "balance": "1000000000000000000000000000" 36 | }, 37 | "ca843569e3427144cead5e4d5999a3d0ccf92b8e": { 38 | "balance": "1000000000000000000000000000" 39 | }, 40 | "0fbdc686b912d7722dc86510934589e0aaf3b55a": { 41 | "balance": "1000000000000000000000000000" 42 | }, 43 | "9186eb3d20cbd1f5f992a950d808c4495153abd5": { 44 | "balance": "1000000000000000000000000000" 45 | } 46 | }, 47 | "number": "0x0", 48 | "gasUsed": "0x0", 49 | "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" 50 | } 51 | -------------------------------------------------------------------------------- /api/client.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // SendRequest sends a new transaction to the enclave for storage and propagation to the provided 4 | // recipients. 5 | type SendRequest struct { 6 | // Payload is the transaction payload data we wish to store. 7 | Payload string `json:"payload"` 8 | // From is the sender node identification. 9 | From string `json:"from"` 10 | // To is a list of the recipient nodes that should be privy to this transaction payload. 11 | To []string `json:"to"` 12 | } 13 | 14 | // SendResponse is the response to the SendRequest 15 | type SendResponse struct { 16 | // Key is the key that can be used to retrieve the submitted transaction. 17 | Key string `json:"key"` 18 | } 19 | 20 | // ReceiveRequest 21 | type ReceiveRequest struct { 22 | Key string `json:"key"` 23 | To string `json:"to"` 24 | } 25 | 26 | // ReceiveResponse returns the raw payload associated with the ReceiveRequest. 27 | type ReceiveResponse struct { 28 | Payload string `json:"payload"` 29 | } 30 | 31 | // DeleteRequest deletes the entry matching the given key from the enclave. 32 | type DeleteRequest struct { 33 | Key string `json:"key"` 34 | } 35 | 36 | // ResendRequest is used to resend previous transactions. 37 | // There are two types of supported request. 38 | // 1. All transactions associated with a node, in which case the Key field should be omitted. 39 | // 2. A specific transaction with the given key value. 40 | type ResendRequest struct { 41 | // Type is the resend request type. It should be either "all" or "individual" depending on if 42 | // you want to request an individual transaction, or all transactions associated with a node. 43 | Type string `json:"type"` 44 | PublicKey string `json:"publicKey"` 45 | Key string `json:"key,omitempty"` 46 | } 47 | 48 | type UpdatePartyInfo struct { 49 | Url string `json:"url"` 50 | Recipients map[string][]byte `json:"recipients"` 51 | Parties map[string]bool `json:"parties"` 52 | } 53 | 54 | type PartyInfoResponse struct { 55 | Payload []byte `json:"payload"` 56 | } 57 | type PrivateKeyBytes struct { 58 | Bytes string `json:"bytes"` 59 | } 60 | 61 | // PrivateKey is a container for a private key. 62 | type PrivateKey struct { 63 | Data PrivateKeyBytes `json:"data"` 64 | Type string `json:"type"` 65 | } 66 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | const configFile = "config_testdata.conf" 9 | 10 | func TestInitFlags(t *testing.T) { 11 | InitFlags() 12 | conf := AllSettings() 13 | expected := map[string]interface{}{ 14 | Port: -1, 15 | Verbosity: 1, 16 | BerkeleyDb: false, 17 | GenerateKeys: "", 18 | AlwaysSendTo: "", 19 | Storage: "crux.db", 20 | WorkDir: ".", 21 | Url: "", 22 | PublicKeys: "", 23 | OtherNodes: "", 24 | PrivateKeys: "", 25 | Socket: "crux.ipc", 26 | } 27 | 28 | verifyConfig(t, conf, expected) 29 | } 30 | 31 | func TestLoadConfig(t *testing.T) { 32 | err := LoadConfig(configFile) 33 | 34 | if err != nil { 35 | t.Fatalf("Unable to load config file: %s, %s", configFile, err) 36 | } 37 | 38 | conf := AllSettings() 39 | 40 | expected := map[string]interface{}{ 41 | Verbosity: 1, 42 | AlwaysSendTo: []interface{}{}, 43 | TlsServerChain: []interface{}{}, 44 | Storage: "dir:storage", 45 | WorkDir: "data", 46 | Url: "http://127.0.0.1:9001/", 47 | TlsServerTrust: "tofu", 48 | PublicKeys: []interface{}{"foo.pub"}, 49 | OtherNodes: []interface{}{"http://127.0.0.1:9000/"}, 50 | TlsKnownServers: "tls-known-servers", 51 | TlsClientCert: "tls-client-cert.pem", 52 | PrivateKeys: []interface{}{"foo.key"}, 53 | TlsServerCert: "tls-server-cert.pem", 54 | Tls: "strict", 55 | TlsKnownClients: "tls-known-clients", 56 | TlsClientChain: []interface{}{}, 57 | TlsClientKey: "tls-client-key.pem", 58 | Socket: "constellation.ipc", 59 | TlsClientTrust: "ca-or-tofu", 60 | TlsServerKey: "tls-server-key.pem", 61 | Port: 9001, 62 | } 63 | 64 | verifyConfig(t, conf, expected) 65 | } 66 | 67 | func verifyConfig(t *testing.T, conf map[string]interface{}, expected map[string]interface{}) { 68 | for expK, expV := range expected { 69 | //if conf[key] != value { 70 | if actV, ok := conf[expK]; ok { 71 | var eq bool 72 | switch actV.(type) { // we cannot use == for equality with []interface{} 73 | case []interface{}: 74 | eq = reflect.DeepEqual(actV, expV) 75 | default: 76 | eq = actV == expV 77 | } 78 | if !eq { 79 | t.Errorf("Key: %s with value %v could not be found", expK, expV) 80 | } 81 | } 82 | } 83 | } 84 | 85 | func TestGetBoolConfig(t *testing.T) { 86 | if GetBool(Verbosity) != true { 87 | t.Errorf("Verbosity is %t", GetBool(Verbosity)) 88 | } 89 | } 90 | 91 | func TestGetIntConfig(t *testing.T) { 92 | if GetInt(Port) != 9001 { 93 | t.Errorf("Port num 9001 is expected but we got %d", GetInt(Port)) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/base64" 5 | "github.com/blk-io/chimera-api/chimera" 6 | "golang.org/x/net/context" 7 | "google.golang.org/grpc" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | const sender = "zSifTnkv5r4K67Dq304eVcM4FpxGfHLe1yTCBm0/7wg=" 13 | const receiver = "I/EbshW61ykJ+qTivXPaKyQ5WmQDUFfMNGEBj2E2uUs=" 14 | 15 | var payload = []byte("payload") 16 | 17 | func TestIntegration(t *testing.T) { 18 | var conn1 *grpc.ClientConn 19 | var conn2 *grpc.ClientConn 20 | // Initiate a connection with the first server 21 | conn1, err := grpc.Dial(":9020", grpc.WithInsecure()) 22 | if err != nil { 23 | t.Fatalf("did not connect: %s", err) 24 | } 25 | defer conn1.Close() 26 | c1 := chimera.NewClientClient(conn1) 27 | // Initiate a connection with the second server 28 | conn2, err = grpc.Dial(":9025", grpc.WithInsecure()) 29 | if err != nil { 30 | t.Fatalf("did not connect: %s", err) 31 | } 32 | defer conn2.Close() 33 | c2 := chimera.NewClientClient(conn2) 34 | 35 | Upcheckresponse1, err := c1.Upcheck(context.Background(), &chimera.UpCheckResponse{Message: "foo"}) 36 | if err != nil { 37 | t.Fatalf("error when calling Upcheck: %s", err) 38 | } 39 | t.Logf("Response from server: %s", Upcheckresponse1.Message) 40 | 41 | Upcheckresponse2, err := c2.Upcheck(context.Background(), &chimera.UpCheckResponse{Message: "foo"}) 42 | if err != nil { 43 | t.Fatalf("error when calling Upcheck: %s", err) 44 | } 45 | t.Logf("Response from server: %s", Upcheckresponse2.Message) 46 | 47 | sendReqs := []chimera.SendRequest{ 48 | { 49 | Payload: []byte("payload"), 50 | From: sender, 51 | To: []string{receiver}, 52 | }, 53 | { 54 | Payload: []byte("test"), 55 | To: []string{}, 56 | }, 57 | { 58 | Payload: []byte("blk-io crux"), 59 | }, 60 | } 61 | 62 | sendResponse := chimera.SendResponse{} 63 | for _, sendReq := range sendReqs { 64 | sendResp, err := c1.Send(context.Background(), &sendReq) 65 | if err != nil { 66 | t.Fatalf("gRPC send failed with %s", err) 67 | } 68 | sendResponse = chimera.SendResponse{Key: sendResp.Key} 69 | t.Logf("The response for Send request is: %s", base64.StdEncoding.EncodeToString(sendResponse.Key)) 70 | 71 | recResp, err := c1.Receive(context.Background(), &chimera.ReceiveRequest{Key: sendResponse.Key, To: receiver}) 72 | if err != nil { 73 | t.Fatalf("gRPC receive failed with %s", err) 74 | } 75 | receiveResponse := chimera.ReceiveResponse{Payload: recResp.Payload} 76 | if !reflect.DeepEqual(receiveResponse.Payload, sendReq.Payload) { 77 | t.Fatalf("handler returned unexpected response: %v, expected: %v\n", receiveResponse.Payload, sendReq.Payload) 78 | } else { 79 | t.Logf("The payload return is %v", receiveResponse.Payload) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /api/encoding_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/blk-io/crux/utils" 5 | "github.com/kevinburke/nacl" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestEncodePayload(t *testing.T) { 11 | 12 | epl := EncryptedPayload{ 13 | Sender: nacl.NewKey(), 14 | CipherText: []byte("C1ph3r T3xt"), 15 | Nonce: nacl.NewNonce(), 16 | RecipientBoxes: [][]byte{[]byte("B0x1"), []byte("B0x2")}, 17 | RecipientNonce: nacl.NewNonce(), 18 | } 19 | 20 | encoded := EncodePayload(epl) 21 | decoded := DecodePayload(encoded) 22 | 23 | if !reflect.DeepEqual(epl, decoded) { 24 | t.Errorf("Decoded payload: %v does not match input %v", decoded, epl) 25 | } 26 | } 27 | 28 | func TestEncodePayloadWithRecipients(t *testing.T) { 29 | 30 | epls := []EncryptedPayload{ 31 | { 32 | Sender: nacl.NewKey(), 33 | CipherText: []byte("C1ph3r T3xt1"), 34 | Nonce: nacl.NewNonce(), 35 | RecipientBoxes: [][]byte{[]byte("B0x1"), []byte("B0x2"), []byte("B0x3")}, 36 | RecipientNonce: nacl.NewNonce(), 37 | }, 38 | { 39 | Sender: nacl.NewKey(), 40 | CipherText: []byte("C1ph3r T3xt2"), 41 | Nonce: nacl.NewNonce(), 42 | RecipientBoxes: [][]byte{[]byte("B0x1")}, 43 | RecipientNonce: nacl.NewNonce(), 44 | }, 45 | } 46 | 47 | recipients := [][][]byte{ 48 | { 49 | (*nacl.NewKey())[:], 50 | (*nacl.NewKey())[:], 51 | (*nacl.NewKey())[:], 52 | }, 53 | {}, // Recipients may be empty 54 | } 55 | 56 | for i, epl := range epls { 57 | encoded := EncodePayloadWithRecipients(epl, recipients[i]) 58 | decodedEpl, decodedRecipients := DecodePayloadWithRecipients(encoded) 59 | 60 | if !reflect.DeepEqual(epl, decodedEpl) { 61 | t.Errorf("Decoded partyInfo: %v does not match input %v", decodedEpl, epl) 62 | } 63 | 64 | if !reflect.DeepEqual(recipients[i], decodedRecipients) { 65 | t.Errorf("Decoded partyInfo: %v does not match input %v", 66 | decodedRecipients, recipients[i]) 67 | } 68 | } 69 | } 70 | 71 | func TestEncodePartyInfo(t *testing.T) { 72 | 73 | pi := PartyInfo{ 74 | url: "https://127.0.0.4:9004/", 75 | recipients: map[[nacl.KeySize]byte]string{ 76 | toKey("ROAZBWtSacxXQrOe3FGAqJDyJjFePR5ce4TSIzmJ0Bc="): "https://127.0.0.7:9007/", 77 | toKey("BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo="): "https://127.0.0.1:9001/", 78 | toKey("QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc="): "https://127.0.0.2:9002/", 79 | toKey("1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg="): "https://127.0.0.3:9003/", 80 | toKey("UfNSeSGySeKg11DVNEnqrUtxYRVor4+CvluI8tVv62Y="): "https://127.0.0.6:9006/", 81 | toKey("oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8="): "https://127.0.0.4:9004/", 82 | toKey("R56gy4dn24YOjwyesTczYa8m5xhP6hF2uTMCju/1xkY="): "https://127.0.0.5:9005/", 83 | }, 84 | parties: map[string]bool{ 85 | "https://127.0.0.5:9005/": true, 86 | "https://127.0.0.3:9003/": true, 87 | "https://127.0.0.1:9001/": true, 88 | "https://127.0.0.7:9007/": true, 89 | "https://127.0.0.6:9006/": true, 90 | "https://127.0.0.4:9004/": true, 91 | "https://127.0.0.2:9002/": true, 92 | }, 93 | } 94 | 95 | runEncodePartyInfoTest(t, pi) 96 | } 97 | 98 | func runEncodePartyInfoTest(t *testing.T, pi PartyInfo) { 99 | encoded := EncodePartyInfo(pi) 100 | decoded, err := DecodePartyInfo(encoded) 101 | 102 | if err != nil { 103 | t.Fatalf("Unable to decode party info: %v", err) 104 | } 105 | 106 | if !reflect.DeepEqual(pi, decoded) { 107 | t.Errorf("Decoded partyInfo: %v does not match input %v", decoded, pi) 108 | } 109 | } 110 | 111 | func toKey(encodedKey string) [nacl.KeySize]byte { 112 | key, _ := utils.LoadBase64Key(encodedKey) 113 | return *key 114 | } 115 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Package config provides the configuration settings to be used by the application at runtime 2 | package config 3 | 4 | import ( 5 | "flag" 6 | "fmt" 7 | "github.com/spf13/pflag" 8 | "github.com/spf13/viper" 9 | "os" 10 | ) 11 | 12 | const ( 13 | Verbosity = "verbosity" 14 | VerbosityShorthand = "v" 15 | AlwaysSendTo = "alwayssendto" 16 | Storage = "storage" 17 | WorkDir = "workdir" 18 | Url = "url" 19 | OtherNodes = "othernodes" 20 | PublicKeys = "publickeys" 21 | PrivateKeys = "privatekeys" 22 | Port = "port" 23 | Socket = "socket" 24 | 25 | GenerateKeys = "generate-keys" 26 | 27 | BerkeleyDb = "berkeleydb" 28 | UseGRPC = "grpc" 29 | GrpcJsonPort = "grpcport" 30 | NetworkInterface = "networkinterface" 31 | 32 | Tls = "tls" 33 | TlsServerChain = "tlsserverchain" 34 | TlsServerTrust = "tlsservertrust" 35 | TlsKnownServers = "tlsknownservers" 36 | TlsClientCert = "tlsclientcert" 37 | TlsServerCert = "tlsservercert" 38 | TlsKnownClients = "tlsknownclients" 39 | TlsClientChain = "tlsclientchain" 40 | TlsClientKey = "tlsclientkey" 41 | TlsClientTrust = "tlsclienttrust" 42 | TlsServerKey = "tlsserverkey" 43 | ) 44 | 45 | // InitFlags initializes all supported command line flags. 46 | func InitFlags() { 47 | flag.String(GenerateKeys, "", "Generate a new keypair") 48 | flag.String(Url, "", "The URL to advertise to other nodes (reachable by them)") 49 | flag.Int(Port, -1, "The local port to listen on") 50 | flag.String(WorkDir, ".", "The folder to put stuff in ") 51 | flag.String(Socket, "crux.ipc", "IPC socket to create for access to the Private API") 52 | flag.String(OtherNodes, "", "\"Boot nodes\" to connect to to discover the network") 53 | flag.String(PublicKeys, "", "Public keys hosted by this node") 54 | flag.String(PrivateKeys, "", "Private keys hosted by this node") 55 | flag.String(Storage, "crux.db", "Database storage file name") 56 | flag.Bool(BerkeleyDb, false, 57 | "Use Berkeley DB for working with an existing Constellation data store [experimental]") 58 | 59 | flag.Int(Verbosity, 1, "Verbosity level of logs (0=fatal, 1=warn, 2=info, 3=debug)") 60 | flag.Int(VerbosityShorthand, 1, "Verbosity level of logs (shorthand)") 61 | flag.String(AlwaysSendTo, "", "List of public keys for nodes to send all transactions too") 62 | flag.Bool(UseGRPC, true, "Use gRPC server") 63 | flag.Bool(Tls, false, "Use TLS to secure HTTP communications") 64 | flag.String(TlsServerCert, "", "The server certificate to be used") 65 | flag.String(TlsServerKey, "", "The server private key") 66 | flag.Int(GrpcJsonPort, -1, "The local port to listen on for JSON extensions of gRPC") 67 | flag.String(NetworkInterface, "localhost", "The network interface to bind the server to") 68 | 69 | // storage not currently supported as we use LevelDB 70 | 71 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 72 | viper.BindPFlags(pflag.CommandLine) // Binding the flags to test the initial configuration 73 | } 74 | 75 | // Usage prints usage instructions to the console. 76 | func Usage() { 77 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 78 | fmt.Fprintf(os.Stderr, " %-25s%s\n", "crux.config", "Optional config file") 79 | pflag.PrintDefaults() 80 | } 81 | 82 | // ParseCommandLine parses all provided command line arguments. 83 | func ParseCommandLine() { 84 | pflag.Parse() 85 | viper.BindPFlags(pflag.CommandLine) 86 | } 87 | 88 | // LoadConfig loads all configuration settings in the provided configPath location. 89 | func LoadConfig(configPath string) error { 90 | viper.SetConfigType("hcl") 91 | viper.SetConfigFile(configPath) 92 | return viper.ReadInConfig() 93 | } 94 | 95 | func AllSettings() map[string]interface{} { 96 | return viper.AllSettings() 97 | } 98 | 99 | func GetBool(key string) bool { 100 | return viper.GetBool(key) 101 | } 102 | 103 | func GetInt(key string) int { 104 | return viper.GetInt(key) 105 | } 106 | 107 | func GetString(key string) string { 108 | return viper.GetString(key) 109 | } 110 | 111 | func GetStringSlice(key string) []string { 112 | return viper.GetStringSlice(key) 113 | } 114 | -------------------------------------------------------------------------------- /crux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/blk-io/crux/api" 5 | "github.com/blk-io/crux/config" 6 | "github.com/blk-io/crux/enclave" 7 | "github.com/blk-io/crux/server" 8 | "github.com/blk-io/crux/storage" 9 | log "github.com/sirupsen/logrus" 10 | "net/http" 11 | "os" 12 | "path" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | func main() { 18 | 19 | config.InitFlags() 20 | 21 | args := os.Args 22 | if len(args) == 1 { 23 | exit() 24 | } 25 | 26 | for _, arg := range args[1:] { 27 | if strings.Contains(arg, ".conf") { 28 | err := config.LoadConfig(arg) 29 | if err != nil { 30 | log.Fatalln(err) 31 | } 32 | break 33 | } 34 | } 35 | config.ParseCommandLine() 36 | 37 | verbosity := 1 38 | if config.GetInt(config.Verbosity) > config.GetInt(config.VerbosityShorthand) { 39 | verbosity = config.GetInt(config.Verbosity) 40 | } else { 41 | verbosity = config.GetInt(config.VerbosityShorthand) 42 | } 43 | 44 | var level log.Level 45 | 46 | switch verbosity { 47 | case 0: 48 | level = log.FatalLevel 49 | case 1: 50 | level = log.WarnLevel 51 | case 2: 52 | level = log.InfoLevel 53 | default: 54 | level = log.DebugLevel 55 | } 56 | log.SetLevel(level) 57 | 58 | keyFile := config.GetString(config.GenerateKeys) 59 | if keyFile != "" { 60 | err := enclave.DoKeyGeneration(keyFile) 61 | if err != nil { 62 | log.Fatalln(err) 63 | } 64 | log.Printf("Key pair successfully written to %s", keyFile) 65 | os.Exit(0) 66 | } 67 | 68 | workDir := config.GetString(config.WorkDir) 69 | dbStorage := config.GetString(config.Storage) 70 | ipcFile := config.GetString(config.Socket) 71 | storagePath := path.Join(workDir, dbStorage) 72 | ipcPath := path.Join(workDir, ipcFile) 73 | var db storage.DataStore 74 | var err error 75 | if config.GetBool(config.BerkeleyDb) { 76 | db, err = storage.InitBerkeleyDb(storagePath) 77 | } else { 78 | db, err = storage.InitLevelDb(storagePath) 79 | } 80 | 81 | if err != nil { 82 | log.Fatalf("Unable to initialise storage, error: %v", err) 83 | } 84 | defer db.Close() 85 | 86 | allOtherNodes := config.GetString(config.OtherNodes) 87 | otherNodes := strings.Split(allOtherNodes, ",") 88 | url := config.GetString(config.Url) 89 | if url == "" { 90 | log.Fatalln("URL must be specified") 91 | } 92 | port := config.GetInt(config.Port) 93 | if port < 0 { 94 | log.Fatalln("Port must be specified") 95 | } 96 | httpClient := &http.Client{ 97 | Timeout: time.Second * 10, 98 | } 99 | grpc := config.GetBool(config.UseGRPC) 100 | 101 | pi := api.InitPartyInfo(url, otherNodes, httpClient, grpc) 102 | 103 | privKeys := config.GetString(config.PrivateKeys) 104 | pubKeys := config.GetString(config.PublicKeys) 105 | pubKeyFiles := strings.Split(pubKeys, ",") 106 | privKeyFiles := strings.Split(privKeys, ",") 107 | 108 | if len(privKeyFiles) != len(pubKeyFiles) { 109 | log.Fatalln("Private keys provided must have corresponding public keys") 110 | } 111 | 112 | if len(privKeyFiles) == 0 { 113 | log.Fatalln("Node key files must be provided") 114 | } 115 | 116 | for i, keyFile := range privKeyFiles { 117 | privKeyFiles[i] = path.Join(workDir, keyFile) 118 | } 119 | 120 | for i, keyFile := range pubKeyFiles { 121 | pubKeyFiles[i] = path.Join(workDir, keyFile) 122 | } 123 | 124 | enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, grpc) 125 | 126 | pi.RegisterPublicKeys(enc.PubKeys) 127 | 128 | tls := config.GetBool(config.Tls) 129 | var tlsCertFile, tlsKeyFile string 130 | if tls { 131 | servCert := config.GetString(config.TlsServerCert) 132 | servKey := config.GetString(config.TlsServerKey) 133 | 134 | if (len(servCert) != len(servKey)) || (len(servCert) <= 0) { 135 | log.Fatalf("Please provide server certificate and key for TLS %s %s %d ", servKey, servCert, len(servCert)) 136 | } 137 | 138 | tlsCertFile = path.Join(workDir, servCert) 139 | tlsKeyFile = path.Join(workDir, servKey) 140 | } 141 | grpcJsonport := config.GetInt(config.GrpcJsonPort) 142 | networkInterface := config.GetString(config.NetworkInterface) 143 | _, err = server.Init(enc, networkInterface, port, ipcPath, grpc, grpcJsonport, tls, tlsCertFile, tlsKeyFile) 144 | if err != nil { 145 | log.Fatalf("Error starting server: %v\n", err) 146 | } 147 | 148 | pi.PollPartyInfo() 149 | 150 | select {} 151 | } 152 | 153 | func exit() { 154 | config.Usage() 155 | os.Exit(1) 156 | } 157 | -------------------------------------------------------------------------------- /server/proto_server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "github.com/blk-io/chimera-api/chimera" 6 | "github.com/blk-io/crux/utils" 7 | "github.com/grpc-ecosystem/grpc-gateway/runtime" 8 | log "github.com/sirupsen/logrus" 9 | "golang.org/x/net/context" 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/credentials" 12 | "net" 13 | "net/http" 14 | ) 15 | 16 | func (tm *TransactionManager) startRpcServer(networkInterface string, port int, grpcJsonPort int, ipcPath string, tls bool, certFile, keyFile string) error { 17 | lis, err := utils.CreateIpcSocket(ipcPath) 18 | if err != nil { 19 | log.Fatalf("failed to listen: %v", err) 20 | } 21 | s := Server{Enclave: tm.Enclave} 22 | grpcServer := grpc.NewServer() 23 | chimera.RegisterClientServer(grpcServer, &s) 24 | go func() { 25 | log.Fatal(grpcServer.Serve(lis)) 26 | }() 27 | 28 | go func() error { 29 | var err error 30 | if tls { 31 | err = tm.startRestServerTLS(networkInterface, port, certFile, keyFile, certFile) 32 | } else { 33 | err = tm.startRestServer(networkInterface, port) 34 | } 35 | if grpcJsonPort != -1 { 36 | if tls { 37 | err = tm.startJsonServerTLS(networkInterface, port, grpcJsonPort, certFile, keyFile, certFile) 38 | } else { 39 | err = tm.startJsonServer(networkInterface, port, grpcJsonPort) 40 | } 41 | } 42 | if err != nil { 43 | log.Fatalf("failed to start gRPC REST server: %s", err) 44 | } 45 | return err 46 | }() 47 | 48 | return err 49 | } 50 | 51 | func (tm *TransactionManager) startJsonServer(networkInterface string, port int, grpcJsonPort int) error { 52 | address := fmt.Sprintf("%s:%d", networkInterface, grpcJsonPort) 53 | ctx := context.Background() 54 | ctx, cancel := context.WithCancel(ctx) 55 | defer cancel() 56 | mux := runtime.NewServeMux() 57 | opts := []grpc.DialOption{grpc.WithInsecure()} 58 | err := chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", networkInterface, port), opts) 59 | if err != nil { 60 | return fmt.Errorf("could not register service: %s", err) 61 | } 62 | log.Printf("starting HTTP/1.1 REST server on %s", address) 63 | err = http.ListenAndServe(address, mux) 64 | if err != nil { 65 | return fmt.Errorf("could not listen on %s due to: %s", address, err) 66 | } 67 | return nil 68 | } 69 | 70 | func (tm *TransactionManager) startRestServer(networkInterface string, port int) error { 71 | grpcAddress := fmt.Sprintf("%s:%d", networkInterface, port) 72 | lis, err := net.Listen("tcp", grpcAddress) 73 | if err != nil { 74 | panic(err) 75 | } 76 | s := Server{Enclave: tm.Enclave} 77 | grpcServer := grpc.NewServer() 78 | chimera.RegisterClientServer(grpcServer, &s) 79 | go func() { 80 | log.Fatal(grpcServer.Serve(lis)) 81 | }() 82 | return nil 83 | } 84 | 85 | func (tm *TransactionManager) startJsonServerTLS(networkInterface string, port int, grpcJsonPort int, certFile, keyFile, ca string) error { 86 | address := fmt.Sprintf("%s:%d", networkInterface, grpcJsonPort) 87 | ctx := context.Background() 88 | ctx, cancel := context.WithCancel(ctx) 89 | defer cancel() 90 | mux := runtime.NewServeMux() 91 | creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) 92 | err = chimera.RegisterClientHandlerFromEndpoint(ctx, mux, fmt.Sprintf("%s:%d", networkInterface, port), []grpc.DialOption{grpc.WithTransportCredentials(creds)}) 93 | if err != nil { 94 | log.Fatalf("could not register service Ping: %s", err) 95 | return err 96 | } 97 | http.ListenAndServe(address, mux) 98 | log.Printf("started HTTPS REST server on %s", address) 99 | return nil 100 | } 101 | 102 | func (tm *TransactionManager) startRestServerTLS(networkInterface string, port int, certFile, keyFile, ca string) error { 103 | grpcAddress := fmt.Sprintf("%s:%d", networkInterface, port) 104 | lis, err := net.Listen("tcp", grpcAddress) 105 | if err != nil { 106 | log.Fatalf("failed to start gRPC REST server: %s", err) 107 | } 108 | s := Server{Enclave: tm.Enclave} 109 | creds, err := credentials.NewServerTLSFromFile(certFile, keyFile) 110 | opts := []grpc.ServerOption{grpc.Creds(creds)} 111 | if err != nil { 112 | log.Fatalf("failed to load credentials : %v", err) 113 | } 114 | grpcServer := grpc.NewServer(opts...) 115 | chimera.RegisterClientServer(grpcServer, &s) 116 | go func() { 117 | log.Fatal(grpcServer.Serve(lis)) 118 | }() 119 | return nil 120 | } 121 | 122 | func GetFreePort(networkInterface string) (int, error) { 123 | addr, err := net.ResolveTCPAddr("tcp", networkInterface + ":0") 124 | if err != nil { 125 | return 0, err 126 | } 127 | 128 | l, err := net.ListenTCP("tcp", addr) 129 | if err != nil { 130 | return 0, err 131 | } 132 | defer l.Close() 133 | return l.Addr().(*net.TCPAddr).Port, nil 134 | } 135 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # The import path is where your repository can be found. 2 | # To import subpackages, always prepend the full import path. 3 | # If you change this, run `make clean`. Read more: https://git.io/vM7zV 4 | IMPORT_PATH := github.com/blk-io/crux 5 | 6 | # V := 1 # When V is set, print commands and build progress. 7 | 8 | # Space separated patterns of packages to skip in list, test, format. 9 | IGNORED_PACKAGES := /vendor/ 10 | 11 | .PHONY: all 12 | all: build 13 | 14 | .PHONY: build 15 | build: .GOPATH/.ok 16 | $Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH) 17 | 18 | ### Code not in the repository root? Another binary? Add to the path like this. 19 | # .PHONY: otherbin 20 | # otherbin: .GOPATH/.ok 21 | # $Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/otherbin 22 | 23 | ##### =====> Utility targets <===== ##### 24 | 25 | .PHONY: clean test list cover format 26 | 27 | clean: 28 | $Q rm -rf bin .GOPATH 29 | 30 | test: .GOPATH/.ok 31 | $Q ./bin/crux --url=http://127.0.0.1:9020/ --port=9020 --workdir=test/test1 --publickeys=testdata/key.pub --privatekeys=testdata/key & 32 | $Q ./bin/crux --url=http://127.0.0.1:9025/ --port=9025 --workdir=test/test2 --publickeys=testdata/rcpt1.pub --privatekeys=testdata/rcpt1 & 33 | $Q go test $(if $V,-v) -i -race $(allpackages) # install -race libs to speed up next run 34 | ifndef CI 35 | $Q go vet $(allpackages) 36 | $Q GODEBUG=cgocheck=2 go test -race $(allpackages) 37 | else 38 | $Q ( go vet $(allpackages); echo $$? ) | \ 39 | tee .GOPATH/test/vet.txt | sed '$$ d'; exit $$(tail -1 .GOPATH/test/vet.txt) 40 | $Q ( GODEBUG=cgocheck=2 go test -v -race $(allpackages); echo $$? ) | \ 41 | tee .GOPATH/test/output.txt | sed '$$ d'; exit $$(tail -1 .GOPATH/test/output.txt) 42 | endif 43 | $Q pkill crux 44 | 45 | list: .GOPATH/.ok 46 | @echo $(allpackages) 47 | 48 | cover: bin/gocovmerge .GOPATH/.ok 49 | @echo "NOTE: make cover does not exit 1 on failure, don't use it to check for tests success!" 50 | $Q rm -f .GOPATH/cover/*.out .GOPATH/cover/all.merged 51 | $(if $V,@echo "-- go test -coverpkg=./... -coverprofile=.GOPATH/cover/... ./...") 52 | @for MOD in $(allpackages); do \ 53 | go test -coverpkg=`echo $(allpackages)|tr " " ","` \ 54 | -coverprofile=.GOPATH/cover/unit-`echo $$MOD|tr "/" "_"`.out \ 55 | $$MOD 2>&1 | grep -v "no packages being tested depend on"; \ 56 | done 57 | $Q ./bin/gocovmerge .GOPATH/cover/*.out > .GOPATH/cover/all.merged 58 | ifndef CI 59 | $Q go tool cover -html .GOPATH/cover/all.merged 60 | else 61 | $Q go tool cover -html .GOPATH/cover/all.merged -o .GOPATH/cover/all.html 62 | endif 63 | @echo "" 64 | @echo "=====> Total test coverage: <=====" 65 | @echo "" 66 | $Q go tool cover -func .GOPATH/cover/all.merged 67 | 68 | format: bin/goimports .GOPATH/.ok 69 | $Q find .GOPATH/src/$(IMPORT_PATH)/ -iname \*.go | grep -v \ 70 | -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) | xargs ./bin/goimports -w 71 | 72 | ##### =====> Internals <===== ##### 73 | 74 | .PHONY: setup 75 | setup: clean .GOPATH/.ok 76 | @if ! grep "/.GOPATH" .gitignore > /dev/null 2>&1; then \ 77 | echo "/.GOPATH" >> .gitignore; \ 78 | echo "/bin" >> .gitignore; \ 79 | fi 80 | go get -u github.com/golang/dep/cmd/dep 81 | go get -u golang.org/x/tools/cmd/goimports 82 | go get -u github.com/wadey/gocovmerge 83 | @test -f Gopkg.toml || \ 84 | (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep init) 85 | (cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && ./bin/dep ensure) 86 | 87 | VERSION := $(shell git describe --tags --always --dirty="-dev") 88 | DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC') 89 | VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"' 90 | 91 | # cd into the GOPATH to workaround ./... not following symlinks 92 | _allpackages = $(shell ( cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && \ 93 | GOPATH=$(CURDIR)/.GOPATH go list ./... 2>&1 1>&3 | \ 94 | grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) 1>&2 ) 3>&1 | \ 95 | grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES))) 96 | 97 | # memoize allpackages, so that it's executed only once and only if used 98 | allpackages = $(if $(__allpackages),,$(eval __allpackages := $$(_allpackages)))$(__allpackages) 99 | 100 | export GOPATH := $(CURDIR)/.GOPATH 101 | unexport GOBIN 102 | 103 | Q := $(if $V,,@) 104 | 105 | .GOPATH/.ok: 106 | $Q mkdir -p "$(dir .GOPATH/src/$(IMPORT_PATH))" 107 | $Q ln -s ../../../.. ".GOPATH/src/$(IMPORT_PATH)" 108 | $Q mkdir -p .GOPATH/test .GOPATH/cover 109 | $Q mkdir -p bin 110 | $Q ln -s ../bin .GOPATH/bin 111 | $Q touch $@ 112 | 113 | .PHONY: bin/gocovmerge bin/goimports 114 | bin/gocovmerge: .GOPATH/.ok 115 | @test -f ./bin/gocovmerge || \ 116 | { echo "Vendored gocovmerge not found, try running 'make setup'..."; exit 1; } 117 | bin/goimports: .GOPATH/.ok 118 | @test -d ./vendor/golang.org/x/tools/cmd/goimports || \ 119 | { echo "Vendored goimports not found, try running 'make setup'..."; exit 1; } 120 | $Q go install $(IMPORT_PATH)/vendor/golang.org/x/tools/cmd/goimports 121 | 122 | -------------------------------------------------------------------------------- /server/server_handler.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/blk-io/chimera-api/chimera" 9 | "github.com/blk-io/crux/api" 10 | "github.com/kevinburke/nacl" 11 | log "github.com/sirupsen/logrus" 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | type Server struct { 16 | Enclave Enclave 17 | } 18 | 19 | func (s *Server) Version(ctx context.Context, in *chimera.ApiVersion) (*chimera.ApiVersion, error) { 20 | return &chimera.ApiVersion{Version: apiVersion}, nil 21 | } 22 | 23 | func (s *Server) Upcheck(ctx context.Context, in *chimera.UpCheckResponse) (*chimera.UpCheckResponse, error) { 24 | return &chimera.UpCheckResponse{Message: upCheckResponse}, nil 25 | } 26 | func (s *Server) Send(ctx context.Context, in *chimera.SendRequest) (*chimera.SendResponse, error) { 27 | key, err := s.processSend(in.GetFrom(), in.GetTo(), &in.Payload) 28 | var sendResp chimera.SendResponse 29 | if err != nil { 30 | log.Error(err) 31 | } else { 32 | sendResp = chimera.SendResponse{Key: key} 33 | } 34 | return &sendResp, err 35 | } 36 | 37 | func (s *Server) processSend(b64from string, b64recipients []string, payload *[]byte) ([]byte, error) { 38 | log.WithFields(log.Fields{ 39 | "b64From": b64from, 40 | "b64Recipients": b64recipients, 41 | "payload": hex.EncodeToString(*payload)}).Debugf( 42 | "Processing send request") 43 | 44 | sender, err := base64.StdEncoding.DecodeString(b64from) 45 | if err != nil { 46 | decodeErrorGRPC("sender", b64from, err) 47 | return nil, err 48 | } 49 | 50 | recipients := make([][]byte, len(b64recipients)) 51 | for i, value := range b64recipients { 52 | recipient, err := base64.StdEncoding.DecodeString(value) 53 | if err != nil { 54 | decodeErrorGRPC("recipients", value, err) 55 | return nil, err 56 | } else { 57 | recipients[i] = recipient 58 | } 59 | } 60 | 61 | return s.Enclave.Store(payload, sender, recipients) 62 | } 63 | 64 | func (s *Server) Receive(ctx context.Context, in *chimera.ReceiveRequest) (*chimera.ReceiveResponse, error) { 65 | payload, err := s.processReceive(in.Key, in.To) 66 | var receiveResp chimera.ReceiveResponse 67 | if err != nil { 68 | log.Error(err) 69 | } else { 70 | receiveResp = chimera.ReceiveResponse{Payload: payload} 71 | } 72 | return &receiveResp, err 73 | } 74 | 75 | func (s *Server) processReceive(b64Key []byte, b64To string) ([]byte, error) { 76 | if b64To != "" { 77 | to, err := base64.StdEncoding.DecodeString(b64To) 78 | if err != nil { 79 | return nil, fmt.Errorf("unable to decode to: %s", b64Key) 80 | } 81 | 82 | return s.Enclave.Retrieve(&b64Key, &to) 83 | } else { 84 | return s.Enclave.RetrieveDefault(&b64Key) 85 | } 86 | } 87 | 88 | func (s *Server) UpdatePartyInfo(ctx context.Context, in *chimera.PartyInfo) (*chimera.PartyInfoResponse, error) { 89 | recipients := make(map[[nacl.KeySize]byte]string) 90 | for url, key := range in.Recipients { 91 | var as [32]byte 92 | copy(as[:], key) 93 | recipients[as] = url 94 | } 95 | s.Enclave.UpdatePartyInfoGrpc(in.Url, recipients, in.Parties) 96 | encoded := s.Enclave.GetEncodedPartyInfoGrpc() 97 | var decodedPartyInfo chimera.PartyInfoResponse 98 | err := json.Unmarshal(encoded, &decodedPartyInfo) 99 | if err != nil { 100 | log.Errorf("Unmarshalling failed with %v", err) 101 | } 102 | return &chimera.PartyInfoResponse{Payload: decodedPartyInfo.Payload}, nil 103 | } 104 | 105 | func (s *Server) Push(ctx context.Context, in *chimera.PushPayload) (*chimera.PartyInfoResponse, error) { 106 | sender := new([nacl.KeySize]byte) 107 | nonce := new([nacl.NonceSize]byte) 108 | recipientNonce := new([nacl.NonceSize]byte) 109 | copy((*sender)[:], in.Ep.Sender) 110 | copy((*nonce)[:], in.Ep.Nonce) 111 | copy((*recipientNonce)[:], in.Ep.ReciepientNonce) 112 | 113 | encyptedPayload := api.EncryptedPayload{ 114 | Sender: sender, 115 | CipherText: in.Ep.CipherText, 116 | Nonce: nonce, 117 | RecipientBoxes: in.Ep.ReciepientBoxes, 118 | RecipientNonce: recipientNonce, 119 | } 120 | 121 | digestHash, err := s.Enclave.StorePayloadGrpc(encyptedPayload, in.Encoded) 122 | if err != nil { 123 | log.Fatalf("Unable to store payload, error: %s\n", err) 124 | } 125 | 126 | return &chimera.PartyInfoResponse{Payload: digestHash}, nil 127 | } 128 | 129 | func (s *Server) Delete(ctx context.Context, in *chimera.DeleteRequest) (*chimera.DeleteRequest, error) { 130 | var deleteReq chimera.DeleteRequest 131 | err := s.Enclave.Delete(&deleteReq.Key) 132 | if err != nil { 133 | log.Fatalf("Unable to delete payload, error: %s\n", err) 134 | } 135 | return &chimera.DeleteRequest{Key: deleteReq.Key}, nil 136 | } 137 | 138 | func (s *Server) Resend(ctx context.Context, in *chimera.ResendRequest) (*chimera.ResendResponse, error) { 139 | var resendReq chimera.ResendRequest 140 | var err error 141 | 142 | if resendReq.Type == "all" { 143 | err = s.Enclave.RetrieveAllFor(&resendReq.PublicKey) 144 | if err != nil { 145 | log.Fatalf("Invalid body, exited with %s", err) 146 | } 147 | return nil, err 148 | } else if resendReq.Type == "individual" { 149 | var encodedPl *[]byte 150 | encodedPl, err = s.Enclave.RetrieveFor(&resendReq.Key, &resendReq.PublicKey) 151 | if err != nil { 152 | log.Fatalf("Invalid body, exited with %s", err) 153 | return nil, err 154 | } 155 | return &chimera.ResendResponse{Encoded: *encodedPl}, nil 156 | } 157 | return nil, err 158 | } 159 | 160 | func decodeErrorGRPC(name string, value string, err error) { 161 | log.Error(fmt.Sprintf("Invalid request: unable to decode %s: %s, error: %s\n", 162 | name, value, err)) 163 | } 164 | -------------------------------------------------------------------------------- /api/encoding.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/blk-io/crux/utils" 6 | "github.com/kevinburke/nacl" 7 | ) 8 | 9 | func EncodePayload(ep EncryptedPayload) []byte { 10 | // constant fields are 216 bytes 11 | encoded := make([]byte, 512) 12 | 13 | offset := 0 14 | encoded, offset = writeSlice((*ep.Sender)[:], encoded, offset) 15 | encoded, offset = writeSlice(ep.CipherText, encoded, offset) 16 | encoded, offset = writeSlice((*ep.Nonce)[:], encoded, offset) 17 | encoded, offset = writeSliceOfSlice(ep.RecipientBoxes, encoded, offset) 18 | encoded, offset = writeSlice((*ep.RecipientNonce)[:], encoded, offset) 19 | 20 | return encoded[:offset] 21 | } 22 | 23 | func DecodePayload(encoded []byte) EncryptedPayload { 24 | 25 | ep := EncryptedPayload{ 26 | Sender: new([nacl.KeySize]byte), 27 | Nonce: new([nacl.NonceSize]byte), 28 | RecipientNonce: new([nacl.NonceSize]byte), 29 | } 30 | 31 | offset := 0 32 | offset = readSliceToArray(encoded, offset, (*ep.Sender)[:]) 33 | ep.CipherText, offset = readSlice(encoded, offset) 34 | offset = readSliceToArray(encoded, offset, (*ep.Nonce)[:]) 35 | ep.RecipientBoxes, offset = readSliceOfSlice(encoded, offset) 36 | offset = readSliceToArray(encoded, offset, (*ep.RecipientNonce)[:]) 37 | 38 | return ep 39 | } 40 | 41 | func EncodePayloadWithRecipients(ep EncryptedPayload, recipients [][]byte) []byte { 42 | encoded := make([][]byte, 2) 43 | 44 | encoded[0] = EncodePayload(ep) 45 | 46 | encodedRecipients := make([]byte, 256) 47 | encodedRecipients, recipientsLength := writeSliceOfSlice(recipients, encodedRecipients, 0) 48 | encoded[1] = encodedRecipients[:recipientsLength] 49 | 50 | encoded2, length := writeSliceOfSlice(encoded, make([]byte, 512), 0) 51 | return encoded2[:length] 52 | } 53 | 54 | func DecodePayloadWithRecipients(encoded []byte) (EncryptedPayload, [][]byte) { 55 | 56 | decoded, _ := readSliceOfSlice(encoded, 0) 57 | 58 | ep := DecodePayload(decoded[0]) 59 | recipients, _ := readSliceOfSlice(decoded[1], 0) 60 | 61 | return ep, recipients 62 | } 63 | 64 | func EncodePartyInfo(pi PartyInfo) []byte { 65 | 66 | encoded := make([]byte, 256) 67 | 68 | offset := 0 69 | 70 | encoded, offset = writeSlice([]byte(pi.url), encoded, offset) 71 | encoded, offset = writeInt(len(pi.recipients), encoded, offset) 72 | 73 | for recipient, url := range pi.recipients { 74 | tuple := [][]byte{ 75 | recipient[:], 76 | []byte(url), 77 | } 78 | encoded, offset = writeSliceOfSlice(tuple, encoded, offset) 79 | } 80 | 81 | parties := make([][]byte, len(pi.parties)) 82 | i := 0 83 | for party := range pi.parties { 84 | parties[i] = []byte(party) 85 | i += 1 86 | } 87 | encoded, offset = writeSliceOfSlice(parties, encoded, offset) 88 | 89 | return encoded 90 | } 91 | 92 | func DecodePartyInfo(encoded []byte) (PartyInfo, error) { 93 | pi := PartyInfo{ 94 | recipients: make(map[[nacl.KeySize]byte]string), 95 | parties: make(map[string]bool), 96 | } 97 | 98 | url, offset := readSlice(encoded, 0) 99 | pi.url = string(url) 100 | 101 | var size int 102 | size, offset = readInt(encoded, offset) 103 | 104 | for i := 0; i < size; i++ { 105 | var kv [][]byte 106 | kv, offset = readSliceOfSlice(encoded, offset) 107 | key, err := utils.ToKey(kv[0]) 108 | if err != nil { 109 | return PartyInfo{}, err 110 | } 111 | pi.recipients[*key] = string(kv[1]) 112 | } 113 | 114 | var parties [][]byte 115 | parties, offset = readSliceOfSlice(encoded, offset) 116 | for _, party := range parties { 117 | pi.parties[string(party)] = true 118 | } 119 | 120 | return pi, nil 121 | } 122 | 123 | func writeInt(v int, dest []byte, offset int) ([]byte, int) { 124 | dest = confirmCapacity(dest, offset, 8) 125 | binary.BigEndian.PutUint64(dest[offset:], uint64(v)) 126 | return dest, offset + 8 127 | } 128 | 129 | func confirmCapacity(dest []byte, offset, required int) []byte { 130 | length := len(dest) 131 | if length-offset < required { 132 | var newLength int 133 | if required > length { 134 | newLength = utils.NextPowerOf2(required) 135 | } else { 136 | newLength = length 137 | } 138 | return append(dest, make([]byte, newLength)...) 139 | } else { 140 | return dest 141 | } 142 | } 143 | 144 | func readInt(src []byte, offset int) (int, int) { 145 | return int(binary.BigEndian.Uint64(src[offset:])), offset + 8 146 | } 147 | 148 | func writeSlice(src []byte, dest []byte, offset int) ([]byte, int) { 149 | length := len(src) 150 | dest, offset = writeInt(length, dest, offset) 151 | 152 | dest = confirmCapacity(dest, offset, length) 153 | copy(dest[offset:], src) 154 | return dest, offset + length 155 | } 156 | 157 | func readSliceToArray(src []byte, offset int, dest []byte) int { 158 | var length int 159 | length, offset = readInt(src, offset) 160 | offset += copy(dest, src[offset:offset+length]) 161 | return offset 162 | } 163 | 164 | func readSlice(src []byte, offset int) ([]byte, int) { 165 | var length int 166 | length, offset = readInt(src, offset) 167 | return src[offset : offset+length], offset + length 168 | } 169 | 170 | func writeSliceOfSlice(src [][]byte, dest []byte, offset int) ([]byte, int) { 171 | length := len(src) 172 | dest, offset = writeInt(length, dest, offset) 173 | 174 | for _, b := range src { 175 | dest, offset = writeSlice(b, dest, offset) 176 | } 177 | 178 | return dest, offset 179 | } 180 | 181 | func readSliceOfSlice(src []byte, offset int) ([][]byte, int) { 182 | arraySize, offset := readInt(src, offset) 183 | 184 | result := make([][]byte, arraySize) 185 | for i := 0; i < arraySize; i++ { 186 | var length int 187 | length, offset = readInt(src, offset) 188 | result[i] = append( 189 | result[i], src[offset:offset+length]...) 190 | offset += length 191 | } 192 | return result, offset 193 | } 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crux 2 | 3 | Quorum Slack 4 | Build Status 5 | Go Report Card 6 | 7 | Data privacy for Quorum. 8 | 9 | Crux is a secure enclave for Quorum written in Golang. 10 | 11 | It is a replacement for [Constellation](https://github.com/jpmorganchase/constellation/), the 12 | secure enclave component of [Quorum](https://github.com/jpmorganchase/quorum/), written in Haskell. 13 | 14 | ## Getting started 15 | 16 | ### 4-node Quorum network with Crux 17 | 18 | The best way to start is to run the 19 | [Quorum-Crux Docker image](https://github.com/blk-io/crux/tree/master/docker/quorum-crux). This 20 | image runs a 4 node Quorum network using Crux as the secure enclave communicating over gRPC. 21 | 22 | ```bash 23 | git clone https://github.com/blk-io/crux.git 24 | docker-compose -f docker/quorum-crux/docker-compose.yaml up 25 | ``` 26 | 27 | Where the node details are as follows: 28 | 29 | | Name | Quorum node address | Account key | Crux node key | 30 | | ------- | ----------------------- | ------------------------------------------ | -------------------------------------------- | 31 | | quorum1 | http://localhost:22001 | 0xed9d02e382b34818e88b88a309c7fe71e65f419d | BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= | 32 | | quorum2 | http://localhost:22002 | 0xca843569e3427144cead5e4d5999a3d0ccf92b8e | QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= | 33 | | quorum3 | http://localhost:22003 | 0x0fbdc686b912d7722dc86510934589e0aaf3b55a | 1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= | 34 | | quorum4 | http://localhost:22004 | 0x9186eb3d20cbd1f5f992a950d808c4495153abd5 | oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= | 35 | 36 | #### local docker 37 | If you want to make changes to e.g. istanbul-start.sh then build the docker image locally: 38 | 39 | docker-compose -f docker-compose-local.yaml up --build 40 | 41 | ### 2-node Crux only-network 42 | 43 | [2 Crux nodes example](https://github.com/blk-io/crux/tree/master/docker/crux) is simple Docker 44 | image to just bring up 2 Crux nodes which communicate with each other. 45 | 46 | ```bash 47 | git clone https://github.com/blk-io/crux.git 48 | docker-compose -f docker/crux/docker-compose.yaml up 49 | ``` 50 | 51 | Where the Crux node keys are the same as `quorum1` and `quorum2` above, and are listening on ports 52 | 9001 and 9002 for gRPC requests. 53 | 54 | ### Vagrant VM 55 | 56 | For those of you who are unable to use Docker, you can run the 57 | [7 Nodes Quorum example](https://github.com/blk-io/quorum-examples) which is an updated version 58 | of JP Morgan's Quorum 7 Nodes example using Crux as the secure enclave. 59 | 60 | ### Download the latest binary 61 | 62 | The latest binaries for different platforms are available on the 63 | [release](https://github.com/blk-io/crux/releases/latest) page. 64 | 65 | ## Generating keys 66 | 67 | Each Crux instance requires at least one key-pair to be associated with it. The key-pair is used 68 | to ensure transaction privacy. Crux uses the [NaCl cryptography library](https://nacl.cr.yp.to/). 69 | 70 | You use the `--generate-keys` argument to generate a new key-pair with Crux: 71 | 72 | ```bash 73 | crux --generate-keys myKey 74 | ``` 75 | 76 | This will produce two files, named `myKey.key` and `myKey.pub` reflecting the private and public keys 77 | respectively. 78 | 79 | ## Core configuration 80 | 81 | At a minimum, Crux requires the following configuration parameters. This tells the Crux instance 82 | what port it is running on and what ip address it should advertise to other peers. 83 | 84 | Details of at least one key-pair must be provided for the Crux node to store requests on behalf of. 85 | 86 | ```bash 87 | crux --url=http://127.0.0.1:9001/ --port=9001 --workdir=crux --publickeys=tm.pub --privatekeys=tm.key --othernodes=https://127.0.0.1:9001/ 88 | ``` 89 | 90 | ## Build instructions 91 | 92 | If you'd prefer to run just a client, you can build using the below instructions and run as per 93 | the below. 94 | 95 | ```bash 96 | git clone https://github.com/blk-io/crux.git 97 | cd crux 98 | make setup && make 99 | ./bin/crux 100 | 101 | Usage of ./bin/crux: 102 | crux.config Optional config file 103 | --alwayssendto string List of public keys for nodes to send all transactions too 104 | --berkeleydb Use Berkeley DB for working with an existing Constellation data store [experimental] 105 | --generate-keys string Generate a new keypair 106 | --grpc Use gRPC server (default true) 107 | --grpcport int The local port to listen on for JSON extensions of gRPC (default -1) 108 | --networkinterface string The network interface to bind the server to (default "localhost") 109 | --othernodes string "Boot nodes" to connect to to discover the network 110 | --port int The local port to listen on (default -1) 111 | --privatekeys string Private keys hosted by this node 112 | --publickeys string Public keys hosted by this node 113 | --socket string IPC socket to create for access to the Private API (default "crux.ipc") 114 | --storage string Database storage file name (default "crux.db") 115 | --tls Use TLS to secure HTTP communications 116 | --tlsservercert string The server certificate to be used 117 | --tlsserverkey string The server private key 118 | --url string The URL to advertise to other nodes (reachable by them) 119 | -v, --v int Verbosity level of logs (shorthand) (default 1) 120 | --verbosity int Verbosity level of logs (default 1) 121 | --workdir string The folder to put stuff in (default: .) (default ".") 122 | ``` 123 | 124 | ## How does it work? 125 | 126 | At present, Crux performs its cryptographic operations in a manner identical to Constellation. You 127 | can read the specifics [here](https://github.com/jpmorganchase/constellation/#how-it-works). 128 | 129 | The two main workflows for handling private transactions are the submission and retrieval 130 | demonstrated below. 131 | 132 | ### New transaction submission 133 | 134 | ![New Transaction Sequence](./docs/new-tx.svg) 135 | 136 | ### Existing transaction retrieval 137 | 138 | ![Read Transaction Sequence](./docs/read-tx.svg) 139 | 140 | ## Logical architecture 141 | 142 | ![Logical architecture](https://github.com/blk-io/crux/blob/master/docs/quorum-architecture.png) 143 | 144 | ## Why Crux? 145 | 146 | *Crux is a constellation located in the southern sky in a bright portion of the Milky Way. It is 147 | among the most easily distinguished constellations, even though it is the smallest of all 88 148 | modern constellations. (Source: [Wikipedia](https://en.wikipedia.org/wiki/Crux))* 149 | 150 | *The critical or transitional moment or issue, a turning point.* 151 | 152 | ## Thanks 153 | 154 | [@patrickmn](https://github.com/patrickmn) the original author of Constellation. Crux would not 155 | exist were it not for his work. 156 | -------------------------------------------------------------------------------- /docker/quorum-crux/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | 4 | node1: &quorum_crux_node 5 | # Pull image down from Docker Hub 6 | image: blkio10/quorum-crux:v1.0.0 7 | # Uncomment the below, and comment out the above line to build the Docker images yourself 8 | # image: blk.io/quorum/quorum-crux 9 | # build: 10 | # context: . 11 | container_name: quorum1 12 | ports: 13 | - 22001:22000 14 | - 21001:21000 15 | - 9001:9000 16 | restart: always 17 | networks: 18 | quorum_net: 19 | ipv4_address: 10.5.0.11 20 | environment: 21 | - GETH_KEY={"address":"ed9d02e382b34818e88b88a309c7fe71e65f419d","crypto":{"cipher":"aes-128-ctr","ciphertext":"4e77046ba3f699e744acb4a89c36a3ea1158a1bd90a076d36675f4c883864377","cipherparams":{"iv":"a8932af2a3c0225ee8e872bc0e462c11"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8ca49552b3e92f79c51f2cd3d38dfc723412c212e702bd337a3724e8937aff0f"},"mac":"6d1354fef5aa0418389b1a5d1f5ee0050d7273292a1171c51fd02f9ecff55264"},"id":"a65d1ac3-db7e-445d-a1cc-b6c5eeaa05e0","version":3} 22 | - NODE_KEY=633b2ef3ed5306f05a532eb44eb84858aca470d4fde039c1c988088e48070e64 23 | - CRUX_PUB=BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= 24 | - CRUX_PRIV={"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} 25 | - PORT=9000 26 | - OWN_URL=node1 27 | - GETH_PORT=21000 28 | - GETH_RPC_PORT=22000 29 | - OTHER_NODES=http://node2:9000/ 30 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 31 | 32 | node2: 33 | <<: *quorum_crux_node 34 | container_name: quorum2 35 | ports: 36 | - 22002:22000 37 | - 21002:21000 38 | - 9002:9000 39 | networks: 40 | quorum_net: 41 | ipv4_address: 10.5.0.12 42 | environment: 43 | - GETH_KEY={"address":"ca843569e3427144cead5e4d5999a3d0ccf92b8e","crypto":{"cipher":"aes-128-ctr","ciphertext":"01d409941ce57b83a18597058033657182ffb10ae15d7d0906b8a8c04c8d1e3a","cipherparams":{"iv":"0bfb6eadbe0ab7ffaac7e1be285fb4e5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7b90f455a95942c7c682e0ef080afc2b494ef71e749ba5b384700ecbe6f4a1bf"},"mac":"4cc851f9349972f851d03d75a96383a37557f7c0055763c673e922de55e9e307"},"id":"354e3b35-1fed-407d-a358-889a29111211","version":3} 44 | - NODE_KEY=d5b8dfced693dbb7bf858dce40e3b2a0373696ac88ccf5e1c0052e26b7d77e49 45 | - CRUX_PUB=QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= 46 | - CRUX_PRIV={"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} 47 | - PORT=9000 48 | - OWN_URL=node2 49 | - GETH_PORT=21000 50 | - GETH_RPC_PORT=22000 51 | - OTHER_NODES=http://node3:9000/ 52 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 53 | 54 | node3: 55 | <<: *quorum_crux_node 56 | container_name: quorum3 57 | ports: 58 | - 22003:22000 59 | - 9003:9000 60 | - 21003:21000 61 | networks: 62 | quorum_net: 63 | ipv4_address: 10.5.0.13 64 | environment: 65 | - GETH_KEY={"address":"0fbdc686b912d7722dc86510934589e0aaf3b55a","crypto":{"cipher":"aes-128-ctr","ciphertext":"6b2c72c6793f3da8185e36536e02f574805e41c18f551f24b58346ef4ecf3640","cipherparams":{"iv":"582f27a739f39580410faa108d5cc59f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1a79b0db3f8cb5c2ae4fa6ccb2b5917ce446bd5e42c8d61faeee512b97b4ad4a"},"mac":"cecb44d2797d6946805d5d744ff803805477195fab1d2209eddc3d1158f2e403"},"id":"f7292e90-af71-49af-a5b3-40e8493f4681","version":3} 66 | - NODE_KEY=45d87820ad7792f86592a8e3494c3e78b4755a480cfad1799d6c3f28c3f0d87c 67 | - CRUX_PUB=1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= 68 | - CRUX_PRIV={"data":{"bytes":"tMxUVR8bX7aq/TbpVHc2QV3SN2iUuExBwefAuFsO0Lg="},"type":"unlocked"} 69 | - PORT=9000 70 | - OWN_URL=node3 71 | - GETH_PORT=21000 72 | - GETH_RPC_PORT=22000 73 | - OTHER_NODES=http://node4:9000/ 74 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 75 | 76 | node4: 77 | <<: *quorum_crux_node 78 | container_name: quorum4 79 | ports: 80 | - 22004:22000 81 | - 9004:9000 82 | - 21004:21000 83 | networks: 84 | quorum_net: 85 | ipv4_address: 10.5.0.14 86 | environment: 87 | - GETH_KEY={"address":"9186eb3d20cbd1f5f992a950d808c4495153abd5","crypto":{"cipher":"aes-128-ctr","ciphertext":"d160a630a39be3ff35556055406d8ff2a635f0535fe298d62ccc812d8f7b3bd5","cipherparams":{"iv":"82fce06bc6e1658a5e81ccef3b753329"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8d0c486db4c942721f4f5e96d48e9344805d101dad8159962b8a2008ac718548"},"mac":"4a92bda949068968d470320260ae1a825aa22f6a40fb8567c9f91d700c3f7e91"},"id":"bdb3b4f6-d8d0-4b00-8473-e223ef371b5c","version":3} 88 | - NODE_KEY=daa89d4ae250d24b33847343d0cc0116c48331b81e28514522bb7f77f2be5676 89 | - CRUX_PUB=oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= 90 | - CRUX_PRIV={"data":{"bytes":"grQjd3dBp4qFs8/5Jdq7xjz++aUx/LXAqISFyPWaCRw="},"type":"unlocked"} 91 | - PORT=9000 92 | - OWN_URL=node4 93 | - GETH_PORT=21000 94 | - GETH_RPC_PORT=22000 95 | - OTHER_NODES=http://node1:9000/ 96 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 97 | 98 | networks: 99 | quorum_net: 100 | driver: bridge 101 | ipam: 102 | driver: default 103 | config: 104 | - subnet: 10.5.0.0/24 -------------------------------------------------------------------------------- /docker/quorum-crux/docker-compose-local.yaml: -------------------------------------------------------------------------------- 1 | # build the docker container locally 2 | version: "3.4" 3 | services: 4 | 5 | node1: &quorum_crux_node 6 | # Pull image down from Docker Hub 7 | # image: blkio10/quorum-crux:v1.0.0 8 | # Uncomment the below, and comment out the above line to build the Docker images yourself 9 | image: blk.io/quorum/quorum-crux 10 | build: 11 | context: . 12 | container_name: quorum1 13 | ports: 14 | - 22001:22000 15 | - 21001:21000 16 | - 9001:9000 17 | restart: always 18 | networks: 19 | quorum_net: 20 | ipv4_address: 10.5.0.11 21 | environment: 22 | - GETH_KEY={"address":"ed9d02e382b34818e88b88a309c7fe71e65f419d","crypto":{"cipher":"aes-128-ctr","ciphertext":"4e77046ba3f699e744acb4a89c36a3ea1158a1bd90a076d36675f4c883864377","cipherparams":{"iv":"a8932af2a3c0225ee8e872bc0e462c11"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8ca49552b3e92f79c51f2cd3d38dfc723412c212e702bd337a3724e8937aff0f"},"mac":"6d1354fef5aa0418389b1a5d1f5ee0050d7273292a1171c51fd02f9ecff55264"},"id":"a65d1ac3-db7e-445d-a1cc-b6c5eeaa05e0","version":3} 23 | - NODE_KEY=633b2ef3ed5306f05a532eb44eb84858aca470d4fde039c1c988088e48070e64 24 | - CRUX_PUB=BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= 25 | - CRUX_PRIV={"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} 26 | - PORT=9000 27 | - OWN_URL=node1 28 | - GETH_PORT=21000 29 | - GETH_RPC_PORT=22000 30 | - OTHER_NODES=http://node2:9000/ 31 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 32 | 33 | node2: 34 | <<: *quorum_crux_node 35 | container_name: quorum2 36 | ports: 37 | - 22002:22000 38 | - 21002:21000 39 | - 9002:9000 40 | networks: 41 | quorum_net: 42 | ipv4_address: 10.5.0.12 43 | environment: 44 | - GETH_KEY={"address":"ca843569e3427144cead5e4d5999a3d0ccf92b8e","crypto":{"cipher":"aes-128-ctr","ciphertext":"01d409941ce57b83a18597058033657182ffb10ae15d7d0906b8a8c04c8d1e3a","cipherparams":{"iv":"0bfb6eadbe0ab7ffaac7e1be285fb4e5"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"7b90f455a95942c7c682e0ef080afc2b494ef71e749ba5b384700ecbe6f4a1bf"},"mac":"4cc851f9349972f851d03d75a96383a37557f7c0055763c673e922de55e9e307"},"id":"354e3b35-1fed-407d-a358-889a29111211","version":3} 45 | - NODE_KEY=d5b8dfced693dbb7bf858dce40e3b2a0373696ac88ccf5e1c0052e26b7d77e49 46 | - CRUX_PUB=QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc= 47 | - CRUX_PRIV={"data":{"bytes":"nDFwJNHSiT1gNzKBy9WJvMhmYRkW3TzFUmPsNzR6oFk="},"type":"unlocked"} 48 | - PORT=9000 49 | - OWN_URL=node2 50 | - GETH_PORT=21000 51 | - GETH_RPC_PORT=22000 52 | - OTHER_NODES=http://node3:9000/ 53 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 54 | 55 | node3: 56 | <<: *quorum_crux_node 57 | container_name: quorum3 58 | ports: 59 | - 22003:22000 60 | - 9003:9000 61 | - 21003:21000 62 | networks: 63 | quorum_net: 64 | ipv4_address: 10.5.0.13 65 | environment: 66 | - GETH_KEY={"address":"0fbdc686b912d7722dc86510934589e0aaf3b55a","crypto":{"cipher":"aes-128-ctr","ciphertext":"6b2c72c6793f3da8185e36536e02f574805e41c18f551f24b58346ef4ecf3640","cipherparams":{"iv":"582f27a739f39580410faa108d5cc59f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"1a79b0db3f8cb5c2ae4fa6ccb2b5917ce446bd5e42c8d61faeee512b97b4ad4a"},"mac":"cecb44d2797d6946805d5d744ff803805477195fab1d2209eddc3d1158f2e403"},"id":"f7292e90-af71-49af-a5b3-40e8493f4681","version":3} 67 | - NODE_KEY=45d87820ad7792f86592a8e3494c3e78b4755a480cfad1799d6c3f28c3f0d87c 68 | - CRUX_PUB=1iTZde/ndBHvzhcl7V68x44Vx7pl8nwx9LqnM/AfJUg= 69 | - CRUX_PRIV={"data":{"bytes":"tMxUVR8bX7aq/TbpVHc2QV3SN2iUuExBwefAuFsO0Lg="},"type":"unlocked"} 70 | - PORT=9000 71 | - OWN_URL=node3 72 | - GETH_PORT=21000 73 | - GETH_RPC_PORT=22000 74 | - OTHER_NODES=http://node4:9000/ 75 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 76 | 77 | node4: 78 | <<: *quorum_crux_node 79 | container_name: quorum4 80 | ports: 81 | - 22004:22000 82 | - 9004:9000 83 | - 21004:21000 84 | networks: 85 | quorum_net: 86 | ipv4_address: 10.5.0.14 87 | environment: 88 | - GETH_KEY={"address":"9186eb3d20cbd1f5f992a950d808c4495153abd5","crypto":{"cipher":"aes-128-ctr","ciphertext":"d160a630a39be3ff35556055406d8ff2a635f0535fe298d62ccc812d8f7b3bd5","cipherparams":{"iv":"82fce06bc6e1658a5e81ccef3b753329"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8d0c486db4c942721f4f5e96d48e9344805d101dad8159962b8a2008ac718548"},"mac":"4a92bda949068968d470320260ae1a825aa22f6a40fb8567c9f91d700c3f7e91"},"id":"bdb3b4f6-d8d0-4b00-8473-e223ef371b5c","version":3} 89 | - NODE_KEY=daa89d4ae250d24b33847343d0cc0116c48331b81e28514522bb7f77f2be5676 90 | - CRUX_PUB=oNspPPgszVUFw0qmGFfWwh1uxVUXgvBxleXORHj07g8= 91 | - CRUX_PRIV={"data":{"bytes":"grQjd3dBp4qFs8/5Jdq7xjz++aUx/LXAqISFyPWaCRw="},"type":"unlocked"} 92 | - PORT=9000 93 | - OWN_URL=node4 94 | - GETH_PORT=21000 95 | - GETH_RPC_PORT=22000 96 | - OTHER_NODES=http://node1:9000/ 97 | - PERMISSIONED_NODES=["enode://5c3c98e3a28a87e73ab40468212de7ab6cf0e2afa77781295925f32369c00baf30f664e52f8d152c02b069d6daa1a61f477e3c1eca64403529dfbd0c31e09524@10.5.0.11:21000?discport=0","enode://9b98a96a8ba080ff4c7863e5fdf3211a7082b612d5897ae4eed687eec391eb421c8ed7c572ca17f257441a0cb544a7c184244dfdf9a114f5251da3dac72e7585@10.5.0.12:21000?discport=0","enode://a51690b44ab39fd83c42b5a7c087ba222970951f06655ebbba1625267fad105fd238c9f092e05b2293f526e748b2fa423b22d66296f770037393c26a9e5d3543@10.5.0.13:21000?discport=0","enode://a68df7cd75e9ea490653bdba7c6868f979944578e59c9efd2aa62878822f16f46a49a13289f6392923053be1acb3a6ec8e2fc92cae59de859fd5892071fbfa88@10.5.0.14:21000?discport=0"] 98 | 99 | networks: 100 | quorum_net: 101 | driver: bridge 102 | ipam: 103 | driver: default 104 | config: 105 | - subnet: 10.5.0.0/24 106 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | digest = "1:86840754a6a45d993a441d23094f501eb0cb0b12fe71f4c6cab2f3a826cb8725" 6 | name = "github.com/blk-io/chimera-api" 7 | packages = ["chimera"] 8 | pruneopts = "T" 9 | revision = "ebd4db90873296427420c2fe2acec18c127b401d" 10 | 11 | [[projects]] 12 | digest = "1:7fc160b460a6fc506b37fcca68332464c3f2cd57b6e3f111f26c5bbfd2d5518e" 13 | name = "github.com/fsnotify/fsnotify" 14 | packages = ["."] 15 | pruneopts = "T" 16 | revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" 17 | version = "v1.4.7" 18 | 19 | [[projects]] 20 | digest = "1:832e17df5ff8bbe0e0693d2fb46c5e53f96c662ee804049ce3ab6557df74e3ab" 21 | name = "github.com/golang/protobuf" 22 | packages = [ 23 | "jsonpb", 24 | "proto", 25 | "protoc-gen-go/descriptor", 26 | "ptypes", 27 | "ptypes/any", 28 | "ptypes/duration", 29 | "ptypes/struct", 30 | "ptypes/timestamp", 31 | ] 32 | pruneopts = "T" 33 | revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" 34 | version = "v1.1.0" 35 | 36 | [[projects]] 37 | branch = "master" 38 | digest = "1:cf5bb7d7c59d8313289e5b756e24462cacd958d1e6db3bdbe1c800c677ad0f94" 39 | name = "github.com/golang/snappy" 40 | packages = ["."] 41 | pruneopts = "T" 42 | revision = "553a641470496b2327abcac10b36396bd98e45c9" 43 | 44 | [[projects]] 45 | digest = "1:69cd81163a00bb8405194d47b8be19283744779b6104f2d6b3735e2a01cdb6fa" 46 | name = "github.com/grpc-ecosystem/grpc-gateway" 47 | packages = [ 48 | "runtime", 49 | "runtime/internal", 50 | "utilities", 51 | ] 52 | pruneopts = "T" 53 | revision = "92583770e3f01b09a0d3e9bdf64321d8bebd48f2" 54 | version = "v1.4.1" 55 | 56 | [[projects]] 57 | branch = "master" 58 | digest = "1:17a030e647213ad422df723c8ee8902328040dd74b7fc68cb016784baaf95657" 59 | name = "github.com/hashicorp/hcl" 60 | packages = [ 61 | ".", 62 | "hcl/ast", 63 | "hcl/parser", 64 | "hcl/printer", 65 | "hcl/scanner", 66 | "hcl/strconv", 67 | "hcl/token", 68 | "json/parser", 69 | "json/scanner", 70 | "json/token", 71 | ] 72 | pruneopts = "T" 73 | revision = "ef8a98b0bbce4a65b5aa4c368430a80ddc533168" 74 | 75 | [[projects]] 76 | branch = "master" 77 | digest = "1:e29757afd23af4c0c109594ccf0daaf8dd8e325f56ad38febbc4d13adfde0614" 78 | name = "github.com/jsimonetti/berkeleydb" 79 | packages = ["."] 80 | pruneopts = "T" 81 | revision = "5cde5eaaf78c6510c5f64f5347244806a06ba87b" 82 | 83 | [[projects]] 84 | digest = "1:599ec2ed1b0ab8e5b2b6d6d849c47cbbf5733d7988aee541fd1e5befe69b6095" 85 | name = "github.com/kevinburke/nacl" 86 | packages = [ 87 | ".", 88 | "box", 89 | "onetimeauth", 90 | "randombytes", 91 | "scalarmult", 92 | "secretbox", 93 | ] 94 | pruneopts = "T" 95 | revision = "247b7cfd826641547b7ee8b89c785901c4e94b0b" 96 | version = "0.5" 97 | 98 | [[projects]] 99 | digest = "1:32ce5f79bec2865f25ae331a39ad67e507c9a6ed0b0da298db2c29efa8fe366f" 100 | name = "github.com/magiconair/properties" 101 | packages = ["."] 102 | pruneopts = "T" 103 | revision = "c3beff4c2358b44d0493c7dda585e7db7ff28ae6" 104 | version = "v1.7.6" 105 | 106 | [[projects]] 107 | branch = "master" 108 | digest = "1:2514da1e59c0a936d8c1e0fbf5592267a3c5893eb4555ce767bb54d149e9cf6e" 109 | name = "github.com/mitchellh/mapstructure" 110 | packages = ["."] 111 | pruneopts = "T" 112 | revision = "00c29f56e2386353d58c599509e8dc3801b0d716" 113 | 114 | [[projects]] 115 | digest = "1:63fc640566e87c5a5f2f3646725f308cf1a6b8e30fdba2d49941d978c3cb9b4d" 116 | name = "github.com/pelletier/go-toml" 117 | packages = ["."] 118 | pruneopts = "T" 119 | revision = "acdc4509485b587f5e675510c4f2c63e90ff68a8" 120 | version = "v1.1.0" 121 | 122 | [[projects]] 123 | digest = "1:047075087eff9bcda0806c636c479fae29b9ae55da320d3d961eb5d3203e2a75" 124 | name = "github.com/sirupsen/logrus" 125 | packages = ["."] 126 | pruneopts = "T" 127 | revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc" 128 | version = "v1.0.5" 129 | 130 | [[projects]] 131 | digest = "1:ed2a3679c070c90f83aacc19d114536db7c5b739980837fbdc86cbf937493400" 132 | name = "github.com/spf13/afero" 133 | packages = [ 134 | ".", 135 | "mem", 136 | ] 137 | pruneopts = "T" 138 | revision = "63644898a8da0bc22138abf860edaf5277b6102e" 139 | version = "v1.1.0" 140 | 141 | [[projects]] 142 | digest = "1:516e71bed754268937f57d4ecb190e01958452336fa73dbac880894164e91c1f" 143 | name = "github.com/spf13/cast" 144 | packages = ["."] 145 | pruneopts = "T" 146 | revision = "8965335b8c7107321228e3e3702cab9832751bac" 147 | version = "v1.2.0" 148 | 149 | [[projects]] 150 | branch = "master" 151 | digest = "1:080e5f630945ad754f4b920e60b4d3095ba0237ebf88dc462eb28002932e3805" 152 | name = "github.com/spf13/jwalterweatherman" 153 | packages = ["."] 154 | pruneopts = "T" 155 | revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" 156 | 157 | [[projects]] 158 | digest = "1:9798f8595f3bf57586a622e8e78b7b8d159ceb375b9485c7fbcd2fd686ac4c46" 159 | name = "github.com/spf13/pflag" 160 | packages = ["."] 161 | pruneopts = "T" 162 | revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66" 163 | version = "v1.0.0" 164 | 165 | [[projects]] 166 | digest = "1:fbfebb70b35ccd17f5a91ba29f3d9dfeea149a98b44780a14efade3526c09bb3" 167 | name = "github.com/spf13/viper" 168 | packages = ["."] 169 | pruneopts = "T" 170 | revision = "b5e8006cbee93ec955a89ab31e0e3ce3204f3736" 171 | version = "v1.0.2" 172 | 173 | [[projects]] 174 | branch = "master" 175 | digest = "1:ef0753127ee10562925146777ed5f5f41431c0ccc8fa63da1cfae678cc7b8c74" 176 | name = "github.com/syndtr/goleveldb" 177 | packages = [ 178 | "leveldb", 179 | "leveldb/cache", 180 | "leveldb/comparer", 181 | "leveldb/errors", 182 | "leveldb/filter", 183 | "leveldb/iterator", 184 | "leveldb/journal", 185 | "leveldb/memdb", 186 | "leveldb/opt", 187 | "leveldb/storage", 188 | "leveldb/table", 189 | "leveldb/util", 190 | ] 191 | pruneopts = "T" 192 | revision = "714f901b98fdb3aa954b4193d8cbd64a28d80cad" 193 | 194 | [[projects]] 195 | branch = "master" 196 | digest = "1:dbe2585d9a08433ff9d1951bab1df0bc8c1bbd50c9fb866f92b661d88beb5694" 197 | name = "golang.org/x/crypto" 198 | packages = [ 199 | "curve25519", 200 | "poly1305", 201 | "salsa20/salsa", 202 | "sha3", 203 | "ssh/terminal", 204 | ] 205 | pruneopts = "T" 206 | revision = "beb2a9779c3b677077c41673505f150149fce895" 207 | 208 | [[projects]] 209 | branch = "master" 210 | digest = "1:1591a31d3ebd0f8d083ec588de4fb0b540809cf75edee9467c554cc17da0620a" 211 | name = "golang.org/x/net" 212 | packages = [ 213 | "context", 214 | "http/httpguts", 215 | "http2", 216 | "http2/hpack", 217 | "idna", 218 | "internal/timeseries", 219 | "trace", 220 | ] 221 | pruneopts = "T" 222 | revision = "89e543239a64caf31d3a6865872ea120b41446df" 223 | 224 | [[projects]] 225 | branch = "master" 226 | digest = "1:0c0ea93b25f3ef36c9662a679ba5fddae56ade049701857d020e3e73ae707e4b" 227 | name = "golang.org/x/sys" 228 | packages = [ 229 | "unix", 230 | "windows", 231 | ] 232 | pruneopts = "T" 233 | revision = "3b87a42e500a6dc65dae1a55d0b641295971163e" 234 | 235 | [[projects]] 236 | digest = "1:6164911cb5e94e8d8d5131d646613ff82c14f5a8ce869de2f6d80d9889df8c5a" 237 | name = "golang.org/x/text" 238 | packages = [ 239 | "collate", 240 | "collate/build", 241 | "internal/colltab", 242 | "internal/gen", 243 | "internal/tag", 244 | "internal/triegen", 245 | "internal/ucd", 246 | "language", 247 | "secure/bidirule", 248 | "transform", 249 | "unicode/bidi", 250 | "unicode/cldr", 251 | "unicode/norm", 252 | "unicode/rangetable", 253 | ] 254 | pruneopts = "T" 255 | revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" 256 | version = "v0.3.0" 257 | 258 | [[projects]] 259 | branch = "master" 260 | digest = "1:baedbfe72924071a9c7a4f8f12819d3b18a87743f645e5b638ba7a25865f00b1" 261 | name = "google.golang.org/genproto" 262 | packages = [ 263 | "googleapis/api/annotations", 264 | "googleapis/rpc/status", 265 | ] 266 | pruneopts = "T" 267 | revision = "694d95ba50e67b2e363f3483057db5d4910c18f9" 268 | 269 | [[projects]] 270 | digest = "1:cb75df728d7afe7b7a7f546c19b8fd1c5fc801bdfea88fbd1a6a4a0af5072e3a" 271 | name = "google.golang.org/grpc" 272 | packages = [ 273 | ".", 274 | "balancer", 275 | "balancer/base", 276 | "balancer/roundrobin", 277 | "channelz", 278 | "codes", 279 | "connectivity", 280 | "credentials", 281 | "encoding", 282 | "encoding/proto", 283 | "grpclb/grpc_lb_v1/messages", 284 | "grpclog", 285 | "internal", 286 | "keepalive", 287 | "metadata", 288 | "naming", 289 | "peer", 290 | "resolver", 291 | "resolver/dns", 292 | "resolver/passthrough", 293 | "stats", 294 | "status", 295 | "tap", 296 | "transport", 297 | ] 298 | pruneopts = "T" 299 | revision = "41344da2231b913fa3d983840a57a6b1b7b631a1" 300 | version = "v1.12.0" 301 | 302 | [[projects]] 303 | digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" 304 | name = "gopkg.in/yaml.v2" 305 | packages = ["."] 306 | pruneopts = "T" 307 | revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" 308 | version = "v2.2.1" 309 | 310 | [solve-meta] 311 | analyzer-name = "dep" 312 | analyzer-version = 1 313 | input-imports = [ 314 | "github.com/blk-io/chimera-api/chimera", 315 | "github.com/grpc-ecosystem/grpc-gateway/runtime", 316 | "github.com/jsimonetti/berkeleydb", 317 | "github.com/kevinburke/nacl", 318 | "github.com/kevinburke/nacl/box", 319 | "github.com/kevinburke/nacl/secretbox", 320 | "github.com/sirupsen/logrus", 321 | "github.com/spf13/pflag", 322 | "github.com/spf13/viper", 323 | "github.com/syndtr/goleveldb/leveldb", 324 | "golang.org/x/crypto/sha3", 325 | "golang.org/x/net/context", 326 | "google.golang.org/grpc", 327 | "google.golang.org/grpc/credentials", 328 | ] 329 | solver-name = "gps-cdcl" 330 | solver-version = 1 331 | -------------------------------------------------------------------------------- /config/config_testdata.conf: -------------------------------------------------------------------------------- 1 | ##### 2 | ## Constellation configuration file example 3 | ## ---------------------------------------- 4 | ## Every option listed here can also be specified on the command line, e.g. 5 | ## `constellation-node --url=http://www.foo.com --port 9001 ...` 6 | ## (lists are given using comma-separated strings) 7 | ## If both command line parameters and a configuration file are given, the 8 | ## command line options will take precedence. 9 | ## 10 | ## The only strictly necessary option is `port`, however it's recommended to 11 | ## set at least the following: 12 | ## 13 | ## --url The URL to advertise to other nodes (reachable by them) 14 | ## --port The local port to listen on 15 | ## --workdir The folder to put stuff in (default: .) 16 | ## --socket IPC socket to create for access to the Private API 17 | ## --othernodes "Boot nodes" to connect to to discover the network 18 | ## --publickeys Public keys hosted by this node 19 | ## --privatekeys Private keys hosted by this node (in corresponding order) 20 | ## 21 | ## Example usage: 22 | ## 23 | ## constellation-node --workdir=data --generatekeys=foo 24 | ## (To generate a keypair foo in the data directory) 25 | ## 26 | ## constellation-node --url=https://localhost:9000/ \ 27 | ## --port=9000 \ 28 | ## --workdir=data \ 29 | ## --socket=constellation.ipc \ 30 | ## --othernodes=https://localhost:9001/ \ 31 | ## --publickeys=foo.pub \ 32 | ## --privatekeys=foo.key 33 | ## 34 | ## constellation-node sample.conf 35 | ## 36 | ## constellation-node --port=9002 sample.conf 37 | ## (This overrides the port value given in sample.conf) 38 | ## 39 | ## Note on defaults: "Default:" below indicates the value that will be assumed 40 | ## if the option is not present either in the configuration file or as a command 41 | ## line parameter. 42 | ## 43 | ## Note about security: In the default configuration, Constellation will 44 | ## automatically generate TLS certificates and trust other nodes' certificates 45 | ## when they're first encountered (trust-on-first-use). See the documentation 46 | ## for tlsservertrust and tlsclienttrust below. To disable TLS entirely, e.g. 47 | ## when using Constellation in conjunction with a VPN like WireGuard, set tls to 48 | ## off. 49 | ##### 50 | 51 | ## Externally accessible URL for this node's public API (this is what's 52 | ## advertised to other nodes on the network, and must be reachable by them.) 53 | url = "http://127.0.0.1:9001/" 54 | 55 | ## Port to listen on for the public API. 56 | port = 9001 57 | 58 | ## Directory in which to put and look for other files referenced here. 59 | ## 60 | ## Default: The current directory 61 | workdir = "data" 62 | 63 | ## Socket file to use for the private API / IPC. If this is commented out, 64 | ## the private API will not be accessible. 65 | ## 66 | ## Default: Not set 67 | socket = "constellation.ipc" 68 | 69 | ## Initial (not necessarily complete) list of other nodes in the network. 70 | ## Constellation will automatically connect to other nodes not in this list 71 | ## that are advertised by the nodes below, thus these can be considered the 72 | ## "boot nodes." 73 | ## 74 | ## Default: [] 75 | othernodes = ["http://127.0.0.1:9000/"] 76 | 77 | ## The set of public keys this node will host. 78 | ## 79 | ## Default: [] 80 | publickeys = ["foo.pub"] 81 | 82 | ## The corresponding set of private keys. These must correspond to the public 83 | ## keys listed above. 84 | ## 85 | ## Default: [] 86 | privatekeys = ["foo.key"] 87 | 88 | ## Optional comma-separated list of paths to public keys to add as recipients 89 | ## for every transaction sent through this node, e.g. for backup purposes. 90 | ## These keys must be advertised by some Constellation node on the network, i.e. 91 | ## be in a node's publickeys/privatekeys lists. 92 | ## 93 | ## Default: [] 94 | alwayssendto = [] 95 | 96 | ## Optional file containing the passwords needed to unlock the given privatekeys 97 | ## (the file should contain one password per line -- add an empty line if any 98 | ## one key isn't locked.) 99 | ## 100 | ## Default: Not set 101 | # passwords = "passwords" 102 | 103 | ## Storage engine used to save payloads and related information. Options: 104 | ## - bdb:path (BerkeleyDB) 105 | ## - dir:path (Directory/file storage - can be used with e.g. FUSE-mounted 106 | ## file systems.) 107 | ## - leveldb:path (LevelDB - experimental) 108 | ## - memory (Contents are cleared when Constellation exits) 109 | ## - sqlite:path (SQLite - experimental) 110 | ## 111 | ## Default: "dir:storage" 112 | storage = "dir:storage" 113 | 114 | ## Verbosity level (each level includes all prior levels) 115 | ## - 0: Only fatal errors 116 | ## - 1: Warnings 117 | ## - 2: Informational messages 118 | ## - 3: Debug messages 119 | ## 120 | ## At the command line this can be specified using -v0, -v1, -v2, -v3, or 121 | ## -v (2) and -vv (3). 122 | ## 123 | ## Default: 1 124 | verbosity = 1 125 | 126 | ## Optional IP whitelist for the public API. If unspecified/empty, 127 | ## connections from all sources will be allowed (but the private API remains 128 | ## accessible only via the IPC socket above.) To allow connections from 129 | ## localhost when a whitelist is defined, e.g. when running multiple 130 | ## Constellation nodes on the same machine, add "127.0.0.1" and "::1" to 131 | ## this list. 132 | ## 133 | ## Default: Not set 134 | # ipwhitelist = ["10.0.0.1", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"] 135 | 136 | ## TLS status. Options: 137 | ## 138 | ## - strict: All connections to and from this node must use TLS with mutual 139 | ## authentication. See the documentation for tlsservertrust and 140 | ## tlsclienttrust below. 141 | ## - off: Mutually authenticated TLS is not used for in- and outbound 142 | ## connections, although unauthenticated connections to HTTPS hosts are 143 | ## still possible. This should only be used if another transport security 144 | ## mechanism like WireGuard is in place. 145 | ## 146 | ## Default: "strict" 147 | tls = "strict" 148 | 149 | ## Path to a file containing the server's TLS certificate in Apache format. 150 | ## This is used to identify this node to other nodes in the network when they 151 | ## connect to the public API. 152 | ## 153 | ## This file will be auto-generated if it doesn't exist. 154 | ## 155 | ## Default: "tls-server-cert.pem" 156 | tlsservercert = "tls-server-cert.pem" 157 | 158 | ## List of files that constitute the CA trust chain for the server certificate. 159 | ## This can be empty for auto-generated/non-PKI-based certificates. 160 | ## 161 | ## Default: [] 162 | tlsserverchain = [] 163 | 164 | ## The private key file for the server TLS certificate. 165 | ## 166 | ## This file will be auto-generated if it doesn't exist. 167 | ## 168 | ## Default: "tls-server-key.pem" 169 | tlsserverkey = "tls-server-key.pem" 170 | 171 | ## TLS trust mode for the server. This decides who's allowed to connect to it. 172 | ## Options: 173 | ## 174 | ## - whitelist: Only nodes that have previously connected to this node and 175 | ## been added to the tlsknownclients file below will be allowed to connect. 176 | ## This mode will not add any new clients to the tlsknownclients file. 177 | ## 178 | ## - tofu: (Trust-on-first-use) Only the first node that connects identifying 179 | ## as a certain host will be allowed to connect as the same host in the 180 | ## future. Note that nodes identifying as other hosts will still be able 181 | ## to connect -- switch to whitelist after populating the tlsknownclients 182 | ## list to restrict access. 183 | ## 184 | ## - ca: Only nodes with a valid certificate and chain of trust to one of 185 | ## the system root certificates will be allowed to connect. The folder 186 | ## containing trusted root certificates can be overriden with the 187 | ## SYSTEM_CERTIFICATE_PATH environment variable. 188 | ## 189 | ## - ca-or-tofu: A combination of ca and tofu: If a certificate is valid, 190 | ## it is always allowed and added to the tlsknownclients list. If it is 191 | ## self-signed, it will be allowed only if it's the first certificate this 192 | ## node has seen for that host. 193 | ## 194 | ## - insecure-no-validation: Any client can connect, however they will still 195 | ## be added to the tlsknownclients file. 196 | ## 197 | ## Default: "tofu" 198 | tlsservertrust = "tofu" 199 | 200 | ## TLS known clients file for the server. This contains the fingerprints of 201 | ## public keys of other nodes that are allowed to connect to this one. 202 | ## 203 | ## Default: "tls-known-clients" 204 | tlsknownclients = "tls-known-clients" 205 | 206 | ## Path to a file containing the client's TLS certificate in Apache format. 207 | ## This is used to identify this node to other nodes in the network when it is 208 | ## connecting to their public APIs. 209 | ## 210 | ## This file will be auto-generated if it doesn't exist. 211 | ## 212 | ## Default: "tls-client-cert.pem" 213 | tlsclientcert = "tls-client-cert.pem" 214 | 215 | ## List of files that constitute the CA trust chain for the client certificate. 216 | ## This can be empty for auto-generated/non-PKI-based certificates. 217 | ## 218 | ## Default: [] 219 | tlsclientchain = [] 220 | 221 | ## The private key file for the client TLS certificate. 222 | ## 223 | ## This file will be auto-generated if it doesn't exist. 224 | ## 225 | ## Default: "tls-client-key.pem" 226 | tlsclientkey = "tls-client-key.pem" 227 | 228 | ## TLS trust mode for the client. This decides which servers it will connect to. 229 | ## Options: 230 | ## 231 | ## - whitelist: This node will only connect to servers it has previously seen 232 | ## and added to the tlsknownclients file below. This mode will not add 233 | ## any new servers to the tlsknownservers file. 234 | ## 235 | ## - tofu: (Trust-on-first-use) This node will only connect to the same 236 | ## server for any given host. (Similar to how OpenSSH works.) 237 | ## 238 | ## - ca: The node will only connect to servers with a valid certificate and 239 | ## chain of trust to one of the system root certificates. The folder 240 | ## containing trusted root certificates can be overriden with the 241 | ## SYSTEM_CERTIFICATE_PATH environment variable. 242 | ## 243 | ## - ca-or-tofu: A combination of ca and tofu: If a certificate is valid, 244 | ## it is always allowed and added to the tlsknownservers list. If it is 245 | ## self-signed, it will be allowed only if it's the first certificate this 246 | ## node has seen for that host. 247 | ## 248 | ## - insecure-no-validation: This node will connect to any server, regardless 249 | ## of certificate, however it will still be added to the tlsknownservers 250 | ## file. 251 | ## 252 | ## Default: "ca-or-tofu" 253 | tlsclienttrust = "ca-or-tofu" 254 | 255 | ## TLS known servers file for the client. This contains the fingerprints of 256 | ## public keys of other nodes that this node has encountered. 257 | ## 258 | ## Default: "tls-known-servers" 259 | tlsknownservers = "tls-known-servers" -------------------------------------------------------------------------------- /api/internal.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/blk-io/chimera-api/chimera" 9 | "github.com/blk-io/crux/utils" 10 | "github.com/kevinburke/nacl" 11 | log "github.com/sirupsen/logrus" 12 | "golang.org/x/net/context" 13 | "google.golang.org/grpc" 14 | "io/ioutil" 15 | "math/rand" 16 | "net/http" 17 | "net/http/httputil" 18 | "net/url" 19 | "time" 20 | ) 21 | 22 | // EncryptedPayload is the struct used for storing all data associated with an encrypted 23 | // transaction. 24 | type EncryptedPayload struct { 25 | Sender nacl.Key 26 | CipherText []byte 27 | Nonce nacl.Nonce 28 | RecipientBoxes [][]byte 29 | RecipientNonce nacl.Nonce 30 | } 31 | 32 | // PartyInfo is a struct that stores details of all enclave nodes (or parties) on the network. 33 | type PartyInfo struct { 34 | url string // URL identifying this node 35 | recipients map[[nacl.KeySize]byte]string // public key -> URL 36 | parties map[string]bool // Node (or party) URLs 37 | client utils.HttpClient 38 | grpc bool 39 | } 40 | 41 | // GetRecipient retrieves the URL associated with the provided recipient. 42 | func (s *PartyInfo) GetRecipient(key nacl.Key) (string, bool) { 43 | value, ok := s.recipients[*key] 44 | return value, ok 45 | } 46 | 47 | func (s *PartyInfo) GetAllValues() (string, map[[nacl.KeySize]byte]string, map[string]bool) { 48 | return s.url, s.recipients, s.parties 49 | } 50 | 51 | // InitPartyInfo initializes a new PartyInfo store. 52 | func InitPartyInfo(rawUrl string, otherNodes []string, client utils.HttpClient, grpc bool) PartyInfo { 53 | parties := make(map[string]bool) 54 | for _, node := range otherNodes { 55 | parties[node] = true 56 | } 57 | 58 | return PartyInfo{ 59 | url: rawUrl, 60 | recipients: make(map[[nacl.KeySize]byte]string), 61 | parties: parties, 62 | client: client, 63 | grpc: grpc, 64 | } 65 | } 66 | 67 | // CreatePartyInfo creates a new PartyInfo struct. 68 | func CreatePartyInfo( 69 | url string, 70 | otherNodes []string, 71 | otherKeys []nacl.Key, 72 | client utils.HttpClient) PartyInfo { 73 | 74 | recipients := make(map[[nacl.KeySize]byte]string) 75 | parties := make(map[string]bool) 76 | for i, node := range otherNodes { 77 | parties[node] = true 78 | recipients[*otherKeys[i]] = node 79 | } 80 | 81 | return PartyInfo{ 82 | url: url, 83 | recipients: recipients, 84 | parties: parties, 85 | client: client, 86 | } 87 | } 88 | 89 | // RegisterPublicKeys associates the provided public keys with this node. 90 | func (s *PartyInfo) RegisterPublicKeys(pubKeys []nacl.Key) { 91 | for _, pubKey := range pubKeys { 92 | s.recipients[*pubKey] = s.url 93 | } 94 | } 95 | 96 | func (s *PartyInfo) GetPartyInfoGrpc() { 97 | recipients := make(map[string][]byte) 98 | for key, url := range s.recipients { 99 | recipients[url] = key[:] 100 | } 101 | urls := make(map[string]bool) 102 | for k, v := range s.parties { 103 | urls[k] = v 104 | } 105 | 106 | for rawUrl := range urls { 107 | if rawUrl == s.url { 108 | continue 109 | } 110 | var completeUrl url.URL 111 | url, err := completeUrl.Parse(rawUrl) 112 | conn, err := grpc.Dial(url.Host, grpc.WithInsecure()) 113 | if err != nil { 114 | log.Errorf("Connection to gRPC server failed with error %s", err) 115 | continue 116 | } 117 | defer conn.Close() 118 | cli := chimera.NewClientClient(conn) 119 | if cli == nil { 120 | log.Errorf("Client is not intialised") 121 | continue 122 | } 123 | party := chimera.PartyInfo{Url: rawUrl, Recipients: recipients, Parties: s.parties} 124 | 125 | partyInfoResp, err := cli.UpdatePartyInfo(context.Background(), &party) 126 | if err != nil { 127 | log.Errorf("Error in updating party info %s", err) 128 | continue 129 | } else { 130 | log.Printf("Connected to the other node %s", rawUrl) 131 | } 132 | err = s.updatePartyInfoGrpc(*partyInfoResp, s.url) 133 | if err != nil { 134 | log.Errorf("Error: %s", err) 135 | break 136 | } 137 | } 138 | } 139 | 140 | // GetPartyInfo requests PartyInfo data from all remote nodes this node is aware of. The data 141 | // provided in each response is applied to this node. 142 | func (s *PartyInfo) GetPartyInfo() { 143 | if s.grpc { 144 | s.GetPartyInfoGrpc() 145 | return 146 | } 147 | encodedPartyInfo := EncodePartyInfo(*s) 148 | 149 | // First copy our endpoints as we update this map in place 150 | urls := make(map[string]bool) 151 | for k, v := range s.parties { 152 | urls[k] = v 153 | } 154 | 155 | for rawUrl := range urls { 156 | if rawUrl == s.url { 157 | continue 158 | } 159 | 160 | endPoint, err := utils.BuildUrl(rawUrl, "/partyinfo") 161 | 162 | if err != nil { 163 | log.WithFields(log.Fields{"rawUrl": rawUrl, "endPoint": "/partyinfo"}).Errorf( 164 | "Invalid endpoint provided") 165 | } 166 | 167 | var req *http.Request 168 | encoded := s.getEncoded(encodedPartyInfo) 169 | req, err = http.NewRequest("POST", endPoint, bytes.NewBuffer(encoded)) 170 | 171 | if err != nil { 172 | log.WithField("url", rawUrl).Errorf( 173 | "Error creating /partyinfo request, %v", err) 174 | break 175 | } 176 | req.Header.Set("Content-Type", "application/octet-stream") 177 | 178 | logRequest(req) 179 | resp, err := s.client.Do(req) 180 | if err != nil { 181 | log.WithField("url", rawUrl).Errorf( 182 | "Error sending /partyinfo request, %v", err) 183 | continue 184 | } 185 | 186 | if resp.StatusCode != http.StatusOK { 187 | log.WithField("url", rawUrl).Errorf( 188 | "Error sending /partyinfo request, non-200 status code: %v", resp) 189 | continue 190 | } 191 | 192 | err = s.updatePartyInfo(resp, rawUrl) 193 | 194 | if err != nil { 195 | break 196 | } 197 | } 198 | } 199 | 200 | func (s *PartyInfo) updatePartyInfoGrpc(partyInfoReq chimera.PartyInfoResponse, rawUrl string) error { 201 | pi, err := DecodePartyInfo(partyInfoReq.Payload) 202 | if err != nil { 203 | log.WithField("url", rawUrl).Errorf( 204 | "Unable to decode partyInfo response from host, %v", err) 205 | return err 206 | } 207 | s.UpdatePartyInfoGrpc(pi.url, pi.recipients, pi.parties) 208 | return nil 209 | } 210 | 211 | func (s *PartyInfo) updatePartyInfo(resp *http.Response, rawUrl string) error { 212 | var encoded []byte 213 | encoded, err := ioutil.ReadAll(resp.Body) 214 | resp.Body.Close() 215 | if err != nil { 216 | log.WithField("url", rawUrl).Errorf( 217 | "Unable to read partyInfo response from host, %v", err) 218 | return err 219 | } 220 | s.UpdatePartyInfo(encoded) 221 | return nil 222 | } 223 | 224 | func (s *PartyInfo) getEncoded(encodedPartyInfo []byte) []byte { 225 | if s.grpc { 226 | recipients := make(map[string][]byte) 227 | for key, url := range s.recipients { 228 | recipients[url] = key[:] 229 | } 230 | e, err := json.Marshal(UpdatePartyInfo{s.url, recipients, s.parties}) 231 | if err != nil { 232 | log.Errorf("Marshalling failed %v", err) 233 | return nil 234 | } 235 | return e 236 | } 237 | return encodedPartyInfo[:] 238 | } 239 | 240 | func (s *PartyInfo) PollPartyInfo() { 241 | time.Sleep(time.Duration(rand.Intn(16)) * time.Second) 242 | s.GetPartyInfo() 243 | 244 | ticker := time.NewTicker(2 * time.Minute) 245 | quit := make(chan struct{}) 246 | go func() { 247 | for { 248 | select { 249 | case <-ticker.C: 250 | s.GetPartyInfo() 251 | case <-quit: 252 | ticker.Stop() 253 | return 254 | } 255 | } 256 | }() 257 | } 258 | 259 | // UpdatePartyInfo updates the PartyInfo datastore with the provided encoded data. 260 | // This can happen from the /partyinfo server endpoint being hit, or by a response from us hitting 261 | // another nodes /partyinfo endpoint. 262 | // TODO: Control access via a channel for updates. 263 | func (s *PartyInfo) UpdatePartyInfo(encoded []byte) { 264 | log.Debugf("Updating party info payload: %s", hex.EncodeToString(encoded)) 265 | pi, err := DecodePartyInfo(encoded) 266 | 267 | if err != nil { 268 | log.WithField("encoded", encoded).Errorf( 269 | "Unable to decode party info, error: %v", err) 270 | } 271 | 272 | for publicKey, url := range pi.recipients { 273 | // we should ignore messages about ourselves 274 | // in order to stop people masquerading as you, there 275 | // should be a digital signature associated with each 276 | // url -> node broadcast 277 | if url != s.url { 278 | s.recipients[publicKey] = url 279 | } 280 | } 281 | 282 | for url := range pi.parties { 283 | // we don't want to broadcast party info to ourselves 284 | s.parties[url] = true 285 | } 286 | } 287 | 288 | func (s *PartyInfo) UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) { 289 | for publicKey, url := range recipients { 290 | // we should ignore messages about ourselves 291 | // in order to stop people masquerading as you, there 292 | // should be a digital signature associated with each 293 | // url -> node broadcast 294 | if url != s.url { 295 | s.recipients[publicKey] = url 296 | } 297 | } 298 | 299 | for url := range parties { 300 | // we don't want to broadcast party info to ourselves 301 | s.parties[url] = true 302 | } 303 | } 304 | 305 | func PushGrpc(encoded []byte, path string, epl EncryptedPayload) error { 306 | var completeUrl url.URL 307 | url, err := completeUrl.Parse(path) 308 | conn, err := grpc.Dial(url.Host, grpc.WithInsecure()) 309 | if err != nil { 310 | log.Fatalf("Connection to gRPC server failed with error %s", err) 311 | } 312 | defer conn.Close() 313 | cli := chimera.NewClientClient(conn) 314 | if cli == nil { 315 | log.Fatalf("Client is not intialised") 316 | } 317 | 318 | var sender [32]byte 319 | var nonce [32]byte 320 | var recipientNonce [32]byte 321 | 322 | copy(sender[:], (*epl.Sender)[:]) 323 | copy(nonce[:], (*epl.Nonce)[:]) 324 | copy(recipientNonce[:], (*epl.RecipientNonce)[:]) 325 | encrypt := chimera.EncryptedPayload{ 326 | Sender: sender[:], 327 | CipherText: epl.CipherText, 328 | Nonce: nonce[:], 329 | ReciepientNonce: recipientNonce[:], 330 | ReciepientBoxes: epl.RecipientBoxes, 331 | } 332 | pushPayload := chimera.PushPayload{Ep: &encrypt, Encoded: encoded} 333 | _, err = cli.Push(context.Background(), &pushPayload) 334 | if err != nil { 335 | log.Errorf("Push failed with %s", err) 336 | return err 337 | } 338 | return nil 339 | } 340 | 341 | // Push is responsible for propagating the encoded payload to the given remote node. 342 | func Push(encoded []byte, url string, client utils.HttpClient) (string, error) { 343 | 344 | endPoint, err := utils.BuildUrl(url, "/push") 345 | if err != nil { 346 | return "", err 347 | } 348 | 349 | var req *http.Request 350 | req, err = http.NewRequest("POST", endPoint, bytes.NewReader(encoded)) 351 | if err != nil { 352 | return "", err 353 | } 354 | req.Header.Set("Content-Type", "application/octet-stream") 355 | 356 | logRequest(req) 357 | resp, err := client.Do(req) 358 | if err != nil { 359 | return "", err 360 | } 361 | 362 | if resp.StatusCode != http.StatusOK { 363 | return "", fmt.Errorf("non-200 status code received: %v", resp) 364 | } 365 | 366 | body, err := ioutil.ReadAll(resp.Body) 367 | resp.Body.Close() 368 | 369 | if err != nil { 370 | return "", err 371 | } 372 | 373 | return string(body), nil 374 | } 375 | 376 | func logRequest(r *http.Request) { 377 | if log.GetLevel() == log.DebugLevel { 378 | dump, err := httputil.DumpRequestOut(r, true) 379 | if err != nil { 380 | log.Fatal(err) 381 | } 382 | 383 | log.Debugf("%q", dump) 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /enclave/enclave_test.go: -------------------------------------------------------------------------------- 1 | package enclave 2 | 3 | import ( 4 | "bytes" 5 | "github.com/blk-io/crux/api" 6 | "github.com/blk-io/crux/storage" 7 | "github.com/blk-io/crux/utils" 8 | "github.com/kevinburke/nacl" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path" 13 | "sync" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | var message = []byte("Test message") 19 | 20 | type MockClient struct { 21 | serviceMu sync.Mutex 22 | requests [][]byte 23 | } 24 | 25 | func (c *MockClient) Do(req *http.Request) (*http.Response, error) { 26 | body, err := ioutil.ReadAll(req.Body) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | c.serviceMu.Lock() 32 | c.requests = append(c.requests, body) 33 | c.serviceMu.Unlock() 34 | 35 | respBody := ioutil.NopCloser(bytes.NewReader([]byte(""))) 36 | return &http.Response{Body: respBody}, nil 37 | } 38 | 39 | func (c *MockClient) reqCount() int { 40 | c.serviceMu.Lock() 41 | defer c.serviceMu.Unlock() 42 | return len(c.requests) 43 | } 44 | 45 | func initEnclave( 46 | t *testing.T, 47 | dbPath string, 48 | pi api.PartyInfo, 49 | client utils.HttpClient) *SecureEnclave { 50 | 51 | db, err := storage.InitLevelDb(dbPath) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | return Init( 57 | db, 58 | []string{"testdata/key.pub"}, 59 | []string{"testdata/key"}, 60 | pi, 61 | client, false) 62 | } 63 | 64 | func initDefaultEnclave(t *testing.T, 65 | dbPath string) *SecureEnclave { 66 | 67 | var client utils.HttpClient 68 | client = &MockClient{} 69 | pi := api.InitPartyInfo( 70 | "http://localhost:8000", 71 | []string{"http://localhost:8001"}, client, false) 72 | 73 | return initEnclave(t, dbPath, pi, client) 74 | } 75 | 76 | func TestStoreAndRetrieve(t *testing.T) { 77 | dbPath, err := ioutil.TempDir("", "TestStoreAndRetrieve") 78 | 79 | if err != nil { 80 | t.Fatal(err) 81 | } else { 82 | defer os.RemoveAll(dbPath) 83 | } 84 | 85 | mockClient := &MockClient{requests: [][]byte{}} 86 | var client utils.HttpClient 87 | client = mockClient 88 | 89 | pubKeys, err := loadPubKeys([]string{"testdata/rcpt1.pub"}) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | rcpt1 := pubKeys[0] 94 | 95 | pi := api.CreatePartyInfo( 96 | "http://localhost:8000", 97 | []string{"http://localhost:8001"}, 98 | []nacl.Key{rcpt1}, 99 | client) 100 | 101 | enc := initEnclave(t, dbPath, pi, client) 102 | 103 | var digest []byte 104 | digest, err = enc.Store(&message, []byte{}, [][]byte{(*rcpt1)[:]}) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | var returned []byte 110 | returned, err = enc.Retrieve(&digest, nil) 111 | 112 | if !bytes.Equal(message, returned) { 113 | t.Errorf( 114 | "Retrieved message is not the same as original:\n"+ 115 | "Original: %v\nRetrieved: %v", 116 | message, returned) 117 | } 118 | 119 | // We verify payload propagation too 120 | if mockClient.reqCount() != 1 { 121 | t.Errorf("Only one request should have been captured, actual: %d\n", 122 | len(mockClient.requests)) 123 | } 124 | 125 | propagatedPl := mockClient.requests[0] 126 | epl, recipients := api.DecodePayloadWithRecipients(propagatedPl) 127 | 128 | if len(recipients) != 0 { 129 | t.Errorf("Recipients should be empty in data sent to other nodes, actual size: %d\n", 130 | len(recipients)) 131 | } 132 | 133 | if len(epl.RecipientBoxes) != 1 { 134 | t.Errorf("There should only be one recipient box present, actual %d\n", 135 | len(epl.RecipientBoxes)) 136 | } 137 | 138 | // Then we simulate the propagation and retrieval by the client 139 | db, err := storage.InitLevelDb(dbPath + "2") 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | enc2 := Init( 145 | db, 146 | []string{"testdata/rcpt1.pub"}, 147 | []string{"testdata/rcpt1"}, 148 | pi, 149 | client, false) 150 | 151 | var digest2 []byte 152 | digest2, err = enc2.StorePayload(propagatedPl) 153 | 154 | if !bytes.Equal(digest, digest2) { 155 | t.Errorf("Local and propgated digests should be equal, local: %v, propagated: %v\n", 156 | digest, digest2) 157 | } 158 | 159 | var returned2 []byte 160 | to := (*rcpt1)[:] 161 | returned2, err = enc2.Retrieve(&digest2, &to) 162 | 163 | if !bytes.Equal(message, returned2) { 164 | t.Errorf( 165 | "Retrieved message is not the same as original:\n"+ 166 | "Original: %v\nRetrieved: %v", 167 | message, returned) 168 | } 169 | } 170 | 171 | func TestStoreAndRetrieveSelf(t *testing.T) { 172 | dbPath, err := ioutil.TempDir("", "TestStoreAndRetrieveSelf") 173 | 174 | if err != nil { 175 | t.Fatal(err) 176 | } else { 177 | defer os.RemoveAll(dbPath) 178 | } 179 | 180 | enc := initDefaultEnclave(t, dbPath) 181 | 182 | digest, err := enc.Store(&message, []byte{}, [][]byte{}) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | 187 | var returned []byte 188 | returned, err = enc.Retrieve(&digest, nil) 189 | 190 | if !bytes.Equal(message, returned) { 191 | t.Errorf( 192 | "Retrieved message is not the same as original:\n"+ 193 | "Original: %v\nRetrieved: %v", 194 | message, returned) 195 | } 196 | } 197 | 198 | func TestStoreNotAuthorised(t *testing.T) { 199 | dbPath, err := ioutil.TempDir("", "TestStoreNotAuthorised") 200 | 201 | if err != nil { 202 | t.Fatal(err) 203 | } else { 204 | defer os.RemoveAll(dbPath) 205 | } 206 | 207 | enc := initDefaultEnclave(t, dbPath) 208 | 209 | pubKeys, err := loadPubKeys([]string{"testdata/rcpt1.pub"}) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | rcpt1 := pubKeys[0] 214 | 215 | _, err = enc.Store(&message, (*rcpt1)[:], [][]byte{(*rcpt1)[:]}) 216 | if err == nil { 217 | t.Error("SecureEnclave is not authorised to store messages") 218 | } 219 | } 220 | 221 | func TestRetrieveInvalid(t *testing.T) { 222 | dbPath, err := ioutil.TempDir("", "TestRetrieveInvalid") 223 | 224 | if err != nil { 225 | t.Fatal(err) 226 | } else { 227 | defer os.RemoveAll(dbPath) 228 | } 229 | 230 | enc := initDefaultEnclave(t, dbPath) 231 | 232 | digest := []byte("invalid") 233 | _, err = enc.Retrieve(&digest, nil) 234 | if err == nil { 235 | t.Error("Invalid digest requested") 236 | } 237 | } 238 | 239 | func TestRetrieveNotAuthorised(t *testing.T) { 240 | // If you know the source enclave of the message, you can retrieve passing in any value you 241 | // want in the to field. This may not be appropriate. 242 | // TODO: Confirm if we want to do this 243 | dbPath, err := ioutil.TempDir("", "TestRetrieveNotAuthorised") 244 | 245 | if err != nil { 246 | t.Fatal(err) 247 | } else { 248 | defer os.RemoveAll(dbPath) 249 | } 250 | 251 | enc := initDefaultEnclave(t, dbPath) 252 | 253 | pubKeys, err := loadPubKeys([]string{"testdata/rcpt1.pub", "testdata/rcpt2.pub"}) 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | rcpt1 := pubKeys[0] 258 | rcpt2 := pubKeys[1] 259 | 260 | var digest []byte 261 | digest, err = enc.Store(&message, []byte{}, [][]byte{(*rcpt1)[:]}) 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | 266 | var returned []byte 267 | to := (*rcpt2)[:] 268 | // we may want this to fail, as it won't work if the message didn't originate with us 269 | returned, err = enc.Retrieve(&digest, &to) 270 | 271 | if !bytes.Equal(message, returned) { 272 | t.Errorf( 273 | "Retrieved message is not the same as original:\n"+ 274 | "Original: %v\nRetrieved: %v", 275 | message, returned) 276 | } 277 | } 278 | 279 | func TestDelete(t *testing.T) { 280 | dbPath, err := ioutil.TempDir("", "TestDelete") 281 | 282 | if err != nil { 283 | t.Fatal(err) 284 | } else { 285 | defer os.RemoveAll(dbPath) 286 | } 287 | 288 | enc := initDefaultEnclave(t, dbPath) 289 | 290 | digest, err := enc.Store(&message, []byte{}, [][]byte{}) 291 | if err != nil { 292 | t.Fatal(err) 293 | } 294 | 295 | var returned []byte 296 | returned, err = enc.Retrieve(&digest, nil) 297 | 298 | if !bytes.Equal(message, returned) { 299 | t.Errorf( 300 | "Retrieved message is not the same as original:\n"+ 301 | "Original: %v\nRetrieved: %v", 302 | message, returned) 303 | } 304 | 305 | err = enc.Delete(&digest) 306 | if err != nil { 307 | t.Errorf("Unable to delete payload for key: %v\n", &digest) 308 | } 309 | 310 | _, err = enc.Retrieve(&digest, nil) 311 | if err == nil { 312 | t.Errorf("No error returned requesting invalid payload") 313 | } 314 | } 315 | 316 | func TestRetrieveFor(t *testing.T) { 317 | dbPath, err := ioutil.TempDir("", "TestRetrieveFor") 318 | 319 | if err != nil { 320 | t.Fatal(err) 321 | } else { 322 | defer os.RemoveAll(dbPath) 323 | } 324 | 325 | enc := initDefaultEnclave(t, dbPath) 326 | 327 | pubKeys, err := loadPubKeys([]string{"testdata/rcpt1.pub"}) 328 | if err != nil { 329 | t.Fatal(err) 330 | } 331 | rcpt1 := (*pubKeys[0])[:] 332 | 333 | digest, err := enc.Store(&message, []byte{}, [][]byte{rcpt1}) 334 | if err != nil { 335 | t.Fatal(err) 336 | } 337 | 338 | var returned *[]byte 339 | returned, err = enc.RetrieveFor(&digest, &rcpt1) 340 | 341 | epl := api.DecodePayload(*returned) 342 | 343 | if len(epl.RecipientBoxes) != 1 { 344 | t.Errorf("Retrieved record does not contain a single box, total: %d", 345 | len(epl.RecipientBoxes)) 346 | } 347 | } 348 | 349 | func TestRetrieveForInvalid(t *testing.T) { 350 | dbPath, err := ioutil.TempDir("", "TestRetrieveFor") 351 | 352 | if err != nil { 353 | t.Fatal(err) 354 | } else { 355 | defer os.RemoveAll(dbPath) 356 | } 357 | 358 | enc := initDefaultEnclave(t, dbPath) 359 | 360 | var pubKeys []nacl.Key 361 | pubKeys, err = loadPubKeys([]string{"testdata/rcpt1.pub"}) 362 | if err != nil { 363 | t.Fatal(err) 364 | } 365 | rcpt1 := (*pubKeys[0])[:] 366 | 367 | digest := []byte("Invalid") 368 | _, err = enc.RetrieveFor(&digest, &rcpt1) 369 | 370 | if err == nil { 371 | t.Error("No error returned requesting invalid payload") 372 | } 373 | } 374 | 375 | func TestRetrieveAllFor(t *testing.T) { 376 | dbPath, err := ioutil.TempDir("", "TestRetrieveAllFor") 377 | 378 | if err != nil { 379 | t.Fatal(err) 380 | } else { 381 | defer os.RemoveAll(dbPath) 382 | } 383 | 384 | mockClient := &MockClient{requests: [][]byte{}} 385 | var client utils.HttpClient 386 | client = mockClient 387 | 388 | pubKeys, err := loadPubKeys([]string{"testdata/rcpt1.pub"}) 389 | if err != nil { 390 | t.Fatal(err) 391 | } 392 | rcpt1 := pubKeys[0] 393 | 394 | pi := api.CreatePartyInfo( 395 | "http://localhost:8000", 396 | []string{"http://localhost:8001"}, 397 | []nacl.Key{rcpt1}, 398 | client) 399 | 400 | enc := initEnclave(t, dbPath, pi, client) 401 | 402 | _, err = enc.Store(&message, []byte{}, [][]byte{(*rcpt1)[:]}) 403 | if err != nil { 404 | t.Fatal(err) 405 | } 406 | 407 | message2 := []byte("Another message") 408 | _, err = enc.Store(&message2, []byte{}, [][]byte{(*rcpt1)[:]}) 409 | if err != nil { 410 | t.Fatal(err) 411 | } 412 | 413 | rcpt1Key := (*rcpt1)[:] 414 | err = enc.RetrieveAllFor(&rcpt1Key) 415 | if err != nil { 416 | t.Fatal(err) 417 | } 418 | 419 | // we need to wait for the replay go-routines to complete 420 | time.Sleep(1 * time.Millisecond) 421 | if mockClient.reqCount() != 4 { 422 | t.Errorf("Four requests should have been captured, actual: %d\n", 423 | len(mockClient.requests)) 424 | } 425 | } 426 | 427 | func TestDoKeyGeneration(t *testing.T) { 428 | dbPath, err := ioutil.TempDir("", "TestDoKeyGeneration") 429 | 430 | if err != nil { 431 | t.Fatal(err) 432 | } else { 433 | defer os.RemoveAll(dbPath) 434 | } 435 | 436 | keyFiles := path.Join(dbPath, "testKey") 437 | err = DoKeyGeneration(keyFiles) 438 | 439 | if err != nil { 440 | t.Fatal(err) 441 | } 442 | 443 | _, err = loadPubKeys([]string{keyFiles + ".pub"}) 444 | if err != nil { 445 | t.Fatal(err) 446 | } 447 | 448 | _, err = loadPrivKeys([]string{keyFiles + ".key"}) 449 | if err != nil { 450 | t.Fatal(err) 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Enterprise Ethereum 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/read-tx.svg: -------------------------------------------------------------------------------- 1 | ClientQuorumCruxgetQuorumPayload(transactionHash, to)receive(transactionHash, to)receiveResponse(transaction)transactionClientQuorumCrux -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | // Package server contains the core server components. 2 | package server 3 | 4 | import ( 5 | "encoding/base64" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/blk-io/crux/api" 10 | "github.com/blk-io/crux/utils" 11 | "github.com/kevinburke/nacl" 12 | log "github.com/sirupsen/logrus" 13 | "io/ioutil" 14 | "net/http" 15 | "net/http/httputil" 16 | "net/textproto" 17 | "os" 18 | "strconv" 19 | ) 20 | 21 | // Enclave is the interface used by the transaction enclaves. 22 | type Enclave interface { 23 | Store(message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) 24 | StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) 25 | StorePayload(encoded []byte) ([]byte, error) 26 | Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) 27 | RetrieveDefault(digestHash *[]byte) ([]byte, error) 28 | RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (*[]byte, error) 29 | RetrieveAllFor(reqRecipient *[]byte) error 30 | Delete(digestHash *[]byte) error 31 | UpdatePartyInfo(encoded []byte) 32 | UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) 33 | GetEncodedPartyInfo() []byte 34 | GetEncodedPartyInfoGrpc() []byte 35 | GetPartyInfo() (url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) 36 | } 37 | 38 | // TransactionManager is responsible for handling all transaction requests. 39 | type TransactionManager struct { 40 | Enclave Enclave 41 | } 42 | 43 | const upCheckResponse = "I'm up!" 44 | const apiVersion = "0.3.2" 45 | 46 | const version = "/version" 47 | const upCheck = "/upcheck" 48 | const push = "/push" 49 | const resend = "/resend" 50 | const partyInfo = "/partyinfo" 51 | const send = "/send" 52 | const sendRaw = "/sendraw" 53 | const receive = "/receive" 54 | const receiveRaw = "/receiveraw" 55 | const delete = "/delete" 56 | 57 | const hFrom = "c11n-from" 58 | const hTo = "c11n-to" 59 | const hKey = "c11n-key" 60 | 61 | func requestLogger(handler http.Handler) http.Handler { 62 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 63 | if log.GetLevel() == log.DebugLevel { 64 | dump, err := httputil.DumpRequest(r, true) 65 | if err != nil { 66 | http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) 67 | return 68 | } 69 | 70 | log.Debugf("%q", dump) 71 | } 72 | 73 | handler.ServeHTTP(w, r) 74 | }) 75 | } 76 | 77 | // Init initializes a new TransactionManager instance. 78 | func Init(enc Enclave, networkInterface string, port int, ipcPath string, grpc bool, grpcJsonPort int, tls bool, certFile, keyFile string) (TransactionManager, error) { 79 | tm := TransactionManager{Enclave: enc} 80 | var err error 81 | if grpc == true { 82 | err = tm.startRpcServer(networkInterface, port, grpcJsonPort, ipcPath, tls, certFile, keyFile) 83 | 84 | } else { 85 | err = tm.startHttpserver(networkInterface, port, ipcPath, tls, certFile, keyFile) 86 | } 87 | 88 | return tm, err 89 | } 90 | 91 | func (tm *TransactionManager) startHttpserver(networkInterface string, port int, ipcPath string, tls bool, certFile, keyFile string) error { 92 | httpServer := http.NewServeMux() 93 | httpServer.HandleFunc(upCheck, tm.upcheck) 94 | httpServer.HandleFunc(version, tm.version) 95 | httpServer.HandleFunc(push, tm.push) 96 | httpServer.HandleFunc(resend, tm.resend) 97 | httpServer.HandleFunc(partyInfo, tm.partyInfo) 98 | 99 | serverUrl := networkInterface + ":" + strconv.Itoa(port) 100 | if tls { 101 | err := CheckCertFiles(certFile, keyFile) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | go func() { 106 | log.Fatal(http.ListenAndServeTLS(serverUrl, certFile, keyFile, requestLogger(httpServer))) 107 | }() 108 | log.Infof("HTTPS server is running at: %s", serverUrl) 109 | } else { 110 | go func() { 111 | log.Fatal(http.ListenAndServe(serverUrl, requestLogger(httpServer))) 112 | }() 113 | log.Infof("HTTP server is running at: %s", serverUrl) 114 | } 115 | 116 | // Restricted to IPC 117 | ipcServer := http.NewServeMux() 118 | ipcServer.HandleFunc(upCheck, tm.upcheck) 119 | ipcServer.HandleFunc(version, tm.version) 120 | ipcServer.HandleFunc(send, tm.send) 121 | ipcServer.HandleFunc(sendRaw, tm.sendRaw) 122 | ipcServer.HandleFunc(receive, tm.receive) 123 | ipcServer.HandleFunc(receiveRaw, tm.receiveRaw) 124 | ipcServer.HandleFunc(delete, tm.delete) 125 | 126 | ipc, err := utils.CreateIpcSocket(ipcPath) 127 | if err != nil { 128 | log.Fatalf("Failed to start IPC Server at %s", ipcPath) 129 | } 130 | go func() { 131 | log.Fatal(http.Serve(ipc, requestLogger(ipcServer))) 132 | }() 133 | log.Infof("IPC server is running at: %s", ipcPath) 134 | 135 | return err 136 | } 137 | 138 | func CheckCertFiles(certFile, keyFile string) error { 139 | if _, err := os.Stat(certFile); os.IsNotExist(err) { 140 | return err 141 | } else if _, err := os.Stat(keyFile); os.IsNotExist(err) { 142 | return err 143 | } 144 | return nil 145 | } 146 | 147 | func (s *TransactionManager) upcheck(w http.ResponseWriter, req *http.Request) { 148 | fmt.Fprint(w, upCheckResponse) 149 | } 150 | 151 | func (s *TransactionManager) version(w http.ResponseWriter, req *http.Request) { 152 | fmt.Fprint(w, apiVersion) 153 | } 154 | 155 | func (s *TransactionManager) send(w http.ResponseWriter, req *http.Request) { 156 | var sendReq api.SendRequest 157 | err := json.NewDecoder(req.Body).Decode(&sendReq) 158 | req.Body.Close() 159 | if err != nil { 160 | invalidBody(w, req, err) 161 | return 162 | } 163 | 164 | payload, err := base64.StdEncoding.DecodeString(sendReq.Payload) 165 | if err != nil { 166 | decodeError(w, req, "payload", sendReq.Payload, err) 167 | return 168 | } 169 | 170 | var key []byte 171 | key, err = s.processSend(w, req, sendReq.From, sendReq.To, &payload) 172 | 173 | if err != nil { 174 | log.Error(err) 175 | badRequest(w, 176 | fmt.Sprintf("Unable to store key: %s, with payload: %s, error: %s\n", 177 | key, payload, err)) 178 | } else { 179 | encodedKey := base64.StdEncoding.EncodeToString(key) 180 | sendResp := api.SendResponse{Key: encodedKey} 181 | json.NewEncoder(w).Encode(sendResp) 182 | w.Header().Set("Content-Type", "application/json") 183 | } 184 | } 185 | 186 | func (s *TransactionManager) sendRaw(w http.ResponseWriter, req *http.Request) { 187 | 188 | from := req.Header.Get(hFrom) 189 | 190 | to, ok := req.Header[hTo] 191 | if !ok { 192 | to, ok = req.Header[textproto.CanonicalMIMEHeaderKey(hTo)] 193 | if !ok { 194 | to = []string{} 195 | } 196 | } 197 | 198 | payload, err := ioutil.ReadAll(req.Body) 199 | req.Body.Close() 200 | if err != nil { 201 | invalidBody(w, req, err) 202 | return 203 | } 204 | 205 | var key []byte 206 | key, err = s.processSend(w, req, from, to, &payload) 207 | if err != nil { 208 | internalServerError(w, "Unable to process request") 209 | return 210 | } 211 | 212 | // Uncomment the below for Quorum v2.0.1 or below 213 | // see https://github.com/jpmorganchase/quorum/commit/ee498061b5a74bf1f3290139a53840345fa038cb#diff-63fbbd6b2c0487b8cd4445e881822cdd 214 | //w.Write(key) 215 | // Then delete the below lines 216 | encodedKey := base64.StdEncoding.EncodeToString(key) 217 | fmt.Fprint(w, encodedKey) 218 | } 219 | 220 | func (s *TransactionManager) processSend( 221 | w http.ResponseWriter, req *http.Request, 222 | b64from string, 223 | b64recipients []string, 224 | payload *[]byte) ([]byte, error) { 225 | 226 | log.WithFields(log.Fields{ 227 | "b64From": b64from, 228 | "b64Recipients": b64recipients, 229 | "payload": hex.EncodeToString(*payload)}).Debugf( 230 | "Processing send request") 231 | 232 | sender, err := base64.StdEncoding.DecodeString(b64from) 233 | if err != nil { 234 | decodeError(w, req, "sender", b64from, err) 235 | return nil, err 236 | } 237 | 238 | recipients := make([][]byte, len(b64recipients)) 239 | for i, value := range b64recipients { 240 | recipient, err := base64.StdEncoding.DecodeString(value) 241 | if err != nil { 242 | decodeError(w, req, "recipient", value, err) 243 | return nil, err 244 | } else { 245 | recipients[i] = recipient 246 | } 247 | } 248 | 249 | return s.Enclave.Store(payload, sender, recipients) 250 | } 251 | 252 | func (s *TransactionManager) receive(w http.ResponseWriter, req *http.Request) { 253 | var receiveReq api.ReceiveRequest 254 | err := json.NewDecoder(req.Body).Decode(&receiveReq) 255 | req.Body.Close() 256 | if err != nil { 257 | invalidBody(w, req, err) 258 | return 259 | } 260 | 261 | var payload []byte 262 | payload, err = s.processReceive(w, req, receiveReq.Key, receiveReq.To) 263 | 264 | if err != nil { 265 | badRequest(w, 266 | fmt.Sprintf("Unable to retrieve payload for key: %s, error: %s\n", 267 | receiveReq.Key, err)) 268 | } else { 269 | encodedPayload := base64.StdEncoding.EncodeToString(payload) 270 | sendResp := api.ReceiveResponse{Payload: encodedPayload} 271 | json.NewEncoder(w).Encode(sendResp) 272 | w.Header().Set("Content-Type", "application/json") 273 | } 274 | } 275 | 276 | func (s *TransactionManager) receiveRaw(w http.ResponseWriter, req *http.Request) { 277 | 278 | key := req.Header.Get(hKey) 279 | if key == "" { 280 | badRequest(w, "key not specified") 281 | return 282 | } 283 | 284 | to := req.Header.Get(hTo) 285 | 286 | payload, err := s.processReceive(w, req, key, to) 287 | 288 | if err != nil { 289 | badRequest(w, fmt.Sprintln(err)) 290 | return 291 | } 292 | 293 | w.Write(payload) 294 | } 295 | 296 | func (s *TransactionManager) processReceive( 297 | w http.ResponseWriter, req *http.Request, b64Key, b64To string) ([]byte, error) { 298 | 299 | key, err := base64.StdEncoding.DecodeString(b64Key) 300 | if err != nil { 301 | return nil, fmt.Errorf("unable to decode key: %s", b64Key) 302 | } 303 | 304 | if b64To != "" { 305 | to, err := base64.StdEncoding.DecodeString(b64To) 306 | if err != nil { 307 | return nil, fmt.Errorf("unable to decode to: %s", b64Key) 308 | } 309 | 310 | return s.Enclave.Retrieve(&key, &to) 311 | } else { 312 | return s.Enclave.RetrieveDefault(&key) 313 | } 314 | } 315 | 316 | func (s *TransactionManager) delete(w http.ResponseWriter, req *http.Request) { 317 | var deleteReq api.DeleteRequest 318 | err := json.NewDecoder(req.Body).Decode(&deleteReq) 319 | req.Body.Close() 320 | if err != nil { 321 | invalidBody(w, req, err) 322 | return 323 | } 324 | key, err := base64.StdEncoding.DecodeString(deleteReq.Key) 325 | if err != nil { 326 | decodeError(w, req, "key", deleteReq.Key, err) 327 | } else { 328 | err = s.Enclave.Delete(&key) 329 | if err != nil { 330 | badRequest(w, fmt.Sprintf("Unable to delete key: %s, error: %s\n", key, err)) 331 | } 332 | } 333 | } 334 | 335 | func (s *TransactionManager) push(w http.ResponseWriter, req *http.Request) { 336 | payload, err := ioutil.ReadAll(req.Body) 337 | req.Body.Close() 338 | if err != nil { 339 | internalServerError(w, fmt.Sprintf("Unable to read request body, error: %s\n", err)) 340 | return 341 | } 342 | 343 | digestHash, err := s.Enclave.StorePayload(payload) 344 | if err != nil { 345 | badRequest(w, fmt.Sprintf("Unable to store payload, error: %s\n", err)) 346 | return 347 | } 348 | 349 | w.Write(digestHash) 350 | } 351 | 352 | func (s *TransactionManager) resend(w http.ResponseWriter, req *http.Request) { 353 | var resendReq api.ResendRequest 354 | err := json.NewDecoder(req.Body).Decode(&resendReq) 355 | req.Body.Close() 356 | if err != nil { 357 | invalidBody(w, req, err) 358 | return 359 | } 360 | 361 | var publicKey []byte 362 | publicKey, err = base64.StdEncoding.DecodeString(resendReq.PublicKey) 363 | if err != nil { 364 | decodeError(w, req, "publicKey", resendReq.PublicKey, err) 365 | return 366 | } 367 | 368 | if resendReq.Type == "all" { 369 | err = s.Enclave.RetrieveAllFor(&publicKey) 370 | if err != nil { 371 | invalidBody(w, req, err) 372 | } 373 | } else if resendReq.Type == "individual" { 374 | var key []byte 375 | key, err = base64.StdEncoding.DecodeString(resendReq.Key) 376 | if err != nil { 377 | decodeError(w, req, "key", resendReq.Key, err) 378 | return 379 | } 380 | 381 | var encodedPl *[]byte 382 | encodedPl, err = s.Enclave.RetrieveFor(&key, &publicKey) 383 | if err != nil { 384 | invalidBody(w, req, err) 385 | return 386 | } 387 | w.Write(*encodedPl) 388 | } 389 | } 390 | 391 | func (s *TransactionManager) partyInfo(w http.ResponseWriter, req *http.Request) { 392 | payload, err := ioutil.ReadAll(req.Body) 393 | req.Body.Close() 394 | if err != nil { 395 | internalServerError(w, fmt.Sprintf("Unable to read request body, error: %s\n", err)) 396 | return 397 | } else { 398 | s.Enclave.UpdatePartyInfo(payload) 399 | w.Write(s.Enclave.GetEncodedPartyInfo()) 400 | } 401 | } 402 | 403 | func invalidBody(w http.ResponseWriter, req *http.Request, err error) { 404 | badRequest(w, fmt.Sprintf("Invalid request: %s, error: %s\n", req.URL, err)) 405 | } 406 | 407 | func decodeError(w http.ResponseWriter, req *http.Request, name string, value string, err error) { 408 | badRequest(w, 409 | fmt.Sprintf("Invalid request: %s, unable to decode %s: %s, error: %s\n", 410 | req.URL, name, value, err)) 411 | } 412 | 413 | func badRequest(w http.ResponseWriter, message string) { 414 | log.Error(message) 415 | w.WriteHeader(http.StatusBadRequest) 416 | fmt.Fprintf(w, message) 417 | } 418 | 419 | func internalServerError(w http.ResponseWriter, message string) { 420 | log.Error(message) 421 | w.WriteHeader(http.StatusInternalServerError) 422 | fmt.Fprintf(w, message) 423 | } 424 | -------------------------------------------------------------------------------- /docs/new-tx.svg: -------------------------------------------------------------------------------- 1 | ClientQuorumCruxRemote Cruxsend(transaction, privateFor)send(transaction, privateFor)push(encryptedPayload)sendResponse(transactionHash)ClientQuorumCruxRemote Crux -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/blk-io/chimera-api/chimera" 9 | "github.com/blk-io/crux/api" 10 | "github.com/blk-io/crux/enclave" 11 | "github.com/blk-io/crux/storage" 12 | "github.com/kevinburke/nacl" 13 | log "github.com/sirupsen/logrus" 14 | "golang.org/x/net/context" 15 | "google.golang.org/grpc" 16 | "io/ioutil" 17 | "net/http" 18 | "net/http/httptest" 19 | "path" 20 | "reflect" 21 | "testing" 22 | ) 23 | 24 | const sender = "BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo=" 25 | const receiver = "QfeDAys9MPDs2XHExtc84jKGHxZg/aj52DTh0vtA3Xc=" 26 | 27 | var payload = []byte("payload") 28 | var encodedPayload = base64.StdEncoding.EncodeToString(payload) 29 | 30 | type MockEnclave struct{} 31 | 32 | func (s *MockEnclave) Store(message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) { 33 | return *message, nil 34 | } 35 | 36 | func (s *MockEnclave) StorePayload(encoded []byte) ([]byte, error) { 37 | return encoded, nil 38 | } 39 | func (s *MockEnclave) StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { 40 | return encoded, nil 41 | } 42 | 43 | func (s *MockEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) { 44 | return *digestHash, nil 45 | } 46 | 47 | func (s *MockEnclave) RetrieveDefault(digestHash *[]byte) ([]byte, error) { 48 | return *digestHash, nil 49 | } 50 | 51 | func (s *MockEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (*[]byte, error) { 52 | return digestHash, nil 53 | } 54 | 55 | func (s *MockEnclave) RetrieveAllFor(reqRecipient *[]byte) error { 56 | return nil 57 | } 58 | 59 | func (s *MockEnclave) Delete(digestHash *[]byte) error { 60 | return nil 61 | } 62 | 63 | func (s *MockEnclave) UpdatePartyInfo(encoded []byte) {} 64 | 65 | func (s *MockEnclave) UpdatePartyInfoGrpc(string, map[[nacl.KeySize]byte]string, map[string]bool) {} 66 | 67 | func (s *MockEnclave) GetEncodedPartyInfo() []byte { 68 | return payload 69 | } 70 | 71 | func (s *MockEnclave) GetEncodedPartyInfoGrpc() []byte { 72 | return payload 73 | } 74 | 75 | func (s *MockEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool) { 76 | return "", nil, nil 77 | } 78 | 79 | func TestUpcheck(t *testing.T) { 80 | tm := TransactionManager{} 81 | runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) 82 | } 83 | 84 | func TestVersion(t *testing.T) { 85 | tm := TransactionManager{} 86 | runSimpleGetRequest(t, version, apiVersion, tm.version) 87 | } 88 | 89 | func runSimpleGetRequest(t *testing.T, url, response string, handlerFunc http.HandlerFunc) { 90 | req, err := http.NewRequest("GET", url, nil) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | rr := httptest.NewRecorder() 96 | 97 | handler := http.HandlerFunc(handlerFunc) 98 | handler.ServeHTTP(rr, req) 99 | 100 | if status := rr.Code; status != http.StatusOK { 101 | t.Errorf("handler returned wrong status code: got %v want %v\n", 102 | status, http.StatusOK) 103 | } 104 | 105 | if rr.Body.String() != response { 106 | t.Errorf("handler returned unexpected body: got %v want %v\n", 107 | rr.Body.String(), upCheckResponse) 108 | } 109 | } 110 | 111 | func TestSend(t *testing.T) { 112 | sendReqs := []api.SendRequest{ 113 | { 114 | Payload: encodedPayload, 115 | From: sender, 116 | To: []string{receiver}, 117 | }, 118 | { 119 | Payload: encodedPayload, 120 | To: []string{}, 121 | }, 122 | { 123 | Payload: encodedPayload, 124 | }, 125 | } 126 | 127 | response := api.SendResponse{} 128 | expected := api.SendResponse{Key: encodedPayload} 129 | 130 | tm := TransactionManager{Enclave: &MockEnclave{}} 131 | 132 | for _, sendReq := range sendReqs { 133 | runJsonHandlerTest(t, &sendReq, &response, &expected, send, tm.send) 134 | } 135 | } 136 | 137 | func TestGRPCSend(t *testing.T) { 138 | sendReqs := []chimera.SendRequest{ 139 | { 140 | Payload: payload, 141 | From: sender, 142 | To: []string{receiver}, 143 | }, 144 | { 145 | Payload: payload, 146 | To: []string{}, 147 | }, 148 | { 149 | Payload: payload, 150 | }, 151 | } 152 | expected := chimera.SendResponse{Key: payload} 153 | 154 | freePort, err := GetFreePort("localhost") 155 | if err != nil { 156 | log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) 157 | } 158 | ipcPath := InitgRPCServer(t, true, freePort) 159 | 160 | var conn *grpc.ClientConn 161 | conn, err = grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) 162 | if err != nil { 163 | t.Fatalf("Connection to gRPC server failed with error %s", err) 164 | } 165 | defer conn.Close() 166 | c := chimera.NewClientClient(conn) 167 | 168 | for _, sendReq := range sendReqs { 169 | resp, err := c.Send(context.Background(), &sendReq) 170 | if err != nil { 171 | t.Fatalf("gRPC send failed with %s", err) 172 | } 173 | response := chimera.SendResponse{Key: resp.Key} 174 | if !reflect.DeepEqual(response, expected) { 175 | t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) 176 | } 177 | } 178 | } 179 | 180 | func TestSendRaw(t *testing.T) { 181 | tm := TransactionManager{Enclave: &MockEnclave{}} 182 | 183 | headers := make(http.Header) 184 | headers[hFrom] = []string{sender} 185 | headers[hTo] = []string{receiver} 186 | 187 | runRawHandlerTest(t, headers, payload, []byte(encodedPayload), sendRaw, tm.sendRaw) 188 | // Uncomment the below for Quorum v2.0.1 or below 189 | //runRawHandlerTest(t, headers, payload, payload, sendRaw, tm.sendRaw) 190 | } 191 | 192 | func TestReceive(t *testing.T) { 193 | 194 | receiveReqs := []api.ReceiveRequest{ 195 | { 196 | Key: encodedPayload, 197 | To: receiver, 198 | }, 199 | } 200 | 201 | response := api.ReceiveResponse{} 202 | expected := api.ReceiveResponse{Payload: encodedPayload} 203 | 204 | tm := TransactionManager{Enclave: &MockEnclave{}} 205 | 206 | for _, receiveReq := range receiveReqs { 207 | runJsonHandlerTest(t, &receiveReq, &response, &expected, receive, tm.receive) 208 | } 209 | } 210 | 211 | func TestGRPCReceive(t *testing.T) { 212 | receiveReqs := []chimera.ReceiveRequest{ 213 | { 214 | Key: payload, 215 | To: receiver, 216 | }, 217 | } 218 | expected := chimera.ReceiveResponse{Payload: payload} 219 | freePort, err := GetFreePort("localhost") 220 | if err != nil { 221 | log.Fatalf("failed to find a free port to start gRPC REST server: %s", err) 222 | } 223 | ipcPath := InitgRPCServer(t, true, freePort) 224 | var conn *grpc.ClientConn 225 | conn, err = grpc.Dial(fmt.Sprintf("passthrough:///unix://%s", ipcPath), grpc.WithInsecure()) 226 | if err != nil { 227 | t.Fatalf("Connection to gRPC server failed with error %s", err) 228 | } 229 | defer conn.Close() 230 | c := chimera.NewClientClient(conn) 231 | 232 | for _, receiveReq := range receiveReqs { 233 | resp, err := c.Receive(context.Background(), &receiveReq) 234 | if err != nil { 235 | t.Fatalf("gRPC receive failed with %s", err) 236 | } 237 | response := chimera.ReceiveResponse{Payload: resp.Payload} 238 | if !reflect.DeepEqual(response, expected) { 239 | t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) 240 | } 241 | } 242 | } 243 | 244 | func TestReceivedRaw(t *testing.T) { 245 | tm := TransactionManager{Enclave: &MockEnclave{}} 246 | 247 | headers := make(http.Header) 248 | headers[hKey] = []string{encodedPayload} 249 | headers[hTo] = []string{receiver} 250 | 251 | runRawHandlerTest(t, headers, payload, payload, receiveRaw, tm.receiveRaw) 252 | } 253 | 254 | func TestNilKeyReceivedRaw(t *testing.T) { 255 | tm := TransactionManager{Enclave: &MockEnclave{}} 256 | 257 | headers := make(http.Header) 258 | headers[hKey] = []string{""} 259 | headers[hTo] = []string{receiver} 260 | 261 | runFailingRawHandlerTest(t, headers, payload, payload, receiveRaw, tm.receiveRaw) 262 | } 263 | 264 | func TestPush(t *testing.T) { 265 | 266 | epl := api.EncryptedPayload{ 267 | Sender: nacl.NewKey(), 268 | CipherText: []byte(payload), 269 | Nonce: nacl.NewNonce(), 270 | RecipientBoxes: [][]byte{[]byte(payload)}, 271 | RecipientNonce: nacl.NewNonce(), 272 | } 273 | var recipients [][]byte 274 | 275 | encoded := api.EncodePayloadWithRecipients(epl, recipients) 276 | req, err := http.NewRequest("POST", push, bytes.NewBuffer(encoded)) 277 | if err != nil { 278 | t.Fatal(err) 279 | } 280 | rr := httptest.NewRecorder() 281 | tm := TransactionManager{Enclave: &MockEnclave{}} 282 | 283 | handler := http.HandlerFunc(tm.push) 284 | handler.ServeHTTP(rr, req) 285 | 286 | if status := rr.Code; status != http.StatusOK { 287 | t.Errorf("handler returned wrong status code: got %v want %v\n", 288 | status, http.StatusOK) 289 | } 290 | 291 | if !bytes.Equal(rr.Body.Bytes(), encoded) { 292 | t.Errorf("handler returned unexpected body: got %v wanted %v\n", 293 | rr.Body.String(), encoded) 294 | } 295 | } 296 | 297 | func TestDelete(t *testing.T) { 298 | sendReq := api.DeleteRequest{ 299 | Key: encodedPayload, 300 | } 301 | 302 | var response, expected interface{} 303 | 304 | tm := TransactionManager{Enclave: &MockEnclave{}} 305 | 306 | runJsonHandlerTest(t, &sendReq, &response, &expected, delete, tm.delete) 307 | } 308 | 309 | func runJsonHandlerTest( 310 | t *testing.T, 311 | request, response, expected interface{}, 312 | url string, 313 | handlerFunc http.HandlerFunc) { 314 | 315 | encoded, err := json.Marshal(request) 316 | if err != nil { 317 | t.Fatal(err) 318 | } 319 | 320 | var req *http.Request 321 | req, err = http.NewRequest("POST", url, bytes.NewBuffer(encoded)) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | 326 | rr := httptest.NewRecorder() 327 | 328 | handler := http.HandlerFunc(handlerFunc) 329 | handler.ServeHTTP(rr, req) 330 | 331 | if status := rr.Code; status != http.StatusOK { 332 | t.Errorf("handler returned wrong status code: got %v want %v", 333 | status, http.StatusOK) 334 | } 335 | 336 | body := rr.Body.Bytes() 337 | if len(body) > 0 { 338 | err = json.Unmarshal(body, &response) 339 | if err != nil { 340 | t.Error(err) 341 | } 342 | } 343 | 344 | if !reflect.DeepEqual(response, expected) { 345 | t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) 346 | } 347 | } 348 | func runFailingRawHandlerTest( 349 | t *testing.T, 350 | headers http.Header, 351 | payload, expected []byte, 352 | url string, 353 | handlerFunc http.HandlerFunc) { 354 | 355 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) 356 | if err != nil { 357 | t.Fatal(err) 358 | } 359 | 360 | for k, v := range headers { 361 | req.Header.Set(k, v[0]) 362 | } 363 | 364 | rr := httptest.NewRecorder() 365 | 366 | handler := http.HandlerFunc(handlerFunc) 367 | handler.ServeHTTP(rr, req) 368 | 369 | if status := rr.Code; status == http.StatusOK { 370 | t.Errorf("handler returned wrong status code: got %v want %v", 371 | status, http.StatusOK) 372 | } 373 | } 374 | 375 | func runRawHandlerTest( 376 | t *testing.T, 377 | headers http.Header, 378 | payload, expected []byte, 379 | url string, 380 | handlerFunc http.HandlerFunc) { 381 | 382 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) 383 | if err != nil { 384 | t.Fatal(err) 385 | } 386 | 387 | for k, v := range headers { 388 | req.Header.Set(k, v[0]) 389 | } 390 | 391 | rr := httptest.NewRecorder() 392 | 393 | handler := http.HandlerFunc(handlerFunc) 394 | handler.ServeHTTP(rr, req) 395 | 396 | if status := rr.Code; status != http.StatusOK { 397 | t.Errorf("handler returned wrong status code: got %v want %v", 398 | status, http.StatusOK) 399 | } 400 | 401 | response := rr.Body.Bytes() 402 | 403 | if !reflect.DeepEqual(response, expected) { 404 | t.Errorf("handler returned unexpected response: %v, expected: %v\n", response, expected) 405 | } 406 | } 407 | 408 | func TestResendIndividual(t *testing.T) { 409 | resendReq := api.ResendRequest{ 410 | Type: "individual", 411 | PublicKey: sender, 412 | Key: encodedPayload, 413 | } 414 | 415 | body := runResendTest(t, resendReq) 416 | 417 | if !bytes.Equal(body, payload) { 418 | t.Errorf("handler returned unexpected body: got %v wanted %v\n", 419 | body, payload) 420 | } 421 | } 422 | 423 | func TestResendAll(t *testing.T) { 424 | resendReq := api.ResendRequest{ 425 | Type: "all", 426 | PublicKey: sender, 427 | } 428 | 429 | body := runResendTest(t, resendReq) 430 | 431 | if len(body) != 0 { 432 | t.Errorf("handler returned unexpected body, it should be empty, instead received: %v\n", 433 | body) 434 | } 435 | } 436 | 437 | func runResendTest(t *testing.T, resendReq api.ResendRequest) []byte { 438 | encoded, err := json.Marshal(resendReq) 439 | if err != nil { 440 | t.Error(err) 441 | } 442 | 443 | var req *http.Request 444 | req, err = http.NewRequest("POST", push, bytes.NewBuffer(encoded)) 445 | if err != nil { 446 | t.Fatal(err) 447 | } 448 | 449 | rr := httptest.NewRecorder() 450 | tm := TransactionManager{Enclave: &MockEnclave{}} 451 | 452 | handler := http.HandlerFunc(tm.resend) 453 | handler.ServeHTTP(rr, req) 454 | 455 | if status := rr.Code; status != http.StatusOK { 456 | t.Errorf("handler returned wrong status code: got %v want %v\n", 457 | status, http.StatusOK) 458 | } 459 | 460 | return rr.Body.Bytes() 461 | } 462 | 463 | func TestPartyInfo(t *testing.T) { 464 | 465 | partyInfos := []api.PartyInfo{ 466 | api.CreatePartyInfo( 467 | "http://localhost:8000", 468 | []string{"http://localhost:8001"}, 469 | []nacl.Key{nacl.NewKey()}, 470 | http.DefaultClient), 471 | 472 | api.InitPartyInfo( 473 | "http://localhost:8000", 474 | []string{"http://localhost:8001"}, 475 | http.DefaultClient, false), 476 | } 477 | 478 | for _, pi := range partyInfos { 479 | testRunPartyInfo(t, pi) 480 | } 481 | } 482 | 483 | func testRunPartyInfo(t *testing.T, pi api.PartyInfo) { 484 | encodedPartyInfo := api.EncodePartyInfo(pi) 485 | encoded, err := json.Marshal(api.PartyInfoResponse{Payload: encodedPartyInfo}) 486 | if err != nil { 487 | t.Errorf("Marshalling failed %v", err) 488 | } 489 | req, err := http.NewRequest("POST", push, bytes.NewBuffer(encoded)) 490 | if err != nil { 491 | t.Fatal(err) 492 | } 493 | 494 | rr := httptest.NewRecorder() 495 | tm := TransactionManager{Enclave: &MockEnclave{}} 496 | 497 | handler := http.HandlerFunc(tm.partyInfo) 498 | handler.ServeHTTP(rr, req) 499 | 500 | if status := rr.Code; status != http.StatusOK { 501 | t.Errorf("handler returned wrong status code: got %v want %v\n", 502 | status, http.StatusOK) 503 | } 504 | 505 | if !bytes.Equal(rr.Body.Bytes(), payload) { 506 | t.Errorf("handler returned unexpected body: got %v wanted %v\n", 507 | rr.Body.Bytes(), payload) 508 | } 509 | } 510 | 511 | func InitgRPCServer(t *testing.T, grpc bool, port int) string { 512 | ipcPath, err := ioutil.TempDir("", "TestInitIpc") 513 | tm, err := Init(&MockEnclave{}, "localhost", port, ipcPath, grpc, -1, false, "", "") 514 | 515 | if err != nil { 516 | t.Errorf("Error starting server: %v\n", err) 517 | } 518 | runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) 519 | return ipcPath 520 | } 521 | 522 | func TestInit(t *testing.T) { 523 | dbPath, err := ioutil.TempDir("", "TestInit") 524 | if err != nil { 525 | t.Error(err) 526 | } 527 | db, err := storage.InitLevelDb(dbPath) 528 | if err != nil { 529 | t.Errorf("Error starting server: %v\n", err) 530 | } 531 | pubKeyFiles := []string{"key.pub"} 532 | privKeyFiles := []string{"key"} 533 | 534 | for i, keyFile := range privKeyFiles { 535 | privKeyFiles[i] = path.Join("../enclave/testdata", keyFile) 536 | } 537 | 538 | for i, keyFile := range pubKeyFiles { 539 | pubKeyFiles[i] = path.Join("../enclave/testdata", keyFile) 540 | } 541 | 542 | key := []nacl.Key{nacl.NewKey()} 543 | 544 | pi := api.CreatePartyInfo( 545 | "http://localhost:9000", 546 | []string{"http://localhost:9001"}, 547 | key, 548 | http.DefaultClient) 549 | 550 | enc := enclave.Init(db, pubKeyFiles, privKeyFiles, pi, http.DefaultClient, false) 551 | 552 | ipcPath, err := ioutil.TempDir("", "TestInitIpc") 553 | if err != nil { 554 | t.Error(err) 555 | } 556 | certFile, keyFile := "../enclave/testdata/cert/server.crt", "../enclave/testdata/cert/server.key" 557 | tm, err := Init(enc, "localhost", 9001, ipcPath, false, -1, true, certFile, keyFile) 558 | if err != nil { 559 | t.Errorf("Error starting server: %v\n", err) 560 | } 561 | runSimpleGetRequest(t, upCheck, upCheckResponse, tm.upcheck) 562 | } 563 | -------------------------------------------------------------------------------- /enclave/enclave.go: -------------------------------------------------------------------------------- 1 | // Package enclave provides enclaves for the secure storage and propagation of transactions. 2 | package enclave 3 | 4 | import ( 5 | "bytes" 6 | "crypto/rand" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "github.com/blk-io/crux/api" 13 | "github.com/blk-io/crux/storage" 14 | "github.com/blk-io/crux/utils" 15 | "github.com/kevinburke/nacl" 16 | "github.com/kevinburke/nacl/box" 17 | "github.com/kevinburke/nacl/secretbox" 18 | log "github.com/sirupsen/logrus" 19 | "io/ioutil" 20 | "path/filepath" 21 | "strings" 22 | ) 23 | 24 | // SecureEnclave is the secure transaction enclave. 25 | type SecureEnclave struct { 26 | Db storage.DataStore // The underlying key-value datastore for encrypted transactions 27 | PubKeys []nacl.Key // Public keys associated with this enclave 28 | PrivKeys []nacl.Key // Private keys associated with this enclave 29 | selfPubKey nacl.Key // An ephemeral key used for transactions only intended for this enclave 30 | PartyInfo api.PartyInfo // Details of all other nodes (or parties) on the network 31 | keyCache map[nacl.Key]map[nacl.Key]nacl.Key // Maps sender -> recipient -> shared key 32 | client utils.HttpClient // The underlying HTTP client used to propagate requests 33 | grpc bool 34 | } 35 | 36 | // Init creates a new instance of the SecureEnclave. 37 | func Init( 38 | db storage.DataStore, 39 | pubKeyFiles, privKeyFiles []string, 40 | pi api.PartyInfo, 41 | client utils.HttpClient, grpc bool) *SecureEnclave { 42 | 43 | // Key format: 44 | // BULeR8JyUWhiuuCMU/HLA0Q5pzkYT+cHII3ZKBey3Bo= 45 | pubKeys, err := loadPubKeys(pubKeyFiles) 46 | if err != nil { 47 | log.Fatalf("Unable to load public key files: %s, error: %v", pubKeyFiles, err) 48 | } 49 | 50 | // Key format: 51 | // {"data":{"bytes":"Wl+xSyXVuuqzpvznOS7dOobhcn4C5auxkFRi7yLtgtA="},"type":"unlocked"} 52 | privKeys, err := loadPrivKeys(privKeyFiles) 53 | if err != nil { 54 | log.Fatalf("Unable to load private key files: %s, error: %v", privKeyFiles, err) 55 | } 56 | 57 | enc := SecureEnclave{ 58 | Db: db, 59 | PubKeys: pubKeys, 60 | PrivKeys: privKeys, 61 | PartyInfo: pi, 62 | client: client, 63 | grpc: grpc, 64 | } 65 | 66 | // We use shared keys for encrypting data. The keys between a specific sender and recipient are 67 | // computed once for each unique pair. 68 | // 69 | // Encrypt scenarios: 70 | // The sender value must always be a public key that we have the corresponding private key for 71 | // privateFor: [] => encrypt with sharedKey [self-private, selfPub-public] 72 | // store in cache as (self-public, selfPub-public) 73 | // privateFor: [recipient1, ...] => encrypt with sharedKey1 [self-private, recipient1-public], ... 74 | // store in cache as (self-public, recipient1-public) 75 | // Decrypt scenarios: 76 | // epl, [] => The payload was pushed to us (we are recipient1), decrypt with sharedKey 77 | // [recipient1-private, sender-public] 78 | // lookup in cache as (recipient1-public, sender-public) 79 | // epl, [recipient1, ...,] => The payload originated with us (we are self), decrypt with 80 | // sharedKey [self-private, recipient1-public] 81 | // lookup in cache as (self-public, recipient1-public) 82 | // 83 | // Note that sharedKey(privA, pubB) produces the same key as sharedKey(pubA, privB), which is 84 | // why when sending to ones self we encrypt with sharedKey [self-private, selfPub-public], then 85 | // retrieve with sharedKey [self-private, selfPub-public] 86 | enc.keyCache = make(map[nacl.Key]map[nacl.Key]nacl.Key) 87 | 88 | enc.selfPubKey = nacl.NewKey() 89 | 90 | for _, pubKey := range enc.PubKeys { 91 | enc.keyCache[pubKey] = make(map[nacl.Key]nacl.Key) 92 | 93 | // We have a once off generated key which we use for storing payloads which are addressed 94 | // only to ourselves. We have to do this, as we cannot use box.Seal with a public and 95 | // private key-pair. 96 | // 97 | // We pre-compute these keys on startup. 98 | enc.resolveSharedKey(enc.PrivKeys[0], pubKey, enc.selfPubKey) 99 | } 100 | 101 | return &enc 102 | } 103 | 104 | // Store a payload submitted via an Ethereum node. 105 | // This function encrypts the payload, and distributes the encrypted payload to the other 106 | // specified recipients in the network. 107 | // The hash of the encrypted payload is returned to the sender. 108 | func (s *SecureEnclave) Store( 109 | message *[]byte, sender []byte, recipients [][]byte) ([]byte, error) { 110 | 111 | var err error 112 | var senderPubKey, senderPrivKey nacl.Key 113 | 114 | if len(sender) == 0 { 115 | // from address is either default or specified on communication 116 | senderPubKey = s.PubKeys[0] 117 | senderPrivKey = s.PrivKeys[0] 118 | } else { 119 | senderPubKey, err = utils.ToKey(sender) 120 | if err != nil { 121 | log.WithField("senderPubKey", sender).Errorf( 122 | "Unable to load sender public key, %v", err) 123 | return nil, err 124 | } 125 | 126 | senderPrivKey, err = s.resolvePrivateKey(senderPubKey) 127 | if err != nil { 128 | log.WithField("senderPubKey", sender).Errorf( 129 | "Unable to locate private key for sender public key, %v", err) 130 | return nil, err 131 | } 132 | } 133 | 134 | return s.store(message, senderPubKey, senderPrivKey, recipients) 135 | } 136 | 137 | func (s *SecureEnclave) store( 138 | message *[]byte, 139 | senderPubKey, senderPrivKey nacl.Key, 140 | recipients [][]byte) ([]byte, error) { 141 | 142 | var toSelf bool 143 | if len(recipients) == 0 { 144 | toSelf = true 145 | recipients = [][]byte{(*s.selfPubKey)[:]} 146 | } else { 147 | toSelf = false 148 | } 149 | 150 | epl, masterKey := createEncryptedPayload(message, senderPubKey, recipients) 151 | 152 | for i, recipient := range recipients { 153 | 154 | recipientKey, err := utils.ToKey(recipient) 155 | if err != nil { 156 | log.WithField("recipientKey", recipientKey).Errorf( 157 | "Unable to load recipient, %v", err) 158 | continue 159 | } 160 | 161 | // TODO: We may choose to loosen this check 162 | if bytes.Equal((*recipientKey)[:], (*senderPubKey)[:]) { 163 | log.WithField("recipientKey", recipientKey).Errorf( 164 | "Sender cannot be recipient, %v", err) 165 | continue 166 | } 167 | 168 | sharedKey := s.resolveSharedKey(senderPrivKey, senderPubKey, recipientKey) 169 | sealedBox := sealPayload(epl.RecipientNonce, masterKey, sharedKey) 170 | 171 | epl.RecipientBoxes[i] = sealedBox 172 | } 173 | 174 | encodedEpl := api.EncodePayloadWithRecipients(epl, recipients) 175 | digest, err := s.storePayload(epl, encodedEpl) 176 | 177 | if !toSelf { 178 | for i, recipient := range recipients { 179 | recipientEpl := api.EncryptedPayload{ 180 | Sender: senderPubKey, 181 | CipherText: epl.CipherText, 182 | Nonce: epl.Nonce, 183 | RecipientBoxes: [][]byte{epl.RecipientBoxes[i]}, 184 | RecipientNonce: epl.RecipientNonce, 185 | } 186 | 187 | log.WithFields(log.Fields{ 188 | "recipient": hex.EncodeToString(recipient), "digest": hex.EncodeToString(digest), 189 | }).Debug("Publishing payload") 190 | 191 | s.publishPayload(recipientEpl, recipient) 192 | } 193 | } 194 | 195 | return digest, err 196 | } 197 | 198 | func createEncryptedPayload( 199 | message *[]byte, senderPubKey nacl.Key, recipients [][]byte) (api.EncryptedPayload, nacl.Key) { 200 | 201 | nonce := nacl.NewNonce() 202 | masterKey := nacl.NewKey() 203 | recipientNonce := nacl.NewNonce() 204 | 205 | sealedMessage := secretbox.Seal([]byte{}, *message, nonce, masterKey) 206 | 207 | return api.EncryptedPayload{ 208 | Sender: senderPubKey, 209 | CipherText: sealedMessage, 210 | Nonce: nonce, 211 | RecipientBoxes: make([][]byte, len(recipients)), 212 | RecipientNonce: recipientNonce, 213 | }, masterKey 214 | } 215 | 216 | func (s *SecureEnclave) publishPayload(epl api.EncryptedPayload, recipient []byte) { 217 | 218 | key, err := utils.ToKey(recipient) 219 | if err != nil { 220 | log.WithField("recipient", recipient).Errorf( 221 | "Unable to decode key for recipient, error: %v", err) 222 | } 223 | 224 | if url, ok := s.PartyInfo.GetRecipient(key); ok { 225 | encoded := api.EncodePayloadWithRecipients(epl, [][]byte{}) 226 | if s.grpc { 227 | api.PushGrpc(encoded, url, epl) 228 | } else { 229 | api.Push(encoded, url, s.client) 230 | } 231 | } else { 232 | log.WithField("recipientKey", hex.EncodeToString(recipient)).Error("Unable to resolve host") 233 | } 234 | } 235 | 236 | func (s *SecureEnclave) resolveSharedKey( 237 | senderPrivKey, senderPubKey, recipientPubKey nacl.Key) nacl.Key { 238 | 239 | keyCache, ok := s.keyCache[senderPubKey] 240 | if !ok { 241 | keyCache = make(map[nacl.Key]nacl.Key) 242 | s.keyCache[senderPubKey] = keyCache 243 | } 244 | 245 | sharedKey, ok := keyCache[recipientPubKey] 246 | if !ok { 247 | sharedKey = box.Precompute(recipientPubKey, senderPrivKey) 248 | keyCache[recipientPubKey] = sharedKey 249 | } 250 | 251 | return sharedKey 252 | } 253 | 254 | func (s *SecureEnclave) resolvePrivateKey(publicKey nacl.Key) (nacl.Key, error) { 255 | for i, key := range s.PubKeys { 256 | if bytes.Equal((*publicKey)[:], (*key)[:]) { 257 | return s.PrivKeys[i], nil 258 | } 259 | } 260 | return nil, fmt.Errorf("unable to find private key for public key: %s", 261 | hex.EncodeToString((*publicKey)[:])) 262 | } 263 | 264 | // Store a binary encoded payload within this SecureEnclave. 265 | // This will be a payload that has been propagated to this node as it is a party on the 266 | // transaction. I.e. it is not the original recipient of the transaction, but one of the recipients 267 | // it is intended for. 268 | func (s *SecureEnclave) StorePayload(encoded []byte) ([]byte, error) { 269 | epl, _ := api.DecodePayloadWithRecipients(encoded) 270 | return s.storePayload(epl, encoded) 271 | } 272 | 273 | func (s *SecureEnclave) StorePayloadGrpc(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { 274 | return s.storePayload(epl, encoded) 275 | } 276 | 277 | func (s *SecureEnclave) storePayload(epl api.EncryptedPayload, encoded []byte) ([]byte, error) { 278 | digestHash := utils.Sha3Hash(epl.CipherText) 279 | err := s.Db.Write(&digestHash, &encoded) 280 | return digestHash, err 281 | } 282 | 283 | func sealPayload( 284 | recipientNonce nacl.Nonce, 285 | masterKey nacl.Key, 286 | sharedKey nacl.Key) []byte { 287 | 288 | return box.SealAfterPrecomputation( 289 | []byte{}, 290 | (*masterKey)[:], 291 | recipientNonce, 292 | sharedKey) 293 | } 294 | 295 | // RetrieveDefault is used to retrieve the provided payload. It attempts to use a default key 296 | // value of the first public key associated with this SecureEnclave instance. 297 | // If the payload cannot be found, or decrypted successfully an error is returned. 298 | func (s *SecureEnclave) RetrieveDefault(digestHash *[]byte) ([]byte, error) { 299 | // to address is either default or specified on communication 300 | key := (*s.PubKeys[0])[:] 301 | return s.Retrieve(digestHash, &key) 302 | } 303 | 304 | // Retrieve is used to retrieve the provided payload. 305 | // If the payload cannot be found, or decrypted successfully an error is returned. 306 | func (s *SecureEnclave) Retrieve(digestHash *[]byte, to *[]byte) ([]byte, error) { 307 | 308 | encoded, err := s.Db.Read(digestHash) 309 | if err != nil { 310 | return nil, err 311 | } 312 | 313 | epl, recipients := api.DecodePayloadWithRecipients(*encoded) 314 | 315 | masterKey := new([nacl.KeySize]byte) 316 | 317 | var senderPubKey, senderPrivKey, recipientPubKey, sharedKey nacl.Key 318 | 319 | if len(recipients) == 0 { 320 | // This is a payload originally sent to us by another node 321 | recipientPubKey = epl.Sender 322 | senderPubKey, err = utils.ToKey(*to) 323 | if err != nil { 324 | return nil, err 325 | } 326 | } else { 327 | // This is a payload that originated from us 328 | senderPubKey = epl.Sender 329 | recipientPubKey, err = utils.ToKey(recipients[0]) 330 | if err != nil { 331 | return nil, err 332 | } 333 | } 334 | 335 | senderPrivKey, err = s.resolvePrivateKey(senderPubKey) 336 | if err != nil { 337 | return nil, err 338 | } 339 | 340 | // we might not have the key in our cache if constellation was restarted, hence we may 341 | // need to recreate 342 | sharedKey = s.resolveSharedKey(senderPrivKey, senderPubKey, recipientPubKey) 343 | 344 | _, ok := secretbox.Open(masterKey[:0], epl.RecipientBoxes[0], epl.RecipientNonce, sharedKey) 345 | if !ok { 346 | return nil, errors.New("unable to open master key secret box") 347 | } 348 | 349 | var payload []byte 350 | payload, ok = secretbox.Open(payload[:0], epl.CipherText, epl.Nonce, masterKey) 351 | if !ok { 352 | return payload, errors.New("unable to open payload secret box") 353 | } 354 | 355 | return payload, nil 356 | } 357 | 358 | // RetrieveFor retrieves a payload with the given digestHash for a specific recipient who was one 359 | // of the original recipients specified on the payload. 360 | func (s *SecureEnclave) RetrieveFor(digestHash *[]byte, reqRecipient *[]byte) (*[]byte, error) { 361 | encoded, err := s.Db.Read(digestHash) 362 | if err != nil { 363 | return nil, err 364 | } 365 | 366 | epl, recipients := api.DecodePayloadWithRecipients(*encoded) 367 | 368 | for i, recipient := range recipients { 369 | if bytes.Equal(*reqRecipient, recipient) { 370 | recipientEpl := api.EncryptedPayload{ 371 | Sender: epl.Sender, 372 | CipherText: epl.CipherText, 373 | Nonce: epl.Nonce, 374 | RecipientBoxes: [][]byte{epl.RecipientBoxes[i]}, 375 | RecipientNonce: epl.RecipientNonce, 376 | } 377 | encoded := api.EncodePayload(recipientEpl) 378 | return &encoded, nil 379 | } 380 | } 381 | return nil, fmt.Errorf("invalid recipient %x requested for payload", reqRecipient) 382 | } 383 | 384 | // RetrieveAllFor retrieves all payloads that the specified recipient was an original recipient 385 | // for. 386 | // Each payload found is published to the specified recipient. 387 | func (s *SecureEnclave) RetrieveAllFor(reqRecipient *[]byte) error { 388 | return s.Db.ReadAll(func(key, value *[]byte) { 389 | epl, recipients := api.DecodePayloadWithRecipients(*value) 390 | 391 | for i, recipient := range recipients { 392 | if bytes.Equal(*reqRecipient, recipient) { 393 | recipientEpl := api.EncryptedPayload{ 394 | Sender: epl.Sender, 395 | CipherText: epl.CipherText, 396 | Nonce: epl.Nonce, 397 | RecipientBoxes: [][]byte{epl.RecipientBoxes[i]}, 398 | RecipientNonce: epl.RecipientNonce, 399 | } 400 | func() { 401 | go s.publishPayload(recipientEpl, *reqRecipient) 402 | }() 403 | } 404 | } 405 | }) 406 | } 407 | 408 | // Delete deletes the payload associated with the given digestHash from the SecureEnclave's store. 409 | func (s *SecureEnclave) Delete(digestHash *[]byte) error { 410 | return s.Db.Delete(digestHash) 411 | } 412 | 413 | // UpdatePartyInfo applies the provided binary encoded party details to the SecureEnclave's 414 | // own party details store. 415 | func (s *SecureEnclave) UpdatePartyInfo(encoded []byte) { 416 | s.PartyInfo.UpdatePartyInfo(encoded) 417 | } 418 | 419 | func (s *SecureEnclave) UpdatePartyInfoGrpc(url string, recipients map[[nacl.KeySize]byte]string, parties map[string]bool) { 420 | s.PartyInfo.UpdatePartyInfoGrpc(url, recipients, parties) 421 | } 422 | 423 | // GetEncodedPartyInfo provides this SecureEnclaves PartyInfo details in a binary encoded format. 424 | func (s *SecureEnclave) GetEncodedPartyInfo() []byte { 425 | return api.EncodePartyInfo(s.PartyInfo) 426 | } 427 | 428 | func (s *SecureEnclave) GetEncodedPartyInfoGrpc() []byte { 429 | encoded, err := json.Marshal(api.PartyInfoResponse{Payload: api.EncodePartyInfo(s.PartyInfo)}) 430 | if err != nil { 431 | log.Errorf("Marshalling failed %v", err) 432 | } 433 | return encoded 434 | } 435 | 436 | func (s *SecureEnclave) GetPartyInfo() (string, map[[nacl.KeySize]byte]string, map[string]bool) { 437 | return s.PartyInfo.GetAllValues() 438 | } 439 | 440 | func loadPubKeys(pubKeyFiles []string) ([]nacl.Key, error) { 441 | return loadKeys( 442 | pubKeyFiles, 443 | func(s string) (string, error) { 444 | src, err := ioutil.ReadFile(s) 445 | if err != nil { 446 | return "", err 447 | } 448 | return string(src), nil 449 | }) 450 | } 451 | 452 | func loadPrivKeys(privKeyFiles []string) ([]nacl.Key, error) { 453 | return loadKeys( 454 | privKeyFiles, 455 | func(s string) (string, error) { 456 | var privateKey api.PrivateKey 457 | src, err := ioutil.ReadFile(s) 458 | if err != nil { 459 | return "", err 460 | } 461 | err = json.Unmarshal(src, &privateKey) 462 | if err != nil { 463 | return "", err 464 | } 465 | 466 | return privateKey.Data.Bytes, nil 467 | }) 468 | } 469 | 470 | func loadKeys( 471 | keyFiles []string, f func(string) (string, error)) ([]nacl.Key, error) { 472 | keys := make([]nacl.Key, len(keyFiles)) 473 | 474 | for i, keyFile := range keyFiles { 475 | data, err := f(keyFile) 476 | if err != nil { 477 | return nil, err 478 | } 479 | var key nacl.Key 480 | key, err = utils.LoadBase64Key( 481 | strings.TrimSuffix(data, "\n")) 482 | if err != nil { 483 | return nil, err 484 | } 485 | keys[i] = key 486 | } 487 | 488 | return keys, nil 489 | } 490 | 491 | // DoKeyGeneration is used to generate new public and private key-pairs, writing them to the 492 | // provided file locations. 493 | // Public keys have the "pub" suffix, whereas private keys have the "key" suffix. 494 | func DoKeyGeneration(keyFile string) error { 495 | pubKey, privKey, err := box.GenerateKey(rand.Reader) 496 | if err != nil { 497 | return fmt.Errorf("error creating keys: %v", err) 498 | } 499 | err = utils.CreateDirForFile(keyFile) 500 | if err != nil { 501 | return fmt.Errorf("invalid destination specified: %s, error: %v", 502 | filepath.Dir(keyFile), err) 503 | } 504 | 505 | b64PubKey := base64.StdEncoding.EncodeToString((*pubKey)[:]) 506 | b64PrivKey := base64.StdEncoding.EncodeToString((*privKey)[:]) 507 | 508 | err = ioutil.WriteFile(keyFile+".pub", []byte(b64PubKey), 0600) 509 | if err != nil { 510 | return fmt.Errorf("unable to write public key: %s, error: %v", keyFile, err) 511 | } 512 | 513 | jsonKey := api.PrivateKey{ 514 | Type: "unlocked", 515 | Data: api.PrivateKeyBytes{ 516 | Bytes: b64PrivKey, 517 | }, 518 | } 519 | 520 | var encoded []byte 521 | encoded, err = json.Marshal(jsonKey) 522 | if err != nil { 523 | return fmt.Errorf("unable to encode private key: %v, error: %v", jsonKey, err) 524 | } 525 | 526 | err = ioutil.WriteFile(keyFile+".key", encoded, 0600) 527 | if err != nil { 528 | return fmt.Errorf("unable to write private key: %s, error: %v", keyFile, err) 529 | } 530 | return nil 531 | } 532 | --------------------------------------------------------------------------------