security: reject reused recovery approval keys
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user