From 42181e6c2430e507e94d80fb963f5b4de6bf03dc Mon Sep 17 00:00:00 2001 From: Minos Galanakis Date: Tue, 9 Jul 2024 14:24:15 +0100 Subject: [PATCH 1/3] Added firmware_signer tool. This pr introduces a simple web-app to sign firmware-files using gpg. Signed-off-by: Minos Galanakis --- tools/fw_signer/firmware_signer.py | 198 ++++++++++++++++++++ tools/fw_signer/fw_sign_requirements.txt | 3 + tools/fw_signer/static/style.css | 15 ++ tools/fw_signer/templates/index.html | 41 ++++ tools/fw_signer/templates/signed.html | 30 +++ tools/fw_signer/templates/verification.html | 21 +++ 6 files changed, 308 insertions(+) create mode 100644 tools/fw_signer/firmware_signer.py create mode 100644 tools/fw_signer/fw_sign_requirements.txt create mode 100644 tools/fw_signer/static/style.css create mode 100644 tools/fw_signer/templates/index.html create mode 100644 tools/fw_signer/templates/signed.html create mode 100644 tools/fw_signer/templates/verification.html diff --git a/tools/fw_signer/firmware_signer.py b/tools/fw_signer/firmware_signer.py new file mode 100644 index 00000000..abcfff1b --- /dev/null +++ b/tools/fw_signer/firmware_signer.py @@ -0,0 +1,198 @@ +"""Create a simple firmware sign and verify server.""" + +# Copyright The Mbed TLS Contributors +# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later +# + +# The script assumes that gpg agent caching is +# disabled. The password provided by the user is used to confirm +# that the user is authorised to sign the firmware. + +# Please configure gpg-agent with `max-cache-ttl 0` when deploying. + +import flask +import random +import os +import string +import shlex +import shutil +import subprocess +import pathlib + +from typing import Text , Tuple + + +################ /* Configuration Parameters ################# +app = flask.Flask(__name__) + +# Allow the host to set the IP for the server3 +server_ip = "0.0.0.0" # Will launch the server but dl links won't work. +server_port = "5000" # Port to bind to. +cleanup_on_startup = True # Cleanup the temporary directory on launch. + +################ Configuration Parameters */ ################# + +# ENV overrides +if "MBEDTLS_FW_SIGN_SERVER_IP" in os.environ: + server_ip = os.environ["MBEDTLS_FW_SIGN_SERVER_IP"] + +if "MBEDTLS_FW_SIGN_SERVER_PORT" in os.environ: + server_port = os.environ["MBEDTLS_FW_SIGN_SERVER_PORT"] + +download_pfix = "http://{}:{}/".format(server_ip, server_port) +sign_cmd = ("gpg --detach-sign --pinentry-mode loopback" + " --passphrase '{pasw}' --armor --batch {tarb}") +verify_cmd = "gpg --verify {sig} {tarb}" +sum_cmd = "sha256sum {tarb}" +zip_cmd = "zip -r {name}.zip ." + + +def do_shell_exec(exec_string: str) -> Tuple[int, str, str]: + """Helper function to do shell execution. + + exec_string - String to execute (as is - function will split) + expected_result - Expected return code. + """ + + shell_process = subprocess.Popen( + shlex.split(exec_string), + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (shell_stdout, shell_stderr) = shell_process.communicate() + + return (shell_process.returncode, + shell_stdout.decode("utf-8"), + shell_stderr.decode("utf-8")) + + +def randomise_path(name: str) -> str: + """Attach a pseudo-ranom postfix of an underscore and 3 uppercase characters.""" + pfix = "".join(random.choices(string.ascii_uppercase, k=3)) + return os.path.join("tmp", "{}_{}".format(name, pfix)) + + +@app.route('/') +def main() -> flask.typing.ResponseReturnValue: + return flask.render_template("index.html") + + +@app.route('//') +def download(path: str, filename: str) -> flask.typing.ResponseReturnValue: + path = os.path.join("tmp", path) + return flask.send_from_directory(path, filename, as_attachment=True) + + +@app.route('/sign', methods=['POST']) +def sign() -> flask.typing.ResponseReturnValue: + if flask.request.method == 'POST': + + # Accept the file + f = flask.request.files['rel_file'] + f.save(f.filename) + + pwd = flask.request.form.get("passw") + + # Update the names with the new tmp directory + artf_name = f.filename + artf_basename = artf_name.split(".")[0] + sha_fname = artf_name + ".sha256.txt" + sign_fname = artf_name + ".asc" + tmp_workdir = randomise_path(artf_basename) + + if cleanup_on_startup: + shutil.rmtree(tmp_workdir) + archive_name = artf_basename + ".zip" + + # Create a workdir + pathlib.Path(tmp_workdir).mkdir(parents=True, exist_ok=True) + + artf_name = os.path.join(tmp_workdir, artf_name) + sha_fname = os.path.join(tmp_workdir, sha_fname) + sign_fname = os.path.join(tmp_workdir, sign_fname) + archive_name = os.path.join(tmp_workdir, archive_name) + + # Move the tarballs + os.rename(f.filename, artf_name) + + # Calculate the sha256 + ret_code, _stdout, _sterr = do_shell_exec(sum_cmd.format(tarb=artf_name)) + if ret_code == 0: + with open(sha_fname, "w") as F: + F.write(_stdout) + sha = shlex.split(_stdout)[0] + else: + raise Exception("Shasum failed!") + + # Sign the tarball + ret_code, _stdout, _sterr = do_shell_exec(sign_cmd.format(pasw=pwd, tarb=artf_name)) + # If password is incorrect or other error exit. + if ret_code != 0: + return flask.render_template("signed.html", + artf_name="Not Authorised", + artf_url=download_pfix, + sha="Not Authorised", + sha_url=download_pfix, + sign_name="Not Authorised", + sign_url=download_pfix, + zip_name="Not Authorised", + zip_url=download_pfix) + # Zip everything + cwd = os.getcwd() + os.chdir(tmp_workdir) + ret_code, _stdout, _sterr = do_shell_exec( + zip_cmd.format(name=artf_basename)) + if ret_code != 0: + raise Exception("zip Failed") + os.chdir(cwd) + + # Calculate the download urls + sha_url = download_pfix + "/".join(sha_fname.split("/")[1:]) + artf_url = download_pfix + "/".join(artf_name.split("/")[1:]) + sign_url = download_pfix + "/".join(sign_fname.split("/")[1:]) + archive_url = download_pfix + "/".join(archive_name.split("/")[1:]) + + # Return the results page + return flask.render_template("signed.html", + artf_name=os.path.basename(artf_name), + artf_url=artf_url, + sha=sha, sha_url=sha_url, + sign_name=os.path.basename(sign_fname), + sign_url=sign_url, + zip_name=os.path.basename(archive_name), + zip_url=archive_url) + + +@app.route('/verify', methods=['POST']) +def verify() -> flask.typing.ResponseReturnValue: + if flask.request.method == 'POST': + # Create a workdir + tmp_workdir = randomise_path("verification") + pathlib.Path(tmp_workdir).mkdir(parents=True, exist_ok=True) + + # Accept the files + f = flask.request.files['rel_file'] + f.save(f.filename) + + s = flask.request.files['sig_file'] + s.save(s.filename) + + artf_fname = os.path.join(tmp_workdir, f.filename) + sig_fname = os.path.join(tmp_workdir, s.filename) + + # Move the files + os.rename(f.filename, artf_fname) + os.rename(s.filename, sig_fname) + + # Verify the archive's signature + ret_code, _stdout, _sterr = do_shell_exec( + verify_cmd.format(sig=sig_fname, tarb=artf_fname)) + verified = "Success" if ret_code == 0 else "Failed" + + # Return the result + return flask.render_template("verification.html", + verified=verified, + result=_sterr) + +if __name__ == '__main__': + app.run(host=server_ip, debug=False) diff --git a/tools/fw_signer/fw_sign_requirements.txt b/tools/fw_signer/fw_sign_requirements.txt new file mode 100644 index 00000000..79487386 --- /dev/null +++ b/tools/fw_signer/fw_sign_requirements.txt @@ -0,0 +1,3 @@ +certifi>=2023.7.22 +requests==2.28.2 +Flask==2.2.5 diff --git a/tools/fw_signer/static/style.css b/tools/fw_signer/static/style.css new file mode 100644 index 00000000..04df18fc --- /dev/null +++ b/tools/fw_signer/static/style.css @@ -0,0 +1,15 @@ +body {background-color: 0091bd; + width:640px} +h1 {color: black;} +p {color: white;} +a {color: white;} +li {color: white;} +color {color: white;} + +h1 { text-align: center; } +h2 { text-align: center; } +.custom-file-upload { text-align: center; + color: white;} +.custom-form { text-align: left; + margin: auto; + color: white;} \ No newline at end of file diff --git a/tools/fw_signer/templates/index.html b/tools/fw_signer/templates/index.html new file mode 100644 index 00000000..8cbd655e --- /dev/null +++ b/tools/fw_signer/templates/index.html @@ -0,0 +1,41 @@ + + + MbedTLS Release Sign + + + +

