Skip to content

Commit

Permalink
scripts: Replace ecdsa library with cryptography
Browse files Browse the repository at this point in the history
Simplify python scripts by removing ecdsa library and
rewriting code with cryptography library,
which provides same functionality.

Signed-off-by: Lukasz Fundakowski <[email protected]>
  • Loading branch information
fundakol committed Jan 8, 2025
1 parent b2298e7 commit afbc748
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 151 deletions.
32 changes: 21 additions & 11 deletions scripts/bootloader/do_sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

import argparse
import contextlib
import hashlib
import sys
from pathlib import Path
from typing import BinaryIO, Generator

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from ecdsa.keys import SigningKey # type: ignore[import-untyped]
from intelhex import IntelHex # type: ignore[import-untyped]


Expand Down Expand Up @@ -66,11 +67,14 @@ def hex_to_binary(input_hex_file: str) -> bytes:
def sign_with_ecdsa(
private_key_file: Path, input_file: Path, output_file: Path | None = None
) -> int:
with open(private_key_file, 'r') as f:
private_key = SigningKey.from_pem(f.read())
with open(private_key_file, 'rb') as f:
private_key = load_pem_private_key(f.read(), password=None)
if not isinstance(private_key, EllipticCurvePrivateKey):
raise SystemExit(f'Private key file {private_key_file} is not Elliptic Curve key')

with open(input_file, 'rb') as f:
data = f.read()
signature = private_key.sign(data, hashfunc=hashlib.sha256)
signature = private_key.sign(data, ec.ECDSA(hashes.SHA256()))
with open_stream(output_file) as stream:
stream.write(signature)
return 0
Expand All @@ -80,7 +84,10 @@ def sign_with_ed25519(
private_key_file: Path, input_file: Path, output_file: Path | None = None
) -> int:
with open(private_key_file, 'rb') as f:
private_key: Ed25519PrivateKey = load_pem_private_key(f.read(), password=None) # type: ignore[assignment]
private_key = load_pem_private_key(f.read(), password=None)
if not isinstance(private_key, Ed25519PrivateKey):
raise SystemExit(f'Private key file {private_key_file} is not Ed25519 key')

