Skip to content

Commit 4bbdd2e

Browse files
authored
Add support for verifying kernels (#635)
* Add support for verifying kernels This change adds support for verifying kernels. Verification consists of two parts: 1. Verify that `metadata.json` is correctly signed using the detached signature in `metadata.json.sigstore`; and if so, 2. verify that the kernel file hashes are the same as the digest in `metadata.json`. The digest in `metadata.json` protects the kernel files, the detached signature in `metadata.json.sigstore` protects the metadata. For signing, we use sigstore, so that kernels can be signed using ephemeral keys. Ephemeral keys reduce the attack surface of leaked keys. In this change, only the API is added. It is not exposed at the top-level, since it is not fully stable yet. It is also not wired up in `get_kernel`, etc. yet because we still want to test code signing for some time. See `test_verify.py` for actual use of the API. Besides that we'll add a `kernels` subcommand for verifying kernels in the PR after this one. * Add `Metadata.from_bytes`, use in signature verification * Check the digest algorithm in verification Also introduce the `DigestAlgorithm` type in Python, to strongly type the algorithm. * Fix RST -> HF doc builder markup in `kernels_data.pyi` * Do not assert on sigstore error messages They are not stable between releases
1 parent eb0863b commit 4bbdd2e

12 files changed

Lines changed: 1898 additions & 17 deletions

File tree

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@
174174
pytest
175175
pytest-benchmark
176176
pyyaml
177+
sigstore
177178
tabulate
178179
tomlkit
179180
torch

kernels-data/bindings/python/kernels_data.pyi

Lines changed: 144 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,18 @@ import os
44
from enum import Enum
55
from typing import Optional, final
66

7-
__all__ = ["Backend", "BackendInfo", "KernelName", "Metadata", "Version", "__version__"]
7+
__all__ = [
8+
"Backend",
9+
"BackendInfo",
10+
"DigestAlgorithm",
11+
"KernelName",
12+
"Metadata",
13+
"Digest",
14+
"DigestViolation",
15+
"DigestValidationError",
16+
"Version",
17+
"__version__",
18+
]
819

920
__version__: str
1021

@@ -25,8 +36,8 @@ class Backend(Enum):
2536
"""Parse a backend name.
2637
2738
Args:
28-
s: One of ``"cann"``, ``"cpu"``, ``"cuda"``, ``"metal"``,
29-
``"neuron"``, ``"rocm"``, ``"xpu"``.
39+
s: One of `"cann"`, `"cpu"`, `"cuda"`, `"metal"`,
40+
`"neuron"`, `"rocm"`, `"xpu"`.
3041
3142
Raises:
3243
ValueError: If the backend name is unknown.
@@ -54,14 +65,14 @@ class BackendInfo:
5465

5566
@final
5667
class Version:
57-
"""A dotted numeric version (e.g. ``12.8.0``).
68+
"""A dotted numeric version (e.g. `12.8.0`).
5869
5970
Trailing zeros are stripped during normalization.
6071
"""
6172

6273
@staticmethod
6374
def from_str(s: str) -> "Version":
64-
"""Parse a version string of the form ``X``, ``X.Y``, ``X.Y.Z``, ...
75+
"""Parse a version string of the form `X`, `X.Y`, `X.Y.Z`, ...
6576
6677
Raises:
6778
ValueError: If the string is empty or contains non-numeric parts.
@@ -79,10 +90,10 @@ class Version:
7990

8091
@final
8192
class KernelName:
82-
"""A validated kernel name matching ``^[a-z][-a-z0-9]*[a-z0-9]$``."""
93+
"""A validated kernel name matching `^[a-z][-a-z0-9]*[a-z0-9]$`."""
8394

8495
def __new__(cls, name: str) -> "KernelName":
85-
"""Create a new ``KernelName``.
96+
"""Create a new `KernelName`.
8697
8798
Raises:
8899
ValueError: If the name does not match the required pattern.
@@ -99,19 +110,141 @@ class KernelName:
99110
def __eq__(self, value: object, /) -> bool: ...
100111
def __hash__(self) -> int: ...
101112

113+
@final
114+
class DigestAlgorithm(Enum):
115+
"""Digest algorithm."""
116+
117+
SHA256 = "SHA256"
118+
SHA512 = "SHA512"
119+
120+
def __str__(self) -> str: ...
121+
def __repr__(self) -> str: ...
122+
123+
@final
124+
class Digest:
125+
"""Source digest for a kernel build variant."""
126+
127+
@staticmethod
128+
def hash_variant(
129+
algorithm: DigestAlgorithm, variant_path: os.PathLike[str] | str
130+
) -> "Digest":
131+
"""Hash the files in `variant_path` using `algorithm`.
132+
133+
Args:
134+
algorithm: Digest algorithm to use.
135+
variant_path: Path to the variant directory to hash.
136+
137+
Raises:
138+
OSError: If a file cannot be read or the directory cannot be walked.
139+
RuntimeError: For other unexpected failures.
140+
"""
141+
...
142+
143+
@property
144+
def algorithm(self) -> DigestAlgorithm:
145+
"""Digest algorithm used."""
146+
...
147+
148+
@property
149+
def files(self) -> dict[str, str]:
150+
"""Mapping of relative file path to base64-encoded digest."""
151+
...
152+
153+
def validate(self, other: "Digest") -> None:
154+
"""Validate `other` (actual) against this digest (expected).
155+
156+
Returns when the digests match. Otherwise, a `DigestValidationError` is
157+
raised.
158+
159+
Raises:
160+
DigestValidationError: If `other` deviates from this digest.
161+
"""
162+
...
163+
164+
def __repr__(self) -> str: ...
165+
166+
class DigestViolation:
167+
"""A violation of a digest when validated against a reference digest.
168+
169+
This tagged union covers the types of violations. Each violation can be
170+
converted to a string using `str(violation)`.
171+
"""
172+
173+
@final
174+
class MissingFile(DigestViolation):
175+
"""A file in the reference digest is missing from the digest."""
176+
177+
path: str
178+
__match_args__ = ("path",)
179+
def __new__(cls, path: str) -> "DigestViolation.MissingFile": ...
180+
181+
@final
182+
class UnknownFile(DigestViolation):
183+
"""A file present in the digest is not part of the reference digest."""
184+
185+
path: str
186+
__match_args__ = ("path",)
187+
def __new__(cls, path: str) -> "DigestViolation.UnknownFile": ...
188+
189+
@final
190+
class HashMismatch(DigestViolation):
191+
"""The hashes for the file differ."""
192+
193+
path: str
194+
expected: str
195+
got: str
196+
__match_args__ = ("path", "expected", "got")
197+
def __new__(
198+
cls, path: str, expected: str, got: str
199+
) -> "DigestViolation.HashMismatch": ...
200+
201+
@final
202+
class AlgorithmMismatch(DigestViolation):
203+
"""The digest algorithms differ.
204+
205+
The digest with algorithm `got` cannot be validated against the
206+
reference digest with algorithm `expected`.
207+
"""
208+
209+
expected: DigestAlgorithm
210+
got: DigestAlgorithm
211+
__match_args__ = ("expected", "got")
212+
def __new__(
213+
cls, expected: DigestAlgorithm, got: DigestAlgorithm
214+
) -> "DigestViolation.AlgorithmMismatch": ...
215+
216+
def __str__(self) -> str: ...
217+
218+
class DigestValidationError(Exception):
219+
"""Raised by `Digest.validate` when a digest cannot be validated against the reference."""
220+
221+
@property
222+
def violations(self) -> list[DigestViolation]:
223+
"""The individual digest violations."""
224+
...
225+
102226
@final
103227
class Metadata:
104-
"""Parsed ``metadata.json`` for a kernel build variant."""
228+
"""Parsed `metadata.json` for a kernel build variant."""
105229

106230
@staticmethod
107231
def read_from_file(metadata_path: os.PathLike[str] | str) -> "Metadata":
108-
"""Parse ``metadata.json`` at the given path.
232+
"""Parse `metadata.json` at the given path.
109233
110234
Raises:
111235
ValueError: On any I/O or parse error.
112236
"""
113237
...
114238

239+
@staticmethod
240+
def from_bytes(bytes: bytes) -> "Metadata":
241+
"""Parse `metadata.json` from JSON in a byte array.
242+
243+
Raises:
244+
ValueError: On any parse error.
245+
"""
246+
...
247+
115248
@property
116249
def id(self) -> str: ...
117250
@property
@@ -128,4 +261,6 @@ class Metadata:
128261
def python_depends(self) -> list[str]: ...
129262
@property
130263
def backend(self) -> BackendInfo: ...
264+
@property
265+
def digest(self) -> Optional[Digest]: ...
131266
def __repr__(self) -> str: ...

0 commit comments

Comments
 (0)