From b436a1dfccf81ba5db432e748a1c2241abaf61f2 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/asn1parse.py | 33 ++++++++-- scripts/bootloader/do_sign.py | 41 ++++++++---- scripts/bootloader/hash.py | 35 ++++++++-- scripts/bootloader/keygen.py | 105 ++++++++++++++++++++++++------ scripts/bootloader/keygen_test.py | 76 +++++++++++++++++++++ 5 files changed, 245 insertions(+), 45 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/keygen_test.py diff --git a/scripts/bootloader/asn1parse.py b/scripts/bootloader/asn1parse.py index 8a19bc6fd8db..35f699a7c8f6 100644 --- a/scripts/bootloader/asn1parse.py +++ b/scripts/bootloader/asn1parse.py @@ -5,13 +5,15 @@ # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause -from subprocess import check_output +import argparse import re import sys -import argparse +from subprocess import check_output +from cryptography.hazmat.primitives.asymmetric import ed25519 -def get_ecdsa_signature(der, clength): + +def get_ecdsa_signature(der: bytes, clength: int) -> bytes: # The der consists of a SEQUENCE with 2 INTEGERS (r and s) # The expected byte format of the file is # 0: type(SEQ), len(SEQ) @@ -24,7 +26,7 @@ def get_ecdsa_signature(der, clength): # leading 0 byte. # clength is the expected length of r and s. # The following code parses the output of openssl asnparse which prints - # the values in hex, together with human-readble metadata. + # the values in hex, together with human-readable metadata. # Disable pylint error as 'input' keyword has specific handling in 'check_output' # pylint: disable=unexpected-keyword-arg @@ -36,13 +38,19 @@ def get_ecdsa_signature(der, clength): return sig +def get_ed25519_signature(der: bytes) -> bytes: + private_key = ed25519.Ed25519PrivateKey.generate() + signature = private_key.sign(der) + return signature + + def parse_args(): parser = argparse.ArgumentParser( description='Decode DER format using OpenSSL.', formatter_class=argparse.RawDescriptionHelpFormatter, allow_abbrev=False) - parser.add_argument('-a', '--alg', required=True, choices=['rsa', 'ecdsa'], + parser.add_argument('-a', '--alg', required=True, choices=['rsa', 'ecdsa', 'ed25519'], help='Expected algorithm') parser.add_argument('-c', '--contents', required=True, choices=['signature'], help='Expected contents') @@ -55,9 +63,20 @@ def parse_args(): return args -if __name__ == '__main__': +def main() -> int: args = parse_args() - assert args.alg == 'ecdsa' # Only ecdsa is currently supported. + if args.alg == 'ecdsa': if args.contents == 'signature': sys.stdout.buffer.write(get_ecdsa_signature(args.infile.read(), 32)) + elif args.alg == 'ed25519': + if args.contents == 'signature': + sys.stdout.buffer.write(get_ed25519_signature(args.infile.read())) + else: + sys.exit(f'Algorythm not supported {args.alg}') + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/bootloader/do_sign.py b/scripts/bootloader/do_sign.py old mode 100644 new mode 100755 index 842c7e6443c1..85055620b773 --- a/scripts/bootloader/do_sign.py +++ b/scripts/bootloader/do_sign.py @@ -3,19 +3,20 @@ # Copyright (c) 2018 Nordic Semiconductor ASA # # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause - - -import sys import argparse import hashlib +import sys + +from cryptography.hazmat.primitives.serialization import load_pem_private_key from ecdsa import SigningKey -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 +26,33 @@ 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( + '--alg', '-a', dest='algorythm', help='Encryption algorythm (default: %(default)s)', + action='store', choices=['ecdsa', 'ed25519'], default='ecdsa', + ) - args = parser.parse_args() + args = parser.parse_args(argv) return args +def main(argv=None) -> int: + args = parse_args(argv) + if args.algorythm == 'ecdsa': + private_key = SigningKey.from_pem(args.private_key.read()) + data = args.infile.read() + signature = private_key.sign(data, hashfunc=hashlib.sha256) + args.outfile.write(signature) + return 0 + if args.algorythm == 'ed25519': + private_key = load_pem_private_key(args.private_key.read(), password=None) + data = args.infile.read() + signature = private_key.sign(data) + args.outfile.write(signature) + return 0 + + return 1 + + if __name__ == '__main__': - args = parse_args() - private_key = SigningKey.from_pem(args.private_key.read()) - data = args.infile.read() - signature = private_key.sign(data, hashfunc=hashlib.sha256) - args.outfile.write(signature) + 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..d5084912aa12 --- a/scripts/bootloader/keygen.py +++ b/scripts/bootloader/keygen.py @@ -5,12 +5,14 @@ # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause +import argparse +import sys +from hashlib import sha256, sha512 + 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 as load_pem -from hashlib import sha256 -import argparse -import sys def generate_legal_key(): @@ -23,8 +25,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 +37,69 @@ def generate_legal_key(): return key -if __name__ == '__main__': +def generate_legal_key_sha512(): + """ + 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 + + +def encrypt_with_elliptic_curve(args): + """Generate private and public keys for Elliptic curve cryptography.""" + 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) + + +def encrypt_with_ed25519(args): + """Generate private and public keys for ED25519 cryptography.""" + if args.infile: + private_key = load_pem(args.infile.read(), password=None) + else: + private_key = generate_legal_key_sha512() + if args.private: + private_bytes = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + args.out.write(private_bytes) + if args.public: + public_key = private_key.public_key() + public_bytes = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + args.out.write(public_bytes) + + +def main(argv=None) -> int: parser = argparse.ArgumentParser( description='Generate PEM file.', formatter_class=argparse.RawDescriptionHelpFormatter, @@ -53,21 +117,20 @@ 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='Encryption Algorithm (default: %(default)s)', + required=False, action='store', choices=('ec', 'ed25519'), default='ec' + ) - args = parser.parse_args() - sk = (load_pem(args.infile.read(), password=None) if args.infile else generate_legal_key()) + args = parser.parse_args(argv) - 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.algorithm == 'ed25519': + encrypt_with_ed25519(args) + else: + encrypt_with_elliptic_curve(args) - 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/keygen_test.py b/scripts/bootloader/keygen_test.py new file mode 100644 index 000000000000..ff674a7060aa --- /dev/null +++ b/scripts/bootloader/keygen_test.py @@ -0,0 +1,76 @@ +# +# Copyright (c) 2024 Nordic Semiconductor ASA +# +# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + +from dataclasses import dataclass +from pathlib import Path + +from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key + +from keygen import encrypt_with_ed25519, encrypt_with_elliptic_curve + + +@dataclass +class ArgsMock: + private: bool = False + public: bool = False + infile: Path = None + out: Path = None + + +def sign_message(private_key, message: bytes): + signature = private_key.sign(message) + return signature + + +def verify_signature(public_key, message, signature): + try: + public_key.verify(signature, message) + return True + except Exception: + return False + + +def test_encryption_with_elliptic_curve(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + encrypt_with_ed25519(ArgsMock(private=True, out=private_key_file.open('wb'))) + encrypt_with_elliptic_curve( + ArgsMock(public=True, infile=private_key_file.open('rb'), out=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 = sign_message(private_key, message) + assert verify_signature(public_key, message, signature) + + +def test_encryption_with_ed25519(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + encrypt_with_ed25519(ArgsMock(private=True, out=private_key_file.open('wb'))) + encrypt_with_ed25519( + ArgsMock(public=True, infile=private_key_file.open('rb'), out=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 = sign_message(private_key, message) + assert verify_signature(public_key, message, signature) + + +def test_encryption_with_ed25519_signature_does_not_match(tmpdir): + private_key_file = tmpdir / 'private.pem' + public_key_file = tmpdir / 'public.pem' + encrypt_with_ed25519(ArgsMock(private=True, out=private_key_file.open('wb'))) + encrypt_with_ed25519(ArgsMock(public=True, out=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 = sign_message(private_key, message) + assert verify_signature(public_key, message, signature) is False