Initial commit

Initial commit.
This commit is contained in:
kntran1
2026-03-23 14:40:39 -05:00
parent e84b2b4166
commit 4e2a5258a5
872 changed files with 165227 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# Copyright 2017 Linaro Limited
# Copyright 2023 Arm Limited
#
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Cryptographic key management for imgtool.
"""
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.rsa import (
RSAPrivateKey, RSAPublicKey)
from cryptography.hazmat.primitives.asymmetric.ec import (
EllipticCurvePrivateKey, EllipticCurvePublicKey)
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey, Ed25519PublicKey)
from cryptography.hazmat.primitives.asymmetric.x25519 import (
X25519PrivateKey, X25519PublicKey)
from .rsa import RSA, RSAPublic, RSAUsageError, RSA_KEY_SIZES
from .ecdsa import (ECDSA256P1, ECDSA256P1Public,
ECDSA384P1, ECDSA384P1Public, ECDSAUsageError)
from .ed25519 import Ed25519, Ed25519Public, Ed25519UsageError
from .x25519 import X25519, X25519Public, X25519UsageError
class PasswordRequired(Exception):
"""Raised to indicate that the key is password protected, but a
password was not specified."""
pass
def load(path, passwd=None):
"""Try loading a key from the given path.
Returns None if the password wasn't specified."""
with open(path, 'rb') as f:
raw_pem = f.read()
try:
pk = serialization.load_pem_private_key(
raw_pem,
password=passwd,
backend=default_backend())
# Unfortunately, the crypto library raises unhelpful exceptions,
# so we have to look at the text.
except TypeError as e:
msg = str(e)
if "private key is encrypted" in msg:
return None
raise e
except ValueError:
# This seems to happen if the key is a public key, let's try
# loading it as a public key.
pk = serialization.load_pem_public_key(
raw_pem,
backend=default_backend())
if isinstance(pk, RSAPrivateKey):
if pk.key_size not in RSA_KEY_SIZES:
raise Exception("Unsupported RSA key size: " + pk.key_size)
return RSA(pk)
elif isinstance(pk, RSAPublicKey):
if pk.key_size not in RSA_KEY_SIZES:
raise Exception("Unsupported RSA key size: " + pk.key_size)
return RSAPublic(pk)
elif isinstance(pk, EllipticCurvePrivateKey):
if pk.curve.name not in ('secp256r1', 'secp384r1'):
raise Exception("Unsupported EC curve: " + pk.curve.name)
if pk.key_size not in (256, 384):
raise Exception("Unsupported EC size: " + pk.key_size)
if pk.curve.name == 'secp256r1':
return ECDSA256P1(pk)
elif pk.curve.name == 'secp384r1':
return ECDSA384P1(pk)
elif isinstance(pk, EllipticCurvePublicKey):
if pk.curve.name not in ('secp256r1', 'secp384r1'):
raise Exception("Unsupported EC curve: " + pk.curve.name)
if pk.key_size not in (256, 384):
raise Exception("Unsupported EC size: " + pk.key_size)
if pk.curve.name == 'secp256r1':
return ECDSA256P1Public(pk)
elif pk.curve.name == 'secp384r1':
return ECDSA384P1Public(pk)
elif isinstance(pk, Ed25519PrivateKey):
return Ed25519(pk)
elif isinstance(pk, Ed25519PublicKey):
return Ed25519Public(pk)
elif isinstance(pk, X25519PrivateKey):
return X25519(pk)
elif isinstance(pk, X25519PublicKey):
return X25519Public(pk)
else:
raise Exception("Unknown key type: " + str(type(pk)))

View File