MbedTLS Release Sign-Tool

+

Sign a release

+

To get started:

+
    +
  • Select the release tarball
  • +
  • Type in the maintainer's password
  • +
  • Press Upload
  • +
+
+ +
+ + +
+
+

Verify a release

+

To get started:

+
    +
  • Select the archive containing the release firmware
  • +
  • Select the asc file with the signature
  • +
  • press Upload.
  • +
+
+ +
+ +
+ +
+ + + diff --git a/tools/fw_signer/templates/signed.html b/tools/fw_signer/templates/signed.html new file mode 100644 index 00000000..44fb3839 --- /dev/null +++ b/tools/fw_signer/templates/signed.html @@ -0,0 +1,30 @@ + + + + Sign + + + +

Sign Results

+ + + + + + + + + + + + + + + + + + + +
Tarball{{artf_name}}
sha256{{sha}}
Signature{{sign_name}}
Download All{{zip_name}}
+ + diff --git a/tools/fw_signer/templates/verification.html b/tools/fw_signer/templates/verification.html new file mode 100644 index 00000000..00e08539 --- /dev/null +++ b/tools/fw_signer/templates/verification.html @@ -0,0 +1,21 @@ + + + success + + + +

Verification Results

+ + + + + + + + + + + +
Verifcation Status{{verified}}
Output{{result}}
+ + From 19f6c5c83b5ef98de70e736730d2056a30db5491 Mon Sep 17 00:00:00 2001 From: Minos Galanakis Date: Tue, 9 Jul 2024 15:13:11 +0100 Subject: [PATCH 2/3] fw_signer: Pacify Pylint. Signed-off-by: Minos Galanakis --- tools/fw_signer/firmware_signer.py | 77 +++++++++++---------- tools/fw_signer/static/style.css | 3 +- tools/fw_signer/templates/index.html | 12 ++-- tools/fw_signer/templates/signed.html | 8 +-- tools/fw_signer/templates/verification.html | 6 +- 5 files changed, 55 insertions(+), 51 deletions(-) diff --git a/tools/fw_signer/firmware_signer.py b/tools/fw_signer/firmware_signer.py index abcfff1b..caf0ade9 100644 --- a/tools/fw_signer/firmware_signer.py +++ b/tools/fw_signer/firmware_signer.py @@ -9,8 +9,8 @@ # that the user is authorised to sign the firmware. # Please configure gpg-agent with `max-cache-ttl 0` when deploying. +# Run as MBEDTLS_FW_SIGN_SERVER_IP=X.X.X.X && python firmware_signer.py -import flask import random import os import string @@ -18,33 +18,32 @@ import shutil import subprocess import pathlib +from typing import Tuple -from typing import Text , Tuple - - +import flask ################ /* Configuration Parameters ################# -app = flask.Flask(__name__) +APP = flask.Flask(__name__) # Allow the host to set the IP for the server3 -server_ip = "0.0.0.0" # Will launch the server but dl links won't work. -server_port = "5000" # Port to bind to. -cleanup_on_startup = True # Cleanup the temporary directory on launch. +SERVER_IP = "0.0.0.0" # Will launch the server but dl links won't work. +SERVER_PORT = "5000" # Port to bind to. +CLEANUP_ON_STARTUP = True # Cleanup the temporary directory on launch. ################ Configuration Parameters */ ################# # ENV overrides if "MBEDTLS_FW_SIGN_SERVER_IP" in os.environ: - server_ip = os.environ["MBEDTLS_FW_SIGN_SERVER_IP"] + SERVER_IP = os.environ["MBEDTLS_FW_SIGN_SERVER_IP"] if "MBEDTLS_FW_SIGN_SERVER_PORT" in os.environ: - server_port = os.environ["MBEDTLS_FW_SIGN_SERVER_PORT"] + SERVER_PORT = os.environ["MBEDTLS_FW_SIGN_SERVER_PORT"] -download_pfix = "http://{}:{}/".format(server_ip, server_port) -sign_cmd = ("gpg --detach-sign --pinentry-mode loopback" +DOWNLOAD_PFIX = "http://{}:{}/".format(SERVER_IP, SERVER_PORT) +SIGN_CMD = ("gpg --detach-sign --pinentry-mode loopback" " --passphrase '{pasw}' --armor --batch {tarb}") -verify_cmd = "gpg --verify {sig} {tarb}" -sum_cmd = "sha256sum {tarb}" -zip_cmd = "zip -r {name}.zip ." +VERIFY_CMD = "gpg --verify {sig} {tarb}" +SUM_CMD = "sha256sum {tarb}" +ZIP_CMD = "zip -r {name}.zip ." def do_shell_exec(exec_string: str) -> Tuple[int, str, str]: @@ -72,19 +71,22 @@ def randomise_path(name: str) -> str: return os.path.join("tmp", "{}_{}".format(name, pfix)) -@app.route('/') +@APP.route('/') def main() -> flask.typing.ResponseReturnValue: + """Invoked on main landing page.""" return flask.render_template("index.html") -@app.route('//') +@APP.route('//') def download(path: str, filename: str) -> flask.typing.ResponseReturnValue: + """Invoked on {SERVERIP:PORT}/tmp_workdir/filename api endpoint.""" path = os.path.join("tmp", path) return flask.send_from_directory(path, filename, as_attachment=True) - -@app.route('/sign', methods=['POST']) +# pylint: disable=too-many-locals, inconsistent-return-statements +@APP.route('/sign', methods=['POST']) def sign() -> flask.typing.ResponseReturnValue: + """Invoked on {SERVERIP:PORT}/sign api endpoint.""" if flask.request.method == 'POST': # Accept the file @@ -100,8 +102,8 @@ def sign() -> flask.typing.ResponseReturnValue: sign_fname = artf_name + ".asc" tmp_workdir = randomise_path(artf_basename) - if cleanup_on_startup: - shutil.rmtree(tmp_workdir) + if CLEANUP_ON_STARTUP: + shutil.rmtree(tmp_workdir, ignore_errors=True) archive_name = artf_basename + ".zip" # Create a workdir @@ -116,41 +118,41 @@ def sign() -> flask.typing.ResponseReturnValue: os.rename(f.filename, artf_name) # Calculate the sha256 - ret_code, _stdout, _sterr = do_shell_exec(sum_cmd.format(tarb=artf_name)) + ret_code, _stdout, _sterr = do_shell_exec(SUM_CMD.format(tarb=artf_name)) if ret_code == 0: - with open(sha_fname, "w") as F: - F.write(_stdout) + with open(sha_fname, "w") as sha_file: + sha_file.write(_stdout) sha = shlex.split(_stdout)[0] else: raise Exception("Shasum failed!") # Sign the tarball - ret_code, _stdout, _sterr = do_shell_exec(sign_cmd.format(pasw=pwd, tarb=artf_name)) + ret_code, _stdout, _sterr = do_shell_exec(SIGN_CMD.format(pasw=pwd, tarb=artf_name)) # If password is incorrect or other error exit. if ret_code != 0: return flask.render_template("signed.html", artf_name="Not Authorised", - artf_url=download_pfix, + artf_url=DOWNLOAD_PFIX, sha="Not Authorised", - sha_url=download_pfix, + sha_url=DOWNLOAD_PFIX, sign_name="Not Authorised", - sign_url=download_pfix, + sign_url=DOWNLOAD_PFIX, zip_name="Not Authorised", - zip_url=download_pfix) + zip_url=DOWNLOAD_PFIX) # Zip everything cwd = os.getcwd() os.chdir(tmp_workdir) ret_code, _stdout, _sterr = do_shell_exec( - zip_cmd.format(name=artf_basename)) + ZIP_CMD.format(name=artf_basename)) if ret_code != 0: raise Exception("zip Failed") os.chdir(cwd) # Calculate the download urls - sha_url = download_pfix + "/".join(sha_fname.split("/")[1:]) - artf_url = download_pfix + "/".join(artf_name.split("/")[1:]) - sign_url = download_pfix + "/".join(sign_fname.split("/")[1:]) - archive_url = download_pfix + "/".join(archive_name.split("/")[1:]) + sha_url = DOWNLOAD_PFIX + "/".join(sha_fname.split("/")[1:]) + artf_url = DOWNLOAD_PFIX + "/".join(artf_name.split("/")[1:]) + sign_url = DOWNLOAD_PFIX + "/".join(sign_fname.split("/")[1:]) + archive_url = DOWNLOAD_PFIX + "/".join(archive_name.split("/")[1:]) # Return the results page return flask.render_template("signed.html", @@ -163,8 +165,9 @@ def sign() -> flask.typing.ResponseReturnValue: zip_url=archive_url) -@app.route('/verify', methods=['POST']) +@APP.route('/verify', methods=['POST']) def verify() -> flask.typing.ResponseReturnValue: + """Invoked on {SERVERIP:PORT}/verify api endpoint.""" if flask.request.method == 'POST': # Create a workdir tmp_workdir = randomise_path("verification") @@ -186,7 +189,7 @@ def verify() -> flask.typing.ResponseReturnValue: # Verify the archive's signature ret_code, _stdout, _sterr = do_shell_exec( - verify_cmd.format(sig=sig_fname, tarb=artf_fname)) + VERIFY_CMD.format(sig=sig_fname, tarb=artf_fname)) verified = "Success" if ret_code == 0 else "Failed" # Return the result @@ -195,4 +198,4 @@ def verify() -> flask.typing.ResponseReturnValue: result=_sterr) if __name__ == '__main__': - app.run(host=server_ip, debug=False) + APP.run(host=SERVER_IP, debug=False) diff --git a/tools/fw_signer/static/style.css b/tools/fw_signer/static/style.css index 04df18fc..2e57a99d 100644 --- a/tools/fw_signer/static/style.css +++ b/tools/fw_signer/static/style.css @@ -12,4 +12,5 @@ h2 { text-align: center; } color: white;} .custom-form { text-align: left; margin: auto; - color: white;} \ No newline at end of file + color: white;} + diff --git a/tools/fw_signer/templates/index.html b/tools/fw_signer/templates/index.html index 8cbd655e..01fa2d5e 100644 --- a/tools/fw_signer/templates/index.html +++ b/tools/fw_signer/templates/index.html @@ -1,5 +1,5 @@ - + MbedTLS Release Sign @@ -17,7 +17,7 @@

Sign a release


- +

Verify a release

@@ -27,15 +27,15 @@

Verify a release

  • Select the asc file with the signature
  • press Upload.
  • -
    -