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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: keylet
3
- Version: 0.7.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
- # Sign a payload
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
- # Sign a payload
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
- # Sign a payload
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
- # Sign a payload
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.7.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.11.23,<0.12.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.7.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.11.23,<0.12.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.tkey import TKeyNotFoundError, TKeyUnexpectedAppError
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
- data = file_path.read_bytes()
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
- signature = signer.sign(data)
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
- from keylet.tkey import Cmd, LenIdx, Rsp, TKey, TKeyError, TKeyUnexpectedAppError
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, it
193
- verifies that the running application matches the expected name and version.
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: bytes, pub_key: bytes | None = None) -> bytes:
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 of the message/payload to sign. When Ed25519 keys
272
- are used, there is a max message size of 4096B. This limitation does
273
- not apply to ML-DSA as FIPS 204 external mu is used.
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
- payload = hashlib.shake_256(tr + b"\x00\x00" + message).digest(64)
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
@@ -1,6 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2026 keylet authors
3
-
4
- from keylet.tkey_sign import SignApp, TKeySign
5
-
6
- __all__ = ["SignApp", "TKeySign"]
File without changes
File without changes