Skip to content

fix(node): harden Arweave anchoring and add verification (#26) - #224

Open
Gravirei wants to merge 15 commits into
Gitlawb:mainfrom
Gravirei:fix/issue-26-harden-arweave-anchoring-verification
Open

fix(node): harden Arweave anchoring and add verification (#26)#224
Gravirei wants to merge 15 commits into
Gitlawb:mainfrom
Gravirei:fix/issue-26-harden-arweave-anchoring-verification

Conversation

@Gravirei

@Gravirei Gravirei commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Replace the Irys upload path with a generic Bundler endpoint, capture the pusher RFC 9421 HTTP Signature in ref certificates, add monotonic sequence numbers with SHA-256 chaining to certificate chains, and expose a /api/v1/arweave/verify/{tx_id} endpoint that fetches the anchor from Arweave and validates the embedded certificate chain.

Motivation & context

Closes #26

Kind of change

  • Bug fix
  • Feature
  • Security fix
  • Docs
  • Tests / CI
  • Refactor (no behavior change)
  • Breaking or protocol change (issue required first)

What changed

gitlawb-node

  • config.rsGITLAWB_IRYS_URL renamed to GITLAWB_BUNDLER_URL; GITLAWB_ARWEAVE_GATEWAY added.
  • arweave.rs — Upload endpoint changed from {url}/upload to {url}/v1/tx; content-type application/octet-stream; header x-bundler-tags; Bundler response parsing. RefAnchor now carries an optional embedded RefCertificate. New verify_anchor() function fetches from gateway, extracts the cert, verifies the node Ed25519 signature and checks prev hash linkage against the local DB.
  • db/mod.rsRefCertificate gains seq (i64), prev (String), pusher_sig (Option). ArweaveAnchor gains status, deadline_height, receipt_sig, cert_id; irys_tx_idarweave_tx_id. Migration v11 upgrades existing databases. RecordAnchorInputV2 replaces the old input struct. Added get_most_recent_cert(), confirm_arweave_anchor(), fail_arweave_anchor(), list_pending_anchors().
  • cert.rsissue_ref_certificate accepts pusher_sig, chains from the previous cert via SHA-256 prev hash, builds a v2 signing payload.
  • auth/mod.rsrequire_signature middleware injects PusherSignature extension (base64-encoded Ed25519 sig bytes).
  • api/repos.rsgit_receive_pack extracts PusherSignature, passes it to cert issuance. RefAnchor construction fetches latest cert from DB. Uses RecordAnchorInputV2.
  • api/arweave.rs — New GET /api/v1/arweave/verify/{tx_id} endpoint.
  • server.rs — Verify route mounted.
  • test_support.rs, api/events.rs — Updated RefCertificate initializers for new fields.

How a reviewer can verify

DATABASE_URL=postgresql://gitlawb:changeme@172.19.0.2:5432/gitlawb cargo test --package gitlawb-node

All 495 tests pass.

Before you request review

  • Scope is one logical change; no unrelated churn
  • cargo test --workspace passes locally (495 passed)
  • New behavior is covered by tests (12 new DB tests, 10 arweave tests)
  • cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings are clean
  • Commit titles use Conventional Commits (fix(...))
  • Docs / .env.example updated if behavior or config changed (or N/A)
  • Checked existing PRs so this isnt a duplicate

Protocol & signing impact

  • Touches DID / did:key, Ed25519 / RFC 9421 signatures, UCAN, ref certs, or P2P wire formats
  • Discussed in an issue before implementation
  • Backward-compatible with existing nodes and previously signed history — new columns have defaults, v11 migration is additive, old Irys fields are renamed gracefully.

Notes for reviewers

The gateway_url field in RecordAnchorInputV2 is passed through by callers but not persisted in the DB — it is used by the caller to construct the Arweave URL at query time. The cert_id column on arweave_anchors is reserved for a follow-up linking pass.

Summary by CodeRabbit

  • New Features

    • Added Arweave anchor verification via GET /api/v1/arweave/verify/:tx_id, returning validity, error details, and optional embedded certificate info.
    • Ref-update anchoring can now embed a signed, chain-linked certificate (including optional HTTP signature evidence).
  • Improvements

    • Permanent anchoring now uses a configurable bundler flow plus gateway-based resolution for verification.
    • Certificate issuance and listing now expose seq, prev, and related optional HTTP signature fields.
    • Arweave anchoring now records per-anchor lifecycle (pending → confirm/fail).
  • Configuration

    • If the bundler URL is unset, startup falls back to the legacy Irys URL with a deprecation warning.

Copilot AI review requested due to automatic review settings July 20, 2026 09:44

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This change replaces Irys uploads with bundler requests, embeds chained ref certificates and pusher signature metadata, adds anchor lifecycle persistence, and exposes gateway-based Arweave transaction verification.

Changes

Arweave integrity flow

Layer / File(s) Summary
Certificate chain and pusher signature
crates/gitlawb-node/src/auth/mod.rs, crates/gitlawb-node/src/cert.rs, crates/gitlawb-node/src/db/mod.rs, crates/gitlawb-node/src/api/repos.rs, crates/gitlawb-node/src/api/certs.rs
Verified pusher signatures are propagated into append-only certificates with sequence, predecessor hashes, RFC 9421 metadata, and expanded API responses.
Bundler anchoring and anchor lifecycle
crates/gitlawb-node/src/arweave.rs, crates/gitlawb-node/src/api/repos.rs, crates/gitlawb-node/src/config.rs, crates/gitlawb-node/src/main.rs, crates/gitlawb-node/src/db/mod.rs, crates/gitlawb-node/src/server.rs
Anchors use bundler /v1/tx requests, configurable bundler and gateway settings, embedded certificates, and pending/confirmed/failed persistence.
Anchor verification API
crates/gitlawb-node/src/arweave.rs, crates/gitlawb-node/src/api/arweave.rs, crates/gitlawb-node/src/server.rs
Gateway payloads are fetched and checked for certificate signatures and predecessor linkage at GET /api/v1/arweave/verify/:tx_id.
Validation and compatibility updates
crates/gitlawb-node/src/arweave.rs, crates/gitlawb-node/src/db/mod.rs, crates/gitlawb-node/src/api/events.rs, crates/gitlawb-node/src/test_support.rs
Tests and fixtures cover bundler routes, gateway failures, append-only certificates, anchor lifecycle transitions, and expanded certificate fields.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant git_receive_pack
  participant issue_ref_certificate
  participant Bundler
  participant verify_anchor_endpoint
  participant verify_anchor
  participant ArweaveGateway
  participant Db
  Client->>git_receive_pack: Authenticated push request
  git_receive_pack->>issue_ref_certificate: Pass pusher signature and proof
  issue_ref_certificate->>Db: Store chained certificate
  git_receive_pack->>Bundler: POST /v1/tx with certificate anchor
  Bundler-->>git_receive_pack: Return transaction ID
  Client->>verify_anchor_endpoint: GET /api/v1/arweave/verify/{tx_id}
  verify_anchor_endpoint->>verify_anchor: Verify transaction ID
  verify_anchor->>ArweaveGateway: GET gateway/{tx_id}
  ArweaveGateway-->>verify_anchor: Return anchored payload
  verify_anchor->>Db: Load predecessor certificate
  verify_anchor-->>Client: Return validity, errors, and certificate
Loading

Possibly related PRs

  • Gitlawb/node#72: Both PRs modify per-ref anchoring payloads and certificate issuance.
  • Gitlawb/node#149: Both PRs modify certificate listing endpoints and certificate response fields.

Suggested labels: sev:high, kind:security, subsystem:attestation, subsystem:api

Suggested reviewers: jatmn, kevincodex1

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements provider-neutral anchoring, embedded signed certificates, pusher signature persistence, seq/prev chaining, and gateway-based verification.
Out of Scope Changes check ✅ Passed The changes stay focused on Arweave anchoring, verification, auth, schema updates, and related tests, with no clear unrelated churn.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title is concise and accurately summarizes the main change: hardening Arweave anchoring and adding verification.
Description check ✅ Passed The description follows the template and covers summary, motivation, change type, implementation details, verification, and signing impact.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@beardthelion beardthelion added crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior subsystem:identity DID/UCAN, http-sig auth, push authorization subsystem:storage Blob/object store, Arweave, IPFS, archives labels Jul 20, 2026
@Gravirei
Gravirei force-pushed the fix/issue-26-harden-arweave-anchoring-verification branch from 0801800 to bd09c35 Compare July 20, 2026 09:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@crates/gitlawb-node/src/arweave.rs`:
- Around line 351-373: Update the prev-linkage validation in verify_anchor to
fetch and hash the local certificate at sequence c.seq - 1, rather than the
newest certificate returned by get_most_recent_cert. Only perform the comparison
when that predecessor exists, while preserving the existing mismatch error
handling and payload hashing behavior.
- Around line 319-343: Update the signature decoding in the certificate
verification flow to use the URL-safe, no-padding base64 engine matching
node_keypair.sign_b64 output, while preserving the existing 64-byte validation
and error-result handling.
🪄 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

Run ID: fee43d49-2e4b-4bbd-9921-8248f57fea48

📥 Commits

Reviewing files that changed from the base of the PR and between ad7c2b2 and 0801800.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • crates/gitlawb-node/src/api/arweave.rs
  • crates/gitlawb-node/src/api/events.rs
  • crates/gitlawb-node/src/api/repos.rs
  • crates/gitlawb-node/src/arweave.rs
  • crates/gitlawb-node/src/auth/mod.rs
  • crates/gitlawb-node/src/cert.rs
  • crates/gitlawb-node/src/config.rs
  • crates/gitlawb-node/src/db/mod.rs
  • crates/gitlawb-node/src/server.rs
  • crates/gitlawb-node/src/test_support.rs

Comment thread crates/gitlawb-node/src/arweave.rs Outdated
Comment thread crates/gitlawb-node/src/arweave.rs Outdated

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Use the configured gateway's data URL when verifying an anchor
    crates/gitlawb-node/src/arweave.rs:265
    arweave_gateway defaults to https://arweave.net, but this requests /v1/tx/{id}, which is the bundler API used for uploads rather than an Arweave gateway data URL. Consequently every normally uploaded anchor is reported invalid with the documented default configuration. Fetch the item through the gateway's data path (or add a separately named bundler-read configuration), and cover the default configuration rather than a mock of the bundler path.

  • [P1] Bind each anchor to the certificate for its own ref update
    crates/gitlawb-node/src/api/repos.rs:1306
    Certificates are issued once per update above this block, but every iteration subsequently reads the repository-wide latest certificate. In a multi-ref push, all permanent anchors therefore embed the last update's certificate; another completed push can also win the asynchronous race. Since verify_anchor never compares the certificate's repo/ref/old/new fields with the outer anchor, it returns valid for an anchor whose advertised transition was never signed. Preserve the returned certificate per update and reject a mismatch during verification.

  • [P1] Preserve certificate history and fail closed on a missing predecessor
    crates/gitlawb-node/src/db/mod.rs:2013
    The existing (repo_id, ref_name) upsert overwrites the predecessor whenever that ref is pushed again, while the new seq/prev design requires that predecessor to remain available. verify_anchor then silently skips the check when get_cert_by_seq returns None or errors, so an ordinary repeated push produces a truncated chain reported as valid. Make chain entries append-only and treat an unavailable declared predecessor as invalid (or explicitly unverifiable).

  • [P2] Allocate chain sequence numbers atomically
    crates/gitlawb-node/src/cert.rs:31
    Sequence allocation is a read-then-increment with no transaction, lock, or unique (repo_id, seq) constraint. Concurrent successful pushes can receive the same sequence and predecessor; get_cert_by_seq then selects an arbitrary row. This makes a signed chain nondeterministic under normal concurrent traffic. Allocate the sequence transactionally and enforce uniqueness, with retry on collision.

  • [P1] Do not describe raw signature bytes as a verifiable pusher authorization proof
    crates/gitlawb-node/src/auth/mod.rs:252
    Only the 64-byte Ed25519 signature is persisted. The RFC 9421 Signature-Input, covered component values, method/path, and content digest are discarded, and verify_anchor never verifies pusher_sig. A third party therefore cannot reconstruct the signing string or bind these bytes to this push, yet the endpoint can report the anchor valid. Persist a complete verifiable authorization artifact and validate it, or remove the proof/verification claim.

  • [P1] Bound the untrusted response read on the public verification route
    crates/gitlawb-node/src/arweave.rs:279
    The new unauthenticated, unthrottled route buffers the full gateway response with resp.bytes() before attempting JSON parsing. A caller can repeatedly select large data items and force corresponding memory and bandwidth consumption on the node. Apply a strict response-size limit (and a route-appropriate rate limit) before buffering or parsing the body.

  • [P3] Implement the promised anchor failure lifecycle instead of dropping failed uploads
    crates/gitlawb-node/src/api/repos.rs:1324
    Upload failures only log a warning; no anchor row is created, retried, confirmed, or marked failed. The new pending/confirmed/failed methods are unused, and success-only rows are always inserted as pending. This does not meet the linked issue's stated retry/visible-gap acceptance criterion, so transient bundler failures silently leave history unanchored. Persist pending work before upload and drive it through bounded retry and terminal status handling.

  • [P2] Keep the documented anchoring configuration working during the rename
    crates/gitlawb-node/src/config.rs:91
    This removes GITLAWB_IRYS_URL without a fallback, while both .env.example and README.md still instruct operators to set it. Upgrading an existing documented deployment leaves bundler_url empty and silently disables both anchoring paths. Support the legacy variable for a deprecation period or make the migration explicit and update all operator documentation in the same change.

  • [P3] Expose the new signed fields through the certificate API
    crates/gitlawb-node/src/api/certs.rs:45
    The certificate signing payload now includes seq, prev, and pusher_sig, but both list and get responses omit all three fields. Consumers of the established certificate API (including gl cert) therefore cannot reconstruct the signed payload or inspect chain continuity after this change. Serialize the new fields and update the client display/verification path accordingly.

@kevincodex1

Copy link
Copy Markdown
Contributor

@Gravirei please rebase to main and fix conflicts

@Gravirei
Gravirei force-pushed the fix/issue-26-harden-arweave-anchoring-verification branch from c94e8ed to ae4f5fc Compare July 22, 2026 16:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
crates/gitlawb-node/src/test_support.rs (1)

4983-4988: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Make seed_cert produce chain-valid fixtures.

This helper creates the 10- and 55-certificate datasets, but every certificate has seq: 1 and a zero predecessor. The tests therefore cannot catch regressions that ignore monotonic ordering or prev links. Pass sequence/predecessor values through the helper or add a dedicated chained fixture.

🤖 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 `@crates/gitlawb-node/src/test_support.rs` around lines 4983 - 4988, Update the
seed_cert fixture helper so generated certificates form a valid chain: assign
increasing sequence values and set each certificate’s prev field to the
preceding certificate’s identifier or digest, with the first certificate using
the chain’s root predecessor. Ensure both the 10- and 55-certificate datasets
exercise monotonic ordering and linked predecessors.
🤖 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 `@crates/gitlawb-node/src/arweave.rs`:
- Around line 292-293: Update the payload parsing in verify_anchor_endpoint so
serde_json::from_slice failure is handled as an invalid verification result
rather than propagated as an internal error. Return VerifyResult with valid set
to false and an appropriate error string, while preserving the existing JSON
parsing path.
- Around line 281-293: Update the response handling around resp.bytes() in the
verification flow to enforce the 1 MiB limit before unbounded buffering: reject
any Content-Length above 1_048_576, and stream or otherwise cap reads so
responses without a trustworthy length header cannot exceed the limit. Preserve
the existing invalid VerifyResult fields and JSON parsing for accepted payloads.

In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 923-938: Migration version 13 must backfill distinct
per-repository sequence values before enforcing uniqueness. In the migration’s
`stmts` array, add an update that assigns deterministic, non-colliding `seq`
values to existing `ref_certificates` rows grouped by `repo_id`, then create
`idx_ref_certs_repo_seq`; preserve the existing column additions and append-only
index changes.
- Around line 5157-5162: Ensure each certificate created through make_cert
receives a unique seq value before insertion, either by incrementing it within
make_cert or overriding it at every test call site. Update the affected
certificate setup so list_ref_certificates_respects_limit and
insert_ref_certificate_append_only use distinct sequence numbers and avoid the
(repo_id, seq) uniqueness conflict.

---

Nitpick comments:
In `@crates/gitlawb-node/src/test_support.rs`:
- Around line 4983-4988: Update the seed_cert fixture helper so generated
certificates form a valid chain: assign increasing sequence values and set each
certificate’s prev field to the preceding certificate’s identifier or digest,
with the first certificate using the chain’s root predecessor. Ensure both the
10- and 55-certificate datasets exercise monotonic ordering and linked
predecessors.
🪄 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

Run ID: 4e15d3f3-7966-4b86-a8fa-640f7c92e10a

📥 Commits

Reviewing files that changed from the base of the PR and between c94e8ed and ae4f5fc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/gitlawb-node/src/api/arweave.rs
  • crates/gitlawb-node/src/api/certs.rs
  • crates/gitlawb-node/src/api/events.rs
  • crates/gitlawb-node/src/api/repos.rs
  • crates/gitlawb-node/src/arweave.rs
  • crates/gitlawb-node/src/auth/mod.rs
  • crates/gitlawb-node/src/cert.rs
  • crates/gitlawb-node/src/config.rs
  • crates/gitlawb-node/src/db/mod.rs
  • crates/gitlawb-node/src/main.rs
  • crates/gitlawb-node/src/server.rs
  • crates/gitlawb-node/src/test_support.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/gitlawb-node/src/api/events.rs
  • crates/gitlawb-node/src/server.rs
  • crates/gitlawb-node/src/api/arweave.rs
  • crates/gitlawb-node/src/config.rs
  • crates/gitlawb-node/src/cert.rs

Comment thread crates/gitlawb-node/src/arweave.rs Outdated
Comment thread crates/gitlawb-node/src/arweave.rs Outdated
Comment thread crates/gitlawb-node/src/db/mod.rs
Comment thread crates/gitlawb-node/src/db/mod.rs Outdated

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Backfill legacy certificate sequence numbers before adding the unique index
    crates/gitlawb-node/src/db/mod.rs:910
    Migration 12 gives every existing certificate seq = 1, but migration 13 then creates a unique (repo_id, seq) index. Version 10 only deduplicated (repo_id, ref_name), so any existing repository with certificates for two refs has duplicate (repo_id, 1) rows and cannot complete this migration or start. This is also immediately exposed by the changed seed_cert fixture, which inserts ten seq = 1 rows and makes list_certs_respects_limit_param fail. Assign deterministic per-repository sequence/chain values before creating the index and update the fixture.

  • [P1] Enforce the verification response limit before buffering the gateway body
    crates/gitlawb-node/src/arweave.rs:282
    The new unauthenticated verification route calls resp.bytes() and only checks the 1 MiB limit after the whole Arweave object has been downloaded and allocated. A caller can select a huge or chunked gateway item and exhaust node memory/bandwidth before the rejection occurs. Stream into a capped reader (and reject a known excessive content length up front) instead of buffering first.

  • [P1] Restore cryptographic verification to gl cert show
    crates/gl/src/cert.rs:151
    This branch has drifted from its base and removes the base's --verify, --expect-node, Ed25519 verification routine, and tests, although the command is still documented as verifying a certificate. It now only prints a proposed payload and returns success for a modified or self-signed certificate. Please rebase without reverting that fail-closed CLI contract, then update its canonical payload for seq, prev, and pusher_sig.

  • [P2] Hold the certificate-chain lock through allocation and insertion
    crates/gitlawb-node/src/db/mod.rs:2160
    pg_advisory_xact_lock is transaction-scoped, but this standalone pooled query commits before issue_ref_certificate reads the previous certificate or inserts the new one. Concurrent pushes can therefore select the same sequence; with three or more contenders the single retry can collide again, leaving an accepted push without its certificate/anchor evidence. Use one acquired connection/transaction for the lock, predecessor read, and insert (or a robust serialization/retry strategy).

  • [P2] Bind the embedded certificate to the enclosing Arweave anchor
    crates/gitlawb-node/src/arweave.rs:303
    Verification checks the copied certificate signature but never compares the untrusted outer repo, owner_did, ref, SHAs, or node DID to that certificate. An attacker can publish a payload with a valid public certificate while claiming a different ref update and receive valid: true. Reject field mismatches (and match a locally recorded transaction too if this endpoint is meant to validate local anchors).

  • [P2] Keep gl status compatible with the remote created by gl init
    crates/gl/src/status.rs:146
    This branch regresses the base's multi-remote lookup: the status command now accepts only a gitlawb:// fetch URL on origin, while gl init adds the same URL under the gitlawb remote. Immediately after the supported init flow, gl status reports that the repository is not a Gitlawb repo and skips the PR/issue queries. Rebase without reverting the base's lookup for the gitlawb remote and other Gitlawb fetch/push URLs.

  • [P2] Do not hard-code main after a plain git init
    crates/gl/src/init.rs:41
    This branch reverts the base's branch/commit-state handling. Plain git init honors the user's init.defaultBranch, but the command unconditionally instructs git push gitlawb main. On master, feature, detached, or unborn HEADs that instruction either targets a nonexistent/wrong ref or fails before the first commit. Rebase without dropping the previous branch/commit-state handling, or initialize main with the compatibility fallback.

  • [P2] Preserve the legacy command-line spelling during the bundler rename
    crates/gitlawb-node/src/config.rs:75
    The environment fallback runs only after Clap parses arguments. Existing operators invoking gitlawb-node --irys-url … now receive an unknown-argument startup error even though the PR claims compatibility for GITLAWB_IRYS_URL. Add a deprecated long alias or normalize a retained legacy option as well as the environment variable.

  • [P2] Do not remove unrelated CI safety gates from this anchoring change
    .github/workflows/pr-checks.yml:189
    This is stale-base drift rather than part of the Arweave feature: the branch deletes the shipped Windows CLI test lane and the only gitlawb-core dependency-purity gate, together with its allowlist and checker script. That removes platform regression visibility and a supply-chain control for the shared cryptographic core; rebase without deleting these protections, or justify and replace them in a separately scoped change.

Gravirei added 5 commits July 23, 2026 10:39
Gitlawb#26)

- Use configured gateway's data URL (/tx_id) instead of bundler API for verify
- Bound untrusted response to 1 MiB on public verify route
- Bind each anchor to its own ref update certificate (not repo-wide latest)
- Make certificate storage append-only with unique (repo_id, seq) constraint
- Add migration v13 to drop old unique index and add sequence uniqueness
- Allocate chain sequence numbers atomically using per-repo advisory lock
- Fail closed on missing predecessor cert during verification
- Persist full RFC 9421 HTTP Signature context (signature-input, content-digest, path)
- Verify pusher authorization proof in verify_anchor
- Add legacy GITLAWB_IRYS_URL fallback for GITLAWB_BUNDLER_URL
- Expose seq, prev, pusher_sig through certificate API list/get responses
@Gravirei
Gravirei force-pushed the fix/issue-26-harden-arweave-anchoring-verification branch from ae4f5fc to e9a1a5e Compare July 23, 2026 04:39
Gravirei added 2 commits July 23, 2026 11:19
…ias, cross-check cert, transaction lock

- v13 migration: backfill seq values per repo before creating
  unique (repo_id, seq) index to prevent failures on existing data
- verify_anchor: check Content-Length before buffering response body
- config: add deprecated --irys-url alias for bundler_url
- verify_anchor: cross-check outer anchor fields (repo, ref_name,
  old_sha, new_sha, node_did) against embedded certificate
- cert issuance: wrap lock/lookup/insert in a single Postgres
  transaction so pg_advisory_xact_lock is held for the full sequence
- Add _tx variants of db methods accepting &mut PgConnection
- Fix test helpers to use atomic counters for unique seq values
- Fix v10 dedup tests to drop v13 index during pre-migration setup
@Gravirei
Gravirei requested a review from jatmn July 23, 2026 05:29
@kevincodex1

Copy link
Copy Markdown
Contributor

@coderabbitai please make an intensive review of this changes

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@kevincodex1 I’ll perform an intensive review of the complete change set, with particular attention to certificate-chain integrity, RFC 9421 signature verification, migration safety, anchoring failure handling, and API/security behavior.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/gitlawb-node/src/api/events.rs (1)

429-435: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

Use valid per-repository certificate-chain fixtures.

  • crates/gitlawb-node/src/api/events.rs#L429-L435: replace the process-global sequence generator with per-chain fixture state.
  • crates/gitlawb-node/src/api/events.rs#L447-L452: derive prev from the preceding certificate instead of always using the genesis hash.
  • crates/gitlawb-node/src/test_support.rs#L1481-L1486: avoid assigning a global sequence to an otherwise standalone certificate.
  • crates/gitlawb-node/src/test_support.rs#L4967-L4973: make sequence generation scoped to a repository/chain.
  • crates/gitlawb-node/src/test_support.rs#L4990-L4995: generate matching predecessor hashes for multi-certificate fixtures.
🤖 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 `@crates/gitlawb-node/src/api/events.rs` around lines 429 - 435, Replace the
process-global NEXT_FCERT_SEQ/ref_cert_seq state in
crates/gitlawb-node/src/api/events.rs:429-435 with sequence state scoped to each
certificate chain; update the certificate construction at
crates/gitlawb-node/src/api/events.rs:447-452 to derive prev from the preceding
certificate. In crates/gitlawb-node/src/test_support.rs:1481-1486, leave
standalone certificates without a global sequence; in lines 4967-4973, scope
sequence generation to the repository or chain; and in lines 4990-4995, generate
predecessor hashes that match the preceding certificates in multi-certificate
fixtures.
🤖 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 `@crates/gitlawb-node/src/api/certs.rs`:
- Around line 55-57: The certificate JSON responses in
crates/gitlawb-node/src/api/certs.rs must include complete pusher-signature
metadata. Update both the listed-certificates response at lines 55-57 and the
single-certificate response at lines 98-100 to include signature_input,
content_digest, and request_path alongside the existing pusher_sig fields.

---

Nitpick comments:
In `@crates/gitlawb-node/src/api/events.rs`:
- Around line 429-435: Replace the process-global NEXT_FCERT_SEQ/ref_cert_seq
state in crates/gitlawb-node/src/api/events.rs:429-435 with sequence state
scoped to each certificate chain; update the certificate construction at
crates/gitlawb-node/src/api/events.rs:447-452 to derive prev from the preceding
certificate. In crates/gitlawb-node/src/test_support.rs:1481-1486, leave
standalone certificates without a global sequence; in lines 4967-4973, scope
sequence generation to the repository or chain; and in lines 4990-4995, generate
predecessor hashes that match the preceding certificates in multi-certificate
fixtures.
🪄 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: 5645b021-f503-4733-92c1-424a3ae2349e

📥 Commits

Reviewing files that changed from the base of the PR and between e9da517 and 8103450.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/gitlawb-node/src/api/arweave.rs
  • crates/gitlawb-node/src/api/certs.rs
  • crates/gitlawb-node/src/api/events.rs
  • crates/gitlawb-node/src/api/repos.rs
  • crates/gitlawb-node/src/arweave.rs
  • crates/gitlawb-node/src/auth/mod.rs
  • crates/gitlawb-node/src/cert.rs
  • crates/gitlawb-node/src/config.rs
  • crates/gitlawb-node/src/db/mod.rs
  • crates/gitlawb-node/src/main.rs
  • crates/gitlawb-node/src/server.rs
  • crates/gitlawb-node/src/test_support.rs

Comment thread crates/gitlawb-node/src/api/certs.rs Outdated
@kevincodex1

Copy link
Copy Markdown
Contributor

@coderabbitai make an intensive review on this please

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

@kevincodex1 I’ll perform an intensive end-to-end review of #224, focusing on certificate-chain correctness, RFC 9421 proof validation, database migrations, anchoring lifecycle/error handling, and API security.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/gitlawb-node/src/arweave.rs (1)

303-304: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Non-JSON gateway payload still returns 500 instead of valid: false. serde_json::from_slice(&body_bytes)? propagates through verify_anchor_endpoint (mapped to AppError::Internal), inconsistent with the other graceful branches and the "could be JSON or raw bytes" comment. Convert a parse failure into VerifyResult { valid: false, .. } with an error string.

🤖 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 `@crates/gitlawb-node/src/arweave.rs` around lines 303 - 304, Update the anchor
parsing in verify_anchor_endpoint so serde_json::from_slice failures are handled
as an invalid verification result rather than propagated as AppError::Internal.
Return VerifyResult with valid set to false and an error string for non-JSON
payloads, while preserving the existing successful JSON path and other graceful
branches.
🧹 Nitpick comments (2)
crates/gitlawb-node/src/server.rs (1)

222-228: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider throttling the unauthenticated verify route. Each GET /api/v1/arweave/verify/{tx_id} triggers an outbound gateway fetch plus a DB lookup with no auth or per-IP brake, so it's an amplification/DoS surface (node → gateway) reachable by anonymous callers. Given the other cost-bearing routes here carry a per-IP IpRateLimiter, consider wrapping arweave_routes similarly. (tx_id is a fixed-host path segment, so this is a load concern, not SSRF.)

🤖 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 `@crates/gitlawb-node/src/server.rs` around lines 222 - 228, Wrap the
arweave_routes router, including GET /api/v1/arweave/verify/{tx_id}, with the
existing per-IP IpRateLimiter used by other cost-bearing routes. Preserve the
current list_anchors and verify_anchor_endpoint handlers while ensuring
anonymous requests are throttled before triggering gateway or database work.
crates/gitlawb-node/src/db/mod.rs (1)

2834-2870: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

cert_id is never persisted on anchor rows. RecordAnchorInputV2 has no cert_id field and record_arweave_anchor's INSERT omits it, so the cert_id column added in migration v12 stays NULL for every anchor even though list_arweave_anchors/list_pending_anchors project it. The push path in api/repos.rs already has the issued certificate in scope (ref_certs_clone), so the anchor→certificate DB linkage this column was added for is currently unreachable. Consider threading the cert id through so audits can join anchors to their certs. (gateway_url on the input is likewise accepted but ignored by this function.)

🤖 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 `@crates/gitlawb-node/src/db/mod.rs` around lines 2834 - 2870, The anchor
record flow does not persist the issued certificate ID. Add a cert_id field to
RecordAnchorInputV2, pass the corresponding ID from the push path using
ref_certs_clone, and include it in record_arweave_anchor’s INSERT and bindings
so cert_id is stored on each anchor row; also remove or persist gateway_url
consistently instead of silently ignoring it.
🤖 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.

Duplicate comments:
In `@crates/gitlawb-node/src/arweave.rs`:
- Around line 303-304: Update the anchor parsing in verify_anchor_endpoint so
serde_json::from_slice failures are handled as an invalid verification result
rather than propagated as AppError::Internal. Return VerifyResult with valid set
to false and an error string for non-JSON payloads, while preserving the
existing successful JSON path and other graceful branches.

---

Nitpick comments:
In `@crates/gitlawb-node/src/db/mod.rs`:
- Around line 2834-2870: The anchor record flow does not persist the issued
certificate ID. Add a cert_id field to RecordAnchorInputV2, pass the
corresponding ID from the push path using ref_certs_clone, and include it in
record_arweave_anchor’s INSERT and bindings so cert_id is stored on each anchor
row; also remove or persist gateway_url consistently instead of silently
ignoring it.

In `@crates/gitlawb-node/src/server.rs`:
- Around line 222-228: Wrap the arweave_routes router, including GET
/api/v1/arweave/verify/{tx_id}, with the existing per-IP IpRateLimiter used by
other cost-bearing routes. Preserve the current list_anchors and
verify_anchor_endpoint handlers while ensuring anonymous requests are throttled
before triggering gateway or database work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d81eba6b-bb9a-4ee3-af1e-5e62923f6b5f

📥 Commits

Reviewing files that changed from the base of the PR and between e9da517 and a01160f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • crates/gitlawb-node/src/api/arweave.rs
  • crates/gitlawb-node/src/api/certs.rs
  • crates/gitlawb-node/src/api/events.rs
  • crates/gitlawb-node/src/api/repos.rs
  • crates/gitlawb-node/src/arweave.rs
  • crates/gitlawb-node/src/auth/mod.rs
  • crates/gitlawb-node/src/cert.rs
  • crates/gitlawb-node/src/config.rs
  • crates/gitlawb-node/src/db/mod.rs
  • crates/gitlawb-node/src/main.rs
  • crates/gitlawb-node/src/server.rs
  • crates/gitlawb-node/src/test_support.rs

…ate limit

- serde_json parse failure in arweave verify returns VerifyResult instead of
  propagating as AppError::Internal
- RecordAnchorInputV2 replaces unused gateway_url with cert_id, persisted in
  arweave_anchors INSERT, sourced from push-time certificate
- arweave routes wrapped with per-IP IpRateLimiter
@Gravirei Gravirei closed this Jul 23, 2026
@Gravirei Gravirei reopened this Jul 23, 2026

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the current head against merged state. The certificate-chain design is genuinely strong and every serious finding from the earlier rounds is resolved on this head: per-ref cert binding, prev-hash linkage, fail-closed on a missing predecessor, atomic seq allocation (transaction-scoped advisory lock plus a (repo_id, seq) unique index and a 23505 retry), the gateway data-URL fix, the migration backfill-before-unique-index, and the full pusher-signature metadata on both cert endpoints. One functional bug blocks it: the verify endpoint never validates a real anchor. Findings highest first.

Findings

  • [P1] Compare the outer anchor repo against the certificate using the same identifier domain
    crates/gitlawb-node/src/arweave.rs:334
    The verify cross-check does outer_repo != Some(&c.repo_id), but the outer anchor's repo is written as the slug {owner_key}/{name} (api/repos.rs:1222) while the embedded certificate's repo_id is the repo UUID (issue_ref_certificate(&record.id, ...), api/repos.rs:1001). Slug never equals UUID, so every honestly produced anchor pushes a repo-mismatch error and returns valid: false. The endpoint cannot go green on real data: push to a public repo with a bundler configured, take arweave_tx_id from /api/v1/arweave/anchors, and GET /api/v1/arweave/verify/{tx_id} reports invalid despite a good node signature. This is the cross-check added in response to the earlier "verify does not compare the transition" finding, so the fix landed but across mismatched identifier domains; the ref/old/new/node comparisons beside it are correct. Either carry the UUID in the outer anchor, or resolve the slug to the repo id on the verify side before comparing. The verify tests set the embedded cert's repo_id equal to the outer slug, which is why CI stays green while production never matches.

  • [P2] Make the pusher authorization proof load-bearing, not silently skippable
    crates/gitlawb-node/src/arweave.rs:465
    The pusher-proof check is gated on all four of pusher_sig, signature_input, content_digest, request_path being present, but the node signing payload (cert.rs) covers only pusher_sig — not the other three. A holder of a valid node signature can null signature_input/content_digest/request_path; the node signature still verifies (those fields are unsigned), the whole if let (Some, Some, Some, Some) block is skipped, and verification returns valid with the independent RFC 9421 proof never checked. That defeats the stated goal of letting a third party verify the pusher authorization without trusting the node alone; it bites under node-key compromise. Bind the three context fields into the node payload and treat a present pusher_sig with missing context as invalid rather than passing.

  • [P2] Bound the gateway body by bytes read, not Content-Length
    crates/gitlawb-node/src/arweave.rs:293
    The 1 MiB guard only short-circuits when the gateway sends an honest Content-Length; a chunked or header-omitting (or low-lying) response skips the pre-check, and resp.bytes().await then buffers the whole body before the post-check runs. The verify route is unauthenticated (IP-rate-limited only) and tx_id is caller-chosen, so a permissionless caller can drive multi-hundred-MB allocations on the async worker, bounded only by the 10s client timeout. Stream with a running cap (resp.chunk() loop, abort past 1 MiB) rather than buffering first.

  • [P2] Add an executed upgrade-path test for the v13 seq backfill
    crates/gitlawb-node/src/db/mod.rs:923
    The v13 backfill and idx_ref_certs_repo_seq build are never exercised through run_migrations() against pre-existing multi-cert data: v10_upgrade_dedup_via_migration re-applies only v10 (it deletes just the v10 row from schema_migrations), and migration_v11_creates_owner_did_column seeds no certificates. The backfill logic itself is sound (ROW_NUMBER() OVER (PARTITION BY repo_id ORDER BY issued_at, id) over a NOT NULL column with a total tiebreaker), but a data migration this consequential needs a test that seeds schema_migrations at v12, inserts several same-repo/different-ref certs (all seq=1 after v12), runs the migrations, and asserts distinct seq plus the unique index present.

  • [P3] Cluster of smaller items
    crates/gitlawb-node/src/arweave.rs:272
    Config docs still point at the dead knob: .env.example, README, and the arweave.rs module comment reference GITLAWB_IRYS_URL and never mention GITLAWB_BUNDLER_URL or GITLAWB_ARWEAVE_GATEWAY (not a break — main.rs falls back to the old env var with a deprecation warning — but the docs should match). A gateway-fetch failure or a malformed embedded node_did returns a 500 that echoes the internal error string (api/arweave.rs) rather than a clean valid:false with the right status. tx_id is unvalidated before being appended to the gateway URL (no host-swap SSRF given the fixed authority and redirect::none, but validate to the 43-char base64url shape as cheap defense). repo_lock_hash uses DefaultHasher, which the std docs do not guarantee stable across Rust versions, so two differently-built nodes on one Postgres could hash a repo to different lock keys (backstopped by the unique index and retry, so retry storms rather than corruption; prefer a stable hash). The outer old_sha/new_sha/node_did cross-checks are skipped when the field is absent (is_some() guards), unlike repo/ref; a forger who omits them still gets valid. Minor: the dead lock_repo_cert_issuance helper locks on the pool connection (immediate release, a no-op) and should be removed or made a session lock, and the endpoint doc comment says "most recent local cert" while the code chains against seq-1.

Core design is sound and the prior integrity findings are genuinely resolved; the P1 is the blocker and it is a last-mile identifier mismatch, not a redesign.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Blocking findings

1. verify_anchor compares anchor repo slug against certificate repo_id

Severity: Blocking
Files: crates/gitlawb-node/src/arweave.rs:335, crates/gitlawb-node/src/api/repos.rs:1328, crates/gitlawb-node/src/cert.rs:97

The outer anchor payload stores the human-readable repo slug (owner_short/repo_name), while the embedded RefCertificate stores the database UUID (record.id). The verify cross-check does:

} else if outer_repo != Some(&c.repo_id) {

For every real anchor these values differ, so the endpoint returns valid: false even when the node signature, prev hash, and pusher proof are correct. This makes the verify endpoint unusable on production data.

Fix: Either store repo_id in the anchor payload (or add a dedicated repo_id field), or resolve the slug to the repo UUID on the verify side before comparing. Prefer storing the UUID in the anchor and keeping the slug for display only.

2. gl cert show --verify uses the old 7-field signed payload

Severity: Blocking
Files: crates/gl/src/cert.rs:259-267, crates/gitlawb-node/src/cert.rs:25-36

The CLI reconstructs the node-signed payload with only the original seven fields:

{ "repo_id", "ref", "old", "new", "pusher", "node", "ts" }

The node now signs ten fields:

{ "repo_id", "ref", "old", "new", "pusher", "node", "ts", "seq", "prev", "pusher_sig" }

Because serde_json serializes maps alphabetically, the added keys change the signed bytes. gl cert show --verify will report INVALID for every certificate issued after this PR, even though the node and the Arweave verifier accept them.

The CLI tests payload_serialization_matches_frozen_canonical_form and verify_signature_round_trip_and_tamper still pin the old format and therefore pass while real-world verification fails.

Fix: Update verify_signature in gl/src/cert.rs to include seq, prev, and pusher_sig, and update the frozen canonical tests accordingly.

High-priority findings

3. New anchor lifecycle methods are dead code

Severity: High
Files: crates/gitlawb-node/src/db/mod.rs:2920-2975, crates/gitlawb-node/src/api/repos.rs:1342-1354

confirm_arweave_anchor, fail_arweave_anchor, and list_pending_anchors are marked #[allow(dead_code)] and never called. Anchors are inserted with status = 'pending', deadline_height = NULL, and receipt_sig = NULL, and no background worker ever updates them. The new status fields are therefore unreliable for monitoring.

Fix: Either add a background confirmation worker in this PR, or remove the unused columns/methods and defer the lifecycle feature to a follow-up.

4. record_arweave_anchor failures are silently dropped

Severity: High
File: crates/gitlawb-node/src/api/repos.rs:1342-1354

let _ = db_clone
    .record_arweave_anchor(&crate::db::RecordAnchorInputV2 { ... })
    .await;

If the local DB insert fails, the anchor transaction exists on Arweave but is not recorded locally, with no log or metric.

Fix: Log a warning/error and optionally increment a metric when the insert fails.

5. contracts_info leaks URLs that may contain credentials

Severity: High
File: crates/gitlawb-node/src/server.rs:583-605

The unauthenticated /api/v1/contracts endpoint returns rpc_url, bundler_url, and arweave_gateway verbatim. If an operator configures a private RPC or paid bundler URL containing an API key, the key is exposed to anonymous callers.

Fix: Mask or omit URLs that may contain credentials, or require authentication for this endpoint.

6. Arweave verify route uses the per-DID creation limiter

Severity: High
Files: crates/gitlawb-node/src/server.rs:225-236, crates/gitlawb-node/src/main.rs:297-298

The verify route is throttled with state.rate_limiter, which is configured as a per-DID repo-creation limiter (10 requests per hour). Even though the middleware keys by IP, the threshold is far too restrictive for a public verification endpoint.

Fix: Add a dedicated Arweave IP rate limiter (e.g. GITLAWB_ARWEAVE_RATE_LIMIT) with a sensible default.

7. Historical certificates get broken prev values after migration

Severity: High
File: crates/gitlawb-node/src/db/mod.rs:923-950

Migration v13 renumbers seq but does not backfill prev. Existing rows except the first per repo keep the zero sentinel, so verify_anchor will reject all pre-upgrade anchors whose embedded certificate has seq > 1.

Fix: Backfill prev in v13 using the same canonical JSON + SHA-256 that cert::prev_hash uses, or explicitly document that historical anchors are intentionally unverifiable.

Medium-priority findings

8. Pusher proof is silently skipped when incomplete

Severity: Medium
File: crates/gitlawb-node/src/arweave.rs:465-551

verify_anchor only runs the RFC 9421 pusher verification when all four of pusher_sig, signature_input, content_digest, and request_path are present. If any is missing (e.g. pre-v13 certificates), the endpoint can still return valid: true without checking who authorized the push.

Fix: At minimum, emit an informational error/warning when a certificate is expected to carry a pusher proof but one is missing.

9. tx_id path parameter is not validated before gateway fetch

Severity: Medium
File: crates/gitlawb-node/src/arweave.rs:267

The user-supplied tx_id is appended directly to the gateway URL. Without length/format validation (Arweave IDs are 43-character base64url strings), the endpoint can be abused as a limited open proxy or SSRF vector against the configured gateway.

Fix: Validate tx_id against ^[A-Za-z0-9_-]{43}$ and return 400 Bad Request early.

10. Outer anchor checks are optional for old_sha, new_sha, node_did

Severity: Medium
File: crates/gitlawb-node/src/arweave.rs:351-371

The cross-check only errors if these fields are present and mismatch. If a malicious anchor omits them, verification still passes. They should be mandatory when a certificate is embedded.

Fix: Fail closed when old_sha, new_sha, or node_did are missing from the outer payload.

11. Advisory lock key uses unstable DefaultHasher

Severity: Medium
File: crates/gitlawb-node/src/db/mod.rs:2241-2246

repo_lock_hash derives the per-repo advisory lock key with std::collections::hash_map::DefaultHasher, which is not guaranteed stable across Rust versions or platforms. Different node builds could compute different lock keys and lose cross-instance serialization.

Fix: Use a stable hash such as the first 8 bytes of SHA-256(repo_id).

12. v1 schema was edited in-place

Severity: Medium
File: crates/gitlawb-node/src/db/mod.rs:462-473, :525-538, :648-663

The migration catalogue comment explicitly states that future changes must be added as new migrations and never appended to v1. However, v1 now already contains the columns that migrations v12/v13 add. While IF NOT EXISTS keeps upgrades safe, this breaks the migration narrative and can confuse future maintainers.

Fix: Either revert the v1 additions and rely solely on v12/v13, or document that v1 in this branch intentionally includes later columns.

Lower-priority findings

13. Test fixtures do not form valid certificate chains

Severity: Low-Medium
Files: crates/gitlawb-node/src/api/events.rs:429-454, crates/gitlawb-node/src/test_support.rs:4964-4997, crates/gitlawb-node/src/db/mod.rs:5215-5252

Test helpers use process-global sequence counters and hard-coded zero prev hashes. They do not exercise per-repo contiguous sequences or cryptographically correct prev linkage, so chain-related regressions could slip through.

Fix: Use per-repo sequence allocation and compute prev from the predecessor certificate, matching production issuance.

14. local_cert events omit the new certificate fields

Severity: Low-Medium
File: crates/gitlawb-node/src/api/events.rs:243-256

The event feed does not include seq, prev, pusher_sig, signature_input, content_digest, or request_path, so event consumers cannot validate chain continuity or reconstruct the pusher proof.

Fix: Include the new fields in the event payload.

… proof, streaming body, v13 test, dead code cleanup, dedicated arweave limiter, P3 fixes

P1:
- arweave.rs: add repo_id to RefAnchor, cross-check compares UUID vs UUID
- gl/src/cert.rs: update verify_signature to sign 10 fields (seq, prev, pusher_sig)

P2:
- cert.rs: bind signature_input/content_digest/request_path into node payload
- arweave.rs: fail closed when pusher_sig present but context fields missing
- arweave.rs: replace Content-Length body guard with streaming 1 MiB chunk cap
- db/mod.rs: add v13_seq_backfill_via_migration upgrade-path test
- db/mod.rs: remove dead confirm/fail/list_pending_anchors methods and tests
- api/repos.rs: log record_arweave_anchor failures via tracing::warn
- server.rs: mask credential URLs in contracts_info endpoint
- state/main/server: add dedicated arweave_rate_limiter (GITLAWB_ARWEAVE_RATE_LIMIT)

P3:
- api/arweave.rs: validate tx_id (43-char base64url) before gateway fetch
- arweave.rs: make old_sha/new_sha/node_did mandatory when cert is present
- db/mod.rs: replace DefaultHasher with stable SHA-256 prefix for lock keys
- db/mod.rs: document v1 schema inclusion of later columns
- api/events.rs: include seq, prev, pusher_sig, context fields in local_cert events
@Gravirei
Gravirei requested review from beardthelion and jatmn July 24, 2026 07:22

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Traced the signing/verification payload construction on both the node and gl sides line-by-line, and cross-checked the migration runner and the pre-existing rate-limiter/auth-middleware code this PR reuses. Most of the design (append-only per-repo hash chain, advisory-lock-serialized seq allocation, streamed-and-capped gateway fetch, fail-closed cross-checks) is solid. One finding should block merge.

Findings

  • [P1] gl cert show always reports a legitimate push-issued certificate as invalid
    crates/gl/src/cert.rs:265
    The node signs the certificate over 13 fields (crates/gitlawb-node/src/cert.rs cert_payload), including signature_input, content_digest, and request_path. gl's client-side verify_signature rebuilds the payload to check the signature against but stops at pusher_sig, never reading or including those three fields. git_receive_pack always passes Some(..) for all three (the route sits behind require_signature, which unconditionally sets them), so every certificate issued for a real push carries them, and the server API (api/certs.rs) already returns them in the JSON — cmd_show just never parses or forwards them. The byte mismatch means the Ed25519 check gl cert show runs fails for every push-issued certificate, reporting a validly node-signed cert as tampered. The PR's own gl/src/cert.rs tests don't catch this because they only exercise the old 10-field shape (pusher_sig: null, no context fields).
    Fix: add signature_input, content_digest, request_path to gl's verify_signature payload and its call sites, matching the server's cert_payload exactly, then add a test that signs and verifies a cert with all three fields populated.

  • [P3] .env.example and README still document the renamed config knob
    .env.example, README.md
    Both still reference GITLAWB_IRYS_URL only; neither mentions the new GITLAWB_BUNDLER_URL or GITLAWB_ARWEAVE_GATEWAY (config.rs). main.rs does fall back to the legacy env var with a deprecation warning, so this isn't a functional break, just stale operator-facing docs for a knob this PR renamed.

  • [P3] verify_anchor 500s on a malformed node_did instead of returning valid:false
    crates/gitlawb-node/src/arweave.rs
    Every other malformed-input case in verify_anchor (gateway non-2xx, oversized/undecodable body, non-JSON payload) returns Ok(VerifyResult{valid:false, ..}). The node-DID parse (gitlawb_core::did::Did::from_str(&c.node_did).map_err(..)?) still uses ?, so a certificate whose embedded node_did fails to parse propagates as Err, which the handler turns into a 500 instead of the same controlled {valid:false} response every other bad-input path returns.

… docs, and fix node_did 500 on verify_anchor
@Gravirei
Gravirei requested a review from beardthelion July 24, 2026 13:47

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed at ed80c90c. All three findings from the last round are genuinely fixed, and I confirmed the signing-payload one by execution rather than inspection: the node's cert_payload and gl's verify_signature now emit byte-identical bytes across all-Some, partial-Some, and unicode/escaping cases. The streamed gateway cap, the fail-closed outer cross-checks, and the incomplete-pusher-context rejection all landed cleanly too.

Three things block merge, all on the verification path this PR exists to add, all reproduced end to end against the real handler.

Findings

  • [P1] Anchor the certificate issuer instead of trusting its self-asserted node_did
    crates/gitlawb-node/src/arweave.rs:414
    verify_anchor derives the verifying key from c.node_did, a field inside the certificate it is checking, so the signature only proves the certificate is internally consistent. I minted a fresh keypair, self-signed a certificate naming its own DID with seq: 1 (skipping the prev-chain check) and pusher_sig: null (skipping the pusher proof), served it from a mock gateway, and the endpoint returned valid: true with an empty errors array for a push that never happened against a repo the node has never seen. Arweave uploads are permissionless and free under 100 KiB, so anyone can place the bytes and hand the node a tx_id. gl already implements exactly this guard (crates/gl/src/cert.rs:241-252), and its comment names the attack verbatim: "A hostile source can mint a keypair, put its DID in node_did, and self-sign." Carry that check to the server: compare c.node_did against state.node_did via crate::api::did_matches and push an error on mismatch. If verifying another node's anchors is intended, that needs an explicit trusted-issuer input (a peer lookup or an ?expect_node=) that fails closed when absent, not an implicit trust of whatever the payload claims.

  • [P1] Verify a certificate against the payload it was actually signed with
    crates/gitlawb-node/src/arweave.rs:396, crates/gl/src/cert.rs:285
    The signing payload grew from 7 fields to 13, and both verifiers rebuild the 13-field form unconditionally with no version discriminator and no fallback. Every certificate issued before this PR was signed over {repo_id, ref, old, new, pusher, node, ts} (origin/main's cert.rs), and after the v12/v13 migration those rows read back as seq (renumbered by the v13 backfill), prev = 64 zeros from the column default, and three NULL context fields. Executed: signing that exact legacy payload and feeding the migrated row shape to the new verifier returns Err("Ed25519 signature does not match the signed payload"), while the same signature verifies against the 7-field payload with true. So gl cert show and the verify endpoint both report every pre-#224 certificate as tampered, and the v13 prev mismatch compounds it independently for any repo with more than one certificate. The PR body's "Backward-compatible with existing nodes and previously signed history" is the claim this contradicts. The cheapest fix that keeps old history verifiable is to fall back to the 7-field payload when the 13-field check fails and all four proof fields are NULL, reporting the result as a legacy certificate whose seq/prev are not covered by the signature; a "v": 2 key in cert_payload would make the next format change detectable instead of silent.

  • [P2] Return a generic message when the predecessor lookup fails
    crates/gitlawb-node/src/arweave.rs:499
    The sqlx error is formatted into errors, and api/arweave.rs:44 serializes that array straight to an anonymous caller on an unauthenticated route. A failing lookup surfaces the raw database error text, including the connecting user, to anyone who can reach the endpoint. Log the error and push a fixed "could not verify prev-hash linkage" string instead.

  • [P1] Bind the pusher proof to the ref transition, and require it
    crates/gitlawb-node/src/arweave.rs:513
    The pusher signature is meant to be the independent leg: evidence the named pusher authorized this specific ref update, not just the node's word for it. It cannot carry that weight as built. The signing string is rebuilt from three values only, @method fixed to POST, @path from request_path, and content-digest (lines 521-525), none of which mention ref_name, old_sha or new_sha, and the verifier holds no pack body to check the digest against. Executed: I generated a pusher keypair, built and signed the real RFC 9421 signing string for POST /alice/repo.git/git-receive-pack with a content-digest, then embedded that genuine signature in a certificate claiming refs/heads/release moving 111... to ddd..., a transition the pusher never signed. The endpoint returned valid: true with an empty errors array. One captured signature therefore vouches for any ref transition on that repo path. Separately the whole block hangs off if let Some(pusher_sig) with no else, so a certificate with pusher_sig: null skips the check silently and still reports valid; the present-but-incomplete arm you added this round is correct, but absence is the easier bypass. Have the pusher sign a component committing to <ref> <old> <new> and check that value against the certificate's own fields, then treat a missing pusher_sig as invalid, since every certificate this branch issues populates all four fields.

  • [P2] Move the new columns out of the applied v1 migration block
    crates/gitlawb-node/src/db/mod.rs:543
    The v1 CREATE TABLE statements for ref_certificates and arweave_anchors were edited to include the columns v12/v13 add, and the NOTE above the array acknowledges it was done "for development convenience". The end states do converge here because v12 uses ADD COLUMN IF NOT EXISTS and guards the rename, so this is not a live break, but it contradicts the array's own standing rule two hundred lines up ("Future schema changes MUST be added as v2, v3, … — never appended to v1") and it has a concrete cost: the migration test now starts from a v1 that already carries the columns, so v12's real ADD COLUMN / rename / DROP COLUMN path is never exercised by anything. Restore v1 to its pre-PR shape and let v12/v13 do the work they already do correctly.

  • [P2] Make the gateway-URL test load-bearing
    crates/gitlawb-node/src/arweave.rs:815
    test_verify_anchor_uses_correct_gateway_url mocks a 404 and asserts only !r.valid, with an Err arm that accepts any message containing "error". I changed the URL construction to {gateway}/v1/tx/{tx_id}, the exact regression the test's own comment describes, and it still passed. Assert the mock was consumed (_mock.assert_async().await) and drive a 200 with a real body so the path shape is actually pinned.

  • [P3] Smaller items
    crates/gitlawb-node/src/arweave.rs:278
    A gateway connect or DNS failure still uses ? and becomes a 500, unlike every other bad-input path beside it that returns a controlled valid: false. prev_hash covers the predecessor's 7 canonical fields but not its own seq or prev, so the link is not recursive over history; the node signature does cover those fields, which is why this is a naming question rather than a hole, but "hash chain" in the docs promises more than the link delivers. arweave_anchors.status is inserted as 'pending' and never updated by any code path, and deadline_height / receipt_sig are never written at all, so list_anchors reports a lifecycle that does not exist; the PR body also still lists confirm_arweave_anchor(), fail_arweave_anchor() and list_pending_anchors(), which are not on this head. repo_lock_hash uses i64::from_ne_bytes two lines below a comment promising portability. mask_credential_url splits on the first @, so https://user:p@ss@host leaks the password tail.

The certificate-chain design continues to hold up well and the trajectory across rounds has been good. The three P1s share one root: the verifier accepts what the certificate says about itself. Pinning the issuer, versioning the payload, and binding the pusher proof to the transition are each contained changes, but the endpoint should not ship reporting valid: true until all three are in.

beardthelion added a commit that referenced this pull request Jul 25, 2026
Three rules a patch author cannot recover from the code alone, all three
found by review rounds on #224 and #236:

Anchor the verifying key outside the artifact. Deriving it from a field of
the object under verification proves self-consistency, not authenticity,
and identities here are permissionless. Found on #236 in the gl client and
again on #224 in the server-side twin, where a self-signed certificate
verified against an unauthenticated route.

Drive the accepting verdict in a test. #224's verify endpoint shipped with
sixteen tests, every one asserting the invalid path, which left it unable
to observe a forged artifact being accepted.

Treat a signed payload's field set as a versioned format. #224 grew it from
seven fields to thirteen with no discriminator and no fallback, so every
certificate issued before it now reports as tampered.
@Gravirei
Gravirei requested a review from beardthelion July 25, 2026 20:26

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Preserve legacy certificate verification in the CLI
    crates/gl/src/cert.rs:285
    This always reconstructs the new 13-field payload. Certificates issued before this PR were signed over the original seven fields, and migrations only add/default/backfill seq, prev, and proof columns; they do not re-sign those rows. As a result, gl cert show --verify reports every existing valid certificate as invalid (and exits nonzero). The node verifier already has the necessary guarded 7-field fallback; apply the same compatibility handling in gl and cover a legacy fixture.

  • [P2] Actually bind the pusher proof to the claimed ref transition
    crates/gitlawb-node/src/arweave.rs:581
    Adding @ref_name, @old_sha, and @new_sha to request_values does not put them in the signature: verification uses only components present in the saved Signature-Input. The shipped signer hard-codes only @method, @path, and content-digest (crates/gitlawb-core/src/http_sig.rs:24,173-186). A node can therefore reuse a valid receive-pack proof while issuing a node-signed certificate for different ref/SHA values, and this endpoint will label the pusher proof valid. Require and sign a canonical transition component end-to-end (or retain enough signed request data to derive it) before claiming this proof authorizes the transition.

  • [P1] Make the documented default anchor configuration verifiable
    .env.example:49
    The default uploader is Irys devnet while the verification endpoint always uses the configured https://arweave.net gateway. Irys documents transaction retrieval through its own gateway and says devnet data is deleted after roughly 60 days; it is not an Arweave-mainnet permanent object. Thus an anchor created with the supplied defaults cannot be fetched by /api/v1/arweave/verify/{tx_id}, contrary to the permanent-anchor/verification flow. Configure a matching Irys gateway for that mode, or use a genuinely Arweave-backed default and document the production credential flow. Irys transaction IDs, Irys networks

  • [P2] Keep the v1 anchor-list response backward compatible
    crates/gitlawb-node/src/db/mod.rs:918
    This migration drops the persisted arweave_url and renames irys_tx_id, while GET /api/v1/arweave/anchors serializes the changed struct directly. Existing v1 clients therefore lose both the URL they use to retrieve historical anchors and the irys_tx_id field, with no response alias or versioned endpoint. Retain the historical URL/data and expose compatibility aliases until clients can migrate.

…ment, env.example defaults, anchor compat fields
@Gravirei
Gravirei requested a review from jatmn July 26, 2026 05:15

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Preserve legacy certificate verification after assigning sequence numbers
    crates/gitlawb-node/src/db/mod.rs:928
    v12 gives every existing certificate the all-zero prev, then this migration assigns sequence numbers without backfilling that field. The verifier subsequently performs the 7-field legacy-signature fallback but, for every seq > 1, unconditionally compares the retained zero value to the predecessor's SHA-256 hash (arweave.rs:521). Thus every pre-upgrade certificate after the first in a repository is reported invalid even though its original signature verifies, contradicting the claimed compatibility with previously signed history. Mark legacy rows as non-chain records and skip this check for them, or migrate their linkage with an explicit versioned compatibility rule.

  • [P2] Pair the legacy Irys fallback with its gateway
    crates/gitlawb-node/src/main.rs:73
    Existing deployments that only set the documented GITLAWB_IRYS_URL=https://devnet.irys.xyz are migrated only for uploads: this copies the value into bundler_url but leaves arweave_gateway at https://arweave.net. Their new anchors are therefore written to Irys devnet while both the verify endpoint and the compatibility URL returned by /api/v1/arweave/anchors attempt to read mainnet, so the advertised verification/retrieval flow cannot work after an otherwise backward-compatible upgrade. Infer or require the matching gateway when using the legacy setting (without overriding an explicitly supplied new gateway).

  • [P2] Do not publish anchors that the new verifier must reject
    crates/gitlawb-node/src/api/repos.rs:1017
    Certificate issuance errors are logged and ignored, but the later anchoring loop still uploads the ref update with certificate: None (:1325) and persists a null cert_id. verify_anchor necessarily rejects that permanent artifact as having no embedded certificate. A transient DB/sequence failure after a successful push therefore creates an anchor that can never satisfy this PR's verification contract. Skip or retry anchoring until its certificate is persisted, or record and expose an explicit unverifiable/failed state instead of publishing it as a normal anchor.

  • [P3] Limit only the expensive verification endpoint
    crates/gitlawb-node/src/server.rs:229
    The new 120/hour IP bucket wraps both the gateway-fetching /verify/{tx_id} route and the pre-existing /anchors listing route. Requests to either consume the same quota, so verification traffic can make ordinary anchor-list clients receive 429 for an hour (and cheap list polling can block verification). Split the routers or apply this limiter to /verify/{tx_id} only; the stated reason for the quota is the new outbound gateway work.

@Gravirei
Gravirei requested a review from jatmn July 27, 2026 09:15

@beardthelion beardthelion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Edited 2026-07-27: the pusher-proof finding below had the mechanism wrong. The finding and the ask are unchanged; the reasoning and the suggested fix are corrected inline, and the follow-up is now #252.

Re-reviewed at 18946fa. The issuer anchor and the legacy-payload fallback both landed and I confirmed them by execution rather than reading: a self-signed certificate naming its own DID is now rejected, and a genuine pre-#224 certificate signed over the old seven fields verifies again through the fallback. The gateway-URL test is load-bearing now too (I broke the path construction and watched it go red), gl's new legacy test fails when I remove the fallback it covers, and this last commit's separate verify limiter, gateway pairing and skip-the-anchor-without-a-certificate all check out.

Two things block merge, both on the accept side of the verifier, and the newest commit widened the first one.

Findings

  • [P1] Cover seq and prev before reporting a legacy certificate as valid
    crates/gitlawb-node/src/arweave.rs:493, crates/gitlawb-node/src/arweave.rs:528
    The seven-field fallback payload omits seq and prev, so on that path both values are whatever the anchor says. Executed against the real handler at the previous head: I took a certificate the node genuinely signed pre-#224, changed nothing the signature covers, rewrote seq to 1 and prev to 64 fs, served it from a mock gateway and got valid: true with an empty errors array. The new all-zeros escape at :528 widens this rather than closing it. It keys on c.prev, which is exactly the field the fallback leaves unsigned, so the attacker no longer has to claim seq: 1 to skip the chain check. Executed on this head: same genuine legacy certificate, seq: 999999, prev left at the migration default, and the chain check is skipped with a warn! and the verdict is valid: true, errors: []. Reaching that state needs no privilege. GET /api/v1/repos/{owner}/{repo}/certs serves the certificate to an anonymous caller for any public repo (api/certs.rs:29 gates through authorize_repo_read), and sub-100 KiB Arweave uploads are permissionless, so anyone can place the bytes and hand the node a tx_id. The transition itself stays authentic. The append-only chain position is the property this PR adds, and on the legacy path nothing signs it. The invariant a fix has to preserve is the one both of these changes exist for: a pre-#224 certificate whose original signature verifies must still come back valid, so deleting the fallback is not the answer. [DIRECTION] Corroborate the unsigned fields against the node's own record rather than trusting the anchor for them (db.get_ref_certificate(&c.id) at db/mod.rs:2163 already returns the row), and error when they disagree, or stop asserting the chain in the verdict and return an explicit legacy marker so valid never covers a field nothing signed. If the all-zeros skip stays in some form it needs to be conditioned on the node's own record saying that row is a migrated one, not on a value the caller supplies.

  • [P1] Make the issuer check load-bearing before shipping it
    crates/gitlawb-node/src/arweave.rs:346
    I deleted the c.node_did != node_did block on this head and all 11 arweave tests stayed green. test_verify_anchor_malformed_node_did is the only test named for it and its assertion is an OR (does not match this node or invalid node DID); the fixture's malformed DID independently fails the parse a few lines down, so the arm that would catch a missing issuer check never has to fire. With the block gone, the exact self-signed forgery from the last round comes back at valid: true with an empty errors array. The reason it can hide is that no test anywhere drives this endpoint to an accepting verdict: both verify_anchor tests assert !valid, so nothing in CI distinguishes this verifier from one that rejects everything, and the prev-linkage and pusher-proof blocks have no test in either direction. Add a sign-then-verify test that builds the real 13-field payload, signs it with a generated keypair, serves it through mockito and asserts valid with empty errors. Once that oracle exists, split the issuer test so it asserts only the issuer error, and the negatives around it start meaning something.

  • [P2] Move the idx_ref_certs_repo_ref drop to a later migration
    crates/gitlawb-node/src/db/mod.rs:948
    v13 drops that index, but pre-#224 code inserts certificates with ON CONFLICT (repo_id, ref_name) (origin/main db/mod.rs:1995), which Postgres rejects once no matching unique index exists. During any rolling or multi-machine rollout the new instance runs the migration while old instances are still serving pushes, so their certificate issuance starts failing for the length of the window. The failure is swallowed at api/repos.rs:1017, so those pushes succeed with no certificate and, on new code, no anchor either, with only a warn line to show for it. Ship v13 with the new (repo_id, seq) index only and drop the old one in v14 next release.

  • [P2] Say what the pusher proof actually proves
    crates/gitlawb-node/src/arweave.rs:572

    Correction to the original version of this finding. I wrote that the pusher signs @method, @path and content-digest, "none of which mention ref_name, old_sha or new_sha," and that one captured proof therefore vouches for every transition on the repo path. That reasoning is wrong and I am retracting it. The component names are fixed, but the content-digest value is SHA-256 over the entire receive-pack body (gitlawb-core/src/http_sig.rs:200-204), the git-remote helper signs those real bytes (git-remote-gitlawb/src/main.rs:386), and the node parses old_sha new_sha ref_name out of the same bytes (api/repos.rs:875, parser at :1658). The pusher does cryptographically commit to the exact transition on every push, and a genuine proof commits to its own transition and no other.

    The finding and the ask are unchanged, because the defect is on the verification side. The node retains none of that commitment: require_signature forwards only AuthenticatedDid and drops the signature headers (auth/mod.rs:241), the body goes to git and is not kept, and ref_certificates has no column for pusher signature material. So the verifier has nothing to check the digest against and cannot tell a matched pairing from a mismatched one. Executed: I signed a real receive-pack proof, embedded it verbatim in a node-signed certificate claiming refs/heads/release moving 111... to ddd..., and the endpoint returned valid: true with no errors. The verdict still reduces to trusting the issuing node.

    My original suggested fix, adding a transition component to COVERED_COMPONENTS, is also retracted. It is a bidirectional flag day: that constant is both the signer's component set and the verifier's floor, an old client against a new node fails the missing_components gate, and a new client against an old node fails too because the verifier builds the signing string from the client's component list against a three-key map (auth/mod.rs:172-184). Eight production call sites are affected including three node-to-node paths, so a node upgrade would sever its own peers.

    What stands is the smaller ask: this PR should not describe the pusher proof as evidence the pusher authorized the transition. Drop the reuse argument from the comment, and correct api/arweave.rs:28, which still calls the pusher check "optional, informational" when valid: errors.is_empty() makes it blocking. The underlying gap and the real remedies are now #252, which also records the three traps I hit, so nobody spends the time I did.

  • [P3] Smaller items
    crates/gitlawb-node/src/db/mod.rs:935
    The v13 comment says existing prev values already match what was computed at issuance. That is not true of any pre-upgrade row, since origin/main's ref_certificates has no prev column at all and every migrated row carries v12's zero default. The new comment at arweave.rs:522 now says the opposite in the same PR, so one of the two is going to mislead whoever reads it next. GITLAWB_ARWEAVE_RATE_LIMIT (main.rs:317) is read through a raw std::env::var instead of a Config field, so it is missing from --help, README and .env.example, and a malformed value silently becomes 120. The new gateway inference keys on std::env::var(...).is_err(), so an operator who exports GITLAWB_ARWEAVE_GATEWAY= empty gets neither the explicit value nor the inferred one. The module doc at arweave.rs:18 still says transaction IDs are 43-character base58 while the validator you added checks base64url. The ref_certificates block in the v1 migration still carries v12's columns, which is why no test ever exercises v12's real ADD COLUMN, rename or DROP COLUMN path.

The certificate design continues to hold up and the fixes have been real ones every round. The pattern worth naming after five rounds is that every defect found so far has been on the accept side, and the suite still has no accepting-verdict test to see them with. Adding that test is worth more than any single fix in this list.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

The certificate-chain work from earlier rounds looks solid on this head: per-ref cert binding, append-only issuance, issuer pinning, streaming gateway cap, legacy CLI payload fallback, separate verify rate limiting, and skip-anchoring-when-no-cert are all in place. The remaining problems are on the verify accept path this PR adds.

Findings

  • [P1] Do not report valid: true for legacy certificates whose unsigned seq/prev were tampered
    crates/gitlawb-node/src/arweave.rs:493
    The guarded 7-field signature fallback verifies only repo_id, ref, and SHAs. seq and prev are not covered on that path. When prev is the migration default all-zero value and seq > 1, the verifier skips the chain check entirely (:528). A genuinely pre-#224 certificate whose 7-field signature still verifies can therefore be served with rewritten seq/prev and come back valid: true with empty errors. Public repos expose certificates over /api/v1/repos/{owner}/{repo}/certs, and sub-100 KiB Arweave uploads are permissionless, so this needs no push privilege. The all-zero skip was added to avoid false negatives on migrated rows, but it leaves chain position unsigned on the legacy accept path. Please corroborate seq/prev against the node's own stored row (db.get_ref_certificate) before accepting the legacy path, or return an explicit legacy/unverifiable-chain marker instead of a blanket valid: true.

  • [P2] Add a sign-then-verify acceptance test for the new endpoint
    crates/gitlawb-node/src/arweave.rs:703
    This is test debt, not a current runtime bug — the issuer check is present at :346. Every existing verify_anchor test still asserts only !valid or transport failures. There is no test that signs the real 13-field payload, serves it through a mock gateway, and expects valid: true with empty errors. That gap already hid a missing issuer check in an earlier round: deleting the c.node_did != node_did block leaves all arweave tests green, and test_verify_anchor_malformed_node_did does not exercise the issuer arm because the malformed DID fails parsing first. Please add the positive oracle first, then split issuer, prev-linkage, and pusher-context negatives so those guards cannot regress silently.

  • [P2] Pair the gateway when GITLAWB_BUNDLER_URL is set directly
    crates/gitlawb-node/src/main.rs:93
    Gateway inference currently runs only when the legacy GITLAWB_IRYS_URL fallback is used. An operator who sets only GITLAWB_BUNDLER_URL=https://devnet.irys.xyz still gets arweave_gateway = https://arweave.net from config defaults. Anchors upload to devnet, but /api/v1/arweave/verify/{tx_id} and /api/v1/arweave/anchors resolve through mainnet. .env.example now sets both knobs correctly, so the copy-paste path is fine; README and clap defaults still allow the mismatch for partial env setup. Mirror the legacy pairing for the new env name (without overriding an explicitly set gateway).

  • [P2] Defer dropping idx_ref_certs_repo_ref until after the rolling-upgrade window
    crates/gitlawb-node/src/db/mod.rs:948
    Migration v13 drops the (repo_id, ref_name) unique index in the same release that switches issuance to append-only INSERTs. origin/main still upserts with ON CONFLICT (repo_id, ref_name), which requires that index. During a mixed-version rollout, old binaries continue to issue certificates against the dropped index and fail; git_receive_pack swallows those failures (api/repos.rs:1017) and the new code then skips anchoring for the affected ref. This only bites if you roll nodes gradually; an all-at-once deploy avoids it. Ship v13 with only the new (repo_id, seq) index and drop the old one in v14 once old writers are gone.

  • [P3] Clamp negative limit on the anchors list route
    crates/gitlawb-node/src/api/arweave.rs:68
    list_anchors passes q.limit.min(200) straight into SQL. list_ref_certificates already clamps with .max(1) at the DB boundary, but list_arweave_anchors does not, so ?limit=-1 becomes LIMIT -1 and returns 500. Apply the same lower bound used elsewhere before querying.

  • [P3] Update stale pusher-proof wording on the verify route
    crates/gitlawb-node/src/api/arweave.rs:28
    The route comment still says the pusher check is "optional, informational", but verify_anchor treats missing/incomplete proof context as blocking. The deeper gap — the verifier cannot re-check the signed receive-pack body the digest names — is tracked in #252. Until that lands, please correct the endpoint/docs wording so callers do not treat valid: true as proof the pusher independently authorized the transition.

  • [P3] Publish the issued cert_id in P2P ref-update gossip
    crates/gitlawb-node/src/api/repos.rs:1285
    This push path now issues per-ref certificates and persists cert_id with anchors, but p2p.publish_ref_update still hardcodes cert_id: None. Peers cannot correlate gossiped ref updates with the certificate/anchor evidence this PR adds. Pass the issued certificate id through the event when present.

  • [P3] Drop unrelated Fly config deletions from this branch
    infra/fly/gitlawb-node-2.fly.toml, infra/fly/gitlawb-node-3.fly.toml
    Both production Fly app configs exist on main and are removed here with no replacement in infra/fly/. That is unrelated anchoring churn; please revert or justify the deletion separately.

  • [P3] Document GITLAWB_ARWEAVE_RATE_LIMIT with the other operator knobs
    crates/gitlawb-node/src/main.rs:317
    The dedicated verify limiter is read from a raw std::env::var and is absent from Config, --help, README, and .env.example. A malformed value silently falls back to 120/hour. Please expose it through the normal configuration surface.

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

Labels

crate:node gitlawb-node — the serving node and REST API kind:bug Defect fix — wrong or unsafe behavior subsystem:identity DID/UCAN, http-sig auth, push authorization subsystem:storage Blob/object store, Arweave, IPFS, archives

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden Arweave anchoring and add verification

5 participants