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 25, 2024
1 parent d839e3c commit 33c4465
Show file tree
Hide file tree
Showing 8 changed files with 694 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())
205 changes: 180 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,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,
Expand All @@ -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())
Loading

0 comments on commit 33c4465

Please sign in to comment.