@@ -0,0 +1,289 @@
"""
ECDSA key management
"""
# SPDX-License-Identifier: Apache-2.0
import os.path
import hashlib
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.hashes import SHA256, SHA384
from .general import KeyClass
from .privatebytes import PrivateBytesMixin
class ECDSAUsageError(Exception):
pass
class ECDSAPublicKey(KeyClass):
"""
Wrapper around an ECDSA public key.
"""
def __init__(self, key):
self.key = key
def _unsupported(self, name):
raise ECDSAUsageError("Operation {} requires private key".format(name))
def _get_public(self):
return self.key
def get_public_bytes(self):
# The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
return self._get_public().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_public_pem(self):
return self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_private_bytes(self, minimal, format):
self._unsupported('get_private_bytes')
def export_private(self, path, passwd=None):
self._unsupported('export_private')
def export_public(self, path):
"""Write the public key to the given file."""
pem = self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
with open(path, 'wb') as f:
f.write(pem)
class ECDSAPrivateKey(PrivateBytesMixin):
"""
Wrapper around an ECDSA private key.
"""
def __init__(self, key):
self.key = key
def _get_public(self):
return self.key.public_key()
def _build_minimal_ecdsa_privkey(self, der, format):
'''
Builds a new DER that only includes the EC private key, removing the
public key that is added as an "optional" BITSTRING.
'''
if format == serialization.PrivateFormat.OpenSSH:
print(os.path.basename(__file__) +
': Warning: --minimal is supported only for PKCS8 '
'or TraditionalOpenSSL formats')
return bytearray(der)
EXCEPTION_TEXT = "Error parsing ecdsa key. Please submit an issue!"
if format == serialization.PrivateFormat.PKCS8:
offset_PUB = 68 # where the context specific TLV starts (tag 0xA1)
if der[offset_PUB] != 0xa1:
raise ECDSAUsageError(EXCEPTION_TEXT)
len_PUB = der[offset_PUB + 1] + 2 # + 2 for 0xA1 0x44 bytes
b = bytearray(der[:offset_PUB]) # remove the TLV with the PUB key
offset_SEQ = 29
if b[offset_SEQ] != 0x30:
raise ECDSAUsageError(EXCEPTION_TEXT)
b[offset_SEQ + 1] -= len_PUB
offset_OCT_STR = 27
if b[offset_OCT_STR] != 0x04:
raise ECDSAUsageError(EXCEPTION_TEXT)
b[offset_OCT_STR + 1] -= len_PUB
if b[0] != 0x30 or b[1] != 0x81:
raise ECDSAUsageError(EXCEPTION_TEXT)
# as b[1] has bit7 set, the length is on b[2]
b[2] -= len_PUB
if b[2] < 0x80:
del(b[1])
elif format == serialization.PrivateFormat.TraditionalOpenSSL:
offset_PUB = 51
if der[offset_PUB] != 0xA1:
raise ECDSAUsageError(EXCEPTION_TEXT)
len_PUB = der[offset_PUB + 1] + 2
b = bytearray(der[0:offset_PUB])
b[1] -= len_PUB
return b
_VALID_FORMATS = {
'pkcs8': serialization.PrivateFormat.PKCS8,
'openssl': serialization.PrivateFormat.TraditionalOpenSSL
}
_DEFAULT_FORMAT = 'pkcs8'
def get_private_bytes(self, minimal, format):
format, priv = self._get_private_bytes(minimal,
format, ECDSAUsageError)
if minimal:
priv = self._build_minimal_ecdsa_privkey(
priv, self._VALID_FORMATS[format])
return priv
def export_private(self, path, passwd=None):
"""Write the private key to the given file, protecting it with '
'the optional password."""
if passwd is None:
enc = serialization.NoEncryption()
else:
enc = serialization.BestAvailableEncryption(passwd)
pem = self.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=enc)
with open(path, 'wb') as f:
f.write(pem)
class ECDSA256P1Public(ECDSAPublicKey):
"""
Wrapper around an ECDSA (p256) public key.
"""
def __init__(self, key):
super().__init__(key)
self.key = key
def shortname(self):
return "ecdsa"
def sig_type(self):
return "ECDSA256_SHA256"
def sig_tlv(self):
return "ECDSASIG"
def sig_len(self):
# Early versions of MCUboot (< v1.5.0) required ECDSA
# signatures to be padded to 72 bytes. Because the DER
# encoding is done with signed integers, the size of the
# signature will vary depending on whether the high bit is set
# in each value. This padding was done in a
# not-easily-reversible way (by just adding zeros).
#
# The signing code no longer requires this padding, and newer
# versions of MCUboot don't require it. But, continue to
# return the total length so that the padding can be done if
# requested.
return 72
def verify(self, signature, payload):
# strip possible paddings added during sign
signature = signature[:signature[1] + 2]
k = self.key
if isinstance(self.key, ec.EllipticCurvePrivateKey):
k = self.key.public_key()
return k.verify(signature=signature, data=payload,
signature_algorithm=ec.ECDSA(SHA256()))
class ECDSA256P1(ECDSAPrivateKey, ECDSA256P1Public):
"""
Wrapper around an ECDSA (p256) private key.
"""
def __init__(self, key):
super().__init__(key)
self.key = key
self.pad_sig = False
@staticmethod
def generate():
pk = ec.generate_private_key(
ec.SECP256R1(),
backend=default_backend())
return ECDSA256P1(pk)
def raw_sign(self, payload):
"""Return the actual signature"""
return self.key.sign(
data=payload,
signature_algorithm=ec.ECDSA(SHA256()))
def sign(self, payload):
sig = self.raw_sign(payload)
if self.pad_sig:
# To make fixed length, pad with one or two zeros.
sig += b'\000' * (self.sig_len() - len(sig))
return sig
else:
return sig
class ECDSA384P1Public(ECDSAPublicKey):
"""
Wrapper around an ECDSA (p384) public key.
"""
def __init__(self, key):
super().__init__(key)
self.key = key
def shortname(self):
return "ecdsap384"
def sig_type(self):
return "ECDSA384_SHA384"
def sig_tlv(self):
return "ECDSASIG"
def sig_len(self):
# Early versions of MCUboot (< v1.5.0) required ECDSA
# signatures to be padded to a fixed length. Because the DER
# encoding is done with signed integers, the size of the
# signature will vary depending on whether the high bit is set
# in each value. This padding was done in a
# not-easily-reversible way (by just adding zeros).
#
# The signing code no longer requires this padding, and newer
# versions of MCUboot don't require it. But, continue to
# return the total length so that the padding can be done if
# requested.
return 103
def verify(self, signature, payload):
# strip possible paddings added during sign
signature = signature[:signature[1] + 2]
k = self.key
if isinstance(self.key, ec.EllipticCurvePrivateKey):
k = self.key.public_key()
return k.verify(signature=signature, data=payload,
signature_algorithm=ec.ECDSA(SHA384()))
class ECDSA384P1(ECDSAPrivateKey, ECDSA384P1Public):
"""
Wrapper around an ECDSA (p384) private key.
"""
def __init__(self, key):
"""key should be an instance of EllipticCurvePrivateKey"""
super().__init__(key)
self.key = key
self.pad_sig = False
@staticmethod
def generate():
pk = ec.generate_private_key(
ec.SECP384R1(),
backend=default_backend())
return ECDSA384P1(pk)
def raw_sign(self, payload):
"""Return the actual signature"""
return self.key.sign(
data=payload,
signature_algorithm=ec.ECDSA(SHA384()))
def sign(self, payload):
sig = self.raw_sign(payload)
if self.pad_sig:
# To make fixed length, pad with one or two zeros.
sig += b'\000' * (self.sig_len() - len(sig))
return sig
else:
return sig

