This guide maps the security claims to the implementation and named tests. It is written for source review, not as a substitute for running the verification commands. No pass count is asserted here: a checkout earns only the result of the commands actually run against that checkout.
UNTRUSTED / CLAIMED INPUTS CONFIGURED TRUST
┌──────────────────────────────┐ ┌──────────────────────────┐
│ signed manifest │ │ manifest signer + digest │
│ identity-endorsed request │ │ authorized identity key │
│ TPM quote + synthetic chain │ │ TPM/AK root + cert/PCR │
└──────────────┬───────────────┘ │ policy; issuer key/kid │
│ └─────────────┬────────────┘
v │
┌──────────────────── BROKER TRUST BOUNDARY ────────────────────v────────────┐
│ verify cA2A challenge -> manifest -> identity -> AK policy -> TPM appraisal│
│ | │
│ SQLite BEGIN IMMEDIATE: recheck time, consume challenge │
│ | │
│ issue signed, bounded cA2A credential under inventoryd-specific issuer │
│ sign canonical broker decision receipt │
└──────────────────────────────────┬─────────────────────────────────────────┘
│ credential
v
UNTRUSTED MCP CALLER CONFIGURED RESOURCE TRUST
┌──────────────────────────┐ ┌───────────────────────────────────────────┐
│ credential + proof │ │ broker public key; exact scope/audience │
│ sku + record ID │ │ receipt verification key; clocks/policy │
└─────────────┬────────────┘ └───────────────────┬───────────────────────┘
v v
┌────────────────── INVENTORYD TRUST BOUNDARY ───────────────────────────────┐
│ one low-level MCP call dispatcher │
│ challenge: persist token hash + exact call context │
│ lookup: verify root, scope, bounds, lifetime, stored context, holder proof│
│ | │
│ SQLite BEGIN IMMEDIATE: recheck time, consume challenge, insert spend │
│ | commit before invocation │
│ v │
│ private `_inventory_lookup` handler │
│ | │
│ signed canonical decision receipt │
└────────────────────────────────────────────────────────────────────────────┘
The deployment and its policy/key material, clocks, process integrity, SQLite files, and routing topology are trusted. The caller, manifest document, quote, credential, proof, tool arguments, and record ID are inputs to be verified. Receipt consumers form a further boundary: they must use the configured receipt public key and expected key ID, not a key delivered alongside a receipt.
The relevant dependencies are exact-pinned in pyproject.toml and resolved in
uv.lock.
-
Agent Manifest
0.12.0. The repository'sverify_signed_manifestwrapper first normalizes through releasedManifest, checks the configured issuer and digest, constructs a strictVerificationContext, calls releasedverify_manifest, then requires bothresult.result is OverallResult.VALIDandresult.signature_verified is True. Merely receiving a truthy result is not enough. Manifest-level hardware attestation is not enabled: the separately verified issuance-transcript quote must not be represented as a verified manifest-attestation binding or proof of manifest execution. -
cryptography
50.0.1synthetic AK policy. Before released quote verification,enforce_synthetic_ak_certificate_policyparses the supplied leaf-first chain and configured roots withx509.load_pem_x509_certificates, reads the UTC validity properties andBasicConstraints/KeyUsageextensions, and applies this experiment's limited, fail-closed profile at an injected evaluation time. It requires current validity for the leaf and every non-anchor intermediate, a non-CA leaf withdigitalSignatureand withoutkeyCertSign, CA BasicConstraints andkeyCertSignfor every issuer and configured root, and intermediate path-length limits. Missing, malformed, or policy-incompatible inputs fail closed.The local profile does not evaluate the validity window of the terminal chain certificate or configured trust anchors; it still enforces their CA role and
keyCertSign. Agent Manifest0.12.0checks every supplied chain certificate's validity, including the terminal root. Both layers usenotBefore <= time < notAfterwhere they apply time checks. This is not a general PKIX engine: the local guard does not establish trust-anchor equality, verify signatures, build arbitrary paths, check revocation, apply a trust-anchor expiry policy, or process the full RFC 5280 policy surface. Certificate signatures and configured-root fingerprint trust remain checks in the released Agent Manifest path. -
Agent Manifest TPM API.
ReleasedTpmAppraiser.appraiseparses the signed PCR selection withtpm2-pytss, requires its exact equality to policy, runs the narrow certificate-policy guard, then callsverify_tpm_quote(...)with the configured roots, qualifying data, PCR digest, andverification_time. One clock snapshot is checked by the local guard and normalized to UTC for the released verifier, so deterministic fixtures do not fall back to host wall-clock time. The local guard is retained because the release does not enforce all of this experiment's mandatory leaf-role/KeyUsage constraints. It accepts only the singleton boolean resultis True; a truthy non-boolean fails closed. The released API does not surface the signed PCR selection, hence the separate standards-backed parse.Release-specific source references:
verify_tpm_quoteand AK-chain delegation, shared chain validity and issuer policy.This is a clean-fixture migration, not an automatic migration of persisted signed manifests or policy digests. Version
0.12.0also corrects number formatting and UTF-16 key ordering. Revalidate existing signatures and approved digest pins containing affected values before deployment; do not rewrite signed data to force acceptance. -
cA2A Runtime
0.2.0. Broker and resource tokens use the documentedv1.<expiry>.<random>.<HMAC-SHA256>challenge wire shape. The smallissue_ca2a_challenge_atadapter exists only to inject the same testable clock into issuance and persistence; it self-checks each token with the releasedverify_challenge. SQLite storesSHA-256(token), not the bearer token. A holder proof's wire object is{challenge, signature};inventorydreconstructs the signed body with credential ID/subject, audience, exact qualified capability, record ID, canonical sealed arguments, and the explicit null channel/parent bindings before callingverify_holder_proof. -
No credential audience in cA2A Runtime
0.2.0.DelegationCredentialsigns issuer, subject, scope, credential ID, depth/parent, and validity bounds, but has no audience field. Resource restriction is therefore the combination of a distinct inventoryd issuer key and exactmcp://inventoryd/tool/inventory.lookupscope. Audience is instead bound in the resource challenge and holder proof. The credential itself is not claimed to be audience-bound. -
mcpPython SDK2.1.1.InventoryApplicationconstructs the low-levelmcp.server.Serverwith oneon_call_tooldispatcher. That dispatcher exposes onlyinventory.challengeand the protectedinventory.lookup; the private handler is not registered. The server-selectedparams.nameand configured method drive authorization, not a caller-provided method field. If receipt signing fails after invocation, the dispatcher catches onlyPostInvocationErrorand returns a strict, explicitly unsigned error state with the spend/invocation facts. That object has no decision, reason, credential, or receipt JWS and is not authenticated evidence. -
JWS.
joserfc==1.7.5performs compact JWS signing and verification with the allowlistedEd25519algorithm. The entire protected header must equal{"alg":"Ed25519","kid":<configured>,"typ":"atcap-decision+jws"}. Payloads must be RFC 8785 canonical JSON and validate against the closed receipt schema. This is library JWS serialization, not bespoke signature framing.
Implementation anchors:
src/atcap/manifest_verifier.pysrc/atcap/tpm.pysrc/atcap/challenge.pysrc/atcap/inventory.pysrc/atcap/receipt.py
CapabilityBroker.new_challengeissues a cA2A-format token and stores its SHA-256 digest plus issuance and expiry times.- The requester constructs
IssuanceRequest. Its identity signature covers the canonical version, broker ID, challenge, signed-manifest digest, identity key, ephemeral holder key, resource issuer key ID, resource issuer public key, and exact requested scope.issuance_qualifying_dataadditionally hashes the complete endorsed request, including that signature, into TPMextraData. CapabilityBroker.issueverifies token MAC/expiry, the signed manifest, the exact manifest-to-identity mapping, the issuer key and key-ID request bindings, the identity signature, PCR selection, the synthetic AK certificate profile, quote signature/AK chain, qualifying data, and expected composite PCR digest. The repository profile check runs before the released quote verifier; the latter remains responsible for signatures, configured-root trust, and the quote.- Only after appraisal completes,
SQLiteStore.consume_broker_challengeobtains a write lock, re-reads time, and atomically consumes the still-live challenge. Failed appraisal therefore does not burn the challenge. A post-appraisal race still yields at most one credential. - The broker uses 32 random bytes rendered as a 64-hex-character credential ID,
sets both
not_beforeandnot_after, sets only the qualified scope, and signs with the policy-matched resource-specific issuer private key. InventoryApplication.issue_resource_challengefixes the method and audience from resource policy, canonicalizes the business arguments, and stores only the token hash plus credential ID, method, argument digest, record ID, audience, and expiry.- The holder calls the protected lookup with the signed credential and the cA2A
{challenge, signature}proof.InventoryApplication.redeemparses both, requires a 256-bit ID, both validity bounds, an allowed maximum lifetime, the configured root at current time, exact singleton scope, stored context, a live challenge, and a valid reconstructed holder proof. SQLiteStore.consume_challenge_and_spend_credentialacquires a write lock and rechecks challenge time, credential time, and the full stored context inside the transaction. It consumes that challenge and performs anINSERT OR IGNOREinto the credential-ID primary-key table, then commits. A non-first insertion isCREDENTIAL_SPENT; its fresh challenge is consumed.- Only the first spender reaches
_record_invocationand the private handler. The commit deliberately precedes invocation, so a crash can spend a credential without returning a result. That is at-most-once redemption, not exactly-once execution. - The deciding service signs a canonical receipt. If the handler itself fails,
the outcome remains an authorized invocation (
allow/HANDLER_FAILED), never a fabricated authorization denial; the MCP result can still setis_errorto report the business failure. Successful raw handler output is normalized through the strict, extra-forbiddenInventoryLookupResultinto onlysku,quantity, and the server-assignedinvocation_number. Malformed, extra-field, or non-JSON-safe output becomes a signed authorizedHANDLER_FAILEDresult rather than leaking raw output. If signing then fails,PostInvocationErroris translated at the MCP boundary into the closedatcap-unsigned-post-invocation/v1state. It preserves the invocation ID and completion flag without returning a false denial, but it is explicitly not a signed receipt.
| Invariant | Enforcing implementation | Direct tests |
|---|---|---|
| Manifest is authorized and cryptographically valid | verify_signed_manifest; ManifestPolicy |
test_allow_uses_real_manifest_verification_and_signed_receipt; test_substituted_manifest_is_not_authorized; test_manifest_with_policy_matching_digest_but_invalid_signature_is_denied |
| One manifest digest maps to one requester identity | verify_identity_endorsement; ManifestPolicy.identity_public_hex |
test_identity_not_bound_to_manifest_is_denied |
| Identity endorses the holder and the complete issuance request | IssuanceRequest.body/signing_bytes; endorse_request; verify_identity_endorsement |
test_holder_substitution_breaks_complete_request_endorsement; test_quote_qualifying_data_commits_to_every_issuance_field |
Resource issuer public key and kid are request- and policy-bound |
verify_identity_endorsement; BrokerPolicy.resource_issuer_*; broker constructor key match |
test_substituted_resource_issuer_is_denied_even_when_identity_endorses_it; test_quote_qualifying_data_commits_to_every_issuance_field |
| Protocol version and holder public-key shape are explicit | verify_identity_endorsement; _ISSUANCE_VERSION; _ED25519_PUBLIC_HEX_RE |
test_unsupported_issuance_request_version_is_denied; test_identity_endorsed_malformed_holder_key_is_denied |
| Runtime-malformed broker request/evidence objects are rejected before that malformed object is input-hashed, appraised, or can cause challenge spend; policy and already-validated safe bindings may already be derived | _validate_issuance_request_runtime; _validate_tpm_evidence_runtime; CapabilityBroker.issue fail-closed preparation |
test_malformed_issuance_request_field_is_signed_without_consuming_challenge; test_non_request_runtime_object_is_signed_without_consuming_challenge; test_malformed_tpm_evidence_field_is_signed_without_consuming_challenge; test_non_evidence_runtime_object_is_signed_without_consuming_challenge |
| Synthetic AK certificate policy is checked before released quote verification | enforce_synthetic_ak_certificate_policy; ReleasedTpmAppraiser.appraise ordering |
test_local_ak_policy_rejects_before_released_quote_verification; test_local_ak_policy_fails_closed_on_missing_or_malformed_pem; test_local_ak_policy_requires_role_extensions; test_local_ak_policy_rejects_naive_evaluation_time; test_real_swtpm_ak_certificate_and_signature_denials |
| Both certificate-policy layers evaluate one instant; leaf/intermediate expiry is exclusive | ReleasedTpmAppraiser.appraise; _require_current; released verification_time parameter |
test_released_adapter_uses_one_normalized_certificate_clock_snapshot; test_released_adapter_passes_all_policy_bindings_and_requires_true; test_local_ak_policy_rejects_exact_not_after |
| Local-guard validity applies to leaf/intermediates; released verification additionally enforces terminal-root validity | _require_current; local terminal-index exclusion; released verify_tpm_quote |
test_local_ak_policy_does_not_apply_validity_to_trust_anchors; test_local_ak_policy_rejects_before_released_quote_verification[expired-intermediate]; test_real_swtpm_ak_certificate_and_signature_denials[expired-root-TPM_UNTRUSTED] |
Quote selection and values are exact and verifier result is literally True |
tpm2_pytss_selection_reader; ReleasedTpmAppraiser.appraise |
test_released_adapter_passes_all_policy_bindings_and_requires_true; test_truthy_non_boolean_tpm_result_fails_closed; test_wrong_signed_pcr_selection_fails_before_quote_acceptance; test_real_swtpm_quote_drives_broker_allow_and_rejects_bad_policy |
| Appraisal precedes broker challenge spend | CapabilityBroker.issue ordering; SQLiteStore.consume_broker_challenge |
test_failed_appraisal_does_not_burn_broker_challenge |
| Clock-injected challenges retain the released cA2A wire and expiry semantics | issue_ca2a_challenge_at; released verify_challenge self-check |
test_clock_injected_challenge_has_exact_ca2a_shape_and_expiry |
| Broker challenge is one-time and time is rechecked under lock | SQLiteStore.consume_broker_challenge (BEGIN IMMEDIATE) |
test_consumed_broker_challenge_cannot_be_reused; test_two_issuances_racing_one_broker_challenge_mint_exactly_one_credential; test_broker_challenge_expiring_while_waiting_for_sqlite_lock_is_not_used |
| Credential IDs contain 256 random bits before hex encoding | CapabilityBroker.issue (secrets.token_hex(32)); resource ID syntax check |
test_credential_ids_have_256_bits_of_randomness_and_do_not_repeat |
| Credential root and exact scope are resource-specific | InventoryApplication.redeem; verify_chain; exact frozenset comparison |
test_untrusted_resource_specific_broker_root_is_denied; test_wrong_resource_scope_is_denied |
| Both signed validity bounds and maximum lifetime are required | InventoryApplication.redeem; ResourcePolicy.max_credential_lifetime_seconds |
test_expired_credential_is_denied; test_credential_with_missing_validity_bound_is_denied; test_overlong_credential_lifetime_is_denied |
| Resource challenge and proof bind the exact call | issue_resource_challenge; verify_holder_proof; consume_challenge_and_spend_credential |
test_resource_challenge_hash_and_full_context_are_persisted; test_holder_key_substitution_is_denied; test_resource_challenge_cannot_be_moved_to_another_credential; test_holder_proof_is_bound_to_audience_and_qualified_capability; test_resource_challenge_rejects_argument_or_record_substitution |
| Challenge and credential validity are rechecked under the redemption lock | SQLiteStore.consume_challenge_and_spend_credential |
test_challenge_expiring_while_waiting_for_sqlite_lock_is_not_spent; test_credential_expiring_while_waiting_for_sqlite_lock_is_not_spent |
| A credential reaches the handler at most once, including concurrent fresh proofs | credential-ID primary key; consume_challenge_and_spend_credential; commit-before-handler ordering |
test_spent_credential_is_denied_with_a_fresh_valid_holder_proof; test_two_fresh_proofs_racing_one_credential_invoke_exactly_once |
| No unauthenticated MCP route reaches the private handler | low-level _list_tools / _call_tool; private _inventory_lookup |
test_only_challenge_and_protected_lookup_are_registered; test_unauthenticated_direct_and_mcp_bypass_attempts_never_invoke_handler; test_registered_challenge_to_holder_proof_to_lookup_choreography; test_malformed_registered_challenge_requests_are_signed_denials |
| Receipts require trusted key, exact protected metadata, signature, canonical payload, non-coercing types, closed fields, consistent semantics, and role-specific allow bindings | DecisionReceiptPayload; ReceiptSigner; ReceiptVerifier |
all tests in tests/test_receipts.py; allow-receipt assertions in broker/resource tests |
| Unexpected preauthorization dependency failures fail closed without leaking inputs or reaching the handler | generic error branches in CapabilityBroker.issue and InventoryApplication.redeem |
test_unexpected_manifest_verifier_failure_is_signed_and_does_not_leak; test_unexpected_tpm_appraiser_failure_is_signed_and_does_not_leak; test_unexpected_authorization_failure_is_signed_fail_closed_and_does_not_leak |
| Handler output crosses a closed, JSON-safe result boundary after invocation; malformed output is an authorized failed execution and raw fields do not escape | InventoryLookupResult; post-handler validation branch in InventoryApplication.redeem |
test_mcp_malformed_handler_result_is_signed_failed_execution_without_leakage; test_mcp_malformed_handler_result_and_receipt_outage_preserve_unsigned_state |
| Failures after invocation are never relabeled as authorization denial | post-spend branch in InventoryApplication.redeem; PostInvocationError; UnsignedPostInvocationState |
test_handler_failure_after_spend_is_an_authorized_failed_execution; test_receipt_failure_after_successful_handler_is_never_reclassified_as_denial; test_mcp_post_invocation_receipt_outage_returns_unsigned_state_without_leakage |
| Broker receipt policy hash changes with every modeled broker trust root and TTL | ManifestPolicy.public_dict; TpmPolicy.public_dict; BrokerPolicy.public_dict |
test_broker_policy_hash_commits_every_security_root_and_ttl; broker allow-receipt artifact-hash assertion |
| Resource receipt policy hash changes with every modeled resource-policy input | ResourcePolicy.public_dict; InventoryApplication._receipt |
test_resource_receipt_policy_hash_commits_every_security_input |
| Parallel successful calls retain their own receipt identity | _record_invocation; DecisionReceiptPayload.invocation_id; handler_count_snapshot |
test_parallel_valid_calls_sign_their_own_invocation_ids |
All listed resource denials assert that the handler counter remains unchanged, or are paired with an assertion that only the already-authorized first call was counted.
The unit and real-swtpm certificate matrices use the exact new profile IDs
expired-intermediate, not-yet-valid-intermediate,
intermediate-key-usage, and leaf-key-cert-sign. They run under
test_local_ak_policy_rejects_before_released_quote_verification and
test_real_swtpm_ak_certificate_and_signature_denials, respectively. The latter
uses a genuine software-TPM AK and quote for every profile.
intermediate-key-usage removes keyCertSign from an intermediate CA;
leaf-key-cert-sign keeps digitalSignature=true while adding the forbidden
keyCertSign=true to the AK leaf.
The release-upgrade profiles expired-root, not-yet-valid-root, and
root-not-after pass the unchanged local root-time exemption but are rejected
by released verification, using genuine software-TPM quotes at the injected
evaluation time. They exercise the composed policy, not a monkeypatched
upstream verifier. The valid-quote control uses the same injected time.
| Attack or failure | Expected result | Exact pytest evidence |
|---|---|---|
| Untrusted/modified TPM evidence | TPM_UNTRUSTED or fail-closed TPM error |
test_untrusted_tpm_evidence_is_denied_and_receipted; production adapter coverage in test_real_swtpm_quote_drives_broker_allow_and_rejects_bad_policy |
| Expired/not-yet-valid AK leaf or non-anchor intermediate, CA-role/path-length violations, incompatible leaf/CA KeyUsage, or malformed certificate input | TPM_UNTRUSTED before released verification |
test_local_ak_policy_rejects_before_released_quote_verification; test_local_ak_policy_fails_closed_on_missing_or_malformed_pem; test_real_swtpm_ak_certificate_and_signature_denials |
| Tampered certificate-chain or quote signature | TPM_UNTRUSTED / TPM_INVALID from released verification before issuance |
test_parseable_tampered_chain_signature_reaches_released_verifier; test_real_swtpm_ak_certificate_and_signature_denials |
Expired/not-yet-valid terminal root, including exactly notAfter |
TPM_UNTRUSTED from Agent Manifest 0.12.0 after the local guard |
test_real_swtpm_ak_certificate_and_signature_denials with expired-root, not-yet-valid-root, and root-not-after |
| Wrong PCR selection or digest | PCR_POLICY / TPM_INVALID |
test_wrong_pcr_state_is_denied; test_wrong_signed_pcr_selection_fails_before_quote_acceptance; test_real_swtpm_quote_drives_broker_allow_and_rejects_bad_policy |
| Stale broker challenge | CHALLENGE_STALE |
test_stale_broker_challenge_is_denied; test_broker_challenge_expiring_while_waiting_for_sqlite_lock_is_not_used |
| Consumed/raced broker challenge | CHALLENGE_CONSUMED; only one credential |
test_consumed_broker_challenge_cannot_be_reused; test_two_issuances_racing_one_broker_challenge_mint_exactly_one_credential |
| Unauthorized manifest/identity pairing | IDENTITY_UNAUTHORIZED |
test_identity_not_bound_to_manifest_is_denied |
| Substituted or invalid manifest | MANIFEST_POLICY / MANIFEST_INVALID |
test_substituted_manifest_is_not_authorized; test_manifest_with_policy_matching_digest_but_invalid_signature_is_denied |
| Substituted holder in issuance request | IDENTITY_SIGNATURE |
test_holder_substitution_breaks_complete_request_endorsement |
| Substituted resource issuer key or key ID | REQUEST_BINDING |
test_substituted_resource_issuer_is_denied_even_when_identity_endorses_it; test_substituted_resource_issuer_kid_is_denied_even_when_identity_endorses_it |
| Unsupported issuance protocol or malformed endorsed holder key | REQUEST_BINDING |
test_unsupported_issuance_request_version_is_denied; test_identity_endorsed_malformed_holder_key_is_denied |
| Runtime-malformed issuance-request or TPM-evidence object/field | signed REQUEST_BINDING / TPM_INVALID, challenge remains usable, no raw value leakage |
test_malformed_issuance_request_field_is_signed_without_consuming_challenge; test_non_request_runtime_object_is_signed_without_consuming_challenge; test_malformed_tpm_evidence_field_is_signed_without_consuming_challenge; test_non_evidence_runtime_object_is_signed_without_consuming_challenge |
| Resource credential from another root | CREDENTIAL_INVALID |
test_untrusted_resource_specific_broker_root_is_denied |
| Wrong resource scope | SCOPE_DENIED |
test_wrong_resource_scope_is_denied |
| Expired, unbounded, or overlong credential | CREDENTIAL_EXPIRED / CREDENTIAL_INVALID |
test_expired_credential_is_denied; test_credential_with_missing_validity_bound_is_denied; test_overlong_credential_lifetime_is_denied |
| Holder key substitution | HOLDER_PROOF_INVALID |
test_holder_key_substitution_is_denied |
| Challenge moved to another credential | HOLDER_PROOF_INVALID |
test_resource_challenge_cannot_be_moved_to_another_credential |
| Wrong audience or capability in proof | HOLDER_PROOF_INVALID |
test_holder_proof_is_bound_to_audience_and_qualified_capability |
| Argument or record substitution | HOLDER_PROOF_INVALID |
test_resource_challenge_rejects_argument_or_record_substitution |
| Stale or replayed resource challenge | CHALLENGE_STALE / CHALLENGE_CONSUMED |
test_stale_resource_challenge_is_denied; test_consumed_resource_challenge_cannot_be_replayed; test_challenge_expiring_while_waiting_for_sqlite_lock_is_not_spent |
| Credential expires while blocked on the spend lock | CREDENTIAL_EXPIRED, no spend |
test_credential_expiring_while_waiting_for_sqlite_lock_is_not_spent |
| Spent credential with a new valid proof | CREDENTIAL_SPENT |
test_spent_credential_is_denied_with_a_fresh_valid_holder_proof |
| Two fresh proofs race one credential | one ALLOW, one CREDENTIAL_SPENT, one invocation |
test_two_fresh_proofs_racing_one_credential_invoke_exactly_once |
| Missing auth or private-name MCP bypass | UNAUTHENTICATED, zero invocations |
test_unauthenticated_direct_and_mcp_bypass_attempts_never_invoke_handler |
| Malformed registered MCP challenge request | signed UNAUTHENTICATED, zero invocations, no raw input echo |
test_malformed_registered_challenge_requests_are_signed_denials |
| Unexpected manifest, TPM-appraiser, holder-proof, or atomic-spend dependency exception before invocation | signed INTERNAL_ERROR, no handler invocation, no raw exception/input/key leakage |
test_unexpected_manifest_verifier_failure_is_signed_and_does_not_leak; test_unexpected_tpm_appraiser_failure_is_signed_and_does_not_leak; test_unexpected_authorization_failure_is_signed_fail_closed_and_does_not_leak |
| Malformed, extra-field, or non-JSON-safe handler result after invocation | signed allow / HANDLER_FAILED, one spend/invocation, only closed failure state escapes |
test_mcp_malformed_handler_result_is_signed_failed_execution_without_leakage |
| Malformed handler result plus receipt-signer outage | explicit unsigned post-invocation state, one spend/invocation, no raw handler or signer detail | test_mcp_malformed_handler_result_and_receipt_outage_preserve_unsigned_state |
| Receipt signer unavailable after handler invocation | explicit unsigned post-invocation MCP error, one spend/invocation, never a denial | test_mcp_post_invocation_receipt_outage_returns_unsigned_state_without_leakage |
| Receipt tampering, wrong key/header, noncanonical JSON, wrong types, unknown fields, inconsistent semantics, or missing allow bindings | RECEIPT_INVALID |
tests named test_*receipt*rejected in tests/test_receipts.py |
On normal runtime-shape-validated input paths, broker receipts contain hashes, not raw evidence, for the TPM attestation, signature, and AK chain. They also carry:
issuance_request_sha256over the complete request including the identity signature; andbroker_policy_sha256over the RFC 8785 canonicalBrokerPolicy.public_dict().
Runtime-malformed denials include only hashes that were safely derived before the failure. A hash for an unavailable or not runtime-shape-validated request/evidence object is omitted rather than fabricated; policy and already-validated binding hashes may still be present.
The broker policy projection includes broker ID, exact scope, resource issuer
kid, resource issuer public key and its SHA-256 digest, challenge and credential
TTLs, the authorized manifest digest/issuer/signing key ID/signing public key,
identity key and three artifact hashes, plus the TPM selection, expected PCR
digest, and SHA-256 of the trusted-root PEM bytes.
Inventory receipts carry resource_policy_sha256 over audience, method, exact
scope, trusted broker public key, challenge TTL, and maximum credential lifetime.
The receipt also directly records the decision, reason, credential ID, challenge
token hash, method, audience, argument digest, record ID, per-call
invocation_id, handler_count_snapshot, handler-invoked flag, and
business-result state.
The verifier uses strict, extra-forbidden Pydantic validation after authenticating
canonical bytes, so it does not reinterpret signed string values as integers or
booleans. It also enforces role-specific conditions. A broker allow requires a
manifest digest, challenge hash, exact scope, and broker_policy_sha256; an
inventory allow requires a challenge hash, record ID, and
resource_policy_sha256 in addition to its call and invocation fields. Denial
receipts explicitly encode unavailable bindings as null; they may not omit the
schema or core execution-state claims and must state that the handler was not
invoked.
These hashes make a signed decision self-describing with respect to the listed inputs; they are not a transparency log. Broker tests mutate the TPM root, resource issuer key, manifest signer public key, and TTL independently. Resource tests mutate audience, method, scope, broker key, challenge TTL, and maximum credential lifetime. Every mutation must change the corresponding policy hash.
For handler execution, the schema records the specific invocation_id allocated
to that call and a handler_count_snapshot taken when its receipt is constructed.
The snapshot may exceed the call's own ID when other successful calls run in
parallel; it is not the identity of the invocation.
The marked integration test performs a real TPM 2.0 command flow against the
single Compose swtpm: reset and extend PCR 16, create EK/AK objects, produce a
quote whose extraData is the issuance transcript digest, independently check
the quote with tpm2_checkquote, parse its signed selection, and feed the bytes
through ReleasedTpmAppraiser and broker issuance. It also rejects an incorrect
PCR digest, a previously valid quote replayed against a fresh issuance request,
an untrusted configured root, leaf/intermediate time violations,
constraint/key-usage violations, and tampered certificate-chain and quote
signatures. These cases use a real software-TPM quote and the actual policy and
released-verification paths; they are not mocks presented as integration
evidence.
For this fixture, the test creates an ephemeral test CA and issues a synthetic
AK certificate for the swtpm AK public key. That construction exercises the
narrow certificate profile plus the configured signature/root verification path.
It is not manufacturer enrollment, proof of a hardware endorsement credential,
or an operational AK issuance ceremony.
test_local_ak_policy_does_not_apply_validity_to_trust_anchors separately fixes
the policy boundary: an expired terminal chain anchor and an additional expired
configured anchor are accepted by this narrow precheck when their required CA
constraints and key usage remain valid. That unit test is not evidence that the
complete released path would select or trust either anchor.
Host checks use the committed lock and exclude only the Linux swtpm marker:
uv sync --frozen --python 3.12 --extra dev
TZ=UTC ./scripts/verify-coverage.sh -m 'not swtpm'
uv run --frozen pytest examples/runpod-untrusted-caller/tests -m 'not swtpm'
uv run --frozen ruff check .
uv run --frozen ruff format --check .
uv run --frozen mypy
uv run --frozen mypy --strict examples/runpod-untrusted-caller/lab \
examples/runpod-untrusted-caller/bounded_capture.py \
examples/runpod-untrusted-caller/deadline_supervisor.py \
examples/runpod-untrusted-caller/billing_observation.py \
examples/runpod-untrusted-caller/evidence_manifest.py \
examples/runpod-untrusted-caller/provider_readback.py \
examples/runpod-untrusted-caller/handler.py \
examples/runpod-untrusted-caller/handler_self_test.py \
examples/runpod-untrusted-caller/self_test.py \
examples/runpod-untrusted-caller/lab_test_support.py
uv run --frozen bandit -q -r src
uv run --frozen bandit -q scripts/verify-package.py
uv run --frozen bandit -q -r examples/runpod-untrusted-caller/lab
uv run --frozen bandit -q \
examples/runpod-untrusted-caller/bounded_capture.py \
examples/runpod-untrusted-caller/deadline_supervisor.py \
examples/runpod-untrusted-caller/billing_observation.py \
examples/runpod-untrusted-caller/evidence_manifest.py \
examples/runpod-untrusted-caller/provider_readback.py \
examples/runpod-untrusted-caller/handler.py \
examples/runpod-untrusted-caller/handler_self_test.py \
examples/runpod-untrusted-caller/self_test.py
package_dist_dir="$(mktemp -d)"
uv run --frozen python -m build --no-isolation --outdir "${package_dist_dir}"
uv run --frozen python scripts/verify-package.py --dist-dir "${package_dist_dir}"
find "${package_dist_dir}" -depth -delete
uv run --frozen pip-audit
uv run --frozen pip-audit -r examples/runpod-untrusted-caller/requirements.lock
uv run --frozen detect-secrets scan --all-files \
--exclude-files '(^|/)(\.git|\.venv|\.mypy_cache|\.pytest_cache|\.ruff_cache|build|dist)/' .The clean Linux/TPM path starts exactly one internal-only simulator and makes the
marked test mandatory by supplying ATCAP_SWTPM_TCTI:
docker compose config --quiet
docker compose build --pull --no-cache
docker compose up --abort-on-container-exit --exit-code-from verify verify
docker compose down --volumes --remove-orphanscontainer-smoke.sh checks simulator usability first and then runs the core
pytest suite under branch-enabled combined coverage plus the isolated lab's
no-Runpod-infrastructure fake-transport and real-local-swtpm tests, Ruff lint and formatting,
strict mypy, Bandit, and strict package verification. Coverage.py combines
statement and branch opportunities for its configured fail_under: the 85.00%
combined floor is below the measured 85.66% pre-change host combined baseline.
The baseline branch-only percentage was 71.51%; it is informational, with no
independent branch-only threshold. Package verification checks exact wheel/sdist
file inventories, expected sdist modes and Unix wheel modes when present,
pyproject-derived metadata, wheel RECORD hashes/sizes, forbidden-file
exclusions, and a frozen-lock offline wheel install/import in a fresh
environment. tests/test_verify_package.py fixes duplicate-member, duplicate
RECORD, mode, and metadata-mutation failures as regressions.
pip-audit and detect-secrets remain explicit host checks because the former's
advisory result changes with its online database and neither is part of the TPM
behavioral smoke.
The optional live runner performs a separate fail-closed worker gate before any
Runpod mutation: it anonymously pulls the configured immutable linux/amd64
digest and runs both deployed self-tests with no network, a read-only root
filesystem, non-root identity, dropped capabilities, and
no-new-privileges. The worker Dockerfile rejects an empty dependency lock,
runs pip check, and imports the copied handler during the build. These are
local startability checks, not evidence that Runpod executed that image.
The Docker path has a digest-pinned base-image index and a committed Python lock.
It is not bit reproducible:
apt package versions come from floating Debian repositories, and the
version-pinned uv bootstrap is not artifact-hash-verified. These commands are a
repeatable verification procedure, not a promise of byte-identical images.
A passing full suite supports only this configured experiment: the broker
accepted a fresh, synthetically enrolled swtpm quote with approved PCR state, a
verified manifest, and a completely endorsed issuance request; then the intended
MCP resource verified and spent the resulting exact-scope capability at most
once.
It does not prove TPM/holder/agent co-location, holder-key residency, agent or manifest execution, continuous runtime integrity, safe behavior, real-hardware provenance, rollback-resistant storage, or exactly-once handler execution. The TPM may be a remote signing oracle for the authenticated requester. See the threat model for the full nonclaim and dependency boundaries.