keylet 0.1.0__tar.gz → 0.2.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.2.0/PKG-INFO +76 -0
- keylet-0.2.0/README.md +62 -0
- keylet-0.2.0/pyproject.toml +85 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/bin/cli.py +33 -14
- keylet-0.2.0/src/keylet/resources/ed25519signer_v3.bin +0 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/tkey_sign.py +112 -59
- 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 → keylet-0.2.0}/src/keylet/__init__.py +0 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/_serial_hack.py +0 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/py.typed +0 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/resources/__init__.py +0 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/resources/pqsigner_v3.bin +0 -0
- {keylet-0.1.0 → keylet-0.2.0}/src/keylet/tkey.py +0 -0
keylet-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: keylet
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Client application for Tillitis TKey hardware 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
|
+
Project-URL: Issues, https://github.com/jku/keylet/issues
|
|
11
|
+
Project-URL: Source, https://github.com/jku/keylet
|
|
12
|
+
Project-URL: Documentation, https://jku.github.io/keylet/
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# keylet -- Client library for Tillitis TKey
|
|
16
|
+
|
|
17
|
+
[](https://github.com/jku/keylet) [](https://pypi.org/project/keylet/) [](https://jku.github.io/keylet/)
|
|
18
|
+
|
|
19
|
+
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
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install keylet
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## CLI Usage
|
|
28
|
+
|
|
29
|
+
The package installs a `keylet` command-line tool forsigning and verification. This is primarily a test/demo application for the library.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Sign without a passphrase, then verify
|
|
33
|
+
$ keylet sign README.md
|
|
34
|
+
$ keylet verify README.md
|
|
35
|
+
|
|
36
|
+
# Get public key, sign with a passphrase, and verify using the saved public key
|
|
37
|
+
$ keylet --passphrase hunter2 pubkey --output pub.key
|
|
38
|
+
$ keylet --passphrase hunter2 sign README.md
|
|
39
|
+
$ keylet verify --pubkey pub.key README.md
|
|
40
|
+
|
|
41
|
+
# When using keylet long-term, remember to specify device app digest (keylet default
|
|
42
|
+
# app version may change, but you will need a specific application to keep using the
|
|
43
|
+
# same key)
|
|
44
|
+
$ keylet --passphrase hunter2 --digest 186bcf6 sign README.md
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Library Usage
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from keylet import TKeySign, SignApp
|
|
52
|
+
|
|
53
|
+
# Load the default embedded ML-DSA signer
|
|
54
|
+
app = SignApp.load_mldsa()
|
|
55
|
+
digest = app.digest
|
|
56
|
+
|
|
57
|
+
# Initialize the signer with a passphrase
|
|
58
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
59
|
+
# Sign a payload
|
|
60
|
+
signature = signer.sign(b"my payload")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
In long-term use, the device app digest should be used to ensure the same application
|
|
64
|
+
is always used for a specific key:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# Load application with a digest stored earlier
|
|
68
|
+
app = SignApp.load_mldsa(digest=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
|
+
See the [API Reference](https://jku.github.io/keylet/api/) for more details.
|
keylet-0.2.0/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install keylet
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## CLI Usage
|
|
14
|
+
|
|
15
|
+
The package installs a `keylet` command-line tool forsigning and verification. This is primarily a test/demo application for the library.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Sign without a passphrase, then verify
|
|
19
|
+
$ keylet sign README.md
|
|
20
|
+
$ keylet verify README.md
|
|
21
|
+
|
|
22
|
+
# Get public key, sign with a passphrase, and verify using the saved public key
|
|
23
|
+
$ keylet --passphrase hunter2 pubkey --output pub.key
|
|
24
|
+
$ keylet --passphrase hunter2 sign README.md
|
|
25
|
+
$ keylet verify --pubkey pub.key README.md
|
|
26
|
+
|
|
27
|
+
# When using keylet long-term, remember to specify device app digest (keylet default
|
|
28
|
+
# app version may change, but you will need a specific application to keep using the
|
|
29
|
+
# same key)
|
|
30
|
+
$ keylet --passphrase hunter2 --digest 186bcf6 sign README.md
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Library Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from keylet import TKeySign, SignApp
|
|
38
|
+
|
|
39
|
+
# Load the default embedded ML-DSA signer
|
|
40
|
+
app = SignApp.load_mldsa()
|
|
41
|
+
digest = app.digest
|
|
42
|
+
|
|
43
|
+
# Initialize the signer with a passphrase
|
|
44
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
45
|
+
# Sign a payload
|
|
46
|
+
signature = signer.sign(b"my payload")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
In long-term use, the device app digest should be used to ensure the same application
|
|
50
|
+
is always used for a specific key:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# Load application with a digest stored earlier
|
|
54
|
+
app = SignApp.load_mldsa(digest=digest)
|
|
55
|
+
|
|
56
|
+
# Initialize the signer with a passphrase
|
|
57
|
+
with TKeySign(app=app, secret="hunter2") as signer:
|
|
58
|
+
# Sign a payload
|
|
59
|
+
signature = signer.sign(b"my payload")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See the [API Reference](https://jku.github.io/keylet/api/) for more details.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "keylet"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Client application for Tillitis TKey hardware 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
|
+
[project.urls]
|
|
19
|
+
Issues = "https://github.com/jku/keylet/issues"
|
|
20
|
+
Source = "https://github.com/jku/keylet"
|
|
21
|
+
Documentation = "https://jku.github.io/keylet/"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"mypy",
|
|
26
|
+
"pytest",
|
|
27
|
+
"ruff",
|
|
28
|
+
"zizmor",
|
|
29
|
+
"zensical",
|
|
30
|
+
"mkdocstrings[python]",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.11.23,<0.12.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 88
|
|
39
|
+
|
|
40
|
+
[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
|
|
58
|
+
]
|
|
59
|
+
ignore = []
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.per-file-ignores]
|
|
62
|
+
"src/keylet/bin/cli.py" = ["T201"]
|
|
63
|
+
"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
|
|
68
|
+
"ARG002", # Unused method argument (common with mocks)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
python_version = "3.10"
|
|
75
|
+
strict = true
|
|
76
|
+
warn_unreachable = true
|
|
77
|
+
|
|
78
|
+
[[tool.mypy.overrides]]
|
|
79
|
+
module = "serial.*"
|
|
80
|
+
ignore_missing_imports = true
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
markers = [
|
|
84
|
+
"device: tests that require a physical TKey device",
|
|
85
|
+
]
|
|
@@ -6,22 +6,22 @@ import sys
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
8
|
from cryptography.exceptions import InvalidSignature
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
9
10
|
from cryptography.hazmat.primitives.asymmetric.mldsa import MLDSA44PublicKey
|
|
10
11
|
|
|
11
12
|
from keylet.tkey import TKeyNotFoundError
|
|
12
13
|
from keylet.tkey_sign import SignApp, TKeySign
|
|
13
14
|
|
|
14
15
|
|
|
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
16
|
def cmd_pubkey(args: argparse.Namespace) -> int:
|
|
22
17
|
try:
|
|
23
|
-
|
|
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:
|
|
24
23
|
pubkey = signer.get_pubkey()
|
|
24
|
+
print(f"Using device app with digest {app.digest[:7]}.")
|
|
25
25
|
if args.output:
|
|
26
26
|
Path(args.output).write_bytes(pubkey)
|
|
27
27
|
print(f"Public key written to {args.output}")
|
|
@@ -41,7 +41,12 @@ def cmd_sign(args: argparse.Namespace) -> int:
|
|
|
41
41
|
|
|
42
42
|
try:
|
|
43
43
|
data = file_path.read_bytes()
|
|
44
|
-
|
|
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]}.")
|
|
45
50
|
print("Please touch the TKey device when it flashes to sign...")
|
|
46
51
|
signature = signer.sign(data)
|
|
47
52
|
|
|
@@ -77,13 +82,22 @@ def cmd_verify(args: argparse.Namespace) -> int:
|
|
|
77
82
|
if args.pubkey:
|
|
78
83
|
pubkey_bytes = Path(args.pubkey).read_bytes()
|
|
79
84
|
else:
|
|
80
|
-
|
|
81
|
-
|
|
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...")
|
|
82
92
|
pubkey_bytes = signer.get_pubkey()
|
|
83
93
|
|
|
84
94
|
# Verify signature using cryptography library
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
87
101
|
print("Verification successful!")
|
|
88
102
|
return 0
|
|
89
103
|
except InvalidSignature:
|
|
@@ -98,10 +112,15 @@ def main() -> None:
|
|
|
98
112
|
parser = argparse.ArgumentParser(
|
|
99
113
|
description="Tillitis TKey Keylet CLI testing tool"
|
|
100
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)")
|
|
101
117
|
parser.add_argument(
|
|
102
|
-
"
|
|
118
|
+
"-t",
|
|
119
|
+
"--type",
|
|
120
|
+
choices=["ml-dsa", "ed25519"],
|
|
121
|
+
default="ml-dsa",
|
|
122
|
+
help="Signer type (default: %(default)s)",
|
|
103
123
|
)
|
|
104
|
-
parser.add_argument("--passphrase", help="User Supplied Secret (passphrase)")
|
|
105
124
|
|
|
106
125
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
107
126
|
|
|
Binary file
|
|
@@ -17,11 +17,15 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
MU_SIZE = (64).to_bytes(4, byteorder="little")
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
# Static registry of
|
|
21
|
-
# First binary in
|
|
22
|
-
# Format: (filename, version
|
|
23
|
-
_EMBEDDED_MLDSA_BINS: list[tuple[str, int
|
|
24
|
-
("pqsigner_v3.bin", 3
|
|
20
|
+
# Static registry of signer binaries
|
|
21
|
+
# First binary in each list is the default binary.
|
|
22
|
+
# Format: (filename, version)
|
|
23
|
+
_EMBEDDED_MLDSA_BINS: list[tuple[str, int]] = [
|
|
24
|
+
("pqsigner_v3.bin", 3),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
_EMBEDDED_ED25519_BINS: list[tuple[str, int]] = [
|
|
28
|
+
("ed25519signer_v3.bin", 3),
|
|
25
29
|
]
|
|
26
30
|
|
|
27
31
|
|
|
@@ -32,17 +36,16 @@ class SignApp:
|
|
|
32
36
|
Attributes:
|
|
33
37
|
binary: The raw bytes of the device application binary.
|
|
34
38
|
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).
|
|
39
|
+
name: Device application name tuple.
|
|
40
|
+
sig_size: The size of the generated signature in bytes.
|
|
41
|
+
key_size: The size of the public key in bytes.
|
|
39
42
|
"""
|
|
40
43
|
|
|
41
44
|
binary: bytes
|
|
42
45
|
version: int
|
|
43
|
-
name: tuple[str, str]
|
|
44
|
-
sig_size: int
|
|
45
|
-
key_size: int
|
|
46
|
+
name: tuple[str, str]
|
|
47
|
+
sig_size: int
|
|
48
|
+
key_size: int
|
|
46
49
|
|
|
47
50
|
@property
|
|
48
51
|
def digest(self) -> str:
|
|
@@ -50,34 +53,14 @@ class SignApp:
|
|
|
50
53
|
return hashlib.blake2s(self.binary, digest_size=32).hexdigest()
|
|
51
54
|
|
|
52
55
|
@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
|
-
"""
|
|
56
|
+
def _find_binary(
|
|
57
|
+
cls, version: int | None, digest: str | None, bins: list[tuple[str, int]]
|
|
58
|
+
) -> tuple[bytes, int]:
|
|
76
59
|
resources_dir = importlib.resources.files("keylet.resources")
|
|
77
60
|
matches = []
|
|
78
61
|
|
|
79
62
|
# Scan registered binaries
|
|
80
|
-
for filename, file_ver
|
|
63
|
+
for filename, file_ver in bins:
|
|
81
64
|
# Filter by version if requested
|
|
82
65
|
if version is not None and file_ver != version:
|
|
83
66
|
continue
|
|
@@ -89,7 +72,7 @@ class SignApp:
|
|
|
89
72
|
if digest is not None and not file_digest.startswith(digest.lower()):
|
|
90
73
|
continue
|
|
91
74
|
|
|
92
|
-
matches.append((binary, file_ver
|
|
75
|
+
matches.append((binary, file_ver))
|
|
93
76
|
|
|
94
77
|
if digest is None and version is None:
|
|
95
78
|
# First binary is the default one
|
|
@@ -106,8 +89,62 @@ class SignApp:
|
|
|
106
89
|
f"digest={digest}."
|
|
107
90
|
)
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
|
|
92
|
+
return matches[0]
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def load_mldsa(
|
|
96
|
+
cls, version: int | None = None, digest: str | None = None
|
|
97
|
+
) -> SignApp:
|
|
98
|
+
"""Load a ML-DSA signer application from package resources.
|
|
99
|
+
|
|
100
|
+
If a digest (or prefix) is provided, it returns the binary matching the
|
|
101
|
+
digest. If a version is provided, it filters by version. If neither is
|
|
102
|
+
provided, current default binary is loaded.
|
|
103
|
+
|
|
104
|
+
TKey key derivation depends on the application binary, so users who want a
|
|
105
|
+
specific key must provide the binary digest.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
version: The version of the signer application to load.
|
|
109
|
+
digest: A BLAKE2s-256 hex digest (or prefix) of the target binary.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
An instance of SignApp configured with the loaded binary.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ValueError: If no binary matches the criteria, or if the search
|
|
116
|
+
is ambiguous (matches multiple binaries).
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
binary, version = cls._find_binary(version, digest, _EMBEDDED_MLDSA_BINS)
|
|
120
|
+
return cls(binary, version, ("tk1", "pqsn"), 2420, 1312)
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def load_ed25519(
|
|
124
|
+
cls, version: int | None = None, digest: str | None = None
|
|
125
|
+
) -> SignApp:
|
|
126
|
+
"""Load a Ed25519 signer application from package resources.
|
|
127
|
+
|
|
128
|
+
If a digest (or prefix) is provided, it returns the binary matching the
|
|
129
|
+
digest. If a version is provided, it filters by version. If neither is
|
|
130
|
+
provided, current default binary is loaded.
|
|
131
|
+
|
|
132
|
+
TKey key derivation depends on the application binary, so users who want a
|
|
133
|
+
specific key must provide the binary digest.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
version: The version of the signer application to load.
|
|
137
|
+
digest: A BLAKE2s-256 hex digest (or prefix) of the target binary.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
An instance of SignApp configured with the loaded binary.
|
|
141
|
+
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If no binary matches the criteria, or if the search
|
|
144
|
+
is ambiguous (matches multiple binaries).
|
|
145
|
+
"""
|
|
146
|
+
binary, version = cls._find_binary(version, digest, _EMBEDDED_ED25519_BINS)
|
|
147
|
+
return cls(binary, version, ("tk1", "sign"), 64, 32)
|
|
111
148
|
|
|
112
149
|
|
|
113
150
|
class SignRsp:
|
|
@@ -135,9 +172,10 @@ class SignCmd:
|
|
|
135
172
|
class TKeySign(TKey):
|
|
136
173
|
"""Client for communicating with the TKey signer application.
|
|
137
174
|
|
|
138
|
-
This class
|
|
139
|
-
|
|
140
|
-
|
|
175
|
+
This class implements public key retrieval and signing as defined in the
|
|
176
|
+
[tkey-pq-device-signer protocol](https://github.com/tillitis/tkey-pq-device-signer)
|
|
177
|
+
but is also compatible with the
|
|
178
|
+
[tkey-device-signer protocol](https://github.com/tillitis/tkey-device-signer).
|
|
141
179
|
"""
|
|
142
180
|
|
|
143
181
|
def __init__(
|
|
@@ -168,6 +206,7 @@ class TKeySign(TKey):
|
|
|
168
206
|
super().__init__(device)
|
|
169
207
|
self.key_size = app.key_size
|
|
170
208
|
self.sig_size = app.sig_size
|
|
209
|
+
self.name = app.name
|
|
171
210
|
|
|
172
211
|
try:
|
|
173
212
|
if not self.load_app(app.binary, secret):
|
|
@@ -215,10 +254,12 @@ class TKeySign(TKey):
|
|
|
215
254
|
return bytes(pubkey)
|
|
216
255
|
|
|
217
256
|
def sign(self, message: bytes, pub_key: bytes | None = None) -> bytes:
|
|
218
|
-
"""Sign a
|
|
257
|
+
"""Sign a payload.
|
|
219
258
|
|
|
220
|
-
|
|
221
|
-
|
|
259
|
+
Sends payload to device and retrieves the signature.
|
|
260
|
+
|
|
261
|
+
For ML-DSA, the FIPS 204 external mu is computed using the message
|
|
262
|
+
and public key: the mu is sent to device instead of payload.
|
|
222
263
|
|
|
223
264
|
Note:
|
|
224
265
|
This method blocks and waits (up to 60 seconds) for the user to touch
|
|
@@ -226,8 +267,8 @@ class TKeySign(TKey):
|
|
|
226
267
|
|
|
227
268
|
Args:
|
|
228
269
|
message: The raw bytes of the message/payload to sign.
|
|
229
|
-
pub_key: The public key bytes
|
|
230
|
-
|
|
270
|
+
pub_key: The public key bytes (only needed for ML-DSA). If not provided,
|
|
271
|
+
key is retrieved from device.
|
|
231
272
|
|
|
232
273
|
Returns:
|
|
233
274
|
The generated signature as raw bytes.
|
|
@@ -237,18 +278,30 @@ class TKeySign(TKey):
|
|
|
237
278
|
TKeyIOError: If writing or reading from the serial port fails.
|
|
238
279
|
TKeyProtocolError: If there is a framing or protocol mismatch.
|
|
239
280
|
"""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
#
|
|
251
|
-
|
|
281
|
+
# pqsn = ML-DSA signer, pqnt = no-touch ML-DSA test signer
|
|
282
|
+
if self.name in [("tk1", "pqsn"), ("tk1", "pqnt")]:
|
|
283
|
+
# Compute FIPS 204 external mu
|
|
284
|
+
if pub_key is None:
|
|
285
|
+
pub_key = self.get_pubkey()
|
|
286
|
+
tr = hashlib.shake_256(pub_key).digest(64)
|
|
287
|
+
payload = hashlib.shake_256(tr + b"\x00\x00" + message).digest(64)
|
|
288
|
+
else:
|
|
289
|
+
payload = message
|
|
290
|
+
|
|
291
|
+
# Set size
|
|
292
|
+
if len(payload) > 4096:
|
|
293
|
+
raise ValueError(f"Payload too large {len(payload)} > 4096]")
|
|
294
|
+
self.send(SignCmd.SET_SIZE, len(payload).to_bytes(4, byteorder="little"))
|
|
295
|
+
|
|
296
|
+
# Load data in chunks
|
|
297
|
+
chunk_size = 127
|
|
298
|
+
offset = 0
|
|
299
|
+
while offset < len(payload):
|
|
300
|
+
chunk = payload[offset : offset + chunk_size]
|
|
301
|
+
rx = self.send(SignCmd.LOAD_DATA, chunk)
|
|
302
|
+
if rx[2] != 0:
|
|
303
|
+
raise TKeyError(f"LoadData chunk NOK status: {rx[2]}")
|
|
304
|
+
offset += chunk_size
|
|
252
305
|
|
|
253
306
|
# Trigger signing (blocks waiting for touch) and read first frame
|
|
254
307
|
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
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|