View File

@@ -0,0 +1,120 @@
"""
Tests for ECDSA keys
"""
# SPDX-License-Identifier: Apache-2.0
import io
import os.path
import sys
import tempfile
import unittest
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.hashes import SHA256
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from imgtool.keys import load, ECDSA256P1, ECDSAUsageError
class EcKeyGeneration(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.TemporaryDirectory()
def tname(self, base):
return os.path.join(self.test_dir.name, base)
def tearDown(self):
self.test_dir.cleanup()
def test_keygen(self):
name1 = self.tname("keygen.pem")
k = ECDSA256P1.generate()
k.export_private(name1, b'secret')
self.assertIsNone(load(name1))
k2 = load(name1, b'secret')
pubname = self.tname('keygen-pub.pem')
k2.export_public(pubname)
pk2 = load(pubname)
# We should be able to export the public key from the loaded
# public key, but not the private key.
pk2.export_public(self.tname('keygen-pub2.pem'))
self.assertRaises(ECDSAUsageError,
pk2.export_private, self.tname('keygen-priv2.pem'))
def test_emit(self):
"""Basic sanity check on the code emitters."""
k = ECDSA256P1.generate()
pubpem = io.StringIO()
k.emit_public_pem(pubpem)
self.assertIn("BEGIN PUBLIC KEY", pubpem.getvalue())
self.assertIn("END PUBLIC KEY", pubpem.getvalue())
ccode = io.StringIO()
k.emit_c_public(ccode)
self.assertIn("ecdsa_pub_key", ccode.getvalue())
self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
hashccode = io.StringIO()
k.emit_c_public_hash(hashccode)
self.assertIn("ecdsa_pub_key_hash", hashccode.getvalue())
self.assertIn("ecdsa_pub_key_hash_len", hashccode.getvalue())
rustcode = io.StringIO()
k.emit_rust_public(rustcode)
self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
# raw data - bytes
pubraw = io.BytesIO()
k.emit_raw_public(pubraw)
self.assertTrue(len(pubraw.getvalue()) > 0)
hashraw = io.BytesIO()
k.emit_raw_public_hash(hashraw)
self.assertTrue(len(hashraw.getvalue()) > 0)
def test_emit_pub(self):
"""Basic sanity check on the code emitters, from public key."""
pubname = self.tname("public.pem")
k = ECDSA256P1.generate()
k.export_public(pubname)
k2 = load(pubname)
ccode = io.StringIO()
k2.emit_c_public(ccode)
self.assertIn("ecdsa_pub_key", ccode.getvalue())
self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
rustcode = io.StringIO()
k2.emit_rust_public(rustcode)
self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
def test_sig(self):
k = ECDSA256P1.generate()
buf = b'This is the message'
sig = k.raw_sign(buf)
# The code doesn't have any verification, so verify this
# manually.
k.key.public_key().verify(
signature=sig,
data=buf,
signature_algorithm=ec.ECDSA(SHA256()))
# Modify the message to make sure the signature fails.
self.assertRaises(InvalidSignature,
k.key.public_key().verify,
signature=sig,
data=b'This is thE message',
signature_algorithm=ec.ECDSA(SHA256()))
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,111 @@
"""
ED25519 key management
"""
# SPDX-License-Identifier: Apache-2.0
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
from .general import KeyClass
class Ed25519UsageError(Exception):
pass
class Ed25519Public(KeyClass):
def __init__(self, key):
self.key = key
def shortname(self):
return "ed25519"
def _unsupported(self, name):
raise Ed25519UsageError("Operation {} requires private key".format(name))
def _get_public(self):
return self.key
def get_public_bytes(self):
# The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
return self._get_public().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_public_pem(self):
return self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_private_bytes(self, minimal, format):
self._unsupported('get_private_bytes')
def export_private(self, path, passwd=None):
self._unsupported('export_private')
def export_public(self, path):
"""Write the public key to the given file."""
pem = self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
with open(path, 'wb') as f:
f.write(pem)
def sig_type(self):
return "ED25519"
def sig_tlv(self):
return "ED25519"
def sig_len(self):
return 64
def verify_digest(self, signature, digest):
"""Verify that signature is valid for given digest"""
k = self.key
if isinstance(self.key, ed25519.Ed25519PrivateKey):
k = self.key.public_key()
return k.verify(signature=signature, data=digest)
class Ed25519(Ed25519Public):
"""
Wrapper around an ED25519 private key.
"""
def __init__(self, key):
"""key should be an instance of EllipticCurvePrivateKey"""
self.key = key
@staticmethod
def generate():
pk = ed25519.Ed25519PrivateKey.generate()
return Ed25519(pk)
def _get_public(self):
return self.key.public_key()
def get_private_bytes(self, minimal, format):
raise Ed25519UsageError("Operation not supported with {} keys".format(
self.shortname()))
def export_private(self, path, passwd=None):
"""
Write the private key to the given file, protecting it with the
optional password.
"""
if passwd is None:
enc = serialization.NoEncryption()
else:
enc = serialization.BestAvailableEncryption(passwd)
pem = self.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=enc)
with open(path, 'wb') as f:
f.write(pem)
def sign_digest(self, digest):
"""Return the actual signature"""
return self.key.sign(data=digest)

