Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/ca2a_runtime/hardware_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ def _nonce_hash(nonce: str) -> str:
return hashlib.sha256(nonce.encode()).hexdigest()


def _loads_strict(text: str) -> Any:
def _no_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
out: dict[str, Any] = {}
for key, value in pairs:
if key in out:
raise ValueError(f"duplicate JSON object key: {key!r}")
out[key] = value
return out

def _no_nonfinite(token: str) -> Any:
raise ValueError(f"non-finite JSON constant is not allowed: {token}")

return json.loads(text, object_pairs_hook=_no_duplicate_keys, parse_constant=_no_nonfinite)


def real_provider() -> SevSnpProvider:
if not SevSnpProvider.detect():
raise AttestationUnsupported("acceptance run requires a real non-paravisor SNP guest")
Expand Down Expand Up @@ -207,10 +222,10 @@ def run(config: dict[str, Any], base: Path, mode: str, log: ReceiptLog) -> None:
log.write("receiver_ready", provider="sev-snp")
service.serve_forever()
return
chain = [
DelegationCredential.from_dict(item)
for item in json.loads((base / config["chain"]).read_text(encoding="utf-8"))
]
raw_chain = _loads_strict((base / config["chain"]).read_text(encoding="utf-8"))
if not isinstance(raw_chain, list):
raise ValueError("chain file must contain a JSON array")
chain = [DelegationCredential.from_dict(item) for item in raw_chain]
key = Ed25519PrivateKey.from_private_bytes(
bytes.fromhex((base / config["holder_key"]).read_text(encoding="utf-8").strip())
)
Expand Down Expand Up @@ -257,7 +272,9 @@ def main() -> int:
args = parser.parse_args()
log = ReceiptLog(args.receipt, args.run_id)
try:
config = json.loads(args.config.read_text(encoding="utf-8"))
config = _loads_strict(args.config.read_text(encoding="utf-8"))
if not isinstance(config, dict):
raise ValueError("config must be a JSON object")
run(config, args.config.resolve().parent, args.mode, log)
except (CA2AError, ValueError, KeyError, OSError) as exc:
log.write("run_failed", error_type=type(exc).__name__)
Expand Down
14 changes: 12 additions & 2 deletions src/ca2a_runtime/transport/a2a_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ def collect_metadata(
2. ``message.metadata`` / ``params.message.metadata``
3. Explicit ``metadata`` keyword (caller override)
"""
if not isinstance(message_or_request, dict):
raise TransportError(
"message_or_request must be a JSON object",
detail=f"got {type(message_or_request).__name__}",
)
if metadata is not None and not isinstance(metadata, dict):
raise TransportError(
"metadata must be a mapping",
detail=f"got {type(metadata).__name__}",
)
collected: dict[str, Any] = {}

params = message_or_request.get("params")
Expand Down Expand Up @@ -140,10 +150,10 @@ def parse_peer_request(
parent_raw = meta[KEY_PARENT_RECORD_HASH]
if parent_raw is None:
parent_record_hash: str | None = None
elif isinstance(parent_raw, str):
elif isinstance(parent_raw, str) and parent_raw:
parent_record_hash = parent_raw
else:
raise TransportError("parent_record_hash must be a string or null")
raise TransportError("parent_record_hash must be a non-empty string or null")

sealed_payload: bytes | None = None
if KEY_SEALED_PAYLOAD in meta and meta[KEY_SEALED_PAYLOAD] is not None:
Expand Down
8 changes: 7 additions & 1 deletion src/ca2a_runtime/transport/a2a_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from __future__ import annotations

import math
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any

Expand Down Expand Up @@ -231,7 +232,12 @@ def metadata_from_sdk_message(message: Any) -> dict[str, Any]:
continue
for field in ("depth", "not_before", "not_after"):
value = credential.get(field)
if isinstance(value, float) and value.is_integer():
if (
isinstance(value, float)
and math.isfinite(value)
and value.is_integer()
and abs(int(value)) <= 9_007_199_254_740_991
):
credential[field] = int(value)
return result

Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_hardware_acceptance.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,38 @@ def offer(self, nonce):
receiver = [json.loads(line) for line in receiver_log.path.read_text().splitlines()]
assert receiver[-1]["event"] == "operation_completed"
assert receiver[-1]["stage"] == "receiver_offer"


@pytest.mark.parametrize(
"bad_cfg",
[
'{"a": 1, "a": 2}',
'{"a": NaN}',
"[1, 2]",
],
)
def test_harness_rejects_duplicate_keys_nonfinite_and_non_dict_config(
tmp_path, monkeypatch, bad_cfg
):
config = tmp_path / "config.json"
config.write_text(bad_cfg, encoding="utf-8")
receipt = tmp_path / "receipt.jsonl"
monkeypatch.setattr(
"sys.argv",
["hardware_acceptance", "call", str(config), "--receipt", str(receipt), "--run-id", "r1"],
)
assert harness.main() == 1
entry = json.loads(receipt.read_text())
assert entry["event"] == "run_failed"
assert entry["error_type"] == "ValueError"


def test_adapter_rejects_non_dict_inputs_empty_parent_hash_and_sdk_unsafe_float():
from ca2a_runtime.errors import TransportError
from ca2a_runtime.transport import a2a_adapter

for bad_msg in ([1, 2], "str", None):
with pytest.raises(TransportError):
a2a_adapter.collect_metadata(bad_msg) # type: ignore[arg-type]
with pytest.raises(TransportError):
a2a_adapter.collect_metadata({}, metadata=["not-a-dict"]) # type: ignore[arg-type]
Loading