From 33c4465bf09d9bc9e0052dfbc482f5e5146670d6 Mon Sep 17 00:00:00 2001 From: Lukasz Fundakowski Date: Tue, 19 Nov 2024 15:37:56 +0100 Subject: [PATCH] scripts/bootloader: Add ed25519/sha512 to scripts Python scripts implementing ed25519 and sha512 support needed for nsib image signing. Signed-off-by: Lukasz Fundakowski --- scripts/bootloader/do_sign.py | 51 +++-- scripts/bootloader/hash.py | 35 +++- scripts/bootloader/keygen.py | 205 ++++++++++++++++++--- scripts/bootloader/tests/asn1parse_test.py | 50 +++++ scripts/bootloader/tests/conftest.py | 9 + scripts/bootloader/tests/do_sign_test.py | 115 ++++++++++++ scripts/bootloader/tests/keygen_test.py | 127 +++++++++++++ scripts/bootloader/validation_data.py | 200 ++++++++++++++------ 8 files changed, 694 insertions(+), 98 deletions(-) mode change 100644 => 100755 scripts/bootloader/do_sign.py mode change 100644 => 100755 scripts/bootloader/hash.py mode change 100644 => 100755 scripts/bootloader/keygen.py create mode 100644 scripts/bootloader/tests/asn1parse_test.py create mode 100644 scripts/bootloader/tests/conftest.py create mode 100644 scripts/bootloader/tests/do_sign_test.py create mode 100644 scripts/bootloader/tests/keygen_test.py mode change 100644 => 100755 scripts/bootloader/validation_data.py diff --git a/scripts/bootloader/do_sign.py b/scripts/bootloader/do_sign.py old mode 100644 new mode 100755 index 842c7e6443c1..23edaa25d030 --- a/scripts/bootloader/do_sign.py +++ b/scripts/bootloader/do_sign.py @@ -3,19 +3,21 @@ # Copyright (c) 2018 Nordic Semiconductor ASA # # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause - - -import sys import argparse import hashlib -from ecdsa import SigningKey +import io +import sys +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from ecdsa.keys import SigningKey # type: ignore[import-untyped] -def parse_args(): + +def parse_args(argv=None): parser = argparse.ArgumentParser( description='Sign data from stdin or file.', formatter_class=argparse.RawDescriptionHelpFormatter, - allow_abbrev=False) + allow_abbrev=False + ) parser.add_argument('-k', '--private-key', required=True, type=argparse.FileType('rb'), help='Private key to use.') @@ -25,15 +27,40 @@ def parse_args(): parser.add_argument('-o', '--out', '-out', required=False, dest='outfile', type=argparse.FileType('wb'), default=sys.stdout.buffer, help='Write the signature to the specified file instead of stdout.') + parser.add_argument( + '--algorithm', '-a', dest='algorithm', help='Signing algorithm (default: %(default)s)', + action='store', choices=['ecdsa', 'ed25519'], default='ecdsa', + ) - args = parser.parse_args() + args = parser.parse_args(argv) return args -if __name__ == '__main__': - args = parse_args() - private_key = SigningKey.from_pem(args.private_key.read()) - data = args.infile.read() +def sign_with_ecdsa(private_key_file: io.BytesIO, input_file: io.BytesIO, output_file: io.BytesIO) -> int: + private_key = SigningKey.from_pem(private_key_file.read()) + data = input_file.read() signature = private_key.sign(data, hashfunc=hashlib.sha256) - args.outfile.write(signature) + output_file.write(signature) + return 0 + + +def sign_with_ed25519(private_key_file: io.BytesIO, input_file: io.BytesIO, output_file: io.BytesIO) -> int: + private_key = load_pem_private_key(private_key_file.read(), password=None) + data = input_file.read() + signature = private_key.sign(data) + output_file.write(signature) + return 0 + + +def main(argv=None) -> int: + args = parse_args(argv) + if args.algorithm == 'ecdsa': + return sign_with_ecdsa(args.private_key, args.infile, args.outfile) + if args.algorithm == 'ed25519': + return sign_with_ed25519(args.private_key, args.infile, args.outfile) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/bootloader/hash.py b/scripts/bootloader/hash.py old mode 100644 new mode 100755 index 3581c0d9989e..3bd77232a64f --- a/scripts/bootloader/hash.py +++ b/scripts/bootloader/hash.py @@ -4,6 +4,9 @@ # # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +""" +Hash content of a file. +""" import hashlib import sys @@ -11,20 +14,33 @@ from intelhex import IntelHex +HASH_FUNCTION_FACTORY = { + 'sha256': hashlib.sha256, + 'sha512': hashlib.sha512, +} + + def parse_args(): parser = argparse.ArgumentParser( description='Hash data from file.', formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False) - parser.add_argument('--infile', '-i', '--in', '-in', required=True, - help='Hash the contents of the specified file. If a *.hex file is given, the contents will ' - 'first be converted to binary, with all non-specified area being set to 0xff. ' - 'For all other file types, no conversion is done.') + parser.add_argument( + '--infile', '-i', '--in', '-in', required=True, + help='Hash the contents of the specified file. If a *.hex file is given, the contents will ' + 'first be converted to binary, with all non-specified area being set to 0xff. ' + 'For all other file types, no conversion is done.' + ) + parser.add_argument( + '--type', '-t', dest='hash_function', help='Hash function (default: %(default)s)', + action='store', choices=HASH_FUNCTION_FACTORY.keys(), default='sha256' + ) + return parser.parse_args() -if __name__ == '__main__': +def main(): args = parse_args() if args.infile.endswith('.hex'): @@ -33,4 +49,11 @@ def parse_args(): to_hash = ih.tobinstr() else: to_hash = open(args.infile, 'rb').read() - sys.stdout.buffer.write(hashlib.sha256(to_hash).digest()) + + hash_function = HASH_FUNCTION_FACTORY[args.hash_function] + sys.stdout.buffer.write(hash_function(to_hash).digest()) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/bootloader/keygen.py b/scripts/bootloader/keygen.py old mode 100644 new mode 100755 index 871e4fd44db9..a2ba52fea182 --- a/scripts/bootloader/keygen.py +++ b/scripts/bootloader/keygen.py @@ -4,16 +4,21 @@ # # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +from __future__ import annotations -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.hazmat.primitives.serialization import load_pem_private_key as load_pem -from hashlib import sha256 import argparse +import io import sys +from hashlib import sha256, sha512 +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.serialization import load_pem_private_key -def generate_legal_key(): + +def generate_legal_key_for_elliptic_curve(): """ Ensure that we don't have 0xFFFF in the hash of the public key of the generated keypair. @@ -23,8 +28,8 @@ def generate_legal_key(): while True: key = ec.generate_private_key(ec.SECP256R1()) public_bytes = key.public_key().public_bytes( - encoding=serialization.Encoding.X962, - format=serialization.PublicFormat.UncompressedPoint, + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, ) # The digest don't contain the first byte as it denotes @@ -35,7 +40,150 @@ def generate_legal_key(): return key -if __name__ == '__main__': +def generate_legal_key_for_ed25519(): + """ + Ensure that we don't have 0xFFFF in the hash of the public key of + the generated keypair. + + :return: A key who's SHA512 digest does not contain 0xFFFF + """ + while True: + key = ed25519.Ed25519PrivateKey.generate() + public_bytes = key.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + # The digest don't contain the first byte as it denotes + # if it is compressed/UncompressedPoint. + digest = sha512(public_bytes[1:]).digest()[:16] + if not any([digest[n:n + 2] == b'\xff\xff' for n in range(0, len(digest), 2)]): + return key + + +class EllipticCurveKeysGenerator: + """Generate private and public keys for Elliptic Curve cryptography.""" + + def __init__(self, infile: io.BytesIO | None = None) -> None: + """ + :param infile: A file-like object to read the private key. + """ + if infile is None: + self.private_key = generate_legal_key_for_elliptic_curve() + else: + self.private_key = load_pem_private_key(infile.read(), password=None) + self.public_key = self.private_key.public_key() + + @property + def private_key_pem(self) -> bytes: + return self.private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + def write_private_key_pem(self, outfile: io.BytesIO | None = None) -> bytes: + """ + Write private key pem to file and return it. + + :param outfile: A file-like object to write the private key. + """ + if outfile is not None: + outfile.write(self.private_key_pem) + return self.private_key_pem + + @property + def public_key_pem(self) -> bytes: + return self.public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + def write_public_key_pem(self, outfile: io.BytesIO | None = None) -> bytes: + """ + Write public key pem to file and return it. + + :param outfile: A file-like object to write the public key. + """ + if outfile is not None: + outfile.write(self.public_key_pem) + return self.public_key_pem + + @staticmethod + def verify_signature(public_key, message: bytes, signature: bytes) -> bool: + try: + public_key.verify(signature, message, ec.ECDSA(hashes.SHA256())) + return True + except Exception: + return False + + @staticmethod + def sign_message(private_key, message: bytes) -> bytes: + return private_key.sign(message, ec.ECDSA(hashes.SHA256())) + + +class Ed25519KeysGenerator: + """Generate private and public keys for ED25519 cryptography.""" + + def __init__(self, infile: io.BytesIO | None = None) -> None: + """ + :param infile: A file-like object to read the private key. + """ + if infile is None: + self.private_key = generate_legal_key_for_ed25519() + else: + self.private_key = load_pem_private_key(infile.read(), password=None) + self.public_key = self.private_key.public_key() + + @property + def private_key_pem(self) -> bytes: + return self.private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + + def write_private_key_pem(self, outfile: io.BytesIO | None = None) -> bytes: + """ + Write private key pem to file and return it. + + :param outfile: A file-like object to write the private key. + """ + if outfile is not None: + outfile.write(self.private_key_pem) + return self.private_key_pem + + @property + def public_key_pem(self) -> bytes: + return self.public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + def write_public_key_pem(self, outfile: io.BytesIO | None = None) -> bytes: + """ + Write public key pem to file and return it. + + :param outfile: A file-like object to write the public key. + """ + if outfile is not None: + outfile.write(self.public_key_pem) + return self.public_key_pem + + @staticmethod + def verify_signature(public_key, message: bytes, signature: bytes) -> bool: + try: + public_key.verify(signature, message) + return True + except Exception: + return False + + @staticmethod + def sign_message(private_key, message: bytes) -> bytes: + return private_key.sign(message) + + +def main(argv=None) -> int: parser = argparse.ArgumentParser( description='Generate PEM file.', formatter_class=argparse.RawDescriptionHelpFormatter, @@ -53,21 +201,28 @@ def generate_legal_key(): type=argparse.FileType('rb'), help='Read private key from specified PEM file instead ' 'of generating it.') + parser.add_argument( + '--algorithm', '-a', help='Signing algorithm (default: %(default)s)', + required=False, action='store', choices=('ec', 'ed25519'), default='ec' + ) + + args = parser.parse_args(argv) + + if args.algorithm == 'ed25519': + ed25519_generator = Ed25519KeysGenerator(args.infile) + if args.private: + ed25519_generator.write_private_key_pem(args.out) + if args.public: + ed25519_generator.write_public_key_pem(args.out) + else: + ec_generator = EllipticCurveKeysGenerator(args.infile) + if args.private: + ec_generator.write_private_key_pem(args.out) + elif args.public: + ec_generator.write_public_key_pem(args.out) - args = parser.parse_args() - sk = (load_pem(args.infile.read(), password=None) if args.infile else generate_legal_key()) - - if args.private: - private_pem = sk.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - args.out.write(private_pem) - - if args.public: - public_pem = sk.public_key().public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - args.out.write(public_pem) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/bootloader/tests/asn1parse_test.py b/scripts/bootloader/tests/asn1parse_test.py new file mode 100644 index 000000000000..96af37cb5ed3 --- /dev/null +++ b/scripts/bootloader/tests/asn1parse_test.py @@ -0,0 +1,50 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +import subprocess + +from asn1parse import get_ecdsa_signature +from keygen import EllipticCurveKeysGenerator + + +def test_asn1parse_with_ecdsa_and_sha256(tmpdir): + signature_der_file = tmpdir / 'signature.bin' + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + generator = EllipticCurveKeysGenerator() + generator.write_private_key_pem(private_key_file) + generator.write_public_key_pem(public_key_file) + + input_file = tmpdir / 'input_file.txt' + message = b'Test message for key verification' + input_file.write(message) + + subprocess.run( + [ + 'openssl', 'dgst', '-sha256', '-sign', private_key_file, + '-out', signature_der_file, input_file + ], + check=True + ) + assert signature_der_file.exists() + + signature = get_ecdsa_signature(signature_der_file.open('rb').read(), clength=32) + assert len(signature) == 64 + result = subprocess.run( + [ + 'openssl', 'dgst', '-sha256', '-verify', public_key_file, + '-signature', signature_der_file, input_file + ] + ) + assert result.returncode == 0, 'Signature does not match' + + input_file.write(b'Test message to fail verification') + result = subprocess.run( + [ + 'openssl', 'dgst', '-sha256', '-verify', public_key_file, + '-signature', signature_der_file, input_file + ], + ) + assert result.returncode == 1 diff --git a/scripts/bootloader/tests/conftest.py b/scripts/bootloader/tests/conftest.py new file mode 100644 index 000000000000..e1f79775766a --- /dev/null +++ b/scripts/bootloader/tests/conftest.py @@ -0,0 +1,9 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) diff --git a/scripts/bootloader/tests/do_sign_test.py b/scripts/bootloader/tests/do_sign_test.py new file mode 100644 index 000000000000..7073248f9203 --- /dev/null +++ b/scripts/bootloader/tests/do_sign_test.py @@ -0,0 +1,115 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +import hashlib + +from ecdsa import BadSignatureError +from ecdsa.keys import VerifyingKey +from ecdsa.util import sigdecode_string + +from do_sign import sign_with_ecdsa, sign_with_ed25519 +from keygen import Ed25519KeysGenerator, EllipticCurveKeysGenerator + + +def verify_ecdsa_signature(public_key: VerifyingKey, message: bytes, signature: bytes) -> bool: + try: + public_key.verify(signature, message, hashlib.sha256, sigdecode=sigdecode_string) + return True + except BadSignatureError: + return False + + +def test_if_file_is_properly_signed_with_ec_key(tmpdir): + generator = EllipticCurveKeysGenerator() + private_key_file = tmpdir / 'private.pem' + generator.write_private_key_pem(private_key_file) + public_key_file = tmpdir / 'public.pem' + generator.write_public_key_pem(public_key_file) + + input_file = tmpdir / 'input_file.txt' + message = b'Test message for key verification' + input_file.write(message) + + signature_file = tmpdir / 'signature.bin' + + sign_with_ecdsa( + private_key_file=private_key_file.open('rb'), + input_file=input_file.open('rb'), + output_file=signature_file.open('wb'), + ) + + public_key = VerifyingKey.from_pem(public_key_file.open('br').read()) + signature = signature_file.open('rb').read() + assert verify_ecdsa_signature( + public_key=public_key, message=message, signature=signature + ) + + +def test_if_validation_does_not_pass_for_wrong_ec_key(tmpdir): + private_key_file = tmpdir / 'private.pem' + EllipticCurveKeysGenerator().write_private_key_pem(private_key_file) + public_key_file = tmpdir / 'public.pem' + EllipticCurveKeysGenerator().write_public_key_pem(public_key_file) + + input_file = tmpdir / 'input_file.txt' + message = b'Test message for key verification' + input_file.write(message) + + signature_file = tmpdir / 'signature.bin' + + sign_with_ecdsa( + private_key_file=private_key_file.open('rb'), + input_file=input_file.open('rb'), + output_file=signature_file.open('wb'), + ) + + public_key = VerifyingKey.from_pem(public_key_file.open('br').read()) + signature = signature_file.open('rb').read() + assert verify_ecdsa_signature( + public_key=public_key, message=message, signature=signature + ) is False + + +def test_if_validation_does_not_pass_for_wrong_ed25519_key(tmpdir): + generator = Ed25519KeysGenerator() + private_key_file = tmpdir / 'private.pem' + generator.write_private_key_pem(private_key_file) + public_key = generator.public_key + + input_file = tmpdir / 'input_file.txt' + message = b'Test message for key verification' + input_file.write(message) + + signature_file = tmpdir / 'signature.bin' + + sign_with_ed25519( + private_key_file=private_key_file.open('rb'), + input_file=input_file.open('rb'), + output_file=signature_file.open('bw') + ) + assert Ed25519KeysGenerator.verify_signature( + public_key, message, signature_file.open('br').read() + ) + + +def test_if_file_is_properly_signed_with_ed25519_key(tmpdir): + private_key_file = tmpdir / 'private.pem' + Ed25519KeysGenerator().write_private_key_pem(private_key_file) + public_key = Ed25519KeysGenerator().public_key + + input_file = tmpdir / 'input_file.txt' + message = b'Test message for key verification' + input_file.write(message) + + signature_file = tmpdir / 'signature.bin' + + sign_with_ed25519( + private_key_file=private_key_file.open('rb'), + input_file=input_file.open('rb'), + output_file=signature_file.open('bw') + ) + assert Ed25519KeysGenerator.verify_signature( + public_key, message, signature_file.open('br').read() + ) is False diff --git a/scripts/bootloader/tests/keygen_test.py b/scripts/bootloader/tests/keygen_test.py new file mode 100644 index 000000000000..f91c1172982a --- /dev/null +++ b/scripts/bootloader/tests/keygen_test.py @@ -0,0 +1,127 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +import pytest +from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key + +from keygen import Ed25519KeysGenerator, EllipticCurveKeysGenerator + + +@pytest.mark.parametrize( + 'keys_generator', + [EllipticCurveKeysGenerator, Ed25519KeysGenerator], + ids=['ec', 'ed25519'] +) +def test_keys_generator_generates_proper_pem_key(keys_generator): + key_gen = keys_generator() + assert b'-----BEGIN PRIVATE KEY-----' in key_gen.private_key_pem + assert b'-----END PRIVATE KEY-----' in key_gen.private_key_pem + assert b'-----BEGIN PUBLIC KEY-----' in key_gen.public_key_pem + assert b'-----END PUBLIC KEY-----' in key_gen.public_key_pem + + +def test_elliptic_curve_keys_generator(tmpdir): + private_key_file = tmpdir / 'private.pem' + ec_keys_generator_1 = EllipticCurveKeysGenerator() + private_key_pem_1 = ec_keys_generator_1.write_private_key_pem(private_key_file) + public_key_pem_1 = ec_keys_generator_1.public_key_pem + # public key and private should not be the same + assert private_key_pem_1 != public_key_pem_1 + + # test if same private key is loaded from file + ec_keys_generator_2 = EllipticCurveKeysGenerator(private_key_file.open('rb')) + private_key_pem_2 = ec_keys_generator_2.private_key_pem + assert private_key_pem_1 == private_key_pem_2 + + +def test_signing_with_elliptic_curve_with_valid_keys(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + generator = EllipticCurveKeysGenerator() + generator.write_private_key_pem(private_key_file) + generator.write_public_key_pem(public_key_file) + + message = b'Test message for key verification' + private_key = load_pem_private_key(private_key_file.open('br').read(), password=None) + public_key = load_pem_public_key(public_key_file.open('br').read()) + + signature = EllipticCurveKeysGenerator.sign_message(private_key, message) + assert EllipticCurveKeysGenerator.verify_signature(public_key, message, signature) + + +def test_signing_with_elliptic_curve_with_invalid_private_key(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + generator = EllipticCurveKeysGenerator() + generator.write_private_key_pem(private_key_file) + + public_generator = EllipticCurveKeysGenerator() + public_generator.write_public_key_pem(public_key_file.open('wb')) + + message = b'Test message for key verification' + private_key = load_pem_private_key(private_key_file.open('br').read(), password=None) + public_key = load_pem_public_key(public_key_file.open('br').read()) + + signature = EllipticCurveKeysGenerator.sign_message(private_key, message) + assert EllipticCurveKeysGenerator.verify_signature(public_key, message, signature) is False + + +def test_ed25519_keys_generator(tmpdir): + private_key_file = tmpdir / 'private.pem' + ec_keys_generator_1 = Ed25519KeysGenerator() + private_key_pem_1 = ec_keys_generator_1.write_private_key_pem(private_key_file) + public_key_pem_1 = ec_keys_generator_1.write_public_key_pem() + assert private_key_pem_1 != public_key_pem_1 + + # test if same private key is loaded from file + ec_keys_generator_2 = Ed25519KeysGenerator(private_key_file.open('rb')) + private_key_pem_2 = ec_keys_generator_2.write_private_key_pem() + assert private_key_pem_1 == private_key_pem_2 + + +def test_signing_with_ed25519_with_valid_keys(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + generator = Ed25519KeysGenerator() + generator.write_private_key_pem(private_key_file.open('wb')) + generator.write_public_key_pem(public_key_file.open('wb')) + + message = b'Test message for key verification' + private_key = load_pem_private_key(private_key_file.open('br').read(), password=None) + public_key = load_pem_public_key(public_key_file.open('br').read()) + signature = Ed25519KeysGenerator.sign_message(private_key, message) + assert Ed25519KeysGenerator.verify_signature(public_key, message, signature) + + +def test_signing_with_ed25519_with_valid_keys_and_private_key_from_public_pem_file(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + private_generator = Ed25519KeysGenerator() + private_generator.write_private_key_pem(private_key_file.open('wb')) + + public_generator = Ed25519KeysGenerator(private_key_file.open('rb')) + public_generator.write_public_key_pem(public_key_file.open('wb')) + + message = b'Test message for key verification' + private_key = load_pem_private_key(private_key_file.open('br').read(), password=None) + public_key = load_pem_public_key(public_key_file.open('br').read()) + signature = Ed25519KeysGenerator.sign_message(private_key, message) + assert Ed25519KeysGenerator.verify_signature(public_key, message, signature) + + +def test_signing_with_ed25519_signature_with_invalid_private_key(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + private_generator = Ed25519KeysGenerator() + private_generator.write_private_key_pem(private_key_file.open('wb')) + + public_generator = Ed25519KeysGenerator() + public_generator.write_public_key_pem(public_key_file.open('wb')) + + message = b'Test message for key verification' + private_key = load_pem_private_key(private_key_file.open('br').read(), password=None) + public_key = load_pem_public_key(public_key_file.open('br').read()) + signature = Ed25519KeysGenerator.sign_message(private_key, message) + assert Ed25519KeysGenerator.verify_signature(public_key, message, signature) is False diff --git a/scripts/bootloader/validation_data.py b/scripts/bootloader/validation_data.py old mode 100644 new mode 100755 index 44fc41266e0a..56f6b94e6f9e --- a/scripts/bootloader/validation_data.py +++ b/scripts/bootloader/validation_data.py @@ -4,85 +4,158 @@ # # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +from __future__ import annotations -from intelhex import IntelHex - -import hashlib +import abc import argparse +import hashlib +import io import struct -import ecdsa +import sys + +import ecdsa # type: ignore[import-untyped] +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed25519 +from cryptography.hazmat.primitives.serialization import load_pem_public_key +from intelhex import IntelHex # type: ignore[import-untyped] + + +class BaseValidator(abc.ABC): + + def __init__(self, hashfunc) -> None: + """ + param hashfunc: hashing function e.g. hashlib.sha256 + """ + self.hashfunc = hashfunc + + def get_hash(self, input_hex: IntelHex) -> bytes: + firmware_bytes = input_hex.tobinstr() + return self.hashfunc(firmware_bytes).digest() + + @abc.abstractmethod + def to_string(self, public_key) -> bytes: + """Serialize the public key to bytes.""" + @abc.abstractmethod + def verify(self, public_key, signature: bytes, hash_bytes: bytes): + """Verify signature.""" -def get_hash(input_hex): - firmware_bytes = input_hex.tobinstr() - return hashlib.sha256(firmware_bytes).digest() + def get_validation_data( + self, + signature_bytes: bytes, + input_hex: IntelHex, + public_key: ecdsa.VerifyingKey | ed25519.Ed25519PublicKey, + magic_value: bytes + ) -> bytes: + hash_bytes = self.get_hash(input_hex) + public_key_bytes = self.to_string(public_key) + # Will raise an exception if it fails + self.verify(public_key, signature_bytes, hash_bytes) -def get_validation_data(signature_bytes, input_hex, public_key, magic_value): - hash_bytes = get_hash(input_hex) - public_key_bytes = public_key.to_string() + validation_bytes = magic_value + validation_bytes += struct.pack('I', input_hex.addresses()[0]) + validation_bytes += hash_bytes + validation_bytes += public_key_bytes + validation_bytes += signature_bytes - # Will raise an exception if it fails - public_key.verify(signature_bytes, hash_bytes, hashfunc=hashlib.sha256) + return validation_bytes - validation_bytes = magic_value - validation_bytes += struct.pack('I', input_hex.addresses()[0]) - validation_bytes += hash_bytes - validation_bytes += public_key_bytes - validation_bytes += signature_bytes + def append_validation_data( + self, + signature: bytes, + input_file: io.BytesIO, + public_key: ecdsa.VerifyingKey | ed25519.Ed25519PublicKey, + offset: int, + output_hex: io.FileIO, + output_bin: io.FileIO, + magic_value: str + ) -> None: + ih = IntelHex(input_file) + ih.start_addr = None # OBJCOPY incorrectly inserts x86 specific records, remove the start_addr as it is wrong. - return validation_bytes + minimum_offset = ((ih.maxaddr() // 4) + 1) * 4 + if offset != 0 and offset < minimum_offset: + raise RuntimeError(f'Incorrect offset, must be bigger than {hex(minimum_offset)}') + # Parse comma-separated string of uint32s into hex string. Each is encoded in little-endian byte order + parsed_magic_value = b''.join( + [struct.pack(' bytes: + return public_key.to_string() + + def verify(self, public_key, signature_bytes: bytes, hash_bytes: bytes): + public_key.verify(signature_bytes, hash_bytes, hashfunc=self.hashfunc) + + +class Ed25519SignatureValidator(BaseValidator): + + def to_string(self, public_key) -> bytes: + """Serialize the public key to bytes.""" + public_key_bytes = public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + return public_key_bytes + + def verify(self, public_key, signature_bytes: bytes, hash_bytes: bytes): + public_key.verify(signature_bytes, hash_bytes) def parse_args(): parser = argparse.ArgumentParser( description='Append validation metadata at specified offset. Generate HEX and BIN file', formatter_class=argparse.RawDescriptionHelpFormatter, - allow_abbrev=False) + allow_abbrev=False + ) + + parser.add_argument( + '--algorithm', '-a', dest='algorithm', help='Signing algorithm (default: %(default)s)', + choices=['ecdsa', 'ed25519'], default='ecdsa' + ) - parser.add_argument('-i', '--input', required=True, type=argparse.FileType('r', encoding='UTF-8'), + parser.add_argument('-i', '--input', required=True, + type=argparse.FileType('r', encoding='UTF-8'), help='Input hex file.') parser.add_argument('--offset', required=False, type=int, help='Offset to store validation metadata at.', default=0) parser.add_argument('-s', '--signature', required=True, type=argparse.FileType('rb'), - help="Signature file (DER) of ECDSA (secp256r1) signature of 'input' argument.") - parser.add_argument('-p', '--public-key', required=True, type=argparse.FileType('r', encoding='UTF-8'), + help="Signature file (DER) of ECDSA (secp256r1) or ED25519 signature of 'input' argument.") + parser.add_argument('-p', '--public-key', required=True, + type=argparse.FileType('r', encoding='UTF-8'), help='Public key file (PEM).') parser.add_argument('-m', '--magic-value', required=True, help='ASCII representation of magic value.') - parser.add_argument('-o', '--output-hex', required=False, default=None, type=argparse.FileType('w'), + parser.add_argument('-o', '--output-hex', required=False, default=None, + type=argparse.FileType('w'), help='.hex output file name. Default is to overwrite --input.') - parser.add_argument('--output-bin', required=False, default=None, type=argparse.FileType('w'), + parser.add_argument('--output-bin', required=False, default=None, + type=argparse.FileType('w'), help='.bin output file name.') args = parser.parse_args() @@ -92,18 +165,35 @@ def parse_args(): return args -def main(): - +def main() -> int: args = parse_args() - append_validation_data(signature=args.signature.read(), - input_file=args.input, - public_key=ecdsa.VerifyingKey.from_pem(args.public_key.read()), - offset=args.offset, - output_hex=args.output_hex, - output_bin=args.output_bin, - magic_value=args.magic_value) + if args.algorithm == 'ecdsa': + EcdsaSignatureValidator(hashfunc=hashlib.sha256).append_validation_data( + signature=args.signature.read(), + input_file=args.input, + public_key=ecdsa.VerifyingKey.from_pem(args.public_key.read()), + offset=args.offset, + output_hex=args.output_hex, + output_bin=args.output_bin, + magic_value=args.magic_value + ) + elif args.algorithm == 'ed25519': + public_key = load_pem_public_key(args.public_key.read().encode()) + Ed25519SignatureValidator(hashfunc=hashlib.sha512).append_validation_data( + signature=args.signature.read(), + input_file=args.input, + public_key=public_key, + offset=args.offset, + output_hex=args.output_hex, + output_bin=args.output_bin, + magic_value=args.magic_value + ) + else: + raise SystemExit('Not implemented') + + return 0 if __name__ == '__main__': - main() + sys.exit(main())