View File

@@ -0,0 +1,124 @@
"""
Tests for ECDSA keys
"""
# SPDX-License-Identifier: Apache-2.0
import hashlib
import io
import os.path
import sys
import tempfile
import unittest
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric import ed25519
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
from imgtool.keys import load, Ed25519, Ed25519UsageError
class Ed25519KeyGeneration(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.TemporaryDirectory()
def tname(self, base):
return os.path.join(self.test_dir.name, base)
def tearDown(self):
self.test_dir.cleanup()
def test_keygen(self):
name1 = self.tname("keygen.pem")
k = Ed25519.generate()
k.export_private(name1, b'secret')
self.assertIsNone(load(name1))
k2 = load(name1, b'secret')
pubname = self.tname('keygen-pub.pem')
k2.export_public(pubname)
pk2 = load(pubname)
# We should be able to export the public key from the loaded
# public key, but not the private key.
pk2.export_public(self.tname('keygen-pub2.pem'))
self.assertRaises(Ed25519UsageError,
pk2.export_private, self.tname('keygen-priv2.pem'))
def test_emit(self):
"""Basic sanity check on the code emitters."""
k = Ed25519.generate()
pubpem = io.StringIO()
k.emit_public_pem(pubpem)
self.assertIn("BEGIN PUBLIC KEY", pubpem.getvalue())
self.assertIn("END PUBLIC KEY", pubpem.getvalue())
ccode = io.StringIO()
k.emit_c_public(ccode)
self.assertIn("ed25519_pub_key", ccode.getvalue())
self.assertIn("ed25519_pub_key_len", ccode.getvalue())
hashccode = io.StringIO()
k.emit_c_public_hash(hashccode)
self.assertIn("ed25519_pub_key_hash", hashccode.getvalue())
self.assertIn("ed25519_pub_key_hash_len", hashccode.getvalue())
rustcode = io.StringIO()
k.emit_rust_public(rustcode)
self.assertIn("ED25519_PUB_KEY", rustcode.getvalue())
# raw data - bytes
pubraw = io.BytesIO()
k.emit_raw_public(pubraw)
self.assertTrue(len(pubraw.getvalue()) > 0)
hashraw = io.BytesIO()
k.emit_raw_public_hash(hashraw)
self.assertTrue(len(hashraw.getvalue()) > 0)
def test_emit_pub(self):
"""Basic sanity check on the code emitters, from public key."""
pubname = self.tname("public.pem")
k = Ed25519.generate()
k.export_public(pubname)
k2 = load(pubname)
ccode = io.StringIO()
k2.emit_c_public(ccode)
self.assertIn("ed25519_pub_key", ccode.getvalue())
self.assertIn("ed25519_pub_key_len", ccode.getvalue())
rustcode = io.StringIO()
k2.emit_rust_public(rustcode)
self.assertIn("ED25519_PUB_KEY", rustcode.getvalue())
def test_sig(self):
k = Ed25519.generate()
buf = b'This is the message'
sha = hashlib.sha256()
sha.update(buf)
digest = sha.digest()
sig = k.sign_digest(digest)
# The code doesn't have any verification, so verify this
# manually.
k.key.public_key().verify(signature=sig, data=digest)
# Modify the message to make sure the signature fails.
sha = hashlib.sha256()
sha.update(b'This is thE message')
new_digest = sha.digest()
self.assertRaises(InvalidSignature,
k.key.public_key().verify,
signature=sig,
data=new_digest)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,115 @@
"""General key class."""
# SPDX-License-Identifier: Apache-2.0
import binascii
import io
import os
import sys
from cryptography.hazmat.primitives.hashes import Hash, SHA256
AUTOGEN_MESSAGE = "/* Autogenerated by imgtool.py, do not edit. */"
class FileHandler(object):
def __init__(self, file, *args, **kwargs):
self.file_in = file
self.args = args
self.kwargs = kwargs
def __enter__(self):
if isinstance(self.file_in, (str, bytes, os.PathLike)):
self.file = open(self.file_in, *self.args, **self.kwargs)
else:
self.file = self.file_in
return self.file
def __exit__(self, *args):
if self.file != self.file_in:
self.file.close()
class KeyClass(object):
def _emit(self, header, trailer, encoded_bytes, indent, file=sys.stdout,
len_format=None):
with FileHandler(file, 'w') as file:
self._emit_to_output(header, trailer, encoded_bytes, indent,
file, len_format)
def _emit_to_output(self, header, trailer, encoded_bytes, indent, file,
len_format):
print(AUTOGEN_MESSAGE, file=file)
print(header, end='', file=file)
for count, b in enumerate(encoded_bytes):
if count % 8 == 0:
print("\n" + indent, end='', file=file)
else:
print(" ", end='', file=file)
print("0x{:02x},".format(b), end='', file=file)
print("\n" + trailer, file=file)
if len_format is not None:
print(len_format.format(len(encoded_bytes)), file=file)
def _emit_raw(self, encoded_bytes, file):
with FileHandler(file, 'wb') as file:
try:
# file.buffer is not part of the TextIOBase API
# and may not exist in some implementations.
file.buffer.write(encoded_bytes)
except AttributeError:
# raw binary data, can be for example io.BytesIO
file.write(encoded_bytes)
def emit_c_public(self, file=sys.stdout):
self._emit(
header="const unsigned char {}_pub_key[] = {{"
.format(self.shortname()),
trailer="};",
encoded_bytes=self.get_public_bytes(),
indent=" ",
len_format="const unsigned int {}_pub_key_len = {{}};"
.format(self.shortname()),
file=file)
def emit_c_public_hash(self, file=sys.stdout):
digest = Hash(SHA256())
digest.update(self.get_public_bytes())
self._emit(
header="const unsigned char {}_pub_key_hash[] = {{"
.format(self.shortname()),
trailer="};",
encoded_bytes=digest.finalize(),
indent=" ",
len_format="const unsigned int {}_pub_key_hash_len = {{}};"
.format(self.shortname()),
file=file)
def emit_raw_public(self, file=sys.stdout):
self._emit_raw(self.get_public_bytes(), file=file)
def emit_raw_public_hash(self, file=sys.stdout):
digest = Hash(SHA256())
digest.update(self.get_public_bytes())
self._emit_raw(digest.finalize(), file=file)
def emit_rust_public(self, file=sys.stdout):
self._emit(
header="static {}_PUB_KEY: &[u8] = &["
.format(self.shortname().upper()),
trailer="];",
encoded_bytes=self.get_public_bytes(),
indent=" ",
file=file)
def emit_public_pem(self, file=sys.stdout):
with FileHandler(file, 'w') as file:
print(str(self.get_public_pem(), 'utf-8'), file=file, end='')
def emit_private(self, minimal, format, file=sys.stdout):
self._emit(
header="const unsigned char enc_priv_key[] = {",
trailer="};",
encoded_bytes=self.get_private_bytes(minimal, format),
indent=" ",
len_format="const unsigned int enc_priv_key_len = {};",
file=file)

View File

@@ -0,0 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
from cryptography.hazmat.primitives import serialization
class PrivateBytesMixin():
def _get_private_bytes(self, minimal, format, exclass):
if format is None:
format = self._DEFAULT_FORMAT
if format not in self._VALID_FORMATS:
raise exclass("{} does not support {}".format(
self.shortname(), format))
return format, self.key.private_bytes(
encoding=serialization.Encoding.DER,
format=self._VALID_FORMATS[format],
encryption_algorithm=serialization.NoEncryption())

View File

@@ -0,0 +1,173 @@
"""
RSA Key management
"""
# SPDX-License-Identifier: Apache-2.0
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric.padding import PSS, MGF1
from cryptography.hazmat.primitives.hashes import SHA256
from .general import KeyClass
from .privatebytes import PrivateBytesMixin
# Sizes that bootutil will recognize
RSA_KEY_SIZES = [2048, 3072]
class RSAUsageError(Exception):
pass
class RSAPublic(KeyClass):
"""The public key can only do a few operations"""
def __init__(self, key):
self.key = key
def key_size(self):
return self.key.key_size
def shortname(self):
return "rsa"
def _unsupported(self, name):
raise RSAUsageError("Operation {} requires private key".format(name))
def _get_public(self):
return self.key
def get_public_bytes(self):
# The key embedded into MCUboot is in PKCS1 format.
return self._get_public().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.PKCS1)
def get_public_pem(self):
return self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_private_bytes(self, minimal, format):
self._unsupported('get_private_bytes')
def export_private(self, path, passwd=None):
self._unsupported('export_private')
def export_public(self, path):
"""Write the public key to the given file."""
pem = self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
with open(path, 'wb') as f:
f.write(pem)
def sig_type(self):
return "PKCS1_PSS_RSA{}_SHA256".format(self.key_size())
def sig_tlv(self):
return"RSA{}".format(self.key_size())
def sig_len(self):
return self.key_size() / 8
def verify(self, signature, payload):
k = self.key
if isinstance(self.key, rsa.RSAPrivateKey):
k = self.key.public_key()
return k.verify(signature=signature, data=payload,
padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
algorithm=SHA256())
class RSA(RSAPublic, PrivateBytesMixin):
"""
Wrapper around an RSA key, with imgtool support.
"""
def __init__(self, key):
"""The key should be a private key from cryptography"""
self.key = key
@staticmethod
def generate(key_size=2048):
if key_size not in RSA_KEY_SIZES:
raise RSAUsageError("Key size {} is not supported by MCUboot"
.format(key_size))
pk = rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend())
return RSA(pk)
def _get_public(self):
return self.key.public_key()
def _build_minimal_rsa_privkey(self, der):
'''
Builds a new DER that only includes N/E/D/P/Q RSA parameters;
standard DER private bytes provided by OpenSSL also includes
CRT params (DP/DQ/QP) which can be removed.
'''
OFFSET_N = 7 # N is always located at this offset
b = bytearray(der)
off = OFFSET_N
if b[off + 1] != 0x82:
raise RSAUsageError("Error parsing N while minimizing")
len_N = (b[off + 2] << 8) + b[off + 3] + 4
off += len_N
if b[off + 1] != 0x03:
raise RSAUsageError("Error parsing E while minimizing")
len_E = b[off + 2] + 4
off += len_E
if b[off + 1] != 0x82:
raise RSAUsageError("Error parsing D while minimizing")
len_D = (b[off + 2] << 8) + b[off + 3] + 4
off += len_D
if b[off + 1] != 0x81:
raise RSAUsageError("Error parsing P while minimizing")
len_P = b[off + 2] + 3
off += len_P
if b[off + 1] != 0x81:
raise RSAUsageError("Error parsing Q while minimizing")
len_Q = b[off + 2] + 3
off += len_Q
# adjust DER size for removed elements
b[2] = (off - 4) >> 8
b[3] = (off - 4) & 0xff
return b[:off]
_VALID_FORMATS = {
'openssl': serialization.PrivateFormat.TraditionalOpenSSL
}
_DEFAULT_FORMAT = 'openssl'
def get_private_bytes(self, minimal, format):
_, priv = self._get_private_bytes(minimal, format, RSAUsageError)
if minimal:
priv = self._build_minimal_rsa_privkey(priv)
return priv
def export_private(self, path, passwd=None):
"""Write the private key to the given file, protecting it with the
optional password."""
if passwd is None:
enc = serialization.NoEncryption()
else:
enc = serialization.BestAvailableEncryption(passwd)
pem = self.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=enc)
with open(path, 'wb') as f:
f.write(pem)
def sign(self, payload):
# The verification code only allows the salt length to be the
# same as the hash length, 32.
return self.key.sign(
data=payload,
padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
algorithm=SHA256())