if str(input_file).endswith('.hex'):
data = hex_to_binary(str(input_file))
else:
Expand All @@ -92,13 +99,16 @@ def sign_with_ed25519(
return 0


ALGORITHMS = {
'ecdsa': sign_with_ecdsa,
'ed25519': sign_with_ed25519,
}


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
sign_function = ALGORITHMS[args.algorithm]
return sign_function(args.private_key, args.infile, args.outfile)


if __name__ == '__main__':
Expand Down
147 changes: 79 additions & 68 deletions scripts/bootloader/keygen.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@

from __future__ import annotations

import abc
import argparse
import sys
from hashlib import sha256, sha512
from typing import BinaryIO
from typing import BinaryIO, Type

from cryptography.exceptions import InvalidSignature
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 import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, ed25519
from cryptography.hazmat.primitives.serialization import load_pem_private_key


Expand Down Expand Up @@ -62,44 +61,53 @@ def generate_legal_key_for_ed25519():
return key


class EllipticCurveKeysGenerator:
"""Generate private and public keys for Elliptic Curve cryptography."""
class KeysGeneratorBase(abc.ABC):

def __init__(self, infile: BinaryIO | 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()
self.private_key = self._generate_private_key()
else:
self.private_key = load_pem_private_key(infile.read(), password=None)
self.public_key = self.private_key.public_key()

@property
@abc.abstractmethod
def private_key_pem(self) -> bytes:
return self.private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
pass

@property
@abc.abstractmethod
def public_key_pem(self) -> bytes:
pass

@abc.abstractmethod
def _generate_private_key(self):
pass

@staticmethod
@abc.abstractmethod
def sign_message(private_key, message: bytes) -> bytes:
pass

@staticmethod
@abc.abstractmethod
def verify_signature(
public_key, message: bytes, signature: bytes
) -> bool:
pass

def write_private_key_pem(self, outfile: BinaryIO) -> 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)
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: BinaryIO) -> bytes:
"""
Write public key pem to file and return it.
Expand All @@ -109,31 +117,48 @@ def write_public_key_pem(self, outfile: BinaryIO) -> bytes:
outfile.write(self.public_key_pem)
return self.public_key_pem


class EllipticCurveKeysGenerator(KeysGeneratorBase):
"""Generate private and public keys for Elliptic Curve cryptography."""

def _generate_private_key(self):
return generate_legal_key_for_elliptic_curve()

@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(),
)

@property
def public_key_pem(self) -> bytes:
return self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)

@staticmethod
def verify_signature(public_key, message: bytes, signature: bytes) -> bool:
def verify_signature(
public_key: ec.EllipticCurvePublicKey, message: bytes, signature: bytes
) -> bool:
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
return True
except InvalidSignature:
return False

@staticmethod
def sign_message(private_key, message: bytes) -> bytes:
def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes:
return private_key.sign(message, ec.ECDSA(hashes.SHA256()))


class Ed25519KeysGenerator:
class Ed25519KeysGenerator(KeysGeneratorBase):
"""Generate private and public keys for ED25519 cryptography."""

def __init__(self, infile: BinaryIO | None = None) -> None:
"""
:param infile: A file-like object to read the private key.
"""
if infile is None:
self.private_key: ed25519.Ed25519PrivateKey = generate_legal_key_for_ed25519()
else:
self.private_key = load_pem_private_key(infile.read(), password=None) # type: ignore[assignment]
self.public_key: ed25519.Ed25519PublicKey = self.private_key.public_key()
def _generate_private_key(self):
return generate_legal_key_for_ed25519()

@property
def private_key_pem(self) -> bytes:
Expand All @@ -143,50 +168,40 @@ def private_key_pem(self) -> bytes:
encryption_algorithm=serialization.NoEncryption()
)

def write_private_key_pem(self, outfile: BinaryIO) -> bytes:
"""
Write private key pem to file and return it.
:param outfile: A file-like object to write the private key.
"""
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: BinaryIO) -> 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: ed25519.Ed25519PublicKey, message: bytes, signature: bytes) -> bool:
def verify_signature(
public_key: ed25519.Ed25519PublicKey, message: bytes, signature: bytes
) -> bool:
try:
public_key.verify(signature, message)
return True
except InvalidSignature:
return False

@staticmethod
def sign_message(private_key, message: bytes) -> bytes:
def sign_message(private_key: ed25519.Ed25519PrivateKey, message: bytes) -> bytes:
return private_key.sign(message)


ALGORITHMS: dict[str, Type[KeysGeneratorBase]] = {
"ed25519": Ed25519KeysGenerator,
"ec": EllipticCurveKeysGenerator,
}


def main(argv=None) -> int:
parser = argparse.ArgumentParser(
description='Generate PEM file.',
formatter_class=argparse.RawDescriptionHelpFormatter,
allow_abbrev=False)
allow_abbrev=False
)

priv_pub_group = parser.add_mutually_exclusive_group(required=True)
priv_pub_group.add_argument('--private', required=False, action='store_true',
Expand All @@ -207,19 +222,15 @@ def main(argv=None) -> int:

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)
try:
generator = ALGORITHMS[args.algorithm](args.infile)
except KeyError:
sys.exit(f'Unknown algorithm {args.algorithm}.')

if args.private:
generator.write_private_key_pem(args.out)
if args.public:
generator.write_public_key_pem(args.out)
return 0


Expand Down
28 changes: 8 additions & 20 deletions scripts/bootloader/tests/do_sign_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,11 @@
#
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause

import hashlib

from ecdsa.keys import VerifyingKey, BadSignatureError # type: ignore[import-untyped]
from ecdsa.util import sigdecode_string # type: ignore[import-untyped]

from cryptography.hazmat.primitives.serialization import load_pem_public_key
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'
Expand All @@ -39,9 +27,10 @@ def test_if_file_is_properly_signed_with_ec_key(tmpdir):
output_file=signature_file,
)

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)
public_key = load_pem_public_key(public_key_file.open('rb').read())
assert EllipticCurveKeysGenerator.verify_signature(
public_key, message, signature_file.open('br').read()
)


def test_if_validation_does_not_pass_for_wrong_ec_key(tmpdir):
Expand All @@ -62,10 +51,9 @@ def test_if_validation_does_not_pass_for_wrong_ec_key(tmpdir):
output_file=signature_file,
)

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
public_key = load_pem_public_key(public_key_file.open('rb').read())
assert EllipticCurveKeysGenerator.verify_signature(
public_key, message, signature_file.open('br').read()
) is False


Expand Down
13 changes: 5 additions & 8 deletions scripts/bootloader/tests/validation_data_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,13 @@
import hashlib
import textwrap

import ecdsa # type: ignore[import-untyped]

import do_sign
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from hash import generate_hash_digest
from keygen import Ed25519KeysGenerator, EllipticCurveKeysGenerator
from validation_data import (
Ed25519SignatureValidator,
EcdsaSignatureValidator,
main as validation_data_main
)
from validation_data import EcdsaSignatureValidator, Ed25519SignatureValidator
from validation_data import main as validation_data_main


DUMMY_ZEPHYR_HEX = textwrap.dedent("""\
:1098000050110020EDA60000E5DE0000D9A6000002
Expand Down Expand Up @@ -56,7 +53,7 @@ def test_data_validation_for_ec(tmpdir):

do_sign.sign_with_ecdsa(private_key_file, hash_file, message_signature_file)

public_key = ecdsa.VerifyingKey.from_pem(public_key_file.read())
public_key = load_pem_public_key(public_key_file.open('rb').read())
EcdsaSignatureValidator(hashfunc=hashlib.sha256).append_validation_data(
signature_file=message_signature_file,
input_file=zephyr_hex_file,
Expand Down
Loading

0 comments on commit afbc748

Please sign in to comment.