Skip to content

Commit

Permalink
scripts/bootloader: Add ed25519/sha512 to scripts
Browse files Browse the repository at this point in the history
Python scripts implementing ed25519 and sha512 support needed
for nsib image signing.

Signed-off-by: Lukasz Fundakowski <[email protected]>
  • Loading branch information
fundakol committed Nov 22, 2024
1 parent d839e3c commit ffa7e39
Show file tree
Hide file tree
Showing 7 changed files with 589 additions and 98 deletions.
51 changes: 39 additions & 12 deletions scripts/bootloader/do_sign.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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.')
Expand All @@ -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())
35 changes: 29 additions & 6 deletions scripts/bootloader/hash.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,43 @@
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause

"""
Hash content of a file.
"""

import hashlib
import sys
import argparse
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'):
Expand All @@ -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())
177 changes: 152 additions & 25 deletions scripts/bootloader/keygen.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -35,7 +40,122 @@ 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()

def get_private_key_bytes(self, outfile: io.BytesIO | None = None) -> bytes:
"""Write private key pem to file."""
private_pem = self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
if outfile is not None:
outfile.write(private_pem)
return private_pem

def get_public_key_bytes(self, outfile: io.BytesIO | None = None) -> bytes:
"""Write public key pem to file."""
public_pem = self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
if outfile is not None:
outfile.write(public_pem)
return public_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()

def get_private_key_bytes(self, outfile: io.BytesIO | None = None) -> bytes:
"""Write private key pem to file."""
private_bytes = self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
if outfile is not None:
outfile.write(private_bytes)
return private_bytes

def get_public_key_bytes(self, outfile: io.BytesIO | None = None) -> bytes:
"""Write public key pem to file."""
public_bytes = self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
if outfile is not None:
outfile.write(public_bytes)
return public_bytes

@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,
Expand All @@ -53,21 +173,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()
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)
args = parser.parse_args(argv)

if args.algorithm == 'ed25519':
ed25519_generator = Ed25519KeysGenerator(args.infile)
if args.private:
ed25519_generator.get_private_key_bytes(args.out)
if args.public:
ed25519_generator.get_public_key_bytes(args.out)
else:
ec_generator = EllipticCurveKeysGenerator(args.infile)
if args.private:
ec_generator.get_private_key_bytes(args.out)
elif args.public:
ec_generator.get_public_key_bytes(args.out)

return 0


if __name__ == '__main__':
sys.exit(main())
4 changes: 4 additions & 0 deletions scripts/bootloader/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import sys

Check warning on line 1 in scripts/bootloader/tests/conftest.py

View workflow job for this annotation

GitHub Actions / call-workflow / Run license checks on patch series (PR)

License Problem

Any license is allowed for this file, but it is recommended to use a more suitable one.
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))
Loading

0 comments on commit ffa7e39

Please sign in to comment.