View File

@@ -0,0 +1,136 @@
"""
Tests for RSA keys
"""
# SPDX-License-Identifier: Apache-2.0
import io
import os
import sys
import tempfile
import unittest
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.padding import PSS, MGF1
from cryptography.hazmat.primitives.hashes import SHA256
# Setup sys path so 'imgtool' is in it.
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__),
'../..')))
from imgtool.keys import load, RSA, RSAUsageError
from imgtool.keys.rsa import RSA_KEY_SIZES
class KeyGeneration(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.TemporaryDirectory()
def tname(self, base):
return os.path.join(self.test_dir.name, base)
def tearDown(self):
self.test_dir.cleanup()
def test_keygen(self):
# Try generating a RSA key with non-supported size
with self.assertRaises(RSAUsageError):
RSA.generate(key_size=1024)
for key_size in RSA_KEY_SIZES:
name1 = self.tname("keygen.pem")
k = RSA.generate(key_size=key_size)
k.export_private(name1, b'secret')
# Try loading the key without a password.
self.assertIsNone(load(name1))
k2 = load(name1, b'secret')
pubname = self.tname('keygen-pub.pem')
k2.export_public(pubname)
pk2 = load(pubname)
# We should be able to export the public key from the loaded
# public key, but not the private key.
pk2.export_public(self.tname('keygen-pub2.pem'))
self.assertRaises(RSAUsageError, pk2.export_private,
self.tname('keygen-priv2.pem'))
def test_emit(self):
"""Basic sanity check on the code emitters."""
for key_size in RSA_KEY_SIZES:
k = RSA.generate(key_size=key_size)
pubpem = io.StringIO()
k.emit_public_pem(pubpem)
self.assertIn("BEGIN PUBLIC KEY", pubpem.getvalue())
self.assertIn("END PUBLIC KEY", pubpem.getvalue())
ccode = io.StringIO()
k.emit_c_public(ccode)
self.assertIn("rsa_pub_key", ccode.getvalue())
self.assertIn("rsa_pub_key_len", ccode.getvalue())
hashccode = io.StringIO()
k.emit_c_public_hash(hashccode)
self.assertIn("rsa_pub_key_hash", hashccode.getvalue())
self.assertIn("rsa_pub_key_hash_len", hashccode.getvalue())
rustcode = io.StringIO()
k.emit_rust_public(rustcode)
self.assertIn("RSA_PUB_KEY", rustcode.getvalue())
# raw data - bytes
pubraw = io.BytesIO()
k.emit_raw_public(pubraw)
self.assertTrue(len(pubraw.getvalue()) > 0)
hashraw = io.BytesIO()
k.emit_raw_public_hash(hashraw)
self.assertTrue(len(hashraw.getvalue()) > 0)
def test_emit_pub(self):
"""Basic sanity check on the code emitters, from public key."""
pubname = self.tname("public.pem")
for key_size in RSA_KEY_SIZES:
k = RSA.generate(key_size=key_size)
k.export_public(pubname)
k2 = load(pubname)
ccode = io.StringIO()
k2.emit_c_public(ccode)
self.assertIn("rsa_pub_key", ccode.getvalue())
self.assertIn("rsa_pub_key_len", ccode.getvalue())
rustcode = io.StringIO()
k2.emit_rust_public(rustcode)
self.assertIn("RSA_PUB_KEY", rustcode.getvalue())
def test_sig(self):
for key_size in RSA_KEY_SIZES:
k = RSA.generate(key_size=key_size)
buf = b'This is the message'
sig = k.sign(buf)
# The code doesn't have any verification, so verify this
# manually.
k.key.public_key().verify(
signature=sig,
data=buf,
padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
algorithm=SHA256())
# Modify the message to make sure the signature fails.
self.assertRaises(InvalidSignature,
k.key.public_key().verify,
signature=sig,
data=b'This is thE message',
padding=PSS(mgf=MGF1(SHA256()), salt_length=32),
algorithm=SHA256())
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,119 @@
"""
X25519 key management
"""
# SPDX-License-Identifier: Apache-2.0
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import x25519
from .general import KeyClass
from .privatebytes import PrivateBytesMixin
class X25519UsageError(Exception):
pass
class X25519Public(KeyClass):
def __init__(self, key):
self.key = key
def shortname(self):
return "x25519"
def _unsupported(self, name):
raise X25519UsageError("Operation {} requires private key".format(name))
def _get_public(self):
return self.key
def get_public_bytes(self):
# The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
return self._get_public().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_public_pem(self):
return self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
def get_private_bytes(self, minimal, format):
self._unsupported('get_private_bytes')
def export_private(self, path, passwd=None):
self._unsupported('export_private')
def export_public(self, path):
"""Write the public key to the given file."""
pem = self._get_public().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
with open(path, 'wb') as f:
f.write(pem)
def sig_type(self):
return "X25519"
def sig_tlv(self):
return "X25519"
def sig_len(self):
return 32
class X25519(X25519Public, PrivateBytesMixin):
"""
Wrapper around an X25519 private key.
"""
def __init__(self, key):
"""key should be an instance of EllipticCurvePrivateKey"""
self.key = key
@staticmethod
def generate():
pk = x25519.X25519PrivateKey.generate()
return X25519(pk)
def _get_public(self):
return self.key.public_key()
_VALID_FORMATS = {
'pkcs8': serialization.PrivateFormat.PKCS8
}
_DEFAULT_FORMAT = 'pkcs8'
def get_private_bytes(self, minimal, format):
_, priv = self._get_private_bytes(minimal, format,
X25519UsageError)
return priv
def export_private(self, path, passwd=None):
"""
Write the private key to the given file, protecting it with the
optional password.
"""
if passwd is None:
enc = serialization.NoEncryption()
else:
enc = serialization.BestAvailableEncryption(passwd)
pem = self.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=enc)
with open(path, 'wb') as f:
f.write(pem)
def sign_digest(self, digest):
"""Return the actual signature"""
return self.key.sign(data=digest)
def verify_digest(self, signature, digest):
"""Verify that signature is valid for given digest"""
k = self.key
if isinstance(self.key, x25519.X25519PrivateKey):
k = self.key.public_key()
return k.verify(signature=signature, data=digest)