keylet 0.2.0__tar.gz → 0.2.1.dev0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 keylet authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,15 +1,19 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: keylet
3
- Version: 0.2.0
4
- Summary: Client application for Tillitis TKey hardware ML-DSA signer
3
+ Version: 0.2.1.dev0
4
+ Summary: Client application for Tillitis TKey hardware ML-DSA/Ed25519 signer
5
+ Keywords: ML-DSA,TKey,Tillitis,PQC,sign,signer
5
6
  Author: Jussi Kukkonen
6
7
  Author-email: Jussi Kukkonen <jkukkonen@google.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
7
10
  Requires-Dist: cryptography>=48.0.0
8
11
  Requires-Dist: pyserial>=3.5
9
12
  Requires-Python: >=3.10
13
+ Project-URL: Documentation, https://jku.github.io/keylet/
14
+ Project-URL: Homepage, https://github.com/jku/keylet
10
15
  Project-URL: Issues, https://github.com/jku/keylet/issues
11
16
  Project-URL: Source, https://github.com/jku/keylet
12
- Project-URL: Documentation, https://jku.github.io/keylet/
13
17
  Description-Content-Type: text/markdown
14
18
 
15
19
  # keylet -- Client library for Tillitis TKey
@@ -18,6 +22,15 @@ Description-Content-Type: text/markdown
18
22
 
