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"