src/pqc_bootloader/key_ring.py
2.3 KB · 83 lines · python Raw
1 """KeyRing - allow-list of trusted manufacturer public keys."""
2
3 from __future__ import annotations
4
5 import hashlib
6 import json
7 from dataclasses import asdict, dataclass
8 from datetime import datetime, timezone
9 from typing import Any
10
11 from pqc_bootloader.errors import UnknownKeyError
12
13
14 @dataclass
15 class KeyRingEntry:
16 key_id: str # fingerprint (hex SHA3-256 of public_key bytes)
17 public_key: str # hex
18 algorithm: str
19 manufacturer: str
20 role: str = "firmware-signer"
21 added_at: str = ""
22 revoked: bool = False
23 revocation_reason: str = ""
24
25 def to_dict(self) -> dict[str, Any]:
26 return asdict(self)
27
28
29 class KeyRing:
30 """Trust store of manufacturer public keys."""
31
32 def __init__(self) -> None:
33 self._entries: dict[str, KeyRingEntry] = {}
34
35 @staticmethod
36 def fingerprint(public_key_hex: str) -> str:
37 return hashlib.sha3_256(bytes.fromhex(public_key_hex)).hexdigest()
38
39 def add(
40 self,
41 public_key_hex: str,
42 algorithm: str,
43 manufacturer: str,
44 role: str = "firmware-signer",
45 ) -> KeyRingEntry:
46 kid = self.fingerprint(public_key_hex)
47 entry = KeyRingEntry(
48 key_id=kid,
49 public_key=public_key_hex,
50 algorithm=algorithm,
51 manufacturer=manufacturer,
52 role=role,
53 added_at=datetime.now(timezone.utc).isoformat(),
54 )
55 self._entries[kid] = entry
56 return entry
57
58 def revoke(self, key_id: str, reason: str) -> None:
59 if key_id not in self._entries:
60 raise UnknownKeyError(f"no key with id {key_id}")
61 self._entries[key_id].revoked = True
62 self._entries[key_id].revocation_reason = reason
63
64 def get(self, key_id: str) -> KeyRingEntry:
65 if key_id not in self._entries:
66 raise UnknownKeyError(f"no key with id {key_id}")
67 return self._entries[key_id]
68
69 def is_trusted(self, key_id: str) -> bool:
70 return key_id in self._entries and not self._entries[key_id].revoked
71
72 def list_entries(self) -> list[KeyRingEntry]:
73 return list(self._entries.values())
74
75 def export_json(self) -> str:
76 return json.dumps(
77 [e.to_dict() for e in self._entries.values()],
78 indent=2,
79 )
80
81 def __len__(self) -> int:
82 return len(self._entries)
83