19
23
  Keylet is a Python client library and CLI tool for the [Tillitis TKey](https://www.tillitis.se/products/tkey/) security token, and implements a ML-DSA / Ed25519 signer application for TKey.
20
24
 
25
+ TKeys unique feature is that it has no long-term memory: signing keys are _always_ generated from a seed at runtime. This seed is built by combining a Unique Device Secret, a Device Application hash and an optional User Supplied Secret. Both the Device Application and User Supplied Secret are provided at runtime by `keylet`.
26
+
27
+ The unique design leads to some API peculiarities:
28
+
29
+ * User Supplied Secret (passphrase) is not directly validated by keylet: a "wrong" passphrase will just lead to using a different signing key. In practice the calling application should look at `TKeySign.get_pubkey()`: if the key is unexpected, then potentially the wrong passphrase was used.
30
+ * In long-term use (where the same signing key is expected to be used over a period of time) the calling application is responsible for always selecting the same Device Application: `keylet` provides a mechanism for this, see examples.
31
+ * The only way to change the device application or passphrase after initialization is to unplug the device and start over.
32
+ * Signer initialization has an optimization where the initialization succeeds if the TKey has already been initialized with matching device application name and version. Unfortunately `keylet` cannot confirm that the exact device binary is the expected one or that the passphrase is still the same one (but again, the calling application can compare `TKeySign.get_pubkey()` to the expected key)
33
+
21
34
  ## Installation
22
35
 
23
36
  ```bash
@@ -26,7 +39,7 @@ pip install keylet
26
39
 
27
40
  ## CLI Usage
28
41
 
29
- The package installs a `keylet` command-line tool forsigning and verification. This is primarily a test/demo application for the library.
42
+ The package installs a `keylet` command-line tool for signing and verification. This is primarily a test/demo application for the library.
30
43
 
31
44
  ```bash
32
45
  # Sign without a passphrase, then verify
@@ -74,3 +87,24 @@ with TKeySign(app=app, secret="hunter2") as signer:
74
87
  ```
75
88
 
76
89
  See the [API Reference](https://jku.github.io/keylet/api/) for more details.
90
+
91
+ ## Development
92
+
93
+ [uv](https://docs.astral.sh/uv/) is a required development tool.
94
+
95
+ ```bash
96
+ # Run keylet CLI from source
97
+ uv run keylet sign README.md
98
+
99
+ # run linters and type checker
100
+ make lint
101
+
102
+ # Fix formatting and lint issues
103
+ make fix
104
+
105
+ # run tests
106
+ make test
107
+
108
+ # run tests, including on-device tests
109
+ make test-device
110
+ ```
@@ -4,6 +4,15 @@
4
4
 
5
5
  Keylet is a Python client library and CLI tool for the [Tillitis TKey](https://www.tillitis.se/products/tkey/) security token, and implements a ML-DSA / Ed25519 signer application for TKey.
6
6
 
7
+ TKeys unique feature is that it has no long-term memory: signing keys are _always_ generated from a seed at runtime. This seed is built by combining a Unique Device Secret, a Device Application hash and an optional User Supplied Secret. Both the Device Application and User Supplied Secret are provided at runtime by `keylet`.
8
+
9
+ The unique design leads to some API peculiarities:
10
+
11
+ * User Supplied Secret (passphrase) is not directly validated by keylet: a "wrong" passphrase will just lead to using a different signing key. In practice the calling application should look at `TKeySign.get_pubkey()`: if the key is unexpected, then potentially the wrong passphrase was used.
12
+ * In long-term use (where the same signing key is expected to be used over a period of time) the calling application is responsible for always selecting the same Device Application: `keylet` provides a mechanism for this, see examples.
13
+ * The only way to change the device application or passphrase after initialization is to unplug the device and start over.
14
+ * Signer initialization has an optimization where the initialization succeeds if the TKey has already been initialized with matching device application name and version. Unfortunately `keylet` cannot confirm that the exact device binary is the expected one or that the passphrase is still the same one (but again, the calling application can compare `TKeySign.get_pubkey()` to the expected key)
15
+
7
16
  ## Installation
8
17
 
9
18
  ```bash
@@ -12,7 +21,7 @@ pip install keylet
12
21
 
13
22
  ## CLI Usage
14
23
 
15
- The package installs a `keylet` command-line tool forsigning and verification. This is primarily a test/demo application for the library.
24
+ The package installs a `keylet` command-line tool for signing and verification. This is primarily a test/demo application for the library.
16
25
 
17
26
  ```bash
18
27
  # Sign without a passphrase, then verify
@@ -60,3 +69,24 @@ with TKeySign(app=app, secret="hunter2") as signer:
60
69
  ```
61
70
 
62
71
  See the [API Reference](https://jku.github.io/keylet/api/) for more details.
72
+
73
+ ## Development
74
+
75
+ [uv](https://docs.astral.sh/uv/) is a required development tool.
76
+
77
+ ```bash
78
+ # Run keylet CLI from source
79
+ uv run keylet sign README.md
80
+
81
+ # run linters and type checker
82
+ make lint
83
+
84
+ # Fix formatting and lint issues
85
+ make fix
86
+
87
+ # run tests
88
+ make test
89
+
90
+ # run tests, including on-device tests
91
+ make test-device
92
+ ```
@@ -1,24 +1,28 @@
1
1
  [project]
2
2
  name = "keylet"
3
- version = "0.2.0"
4
- description = "Client application for Tillitis TKey hardware ML-DSA signer"
3
+ version = "0.2.1-dev"
4
+ description = "Client application for Tillitis TKey hardware ML-DSA/Ed25519 signer"
5
5
  readme = "README.md"
6
6
  authors = [
7
7
  { name = "Jussi Kukkonen", email = "jkukkonen@google.com" }
8
8
  ]
9
+ license = "MIT"
10
+ license-files = ["LICENSE"]
9
11
  requires-python = ">=3.10"
10
12
  dependencies = [
11
13
  "cryptography>=48.0.0",
12
14
  "pyserial>=3.5",
13
15
  ]
16
+ keywords = ["ML-DSA", "TKey", "Tillitis", "PQC", "sign", "signer"]
14
17
 
15
18
  [project.scripts]
16
19
  keylet = "keylet.bin.cli:main"
17
20
 
18
21
  [project.urls]
22
+ Documentation = "https://jku.github.io/keylet/"
23
+ Homepage = "https://github.com/jku/keylet"
19
24
  Issues = "https://github.com/jku/keylet/issues"
20
25
  Source = "https://github.com/jku/keylet"
21
- Documentation = "https://jku.github.io/keylet/"
22
26
 
23
27
  [dependency-groups]
24
28
  dev = [
@@ -28,58 +32,52 @@ dev = [
28
32
  "zizmor",
29
33
  "zensical",
30
34
  "mkdocstrings[python]",
35
+ "types-pyserial",
31
36
  ]
32
37
 
38
+ # pin uv in setup-uv action
39
+ uv = ["uv"]
40
+
33
41
  [build-system]
34
42
  requires = ["uv_build>=0.11.23,<0.12.0"]
35
43
  build-backend = "uv_build"
36
44
 
37
- [tool.ruff]
38
- line-length = 88
39
-
40
45
  [tool.ruff.lint]
41
- select = [
42
- "E", # pycodestyle errors
43
- "W", # pycodestyle warnings
44
- "F", # pyflakes
45
- "I", # isort
46
- "B", # flake8-bugbear
47
- "C4", # flake8-comprehensions
48
- "UP", # pyupgrade
49
- "RUF", # Ruff-specific rules
50
- "S", # flake8-bandit (Security)
51
- "SIM", # flake8-simplify
52
- "PT", # flake8-pytest-style
53
- "PTH", # flake8-use-pathlib
54
- "TID", # flake8-tidy-imports
55
- "T20", # flake8-print
56
- "ARG", # flake8-unused-arguments
57
- "SIM", # flake8-simplify
46
+ select = ["ALL"]
47
+ ignore = [
48
+ "BLE001", # blind exception
49
+ "PLR2004", # magic value
50
+ "COM812", # trailing comma
51
+ "D", # pydocstyle
52
+ "EM", # flake8-errmsg
53
+ "TRY003", # long message in exception argument
54
+ "PYI019" # typing.Self is only available in >= 3.13
58
55
  ]
59
- ignore = []
60
56
 
61
57
  [tool.ruff.lint.per-file-ignores]
62
- "src/keylet/bin/cli.py" = ["T201"]
58
+ "src/keylet/bin/**" = [
59
+ "T201" # print is ok in CLI tool
60
+ ]
63
61
  "tests/**" = [
64
- "S101", # Use of assert
65
- "S105", # Possible hardcoded password
66
- "PT009", # Use regular assert instead of unittest assertEqual
67
- "PT027", # Use pytest.raises instead of unittest assertRaises
62
+ "S101", # assert is ok in tests
63
+ "S105", # hardcoded password is ok in tests
68
64
  "ARG002", # Unused method argument (common with mocks)
69
65
  ]
70
66
 
71
-
72
-
73
67
  [tool.mypy]
74
68
  python_version = "3.10"
69
+ pretty = true
75
70
  strict = true
71
+ strict_equality = true
72
+ disallow_any_unimported = true
73
+ disallow_untyped_calls = true
74
+ disallow_untyped_defs = true
75
+ warn_redundant_casts = true
76
+ warn_return_any = true
76
77
  warn_unreachable = true
78
+ warn_unused_ignores = true
77
79
 
78
- [[tool.mypy.overrides]]
79
- module = "serial.*"
80
- ignore_missing_imports = true
81
-
82
- [tool.pytest.ini_options]
80
+ [tool.pytest]
83
81
  markers = [
84
82
  "device: tests that require a physical TKey device",
85
83
  ]
@@ -98,11 +98,11 @@ class RawSerialConnection:
98
98
  # 4. Acquire exclusive access
99
99
  tiocexcl = 0x540C
100
100
  fcntl.ioctl(fd, tiocexcl, 0)
101
-
102
- return fd
103
101
  except Exception:
104
102
  os.close(fd)
105
103
  raise
104
+ else:
105
+ return fd
106
106
 
107
107
  def write(self, data: bytes) -> int:
108
108
  if self._fd is None:
@@ -0,0 +1,145 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2026 keylet authors
3
+
4
+ import argparse
5
+ import sys
6
+ from collections.abc import Generator
7
+ from contextlib import contextmanager
8
+ from pathlib import Path
9
+
10
+ from cryptography.exceptions import InvalidSignature
11
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
12
+ from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA44PublicKey
13
+
14
+ from keylet.tkey import TKeyNotFoundError, TKeyUnexpectedAppError
15
+ from keylet.tkey_sign import SignApp, TKeySign
16
+
17
+
18
+ @contextmanager
19
+ def _app_signer(args: argparse.Namespace) -> Generator[TKeySign, None, None]:
20
+ try:
21
+ if args.type == "ed25519":
22
+ app = SignApp.load_ed25519(digest=args.digest)
23
+ else:
24
+ app = SignApp.load_mldsa(digest=args.digest)
25
+ with TKeySign(app, secret=args.passphrase) as signer:
26
+ print(f"Using {args.type} device app with digest {app.digest[:7]}")
27
+ yield signer
28
+ except TKeyNotFoundError as e:
29
+ sys.exit(f"Error: {e}")
30
+ except TKeyUnexpectedAppError as e:
31
+ sys.exit(f"Error: {e}")
32
+
33
+
34
+ def cmd_pubkey(args: argparse.Namespace) -> None:
35
+ with _app_signer(args) as signer:
36
+ pubkey = signer.get_pubkey()
37
+ if args.output:
38
+ Path(args.output).write_bytes(pubkey)
39
+ print(f"Public key written to {args.output}")
40
+ else:
41
+ print(pubkey.hex())
42
+
43
+
44
+ def cmd_sign(args: argparse.Namespace) -> None:
45
+ file_path = Path(args.file)
46
+ if not file_path.exists():
47
+ sys.exit(f"Error: File {args.file} does not exist")
48
+
49
+ data = file_path.read_bytes()
50
+ with _app_signer(args) as signer:
51
+ print("Please touch the TKey device when it flashes to sign...")
52
+ signature = signer.sign(data)
53
+
54
+ sig_path = file_path.with_suffix(file_path.suffix + ".signature")
55
+ sig_path.write_bytes(signature)
56
+ print(f"Signature written to {sig_path}")
57
+
58
+
59
+ def cmd_verify(args: argparse.Namespace) -> None:
60
+ file_path = Path(args.file)
61
+ if not file_path.exists():
62
+ sys.exit(f"Error: File {args.file} does not exist")
63
+
64
+ sig_path = (
65
+ Path(args.signature)
66
+ if args.signature
67
+ else file_path.with_suffix(file_path.suffix + ".signature")
68
+ )
69
+ if not sig_path.exists():
70
+ sys.exit(f"Error: Signature file {sig_path} does not exist")
71
+
72
+ file_bytes = file_path.read_bytes()
73
+ sig_bytes = sig_path.read_bytes()
74
+
75
+ # Get public key either from file or from device
76
+ if args.pubkey:
77
+ pubkey_bytes = Path(args.pubkey).read_bytes()
78
+ else:
79
+ with _app_signer(args) as signer:
80
+ print("Retrieving public key from device...")
81
+ pubkey_bytes = signer.get_pubkey()
82
+
83
+ try:
84
+ # Verify signature using cryptography
85
+ if args.type == "ed25519":
86
+ ed_pubkey = Ed25519PublicKey.from_public_bytes(pubkey_bytes)
87
+ ed_pubkey.verify(sig_bytes, file_bytes)
88
+ else:
89
+ ml_pubkey = MLDSA44PublicKey.from_public_bytes(pubkey_bytes)
90
+ ml_pubkey.verify(sig_bytes, file_bytes)
91
+ except InvalidSignature:
92
+ sys.exit("Verification failed: Invalid signature")
93
+
94
+ print("Verification successful!")
95
+
96
+
97
+ def main() -> None:
98
+ parser = argparse.ArgumentParser(
99
+ description="keylet -- Signing tool for Tillitis TKey"
100
+ )
101
+ parser.add_argument(
102
+ "--digest", help="Optional digest of the device application to use"
103
+ )
104
+ parser.add_argument("--passphrase")
105
+ parser.add_argument(
106
+ "-t",
107
+ "--type",
108
+ choices=["ml-dsa", "ed25519"],
109
+ default="ml-dsa",
110
+ help="key type (default: %(default)s)",
111
+ )
112
+
113
+ subparsers = parser.add_subparsers(dest="command", required=True)
114
+
115
+ # pubkey command
116
+ parser_pubkey = subparsers.add_parser("pubkey", help="Get public key from device")
117
+ parser_pubkey.add_argument("-o", "--output", help="File to write public key to")
118
+
119
+ # sign command
120
+ parser_sign = subparsers.add_parser("sign", help="Sign a file")
121
+ parser_sign.add_argument("file", help="File to sign")
122
+
123
+ # verify command
124
+ parser_verify = subparsers.add_parser("verify", help="Verify a signature")
125
+ parser_verify.add_argument("file", help="File to verify")
126
+ parser_verify.add_argument(
127
+ "--signature", help="Signature file (defaults to <FILE>.signature)"
128
+ )
129
+ parser_verify.add_argument(
130
+ "--pubkey",
131
+ help="Optional public key file (retrieved from device if not specified)",
132
+ )
133
+
134
+ args = parser.parse_args()
135
+
136
+ if args.command == "pubkey":
137
+ cmd_pubkey(args)
138
+ elif args.command == "sign":
139
+ cmd_sign(args)
140
+ elif args.command == "verify":
141
+ cmd_verify(args)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
File without changes
@@ -3,12 +3,12 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import errno
6
7
  import hashlib
7
8
  import logging
8
9
  import sys
9
10
  from dataclasses import dataclass
10
- from types import TracebackType
11
- from typing import TypeVar
11
+ from typing import TYPE_CHECKING, TypeVar
12
12
 
13
13
  import serial
14
14
  from serial.tools import list_ports
@@ -18,6 +18,9 @@ from keylet._serial_hack import (
18
18
  SerialConnection,
19
19
  )
20
20
 
21
+ if TYPE_CHECKING:
22
+ from types import TracebackType
23
+
21
24
  logger = logging.getLogger(__name__)
22
25
 
23
26
  # USB Vendor & Product ID for TKey
@@ -100,7 +103,7 @@ class FwCmd:
100
103
  """Command to send a chunk of application binary data."""
101
104
 
102
105
 
103
- _TKey = TypeVar("_TKey", bound="TKey")
106
+ Self = TypeVar("Self", bound="TKey")
104
107
 
105
108
 
106
109
  class TKeyError(Exception):
@@ -111,6 +114,10 @@ class TKeyNotFoundError(TKeyError):
111
114
  """A TKey device was not found"""
112
115
 
113
116
 
117
+ class TKeyDeviceBusyError(TKeyError):
118
+ """Raised when the TKey device is already in use."""
119
+
120
+
114
121
  class TKeyAppError(TKeyError):
115
122
  """Raised when loading the application fails."""
116
123
 
@@ -123,6 +130,14 @@ class TKeyProtocolError(TKeyError):
123
130
  """Raised upon protocol errors in command or response."""
124
131
 
125
132
 
133
+ class TKeyNOKError(TKeyProtocolError):
134
+ """Raised when the TKey device returns a NOK (Not OK) status."""
135
+
136
+
137
+ class TKeyUnexpectedAppError(TKeyAppError):
138
+ """Raised when TKey is already running a different application."""
139
+
140
+
126
141
  class TKey:
127
142
  """Base TKey Client
128
143
 
@@ -176,13 +191,19 @@ class TKey:
176
191
  def _get_connection(
177
192
  self, port: str, baudrate: int, timeout: float
178
193
  ) -> SerialConnection:
179
- if sys.platform == "linux":
180
- return RawSerialConnection(port, baudrate, timeout)
181
- else:
182
- try:
183
- return serial.Serial(port, baudrate=baudrate, timeout=timeout)
184
- except Exception as e:
185
- raise TKeyError(f"Failed to open serial port {port}: {e}") from e
194
+ try:
195
+ if sys.platform == "linux":
196
+ conn = RawSerialConnection(port, baudrate, timeout)
197
+ else:
198
+ conn = serial.Serial(port, baudrate=baudrate, timeout=timeout)
199
+ except OSError as e:
200
+ if e.errno in (errno.EBUSY, errno.EACCES) or "Access is denied" in str(e):
201
+ raise TKeyDeviceBusyError(f"TKey device {port} is busy") from e
202
+ raise TKeyError(f"Failed to open serial port {port}") from e
203
+ except Exception as e:
204
+ raise TKeyError(f"Failed to open serial port {port}") from e
205
+
206
+ return conn
186
207
 
187
208
  def disconnect(self) -> None:
188
209
  if self._conn is not None:
@@ -195,7 +216,7 @@ class TKey:
195
216
  def __del__(self) -> None:
196
217
  self.disconnect()
197
218
 
198
- def __enter__(self: _TKey) -> _TKey:
219
+ def __enter__(self: Self) -> Self:
199
220
  return self
200
221
 
201
222
  def __exit__(
@@ -310,7 +331,7 @@ class TKey:
310
331
  self._conn.read(resp_len)
311
332
  except Exception as e:
312
333
  logger.debug("Failed to read remaining bytes after NOK status: %s", e)
313
- raise TKeyProtocolError("Response status code not OK (1)")
334
+ raise TKeyNOKError("Response status code not OK (1)")
314
335
 
315
336
  try:
316
337
  resp_data = self._conn.read(resp_len)
@@ -366,9 +387,8 @@ class TKey:
366
387
  try:
367
388
  # Query firmware name
368
389
  rx = self.send(FwCmd.NAME_VERSION)
369
- except TKeyError:
370
- # Not in firmware mode
371
- # TODO would be nice to only do this on NOK response, not other errors
390
+ except TKeyNOKError:
391
+ # Device returned NOK: we are running an application already
372
392
  return False
373
393
 
374
394
  # we are in firmware mode. Load the app
@@ -10,16 +10,15 @@ import importlib.resources
10
10
  import logging
11
11
  from dataclasses import dataclass
12
12
 
13
- from keylet.tkey import Cmd, LenIdx, Rsp, TKey, TKeyAppError, TKeyError
13
+ from keylet.tkey import Cmd, LenIdx, Rsp, TKey, TKeyError, TKeyUnexpectedAppError
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
17
17
  MU_SIZE = (64).to_bytes(4, byteorder="little")
18
18
 
19
19
 
20
- # Static registry of signer binaries
20
+ # Static registry of signer binaries (filename, version)
21
21
  # First binary in each list is the default binary.
22
- # Format: (filename, version)
23
22
  _EMBEDDED_MLDSA_BINS: list[tuple[str, int]] = [
24
23
  ("pqsigner_v3.bin", 3),
25
24
  ]
@@ -132,6 +131,9 @@ class SignApp:
132
131
  TKey key derivation depends on the application binary, so users who want a
133
132
  specific key must provide the binary digest.
134
133
 
134
+ Warning:
135
+ When Ed25519 is used, there is a 4096K size limit to signing payloads.
136
+
135
137
  Args:
136
138
  version: The version of the signer application to load.
137
139
  digest: A BLAKE2s-256 hex digest (or prefix) of the target binary.
@@ -199,7 +201,7 @@ class TKeySign(TKey):
199
201
 
200
202
  Raises:
201
203
  TKeyNotFoundError: If the TKey device cannot be found.
202
- TKeyAppError: If loading the application fails or the device is
204
+ TKeyUnexpectedAppError: If loading the application fails or the device is
203
205
  running a mismatched application.
204
206
  TKeyError: For other connection or initialization failures.
205
207
  """
@@ -220,7 +222,7 @@ class TKeySign(TKey):
220
222
  if name == app.name and ver == app.version:
221
223
  return # Signer application is already loaded
222
224
 
223
- raise TKeyAppError(
225
+ raise TKeyUnexpectedAppError(
224
226
  f"TKey is running an unknown application {name, ver}, "
225
227
  f"expected {app.name, app.version}"
226
228
  )
@@ -266,7 +268,9 @@ class TKeySign(TKey):
266
268
  the physical TKey device when it flashes.
267
269
 
268
270
  Args:
269
- message: The raw bytes of the message/payload to sign.
271
+ message: The raw bytes of the message/payload to sign. When Ed25519 keys
272
+ are used, there is a max message size of 4096K. This limitation does
273
+ not apply to ML-DSA as FIPS 204 external mu is used.
270
274
  pub_key: The public key bytes (only needed for ML-DSA). If not provided,
271
275
  key is retrieved from device.
272
276
 
@@ -1,158 +0,0 @@
1
- # SPDX-License-Identifier: MIT
2
- # Copyright (c) 2026 keylet authors
3
-
4
- import argparse
5
- import sys
6
- from pathlib import Path
7
-
8
- from cryptography.exceptions import InvalidSignature
9
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
10
- from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA44PublicKey
11
-
12
- from keylet.tkey import TKeyNotFoundError
13
- from keylet.tkey_sign import SignApp, TKeySign
14
-
15
-
16
- def cmd_pubkey(args: argparse.Namespace) -> int:
17
- try:
18
- if args.type == "ed25519":
19
- app = SignApp.load_ed25519(digest=args.digest)
20
- else:
21
- app = SignApp.load_mldsa(digest=args.digest)
22
- with TKeySign(app, secret=args.passphrase) as signer:
23
- pubkey = signer.get_pubkey()
24
- print(f"Using device app with digest {app.digest[:7]}.")
25
- if args.output:
26
- Path(args.output).write_bytes(pubkey)
27
- print(f"Public key written to {args.output}")
28
- else:
29
- print(pubkey.hex())
30
- return 0
31
- except TKeyNotFoundError as e:
32
- print(f"Error: {e}", file=sys.stderr)
33
- return 1
34
-
35
-
36
- def cmd_sign(args: argparse.Namespace) -> int:
37
- file_path = Path(args.file)
38
- if not file_path.exists():
39
- print(f"Error: File {args.file} does not exist", file=sys.stderr)
40
- return 1
41
-
42
- try:
43
- data = file_path.read_bytes()
44
- if args.type == "ed25519":
45
- app = SignApp.load_ed25519(digest=args.digest)
46
- else:
47
- app = SignApp.load_mldsa(digest=args.digest)
48
- with TKeySign(app, secret=args.passphrase) as signer:
49
- print(f"Using device app with digest {app.digest[:7]}.")
50
- print("Please touch the TKey device when it flashes to sign...")
51
- signature = signer.sign(data)
52
-
53
- sig_path = file_path.with_suffix(file_path.suffix + ".signature")
54
- sig_path.write_bytes(signature)
55
- print(f"Signature written to {sig_path}")
56
- return 0
57
- except TKeyNotFoundError as e:
58
- print(f"Signing failed: {e}", file=sys.stderr)
59
- return 1
60
-
61
-
62
- def cmd_verify(args: argparse.Namespace) -> int:
63
- file_path = Path(args.file)
64
- if not file_path.exists():
65
- print(f"Error: File {args.file} does not exist", file=sys.stderr)
66
- return 1
67
-
68
- sig_path = (
69
- Path(args.signature)
70
- if args.signature
71
- else file_path.with_suffix(file_path.suffix + ".signature")
72
- )
73
- if not sig_path.exists():
74
- print(f"Error: Signature file {sig_path} does not exist", file=sys.stderr)
75
- return 1
76
-
77
- try:
78
- file_bytes = file_path.read_bytes()
79
- sig_bytes = sig_path.read_bytes()
80
-
81
- # Get public key either from file or from device
82
- if args.pubkey:
83
- pubkey_bytes = Path(args.pubkey).read_bytes()
84
- else:
85
- if args.type == "ed25519":
86
- app = SignApp.load_ed25519(digest=args.digest)
87
- else:
88
- app = SignApp.load_mldsa(digest=args.digest)
89
- with TKeySign(app, secret=args.passphrase) as signer:
90
- print(f"Using device app with digest {app.digest[:7]}.")
91
- print("Retrieving public key from device...")
92
- pubkey_bytes = signer.get_pubkey()
93
-
94
- # Verify signature using cryptography library
95
- if args.type == "ed25519":
96
- ed_pubkey = Ed25519PublicKey.from_public_bytes(pubkey_bytes)
97
- ed_pubkey.verify(sig_bytes, file_bytes)
98
- else:
99
- ml_pubkey = MLDSA44PublicKey.from_public_bytes(pubkey_bytes)
100
- ml_pubkey.verify(sig_bytes, file_bytes)
101
- print("Verification successful!")
102
- return 0
103
- except InvalidSignature:
104
- print("Verification failed: Invalid signature", file=sys.stderr)
105
- return 1
106
- except TKeyNotFoundError as e:
107
- print(f"Verification failed: {e}", file=sys.stderr)
108
- return 1
109
-
110
-
111
- def main() -> None:
112
- parser = argparse.ArgumentParser(
113
- description="Tillitis TKey Keylet CLI testing tool"
114
- )
115
- parser.add_argument("--digest", help="The digest of the device application to use")
116
- parser.add_argument("--passphrase", help="User Supplied Secret (passphrase)")
117
- parser.add_argument(
118
- "-t",
119
- "--type",
120
- choices=["ml-dsa", "ed25519"],
121
- default="ml-dsa",
122
- help="Signer type (default: %(default)s)",
123
- )
124
-
125
- subparsers = parser.add_subparsers(dest="command", required=True)
126
-
127
- # pubkey command
128
- parser_pubkey = subparsers.add_parser("pubkey", help="Get public key from device")
129
- parser_pubkey.add_argument(
130
- "-o", "--output", help="Output file to write public key to"
131
- )
132
-
133
- # sign command
134
- parser_sign = subparsers.add_parser("sign", help="Sign a file")
135
- parser_sign.add_argument("file", help="File to sign")
136
-
137
- # verify command
138
- parser_verify = subparsers.add_parser("verify", help="Verify a signature")
139
- parser_verify.add_argument("file", help="File that was signed")
140
- parser_verify.add_argument(
141
- "--signature", help="Signature file (defaults to <FILE>.signature)"
142
- )
143
- parser_verify.add_argument(
144
- "--pubkey", help="Public key file (retrieved from device if not specified)"
145
- )
146
-
147
- args = parser.parse_args()
148
-
149
- if args.command == "pubkey":
150
- sys.exit(cmd_pubkey(args))
151
- elif args.command == "sign":
152
- sys.exit(cmd_sign(args))
153
- elif args.command == "verify":
154
- sys.exit(cmd_verify(args))
155
-
156
-
157
- if __name__ == "__main__":
158
- main()