Skip to content

prototype(azure_execution_binding): bind HCLAkPub exponent e and key size, reject duplicate JSON keys, and harden _secure_boot_state and authenticode_sha256 - #620

Open
drk-ram wants to merge 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-azure-execution-binding-613
Open

drk-ram wants to merge 1 commit into
agentrust-io:mainfrom
drk-ram:drk-ram-fix-azure-execution-binding-613

Conversation

@drk-ram

@drk-ram drk-ram commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Contributor: Raghuvar Durga (@drk-ram)

Closes #613

Hardens prototype/azure_execution_binding.py across four validation boundaries while preserving acceptance of the captured Azure SEV-SNP/vTPM packet tests/fixtures/azure-execution-20260929/b1-approved-packet.json:

  1. Duplicate JSON key rejection (_pairs) in authenticate_ak and _runtime_claims:
    • Adds _pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any] raising ExecutionDenied("duplicate_json_key") (a ValueError subclass) on duplicate JSON object keys and passes object_pairs_hook=_pairs to json.loads in both authenticate_ak (surfacing as ExecutionDenied("hcl_ak")) and _runtime_claims (surfacing as ExecutionDenied("hcl_vm_configuration")).
  2. HCLAkPub RSA key validation and exponent binding in authenticate_ak:
    • Adds _b64url_uint(value: Any, label: str) -> int enforcing isinstance(value, str), re.fullmatch(r"[A-Za-z0-9_-]+", value), len(value) % 4 != 1, minimal big-endian byte encoding (raw and raw[0] != 0), and canonical base64url round-trip (base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") == value).
    • Requires keys[0].get("kty") == "RSA", decodes modulus = _b64url_uint(keys[0]["n"], "n") and exponent = _b64url_uint(keys[0]["e"], "e"), requires modulus.bit_length() >= 2048 and exponent == 65537 (ExecutionDenied("hcl_ak")), and binds (modulus, exponent) == (ak.public_numbers().n, ak.public_numbers().e) (ExecutionDenied("ak_not_hcl_bound")).
  3. PCR 7 EV_EFI_VARIABLE_DRIVER_CONFIG bounds and UTF-16-LE decoding in _secure_boot_state:
    • Requires isinstance(event.data, bytes) and len(event.data) >= 32 (ExecutionDenied("secure_boot_state")), unpacks name_len, data_len = struct.unpack_from("<QQ", event.data, 16), requires exact payload framing 32 + 2 * name_len + data_len == len(event.data) (verified across all 20 PCR 7 EV_EFI_VARIABLE_DRIVER_CONFIG events in b1-approved-packet.json, b2-substituted-relabel-packet.json, b4b-after-sudo-packet.json, and stock-relabel-packet.json), and catches UnicodeDecodeError from .decode("utf-16-le") as ExecutionDenied("secure_boot_state") (strict utf-16-le decoding rejects unpaired UTF-16 surrogates 0xD800..0xDFFF).
  4. PE header, section, and certificate table bounds in authenticode_sha256:
    • Requires isinstance(image, bytes), len(image) >= 0x40, pe + 24 <= len(image), and image[pe : pe + 4] == b"PE\x00\x00" (ValueError("not a PE image")), requires magic in (0x10B, 0x20B), cert_entry + 8 <= table, table + 40 * sections <= headers_size <= len(image), valid certificate table bounds ((cert_offset == 0 and cert_size == 0) or headers_size <= cert_offset and cert_offset + cert_size <= len(image)), and non-overlapping in-bounds sections (hashed <= pointer and pointer + size <= end), and catches struct.error as ValueError("malformed PE image").

What this change leaves alone and what was measured:

  • Normative specification text (spec/trace-v0.2.md), JSON schemas (schema/), and src/agentrust_trace/ are untouched; this change is scoped to prototype/azure_execution_binding.py, tests/test_execution_appraisal_and_azure_binding_hardening.py, and CHANGELOG.md.
  • Every check in prototype/azure_execution_binding.py fails closed with ExecutionDenied (or ValueError in the standalone authenticode_sha256 helper).
  • All measurements below were taken on commit 1d772c0 (1d772c0d495d5a09d10069a7c6d649c76ed67eda) on Python 3.11.9 (pytest 9.1.1):
    • pytest tests/test_azure_execution_binding.py tests/test_execution_appraisal_and_azure_binding_hardening.py -q: 29 passed, 14 skipped (0 failed, 0 xfailed, 0 errors). The 14 skipped tests in tests/test_azure_execution_binding.py skip when the external ../cloud-20260929/wcm checkout is absent; tests/test_execution_appraisal_and_azure_binding_hardening.py exercises tests/fixtures/azure-execution-20260929/b1-approved-packet.json directly (15 passed, 0 skipped).
    • python tools/review_bar.py --base origin/main --head HEAD: 15 fail on behaviour, 0 fail on a missing name or file, 0 pass or skip across a8e3559..1d772c0.

Independent review findings and dispositions:

  1. Changed: An earlier draft combined multiple rejections into 6 test functions and passed a 2048-bit ak_pem alongside a < 2048-bit HCLAkPub JWK in the modulus-size test, which caused origin/main to fail at ak_not_hcl_bound rather than accepting the weak key (DID NOT RAISE ExecutionDenied). We split the suite into 15 single-rejection tests and paired the 1024-bit and 2047-bit HCLAkPub JWKs with matching < 2048-bit ak_pem keys so origin/main accepts them and fails with DID NOT RAISE ExecutionDenied.
  2. Changed: An earlier draft added an unreachable post-decode surrogate check after .decode("utf-16-le") in _secure_boot_state (Python's strict utf-16-le decoder already raises UnicodeDecodeError on unpaired surrogates) and a duplicate HCL truncation check in _runtime_claims. Both were removed so every branch in the diff is reachable and killed by its dedicated test when switched off.

Type of change

  • Editorial (typo, link fix, clarification: no normative effect)
  • Non-breaking spec change (new optional field, new platform profile, informative addition)
  • Breaking spec change (requires 14-day comment period and Project Lead sign-off)
  • Schema change
  • Example addition

Spec section

None (prototype/azure_execution_binding.py, tests/test_execution_appraisal_and_azure_binding_hardening.py, and CHANGELOG.md only).

Tests for a fix

All 15 tests in tests/test_execution_appraisal_and_azure_binding_hardening.py pass on HEAD (1d772c0) and fail on behaviour against origin/main (a8e3559) and when each corresponding guard in prototype/azure_execution_binding.py is individually switched off while keeping _pairs and _b64url_uint:

  1. test_authenticate_ak_rejects_duplicate_json_keys (fails when object_pairs_hook=_pairs in authenticate_ak is switched off)
  2. test_runtime_claims_rejects_duplicate_json_keys (fails when object_pairs_hook=_pairs in _runtime_claims is switched off)
  3. test_authenticate_ak_rejects_non_rsa_kty (fails when keys[0].get("kty") != "RSA" is switched off)
  4. test_authenticate_ak_rejects_invalid_or_noncanonical_exponent_encoding (fails when _b64url_uint(keys[0]["e"], "e") / len(value) % 4 == 1 is switched off)
  5. test_authenticate_ak_rejects_sub_2048_bit_modulus (fails when modulus.bit_length() < 2048 is switched off)
  6. test_authenticate_ak_rejects_non_65537_exponent (fails when exponent != 65537 is switched off)
  7. test_authenticate_ak_rejects_mismatched_ak_pem_exponent (fails when (modulus, exponent) != (numbers.n, numbers.e) is reverted to modulus != numbers.n)
  8. test_secure_boot_state_rejects_non_bytes_and_short_event_data (fails when not isinstance(event.data, bytes) or len(event.data) < 32 is switched off)
  9. test_secure_boot_state_rejects_mismatched_variable_payload_length (fails when 32 + 2 * name_len + data_len != len(event.data) is switched off or relaxed to <=)
  10. test_secure_boot_state_rejects_invalid_utf16_variable_name (fails when except UnicodeDecodeError in _secure_boot_state is switched off)
  11. test_authenticode_sha256_rejects_non_bytes_and_truncated_pe_headers (fails when not isinstance(image, bytes) or len(image) < 0x40, pe + 24 > len(image), or except struct.error is switched off)
  12. test_authenticode_sha256_rejects_unknown_optional_header_magic (fails when magic not in (0x10B, 0x20B) is switched off)
  13. test_authenticode_sha256_rejects_out_of_bounds_header_and_section_table_sizes (fails when cert_entry + 8 > table or table + 40 * sections > headers_size or headers_size > len(image) is switched off)
  14. test_authenticode_sha256_rejects_invalid_certificate_table_bounds (fails when the cert_offset / cert_size bounds check is switched off)
  15. test_authenticode_sha256_rejects_overlapping_or_out_of_bounds_sections (fails when pointer < hashed or pointer + size > end is switched off)

Validation commands run on 1d772c0:

  • pytest tests/test_azure_execution_binding.py tests/test_execution_appraisal_and_azure_binding_hardening.py -q: passed (29 passed, 14 skipped)
  • python tools/review_bar.py --base origin/main --head HEAD: passed (15 fail on behaviour, 0 fail on a missing name or file, 0 pass or skip)
  • python tools/sweep_public_surface.py --strict: passed (89 parameters swept, 140 accepted junk values, 0 unfiled leak(s))
  • python tools/check_dashes.py: passed (no dashes outside examples/, spec/trace-v0.1.md)
  • ruff check prototype/azure_execution_binding.py tests/test_execution_appraisal_and_azure_binding_hardening.py: passed (All checks passed!)
  • mypy --strict --follow-imports=silent prototype/azure_execution_binding.py: passed (Success: no issues found in 1 source file)

Checklist

  • DCO sign-off on all commits (git commit -s)
  • CHANGELOG.md updated (for any normative change)
  • Breaking changes marked with <!-- CHANGED: #NNN: description --> in spec text
  • Backward compatibility statement included (for breaking changes)

Changelog entry

@drk-ram
drk-ram requested review from a team, lywinged and rajnisht7 as code owners October 23, 2026 03:55
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential LOW
Overall MEDIUM

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor check flagged MEDIUM risk label Oct 23, 2026

@dinakarjs dinakarjs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 1d772c0. Checked canonical RSA integer decoding, the single RSA HCLAkPub selection, exponent/modulus and supplied-key matching, duplicate runtime JSON rejection, secure-boot decoding and PE bounds. Focused local suites: 29 passed, 14 skipped; Ruff and diff checks passed. The skipped cases need the absent pinned WCM/cloud capture snapshot; the passing mocked authentication cases are unit boundary evidence, not live vTPM or hardware-attestation evidence. Current hosted CI/conformance/review-bar checks are successful. No blocking finding in this bounded change.

…size, reject duplicate JSON keys, and harden _secure_boot_state and authenticode_sha256

Closes agentrust-io#613

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Raghuvar Durga <177662415+drk-ram@users.noreply.github.com>
@drk-ram
drk-ram force-pushed the drk-ram-fix-azure-execution-binding-613 branch from 1d772c0 to d33ab87 Compare October 24, 2026 04:11

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk

Projects

None yet

2 participants