Authenticated HTTP API - first pass

This commit is contained in:
kexkey
2018-10-04 13:09:00 -04:00
parent 926f0a000e
commit e6edd5b0e6
11 changed files with 316 additions and 0 deletions

View File

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

120
api_auth_docker/README.md Normal file
View File

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

78
api_auth_docker/auth.sh Normal file
View File

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

View File

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

View File

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

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,3 @@
#keyid=hex(key)
key001=2df1eeea370eacdc5cf7e96c2d82140d1568079a5d4d87006ec8718a98883b36
key002=50c5e483b80964595508f214229b014aa6c013594d57d38bcb841093a39f1d83

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}" > /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
}

View File

View File

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

View File

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