keylet 0.7.0__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {keylet-0.7.0 → keylet-1.0.0}/PKG-INFO +6 -5
- {keylet-0.7.0 → keylet-1.0.0}/README.md +5 -4
- {keylet-0.7.0 → keylet-1.0.0}/pyproject.toml +2 -2
- {keylet-0.7.0 → keylet-1.0.0}/pyproject.toml.orig +2 -2
- keylet-1.0.0/src/keylet/__init__.py +29 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/bin/cli.py +16 -4
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/tkey.py +4 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/tkey_sign.py +71 -12
- keylet-0.7.0/src/keylet/__init__.py +0 -6
- {keylet-0.7.0 → keylet-1.0.0}/LICENSE +0 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/_serial_hack.py +0 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/bin/__init__.py +0 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/py.typed +0 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/resources/__init__.py +0 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/resources/ed25519signer_v3.bin +0 -0
- {keylet-0.7.0 → keylet-1.0.0}/src/keylet/resources/pqsigner_v3.bin +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keylet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Client application for Tillitis TKey hardware ML-DSA/Ed25519 signer
|
|
5
5
|
Keywords: ML-DSA,TKey,Tillitis,PQC,sign,signer
|
|
6
6
|
Author: Jussi Kukkonen
|
|
@@ -67,9 +67,9 @@ from keylet import TKeySign, SignApp
|
|
|
67
67
|
app = SignApp.load_mldsa()
|
|
68
68
|
digest = app.digest
|
|
69
69
|
|
|
70
|
-
# Initialize the signer with a passphrase
|
|
70
|
+
# Initialize the signer with a passphrase, sign a payload
|
|
71
71
|
with TKeySign(app=app, secret="hunter2") as signer:
|
|
72
|
-
|
|
72
|
+
pubkey = signer.get_pubkey()
|
|
73
73
|
signature = signer.sign(b"my payload")
|
|
74
74
|
```
|
|
75
75
|
|
|
@@ -80,9 +80,10 @@ is always used for a specific key:
|
|
|
80
80
|
# Load application with a digest stored earlier
|
|
81
81
|
app = SignApp.load_mldsa(digest=digest)
|
|
82
82
|
|
|
83
|
-
# Initialize the signer with a passphrase
|
|
83
|
+
# Initialize the signer with a passphrase, sign a payload
|
|
84
84
|
with TKeySign(app=app, secret="hunter2") as signer:
|
|
85
|
-
|
|
85
|
+
if signer.get_pubkey() != pubkey:
|
|
86
|
+
exit("Unexpected signing key: maybe incorrect password?")
|
|
86
87
|
signature = signer.sign(b"my payload")
|
|
87
88
|
```
|
|
88
89
|
|
|
@@ -49,9 +49,9 @@ from keylet import TKeySign, SignApp
|
|
|
49
49
|
app = SignApp.load_mldsa()
|
|
50
50
|
digest = app.digest
|
|
51
51
|
|
|
52
|
-
# Initialize the signer with a passphrase
|
|
52
|
+
# Initialize the signer with a passphrase, sign a payload
|
|
53
53
|
with TKeySign(app=app, secret="hunter2") as signer:
|
|
54
|
-
|
|
54
|
+
pubkey = signer.get_pubkey()
|
|
55
55
|
signature = signer.sign(b"my payload")
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -62,9 +62,10 @@ is always used for a specific key:
|
|
|
62
62
|
# Load application with a digest stored earlier
|
|
63
63
|
app = SignApp.load_mldsa(digest=digest)
|
|
64
64
|
|
|
65
|
-
# Initialize the signer with a passphrase
|
|
65
|
+
# Initialize the signer with a passphrase, sign a payload
|
|
66
66
|
with TKeySign(app=app, secret="hunter2") as signer:
|
|
67
|
-
|
|
67
|
+
if signer.get_pubkey() != pubkey:
|
|
68
|
+
exit("Unexpected signing key: maybe incorrect password?")
|
|
68
69
|
signature = signer.sign(b"my payload")
|
|
69
70
|
```
|
|
70
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "keylet"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Client application for Tillitis TKey hardware ML-DSA/Ed25519 signer"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -45,7 +45,7 @@ dev = [
|
|
|
45
45
|
uv = ["uv"]
|
|
46
46
|
|
|
47
47
|
[build-system]
|
|
48
|
-
requires = ["uv_build>=0.
|
|
48
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
49
49
|
build-backend = "uv_build"
|
|
50
50
|
|
|
51
51
|
[tool.ruff.lint]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "keylet"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "1.0.0"
|
|
4
4
|
description = "Client application for Tillitis TKey hardware ML-DSA/Ed25519 signer"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -39,7 +39,7 @@ dev = [
|
|
|
39
39
|
uv = ["uv"]
|
|
40
40
|
|
|
41
41
|
[build-system]
|
|
42
|
-
requires = ["uv_build>=0.
|
|
42
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
43
43
|
build-backend = "uv_build"
|
|
44
44
|
|
|
45
45
|
[tool.ruff.lint]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 keylet authors
|
|
3
|
+
|
|
4
|
+
from keylet.tkey import (
|
|
5
|
+
TKeyAppError,
|
|
6
|
+
TKeyDeviceBusyError,
|
|
7
|
+
TKeyError,
|
|
8
|
+
TKeyIOError,
|
|
9
|
+
TKeyNOKError,
|
|
10
|
+
TKeyNotFoundError,
|
|
11
|
+
TKeyNotInFirmwareModeError,
|
|
12
|
+
TKeyProtocolError,
|
|
13
|
+
TKeyUnexpectedAppError,
|
|
14
|
+
)
|
|
15
|
+
from keylet.tkey_sign import SignApp, TKeySign
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"SignApp",
|
|
19
|
+
"TKeyAppError",
|
|
20
|
+
"TKeyDeviceBusyError",
|
|
21
|
+
"TKeyError",
|
|
22
|
+
"TKeyIOError",
|
|
23
|
+
"TKeyNOKError",
|
|
24
|
+
"TKeyNotFoundError",
|
|
25
|
+
"TKeyNotInFirmwareModeError",
|
|
26
|
+
"TKeyProtocolError",
|
|
27
|
+
"TKeySign",
|
|
28
|
+
"TKeyUnexpectedAppError",
|
|
29
|
+
]
|
|
@@ -12,8 +12,7 @@ from cryptography.exceptions import InvalidSignature
|
|
|
12
12
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
13
13
|
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA44PublicKey
|
|
14
14
|
|
|
15
|
-
from keylet
|
|
16
|
-
from keylet.tkey_sign import SignApp, TKeySign
|
|
15
|
+
from keylet import SignApp, TKeyNotFoundError, TKeySign, TKeyUnexpectedAppError
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
@contextmanager
|
|
@@ -48,10 +47,19 @@ def cmd_sign(args: argparse.Namespace) -> None:
|
|
|
48
47
|
if not file_path.exists():
|
|
49
48
|
sys.exit(f"Error: File {args.file} does not exist")
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
pubkey = None
|
|
51
|
+
if args.pubkey is not None:
|
|
52
|
+
key_path = Path(args.pubkey)
|
|
53
|
+
if not key_path.exists():
|
|
54
|
+
sys.exit(f"Error: Public key {args.pubkey} does not exist")
|
|
55
|
+
pubkey = key_path.read_bytes()
|
|
56
|
+
# Note that we do not compare the given pubkey to the pubkey from the device
|
|
57
|
+
# A real application would want to do that to check for passphrase typos etc.
|
|
58
|
+
|
|
52
59
|
with _app_signer(args) as signer:
|
|
53
60
|
print("Please touch the TKey device when it flashes to sign...")
|
|
54
|
-
|
|
61
|
+
with file_path.open("rb") as f:
|
|
62
|
+
signature = signer.sign(f, pubkey)
|
|
55
63
|
|
|
56
64
|
sig_path = file_path.with_suffix(file_path.suffix + ".signature")
|
|
57
65
|
sig_path.write_bytes(signature)
|
|
@@ -120,6 +128,10 @@ def main() -> None:
|
|
|
120
128
|
# sign command
|
|
121
129
|
parser_sign = subparsers.add_parser("sign", help="Sign a file")
|
|
122
130
|
parser_sign.add_argument("file", help="File to sign")
|
|
131
|
+
parser_sign.add_argument(
|
|
132
|
+
"--pubkey",
|
|
133
|
+
help="Optional public key file (retrieved from device if not specified)",
|
|
134
|
+
)
|
|
123
135
|
|
|
124
136
|
# verify command
|
|
125
137
|
parser_verify = subparsers.add_parser("verify", help="Verify a signature")
|
|
@@ -138,6 +138,10 @@ class TKeyUnexpectedAppError(TKeyAppError):
|
|
|
138
138
|
"""Raised when TKey is already running a different application."""
|
|
139
139
|
|
|
140
140
|
|
|
141
|
+
class TKeyNotInFirmwareModeError(TKeyAppError):
|
|
142
|
+
"""Raised when the TKey is not in firmware mode and firmware mode was required."""
|
|
143
|
+
|
|
144
|
+
|
|
141
145
|
class TKey:
|
|
142
146
|
"""Base TKey Client
|
|
143
147
|
|
|
@@ -8,13 +8,56 @@ from __future__ import annotations
|
|
|
8
8
|
import hashlib
|
|
9
9
|
import importlib.resources
|
|
10
10
|
import logging
|
|
11
|
+
from collections.abc import Iterable, Iterator
|
|
11
12
|
from dataclasses import dataclass
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
from typing import Protocol, TypeAlias, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from keylet.tkey import (
|
|
16
|
+
Cmd,
|
|
17
|
+
LenIdx,
|
|
18
|
+
Rsp,
|
|
19
|
+
TKey,
|
|
20
|
+
TKeyError,
|
|
21
|
+
TKeyNotInFirmwareModeError,
|
|
22
|
+
TKeyUnexpectedAppError,
|
|
23
|
+
)
|
|
14
24
|
|
|
15
25
|
logger = logging.getLogger(__name__)
|
|
16
26
|
|
|
17
27
|
MAX_PAYLOAD_SIZE = 4096
|
|
28
|
+
_STREAM_CHUNK_SIZE = 64 * 1024 # 64 KiB buffer for streaming
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class BinaryReader(Protocol):
|
|
33
|
+
"""Protocol for binary streams supporting chunked reads."""
|
|
34
|
+
|
|
35
|
+
def read(self, size: int = -1, /) -> bytes: ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SignableMessage: TypeAlias = bytes | BinaryReader | Iterable[bytes]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _iter_chunks(message: SignableMessage) -> Iterator[bytes]:
|
|
42
|
+
"""Yield chunks of bytes from bytes, a binary reader, or an iterable of bytes."""
|
|
43
|
+
if isinstance(message, bytes):
|
|
44
|
+
yield message
|
|
45
|
+
elif isinstance(message, BinaryReader):
|
|
46
|
+
while chunk := message.read(_STREAM_CHUNK_SIZE):
|
|
47
|
+
yield chunk
|
|
48
|
+
else: # iterable
|
|
49
|
+
yield from message
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _read_bounded(message: SignableMessage, max_size: int) -> bytes:
|
|
53
|
+
"""Read stream into memory up to max_size, failing fast on overflow."""
|
|
54
|
+
buf = bytearray()
|
|
55
|
+
for chunk in _iter_chunks(message):
|
|
56
|
+
buf.extend(chunk)
|
|
57
|
+
if len(buf) > max_size:
|
|
58
|
+
raise ValueError(f"Payload size exceeds maximum {max_size} bytes")
|
|
59
|
+
return bytes(buf)
|
|
60
|
+
|
|
18
61
|
|
|
19
62
|
# Static registry of signer binaries (filename, version)
|
|
20
63
|
# First binary in each list is the default binary.
|
|
@@ -185,12 +228,15 @@ class TKeySign(TKey):
|
|
|
185
228
|
app: SignApp,
|
|
186
229
|
device: str | None = None,
|
|
187
230
|
secret: str | None = None,
|
|
231
|
+
*,
|
|
232
|
+
require_firmware_mode: bool = False,
|
|
188
233
|
) -> None:
|
|
189
234
|
"""Initialize the TKey signing client.
|
|
190
235
|
|
|
191
236
|
If the TKey device is in firmware mode, this will automatically load the
|
|
192
|
-
application binary. If the device is already running an application
|
|
193
|
-
verifies that the running application
|
|
237
|
+
application binary. If the device is already running an application (and
|
|
238
|
+
require_firmware_mode is not set), verifies that the running application
|
|
239
|
+
matches the expected name and version.
|
|
194
240
|
|
|
195
241
|
Args:
|
|
196
242
|
app: The SignApp configuration containing the binary and metadata.
|
|
@@ -198,9 +244,13 @@ class TKeySign(TKey):
|
|
|
198
244
|
the port is auto-detected.
|
|
199
245
|
secret: Optional User Supplied Secret (passphrase) used as a seed
|
|
200
246
|
for key derivation.
|
|
247
|
+
require_firmware_mode: If True, fail if the device is not in firmware
|
|
248
|
+
mode instead of attempting to use an already running application.
|
|
201
249
|
|
|
202
250
|
Raises:
|
|
203
251
|
TKeyNotFoundError: If the TKey device cannot be found.
|
|
252
|
+
TKeyNotInFirmwareModeError: when require_firmware_mode is set and the
|
|
253
|
+
device is not in firmware mode.
|
|
204
254
|
TKeyUnexpectedAppError: If loading the application fails or the device is
|
|
205
255
|
running a mismatched application.
|
|
206
256
|
TKeyError: For other connection or initialization failures.
|
|
@@ -212,6 +262,11 @@ class TKeySign(TKey):
|
|
|
212
262
|
|
|
213
263
|
try:
|
|
214
264
|
if not self.load_app(app.binary, secret):
|
|
265
|
+
if require_firmware_mode:
|
|
266
|
+
raise TKeyNotInFirmwareModeError(
|
|
267
|
+
"TKey is not in firmware mode but require_firmware_mode was set"
|
|
268
|
+
)
|
|
269
|
+
|
|
215
270
|
# TKey is not in firmware mode: Query application name and version
|
|
216
271
|
rx = self.send(SignCmd.GET_NAMEVERSION)
|
|
217
272
|
name = (
|
|
@@ -255,22 +310,25 @@ class TKeySign(TKey):
|
|
|
255
310
|
|
|
256
311
|
return bytes(pubkey)
|
|
257
312
|
|
|
258
|
-
def sign(self, message:
|
|
313
|
+
def sign(self, message: SignableMessage, pub_key: bytes | None = None) -> bytes:
|
|
259
314
|
"""Sign a payload.
|
|
260
315
|
|
|
261
316
|
Sends payload to device and retrieves the signature.
|
|
262
317
|
|
|
263
318
|
For ML-DSA, the FIPS 204 external mu is computed using the message
|
|
264
319
|
and public key: the mu is sent to device instead of payload.
|
|
320
|
+
Streaming messages of arbitrary size are supported for ML-DSA.
|
|
321
|
+
For Ed25519, the message (or stream) must not exceed 4096 bytes.
|
|
265
322
|
|
|
266
323
|
Note:
|
|
267
324
|
This method blocks and waits (up to 60 seconds) for the user to touch
|
|
268
325
|
the physical TKey device when it flashes.
|
|
269
326
|
|
|
270
327
|
Args:
|
|
271
|
-
message: The raw bytes
|
|
272
|
-
|
|
273
|
-
not apply to ML-DSA as FIPS 204 external mu
|
|
328
|
+
message: The raw bytes, binary reader (file-like object), or chunk iterable
|
|
329
|
+
to sign. For Ed25519, the total message size cannot exceed 4096B.
|
|
330
|
+
This limitation does not apply to ML-DSA as FIPS 204 external mu
|
|
331
|
+
is used.
|
|
274
332
|
pub_key: The public key bytes (only needed for ML-DSA). If not provided,
|
|
275
333
|
key is retrieved from device.
|
|
276
334
|
|
|
@@ -287,13 +345,14 @@ class TKeySign(TKey):
|
|
|
287
345
|
if pub_key is None:
|
|
288
346
|
pub_key = self.get_pubkey()
|
|
289
347
|
tr = hashlib.shake_256(pub_key).digest(64)
|
|
290
|
-
|
|
348
|
+
shake = hashlib.shake_256(tr + b"\x00\x00")
|
|
349
|
+
for chunk in _iter_chunks(message):
|
|
350
|
+
shake.update(chunk)
|
|
351
|
+
payload = shake.digest(64)
|
|
291
352
|
else:
|
|
292
|
-
payload = message
|
|
353
|
+
payload = _read_bounded(message, MAX_PAYLOAD_SIZE)
|
|
293
354
|
|
|
294
355
|
# Set size
|
|
295
|
-
if len(payload) > MAX_PAYLOAD_SIZE:
|
|
296
|
-
raise ValueError(f"Payload too large {len(payload)} > {MAX_PAYLOAD_SIZE}]")
|
|
297
356
|
self.send(SignCmd.SET_SIZE, len(payload).to_bytes(4, byteorder="little"))
|
|
298
357
|
|
|
299
358
|
# Load data in chunks
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|