keylet 0.1.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.
- keylet-0.2.1.dev0/LICENSE +21 -0
- keylet-0.2.1.dev0/PKG-INFO +110 -0
- keylet-0.2.1.dev0/README.md +92 -0
- keylet-0.2.1.dev0/pyproject.toml +83 -0
- {keylet-0.1.0 → keylet-0.2.1.dev0}/src/keylet/_serial_hack.py +2 -2
- keylet-0.2.1.dev0/src/keylet/bin/cli.py +145 -0
- keylet-0.2.1.dev0/src/keylet/py.typed +0 -0
- keylet-0.2.1.dev0/src/keylet/resources/ed25519signer_v3.bin +0 -0
- {keylet-0.1.0 → keylet-0.2.1.dev0}/src/keylet/tkey.py +35 -15
- {keylet-0.1.0 → keylet-0.2.1.dev0}/src/keylet/tkey_sign.py +120 -63
- keylet-0.1.0/PKG-INFO +0 -15
- keylet-0.1.0/README.md +0 -4
- keylet-0.1.0/pyproject.toml +0 -64
- keylet-0.1.0/src/keylet/bin/cli.py +0 -139
- {keylet-0.1.0 → keylet-0.2.1.dev0}/src/keylet/__init__.py +0 -0
- /keylet-0.1.0/src/keylet/py.typed → /keylet-0.2.1.dev0/src/keylet/bin/__init__.py +0 -0
- {keylet-0.1.0 → keylet-0.2.1.dev0}/src/keylet/resources/__init__.py +0 -0
- {keylet-0.1.0 → keylet-0.2.1.dev0}/src/keylet/resources/pqsigner_v3.bin +0 -0
|
@@ -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.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keylet
|
|
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
|
|
6
|
+
Author: Jussi Kukkonen
|
|
7
|
+
Author-email: Jussi Kukkonen <jkukkonen@google.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: cryptography>=48.0.0
|
|
11
|
+
Requires-Dist: pyserial>=3.5
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Project-URL: Documentation, https://jku.github.io/keylet/
|
|
14
|
+
Project-URL: Homepage, https://github.com/jku/keylet
|
|
15
|
+
Project-URL: Issues, https://github.com/jku/keylet/issues
|
|
16
|
+
Project-URL: Source, https://github.com/jku/keylet
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# keylet -- Client library for Tillitis TKey
|
|
20
|
+
|
|
21
|
+
[](https://github.com/jku/keylet) [](https://pypi.org/project/keylet/) [](https://jku.github.io/keylet/)
|
|
22
|
+
|
|
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.
|
|
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
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install keylet
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CLI Usage
|
|
41
|
+
|
|
42
|
+
The package installs a `keylet` command-line tool for signing and verification. This is primarily a test/demo application for the library.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Sign without a passphrase, then verify
|
|
46
|
+
$ keylet sign README.md
|
|
47
|
+
$ keylet verify README.md
|
|
48
|
+
|
|
49
|
+
# Get public key, sign with a passphrase, and verify using the saved public key
|
|
50
|
+
$ keylet --passphrase hunter2 pubkey --output pub.key
|
|
51
|
+
$ keylet --passphrase hunter2 sign README.md
|
|
52
|
+
$ keylet verify --pubkey pub.key README.md
|
|
53
|
+
|
|
54
|
+
# When using keylet long-term, remember to specify device app digest (keylet default
|
|
55
|
+
# app version may change, but you will need a specific application to keep using the
|
|
56
|
+
# same key)
|
|
57
|
+
$ keylet --passphrase hunter2 --digest 186bcf6 sign README.md
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Library Usage
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from keylet import TKeySign, SignApp
|
|
65
|
+
|
|
66
|
+
# Load the default embedded ML-DSA signer
|
|
67
|
+
app = SignApp.load_mldsa()
|
|
68
|
+
digest = app.digest
|
|
69
|
+
|
|
70
|
+
# Initialize the signer with a passphrase
|
|
71
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
72
|
+
# Sign a payload
|
|
73
|
+
signature = signer.sign(b"my payload")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
In long-term use, the device app digest should be used to ensure the same application
|
|
77
|
+
is always used for a specific key:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# Load application with a digest stored earlier
|
|
81
|
+
app = SignApp.load_mldsa(digest=digest)
|
|
82
|
+
|
|
83
|
+
# Initialize the signer with a passphrase
|
|
84
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
85
|
+
# Sign a payload
|
|
86
|
+
signature = signer.sign(b"my payload")
|
|
87
|
+
```
|
|
88
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# keylet -- Client library for Tillitis TKey
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jku/keylet) [](https://pypi.org/project/keylet/) [](https://jku.github.io/keylet/)
|
|
4
|
+
|
|
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
|
+
|
|
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
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install keylet
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CLI Usage
|
|
23
|
+
|
|
24
|
+
The package installs a `keylet` command-line tool for signing and verification. This is primarily a test/demo application for the library.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Sign without a passphrase, then verify
|
|
28
|
+
$ keylet sign README.md
|
|
29
|
+
$ keylet verify README.md
|
|
30
|
+
|
|
31
|
+
# Get public key, sign with a passphrase, and verify using the saved public key
|
|
32
|
+
$ keylet --passphrase hunter2 pubkey --output pub.key
|
|
33
|
+
$ keylet --passphrase hunter2 sign README.md
|
|
34
|
+
$ keylet verify --pubkey pub.key README.md
|
|
35
|
+
|
|
36
|
+
# When using keylet long-term, remember to specify device app digest (keylet default
|
|
37
|
+
# app version may change, but you will need a specific application to keep using the
|
|
38
|
+
# same key)
|
|
39
|
+
$ keylet --passphrase hunter2 --digest 186bcf6 sign README.md
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Library Usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from keylet import TKeySign, SignApp
|
|
47
|
+
|
|
48
|
+
# Load the default embedded ML-DSA signer
|
|
49
|
+
app = SignApp.load_mldsa()
|
|
50
|
+
digest = app.digest
|
|
51
|
+
|
|
52
|
+
# Initialize the signer with a passphrase
|
|
53
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
54
|
+
# Sign a payload
|
|
55
|
+
signature = signer.sign(b"my payload")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
In long-term use, the device app digest should be used to ensure the same application
|
|
59
|
+
is always used for a specific key:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Load application with a digest stored earlier
|
|
63
|
+
app = SignApp.load_mldsa(digest=digest)
|
|
64
|
+
|
|
65
|
+
# Initialize the signer with a passphrase
|
|
66
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
67
|
+
# Sign a payload
|
|
68
|
+
signature = signer.sign(b"my payload")
|
|
69
|
+
```
|
|
70
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "keylet"
|
|
3
|
+
version = "0.2.1-dev"
|
|
4
|
+
description = "Client application for Tillitis TKey hardware ML-DSA/Ed25519 signer"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Jussi Kukkonen", email = "jkukkonen@google.com" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"cryptography>=48.0.0",
|
|
14
|
+
"pyserial>=3.5",
|
|
15
|
+
]
|
|
16
|
+
keywords = ["ML-DSA", "TKey", "Tillitis", "PQC", "sign", "signer"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
keylet = "keylet.bin.cli:main"
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Documentation = "https://jku.github.io/keylet/"
|
|
23
|
+
Homepage = "https://github.com/jku/keylet"
|
|
24
|
+
Issues = "https://github.com/jku/keylet/issues"
|
|
25
|
+
Source = "https://github.com/jku/keylet"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"mypy",
|
|
30
|
+
"pytest",
|
|
31
|
+
"ruff",
|
|
32
|
+
"zizmor",
|
|
33
|
+
"zensical",
|
|
34
|
+
"mkdocstrings[python]",
|
|
35
|
+
"types-pyserial",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# pin uv in setup-uv action
|
|
39
|
+
uv = ["uv"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.11.23,<0.12.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
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
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint.per-file-ignores]
|
|
58
|
+
"src/keylet/bin/**" = [
|
|
59
|
+
"T201" # print is ok in CLI tool
|
|
60
|
+
]
|
|
61
|
+
"tests/**" = [
|
|
62
|
+
"S101", # assert is ok in tests
|
|
63
|
+
"S105", # hardcoded password is ok in tests
|
|
64
|
+
"ARG002", # Unused method argument (common with mocks)
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.mypy]
|
|
68
|
+
python_version = "3.10"
|
|
69
|
+
pretty = true
|
|
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
|
|
77
|
+
warn_unreachable = true
|
|
78
|
+
warn_unused_ignores = true
|
|
79
|
+
|
|
80
|
+
[tool.pytest]
|
|
81
|
+
markers = [
|
|
82
|
+
"device: tests that require a physical TKey device",
|
|
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
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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:
|
|
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
|
|
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
|
|
370
|
-
#
|
|
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,18 +10,21 @@ import importlib.resources
|
|
|
10
10
|
import logging
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
|
|
13
|
-
from keylet.tkey import Cmd, LenIdx, Rsp, TKey,
|
|
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
|
|
21
|
-
# First binary in
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
# Static registry of signer binaries (filename, version)
|
|
21
|
+
# First binary in each list is the default binary.
|
|
22
|
+
_EMBEDDED_MLDSA_BINS: list[tuple[str, int]] = [
|
|
23
|
+
("pqsigner_v3.bin", 3),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
_EMBEDDED_ED25519_BINS: list[tuple[str, int]] = [
|
|
27
|
+
("ed25519signer_v3.bin", 3),
|
|
25
28
|
]
|
|
26
29
|
|
|
27
30
|
|
|
@@ -32,17 +35,16 @@ class SignApp:
|
|
|
32
35
|
Attributes:
|
|
33
36
|
binary: The raw bytes of the device application binary.
|
|
34
37
|
version: The version number of the device application.
|
|
35
|
-
name:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
key_size: The size of the public key in bytes (defaults to 1312).
|
|
38
|
+
name: Device application name tuple.
|
|
39
|
+
sig_size: The size of the generated signature in bytes.
|
|
40
|
+
key_size: The size of the public key in bytes.
|
|
39
41
|
"""
|
|
40
42
|
|
|
41
43
|
binary: bytes
|
|
42
44
|
version: int
|
|
43
|
-
name: tuple[str, str]
|
|
44
|
-
sig_size: int
|
|
45
|
-
key_size: int
|
|
45
|
+
name: tuple[str, str]
|
|
46
|
+
sig_size: int
|
|
47
|
+
key_size: int
|
|
46
48
|
|
|
47
49
|
@property
|
|
48
50
|
def digest(self) -> str:
|
|
@@ -50,34 +52,14 @@ class SignApp:
|
|
|
50
52
|
return hashlib.blake2s(self.binary, digest_size=32).hexdigest()
|
|
51
53
|
|
|
52
54
|
@classmethod
|
|
53
|
-
def
|
|
54
|
-
cls, version: int | None
|
|
55
|
-
) ->
|
|
56
|
-
"""Load a ML-DSA signer application from package resources.
|
|
57
|
-
|
|
58
|
-
If a digest (or prefix) is provided, it returns the binary matching the
|
|
59
|
-
digest. If a version is provided, it filters by version. If neither is
|
|
60
|
-
provided, current default binary is loaded.
|
|
61
|
-
|
|
62
|
-
TKey key derivation depends on the application binary, so users who want a
|
|
63
|
-
specific key must provide the binary digest.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
version: The version of the signer application to load.
|
|
67
|
-
digest: A BLAKE2s-256 hex digest (or prefix) of the target binary.
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
An instance of SignApp configured with the loaded binary.
|
|
71
|
-
|
|
72
|
-
Raises:
|
|
73
|
-
ValueError: If no binary matches the criteria, or if the search
|
|
74
|
-
is ambiguous (matches multiple binaries).
|
|
75
|
-
"""
|
|
55
|
+
def _find_binary(
|
|
56
|
+
cls, version: int | None, digest: str | None, bins: list[tuple[str, int]]
|
|
57
|
+
) -> tuple[bytes, int]:
|
|
76
58
|
resources_dir = importlib.resources.files("keylet.resources")
|
|
77
59
|
matches = []
|
|
78
60
|
|
|
79
61
|
# Scan registered binaries
|
|
80
|
-
for filename, file_ver
|
|
62
|
+
for filename, file_ver in bins:
|
|
81
63
|
# Filter by version if requested
|
|
82
64
|
if version is not None and file_ver != version:
|
|
83
65
|
continue
|
|
@@ -89,7 +71,7 @@ class SignApp:
|
|
|
89
71
|
if digest is not None and not file_digest.startswith(digest.lower()):
|
|
90
72
|
continue
|
|
91
73
|
|
|
92
|
-
matches.append((binary, file_ver
|
|
74
|
+
matches.append((binary, file_ver))
|
|
93
75
|
|
|
94
76
|
if digest is None and version is None:
|
|
95
77
|
# First binary is the default one
|
|
@@ -106,8 +88,65 @@ class SignApp:
|
|
|
106
88
|
f"digest={digest}."
|
|
107
89
|
)
|
|
108
90
|
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
return matches[0]
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def load_mldsa(
|
|
95
|
+
cls, version: int | None = None, digest: str | None = None
|
|
96
|
+
) -> SignApp:
|
|
97
|
+
"""Load a ML-DSA signer application from package resources.
|
|
98
|
+
|
|
99
|
+
If a digest (or prefix) is provided, it returns the binary matching the
|
|
100
|
+
digest. If a version is provided, it filters by version. If neither is
|
|
101
|
+
provided, current default binary is loaded.
|
|
102
|
+
|
|
103
|
+
TKey key derivation depends on the application binary, so users who want a
|
|
104
|
+
specific key must provide the binary digest.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
version: The version of the signer application to load.
|
|
108
|
+
digest: A BLAKE2s-256 hex digest (or prefix) of the target binary.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
An instance of SignApp configured with the loaded binary.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValueError: If no binary matches the criteria, or if the search
|
|
115
|
+
is ambiguous (matches multiple binaries).
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
binary, version = cls._find_binary(version, digest, _EMBEDDED_MLDSA_BINS)
|
|
119
|
+
return cls(binary, version, ("tk1", "pqsn"), 2420, 1312)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def load_ed25519(
|
|
123
|
+
cls, version: int | None = None, digest: str | None = None
|
|
124
|
+
) -> SignApp:
|
|
125
|
+
"""Load a Ed25519 signer application from package resources.
|
|
126
|
+
|
|
127
|
+
If a digest (or prefix) is provided, it returns the binary matching the
|
|
128
|
+
digest. If a version is provided, it filters by version. If neither is
|
|
129
|
+
provided, current default binary is loaded.
|
|
130
|
+
|
|
131
|
+
TKey key derivation depends on the application binary, so users who want a
|
|
132
|
+
specific key must provide the binary digest.
|
|
133
|
+
|
|
134
|
+
Warning:
|
|
135
|
+
When Ed25519 is used, there is a 4096K size limit to signing payloads.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
version: The version of the signer application to load.
|
|
139
|
+
digest: A BLAKE2s-256 hex digest (or prefix) of the target binary.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
An instance of SignApp configured with the loaded binary.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ValueError: If no binary matches the criteria, or if the search
|
|
146
|
+
is ambiguous (matches multiple binaries).
|
|
147
|
+
"""
|
|
148
|
+
binary, version = cls._find_binary(version, digest, _EMBEDDED_ED25519_BINS)
|
|
149
|
+
return cls(binary, version, ("tk1", "sign"), 64, 32)
|
|
111
150
|
|
|
112
151
|
|
|
113
152
|
class SignRsp:
|
|
@@ -135,9 +174,10 @@ class SignCmd:
|
|
|
135
174
|
class TKeySign(TKey):
|
|
136
175
|
"""Client for communicating with the TKey signer application.
|
|
137
176
|
|
|
138
|
-
This class
|
|
139
|
-
|
|
140
|
-
|
|
177
|
+
This class implements public key retrieval and signing as defined in the
|
|
178
|
+
[tkey-pq-device-signer protocol](https://github.com/tillitis/tkey-pq-device-signer)
|
|
179
|
+
but is also compatible with the
|
|
180
|
+
[tkey-device-signer protocol](https://github.com/tillitis/tkey-device-signer).
|
|
141
181
|
"""
|
|
142
182
|
|
|
143
183
|
def __init__(
|
|
@@ -161,13 +201,14 @@ class TKeySign(TKey):
|
|
|
161
201
|
|
|
162
202
|
Raises:
|
|
163
203
|
TKeyNotFoundError: If the TKey device cannot be found.
|
|
164
|
-
|
|
204
|
+
TKeyUnexpectedAppError: If loading the application fails or the device is
|
|
165
205
|
running a mismatched application.
|
|
166
206
|
TKeyError: For other connection or initialization failures.
|
|
167
207
|
"""
|
|
168
208
|
super().__init__(device)
|
|
169
209
|
self.key_size = app.key_size
|
|
170
210
|
self.sig_size = app.sig_size
|
|
211
|
+
self.name = app.name
|
|
171
212
|
|
|
172
213
|
try:
|
|
173
214
|
if not self.load_app(app.binary, secret):
|
|
@@ -181,7 +222,7 @@ class TKeySign(TKey):
|
|
|
181
222
|
if name == app.name and ver == app.version:
|
|
182
223
|
return # Signer application is already loaded
|
|
183
224
|
|
|
184
|
-
raise
|
|
225
|
+
raise TKeyUnexpectedAppError(
|
|
185
226
|
f"TKey is running an unknown application {name, ver}, "
|
|
186
227
|
f"expected {app.name, app.version}"
|
|
187
228
|
)
|
|
@@ -215,19 +256,23 @@ class TKeySign(TKey):
|
|
|
215
256
|
return bytes(pubkey)
|
|
216
257
|
|
|
217
258
|
def sign(self, message: bytes, pub_key: bytes | None = None) -> bytes:
|
|
218
|
-
"""Sign a
|
|
259
|
+
"""Sign a payload.
|
|
219
260
|
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
Sends payload to device and retrieves the signature.
|
|
262
|
+
|
|
263
|
+
For ML-DSA, the FIPS 204 external mu is computed using the message
|
|
264
|
+
and public key: the mu is sent to device instead of payload.
|
|
222
265
|
|
|
223
266
|
Note:
|
|
224
267
|
This method blocks and waits (up to 60 seconds) for the user to touch
|
|
225
268
|
the physical TKey device when it flashes.
|
|
226
269
|
|
|
227
270
|
Args:
|
|
228
|
-
message: The raw bytes of the message/payload to sign.
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
274
|
+
pub_key: The public key bytes (only needed for ML-DSA). If not provided,
|
|
275
|
+
key is retrieved from device.
|
|
231
276
|
|
|
232
277
|
Returns:
|
|
233
278
|
The generated signature as raw bytes.
|
|
@@ -237,18 +282,30 @@ class TKeySign(TKey):
|
|
|
237
282
|
TKeyIOError: If writing or reading from the serial port fails.
|
|
238
283
|
TKeyProtocolError: If there is a framing or protocol mismatch.
|
|
239
284
|
"""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
#
|
|
251
|
-
|
|
285
|
+
# pqsn = ML-DSA signer, pqnt = no-touch ML-DSA test signer
|
|
286
|
+
if self.name in [("tk1", "pqsn"), ("tk1", "pqnt")]:
|
|
287
|
+
# Compute FIPS 204 external mu
|
|
288
|
+
if pub_key is None:
|
|
289
|
+
pub_key = self.get_pubkey()
|
|
290
|
+
tr = hashlib.shake_256(pub_key).digest(64)
|
|
291
|
+
payload = hashlib.shake_256(tr + b"\x00\x00" + message).digest(64)
|
|
292
|
+
else:
|
|
293
|
+
payload = message
|
|
294
|
+
|
|
295
|
+
# Set size
|
|
296
|
+
if len(payload) > 4096:
|
|
297
|
+
raise ValueError(f"Payload too large {len(payload)} > 4096]")
|
|
298
|
+
self.send(SignCmd.SET_SIZE, len(payload).to_bytes(4, byteorder="little"))
|
|
299
|
+
|
|
300
|
+
# Load data in chunks
|
|
301
|
+
chunk_size = 127
|
|
302
|
+
offset = 0
|
|
303
|
+
while offset < len(payload):
|
|
304
|
+
chunk = payload[offset : offset + chunk_size]
|
|
305
|
+
rx = self.send(SignCmd.LOAD_DATA, chunk)
|
|
306
|
+
if rx[2] != 0:
|
|
307
|
+
raise TKeyError(f"LoadData chunk NOK status: {rx[2]}")
|
|
308
|
+
offset += chunk_size
|
|
252
309
|
|
|
253
310
|
# Trigger signing (blocks waiting for touch) and read first frame
|
|
254
311
|
rx = self.send(SignCmd.GET_SIG, timeout=60)
|
keylet-0.1.0/PKG-INFO
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.3
|
|
2
|
-
Name: keylet
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Client application Tillitis TKey ML-DSA signer
|
|
5
|
-
Author: Jussi Kukkonen
|
|
6
|
-
Author-email: Jussi Kukkonen <jkukkonen@google.com>
|
|
7
|
-
Requires-Dist: cryptography>=48.0.0
|
|
8
|
-
Requires-Dist: pyserial>=3.5
|
|
9
|
-
Requires-Python: >=3.10
|
|
10
|
-
Description-Content-Type: text/markdown
|
|
11
|
-
|
|
12
|
-
## Keylet -- Client library for Tillitis TKey
|
|
13
|
-
|
|
14
|
-
This library provides a client implementation for the Tillitis TKey, and specifically
|
|
15
|
-
the [tkey-pq-device-signer](https://github.com/tillitis/tkey-pq-device-signer) application.
|
keylet-0.1.0/README.md
DELETED
keylet-0.1.0/pyproject.toml
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "keylet"
|
|
3
|
-
version = "0.1.0"
|
|
4
|
-
description = "Client application Tillitis TKey ML-DSA signer"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
authors = [
|
|
7
|
-
{ name = "Jussi Kukkonen", email = "jkukkonen@google.com" }
|
|
8
|
-
]
|
|
9
|
-
requires-python = ">=3.10"
|
|
10
|
-
dependencies = [
|
|
11
|
-
"cryptography>=48.0.0",
|
|
12
|
-
"pyserial>=3.5",
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
[project.scripts]
|
|
16
|
-
keylet = "keylet.bin.cli:main"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[dependency-groups]
|
|
20
|
-
dev = [
|
|
21
|
-
"mypy>=1.10.0",
|
|
22
|
-
"pytest>=8.0.0",
|
|
23
|
-
"pytest-cov>=4.1.0",
|
|
24
|
-
"ruff>=0.3.0",
|
|
25
|
-
"types-pyserial>=3.5.0.20240310",
|
|
26
|
-
"zizmor>=0.8.0",
|
|
27
|
-
"zensical>=0.0.46",
|
|
28
|
-
"mkdocstrings[python]>=1.0.4",
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
[build-system]
|
|
32
|
-
requires = ["uv_build>=0.11.23,<0.12.0"]
|
|
33
|
-
build-backend = "uv_build"
|
|
34
|
-
|
|
35
|
-
[tool.ruff]
|
|
36
|
-
line-length = 88
|
|
37
|
-
target-version = "py310"
|
|
38
|
-
|
|
39
|
-
[tool.ruff.lint]
|
|
40
|
-
select = [
|
|
41
|
-
"E", # pycodestyle errors
|
|
42
|
-
"W", # pycodestyle warnings
|
|
43
|
-
"F", # pyflakes
|
|
44
|
-
"I", # isort
|
|
45
|
-
"B", # flake8-bugbear
|
|
46
|
-
"C4", # flake8-comprehensions
|
|
47
|
-
"UP", # pyupgrade
|
|
48
|
-
"RUF", # Ruff-specific rules
|
|
49
|
-
]
|
|
50
|
-
ignore = []
|
|
51
|
-
|
|
52
|
-
[tool.mypy]
|
|
53
|
-
python_version = "3.10"
|
|
54
|
-
strict = true
|
|
55
|
-
warn_unreachable = true
|
|
56
|
-
|
|
57
|
-
[[tool.mypy.overrides]]
|
|
58
|
-
module = "serial.*"
|
|
59
|
-
ignore_missing_imports = true
|
|
60
|
-
|
|
61
|
-
[tool.pytest.ini_options]
|
|
62
|
-
markers = [
|
|
63
|
-
"device: tests requiring a physical TKey device running a no-touch binary",
|
|
64
|
-
]
|
|
@@ -1,139 +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.mldsa import MLDSA44PublicKey
|
|
10
|
-
|
|
11
|
-
from keylet.tkey import TKeyNotFoundError
|
|
12
|
-
from keylet.tkey_sign import SignApp, TKeySign
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def get_signer(device: str | None, passphrase: str | None) -> TKeySign:
|
|
16
|
-
"""Helper to initialize TKeySign with the default device signer binary."""
|
|
17
|
-
app = SignApp.load_mldsa()
|
|
18
|
-
return TKeySign(app, device=device, secret=passphrase)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def cmd_pubkey(args: argparse.Namespace) -> int:
|
|
22
|
-
try:
|
|
23
|
-
with get_signer(args.device, args.passphrase) as signer:
|
|
24
|
-
pubkey = signer.get_pubkey()
|
|
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
|
-
with get_signer(args.device, args.passphrase) as signer:
|
|
45
|
-
print("Please touch the TKey device when it flashes to sign...")
|
|
46
|
-
signature = signer.sign(data)
|
|
47
|
-
|
|
48
|
-
sig_path = file_path.with_suffix(file_path.suffix + ".signature")
|
|
49
|
-
sig_path.write_bytes(signature)
|
|
50
|
-
print(f"Signature written to {sig_path}")
|
|
51
|
-
return 0
|
|
52
|
-
except TKeyNotFoundError as e:
|
|
53
|
-
print(f"Signing failed: {e}", file=sys.stderr)
|
|
54
|
-
return 1
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def cmd_verify(args: argparse.Namespace) -> int:
|
|
58
|
-
file_path = Path(args.file)
|
|
59
|
-
if not file_path.exists():
|
|
60
|
-
print(f"Error: File {args.file} does not exist", file=sys.stderr)
|
|
61
|
-
return 1
|
|
62
|
-
|
|
63
|
-
sig_path = (
|
|
64
|
-
Path(args.signature)
|
|
65
|
-
if args.signature
|
|
66
|
-
else file_path.with_suffix(file_path.suffix + ".signature")
|
|
67
|
-
)
|
|
68
|
-
if not sig_path.exists():
|
|
69
|
-
print(f"Error: Signature file {sig_path} does not exist", file=sys.stderr)
|
|
70
|
-
return 1
|
|
71
|
-
|
|
72
|
-
try:
|
|
73
|
-
file_bytes = file_path.read_bytes()
|
|
74
|
-
sig_bytes = sig_path.read_bytes()
|
|
75
|
-
|
|
76
|
-
# Get public key either from file or from device
|
|
77
|
-
if args.pubkey:
|
|
78
|
-
pubkey_bytes = Path(args.pubkey).read_bytes()
|
|
79
|
-
else:
|
|
80
|
-
print("Retrieving public key from device...")
|
|
81
|
-
with get_signer(args.device, args.passphrase) as signer:
|
|
82
|
-
pubkey_bytes = signer.get_pubkey()
|
|
83
|
-
|
|
84
|
-
# Verify signature using cryptography library
|
|
85
|
-
pubkey = MLDSA44PublicKey.from_public_bytes(pubkey_bytes)
|
|
86
|
-
pubkey.verify(sig_bytes, file_bytes)
|
|
87
|
-
print("Verification successful!")
|
|
88
|
-
return 0
|
|
89
|
-
except InvalidSignature:
|
|
90
|
-
print("Verification failed: Invalid signature", file=sys.stderr)
|
|
91
|
-
return 1
|
|
92
|
-
except TKeyNotFoundError as e:
|
|
93
|
-
print(f"Verification failed: {e}", file=sys.stderr)
|
|
94
|
-
return 1
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def main() -> None:
|
|
98
|
-
parser = argparse.ArgumentParser(
|
|
99
|
-
description="Tillitis TKey Keylet CLI testing tool"
|
|
100
|
-
)
|
|
101
|
-
parser.add_argument(
|
|
102
|
-
"--device", help="Serial port of the TKey device (e.g. /dev/ttyACM0)"
|
|
103
|
-
)
|
|
104
|
-
parser.add_argument("--passphrase", help="User Supplied Secret (passphrase)")
|
|
105
|
-
|
|
106
|
-
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
107
|
-
|
|
108
|
-
# pubkey command
|
|
109
|
-
parser_pubkey = subparsers.add_parser("pubkey", help="Get public key from device")
|
|
110
|
-
parser_pubkey.add_argument(
|
|
111
|
-
"-o", "--output", help="Output file to write public key to"
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
# sign command
|
|
115
|
-
parser_sign = subparsers.add_parser("sign", help="Sign a file")
|
|
116
|
-
parser_sign.add_argument("file", help="File to sign")
|
|
117
|
-
|
|
118
|
-
# verify command
|
|
119
|
-
parser_verify = subparsers.add_parser("verify", help="Verify a signature")
|
|
120
|
-
parser_verify.add_argument("file", help="File that was signed")
|
|
121
|
-
parser_verify.add_argument(
|
|
122
|
-
"--signature", help="Signature file (defaults to <FILE>.signature)"
|
|
123
|
-
)
|
|
124
|
-
parser_verify.add_argument(
|
|
125
|
-
"--pubkey", help="Public key file (retrieved from device if not specified)"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
args = parser.parse_args()
|
|
129
|
-
|
|
130
|
-
if args.command == "pubkey":
|
|
131
|
-
sys.exit(cmd_pubkey(args))
|
|
132
|
-
elif args.command == "sign":
|
|
133
|
-
sys.exit(cmd_sign(args))
|
|
134
|
-
elif args.command == "verify":
|
|
135
|
-
sys.exit(cmd_verify(args))
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if __name__ == "__main__":
|
|
139
|
-
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|