Skip to content

Commit 8101d1f

Browse files
committed
Add ed25519 signing support to imgtool
This adds ed25519 signature support using the "prehash" method. Instead of using the direct contents of the image and header payloads, a sha256 is generated and signed (SHA256-Ed25519). This allows for compatibility with already existing tools that use the sha256 hash, like mcumgr, etc. Signed-off-by: Fabio Utzig <[email protected]>
1 parent fc07eab commit 8101d1f

File tree

6 files changed

+221
-6
lines changed

6 files changed

+221
-6
lines changed

docs/imgtool.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ Python libraries. These can be installed using 'pip3':
1212

1313
## Managing keys
1414

15-
This tool currently supports rsa-2048, rsa-3072 and ecdsa-p256 keys. You
16-
can generate a keypair for one of these types using the 'keygen' command:
15+
This tool currently supports rsa-2048, rsa-3072, ecdsa-p256 and ed25519 keys.
16+
You can generate a keypair for one of these types using the 'keygen' command:
1717

1818
./scripts/imgtool.py keygen -k filename.pem -t rsa-2048
1919

20-
or use ecdsa-p256 for the type. The key type used should match what
21-
mcuboot is configured to verify.
20+
or use rsa-3072, ecdsa-p256, or ed25519 for the type. The key type used
21+
should match what mcuboot is configured to verify.
2222

2323
This key file is what is used to sign images, this file should be
2424
protected, and not widely distributed.

scripts/imgtool/image.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
'ECDSA224': 0x21,
5353
'ECDSA256': 0x22,
5454
'RSA3072': 0x23,
55+
'ED25519': 0x24,
5556
'ENCRSA2048': 0x30,
5657
'ENCKW128': 0x31,
5758
'DEPENDENCY': 0x40
@@ -241,7 +242,13 @@ def create(self, key, enckey, dependencies=None):
241242
pubbytes = sha.digest()
242243
tlv.add('KEYHASH', pubbytes)
243244

244-
sig = key.sign(bytes(self.payload))
245+
# `sign` expects the full image payload (sha256 done internally),
246+
# while `sign_digest` expects only the digest of the payload
247+
248+
if hasattr(key, 'sign'):
249+
sig = key.sign(bytes(self.payload))
250+
else:
251+
sig = key.sign_digest(digest)
245252
tlv.add(key.sig_tlv(), sig)
246253

247254
if enckey is not None:
@@ -354,7 +361,10 @@ def verify(imgfile, key):
354361
tlv_sig = b[off:off+tlv_len]
355362
payload = b[:header_size+img_size]
356363
try:
357-
key.verify(tlv_sig, payload)
364+
if hasattr(key, 'verify'):
365+
key.verify(tlv_sig, payload)
366+
else:
367+
key.verify_digest(tlv_sig, digest)
358368
return VerifyResult.OK
359369
except InvalidSignature:
360370
# continue to next TLV

scripts/imgtool/keys/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
from cryptography.hazmat.primitives import serialization
2121
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
2222
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey, EllipticCurvePublicKey
23+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
2324

2425
from .rsa import RSA, RSAPublic, RSAUsageError, RSA_KEY_SIZES
2526
from .ecdsa import ECDSA256P1, ECDSA256P1Public, ECDSAUsageError
27+
from .ed25519 import Ed25519, Ed25519Public, Ed25519UsageError
2628

