From e6edd5b0e6275048bc1205eb23bd68b9b028f6c2 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Oct 2018 13:09:00 -0400 Subject: [PATCH 01/10] Authenticated HTTP API - first pass --- api_auth_docker/Dockerfile | 19 ++++ api_auth_docker/README.md | 120 ++++++++++++++++++++++++ api_auth_docker/auth.sh | 78 +++++++++++++++ api_auth_docker/default-ssl.conf | 29 ++++++ api_auth_docker/default.conf | 26 +++++ api_auth_docker/entrypoint.sh | 5 + api_auth_docker/keys.properties | 3 + api_auth_docker/trace.sh | 15 +++ clients/javascript/cyphernode-client.js | 0 clients/shell/cyphernode-client.sh | 6 ++ docker-compose.yml | 15 +++ 11 files changed, 316 insertions(+) create mode 100644 api_auth_docker/Dockerfile create mode 100644 api_auth_docker/README.md create mode 100644 api_auth_docker/auth.sh create mode 100644 api_auth_docker/default-ssl.conf create mode 100644 api_auth_docker/default.conf create mode 100644 api_auth_docker/entrypoint.sh create mode 100644 api_auth_docker/keys.properties create mode 100644 api_auth_docker/trace.sh create mode 100644 clients/javascript/cyphernode-client.js create mode 100644 clients/shell/cyphernode-client.sh diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile new file mode 100644 index 0000000..441e0c1 --- /dev/null +++ b/api_auth_docker/Dockerfile @@ -0,0 +1,19 @@ +FROM nginx:alpine + +RUN apk add --update --no-cache \ + git \ + openssl \ + fcgiwrap \ + spawn-fcgi \ + curl \ + jq + +COPY auth.sh /etc/nginx/conf.d +COPY default-ssl.conf /etc/nginx/conf.d/default.conf +COPY entrypoint.sh entrypoint.sh +COPY keys.properties /etc/nginx/conf.d +COPY trace.sh /etc/nginx/conf.d + +RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md new file mode 100644 index 0000000..de8c721 --- /dev/null +++ b/api_auth_docker/README.md @@ -0,0 +1,120 @@ +# HTTP/S API supporting HMAC API keys + +So all the other containers are in the Docker Swarm and we want to expose a real HTTP/S interface to clients outside of the Swarm, that makes sense. Clients have to get an API key first. + +## API key + +Let's produce a 256-bits key that we'll convert in an hex string to store and use with openssl hmac feature. + +```shell +dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32 +``` + +The key is stored in keys.properties and must be given to the client. This is a secret key. keys.properties looks like this: + +```property +#keyid=hex(key) +key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 +key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 +``` + +### Bearer token + +Following JWT (JSON Web Tokens) standard, we build a bearer token that will be in the request header and signed with the secret key. We need this in the request header: + +```shell +Authorization: Bearer +``` + +...where token is: + +```shell +token = hhh.ppp.sss +``` + +...where hhh is the header in base64, ppp is the payload in base64 and sss is the signature. Here are the expected formats and contents: + +```shell +header = {"alg":"HS256","typ":"JWT"} +header64 = base64(header) = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg== +``` + +```shell +payload = {"id":"001","exp":1538528077} +payload64 = base64(payload) = eyJpZCI6IjAwMSIsImV4cCI6MTUzODUyODA3N30K +``` + +The "id" property is the client id and the "exp" property should be current epoch time + 10 seconds, like: + +```shell +$((`date +"%s"`+10)) +``` + +...so that the request will be expired in 10 seconds. That should take care of most Replay attacks if any. You should run nginx with TLS so that the replay attack can't be possible. + +```shell +signature = hmacsha256(header64.payload64, key) +``` + +```shell +token = header64 + "." + payload64 + "." + signature +``` + +### cURL example of an API invocation + +Instruction should be in the form: + +```shell +curl -v -H "Authorization: Bearer hhh.ppp.sss" localhost +``` + +10 seconds request expiration: + +```shell +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash +``` + +60 seconds request expiration: + +```shell +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash +``` + +## SSL + +Create your key and certificates. + +openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ~/cyphernode/private/key.pem -out ~/cyphernode/certs/cert.pem -days 365 + +Use default-ssl.conf as the template instead of default.conf. + +## Build + +### Create your API key and put it in keys.properties + +```shell +dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32 +``` + +### Build and run docker image + +```shell +docker build -t authapi . +docker run -d --rm --name authapi -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode/certs:/etc/ssl/certs" -v "~/cyphernode/private:/etc/ssl/private" authapi +``` + +## Invoke cyphernode through authenticated API + +```shell +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash +``` + +## Technicalities + +```shell +h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) +p64=$(echo "{\"id\":\"001\",\"exp\":$((`date +"%s"`+10))}" | base64) +k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36" +s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) +token="$h64.$p64.$s" +``` diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh new file mode 100644 index 0000000..3bac731 --- /dev/null +++ b/api_auth_docker/auth.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +# +# This is not designed to serve thousands of API key! +# +# header = {"alg":"HS256","typ":"JWT"} +# header64 = base64(header) = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg== +# +# payload = {"id":"001","exp":1538528077} +# payload64 = base64(payload) = eyJpZCI6IjAwMSIsImV4cCI6MTUzODUyODA3N30K +# +# signature = hmacsha256(header64.payload64, key) +# +# token = header64.payload64.signature = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==.eyJpZCI6IjAwMSIsImV4cCI6MTUzODUyODA3N30K.signature +# + +. ./trace.sh + +verify() +{ + local header64=$(echo ${1} | cut -sd '.' -f1) + local payload64=$(echo ${1} | cut -sd '.' -f2) + local signature=$(echo ${1} | cut -sd '.' -f3) + + trace "[verify] header64=${header64}" + trace "[verify] payload64=${payload64}" + trace "[verify] signature=${signature}" + + local payload=$(echo ${payload64} | base64 -d) + local exp=$(echo ${payload} | jq ".exp") + local current=$(date +"%s") + + trace "[verify] payload=${payload}" + trace "[verify] exp=${exp}" + trace "[verify] current=${current}" + + if [ ${exp} -gt ${current} ]; then + trace "[verify] Not expired, let's validate signature" + local id=$(echo ${payload} | jq ".id" | tr -d '"') + trace "[verify] id=${id}" + + # It is so much faster to include the keys here instead of grep'ing the file for key. + . ./keys.properties + + local key + eval key='$key'$id + trace "[verify] key=${key}" + local comp_sign=$(echo "${header64}.${payload64}" | openssl dgst -hmac "${key}" -sha256 -r | cut -sd ' ' -f1) + + trace "[verify] comp_sign=${comp_sign}" + + if [ "${comp_sign}" = "${signature}" ]; then + trace "[verify] Valid signature!" + echo -en "Status: 200 OK\r\n\r\n" + return + fi + trace "[verify] Invalid signature!" + return 1 + fi + + trace "[verify] Expired!" + + return 1 +} + +# $HTTP_AUTHORIZATION = Bearer +trace "[auth.sh] HTTP_AUTHORIZATION=${HTTP_AUTHORIZATION}" +if [ "${HTTP_AUTHORIZATION:0:6}" = "Bearer" ]; then + token="${HTTP_AUTHORIZATION:6}" + + if [ -n "$token" ]; then + trace "[auth.sh] Valid format for authorization header" + verify "${token}" + [ "$?" -eq "0" ] && return + fi +fi + +echo -en "Status: 403 Forbidden\r\n\r\n" diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf new file mode 100644 index 0000000..07735d4 --- /dev/null +++ b/api_auth_docker/default-ssl.conf @@ -0,0 +1,29 @@ +server { + listen 443 ssl; + server_name localhost; + + ssl_certificate /etc/ssl/certs/cert.pem; + ssl_certificate_key /etc/ssl/private/key.pem; + + location / { + auth_request /auth; + proxy_pass http://cyphernode:8888; + } + + location /auth { + internal; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /etc/nginx/conf.d/auth.sh; + fastcgi_pass unix:/var/run/fcgiwrap.socket; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf new file mode 100644 index 0000000..dd4b951 --- /dev/null +++ b/api_auth_docker/default.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name localhost; + + location / { + auth_request /auth; + proxy_pass http://cyphernode:8888; + } + + location /auth { + internal; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME /etc/nginx/conf.d/auth.sh; + fastcgi_pass unix:/var/run/fcgiwrap.socket; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + +} diff --git a/api_auth_docker/entrypoint.sh b/api_auth_docker/entrypoint.sh new file mode 100644 index 0000000..be8f164 --- /dev/null +++ b/api_auth_docker/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +spawn-fcgi -s /var/run/fcgiwrap.socket -u nginx -g nginx -U nginx -- /usr/bin/fcgiwrap + +nginx -g "daemon off;" diff --git a/api_auth_docker/keys.properties b/api_auth_docker/keys.properties new file mode 100644 index 0000000..781bbff --- /dev/null +++ b/api_auth_docker/keys.properties @@ -0,0 +1,3 @@ +#keyid=hex(key) +key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 +key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 diff --git a/api_auth_docker/trace.sh b/api_auth_docker/trace.sh new file mode 100644 index 0000000..34a18df --- /dev/null +++ b/api_auth_docker/trace.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +trace() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + fi +} + +trace_rc() +{ + if [ -n "${TRACING}" ]; then + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + fi +} diff --git a/clients/javascript/cyphernode-client.js b/clients/javascript/cyphernode-client.js new file mode 100644 index 0000000..e69de29 diff --git a/clients/shell/cyphernode-client.sh b/clients/shell/cyphernode-client.sh new file mode 100644 index 0000000..dc62791 --- /dev/null +++ b/clients/shell/cyphernode-client.sh @@ -0,0 +1,6 @@ + +invoke_cyphernode +{ + id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +\"%s\"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" localhost/getbestblockhash + +} diff --git a/docker-compose.yml b/docker-compose.yml index 54ae02f..9135b04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,21 @@ version: "3" services: + authapi: + # HTTP authentication API gate + image: authapi + ports: + - "80:80" + - "443:443" + volumes: + - "~/cyphernode/certs:/etc/ssl/certs" + - "~/cyphernode/private:/etc/ssl/private" +# deploy: +# placement: +# constraints: [node.hostname==dev] + networks: + - cyphernodenet + cyphernode: # Bitcoin Mini Proxy env_file: From 780f17fc8a00e0ab6356feb23334e3bcabd611ed Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Oct 2018 13:57:40 -0400 Subject: [PATCH 02/10] Added clarity in README and INSTALL --- api_auth_docker/README.md | 68 ++++++++++++++++++++++++--------------- doc/INSTALL.md | 7 ++++ docker-compose.yml | 4 +-- 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index de8c721..2dfa539 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -2,15 +2,25 @@ So all the other containers are in the Docker Swarm and we want to expose a real HTTP/S interface to clients outside of the Swarm, that makes sense. Clients have to get an API key first. -## API key +## Build + +### Create your API key and put it in keys.properties Let's produce a 256-bits key that we'll convert in an hex string to store and use with openssl hmac feature. +Alpine (Busybox): + ```shell dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32 ``` -The key is stored in keys.properties and must be given to the client. This is a secret key. keys.properties looks like this: +Linux: + +```shell +dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -ps -c 32 +``` + +Put the key in keys.properties and keep it for the client. This is a secret key. keys.properties looks like this: ```property #keyid=hex(key) @@ -18,7 +28,36 @@ key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 ``` -### Bearer token +You can have multiple keys, but be aware that this container has **not** been built to support thousands of API keys! **Cyphernode should be used locally**, not publicly as a service. + +## SSL + +If you already have your certificates and keystores infra, you already know what to do and your can skip this section. Put your files in the bound volume (~/cyphernode-ssl/ see volume path in docker-compose.yml). + +If not, you can create your keys and self-signed certificates. + +```shell +mkdir -p ~/cyphernode-ssl/certs ~/cyphernode-ssl/private +openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ~/cyphernode-ssl/private/key.pem -out ~/cyphernode-ssl/certs/cert.pem -days 365 +``` + +If you don't want to use HTTPS, just copy default.conf instead of default-ssl.conf in Dockerfile. + +**Nota bene**: If you self-sign the certificate, you have to trust the certificate on the client side by adding it to the Trusted Root Certification Authorities or whatever your client needs. + +### Build and run docker image + +```shell +docker build -t authapi . +``` + +If you are using it independantly from the Docker stack (docker-compose.yml), you can run it like that: + +```shell +docker run -d --rm --name authapi -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode-ssl/certs:/etc/ssl/certs" -v "~/cyphernode-ssl/private:/etc/ssl/private" authapi +``` + +## FYI: Bearer token Following JWT (JSON Web Tokens) standard, we build a bearer token that will be in the request header and signed with the secret key. We need this in the request header: @@ -80,29 +119,6 @@ id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo " id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash ``` -## SSL - -Create your key and certificates. - -openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ~/cyphernode/private/key.pem -out ~/cyphernode/certs/cert.pem -days 365 - -Use default-ssl.conf as the template instead of default.conf. - -## Build - -### Create your API key and put it in keys.properties - -```shell -dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -pc 32 -``` - -### Build and run docker image - -```shell -docker build -t authapi . -docker run -d --rm --name authapi -p 80:80 -p 443:443 --network cyphernodenet -v "~/cyphernode/certs:/etc/ssl/certs" -v "~/cyphernode/private:/etc/ssl/private" authapi -``` - ## Invoke cyphernode through authenticated API ```shell diff --git a/doc/INSTALL.md b/doc/INSTALL.md index c717abf..17f951f 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -73,12 +73,17 @@ debian@dev:~/dev/Cyphernode$ vi pycoin_docker/env.properties [See how to build clightning image](https://github.com/SatoshiPortal/dockers/tree/master/x86_64/LN/c-lightning) +### Build the authenticated HTTP API image + +[See how to build authapi image](../api_auth_docker) + ### Deploy **Edit docker-compose.yml to specify special deployment constraints or if you want to run the Bitcoin node on the same machine: uncomment corresponding lines.** ```shell debian@dev:~/dev/Cyphernode$ USER=`id -u cyphernode`:`id -g cyphernode` docker stack deploy --compose-file docker-compose.yml cyphernodestack +Creating service cyphernodestack_authapi Creating service cyphernodestack_cyphernode Creating service cyphernodestack_proxycronnode Creating service cyphernodestack_pycoinnode @@ -87,6 +92,8 @@ Creating service cyphernodestack_clightningnode ## Off-site Bitcoin Node +This section is useful if you already have a Bitcoin Core node running and you want to use it in Cyphernode. In that case, please comment out the btcnode section from docker-compose.yml. + ### Join swarm created on Cyphernode server ```shell diff --git a/docker-compose.yml b/docker-compose.yml index 9135b04..e354c0f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,8 +8,8 @@ services: - "80:80" - "443:443" volumes: - - "~/cyphernode/certs:/etc/ssl/certs" - - "~/cyphernode/private:/etc/ssl/private" + - "~/cyphernode-ssl/certs:/etc/ssl/certs" + - "~/cyphernode-ssl/private:/etc/ssl/private" # deploy: # placement: # constraints: [node.hostname==dev] From 3c88f04dac8c075f758774947aa53a9b1dbf7bd8 Mon Sep 17 00:00:00 2001 From: kexkey Date: Thu, 4 Oct 2018 14:12:01 -0400 Subject: [PATCH 03/10] Docs tweaks, -k arg to curl --- api_auth_docker/README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index 2dfa539..0a80c90 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -110,19 +110,13 @@ curl -v -H "Authorization: Bearer hhh.ppp.sss" localhost 10 seconds request expiration: ```shell -id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash ``` 60 seconds request expiration: ```shell -id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash -``` - -## Invoke cyphernode through authenticated API - -```shell -id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" https://localhost/getbestblockhash +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+60))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash ``` ## Technicalities From 2b67da35b2c56c1c2f47fc907a1e26fc77ad3ab0 Mon Sep 17 00:00:00 2001 From: kexkey Date: Fri, 5 Oct 2018 09:11:45 -0400 Subject: [PATCH 04/10] Client-side code examples --- clients/README.md | 7 +++ clients/javascript/cyphernode-client.js | 77 +++++++++++++++++++++++++ clients/shell/cyphernode-client.sh | 75 +++++++++++++++++++++++- clients/shell/cyphernode.conf | 3 + doc/INSTALL-MANUAL-STEPS.md | 3 + docker-compose.yml | 6 -- 6 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 clients/README.md create mode 100644 clients/shell/cyphernode.conf diff --git a/clients/README.md b/clients/README.md new file mode 100644 index 0000000..f7d2384 --- /dev/null +++ b/clients/README.md @@ -0,0 +1,7 @@ +# Client-side helpers + +These are just examples of how clients can use Cyphernode in their code. + +# Contributing + +You are welcome to add more languages and/or improve current code. diff --git a/clients/javascript/cyphernode-client.js b/clients/javascript/cyphernode-client.js index e69de29..c93506c 100644 --- a/clients/javascript/cyphernode-client.js +++ b/clients/javascript/cyphernode-client.js @@ -0,0 +1,77 @@ +CyphernodeClient = function(is_prod) { + this.baseURL = is_prod ? 'https://cyphernode:443' : 'https://cyphernode-dev:443' +}; + +CyphernodeClient.prototype._post = function(url, postdata, cb) { + let urlr = this.baseURL + url; + + let h64 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==' + let current = Math.round(new Date().getTime/1000) + 10 + let p64 = btoa('{"id":"${id}","exp":' + current + '}') + let api_key = Meteor.settings.CYPHERNODE.api_key + let s = CryptoJS.HmacSHA256(p64, api_key).toString() + let token = h64 + '.' + p64 + '.' + s + + HTTP.post( + urlr, + { + data: postdata, + headers: {'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token}, + }, function (err, resp) { + cb(err, resp.data) + } + ) +}; + +CyphernodeClient.prototype._get = function(url, cb) { + let urlr = this.baseURL + url; + + let h64 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==' + let current = Math.round(new Date().getTime/1000) + 10 + let p64 = btoa('{"id":"${id}","exp":' + current + '}') + let api_key = Meteor.settings.CYPHERNODE.api_key + let s = CryptoJS.HmacSHA256(p64, api_key).toString() + let token = h64 + '.' + p64 + '.' + s + + HTTP.get(urlr, {headers: {'Authorization': 'Bearer ' + token}}, function (err, resp) { + cb(err, resp.data) + }) +}; + +CyphernodeClient.prototype.watch = function(btcaddr, cb0conf, cb1conf, cbreply) { + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.122.233:1111/callback0conf","confirmedCallbackURL":"192.168.122.233:1111/callback1conf"} + let data = { address: btcaddr, unconfirmedCallbackURL: cb0conf, confirmedCallbackURL: cb1conf } + this._post('/watch', data, cbreply); +}; + +CyphernodeClient.prototype.unwatch = function(btcaddr, cbreply) { + // 192.168.122.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp + this._get('/unwatch/' + btcaddr, cbreply); +}; + +CyphernodeClient.prototype.getActiveWatches = function(cbreply) { + // 192.168.122.152:8080/getactivewatches + this._get('/getactivewatches', cbreply); +}; + +CyphernodeClient.prototype.getTransaction = function(txid, cbreply) { + // http://192.168.122.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 + this._get('/gettransaction/' + txid, cbreply); +}; + +CyphernodeClient.prototype.spend = function(btcaddr, amnt, cbreply) { + // BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + let data = { address: btcaddr, amount: amnt } + this._post('/spend', data, cbreply); +}; + +CyphernodeClient.prototype.getBalance = function(cbreply) { + // http://192.168.122.152:8080/getbalance + this._get('/getbalance', cbreply); +}; + +CyphernodeClient.prototype.getNewAddress = function(cbreply) { + // http://192.168.122.152:8080/getnewaddress + this._get('/getnewaddress', cbreply); +}; diff --git a/clients/shell/cyphernode-client.sh b/clients/shell/cyphernode-client.sh index dc62791..9d1312b 100644 --- a/clients/shell/cyphernode-client.sh +++ b/clients/shell/cyphernode-client.sh @@ -1,6 +1,77 @@ +#!/bin/sh -invoke_cyphernode +. .cyphernode.conf + +invoke_cyphernode() { - id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +\"%s\"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -v -H "Authorization: Bearer $token" localhost/getbestblockhash + local action=${1} + local post=${2} + local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + local p64=$(echo "{\"id\":\"${id}\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo "$h64.$p64" | openssl dgst -hmac "$key" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + if [ -n "${post}" ]; then + echo $(curl -v -H "Authorization: Bearer $token" -d "${post}" -k "https://cyphernode/${action}") + return $? + else + echo $(curl -v -H "Authorization: Bearer $token" -k "https://cyphernode/${action}") + return $? + fi +} + +watch() +{ + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","unconfirmedCallbackURL":"192.168.122.233:1111/callback0conf","confirmedCallbackURL":"192.168.122.233:1111/callback1conf"} + local btcaddr=${1} + local cb0conf=${2} + local cb1conf=${3} + local post="{\"address\":\"${btcaddr}\",\"unconfirmedCallbackURL\":\"${cb0conf}\",\"confirmedCallbackURL\":\"${cb1conf}\"}" + + echo $(invoke_cyphernode "watch" ${post}) +} + +unwatch() +{ + # 192.168.122.152:8080/unwatch/2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp + local btcaddr=${1} + + echo $(invoke_cyphernode "unwatch/${btcaddr}") +} + +getactivewatches() +{ + # 192.168.122.152:8080/getactivewatches + echo $(invoke_cyphernode "getactivewatches") +} + +gettransaction() +{ + # http://192.168.122.152:8080/gettransaction/af867c86000da76df7ddb1054b273ca9e034e8c89d049b5b2795f9f590f67648 + local txid=${1} + + echo $(invoke_cyphernode "gettransaction/${txid}") +} + +spend() +{ + # BODY {"address":"2N8DcqzfkYi8CkYzvNNS5amoq3SbAcQNXKp","amount":0.00233} + local btcaddr=${1} + local amount=${2} + local post="{\"address\":\"${btcaddr}\",\"amount\":\"${amount}\"}" + + echo $(invoke_cyphernode "spend" ${post}) +} + +getbalance() +{ + # http://192.168.122.152:8080/getbalance + echo $(invoke_cyphernode "getbalance") +} + +getnewaddress() +{ + # http://192.168.122.152:8080/getnewaddress + echo $(invoke_cyphernode "getnewaddress") } diff --git a/clients/shell/cyphernode.conf b/clients/shell/cyphernode.conf new file mode 100644 index 0000000..d19bf3d --- /dev/null +++ b/clients/shell/cyphernode.conf @@ -0,0 +1,3 @@ +# Provided by cyphernode, see api_auth_docker/README.md +id=001 +key=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 4a5d3fb..a95161c 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -45,6 +45,9 @@ vi pycoin_docker/env.properties ```shell sudo useradd cyphernode mkdir ~/btcproxydb ; sudo chown -R cyphernode:debian ~/btcproxydb ; sudo chmod g+ws ~/btcproxydb +mkdir -p ~/cyphernode-ssl/certs ~/cyphernode-ssl/private +openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout ~/cyphernode-ssl/private/key.pem -out ~/cyphernode-ssl/certs/cert.pem -days 365 +docker build -t authapi api_auth_docker/. docker build -t proxycronimg cron_docker/. docker build -t btcproxyimg proxy_docker/. docker build -t pycoinimg pycoin_docker/. diff --git a/docker-compose.yml b/docker-compose.yml index e354c0f..3138639 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,8 +21,6 @@ services: env_file: - proxy_docker/env.properties image: btcproxyimg -# ports: -# - "8888:8888" volumes: # Variable substitutions don't work # Match with DB_PATH in proxy_docker/env.properties @@ -52,8 +50,6 @@ services: env_file: - pycoin_docker/env.properties image: pycoinimg -# ports: -# - "7777:7777" # deploy: # placement: # constraints: [node.hostname==dev] @@ -80,10 +76,8 @@ services: image: btcnode # ports: # - "18333:18333" -# - "18332:18332" # - "29000:29000" # - "8333:8333" -# - "8332:8332" volumes: - "~/btcdata:/.bitcoin" command: $USER bitcoind From b60cb3f1db7844f13e3fb7f510462cbb8dc8a052 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 8 Oct 2018 12:03:32 -0400 Subject: [PATCH 05/10] Un-tested client examples --- clients/javascript/cyphernode-client.js | 14 ++++++-------- clients/shell/cyphernode-client.sh | 1 - clients/shell/cyphernode.conf | 1 + 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/clients/javascript/cyphernode-client.js b/clients/javascript/cyphernode-client.js index c93506c..c8a008e 100644 --- a/clients/javascript/cyphernode-client.js +++ b/clients/javascript/cyphernode-client.js @@ -1,16 +1,16 @@ CyphernodeClient = function(is_prod) { this.baseURL = is_prod ? 'https://cyphernode:443' : 'https://cyphernode-dev:443' + this.h64 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==' + this.api_key = Meteor.settings.CYPHERNODE.api_key }; CyphernodeClient.prototype._post = function(url, postdata, cb) { let urlr = this.baseURL + url; - let h64 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==' let current = Math.round(new Date().getTime/1000) + 10 let p64 = btoa('{"id":"${id}","exp":' + current + '}') - let api_key = Meteor.settings.CYPHERNODE.api_key - let s = CryptoJS.HmacSHA256(p64, api_key).toString() - let token = h64 + '.' + p64 + '.' + s + let s = CryptoJS.HmacSHA256(p64, this.api_key).toString() + let token = this.h64 + '.' + p64 + '.' + s HTTP.post( urlr, @@ -27,12 +27,10 @@ CyphernodeClient.prototype._post = function(url, postdata, cb) { CyphernodeClient.prototype._get = function(url, cb) { let urlr = this.baseURL + url; - let h64 = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==' let current = Math.round(new Date().getTime/1000) + 10 let p64 = btoa('{"id":"${id}","exp":' + current + '}') - let api_key = Meteor.settings.CYPHERNODE.api_key - let s = CryptoJS.HmacSHA256(p64, api_key).toString() - let token = h64 + '.' + p64 + '.' + s + let s = CryptoJS.HmacSHA256(p64, this.api_key).toString() + let token = this.h64 + '.' + p64 + '.' + s HTTP.get(urlr, {headers: {'Authorization': 'Bearer ' + token}}, function (err, resp) { cb(err, resp.data) diff --git a/clients/shell/cyphernode-client.sh b/clients/shell/cyphernode-client.sh index 9d1312b..e904402 100644 --- a/clients/shell/cyphernode-client.sh +++ b/clients/shell/cyphernode-client.sh @@ -7,7 +7,6 @@ invoke_cyphernode() local action=${1} local post=${2} - local h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) local p64=$(echo "{\"id\":\"${id}\",\"exp\":$((`date +"%s"`+10))}" | base64) local s=$(echo "$h64.$p64" | openssl dgst -hmac "$key" -sha256 -r | cut -sd ' ' -f1) local token="$h64.$p64.$s" diff --git a/clients/shell/cyphernode.conf b/clients/shell/cyphernode.conf index d19bf3d..fc3c9ff 100644 --- a/clients/shell/cyphernode.conf +++ b/clients/shell/cyphernode.conf @@ -1,3 +1,4 @@ # Provided by cyphernode, see api_auth_docker/README.md +h64='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg==' id=001 key=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 From 1679ee89d1b4b680f70a0738e7febafa6b97ddfc Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 15 Oct 2018 15:44:59 -0400 Subject: [PATCH 06/10] Added tests and group authorization: watcher, spender, admin, internal --- api_auth_docker/Dockerfile | 2 + api_auth_docker/README.md | 10 +- api_auth_docker/api.properties | 29 ++++ api_auth_docker/auth.sh | 90 +++++++--- api_auth_docker/env.properties | 1 + api_auth_docker/keys.properties | 10 +- api_auth_docker/tests.sh | 282 ++++++++++++++++++++++++++++++++ api_auth_docker/trace.sh | 4 +- docker-compose.yml | 2 + 9 files changed, 396 insertions(+), 34 deletions(-) create mode 100644 api_auth_docker/api.properties create mode 100644 api_auth_docker/env.properties create mode 100644 api_auth_docker/tests.sh diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 441e0c1..09001ba 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -12,7 +12,9 @@ COPY auth.sh /etc/nginx/conf.d COPY default-ssl.conf /etc/nginx/conf.d/default.conf COPY entrypoint.sh entrypoint.sh COPY keys.properties /etc/nginx/conf.d +COPY api.properties /etc/nginx/conf.d COPY trace.sh /etc/nginx/conf.d +COPY tests.sh /etc/nginx/conf.d RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index 0a80c90..5f3740c 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -23,9 +23,13 @@ dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -ps -c 32 Put the key in keys.properties and keep it for the client. This is a secret key. keys.properties looks like this: ```property -#keyid=hex(key) -key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 -key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 +#group.id=hex(key) +watcher.001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 +watcher.002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 +spender.001=b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af +spender.002=bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd +admin.001=6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87 +admin.002=19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b ``` You can have multiple keys, but be aware that this container has **not** been built to support thousands of API keys! **Cyphernode should be used locally**, not publicly as a service. diff --git a/api_auth_docker/api.properties b/api_auth_docker/api.properties new file mode 100644 index 0000000..3ae021b --- /dev/null +++ b/api_auth_docker/api.properties @@ -0,0 +1,29 @@ + +# Watcher can: +action_watch=watcher +action_unwatch=watcher +action_getactivewatches=watcher +action_getbestblockhash=watcher +action_getbestblockinfo=watcher +action_getblockinfo=watcher +action_gettransaction=watcher +action_ln_getinfo=watcher +action_ln_create_invoice=watcher + +# Spender can do what the watcher can do plus: +action_getbalance=spender +action_getnewaddress=spender +action_spend=spender +action_addtobatch=spender +action_batchspend=spender +action_deriveindex=spender +action_derivepubpath=spender +action_ln_pay=spender +action_ln_newaddr=spender + +# Admin can do what the spender can do plus: + + +# Should be called from inside the Swarm: +action_conf=internal +action_executecallbacks=internal diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh index 3bac731..7634b7a 100644 --- a/api_auth_docker/auth.sh +++ b/api_auth_docker/auth.sh @@ -16,61 +16,99 @@ . ./trace.sh -verify() +verify_sign() { + local returncode + local header64=$(echo ${1} | cut -sd '.' -f1) local payload64=$(echo ${1} | cut -sd '.' -f2) local signature=$(echo ${1} | cut -sd '.' -f3) - trace "[verify] header64=${header64}" - trace "[verify] payload64=${payload64}" - trace "[verify] signature=${signature}" + trace "[verify_sign] header64=${header64}" + trace "[verify_sign] payload64=${payload64}" + trace "[verify_sign] signature=${signature}" local payload=$(echo ${payload64} | base64 -d) local exp=$(echo ${payload} | jq ".exp") local current=$(date +"%s") - trace "[verify] payload=${payload}" - trace "[verify] exp=${exp}" - trace "[verify] current=${current}" + trace "[verify_sign] payload=${payload}" + trace "[verify_sign] exp=${exp}" + trace "[verify_sign] current=${current}" if [ ${exp} -gt ${current} ]; then - trace "[verify] Not expired, let's validate signature" - local id=$(echo ${payload} | jq ".id" | tr -d '"') - trace "[verify] id=${id}" + trace "[verify_sign] Not expired, let's validate signature" + local id=$(echo ${payload} | jq ".id" | tr -d '"') + trace "[verify_sign] id=${id}" - # It is so much faster to include the keys here instead of grep'ing the file for key. - . ./keys.properties + # It is so much faster to include the keys here instead of grep'ing the file for key. + . ./keys.properties - local key - eval key='$key'$id - trace "[verify] key=${key}" + local key + eval key='$ukey_'$id + trace "[verify_sign] key=${key}" local comp_sign=$(echo "${header64}.${payload64}" | openssl dgst -hmac "${key}" -sha256 -r | cut -sd ' ' -f1) - trace "[verify] comp_sign=${comp_sign}" + trace "[verify_sign] comp_sign=${comp_sign}" - if [ "${comp_sign}" = "${signature}" ]; then - trace "[verify] Valid signature!" - echo -en "Status: 200 OK\r\n\r\n" - return - fi - trace "[verify] Invalid signature!" - return 1 + if [ "${comp_sign}" = "${signature}" ]; then + trace "[verify_sign] Valid signature!" + + verify_group ${id} + returncode=$? + + if [ "${returncode}" -eq 0 ]; then + echo -en "Status: 200 OK\r\n\r\n" + return + fi + trace "[verify_sign] Invalid group!" + return 1 + fi + trace "[verify_sign] Invalid signature!" + return 1 fi - trace "[verify] Expired!" - + trace "[verify_sign] Expired!" return 1 } +verify_group() +{ + trace "[verify_group] Verifying group..." + + local id=${1} + local action=${REQUEST_URI:1} + + # It is so much faster to include the keys here instead of grep'ing the file for key. + . ./api.properties + + local needed_group + local ugroups + + eval needed_group='$action_'${action} + trace "[verify_group] needed_group=${needed_group}" + + eval ugroups='$ugroups_'$id + trace "[verify_group] user groups=${ugroups}" + + case "${ugroups}" in + *${needed_group}*) trace "[verify_group] Access granted"; return 0 ;; + esac + + trace "[verify_group] Access NOT granted" + return 1 +} + + # $HTTP_AUTHORIZATION = Bearer +# If this is not found in header, we leave trace "[auth.sh] HTTP_AUTHORIZATION=${HTTP_AUTHORIZATION}" if [ "${HTTP_AUTHORIZATION:0:6}" = "Bearer" ]; then token="${HTTP_AUTHORIZATION:6}" if [ -n "$token" ]; then trace "[auth.sh] Valid format for authorization header" - verify "${token}" + verify_sign "${token}" [ "$?" -eq "0" ] && return fi fi diff --git a/api_auth_docker/env.properties b/api_auth_docker/env.properties new file mode 100644 index 0000000..aa5f04b --- /dev/null +++ b/api_auth_docker/env.properties @@ -0,0 +1 @@ +TRACING=1 diff --git a/api_auth_docker/keys.properties b/api_auth_docker/keys.properties index 781bbff..1a35c49 100644 --- a/api_auth_docker/keys.properties +++ b/api_auth_docker/keys.properties @@ -1,3 +1,7 @@ -#keyid=hex(key) -key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 -key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 +#kappiid="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact +kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="004";kapi_key="bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} diff --git a/api_auth_docker/tests.sh b/api_auth_docker/tests.sh new file mode 100644 index 0000000..c4d023c --- /dev/null +++ b/api_auth_docker/tests.sh @@ -0,0 +1,282 @@ +#!/bin/sh + +# We just want to test the authentication/authorization, not the actual called function +# Replace +# proxy_pass http://cyphernode:8888; +# by +# proxy_pass http://tests:8888; +# in /etc/nginx/conf.d/default.conf to run the tests + +test_expiration() +{ + # Let's test expiration: 1 second in payload, request 2 seconds later + + local id=${1} +# echo "id=${id}" + local k + eval k='$ukey_'$id + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64) + local s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + echo " Sleeping 2 seconds... " + sleep 2 + + local rc + echo -n " Testing expired request... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getblockinfo) + [ "${rc}" -ne "403" ] && return 10 + + return 0 +} + +test_authentication() +{ + # Let's test authentication/signature + + local id=${1} +# echo "id=${id}" + local k + eval k='$ukey_'$id + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + local rc + + echo -n " Testing good signature... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getblockinfo) + [ "${rc}" -eq "403" ] && return 20 + + token="$h64.$p64.a$s" + echo -n " Testing bad signature... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getblockinfo) + [ "${rc}" -ne "403" ] && return 30 + + return 0 +} + +test_authorization_watcher() +{ + # Let's test autorization + + local id=${1} +# echo "id=${id}" + local k + eval k='$ukey_'$id + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + local rc + + # Watcher can: + # watch + echo -n " Testing watch... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/watch) + [ "${rc}" -eq "403" ] && return 40 + + # unwatch + echo -n " Testing unwatch... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/unwatch) + [ "${rc}" -eq "403" ] && return 50 + + # getactivewatches + echo -n " Testing getactivewatches... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getactivewatches) + [ "${rc}" -eq "403" ] && return 60 + + # getbestblockhash + echo -n " Testing getbestblockhash... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash) + [ "${rc}" -eq "403" ] && return 70 + + # getbestblockinfo + echo -n " Testing getbestblockinfo... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getbestblockinfo) + [ "${rc}" -eq "403" ] && return 80 + + # getblockinfo + echo -n " Testing getblockinfo... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getblockinfo) + [ "${rc}" -eq "403" ] && return 90 + + # gettransaction + echo -n " Testing gettransaction... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/gettransaction) + [ "${rc}" -eq "403" ] && return 100 + + # ln_getinfo + echo -n " Testing ln_getinfo... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ln_getinfo) + [ "${rc}" -eq "403" ] && return 110 + + # ln_create_invoice + echo -n " Testing ln_create_invoice... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ln_create_invoice) + [ "${rc}" -eq "403" ] && return 120 + + return 0 +} + +test_authorization_spender() +{ + # Let's test autorization + + local id=${1} +# echo "id=${id}" + local is_spender=${2} +# echo "is_spender=${is_spender}" + local k + eval k='$ukey_'$id + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + local rc + + # Spender can do what the watcher can do, plus: + # getbalance + echo -n " Testing getbalance... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getbalance) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 130 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 135 + + # getnewaddress + echo -n " Testing getnewaddress... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/getnewaddress) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 140 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 145 + + # spend + echo -n " Testing spend... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/spend) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 150 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 155 + + # addtobatch + echo -n " Testing addtobatch... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/addtobatch) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 160 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 165 + + # batchspend + echo -n " Testing batchspend... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/batchspend) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 170 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 175 + + # deriveindex + echo -n " Testing deriveindex... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/deriveindex) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 180 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 185 + + # derivepubpath + echo -n " Testing derivepubpath... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/derivepubpath) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 190 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 195 + + # ln_pay + echo -n " Testing ln_pay... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ln_pay) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 200 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 205 + + # ln_newaddr + echo -n " Testing ln_newaddr... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/ln_newaddr) + [ ${is_spender} = true ] && [ "${rc}" -eq "403" ] && return 210 + [ ${is_spender} = false ] && [ "${rc}" -ne "403" ] && return 215 + + return 0 +} + +test_authorization_internal() +{ + # Let's test autorization + + local id=${1} +# echo "id=${id}" + local k + eval k='$ukey_'$id + + local p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64) + local s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1) + local token="$h64.$p64.$s" + + local rc + + # Should be called from inside the Swarm: + # conf + echo -n " Testing conf... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/conf) + [ "${rc}" -ne "403" ] && return 220 + + # executecallbacks + echo -n " Testing executecallbacks... " + rc=$(time -f "%E" curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $token" -k https://localhost/executecallbacks) + [ "${rc}" -ne "403" ] && return 230 + + return 0 +} + +kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64) + +# Let's test expiration: 1 second in payload, request 2 seconds later + +echo 'test_expiration "001"' +test_expiration "001" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_expiration "003"' +test_expiration "003" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_expiration "005"' +test_expiration "005" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc + +# Let's test authentication/signature + +echo 'test_authentication "001"' +test_authentication "001" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authentication "003"' +test_authentication "003" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authentication "005"' +test_authentication "005" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc + +# Let's test autorization for watcher actions + +echo 'test_authorization_watcher "001"' +test_authorization_watcher "001" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authorization_watcher "003"' +test_authorization_watcher "003" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authorization_watcher "005"' +test_authorization_watcher "005" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc + +# Let's test autorization for spender actions + +echo 'test_authorization_spender "001" false' +test_authorization_spender "001" false ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authorization_spender "003" true' +test_authorization_spender "003" true ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authorization_spender "005" true' +test_authorization_spender "005" true ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc + +# Let's test autorization for admin actions + +#test_authorization_admin "001" +#test_authorization_admin "003" +#test_authorization_admin "005" + +# Let's test autorization for internal actions +echo 'test_authorization_internal "001"' +test_authorization_internal "001" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authorization_internal "003"' +test_authorization_internal "003" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc +echo 'test_authorization_internal "005"' +test_authorization_internal "005" ; rc=$? ; [ $rc -ne 0 ] && echo $rc && return $rc diff --git a/api_auth_docker/trace.sh b/api_auth_docker/trace.sh index 34a18df..c2c46ed 100644 --- a/api_auth_docker/trace.sh +++ b/api_auth_docker/trace.sh @@ -3,13 +3,13 @@ trace() { if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" > /dev/stderr + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] ${1}" 1>&2 fi } trace_rc() { if [ -n "${TRACING}" ]; then - echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" > /dev/stderr + echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] Last return code: ${1}" 1>&2 fi } diff --git a/docker-compose.yml b/docker-compose.yml index 3138639..257737c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,8 @@ version: "3" services: authapi: # HTTP authentication API gate + env_file: + - api_auth_docker/env.properties image: authapi ports: - "80:80" From 53e41cb77aa28f1e6a7c11475c18886ad914dd0b Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 15 Oct 2018 16:21:50 -0400 Subject: [PATCH 07/10] Adjusted README doc for authapi --- api_auth_docker/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index 5f3740c..aab527d 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -20,16 +20,16 @@ Linux: dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -ps -c 32 ``` -Put the key in keys.properties and keep it for the client. This is a secret key. keys.properties looks like this: +Put the id, key and groups in keys.properties and give the id and key to the client. The key is a secret. keys.properties looks like this: ```property -#group.id=hex(key) -watcher.001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36 -watcher.002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83 -spender.001=b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af -spender.002=bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd -admin.001=6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87 -admin.002=19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b +#kappiid="id";kapi_key="key";kapi_groups="group1,group2";leave the rest intact +kapi_id="001";kapi_key="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="002";kapi_key="50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83";kapi_groups="watcher";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="003";kapi_key="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="004";kapi_key="bb0458b705e774c0c9622efaccfe573aa30c82f62386d9435f04e9727cdc26fd";kapi_groups="watcher,spender";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="005";kapi_key="6c009201b123e8c24c6b74590de28c0c96f3287e88cac9460a2173a53d73fb87";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} +kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc88971059c69da4b";kapi_groups="watcher,spender,admin";eval ugroups_${kapi_id}=${kapi_groups};eval ukey_${kapi_id}=${kapi_key} ``` You can have multiple keys, but be aware that this container has **not** been built to support thousands of API keys! **Cyphernode should be used locally**, not publicly as a service. From 805291f624287edde3ab8557d39a1870aec04770 Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 15 Oct 2018 17:05:21 -0400 Subject: [PATCH 08/10] Added more docs for new groups authorization in API --- doc/INSTALL-MANUAL-STEPS.md | 6 ++++++ doc/INSTALL.md | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index a95161c..34fe30e 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -38,6 +38,7 @@ vi proxy_docker/env.properties ```shell vi cron_docker/env.properties vi pycoin_docker/env.properties +vi api_auth_docker/env.properties ``` ## Create cyphernode user, create proxy DB folder and build images @@ -133,6 +134,11 @@ sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f ## Test the deployment +```shell +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash +id="003";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbalance +``` + ```shell echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - echo "GET /getbalance" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - diff --git a/doc/INSTALL.md b/doc/INSTALL.md index 17f951f..fe816c1 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -51,6 +51,7 @@ debian@dev:~/dev/Cyphernode$ docker network connect cyphernodenet yourappcontain debian@dev:~/dev/Cyphernode$ vi proxy_docker/env.properties debian@dev:~/dev/Cyphernode$ vi cron_docker/env.properties debian@dev:~/dev/Cyphernode$ vi pycoin_docker/env.properties +debian@dev:~/dev/Cyphernode$ vi api_auth_docker/env.properties ``` ### Build cron image @@ -110,7 +111,14 @@ pi@SP-BTC01:~ $ docker swarm join --token SWMTKN-1-2pxouynn9g8si42e8g9ujwy0v9po4 pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode ``` -## Test deployment (from any host) +## Test deployment from outside of the Swarm + +```shell +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash +id="003";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbalance +``` + +## Test deployment from any host of the swarm ```shell echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 - From 2a7ca276002b0639593b7b180c04a656752308dd Mon Sep 17 00:00:00 2001 From: kexkey Date: Mon, 15 Oct 2018 20:10:37 -0400 Subject: [PATCH 09/10] Closing port 80 by default and 1 second expiration is to low in examples --- doc/INSTALL-MANUAL-STEPS.md | 4 ++-- doc/INSTALL.md | 4 ++-- docker-compose.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/INSTALL-MANUAL-STEPS.md b/doc/INSTALL-MANUAL-STEPS.md index 34fe30e..9076a10 100644 --- a/doc/INSTALL-MANUAL-STEPS.md +++ b/doc/INSTALL-MANUAL-STEPS.md @@ -135,8 +135,8 @@ sudo find ~/btcdata -type d -exec chmod 2775 {} \; ; sudo find ~/btcdata -type f ## Test the deployment ```shell -id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash -id="003";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbalance +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash +id="003";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbalance ``` ```shell diff --git a/doc/INSTALL.md b/doc/INSTALL.md index fe816c1..e30037b 100644 --- a/doc/INSTALL.md +++ b/doc/INSTALL.md @@ -114,8 +114,8 @@ pi@SP-BTC01:~ $ docker network connect cyphernodenet btcnode ## Test deployment from outside of the Swarm ```shell -id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash -id="003";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+1))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbalance +id="001";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbestblockhash +id="003";h64=$(echo "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" | base64);p64=$(echo "{\"id\":\"$id\",\"exp\":$((`date +"%s"`+10))}" | base64);k="b9b8d527a1a27af2ad1697db3521f883760c342fc386dbc42c4efbb1a4d5e0af";s=$(echo "$h64.$p64" | openssl dgst -hmac "$k" -sha256 -r | cut -sd ' ' -f1);token="$h64.$p64.$s";curl -H "Authorization: Bearer $token" -k https://localhost/getbalance ``` ## Test deployment from any host of the swarm diff --git a/docker-compose.yml b/docker-compose.yml index 257737c..d5a652e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: - api_auth_docker/env.properties image: authapi ports: - - "80:80" +# - "80:80" - "443:443" volumes: - "~/cyphernode-ssl/certs:/etc/ssl/certs" From 208d61361c641a7c8fcd77b29fc1c6190ea7ee1b Mon Sep 17 00:00:00 2001 From: kexkey Date: Tue, 16 Oct 2018 12:55:02 -0400 Subject: [PATCH 10/10] Started IP whitelist, but defunct because of docker swarm obfuscating real IP --- api_auth_docker/Dockerfile | 1 + api_auth_docker/README.md | 13 +++++++++++++ api_auth_docker/auth.sh | 17 ++++++++++++++++- api_auth_docker/default-ssl.conf | 2 ++ api_auth_docker/default.conf | 2 ++ api_auth_docker/ip-whitelist.conf | 8 ++++++++ 6 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 api_auth_docker/ip-whitelist.conf diff --git a/api_auth_docker/Dockerfile b/api_auth_docker/Dockerfile index 09001ba..c1db73b 100644 --- a/api_auth_docker/Dockerfile +++ b/api_auth_docker/Dockerfile @@ -15,6 +15,7 @@ COPY keys.properties /etc/nginx/conf.d COPY api.properties /etc/nginx/conf.d COPY trace.sh /etc/nginx/conf.d COPY tests.sh /etc/nginx/conf.d +COPY ip-whitelist.conf /etc/nginx/conf.d RUN chmod +x /etc/nginx/conf.d/auth.sh entrypoint.sh diff --git a/api_auth_docker/README.md b/api_auth_docker/README.md index aab527d..ddb6a77 100644 --- a/api_auth_docker/README.md +++ b/api_auth_docker/README.md @@ -34,6 +34,19 @@ kapi_id="006";kapi_key="19e121b698014fac638f772c4ff5775a738856bf6cbdef0dc8897105 You can have multiple keys, but be aware that this container has **not** been built to support thousands of API keys! **Cyphernode should be used locally**, not publicly as a service. +## IP Addresses Whitelist (**do not use for now**) +**Docker Swarm obfuscates real client IP, this feature is not ready for now** + +You can have an IP whitelist policy, denying everything except the explicit IP addresses you need. Edit ip-whitelist.conf file: + +```conf +# Leave commented if you don't want to use IP whitelist + +# List of white listed IP addresses... +#allow 45.56.67.78; +#deny all; +``` + ## SSL If you already have your certificates and keystores infra, you already know what to do and your can skip this section. Put your files in the bound volume (~/cyphernode-ssl/ see volume path in docker-compose.yml). diff --git a/api_auth_docker/auth.sh b/api_auth_docker/auth.sh index 7634b7a..ac2bf0d 100644 --- a/api_auth_docker/auth.sh +++ b/api_auth_docker/auth.sh @@ -39,7 +39,14 @@ verify_sign() if [ ${exp} -gt ${current} ]; then trace "[verify_sign] Not expired, let's validate signature" local id=$(echo ${payload} | jq ".id" | tr -d '"') - trace "[verify_sign] id=${id}" + trace "[verify_sign] id=${id}" + + # Check for code injection + # id will usually be an int, but could be alphanum... nothing else + if ! [[ $id =~ '^[A-Za-z0-9]$']]; then + trace "[verify_sign] Potential code injection, exiting" + return 1 + fi # It is so much faster to include the keys here instead of grep'ing the file for key. . ./keys.properties @@ -78,6 +85,14 @@ verify_group() local id=${1} local action=${REQUEST_URI:1} + trace "[verify_group] action=${action}" + + # Check for code injection + # action could be alphanum... nothing else + if ! [[ $action =~ '^[A-Za-z]$']]; then + trace "[verify_group] Potential code injection, exiting" + return 1 + fi # It is so much faster to include the keys here instead of grep'ing the file for key. . ./api.properties diff --git a/api_auth_docker/default-ssl.conf b/api_auth_docker/default-ssl.conf index 07735d4..36edeeb 100644 --- a/api_auth_docker/default-ssl.conf +++ b/api_auth_docker/default-ssl.conf @@ -2,6 +2,8 @@ server { listen 443 ssl; server_name localhost; + include /etc/nginx/conf.d/ip-whitelist.conf; + ssl_certificate /etc/ssl/certs/cert.pem; ssl_certificate_key /etc/ssl/private/key.pem; diff --git a/api_auth_docker/default.conf b/api_auth_docker/default.conf index dd4b951..fca3c1b 100644 --- a/api_auth_docker/default.conf +++ b/api_auth_docker/default.conf @@ -2,6 +2,8 @@ server { listen 80; server_name localhost; + include /etc/nginx/conf.d/ip-whitelist.conf; + location / { auth_request /auth; proxy_pass http://cyphernode:8888; diff --git a/api_auth_docker/ip-whitelist.conf b/api_auth_docker/ip-whitelist.conf new file mode 100644 index 0000000..aa6e2c4 --- /dev/null +++ b/api_auth_docker/ip-whitelist.conf @@ -0,0 +1,8 @@ +# Leave commented if you don't want to use IP whitelist + +#real_ip_header X-Forwarded-For; +#set_real_ip_from 0.0.0.0/0; + +# List of white listed IP addresses... +#allow 45.56.67.78; +#deny all;