|
3 | 3 | # Copyright (c) 2018 Nordic Semiconductor ASA |
4 | 4 | # |
5 | 5 | # SPDX-License-Identifier: LicenseRef-Nordic-5-Clause |
| 6 | +from __future__ import annotations |
6 | 7 |
|
7 | | - |
8 | | -import sys |
9 | 8 | import argparse |
| 9 | +import contextlib |
10 | 10 | import hashlib |
11 | | -from ecdsa import SigningKey |
| 11 | +import sys |
| 12 | +from pathlib import Path |
| 13 | +from typing import BinaryIO, Generator |
| 14 | + |
| 15 | +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey |
| 16 | +from cryptography.hazmat.primitives.serialization import load_pem_private_key |
| 17 | +from ecdsa.keys import SigningKey # type: ignore[import-untyped] |
| 18 | +from intelhex import IntelHex # type: ignore[import-untyped] |
12 | 19 |
|
13 | 20 |
|
14 | | -def parse_args(): |
| 21 | +def parse_args(argv=None): |
15 | 22 | parser = argparse.ArgumentParser( |
16 | 23 | description='Sign data from stdin or file.', |
17 | 24 | formatter_class=argparse.RawDescriptionHelpFormatter, |
18 | | - allow_abbrev=False) |
| 25 | + allow_abbrev=False |
| 26 | + ) |
19 | 27 |
|
20 | | - parser.add_argument('-k', '--private-key', required=True, type=argparse.FileType('rb'), |
| 28 | + parser.add_argument('-k', '--private-key', required=True, type=Path, |
21 | 29 | help='Private key to use.') |
22 | 30 | parser.add_argument('-i', '--in', '-in', required=False, dest='infile', |
23 | | - type=argparse.FileType('rb'), default=sys.stdin.buffer, |
| 31 | + type=Path, default=sys.stdin.buffer, |
24 | 32 | help='Sign the contents of the specified file instead of stdin.') |
25 | 33 | parser.add_argument('-o', '--out', '-out', required=False, dest='outfile', |
26 | | - type=argparse.FileType('wb'), default=sys.stdout.buffer, |
| 34 | + type=Path, default=None, |
27 | 35 | help='Write the signature to the specified file instead of stdout.') |
| 36 | + parser.add_argument( |
| 37 | + '--algorithm', '-a', dest='algorithm', |
| 38 | + help='Signing algorithm (default: %(default)s)', |
| 39 | + action='store', choices=['ecdsa', 'ed25519'], default='ecdsa', |
| 40 | + ) |
28 | 41 |
|
29 | | - args = parser.parse_args() |
| 42 | + args = parser.parse_args(argv) |
30 | 43 |
|
31 | 44 | return args |
32 | 45 |
|
33 | 46 |
|
34 | | -if __name__ == '__main__': |
35 | | - args = parse_args() |
36 | | - private_key = SigningKey.from_pem(args.private_key.read()) |
37 | | - data = args.infile.read() |
| 47 | +@contextlib.contextmanager |
| 48 | +def open_stream(output_file: Path | None = None) -> Generator[BinaryIO, None, None]: |
| 49 | + if output_file is not None: |
| 50 | + stream = open(output_file, 'wb') |
| 51 | + try: |
| 52 | + yield stream |
| 53 | + finally: |
| 54 | + stream.close() |
| 55 | + else: |
| 56 | + yield sys.stdout.buffer |
| 57 | + |
| 58 | + |
| 59 | +def hex_to_binary(input_hex_file: str) -> bytes: |
| 60 | + ih = IntelHex(input_hex_file) |
| 61 | + ih.padding = 0xff # Allows hashing with empty regions |
| 62 | + data = ih.tobinstr() |
| 63 | + return data |
| 64 | + |
| 65 | + |
| 66 | +def sign_with_ecdsa( |
| 67 | + private_key_file: Path, input_file: Path, output_file: Path | None = None |
| 68 | +) -> int: |
| 69 | + with open(private_key_file, 'r') as f: |
| 70 | + private_key = SigningKey.from_pem(f.read()) |
| 71 | + with open(input_file, 'rb') as f: |
| 72 | + data = f.read() |
38 | 73 | signature = private_key.sign(data, hashfunc=hashlib.sha256) |
39 | | - args.outfile.write(signature) |
| 74 | + with open_stream(output_file) as stream: |
| 75 | + stream.write(signature) |
| 76 | + return 0 |
| 77 | + |
| 78 | + |
| 79 | +def sign_with_ed25519( |
| 80 | + private_key_file: Path, input_file: Path, output_file: Path | None = None |
| 81 | +) -> int: |
| 82 | + with open(private_key_file, 'rb') as f: |
| 83 | + private_key: Ed25519PrivateKey = load_pem_private_key(f.read(), password=None) # type: ignore[assignment] |
| 84 | + if str(input_file).endswith('.hex'): |
| 85 | + data = hex_to_binary(str(input_file)) |
| 86 | + else: |
| 87 | + with open(input_file, 'rb') as f: |
| 88 | + data = f.read() |
| 89 | + signature = private_key.sign(data) |
| 90 | + with open_stream(output_file) as stream: |
| 91 | + stream.write(signature) |
| 92 | + return 0 |
| 93 | + |
| 94 | + |
| 95 | +def main(argv=None) -> int: |
| 96 | + args = parse_args(argv) |
| 97 | + if args.algorithm == 'ecdsa': |
| 98 | + return sign_with_ecdsa(args.private_key, args.infile, args.outfile) |
| 99 | + if args.algorithm == 'ed25519': |
| 100 | + return sign_with_ed25519(args.private_key, args.infile, args.outfile) |
| 101 | + return 1 |
| 102 | + |
| 103 | + |
| 104 | +if __name__ == '__main__': |
| 105 | + sys.exit(main()) |
0 commit comments