Skip to content

Commit

Permalink
Merge pull request #1 from harryttd/kms
Browse files Browse the repository at this point in the history
Add AWS KMS signer and file based ratchet
  • Loading branch information
harryttd committed Mar 24, 2023
2 parents 299ed4d + a0b5702 commit 06cbd01
Show file tree
Hide file tree
Showing 19 changed files with 932 additions and 220 deletions.
6 changes: 6 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
./src/__pycache__
.git
.github/**
.gitignore
.idea
docker-compose.yaml
LICENSE.md
Makefile
README.md
test
tezos-kms
venv
77 changes: 77 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: CI

on:
workflow_dispatch:

push:
# Trigger CI on all branch pushes but...
branches:
- "**"
# don't double trigger on new tag push when creating release. Should only
# trigger once for the release.
tags-ignore:
- "*.*.*"
paths-ignore:
- README.md
- LICENSE.md
- MAKEFILE
- .gitignore
- docker-compose*

release:
types: [created]

jobs:
publish-to-ghcr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 1
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master

- name: Login to registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-single-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-single-buildx
- name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: ghcr.io/${{ github.repository_owner }}/tacoinfra-remote-signer
tags: |
type=ref,event=branch
type=ref,event=pr
type=match,pattern=([0-9]+\.[0-9]+\.[0-9]+),group=1
- name: Push ${{ matrix.container }} container to GHCR
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new
context: .
file: Dockerfile
labels: ${{ steps.meta.outputs.labels }}
push: true
tags: ${{ steps.meta.outputs.tags }}

# Temp fix
# https://github.com/docker/build-push-action/issues/252
# https://github.com/moby/buildkit/issues/1896
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
src/bitcoin
keys.json
remote-signer.log

testing-files
79 changes: 59 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
FROM amazonlinux:1
FROM python:3.9-slim@sha256:3e45d072301981add6ab1d376394edc4eb0ec2afb70e7ffcbb3113eb432ab709

RUN \
yum install -y wget aws-cli python36 python36-devel git gcc && \
easy_install-3.6 pip
RUN mkdir -p /app
WORKDIR /app

COPY requirements.txt .

RUN apt-get update \
&& apt-get install -y git gcc g++ make python3-dev swig \
&& apt-get install -y jq awscli curl \
&& apt-get install -y libsodium23 libsecp256k1-0 libgmp10 \
&& apt-get install -y libsodium-dev libsecp256k1-dev libgmp-dev \
&& pip --no-cache install -r ./requirements.txt \
&& cd /tmp \
&& git clone https://github.com/tacoinfra/libhsm \
&& cd libhsm/build \
&& ./build_libhsm \
&& cp libhsm.so /usr/lib/x86_64-linux-gnu/libhsm.so \
&& cd / \
&& rm -rf /tmp/libhsm \
&& apt-get purge -y git gcc g++ make python3-dev swig \
&& apt-get purge -y libsodium-dev libsecp256k1-dev libgmp-dev \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt /var/cache/apt /root/.cache \
&& rm -rf __pycache__
#
# We do not install the dependencies for the following packages because
# we use only a subset of their functionality and the dependencies are
# not necesary for us.
#
# XXXrcd: We should fetch a particular version of these libraries.
#
# XXXrcd: in future we might only install the .so because we only use
# the "configure" command which just manipulates a little JSON.

RUN TOP=https://s3.amazonaws.com/cloudhsmv2-software/CloudHsmClient \
VER=EL6 \
CLIENT=cloudhsm-client-3.1.0-3.el6.x86_64.rpm \
PKCS11=cloudhsm-client-pkcs11-3.1.0-3.el6.x86_64.rpm; \
VER=Bionic \
PKCS11=cloudhsm-pkcs11_latest_u18.04_amd64.deb; \
\
set -e; \
\
for i in $CLIENT $PKCS11; do \
wget "$TOP/$VER/$i"; \
yum install -y "$i"; \
rm -f "$i"; \
done
curl -s -o "$PKCS11" "$TOP/$VER/$PKCS11"; \
dpkg -i --force-depends "$PKCS11"; \
rm -f "$PKCS11"

ARG FLASK_ENV=production
ENV FLASK_ENV=$FLASK_ENV

RUN groupadd -r -g 999 remotesigner && \
useradd -r -u 999 -g remotesigner remotesigner

COPY requirements.txt /
RUN pip3 install -r /requirements.txt && \
/opt/cloudhsm/bin/configure -a hsm.internal && \
yum clean all
COPY src ./src
COPY entrypoint.sh ./
COPY hsm-remote-signer.sh ./
COPY signer.py ./

COPY src/. /src/
RUN chmod 755 /src/start-remote-signer.sh
# Make files un-writeable
RUN rm -rf /usr/local/bin/pip \
&& rm requirements.txt \
&& chown -R remotesigner:remotesigner . \
&& chmod 540 entrypoint.sh hsm-remote-signer.sh signer.py \
&& chmod -R 540 src \
&& chmod 770 .

COPY signer.py /
USER 999

ENTRYPOINT ["/src/start-remote-signer.sh"]
ENTRYPOINT ["./entrypoint.sh"]
22 changes: 22 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

set -xe

CMD="$1"
shift

case "$CMD" in
hsm) exec hsm-remote-signer.sh "$@" ;;
kms) if ! python3 signer.py "kms"; then
echo "Failed to start kms signer."
exit 1
fi
esac

echo "ERROR: could not find \"$CMD\"."
echo
echo "Valid options are:"
echo " hsm"
echo " kms"

exit 1
1 change: 1 addition & 0 deletions src/start-remote-signer.sh → hsm-remote-signer.sh
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# "hsm_username": "${HSMUser}",
# "hsm_slot": ${HSMSlot},
# "hsm_lib": "${HSMLibFile}",
# "node_addr": "${NodeAddress}",
# "keys": {
# "${HSMPubKey}": {
# "hash": "${HSMPubKeyHash}",
Expand Down
31 changes: 16 additions & 15 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
-e git+https://github.com/tacoinfra/pybitcointools.git@aeb0a2bbb8bbfe421432d776c649650eaeb882a5#egg=bitcoin
boto3==1.9.142
botocore==1.12.142
Click==7.0
docutils==0.14
pytezos==3.3.0
boto3==1.20.49
botocore==1.23.49
Click==8.0.3
docutils==0.18.1
dyndbmutex==0.4.0
Flask==1.0.2
itsdangerous==1.1.0
Jinja2==2.11.3
jmespath==0.9.4
MarkupSafe==1.1.1
Flask==2.0.2
itsdangerous==2.0.1
Jinja2==3.0.3
jmespath==0.10.0
MarkupSafe==2.0.1
PyKCS11==1.5.10
py-hsm==2.5.0
pyblake2==1.1.2
python-dateutil==2.8.0
s3transfer==0.2.0
six==1.12.0
urllib3==1.24.3
python-dateutil==2.8.2
s3transfer==0.5.1
six==1.16.0
urllib3==1.26.8
uuid==1.30
Werkzeug==0.15.3
Werkzeug==2.0.2
103 changes: 59 additions & 44 deletions signer.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
#!/usr/bin/env python3
import logging
import sys
from os import environ, path

from flask import Flask, request, Response, json, jsonify
import boto3
from flask import Flask, Response, json, jsonify, request
from werkzeug.exceptions import HTTPException
from src.sigreq import SignatureReq
from src.validatesigner import ValidateSigner

from src.ddbchainratchet import DDBChainRatchet
from src.file_ratchet import FileRatchet
from src.hsmsigner import HsmSigner
from os import path, environ
import logging

def logreq(sigreq, msg):
if sigreq != None:
logging.info(f"Request: {sigreq.get_logstr()}:{msg}")

logging.basicConfig(filename='./remote-signer.log',
format='%(asctime)s %(threadName)s %(message)s',
level=logging.INFO)
from src.kms_signer import KmsSigner
from src.sigreq import SignatureReq
from src.validatesigner import ValidateSigner

app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)

#
# The config file (keys.json) has a structure:
Expand All @@ -39,49 +38,67 @@ def logreq(sigreq, msg):
# 'voting': ['pass'], # a list of permitted votes
# }
# }
config = {}

if path.isfile('keys.json'):
with open('keys.json', 'r') as myfile:
json_blob = myfile.read().replace('\n', '')
config = json.loads(json_blob)
keys_path = "./signer-config/keys.json"
if path.isfile(keys_path):
with open(keys_path, "r") as myfile:
json_blob = myfile.read().replace("\n", "")
config["keys"] = json.loads(json_blob)
logging.info(f"Loaded config contains: {json.dumps(config, indent=2)}")

#
# We keep the ChainRatchet, HSM, and ValidateSigner outside sign()
# so that they persist.
try:
signer_type = sys.argv[1]
except:
signer_type = None

SIGNER = None
REGION = environ.get("REGION") or environ.get("AWS_REGION")
if not REGION:
raise Exception("No REGION or AWS_REGION env var set.")

if signer_type == "kms":
client = boto3.client("kms", region_name=REGION)
SIGNER = KmsSigner(client, ratchet=FileRatchet())
elif signer_type == "hsm":
ratchet = DDBChainRatchet(REGION, environ["DDB_TABLE"])
hsm_signer = HsmSigner(config)
SIGNER = ValidateSigner(config, ratchet=ratchet, subsigner=hsm_signer)
else:
raise Exception("Either 'hsm' or 'kms' must be provided as the signer type.")


def logreq(sigreq, msg):
if sigreq != None:
logging.info(f"Request: {sigreq.get_logstr()}:{msg}")

cr = DDBChainRatchet(environ['REGION'], environ['DDB_TABLE'])
hsm = HsmSigner(config)
rs = ValidateSigner(config, ratchet=cr, subsigner=hsm)

@app.route('/keys/<key_hash>', methods=['GET', 'POST'])
@app.route("/keys/<key_hash>", methods=["GET", "POST"])
def sign(key_hash):
response = None
sigreq = None
try:
if key_hash in config['keys']:
key = config['keys'][key_hash]
if request.method == 'POST':
if key_hash in config["keys"]:
key_data = config["keys"][key_hash]
if request.method == "POST":
sigreq = SignatureReq(request.get_json(force=True))
response = jsonify({
'signature': rs.sign(key['private_handle'], sigreq)
})
response = jsonify(
{"signature": SIGNER.sign(sigreq, key_data, key_hash)}
)
else:
response = jsonify({ 'public_key': key['public_key'] })
response = jsonify({"public_key": key_data["public_key"]})
else:
logging.warning(f"Couldn't find key {key_hash}")
response = Response('Key not found', status=404)
response = Response("Key not found", status=404)
except HTTPException as e:
logging.error(e)
logreq(sigreq, "Failed")
raise
except Exception as e:
data = {'error': str(e)}
logging.error(f'Exception thrown during request: {str(e)}')
data = {"error": str(e)}
logging.error(f"Exception thrown during request:", exc_info=True)
response = app.response_class(
response=json.dumps(data),
status=500,
mimetype='application/json'
response=json.dumps(data), status=500, mimetype="application/json"
)
logreq(sigreq, "Failed")
return response
Expand All @@ -91,14 +108,12 @@ def sign(key_hash):
return response


@app.route('/authorized_keys', methods=['GET'])
@app.route("/authorized_keys", methods=["GET"])
def authorized_keys():
return app.response_class(
response=json.dumps({}),
status=200,
mimetype='application/json'
response=json.dumps({}), status=200, mimetype="application/json"
)


if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Empty file removed src/__init__.py
Empty file.
Loading

0 comments on commit 06cbd01

Please sign in to comment.