Skip to content

feat(promote): bind HMAC tokens to config hash + inventory fingerprint #24

Closed

Description

@gesh75

Problem

Phase 3 HMAC tokens (aegis1.<payload>.<mac>) are bound to:

  • approver
  • evidence bundle integrity.sha256
  • expiry
  • nonce

That is enough to kill a token when the sealed bundle changes. It is not enough to make the approval claims explicit, and it does not give G2/G3 a first-class way to refuse an approved token after the target inventory has moved.

External review (X / @ThorCommerce): "Bind the token to the config hash and target inventory version. An approved token shouldn't survive a change to either one."

That is the correct binding.

Why it matters

TOCTOU: validate against inventory N, mint a token for bundle B_N, inventory becomes N+1, promote B_N onto the new target. v1 still verifies because it only checks the bundle hash.

A config-only or inventory-only mismatch should also fail closed with a specific reason, not a generic bundle-hash miss.

Recommended fix

Add token v2 claims. Keep v1 verifiable until expiry.

{
  "a": "<approver>",
  "b": "<bundle sha256>",
  "c": "<config sha256>",
  "inv": "<inventory sha256>",
  "exp": 0,
  "n": "<nonce>",
  "v": 2
}
  • c = SHA-256 of canonical {device, vendor, config} rows from change.generated_configs
  • inv = SHA-256 of canonical {topology, engine, node_count, devices[]} from twin + device list
  • verify_approval() on v2 requires c and inv to match the values supplied at verify time
  • gate.evaluate() derives c from the bundle and inv from the bundle unless the caller passes live inventory_sha256
  • live inventory mismatch → deny, even if the bundle hash still matches
  • v1 tokens keep working (approver + bundle hash + expiry only)
  • mint without c/inv still emits v1 so existing callers do not break
  • mint_token_for_bundle() and POST /api/approve/mint with a bundle emit v2

Tests

  • v1 mint/verify unchanged
  • v2 mint from bundle verifies against the same bundle
  • v2 + different config hash → deny (token is not bound to this config hash)
  • v2 + different inventory hash → deny (token is not bound to this inventory)
  • v2 + live inventory N+1 at the gate → deny
  • v2 with missing c/inv at mint → TokenError
  • unknown v → deny
  • promote P8 still accepts a bound token (v1 or v2)

Honesty

Community/sim with no AEGIS_APPROVE_KEY stays asserted-unverified.
This is not YubiKey PIV and not a live CMDB inventory connector. The inventory fingerprint is derived from the twin record in the bundle unless the caller supplies a live digest at promote time.

Out of scope

  • Hardware PIV signer
  • SSH/NETCONF live connector
  • Inventing a new inventory_rev field on the evidence schema (use twin topology + device list; add twin.inventory_rev later if a source of truth exists)

Activity

  1. gesh75 commented on Aug 30, 2026

    @gesh75
    OwnerAuthor

    Implementation started on #25 (feat/hmac-v2-inventory-binding).

    Core already on the branch: tokens.py v2 claims (c, inv), gate.evaluate(live_inventory_sha256=...), promote() records the live digest.

    Local verification before the remaining test/docs/serve commit:

    • python -m aegis.tests.tokens_test — PASS (33)
    • python -m aegis.tests.promote_test 200 — PASS
    • python -m aegis.tests.api_test — PASS (48)

    After merge: tag v0.2.1 so the X reply can point at the cut.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions