Skip to content
This repository was archived by the owner on Oct 27, 2025. It is now read-only.

Commit 82441bd

Browse files
M1chaamoskopp
authored andcommitted
scripts/imgtool: add pkcs11 support
1 parent 137d797 commit 82441bd

File tree

4 files changed

+228
-0
lines changed

4 files changed

+228
-0
lines changed

scripts/imgtool/keys/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,17 @@
2929
from cryptography.hazmat.primitives.asymmetric.x25519 import (
3030
X25519PrivateKey, X25519PublicKey)
3131

32+
import pkcs11
33+
import pkcs11.exceptions
34+
import sys
35+
3236
from .rsa import RSA, RSAPublic, RSAUsageError, RSA_KEY_SIZES
3337
from .ecdsa import ECDSA256P1, ECDSA256P1Public, ECDSAUsageError
3438
from .ed25519 import Ed25519, Ed25519Public, Ed25519UsageError
3539
from .x25519 import X25519, X25519Public, X25519UsageError
3640

41+
from .imgtool_keys_pkcs11 import PKCS11
42+
3743

3844
class PasswordRequired(Exception):
3945
"""Raised to indicate that the key is password protected, but a
@@ -42,6 +48,19 @@ class PasswordRequired(Exception):
4248

4349

4450
def load(path, passwd=None):
51+
if path == "pkcs11":
52+
try:
53+
return PKCS11()
54+
except pkcs11.exceptions.PinIncorrect:
55+
print('ERROR: WRONG PIN')
56+
sys.exit(1)
57+
except pkcs11.exceptions.PinLocked:
58+
print('ERROR: WRONG PIN, MAX ATTEMPTS REACHED. CONTACT YOUR SECURITY OFFICER.')
59+
sys.exit(1)
60+
except pkcs11.exceptions.DataLenRange:
61+
print('ERROR: \'DataLenRange\' (maybe the PIN is too short?)')
62+
sys.exit(1)
63+
4564
"""Try loading a key from the given path. Returns None if the password wasn't specified."""
4665
with open(path, 'rb') as f:
4766
raw_pem = f.read()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
PKCS11 key management
3+
"""
4+
5+
import hashlib
6+
import os
7+
import oscrypto.asymmetric
8+
import pkcs11
9+
import pkcs11.util.ec
10+
11+
from .general import KeyClass
12+
13+
14+
class PKCS11UsageError(Exception):
15+
pass
16+
17+
18+
class PKCS11(KeyClass):
19+
def __init__(self, env=os.environ):
20+
lib = pkcs11.lib(env['PKCS11_MODULE'])
21+
self.token = lib.get_token(token_label=env['PKCS11_TOKEN_LABEL'])
22+
self.key_id = bytes.fromhex(env['PKCS11_KEY_ID'])
23+
self.session = self.token.open(user_pin=env['PKCS11_PIN'])
24+
25+
def __del__(self):
26+
if hasattr(self, 'session'):
27+
self.session.close()
28+
29+
def shortname(self):
30+
return "ecdsa"
31+
32+
def _unsupported(self, name):
33+
raise PKCS11UsageError("Operation {} requires private key".format(name))
34+
35+
def get_public_bytes(self):
36+
pub = self.session.get_key(
37+
id=self.key_id, key_type=pkcs11.KeyType.EC, object_class=pkcs11.ObjectClass.PUBLIC_KEY)
38+
key = oscrypto.asymmetric.load_public_key(
39+
pkcs11.util.ec.encode_ec_public_key(pub))
40+
key = pkcs11.util.ec.encode_ec_public_key(pub)
41+
return key
42+
43+
def get_private_bytes(self, minimal):
44+
self._unsupported('get_private_bytes')
45+
46+
def export_private(self, path, passwd=None):
47+
self._unsupported('export_private')
48+
49+
def export_public(self, path):
50+
"""Write the public key to the given file."""
51+
pub = self.session.get_key(
52+
id=self.key_id, key_type=pkcs11.KeyType.EC, object_class=pkcs11.ObjectClass.PUBLIC_KEY)
53+
key = oscrypto.asymmetric.load_public_key(
54+
pkcs11.util.ec.encode_ec_public_key(pub))
55+
pem = oscrypto.asymmetric.dump_public_key(key, encoding='pem')
56+
57+
with open(path, 'wb') as f:
58+
f.write(pem)
59+
60+
def sig_type(self):
61+
return "ECDSA256_SHA256"
62+
63+
def sig_tlv(self):
64+
return "ECDSA256"
65+
66+
def sig_len(self):
67+
# The DER encoding depends on the high bit, and can be
68+
# anywhere from 70 to 72 bytes. Because we have to fill in
69+
# the length field before computing the signature, however,
70+
# we'll give the largest, and the sig checking code will allow
71+
# for it to be up to two bytes larger than the actual
72+
# signature.
73+
return 72
74+
75+
def raw_sign(self, payload):
76+
"""Return the actual signature"""
77+
priv = self.session.get_key(
78+
id=self.key_id, key_type=pkcs11.KeyType.EC, object_class=pkcs11.ObjectClass.PRIVATE_KEY)
79+
sig = priv.sign(hashlib.sha256(payload).digest(),
80+
mechanism=pkcs11.Mechanism.ECDSA)
81+
return pkcs11.util.ec.encode_ecdsa_signature(sig)
82+
83+
def sign(self, payload):
84+
# To make fixed length, pad with one or two zeros.
85+
while True:
86+
sig = self.raw_sign(payload)
87+
if sig[-1] != 0x00:
88+
break
89+
90+
sig += b'\000' * (self.sig_len() - len(sig))
91+
return sig
92+
93+
def verify(self, signature, payload):
94+
# strip possible paddings added during sign
95+
signature = signature[:signature[1] + 2]
96+
97+
k = oscrypto.asymmetric.load_public_key(self.get_public_bytes())
98+
return oscrypto.asymmetric.ecdsa_verify(k, signature, payload, 'sha256')
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Tests for PKCS11 keys
3+
"""
4+
5+
import io
6+
import os.path
7+
import sys
8+
import tempfile
9+
import unittest
10+
import oscrypto
11+
import oscrypto.errors
12+
import pkcs11
13+
import pkcs11.exceptions
14+
15+
sys.path.insert(0, os.path.abspath(
16+
os.path.join(os.path.dirname(__file__), '../..')))
17+
18+
from imgtool.keys import load, PKCS11
19+
20+
21+
class EcKeyGeneration(unittest.TestCase):
22+
23+
def setUp(self):
24+
self.test_dir = tempfile.TemporaryDirectory()
25+
26+
def tname(self, base):
27+
return os.path.join(self.test_dir.name, base)
28+
29+
def tearDown(self):
30+
self.test_dir.cleanup()
31+
32+
def test_emit(self):
33+
"""Basic sanity check on the code emitters."""
34+
k = PKCS11()
35+
36+
ccode = io.StringIO()
37+
k.emit_c_public(ccode)
38+
self.assertIn("ecdsa_pub_key", ccode.getvalue())
39+
self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
40+
41+
rustcode = io.StringIO()
42+
k.emit_rust_public(rustcode)
43+
self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
44+
45+
def test_emit_pub(self):
46+
"""Basic sanity check on the code emitters."""
47+
pubname = self.tname("public.pem")
48+
k = PKCS11()
49+
k.export_public(pubname)
50+
51+
k2 = load(pubname)
52+
53+
ccode = io.StringIO()
54+
k2.emit_c_public(ccode)
55+
self.assertIn("ecdsa_pub_key", ccode.getvalue())
56+
self.assertIn("ecdsa_pub_key_len", ccode.getvalue())
57+
58+
rustcode = io.StringIO()
59+
k2.emit_rust_public(rustcode)
60+
self.assertIn("ECDSA_PUB_KEY", rustcode.getvalue())
61+
62+
def test_sig(self):
63+
k = PKCS11()
64+
buf = b'This is the message'
65+
sig = k.raw_sign(buf)
66+
67+
k.verify(
68+
signature=sig,
69+
payload=buf)
70+
71+
# Modify the message to make sure the signature fails.
72+
self.assertRaises(oscrypto.errors.SignatureError,
73+
k.verify,
74+
signature=sig,
75+
payload=b'This is thE message')
76+
77+
def clone_env(self):
78+
return {
79+
'PKCS11_MODULE': os.environ['PKCS11_MODULE'],
80+
'PKCS11_TOKEN_LABEL': os.environ['PKCS11_TOKEN_LABEL'],
81+
'PKCS11_KEY_ID': os.environ['PKCS11_KEY_ID'],
82+
'PKCS11_PIN': os.environ['PKCS11_PIN'],
83+
}
84+
85+
def test_env(self):
86+
env = self.clone_env()
87+
env['PKCS11_MODULE'] = '/invalid/path'
88+
self.assertRaises(
89+
pkcs11.exceptions.AlreadyInitialized, PKCS11, env=env)
90+
91+
env = self.clone_env()
92+
env['PKCS11_TOKEN_LABEL'] = 'invalid_label'
93+
self.assertRaises(pkcs11.exceptions.NoSuchToken, PKCS11, env=env)
94+
95+
env = self.clone_env()
96+
env['PKCS11_PIN'] = '123456'
97+
self.assertRaises(pkcs11.exceptions.PinIncorrect, PKCS11, env=env)
98+
99+
env = self.clone_env()
100+
env['PKCS11_KEY_ID'] = '00'
101+
k = PKCS11(env)
102+
self.assertRaises(pkcs11.exceptions.NoSuchKey,
103+
k.raw_sign, payload=b'test')
104+
105+
106+
if __name__ == '__main__':
107+
unittest.main()

scripts/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@ cryptography>=2.6
22
intelhex
33
click
44
cbor>=1.0.0
5+
oscrypto
6+
7+
# the current release doesn't parse 0-bytes in token labels correctly
8+
-e git+https://github.com/danni/python-pkcs11.git@3639994#egg=python-pkcs11

0 commit comments

Comments
 (0)