2729
class PasswordRequired(Exception):
2830
"""Raised to indicate that the key is password protected, but a
@@ -72,5 +74,9 @@ def load(path, passwd=None):
7274
if pk.key_size != 256:
7375
raise Exception("Unsupported EC size: " + pk.key_size)
7476
return ECDSA256P1Public(pk)
77+
elif isinstance(pk, Ed25519PrivateKey):
78+
return Ed25519(pk)
79+
elif isinstance(pk, Ed25519PublicKey):
80+
return Ed25519Public(pk)
7581
else:
7682
raise Exception("Unknown key type: " + str(type(pk)))

scripts/imgtool/keys/ed25519.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
ECDSA key management
3+
"""
4+
5+
from cryptography.hazmat.backends import default_backend
6+
from cryptography.hazmat.primitives import serialization
7+
from cryptography.hazmat.primitives.asymmetric import ed25519
8+
9+
from .general import KeyClass
10+
11+
12+
class Ed25519UsageError(Exception):
13+
pass
14+
15+
16+
class Ed25519Public(KeyClass):
17+
def __init__(self, key):
18+
self.key = key
19+
20+
def shortname(self):
21+
return "ed25519"
22+
23+
def _unsupported(self, name):
24+
raise Ed25519UsageError("Operation {} requires private key".format(name))
25+
26+
def _get_public(self):
27+
return self.key
28+
29+
def get_public_bytes(self):
30+
# The key is embedded into MBUboot in "SubjectPublicKeyInfo" format
31+
return self._get_public().public_bytes(
32+
encoding=serialization.Encoding.DER,
33+
format=serialization.PublicFormat.SubjectPublicKeyInfo)
34+
35+
def export_private(self, path, passwd=None):
36+
self._unsupported('export_private')
37+
38+
def export_public(self, path):
39+
"""Write the public key to the given file."""
40+
pem = self._get_public().public_bytes(
41+
encoding=serialization.Encoding.PEM,
42+
format=serialization.PublicFormat.SubjectPublicKeyInfo)
43+
with open(path, 'wb') as f:
44+
f.write(pem)
45+
46+
def sig_type(self):
47+
return "ED25519"
48+
49+
def sig_tlv(self):
50+
return "ED25519"
51+
52+
def sig_len(self):
53+
return 64
54+
55+
56+
class Ed25519(Ed25519Public):
57+
"""
58+
Wrapper around an ECDSA private key.
59+
"""
60+
61+
def __init__(self, key):
62+
"""key should be an instance of EllipticCurvePrivateKey"""
63+
self.key = key
64+
65+
@staticmethod
66+
def generate():
67+
pk = ed25519.Ed25519PrivateKey.generate()
68+
return Ed25519(pk)
69+
70+
def _get_public(self):
71+
return self.key.public_key()
72+
73+
def export_private(self, path, passwd=None):
74+
"""
75+
Write the private key to the given file, protecting it with the
76+
optional password.
77+
"""
78+
if passwd is None:
79+
enc = serialization.NoEncryption()
80+
else:
81+
enc = serialization.BestAvailableEncryption(passwd)
82+
pem = self.key.private_bytes(
83+
encoding=serialization.Encoding.PEM,
84+
format=serialization.PrivateFormat.PKCS8,
85+
encryption_algorithm=enc)
86+
with open(path, 'wb') as f:
87+
f.write(pem)
88+
89+
def sign_digest(self, digest):
90+
"""Return the actual signature"""
91+
return self.key.sign(data=digest)
92+
93+
def verify_digest(self, signature, digest):
94+
"""Verify that signature is valid for given digest"""
95+
k = self.key
96+
if isinstance(self.key, ed25519.Ed25519PrivateKey):
97+
k = self.key.public_key()
98+
return k.verify(signature=signature, data=digest)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Tests for ECDSA keys
3+
"""
4+
5+
import io
6+
import os.path
7+
import sys
8+
import tempfile
9+
import unittest
10+
11+
from cryptography.exceptions import InvalidSignature
12+
from cryptography.hazmat.primitives.asymmetric import ed25519
13+
14+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
15+
16+
from imgtool.keys import load, Ed25519, Ed25519UsageError
17+
18+
19+
class Ed25519KeyGeneration(unittest.TestCase):
20+
21+
def setUp(self):
22+
self.test_dir = tempfile.TemporaryDirectory()
23+
24+
def tname(self, base):
25+
return os.path.join(self.test_dir.name, base)
26+
27+
def tearDown(self):
28+
self.test_dir.cleanup()
29+
30+
def test_keygen(self):
31+
name1 = self.tname("keygen.pem")
32+
k = Ed25519.generate()
33+
k.export_private(name1, b'secret')
34+
35+
self.assertIsNone(load(name1))
36+
37+
k2 = load(name1, b'secret')
38+
39+
pubname = self.tname('keygen-pub.pem')
40+
k2.export_public(pubname)
41+
pk2 = load(pubname)
42+
43+
# We should be able to export the public key from the loaded
44+
# public key, but not the private key.
45+
pk2.export_public(self.tname('keygen-pub2.pem'))
46+
self.assertRaises(Ed25519UsageError,
47+
pk2.export_private, self.tname('keygen-priv2.pem'))
48+
49+
def test_emit(self):
50+
"""Basic sanity check on the code emitters."""
51+
k = Ed25519.generate()
52+
53+
ccode = io.StringIO()
54+
k.emit_c(ccode)
55+
self.assertIn("ed25519_pub_key", ccode.getvalue())
56+
self.assertIn("ed25519_pub_key_len", ccode.getvalue())
57+
58+
rustcode = io.StringIO()
59+
k.emit_rust(rustcode)
60+
self.assertIn("ED25519_PUB_KEY", rustcode.getvalue())
61+
62+
def test_emit_pub(self):
63+
"""Basic sanity check on the code emitters."""
64+
pubname = self.tname("public.pem")
65+
k = Ed25519.generate()
66+
k.export_public(pubname)
67+
68+
k2 = load(pubname)
69+
70+
ccode = io.StringIO()
71+
k2.emit_c(ccode)
72+
self.assertIn("ed25519_pub_key", ccode.getvalue())
73+
self.assertIn("ed25519_pub_key_len", ccode.getvalue())
74+
75+
rustcode = io.StringIO()
76+
k2.emit_rust(rustcode)
77+
self.assertIn("ED25519_PUB_KEY", rustcode.getvalue())
78+
79+
def test_sig(self):
80+
k = Ed25519.generate()
81+
buf = b'This is the message'
82+
sig = k.raw_sign(buf)
83+
84+
# The code doesn't have any verification, so verify this
85+
# manually.
86+
k.key.public_key().verify(signature=sig, data=buf)
87+
88+
# Modify the message to make sure the signature fails.
89+
self.assertRaises(InvalidSignature,
90+
k.key.public_key().verify,
91+
signature=sig,
92+
data=b'This is thE message')
93+
94+
95+
if __name__ == '__main__':
96+
unittest.main()

scripts/imgtool/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ def gen_ecdsa_p224(keyfile, passwd):
4141
print("TODO: p-224 not yet implemented")
4242

4343

44+
def gen_ed25519(keyfile, passwd):
45+
keys.Ed25519.generate().export_private(path=keyfile)
46+
47+
4448
valid_langs = ['c', 'rust']
4549
keygens = {
4650
'rsa-2048': gen_rsa2048,
4751
'rsa-3072': gen_rsa3072,
4852
'ecdsa-p256': gen_ecdsa_p256,
4953
'ecdsa-p224': gen_ecdsa_p224,
54+
'ed25519': gen_ed25519,
5055
}
5156

5257

0 commit comments

Comments
 (0)