xous-core/tools/sign_image.py

140 lines
6.5 KiB
Python
Executable file

#! /usr/bin/env python3
import argparse
import sys
from Crypto.IO import PEM # counter-intuitively, this is from pycryptodome: `pip3 install pycryptodome` (ubuntu) `pip install pycryptodome` (maybe others?)
# note: if you did the sensible thing and tried to install the crypto or pycrypto libraries first when you hit this error
# you have to uninstall those before putting in pycryptodome: `pip remove crypto`, and then `pip install pycryptodome` (or pip3 on ubuntu)
from nacl.signing import SigningKey # from pynacl package: `pip3 install pynacl`
from nacl.encoding import RawEncoder
import os.path
import binascii
import hashlib
DEVKEY_PATH='../devkey/dev.key'
SIGNER_VERSION=1
def blob_sign(source, output, key, defile=False):
global SIGNER_VERSION
with open(source, "rb") as source_f:
source = list(source_f.read())
source += int(SIGNER_VERSION).to_bytes(4, 'little') # protect the version number
source += len(source).to_bytes(4, 'little') # append the length to the image, and sign that
# NOTE NOTE NOTE
# can't find a good ASN.1 ED25519 key decoder, just relying on the fact that the last 32 bytes are "always" the private key. always? the private key?
signing_key = SigningKey(key[-32:], encoder=RawEncoder)
signature = signing_key.sign(source, encoder=RawEncoder)
print("signing {} bytes".format(len(source)))
m = hashlib.sha512()
m.update(bytearray(source))
print("hash of {}".format(m.hexdigest()))
print("signing key: [{}]".format(', '.join(hex(x) for x in key[-32:])))
with open(output, "wb") as output_f:
written = 0
written += output_f.write(int(SIGNER_VERSION).to_bytes(4, 'little')) # version number record - mirrored inside the signed data, too
written += output_f.write(len(source).to_bytes(4, 'little')) # record the length of the final signed record (which /also/ includes a length)
written += output_f.write(signature.signature)
print("signature: [{}]".format(', '.join(hex(x) for x in list(signature.signature))))
output_f.write(bytearray([0] * (4096 - written))) # pad out to one page beyond
message = bytearray(signature.message)
if defile is True:
print("WARNING: defiling the image. This corrupts the binary and should cause it to fail the signature check.")
message[16778] ^= 0x1 # flip one bit at some random offset
output_f.write(message) # the actual signed message
def main():
global DEVKEY_PATH
parser = argparse.ArgumentParser(description="Sign binary images for Precursor")
parser.add_argument(
"--loader-image", required=False, help="loader image", type=str, nargs='?', metavar=('loader image'), const='../target/riscv32imac-unknown-xous-elf/release/loader_presign.bin'
)
parser.add_argument(
"--kernel-image", required=False, help="kernel image", type=str, nargs='?', metavar=('kernel image'), const='../target/riscv32imac-unknown-xous-elf/release/xous_presign.img'
)
parser.add_argument(
"--loader-key", required=False, help="loader signing key", type=str, nargs='?', metavar=('loader signing key'), const=DEVKEY_PATH
)
parser.add_argument(
"--kernel-key", required=False, help="kernel signing key", type=str, nargs='?', metavar=('kernel signing key'), const=DEVKEY_PATH
)
parser.add_argument(
"--loader-output", required=False, help="loader output image", type=str, nargs='?', metavar=('loader output image'), const='../target/riscv32imac-unknown-none-elf/release/loader.bin'
)
parser.add_argument(
"--kernel-output", required=False, help="kernel output image", type=str, nargs='?', metavar=('kernel output image'), const='../target/riscv32imac-unknown-none-elf/release/xous.img'
)
parser.add_argument(
"--defile", help="patch the resulting image, to create a test file to catch signature failure", default=False, action="store_true"
)
args = parser.parse_args()
if not len(sys.argv) > 1:
print("No arguments specified, doing nothing. Use --help for more information.")
exit(1)
if args.loader_image and (args.loader_key is None):
loader_key = DEVKEY_PATH
else:
loader_key = args.loader_key
if args.loader_image and (args.loader_output is None):
loader_output = '../target/riscv32imac-unknown-none-elf/release/loader.bin'
else:
loader_output = args.loader_output
if loader_key is not None and loader_key is not DEVKEY_PATH:
with open(loader_key) as loader_f:
loader_pem = loader_f.read()
try:
pem = PEM.decode(loader_pem, None)
except:
passphrase = input("Enter loader key passphrase: ")
pem = PEM.decode(loader_pem, passphrase)
(loader_pkey, pemtype, enc) = pem
if pemtype != 'PRIVATE KEY':
print("PEM type for loader was not a private key. Aborting.")
exit(1)
else:
loader_pkey = None
if loader_pkey != None:
blob_sign(args.loader_image, loader_output, loader_pkey, defile=args.defile)
# for now, the kernel signing path is just signing a blob that is the overall binary
# however, there is some aspiration to sign individual binaries, and to incorporate
# signatures into the kernel tags. leave this path separate so there's an on-ramp
# to add these features
if args.kernel_image and (args.kernel_key is None):
kernel_key = DEVKEY_PATH
else:
kernel_key = args.kernel_key
if args.kernel_image and (args.kernel_output is None):
kernel_output = '../target/riscv32imac-unknown-none-elf/release/xous.img'
else:
kernel_output = args.kernel_output
if kernel_key is not None and kernel_key is not DEVKEY_PATH:
with open(kernel_key) as kernel_f:
kernel_pem = kernel_f.read()
try:
pem = PEM.decode(kernel_pem, None)
except:
passphrase = input("Enter kernel key passphrase: ")
pem = PEM.decode(kernel_pem, passphrase)
(kernel_pkey, pemtype, enc) = pem
if pemtype != 'PRIVATE KEY':
print("PEM type for kernel was not a private key. Aborting.")
exit(1)
else:
kernel_pkey = None
if kernel_pkey != None:
blob_sign(args.kernel_image, kernel_output, kernel_pkey, defile=args.defile)
if __name__ == "__main__":
main()
exit(0)