diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 35b292d..158ae76 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -45,6 +45,7 @@ jobs: - name: Test with filesystem backend run: | cp test_keys/* ../ + chmod 600 ../server_private_key.key cd .. python -m unittest -v env: @@ -54,6 +55,7 @@ jobs: - name: Test with Redis backend run: | cp test_keys/* ../ + chmod 600 ../server_private_key.key cd .. python -m unittest -v env: diff --git a/README.md b/README.md index 38778f9..5b0d247 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,19 @@ Green. `PYTHONPATH=.. python -m blind_pin_server.generateserverkey` +## Securing the server key + +Anyone who can read `server_private_key.key` can impersonate this server, so it is created +mode `0600` and the server refuses to start if it is readable by anyone else. + +In the container it is read by `www-data`: + +`sudo chown www-data:www-data server_private_key.key` + +`sudo chmod 600 server_private_key.key` + +Mount it read-only (`:ro`, as below) so the container can't alter it. + ## Build the docker image `docker build -f Dockerfile . -t dockerized_pinserver` @@ -28,4 +41,4 @@ Green. ## Run the docker image (requires the previous steps) -`docker run -v $PWD/server_private_key.key:/server_private_key.key -v $PWD/pinsdir:/pins -p 8096:8096 dockerized_pinserver` +`docker run -v $PWD/server_private_key.key:/server_private_key.key:ro -v $PWD/pinsdir:/pins -p 8096:8096 dockerized_pinserver` diff --git a/server.py b/server.py index bb1a0a6..a83dbd5 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,7 @@ import time from hmac import compare_digest import os +import stat from .lib import decrypt, encrypt, E_ECDH from wallycore import ec_private_key_verify, ec_sig_from_bytes, sha256, \ hmac_sha256, EC_FLAG_ECDSA, ec_private_key_bip341_tweak, ec_public_key_from_private_key @@ -20,7 +21,11 @@ def generate_server_key_pair(cls): private_key, public_key = cls.generate_ec_key_pair() - with open(cls.STATIC_SERVER_PRIVATE_KEY_FILE, 'wb') as f: + # O_EXCL: fail if it exists, S_IRUSR|S_IWUSR: chmod 600 + fd = os.open(cls.STATIC_SERVER_PRIVATE_KEY_FILE, + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + stat.S_IRUSR | stat.S_IWUSR) + with os.fdopen(fd, 'wb') as f: f.write(private_key) with open(cls.STATIC_SERVER_PUBLIC_KEY_FILE, 'wb') as f: @@ -32,6 +37,11 @@ def generate_server_key_pair(cls): @classmethod def load_private_key(cls): if not cls.STATIC_SERVER_PRIVATE_KEY: + mode = stat.S_IMODE(os.stat(cls.STATIC_SERVER_PRIVATE_KEY_FILE).st_mode) + if mode & (stat.S_IRWXG | stat.S_IRWXO): + raise Exception(f'{cls.STATIC_SERVER_PRIVATE_KEY_FILE} is mode {mode:04o}, ' + 'must not be readable by other users') + with open(cls.STATIC_SERVER_PRIVATE_KEY_FILE, 'rb') as f: cls.STATIC_SERVER_PRIVATE_KEY = f.read() ec_private_key_verify(cls.STATIC_SERVER_PRIVATE_KEY)