Skip to content

fix(fetch+chain): realign CLI to live Ed25519 well-known + correct audit path (P0)#16

Open
hizrianraz wants to merge 2 commits into
mainfrom
fix/audit-verify-p0-contract-20260615
Open

fix(fetch+chain): realign CLI to live Ed25519 well-known + correct audit path (P0)#16
hizrianraz wants to merge 2 commits into
mainfrom
fix/audit-verify-p0-contract-20260615

Conversation

@hizrianraz

@hizrianraz hizrianraz commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Two P0 bugs that caused every live CLI invocation to fail.

P0-1: well-known key schema mismatch

  • fetch_public_key() read hmac_key_b64 which no longer exists in the live doc
  • Live doc at https://ainfera.ai/.well-known/ainfera-public-key.json now returns {"alg":"Ed25519","public_key_pem":"...","public_key_b64":"...","fingerprint":"..."}
  • Fix: returns a new PublicKeyDoc dataclass; --pinned-key-fingerprint pins against the server-published hex fingerprint

P0-2: audit-chain endpoint path returns 404

  • fetch_chain() called GET /v1/agents/{id}/audit-chain — dead path (404 in prod)
  • Live public endpoint is GET /v1/audit/public?agent={id}&since_seq={cursor}
  • API spec: since_seq activates ascending cursor mode; without it the feed is newest-first and not paginatable
  • Pagination starts at since_seq=0, advances cursor to page[-1]["seq"] each page
  • fetch_event() corrected similarly (uses since_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:

  • PublicAuditEvent dataclass matching the live feed shape (event_hash, agent_name, seq, event_type, created_at, etc.)
  • verify_public_chain() — checks sequential integrity and event_hash format
  • render_event() and render_chain_result() updated to handle both full and masked events
  • AuditEvent, verify_chain(), and the bundle command unchanged

Separate findings — NOT fixed here (needs_founder_or_api)

  1. verify_sigstore() trusts producer-supplied verified flag — no real Rekor check. Security weakness; needs sigstore-python wired to Rekor (AIN-46). Not fake-fixed.
  2. Annex IV JSON export HMAC key mismatch — well-known no longer serves HMAC key; JSON exports have legacy hmac_signature fields. Server needs to add Ed25519 signatures to exports, or CLI needs --hmac-key-file. Left with an inline note in cli.py.

Verification (live)

$ uv run ainfera-verify chain manwe
✅ Fetched 451 AuditEvents (alg: Ed25519, fingerprint: e141b750...)
✅ Chain intact — 451 events consistency-checked

$ uv run ainfera-verify chain varda
✅ Fetched 4415 AuditEvents
✅ Chain intact — 4415 events consistency-checked

$ uv run ainfera-verify event manwe 449  -> renders event card without error

$ uv run pytest tests/ -q  ->  34 passed
$ uv run ruff check ainfera_verify/  ->  All checks passed!

🤖 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_key now parses the live Ed25519 well-known document (PublicKeyDoc, server fingerprint for --pinned-key-fingerprint) instead of the removed hmac_key_b64 field, and fetch_chain / fetch_event call GET /v1/audit/public with since_seq ascending pagination instead of the 404 /v1/agents/{id}/audit-chain paths.

Live chain and event commands use masked PublicAuditEvent rows and verify_public_chain (strict seq, event_hash hex 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 calls verify_chain with 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-test job (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

    • Added lightweight public audit chain verification for consistency checks
    • Introduced Ed25519 well-known key verification support
    • CLI now supports public audit chain verification
  • Chores

    • Added automated CI/CD workflow with linting and testing

hizrianraz and others added 2 commits June 15, 2026 14:04
.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>
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Migrates the ainfera-verify tool from authenticated HMAC-based audit chain access to public unauthenticated Ed25519 endpoints. Introduces PublicKeyDoc and PublicAuditEvent dataclasses, rewrites all three fetch functions to target /v1/audit/public, adds verify_public_chain for lightweight chain consistency checking, generalizes rendering to handle both event shapes, and rewires the CLI chain, event, and bundle commands accordingly. Adds a GitHub Actions CI job for linting and testing.

Changes

Public masked feed migration

Layer / File(s) Summary
PublicKeyDoc, PublicAuditEvent dataclasses and fetch functions
ainfera_verify/fetch.py
Introduces PublicKeyDoc and PublicAuditEvent dataclasses; rewrites fetch_public_key to return PublicKeyDoc with field validation and fingerprint pinning; rewrites fetch_chain to paginate /v1/audit/public with a since_seq cursor; rewrites fetch_event to query the public feed window and scan for a seq match.
verify_public_chain and ChainVerifyResult.is_public_feed
ainfera_verify/chain.py
Adds is_public_feed: bool = False field to ChainVerifyResult; implements verify_public_chain that sorts events by seq, enforces strict monotonic sequence, validates 64-char hex event_hash format, tracks tip_hash, and returns with is_public_feed=True.
Generalized AnyEvent rendering
ainfera_verify/render.py
Adds AnyEvent type alias; updates render_chain_result to vary success label on is_public_feed, print tip_hash, and derive timestamps from either created_at or timestamp; rewrites render_event to handle both AuditEvent and PublicAuditEvent shapes, adds key_doc display, receipt URL, and masked-payload note.
CLI chain/event/bundle command rewiring
ainfera_verify/cli.py
Adds verify_public_chain and BundleError imports; removes crypto-verifier imports; rewires chain to call verify_public_chain, event to fetch-and-render without local signature verification, and bundle .json path to call verify_chain(events, b"", ...); adjusts help text for Ed25519.
CI lint-and-test job and test cleanup
.github/workflows/ci.yml, tests/test_ed25519_signatures.py, tests/test_export_json_cli.py
Adds GitHub Actions job running ruff format --check, ruff check, and pytest; reformats _keypair() public-bytes call to multi-line explicit args; consolidates SPECS_FIXTURE path construction to a single expression.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 Hopping down the public trail,
No secret keys, no HMAC veil,
Ed25519 signs with flair,
The masked feed floats through open air,
verify_public_chain hops by—
Tip hash confirmed beneath the sky! 🌿

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive PR description comprehensively covers the what, why, and how of both P0 bugs and implementation details, but lacks vocabulary checks, testing confirmation, and linear ticket reference. Add vocabulary check section with banned terms verification, confirm testing status (existing + new tests), and include Linear ticket reference (AIN-XXX).
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main changes: fixing fetch and chain functions to align with Ed25519 well-known endpoint and the correct audit API path (P0 priority).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/audit-verify-p0-contract-20260615

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

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.

Comment thread ainfera_verify/cli.py
verify_sigstore_sigs=sigstore,
agent_public_key_pem=agent_pem,
)
result = verify_public_chain(events, key_doc)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.

Comment thread ainfera_verify/cli.py
# 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.

Comment thread ainfera_verify/fetch.py
import base64

raw = base64.b64decode(public_key_b64)
return hashlib.sha256(raw).hexdigest()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6e9c133. Configure here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Reject public-feed verifier options that are now no-ops.

--sigstore and --agent-pubkey-file imply 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 win

Do not run JSON export verification with an empty HMAC key.

This branch advertises .json verification and key-pinning inputs, but then intentionally calls verify_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

📥 Commits

Reviewing files that changed from the base of the PR and between 8994bdb and 6e9c133.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • ainfera_verify/chain.py
  • ainfera_verify/cli.py
  • ainfera_verify/fetch.py
  • ainfera_verify/render.py
  • tests/test_ed25519_signatures.py
  • tests/test_export_json_cli.py

Comment thread .github/workflows/ci.yml
Comment on lines +27 to +30
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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")
PY

Repository: 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.yml

Repository: 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 -25

Repository: 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

Comment thread ainfera_verify/chain.py
Comment on lines +13 to +18
``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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Comment thread ainfera_verify/chain.py
if TYPE_CHECKING:
from ainfera_verify.fetch import PublicAuditEvent, PublicKeyDoc

GENESIS_HASH = "0" * 64

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

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

Comment thread ainfera_verify/cli.py
Comment on lines 91 to +93
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment thread ainfera_verify/fetch.py
Comment on lines +3 to +5
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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.

Comment thread ainfera_verify/fetch.py
Comment on lines +90 to +95
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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread ainfera_verify/render.py
Comment on lines +145 to +148
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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
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.

Comment thread ainfera_verify/render.py
Comment on lines +155 to +158
# 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]")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant