security: reject reused recovery approval keys

This commit is contained in:
2026-09-27 04:03:35 +08:00
parent 87fd517376
commit 97334846af
2 changed files with 33 additions and 2 deletions
@@ -681,6 +681,7 @@ def _validate_keyring(
if not isinstance(raw_keys, list) or len(raw_keys) < 2:
raise SnapshotRestorePlanError("recovery keyring has insufficient keys")
result: dict[str, dict[str, Any]] = {}
public_keys: set[bytes] = set()
for raw in raw_keys:
item = _exact(
raw,
@@ -708,6 +709,11 @@ def _validate_keyring(
public_key = Ed25519PublicKey.from_public_bytes(public_raw)
except (TypeError, ValueError) as exc:
raise SnapshotRestorePlanError("recovery public key is invalid") from exc
if public_raw in public_keys:
raise SnapshotRestorePlanError(
"recovery keyring reuses an Ed25519 public key across identities"
)
public_keys.add(public_raw)
not_before = _timestamp(item.get("not_before"), label="key not_before")
not_after = _timestamp(item.get("not_after"), label="key not_after")
if not_after <= not_before:
@@ -105,6 +105,7 @@ def _case(
database_downgrade_performed: bool = False,
successful_release_count: int = 0,
same_approver_person: bool = False,
reused_approval_public_key: bool = False,
restore_bindings_match: bool = True,
empty_image_map: str | None = None,
) -> dict[str, Any]:
@@ -414,9 +415,14 @@ def _case(
restore_path = _write_json(evidence_root / "independent-restore-test.json", restore_test)
root_private = Ed25519PrivateKey.generate()
release_manager_key = Ed25519PrivateKey.generate()
role_keys = {
"release_manager": Ed25519PrivateKey.generate(),
"database_owner": Ed25519PrivateKey.generate(),
"release_manager": release_manager_key,
"database_owner": (
release_manager_key
if reused_approval_public_key
else Ed25519PrivateKey.generate()
),
}
approver_ids = {
"release_manager": "approver-release-manager",
@@ -1024,6 +1030,25 @@ def test_approvals_require_two_different_people_and_keys(
_validate(case)
def test_keyring_rejects_public_key_reused_across_approval_identities(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
case = _case(tmp_path, reused_approval_public_key=True)
key_records = case["keyring"]["keys"]
assert len({item["key_id"] for item in key_records}) == 2
assert len({item["approver_id"] for item in key_records}) == 2
assert len({item["role"] for item in key_records}) == 2
assert len({item["public_key_base64"] for item in key_records}) == 1
_provision_test_pins(monkeypatch, case)
with pytest.raises(
planner.SnapshotRestorePlanError,
match="reuses an Ed25519 public key across identities",
):
_validate(case)
def test_cli_has_no_apply_or_caller_supplied_trust_pin() -> None:
options = {
option