fix(fetch+chain): realign CLI to live Ed25519 well-known + correct audit path (P0)#16
fix(fetch+chain): realign CLI to live Ed25519 well-known + correct audit path (P0)#16hizrianraz wants to merge 2 commits into
Conversation
.github/workflows/ci.yml previously ran only a community-file
existence check, meaning ruff format drift and test regressions
went undetected in CI. This PR:
• adds a lint-and-test job (uv sync --extra dev → ruff format
--check → ruff check → pytest -q) matching the release.yml
convention of astral-sh/setup-uv@v7
• applies `ruff format` to the 5 files that had drifted
(ainfera_verify/cli.py, ainfera_verify/fetch.py,
ainfera_verify/render.py, tests/test_ed25519_signatures.py,
tests/test_export_json_cli.py)
• mypy omitted — no [tool.mypy] section in pyproject.toml
All 34 tests pass; ruff format --check and ruff check clean.
Refs: 2026-06-15 repo-debug fleet audit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…audit path
P0-1: fetch_public_key() read hmac_key_b64 which no longer exists; live doc
serves alg=Ed25519 with public_key_pem/public_key_b64/fingerprint. Now
returns PublicKeyDoc dataclass with those fields. Fingerprint pin works
against the server-published value.
P0-2: fetch_chain() called GET /v1/agents/{id}/audit-chain (404). Live
public endpoint is GET /v1/audit/public?agent={id}&since_seq={cursor}.
Pagination: since_seq=0 activates ascending cursor mode per API spec; each
page advances cursor to page[-1]["seq"]. fetch_event() similarly uses
since_seq to locate a single row.
Introduces PublicAuditEvent for the masked public feed (no payload/
previous_hash/signatures) and verify_public_chain() in chain.py which checks
sequential integrity and event_hash format — the most that is verifiable
without auth. Full payload re-hash requires an Annex IV bundle.
AuditEvent, verify_chain(), and the bundle command are preserved unchanged
for offline Annex IV verification.
Verified live: ainfera-verify chain manwe (451 events OK),
ainfera-verify chain varda (4415 events OK),
ainfera-verify event manwe 449 (OK).
34 tests pass, ruff clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughMigrates the ChangesPublic masked feed migration
Sequence DiagramsequenceDiagram
actor User
participant CLI as ainfera-verify CLI
participant fetch as fetch.py
participant chain as chain.py
participant render as render.py
participant API as Ainfera Public API
User->>CLI: ainfera-verify chain <agent_id>
CLI->>fetch: fetch_public_key(well_known_url)
fetch->>API: GET /.well-known/ed25519-key
API-->>fetch: {public_key_pem, fingerprint}
fetch-->>CLI: PublicKeyDoc
CLI->>fetch: fetch_chain(agent_id, base_url)
loop since_seq pagination
fetch->>API: GET /v1/audit/public?since_seq=N
API-->>fetch: list[PublicAuditEvent]
end
fetch-->>CLI: list[PublicAuditEvent]
CLI->>chain: verify_public_chain(events, key_doc)
chain-->>CLI: ChainVerifyResult(is_public_feed=True, tip_hash=...)
CLI->>render: render_chain_result(agent_id, events, result)
render-->>User: console output with public-feed label and tip hash
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issues.
Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.
| verify_sigstore_sigs=sigstore, | ||
| agent_public_key_pem=agent_pem, | ||
| ) | ||
| result = verify_public_chain(events, key_doc) |
There was a problem hiding this comment.
Chain command ignores CLI flags
Medium Severity
The chain command still accepts --sigstore and --agent-pubkey-file, but after switching to verify_public_chain those values are never read. Users who pass them get the same public-feed consistency check with no Sigstore or agent Ed25519 verification.
Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.
| # fail. A future server update should embed the Ed25519 signature in | ||
| # the export (AIN-TODO); until then, use a .zip bundle which carries | ||
| # its own hmac_public_key. See needs_founder_or_api in the PR. | ||
| result = verify_chain(events, b"", verify_sigstore_sigs=sigstore) |
There was a problem hiding this comment.
JSON bundle ignores pin options
Medium Severity
For .json bundle verification, --well-known-url and --pinned-key-fingerprint are still declared but fetch_public_key was removed from that branch. Pinning and well-known key fetch no longer run for JSON exports while the options remain visible.
Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.
| import base64 | ||
|
|
||
| raw = base64.b64decode(public_key_b64) | ||
| return hashlib.sha256(raw).hexdigest() |
There was a problem hiding this comment.
Unused fingerprint helper function
Low Severity
_key_fingerprint_from_b64 is added in this change but never called; fetch_public_key pins using the server fingerprint field only. The helper is dead code.
Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ainfera_verify/cli.py (2)
64-69:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject public-feed verifier options that are now no-ops.
--sigstoreand--agent-pubkey-fileimply stronger signature verification, but the public-feed commands no longer use them. Please fail fast when callers pass them, or remove the options.Suggested fix
def chain( @@ """Fetch and verify a public AuditChain by agent name. Uses the public masked feed (GET /v1/audit/public). This verifies sequential integrity and event_hash format. For full payload re-hash verification use an Annex IV bundle: ``ainfera-verify bundle <path>``. """ + if sigstore or agent_pubkey_file is not None: + console.print( + "[red]Error:[/red] --sigstore/--agent-pubkey-file are not " + "available for the public feed; use an Annex IV bundle for " + "signature verification." + ) + raise typer.Exit(code=2) with Progress( @@ def event( @@ The public feed returns a masked projection (event_hash only, no payload or signatures). Signature verification is not available here; use an Annex IV bundle for cryptographic proof. """ + if agent_pubkey_file is not None: + console.print( + "[red]Error:[/red] --agent-pubkey-file is not used for " + "public-feed events; use an Annex IV bundle for signature " + "verification." + ) + raise typer.Exit(code=2) try:Also applies to: 109-113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ainfera_verify/cli.py` around lines 64 - 69, The sigstore and agent_pubkey_file options in the public-feed verify command are no longer used but remain defined in the CLI interface, making them misleading to users. Either remove these option parameters entirely from the function signature (including both the anchor location at lines 64-69 and the sibling location at lines 109-113), or add validation logic that explicitly rejects these options when passed by checking if sigstore is True or agent_pubkey_file is not None and raising an error with a clear message explaining they are no longer supported. The recommended approach is to remove the unused parameters entirely to avoid confusion.
135-147:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not run JSON export verification with an empty HMAC key.
This branch advertises
.jsonverification and key-pinning inputs, but then intentionally callsverify_chain(events, b"", ...); the inline comment says HMAC checks will fail. That makes the path fail by construction and ignores the advertised trust inputs.Suggested low-risk fix if JSON exports are not currently verifiable
path: str = typer.Argument( ..., - help="Path to Annex IV .zip bundle or minimal .json export from the API.", + help=( + "Path to Annex IV .zip bundle. Minimal .json exports are not " + "currently cryptographically verifiable." + ), @@ if path_lower.endswith(".json"): - console.print("Parsing Annex IV JSON export...") - try: - manifest, events = load_annex_iv_json(path) - except BundleError as exc: - console.print(f"[red]Error:[/red] {exc}") - raise typer.Exit(code=2) from exc - agent_id = str(manifest.get("agent_id", "unknown")) - console.print(f"[green]✅ Loaded {len(events)} events for agent {agent_id}[/green]") - console.print("Verifying chain...") - # NOTE: The well-known endpoint now publishes an Ed25519 key, not an - # HMAC key. Annex IV JSON exports carry per-event hmac_signature from - # the legacy HMAC scheme. Pass an empty key here — HMAC checks will - # fail. A future server update should embed the Ed25519 signature in - # the export (AIN-TODO); until then, use a .zip bundle which carries - # its own hmac_public_key. See needs_founder_or_api in the PR. - result = verify_chain(events, b"", verify_sigstore_sigs=sigstore) - render_chain_result(agent_id, events, result) - raise typer.Exit(code=0 if result.ok else 1) + console.print( + "[red]Error:[/red] Minimal JSON exports cannot be " + "cryptographically verified against the current Ed25519 " + "well-known key yet. Use an Annex IV .zip bundle." + ) + raise typer.Exit(code=2)Also applies to: 163-170
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ainfera_verify/cli.py` around lines 135 - 147, The function signature defines well_known_url and pinned_key_fingerprint parameters to support JSON export verification with key-pinning, but the verify_chain function is being called with an empty HMAC key (b"") which causes HMAC checks to fail intentionally. Replace the empty key argument in the verify_chain call with the actual pinned_key_fingerprint (or the key derived from well_known_url) to make JSON export verification work with the advertised trust inputs instead of failing by construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 27-30: Pin both GitHub Actions to specific commit SHAs instead of
using version tags. For the actions/checkout action on line 27, replace the tag
reference with a pinned commit SHA and add configuration to disable credential
persistence using persist-credentials: false in the with section. For the
astral-sh/setup-uv action on line 30, replace the tag reference with a pinned
commit SHA. This improves CI supply-chain security and token hygiene by
preventing unnecessary credential exposure.
In `@ainfera_verify/chain.py`:
- Line 35: The GENESIS_HASH constant in ainfera_verify/chain.py is currently set
to "0" * 64, but the shared audit-chain contract and API specifications require
the literal frozen genesis anchor string. Replace the value of GENESIS_HASH with
the exact string "ainfera-os:genesis" to ensure verification compatibility with
chains produced using the frozen API contract and comply with the audit-chain
genesis anchor requirements.
- Around line 13-18: The docstring for the `verify_public_chain()` function
claims it checks chain height against the `/v1/audit/height` endpoint, but the
actual implementation does not perform this check. Either implement the height
validation logic in the function to match the documented behavior by calling the
`/v1/audit/height` endpoint and comparing the returned height to the chain, or
update the docstring to accurately describe what the function actually does
(validate seq monotonicity and event_hash format only). This ensures the
documentation and implementation remain consistent.
In `@ainfera_verify/cli.py`:
- Around line 91-93: The console message "Verifying hash chain..." overclaims
the scope of verification being performed. The verify_public_chain function only
validates public-feed consistency and hash format, not a complete hash chain.
Update the console.print message to accurately reflect what verify_public_chain
actually does, such as messaging that focuses on verifying the public feed and
hash format rather than claiming full hash chain verification.
In `@ainfera_verify/fetch.py`:
- Around line 90-95: The current validation only compares the fingerprint field
from the response against the pinned_fingerprint, but does not verify that the
actual public key bytes match. Use the _key_fingerprint_from_b64 helper function
to compute the SHA-256 fingerprint directly from the public_key_b64 field in the
response, then compare this computed fingerprint with the pinned_fingerprint to
ensure the public key itself has not been tampered with. This validation must be
applied at both the anchor location (lines 90-95) and the sibling location
(lines 132-156) where public key validation occurs.
- Around line 3-5: Update the docstring at lines 3-5 in ainfera_verify/fetch.py
to accurately reflect the actual trust guarantees of the public feed
verification. Remove claims that each event hash is checked against its
predecessor and Ed25519 signature, since the public feed omits previous_hash and
per-event signatures. Instead, reword the docstring to accurately state that
verify_public_chain() only validates sequence monotonicity and hash format.
In `@ainfera_verify/render.py`:
- Around line 145-148: The key fingerprint displayed in the render function is
being truncated using the _short() function, which leaves only 10 hex characters
and makes it difficult for users to reliably compare the fingerprint out of band
as a trust anchor. Remove the _short() function call wrapping
key_doc.fingerprint on the "key fingerprint:" row and display the full
fingerprint value instead to enable unambiguous manual comparison by users.
- Around line 155-158: The condition checking `not hmac_ok` and `not ed25519_ok`
treats both the "not checked" and "checked and invalid" cases the same way,
since both result in falsy values. Modify the condition in the signature check
block to explicitly distinguish between these two states by checking if the
values are None (indicating "not checked") rather than just checking for falsy
values. This will ensure the "no sig check" message is only shown when
signatures truly weren't checked, allowing the UI to properly display both an
invalid signature row and the "no sig check" note when appropriate.
---
Outside diff comments:
In `@ainfera_verify/cli.py`:
- Around line 64-69: The sigstore and agent_pubkey_file options in the
public-feed verify command are no longer used but remain defined in the CLI
interface, making them misleading to users. Either remove these option
parameters entirely from the function signature (including both the anchor
location at lines 64-69 and the sibling location at lines 109-113), or add
validation logic that explicitly rejects these options when passed by checking
if sigstore is True or agent_pubkey_file is not None and raising an error with a
clear message explaining they are no longer supported. The recommended approach
is to remove the unused parameters entirely to avoid confusion.
- Around line 135-147: The function signature defines well_known_url and
pinned_key_fingerprint parameters to support JSON export verification with
key-pinning, but the verify_chain function is being called with an empty HMAC
key (b"") which causes HMAC checks to fail intentionally. Replace the empty key
argument in the verify_chain call with the actual pinned_key_fingerprint (or the
key derived from well_known_url) to make JSON export verification work with the
advertised trust inputs instead of failing by construction.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 46e86f84-b293-4893-afda-a99d6be3c065
📒 Files selected for processing (7)
.github/workflows/ci.ymlainfera_verify/chain.pyainfera_verify/cli.pyainfera_verify/fetch.pyainfera_verify/render.pytests/test_ed25519_signatures.pytests/test_export_json_cli.py
| - uses: actions/checkout@v6 | ||
|
|
||
| - name: Install uv | ||
| uses: astral-sh/setup-uv@v7 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify all workflow `uses:` refs are SHA-pinned and checkout disables credential persistence.
set -euo pipefail
echo "== uses refs not pinned to 40-char SHA =="
rg -n '^\s*uses:\s*[^@]+@(?!(?:[a-f0-9]{40})\b).+$' .github/workflows || true
echo
echo "== checkout steps without persist-credentials: false nearby =="
python - <<'PY'
from pathlib import Path
import re
for wf in Path(".github/workflows").glob("*.yml"):
lines = wf.read_text().splitlines()
for i, line in enumerate(lines):
if re.search(r'uses:\s*actions/checkout@', line):
window = "\n".join(lines[i:i+8])
if "persist-credentials: false" not in window:
print(f"{wf}:{i+1} checkout without persist-credentials: false")
PYRepository: ainfera-ai/verify
Length of output: 824
🏁 Script executed:
#!/bin/bash
# Check if actions are pinned to commit SHA (40-char hex) or use version tags
rg -n 'uses:\s*[^@]+@[a-f0-9]{40}' .github/workflows/ci.yml || echo "No SHA-pinned actions found"
echo "---"
rg -n 'uses:' .github/workflows/ci.ymlRepository: ainfera-ai/verify
Length of output: 206
🏁 Script executed:
# Read the actual ci.yml file to inspect lines around 27-30
head -45 .github/workflows/ci.yml | tail -25Repository: ainfera-ai/verify
Length of output: 688
Pin GitHub Actions by commit SHA and disable checkout credential persistence.
Lines 27 and 30 use tag-based action refs, and checkout at line 27 keeps credentials by default. This weakens CI supply-chain hardening and token hygiene.
Suggested patch
- uses: actions/checkout@v6
+ with:
+ persist-credentials: false
- name: Install uv
- uses: astral-sh/setup-uv@v7
+ uses: astral-sh/setup-uv@<PINNED_SHA>🧰 Tools
🪛 zizmor (1.25.2)
[warning] 27-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 27-27: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 30-30: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 27 - 30, Pin both GitHub Actions to
specific commit SHAs instead of using version tags. For the actions/checkout
action on line 27, replace the tag reference with a pinned commit SHA and add
configuration to disable credential persistence using persist-credentials: false
in the with section. For the astral-sh/setup-uv action on line 30, replace the
tag reference with a pinned commit SHA. This improves CI supply-chain security
and token hygiene by preventing unnecessary credential exposure.
Source: Linters/SAST tools
| ``verify_public_chain`` | ||
| Lightweight verification against the public masked feed. The public API | ||
| (GET /v1/audit/public) omits payload and previous_hash for privacy, so | ||
| payload re-hash is not possible. This function checks what *is* available: | ||
| sequential seq ordering, non-empty event_hash fields, and the chain height | ||
| against ``/v1/audit/height``. It is a consistency check, not a full |
There was a problem hiding this comment.
Don’t document a public height check that is not performed.
The module doc says verify_public_chain() checks chain height against /v1/audit/height, but the implementation only validates seq monotonicity and event_hash format. Either add that endpoint check or remove the claim to keep the public-feed trust scope accurate.
Suggested wording
- sequential seq ordering, non-empty event_hash fields, and the chain height
- against ``/v1/audit/height``. It is a consistency check, not a full
+ monotonically increasing seq values and valid event_hash fields. It is a
+ consistency check, not a full🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/chain.py` around lines 13 - 18, The docstring for the
`verify_public_chain()` function claims it checks chain height against the
`/v1/audit/height` endpoint, but the actual implementation does not perform this
check. Either implement the height validation logic in the function to match the
documented behavior by calling the `/v1/audit/height` endpoint and comparing the
returned height to the chain, or update the docstring to accurately describe
what the function actually does (validate seq monotonicity and event_hash format
only). This ensures the documentation and implementation remain consistent.
| if TYPE_CHECKING: | ||
| from ainfera_verify.fetch import PublicAuditEvent, PublicKeyDoc | ||
|
|
||
| GENESIS_HASH = "0" * 64 |
There was a problem hiding this comment.
Use the frozen genesis anchor string.
The first-link hash input currently uses "0" * 64, but the shared audit-chain contract requires the literal genesis anchor. This will break verification against chains produced with the frozen API contract.
Proposed fix
-GENESIS_HASH = "0" * 64
+GENESIS_HASH = "ainfera-os:genesis"As per coding guidelines, “The audit-chain genesis anchor string must be exactly ainfera-os:genesis” and must match the API contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/chain.py` at line 35, The GENESIS_HASH constant in
ainfera_verify/chain.py is currently set to "0" * 64, but the shared audit-chain
contract and API specifications require the literal frozen genesis anchor
string. Replace the value of GENESIS_HASH with the exact string
"ainfera-os:genesis" to ensure verification compatibility with chains produced
using the frozen API contract and comply with the audit-chain genesis anchor
requirements.
Source: Coding guidelines
| console.print("Verifying hash chain...") | ||
|
|
||
| result = verify_chain( | ||
| events, | ||
| key, | ||
| verify_sigstore_sigs=sigstore, | ||
| agent_public_key_pem=agent_pem, | ||
| ) | ||
| result = verify_public_chain(events, key_doc) |
There was a problem hiding this comment.
Avoid overclaiming hash-chain verification for the public feed.
Line 93 calls verify_public_chain, which only checks public-feed consistency and hash format, so “Verifying hash chain...” overstates the guarantee.
Suggested fix
- console.print("Verifying hash chain...")
+ console.print("Checking public-feed consistency...")
result = verify_public_chain(events, key_doc)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| console.print("Verifying hash chain...") | |
| result = verify_chain( | |
| events, | |
| key, | |
| verify_sigstore_sigs=sigstore, | |
| agent_public_key_pem=agent_pem, | |
| ) | |
| result = verify_public_chain(events, key_doc) | |
| console.print("Checking public-feed consistency...") | |
| result = verify_public_chain(events, key_doc) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/cli.py` around lines 91 - 93, The console message "Verifying
hash chain..." overclaims the scope of verification being performed. The
verify_public_chain function only validates public-feed consistency and hash
format, not a complete hash chain. Update the console.print message to
accurately reflect what verify_public_chain actually does, such as messaging
that focuses on verifying the public feed and hash format rather than claiming
full hash chain verification.
| The CLI treats the API as a delivery mechanism only. Every event hash is | ||
| checked locally against its predecessor and against the server-signed Ed25519 | ||
| signature, so a malicious response cannot mint a passing chain. |
There was a problem hiding this comment.
Remove the public-feed signature/predecessor guarantee.
The public feed omits previous_hash and per-event signatures, and verify_public_chain() only checks sequence monotonicity plus hash format. This docstring currently overstates the trust guarantee by saying each event hash is checked against its predecessor and Ed25519 signature.
Suggested wording
-The CLI treats the API as a delivery mechanism only. Every event hash is
-checked locally against its predecessor and against the server-signed Ed25519
-signature, so a malicious response cannot mint a passing chain.
+The CLI treats the public API as a delivery mechanism for a masked feed. Public
+events do not include payload, previous_hash, or per-event signatures, so the
+CLI can only consistency-check sequence ordering and hash format. Full
+cryptographic proof requires an Annex IV bundle.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| The CLI treats the API as a delivery mechanism only. Every event hash is | |
| checked locally against its predecessor and against the server-signed Ed25519 | |
| signature, so a malicious response cannot mint a passing chain. | |
| The CLI treats the public API as a delivery mechanism for a masked feed. Public | |
| events do not include payload, previous_hash, or per-event signatures, so the | |
| CLI can only consistency-check sequence ordering and hash format. Full | |
| cryptographic proof requires an Annex IV bundle. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/fetch.py` around lines 3 - 5, Update the docstring at lines
3-5 in ainfera_verify/fetch.py to accurately reflect the actual trust guarantees
of the public feed verification. Remove claims that each event hash is checked
against its predecessor and Ed25519 signature, since the public feed omits
previous_hash and per-event signatures. Instead, reword the docstring to
accurately state that verify_public_chain() only validates sequence monotonicity
and hash format.
| def _key_fingerprint_from_b64(public_key_b64: str) -> str: | ||
| """SHA-256 hex fingerprint of the raw Ed25519 key bytes (from base64).""" | ||
| import base64 | ||
|
|
||
| raw = base64.b64decode(public_key_b64) | ||
| return hashlib.sha256(raw).hexdigest() |
There was a problem hiding this comment.
Bind the pinned fingerprint to the actual key bytes.
pinned_fingerprint is compared only to the response’s fingerprint field, so a bad well-known response can repeat the expected fingerprint while changing public_key_pem/public_key_b64. Compute the SHA-256 fingerprint from public_key_b64, require it to match the published fingerprint, and pin against the computed value.
Proposed fix
+import base64
+import binascii
import hashlib
from dataclasses import dataclass
from typing import Any
@@
def _key_fingerprint_from_b64(public_key_b64: str) -> str:
"""SHA-256 hex fingerprint of the raw Ed25519 key bytes (from base64)."""
- import base64
-
- raw = base64.b64decode(public_key_b64)
+ try:
+ raw = base64.b64decode(public_key_b64, validate=True)
+ except (binascii.Error, ValueError) as exc:
+ raise FetchError("public key document has invalid 'public_key_b64'") from exc
return hashlib.sha256(raw).hexdigest()
@@
if not public_key_pem:
raise FetchError(
f"public key document missing 'public_key_pem' at {well_known_url} "
f"(got fields: {sorted(data.keys())})"
)
+ if alg != "Ed25519":
+ raise FetchError(f"public key document has unsupported alg '{alg}' at {well_known_url}")
+ if not public_key_b64:
+ raise FetchError(f"public key document missing 'public_key_b64' at {well_known_url}")
if not fingerprint:
raise FetchError(f"public key document missing 'fingerprint' at {well_known_url}")
+ computed_fingerprint = _key_fingerprint_from_b64(public_key_b64)
+ if fingerprint.lower() != computed_fingerprint:
+ raise FetchError(
+ "public key document fingerprint does not match public_key_b64: "
+ f"expected {computed_fingerprint}, got {fingerprint}"
+ )
+
if pinned_fingerprint:
expected = pinned_fingerprint.strip().lower()
- if fingerprint.lower() != expected:
+ if computed_fingerprint != expected:
raise FetchError(
- f"pinned key fingerprint mismatch: expected {expected}, got {fingerprint}"
+ f"pinned key fingerprint mismatch: expected {expected}, got {computed_fingerprint}"
)
@@
public_key_pem=public_key_pem,
public_key_b64=public_key_b64,
- fingerprint=fingerprint,
+ fingerprint=computed_fingerprint,
)Also applies to: 132-156
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/fetch.py` around lines 90 - 95, The current validation only
compares the fingerprint field from the response against the pinned_fingerprint,
but does not verify that the actual public key bytes match. Use the
_key_fingerprint_from_b64 helper function to compute the SHA-256 fingerprint
directly from the public_key_b64 field in the response, then compare this
computed fingerprint with the pinned_fingerprint to ensure the public key itself
has not been tampered with. This validation must be applied at both the anchor
location (lines 90-95) and the sibling location (lines 132-156) where public key
validation occurs.
| if key_doc is not None: | ||
| table.add_row("", "") | ||
| table.add_row("key alg:", key_doc.alg) | ||
| table.add_row("key fingerprint:", _short(key_doc.fingerprint)) |
There was a problem hiding this comment.
Show the full key fingerprint.
key_doc.fingerprint is the trust anchor users pin or compare out of band; _short() leaves only 10 hex chars and makes manual comparison ambiguous.
Suggested fix
if key_doc is not None:
table.add_row("", "")
table.add_row("key alg:", key_doc.alg)
- table.add_row("key fingerprint:", _short(key_doc.fingerprint))
+ table.add_row("key fingerprint:", key_doc.fingerprint)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if key_doc is not None: | |
| table.add_row("", "") | |
| table.add_row("key alg:", key_doc.alg) | |
| table.add_row("key fingerprint:", _short(key_doc.fingerprint)) | |
| if key_doc is not None: | |
| table.add_row("", "") | |
| table.add_row("key alg:", key_doc.alg) | |
| table.add_row("key fingerprint:", key_doc.fingerprint) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/render.py` around lines 145 - 148, The key fingerprint
displayed in the render function is being truncated using the _short() function,
which leaves only 10 hex characters and makes it difficult for users to reliably
compare the fingerprint out of band as a trust anchor. Remove the _short()
function call wrapping key_doc.fingerprint on the "key fingerprint:" row and
display the full fingerprint value instead to enable unambiguous manual
comparison by users.
| # Note if this is a masked public-feed event | ||
| if payload is None and not hmac_ok and not ed25519_ok: | ||
| table.add_row("", "") | ||
| table.add_row("[dim]note:[/dim]", "[dim]public feed — payload masked, no sig check[/dim]") |
There was a problem hiding this comment.
Distinguish “not checked” from “checked and invalid”.
not hmac_ok and not ed25519_ok are also true when a caller passed False, so the UI can show both an invalid signature row and “no sig check”.
Suggested fix
- if payload is None and not hmac_ok and not ed25519_ok:
+ if (
+ payload is None
+ and hmac_ok is None
+ and ed25519_ok is None
+ and sigstore_ok is None
+ ):
table.add_row("", "")
table.add_row("[dim]note:[/dim]", "[dim]public feed — payload masked, no sig check[/dim]")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ainfera_verify/render.py` around lines 155 - 158, The condition checking `not
hmac_ok` and `not ed25519_ok` treats both the "not checked" and "checked and
invalid" cases the same way, since both result in falsy values. Modify the
condition in the signature check block to explicitly distinguish between these
two states by checking if the values are None (indicating "not checked") rather
than just checking for falsy values. This will ensure the "no sig check" message
is only shown when signatures truly weren't checked, allowing the UI to properly
display both an invalid signature row and the "no sig check" note when
appropriate.


Summary
Two P0 bugs that caused every live CLI invocation to fail.
P0-1: well-known key schema mismatch
fetch_public_key()readhmac_key_b64which no longer exists in the live dochttps://ainfera.ai/.well-known/ainfera-public-key.jsonnow returns{"alg":"Ed25519","public_key_pem":"...","public_key_b64":"...","fingerprint":"..."}PublicKeyDocdataclass;--pinned-key-fingerprintpins against the server-published hex fingerprintP0-2: audit-chain endpoint path returns 404
fetch_chain()calledGET /v1/agents/{id}/audit-chain— dead path (404 in prod)GET /v1/audit/public?agent={id}&since_seq={cursor}since_seqactivates ascending cursor mode; without it the feed is newest-first and not paginatablesince_seq=0, advances cursor topage[-1]["seq"]each pagefetch_event()corrected similarly (usessince_seq=seq-1)New: PublicAuditEvent + verify_public_chain()
The public feed is a masked projection (payload and previous_hash omitted for privacy). Full payload re-hash is not possible without auth. Added:
PublicAuditEventdataclass matching the live feed shape (event_hash,agent_name,seq,event_type,created_at, etc.)verify_public_chain()— checks sequential integrity and event_hash formatrender_event()andrender_chain_result()updated to handle both full and masked eventsAuditEvent,verify_chain(), and thebundlecommand unchangedSeparate findings — NOT fixed here (needs_founder_or_api)
verify_sigstore()trusts producer-suppliedverifiedflag — no real Rekor check. Security weakness; needssigstore-pythonwired to Rekor (AIN-46). Not fake-fixed.hmac_signaturefields. Server needs to add Ed25519 signatures to exports, or CLI needs--hmac-key-file. Left with an inline note incli.py.Verification (live)
🤖 Generated with Claude Code
Note
Medium Risk
Changes what “verified” means for live CLI (consistency check vs full crypto) and leaves JSON Annex IV HMAC verification broken by design until server/export updates; bundle zip path remains the trusted offline path.
Overview
Fixes two production breakages:
fetch_public_keynow parses the live Ed25519 well-known document (PublicKeyDoc, serverfingerprintfor--pinned-key-fingerprint) instead of the removedhmac_key_b64field, andfetch_chain/fetch_eventcallGET /v1/audit/publicwithsince_seqascending pagination instead of the 404/v1/agents/{id}/audit-chainpaths.Live
chainandeventcommands use maskedPublicAuditEventrows andverify_public_chain(strictseq,event_hashhex shape)—not full payload re-hash or per-event signatures.render_*distinguishes public-feed “consistency-checked” vs full bundle verification;bundle.zip path is unchanged. JSON export verification still callsverify_chainwith an empty HMAC key and an explicit note that legacy HMAC in exports won’t verify until API/export alignment.CI adds a
lint-and-testjob (uv, ruff format/check, pytest).Reviewed by Cursor Bugbot for commit 6e9c133. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit
New Features
Chores