Skip to content

Commit 21627be

Browse files
IanJ-ArmM1chaamoskopprretanubun
committed
scripts/imgtool.py: Add PKCS#11 ECDSA P384 support
Based on work submitted in relation to: Issue #599 ref: #599 particularly these commits: grandcentrix/mcuboot@82441bd4286 grandcentrix/mcuboot@010ea89f rretanubun@33c6400a40 Updated and modified to support ECDSA P384 keys. Tests also updated and fixed, tested with SoftHSMv2. Signed-off-by: Ian Jamison <[email protected]> Co-authored-by: Michael Zimmermann <[email protected]> Co-authored-by: Nils Dagsson Moskopp <[email protected]> Co-authored-by: Richard Retanubun <[email protected]> Change-Id: I175b710834bd20a868961634483d43b459959769
1 parent 82bd4a7 commit 21627be

File tree

7 files changed

+468
-6
lines changed

7 files changed

+468
-6
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Added support for PKCS#11 URIs and ECDSA-P384 keys.

scripts/imgtool/image.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Copyright 2018 Nordic Semiconductor ASA
22
# Copyright 2017-2020 Linaro Limited
3-
# Copyright 2019-2024 Arm Limited
3+
# Copyright 2019-2025 Arm Limited
44
#
55
# SPDX-License-Identifier: Apache-2.0
66
#
@@ -184,6 +184,7 @@ def tlv_sha_to_sha(tlv):
184184
ALLOWED_KEY_SHA = {
185185
keys.ECDSA384P1 : ['384'],
186186
keys.ECDSA384P1Public : ['384'],
187+
keys.PKCS11 : ['384'],
187188
keys.ECDSA256P1 : ['256'],
188189
keys.ECDSA256P1Public : ['256'],
189190
keys.RSA : ['256'],
@@ -220,7 +221,7 @@ def key_and_user_sha_to_alg_and_tlv(key, user_sha, is_pure = False):
220221
allowed = allowed_key_ssh[type(key)]
221222

222223
except KeyError:
223-
raise click.UsageError("Colud not find allowed hash algorithms for {}"
224+
raise click.UsageError("Could not find allowed hash algorithms for {}"
224225
.format(type(key)))
225226

226227
# Pure enforces auto, and user selection is ignored

scripts/imgtool/keys/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,18 @@
3030
from cryptography.hazmat.primitives.asymmetric.x25519 import (
3131
X25519PrivateKey, X25519PublicKey)
3232

33+
import pkcs11
34+
import pkcs11.exceptions
35+
import sys
36+
3337
from .rsa import RSA, RSAPublic, RSAUsageError, RSA_KEY_SIZES
3438
from .ecdsa import (ECDSA256P1, ECDSA256P1Public,
3539
ECDSA384P1, ECDSA384P1Public, ECDSAUsageError)
3640
from .ed25519 import Ed25519, Ed25519Public, Ed25519UsageError
3741
from .x25519 import X25519, X25519Public, X25519UsageError
3842

43+
from .imgtool_keys_pkcs11 import PKCS11
44+
3945

4046
class PasswordRequired(Exception):
4147
"""Raised to indicate that the key is password protected, but a
@@ -44,6 +50,19 @@ class PasswordRequired(Exception):
4450

4551

4652
def load(path, passwd=None):
53+
if path.startswith("pkcs11:"):
54+
try:
55+
return PKCS11(path) # assume a PKCS #11 URI according to RFC7512
56+
except pkcs11.exceptions.PinIncorrect:
57+
print('ERROR: WRONG PIN')
58+
sys.exit(1)
59+
except pkcs11.exceptions.PinLocked:
60+
print('ERROR: WRONG PIN, MAX ATTEMPTS REACHED. CONTACT YOUR SECURITY OFFICER.')
61+
sys.exit(1)
62+
except pkcs11.exceptions.DataLenRange:
63+
print('ERROR: PIN IS TOO SHORT OR TOO LONG')
64+
sys.exit(1)
65+
4766
"""Try loading a key from the given path.
4867
Returns None if the password wasn't specified."""
4968
with open(path, 'rb') as f:
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""
2+
PKCS11 key management
3+
"""
4+
# SPDX-License-Identifier: Apache-2.0
5+
6+
import hashlib
7+
import os
8+
from pathlib import Path
9+
from urllib.parse import unquote, urlparse
10+
11+
import pkcs11
12+
import pkcs11.util.ec
13+
14+
from cryptography.hazmat.primitives.serialization import (
15+
load_der_public_key,
16+
Encoding,
17+
PublicFormat
18+
)
19+
from cryptography.hazmat.primitives.asymmetric.ec import (
20+
ECDSA, SECP256R1, SECP384R1,
21+
EllipticCurvePublicKey
22+
)
23+
from cryptography.hazmat.primitives import hashes
24+
from cryptography.exceptions import InvalidSignature
25+
26+
from .general import KeyClass
27+
28+
29+
def unquote_to_bytes(urlencoded_string):
30+
"""Replace %xx escapes by their single-character equivalent,
31+
using the “iso-8859-1” encoding to decode all 8-bit values.
32+
"""
33+
return bytes(
34+
unquote(urlencoded_string, encoding='iso-8859-1'),
35+
encoding='iso-8859-1'
36+
)
37+
38+
def get_pkcs11_uri_params(uri):
39+
"""Return a dict of decoded URI key=val pairs
40+
"""
41+
uri_tokens = urlparse(uri)
42+
assert uri_tokens.scheme == 'pkcs11'
43+
assert uri_tokens.query == ''
44+
assert uri_tokens.fragment == ''
45+
return {
46+
unquote_to_bytes(key): unquote_to_bytes(value)
47+
for key, value
48+
in [
49+
line.split('=')
50+
for line
51+
in uri_tokens.path.split(';')
52+
]
53+
}
54+
55+
class PKCS11UsageError(Exception):
56+
pass
57+
58+
59+
class PKCS11(KeyClass):
60+
"""
61+
Wrapper around an ECDSA P384 key accessed via PKCS#11 URIs
62+
"""
63+
def __init__(self, uri, env=None):
64+
if env is None:
65+
env = os.environ
66+
if not 'PKCS11_PIN' in env.keys():
67+
raise RuntimeError("Environment variable PKCS11_PIN not set. Set it to the user PIN.")
68+
params = get_pkcs11_uri_params(uri)
69+
assert b'serial' in params.keys()
70+
assert b'id' in params.keys() or b'label' in params.keys()
71+
self.user_pin = env['PKCS11_PIN']
72+
73+
# Fall back to OpenSC
74+
pkcs11_module_path = env.get('PKCS11_MODULE', 'opensc-pkcs11.so')
75+
76+
lib = ''
77+
try:
78+
lib = pkcs11.lib(pkcs11_module_path)
79+
except RuntimeError:
80+
pass # happens if lib does not exist or is corrupt
81+
if '' == lib:
82+
raise RuntimeError(f"PKCS11 module {pkcs11_module_path} not loaded.")
83+
84+
self.token = lib.get_token(token_serial=params[b'serial'])
85+
# try to open a session to see if the PIN is valid
86+
with self.token.open(user_pin=self.user_pin) as _:
87+
pass
88+
self.key_id = params.get(b'id', None)
89+
self.key_label = params.get(b'label', None)
90+
self.key_label = self.key_label.decode('utf-8') if self.key_label else None
91+
92+
def shortname(self):
93+
return "ecdsa"
94+
95+
def _unsupported(self, name):
96+
raise PKCS11UsageError(f"Operation {name} requires private key")
97+
98+
def get_public_bytes(self):
99+
with self.token.open(user_pin=self.user_pin) as session:
100+
pub = session.get_key(
101+
id=self.key_id,
102+
label=self.key_label,
103+
key_type=pkcs11.KeyType.EC,
104+
object_class=pkcs11.ObjectClass.PUBLIC_KEY
105+
)
106+
key = pkcs11.util.ec.encode_ec_public_key(pub)
107+
return key
108+
109+
def get_private_bytes(self, minimal):
110+
self._unsupported('get_private_bytes')
111+
112+
def export_private(self, path, passwd=None):
113+
self._unsupported('export_private')
114+
115+
def export_public(self, path):
116+
"""Write the public key to the given file."""
117+
with self.token.open(user_pin=self.user_pin) as session:
118+
pub = session.get_key(
119+
id=self.key_id,
120+
label=self.key_label,
121+
key_type=pkcs11.KeyType.EC,
122+
object_class=pkcs11.ObjectClass.PUBLIC_KEY
123+
)
124+
# Encode to DER
125+
der_bytes = pkcs11.util.ec.encode_ec_public_key(pub)
126+
127+
# Convert to PEM using cryptography
128+
public_key = load_der_public_key(der_bytes)
129+
pem = public_key.public_bytes(
130+
encoding=Encoding.PEM,
131+
format=PublicFormat.SubjectPublicKeyInfo
132+
)
133+
134+
with open(path, 'wb') as f:
135+
f.write(pem)
136+
137+
def sig_type(self):
138+
return "ECDSA384_SHA384"
139+
140+
def sig_tlv(self):
141+
return "ECDSASIG"
142+
143+
def sig_len(self):
144+
# Early versions of MCUboot (< v1.5.0) required ECDSA
145+
# signatures to be padded to a fixed length. Because the DER
146+
# encoding is done with signed integers, the size of the
147+
# signature will vary depending on whether the high bit is set
148+
# in each value. This padding was done in a
149+
# not-easily-reversible way (by just adding zeros).
150+
#
151+
# The signing code no longer requires this padding, and newer
152+
# versions of MCUboot don't require it. But, continue to
153+
# return the total length so that the padding can be done if
154+
# requested.
155+
return 103
156+
157+
def raw_sign(self, payload):
158+
"""Return the actual signature"""
159+
with self.token.open(user_pin=self.user_pin) as session:
160+
priv = session.get_key(
161+
id=self.key_id,
162+
label=self.key_label,
163+
key_type=pkcs11.KeyType.EC,
164+
object_class=pkcs11.ObjectClass.PRIVATE_KEY
165+
)
166+
sig = priv.sign(
167+
hashlib.sha384(payload).digest(),
168+
mechanism=pkcs11.Mechanism.ECDSA
169+
)
170+
return pkcs11.util.ec.encode_ecdsa_signature(sig)
171+
172+
def sign(self, payload):
173+
"""Return signature with legacy padding"""
174+
# To make fixed length, pad with one or two zeros.
175+
while True:
176+
sig = self.raw_sign(payload)
177+
if sig[-1] != 0x00:
178+
break
179+
180+
sig += b'\000' * (self.sig_len() - len(sig))
181+
return sig
182+
183+
def verify(self, signature, payload):
184+
"""Verify the signature of the payload"""
185+
# strip possible paddings added during sign
186+
signature = signature[:signature[1] + 2]
187+
188+
# Load public key from DER bytes
189+
public_key = load_der_public_key(self.get_public_bytes())
190+
191+
if not isinstance(public_key, EllipticCurvePublicKey):
192+
raise TypeError(f"Unsupported key type: {type(public_key).__name__}")
193+
194+
# Determine correct hash algorithm based on curve
195+
if isinstance(public_key.curve, SECP256R1):
196+
hash_alg = hashes.SHA256()
197+
elif isinstance(public_key.curve, SECP384R1):
198+
hash_alg = hashes.SHA384()
199+
else:
200+
raise ValueError(f"Unsupported curve: {public_key.curve.name}")
201+
202+
try:
203+
# Attempt ECDSA verification
204+
public_key.verify(signature, payload, ECDSA(hash_alg))
205+
return True
206+
except InvalidSignature:
207+
return False

0 commit comments

Comments
 (0)