Merge pull request #19 from SatoshiPortal/features/authapi

Features/authapi
This commit is contained in:
kexkey
2018-10-16 13:27:45 -04:00
committed by GitHub
19 changed files with 910 additions and 7 deletions

View File

@@ -0,0 +1,22 @@
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 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
ENTRYPOINT ["./entrypoint.sh"]

147
api_auth_docker/README.md Normal file
View File

@@ -0,0 +1,147 @@
# 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.
## 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
```
Linux:
```shell
dd if=/dev/urandom bs=32 count=1 2> /dev/null | xxd -ps -c 32
```
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
#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.
## 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).
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:
```shell
Authorization: Bearer <token>
```
...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" -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" -k 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"
```

View File

@@ -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

131
api_auth_docker/auth.sh Normal file
View File

@@ -0,0 +1,131 @@
#!/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_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_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_sign] payload=${payload}"
trace "[verify_sign] exp=${exp}"
trace "[verify_sign] current=${current}"
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}"
# 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
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_sign] comp_sign=${comp_sign}"
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_sign] Expired!"
return 1
}
verify_group()
{
trace "[verify_group] Verifying 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
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 <token>
# 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_sign "${token}"
[ "$?" -eq "0" ] && return
fi
fi
echo -en "Status: 403 Forbidden\r\n\r\n"

View File

@@ -0,0 +1,31 @@
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;
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;
}
}

View File

@@ -0,0 +1,28 @@
server {
listen 80;
server_name localhost;
include /etc/nginx/conf.d/ip-whitelist.conf;
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;
}
}

View File

@@ -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;"

View File

@@ -0,0 +1 @@
TRACING=1

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
#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}

282
api_auth_docker/tests.sh Normal file
View File

@@ -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

15
api_auth_docker/trace.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
trace()
{
if [ -n "${TRACING}" ]; then
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}" 1>&2
fi
}

7
clients/README.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,75 @@
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 current = Math.round(new Date().getTime/1000) + 10
let p64 = btoa('{"id":"${id}","exp":' + current + '}')
let s = CryptoJS.HmacSHA256(p64, this.api_key).toString()
let token = this.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 current = Math.round(new Date().getTime/1000) + 10
let p64 = btoa('{"id":"${id}","exp":' + current + '}')
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)
})
};
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);
};

View File

@@ -0,0 +1,76 @@
#!/bin/sh
. .cyphernode.conf
invoke_cyphernode()
{
local action=${1}
local post=${2}
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")
}

View File

@@ -0,0 +1,4 @@
# Provided by cyphernode, see api_auth_docker/README.md
h64='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Cg=='
id=001
key=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36

View File

@@ -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
@@ -45,6 +46,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/.
@@ -130,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"`+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
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 -

View File

@@ -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
@@ -73,12 +74,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 +93,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
@@ -103,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"`+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
```shell
echo "GET /getbestblockinfo" | docker run --rm -i --network=cyphernodenet alpine nc cyphernode:8888 -

View File

@@ -1,13 +1,28 @@
version: "3"
services:
authapi:
# HTTP authentication API gate
env_file:
- api_auth_docker/env.properties
image: authapi
ports:
# - "80:80"
- "443:443"
volumes:
- "~/cyphernode-ssl/certs:/etc/ssl/certs"
- "~/cyphernode-ssl/private:/etc/ssl/private"
# deploy:
# placement:
# constraints: [node.hostname==dev]
networks:
- cyphernodenet
cyphernode:
# Bitcoin Mini Proxy
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
@@ -37,8 +52,6 @@ services:
env_file:
- pycoin_docker/env.properties
image: pycoinimg
# ports:
# - "7777:7777"
# deploy:
# placement:
# constraints: [node.hostname==dev]
@@ -65,10 +78,8 @@ services:
image: btcnode
# ports:
# - "18333:18333"
# - "18332:18332"
# - "29000:29000"
# - "8333:8333"
# - "8332:8332"
volumes:
- "~/btcdata:/.bitcoin"
command: $USER bitcoind