Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
50b955e
fix(node): harden Arweave anchoring and add verification (#26)
Gravirei Jul 20, 2026
a0235a5
fix(node): improve code formatting and readability in various modules
Gravirei Jul 20, 2026
2781259
fix(node): use URL_SAFE_NO_PAD base64 for verify, fetch predecessor f…
Gravirei Jul 20, 2026
7c3c1d0
fix(node): allow dead code for future-use methods, fmt/clippy clean
Gravirei Jul 20, 2026
d964b4d
fix(node): address review findings on Arweave anchoring and cert chai…
Gravirei Jul 22, 2026
ec55735
address review findings: backfill seq, cap verify body, --irys-url al…
Gravirei Jul 23, 2026
4fd48ab
suppress dead_code warnings on pool-based cert methods used by tests
Gravirei Jul 23, 2026
29c6390
include signature_input, content_digest, request_path in cert JSON re…
Gravirei Jul 23, 2026
e7d2ebc
address review findings: cert_id in anchor, graceful parse, arweave r…
Gravirei Jul 23, 2026
5e5ac31
address review findings: repo_id cross-check, gl cert payload, pusher…
Gravirei Jul 24, 2026
7d22395
fix: resolve certificate verification payload mismatch, update config…
Gravirei Jul 24, 2026
7521253
style: fix formatting in arweave tests
Gravirei Jul 24, 2026
4841843
address third review round: P1 issuer check, payload fallback, pusher…
Gravirei Jul 25, 2026
2f6fef4
address fourth review round: legacy cert fallback, pusher binding com…
Gravirei Jul 26, 2026
3ecf88b
address fifth review round: legacy prev, Irys gateway pairing, skip u…
Gravirei Jul 27, 2026
61d1d8a
fix: renumber arweave migrations to v18/v19 after rebasing onto main
Gravirei Aug 7, 2026
dd0e007
address sixth review round: legacy cert corroboration, repo/owner cro…
Gravirei Aug 10, 2026
5c68747
feat(node): anchor ref updates and manifests as signed ANS-104 data i…
Gravirei Aug 12, 2026
f1a279a
fix(node): fund anchoring, redact gateway URLs, fail closed, keep v1 …
Gravirei Aug 13, 2026
c21ab66
fix(node): ANS-104 deep-hash preimage, Irys payer token, tamper rejec…
Gravirei Aug 14, 2026
393418c
style(node): rustfmt the U5 ordering gate
Gravirei Aug 14, 2026
b390d40
fix(node): durable post-receive jobs, restart recovery, ANS-104 flat …
Gravirei Aug 15, 2026
eb86d4c
fix(node): make the Arweave anchor a durable job unit and require an …
Gravirei Aug 16, 2026
9b4be0e
fix(node): per-transition durable anchor outbox, distinct-signer thre…
Gravirei Aug 20, 2026
fecf4cd
fix(node): address R2 anchor-outbox invariants and cert versioning
Gravirei Aug 23, 2026
d53950f
fix(node): reconcile recovery probe with gateway JSON, sweep arweave …
Gravirei Aug 24, 2026
33ce3b4
fix(node): address R4 review — visibility error propagation, malforme…
Gravirei Aug 25, 2026
1ec5d98
fix(node): address R5 review — bind verify_anchor/probe to served ite…
Gravirei Aug 26, 2026
aa9fc8b
fix: resolve post-rebase compilation errors from conflict resolution
Gravirei Aug 26, 2026
04271ad
fix: restore migration ordering, fix CI test failures
Gravirei Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,37 @@ GITLAWB_DB_RETRY_MAX_SECS=60
GITLAWB_PINATA_JWT=
GITLAWB_PINATA_UPLOAD_URL=https://uploads.pinata.cloud/v3/files

# ── Arweave permanent anchoring (Irys devnet) ─────────────────────────────
# Leave empty to disable Arweave anchoring.
GITLAWB_IRYS_URL=https://devnet.irys.xyz
# ── Arweave permanent anchoring (Bundler / Arweave gateway) ───────────────────
# Bundler URL for permanent anchoring. Leave empty to disable anchoring.
# (Legacy name: GITLAWB_IRYS_URL)
# Anchoring is PAID, and the node refuses to start when a bundler URL is set
# without BOTH GITLAWB_BUNDLER_ACCOUNT (a funded account) and
# GITLAWB_BUNDLER_TOKEN (the token that account holds): Irys bills uploads at
# /tx/{token} via the x-irys-paid-by header, so a URL with no funded account and
# token would silently fail every anchor. Default (empty) disables anchoring.
GITLAWB_BUNDLER_URL=
# To enable, uncomment the devnet block below and fund the account via the
# bundler's devnet faucet (https://docs.irys.xyz/devnet/faucet), or use the
# production block with a funded wallet and https://node2.irys.xyz.
# Anchoring is PAID and needs the funded-account pair AND an explicit
# GITLAWB_ARWEAVE_GATEWAY for the SAME network: the node refuses to start with
# a bundler URL but no gateway, because an anchor is only resolvable through
# the gateway of the network that recorded it.
#
# Devnet:
#GITLAWB_BUNDLER_URL=https://devnet.irys.xyz
#GITLAWB_BUNDLER_ACCOUNT=<funded account address on devnet>
#GITLAWB_BUNDLER_TOKEN=matic
#GITLAWB_ARWEAVE_GATEWAY=https://devnet.irys.xyz
#
# Production (mainnet Irys + Arweave):
#GITLAWB_BUNDLER_URL=https://node2.irys.xyz
#GITLAWB_BUNDLER_ACCOUNT=<funded account address on mainnet>
#GITLAWB_BUNDLER_TOKEN=ethereum
#GITLAWB_ARWEAVE_GATEWAY=https://arweave.net
# Per-client-IP rate limit for the unauthenticated /api/v1/arweave/verify/:tx_id
# endpoint, in requests per hour. 0 disables. Default 120.
GITLAWB_ARWEAVE_RATE_LIMIT=120

# ── Base L2 smart contracts ───────────────────────────────────────────────
GITLAWB_CHAIN_RPC_URL=https://sepolia.base.org
Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ chrono = { version = "0.4", features = ["serde"] }
# uuid
uuid = { version = "1", features = ["v4"] }
# http client
reqwest = { version = "0.12", features = ["blocking", "json", "multipart", "rustls-tls"], default-features = false }
reqwest = { version = "0.12", features = ["blocking", "json", "multipart", "rustls-tls", "stream"], default-features = false }
# URL parsing (what reqwest::Url re-exports, so the shared redirect predicate can
# take a parsed URL without pulling reqwest into gitlawb-core)
url = "2"
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,11 @@ Important node settings:
| `GITLAWB_IPFS_RATE_LIMIT` | Max `/ipfs/{cid}` requests per client IP per hour (route flood brake). 0 disables. Default 600. |
| `GITLAWB_TIGRIS_BUCKET` | Optional S3/Tigris shared repo storage bucket. |
| `GITLAWB_PINATA_JWT` | Optional Pinata/IPFS warm-storage pinning. |
| `GITLAWB_IRYS_URL` | Optional Irys/Arweave permanent anchoring. |
| `GITLAWB_BUNDLER_URL` | Bundler URL for Arweave permanent anchoring (e.g., https://devnet.irys.xyz for devnet, https://node2.irys.xyz for mainnet Irys). Leave empty to disable. (Legacy name: `GITLAWB_IRYS_URL`). |
| `GITLAWB_BUNDLER_ACCOUNT` | Funded bundler account (public address/identity) that pays for uploads. The node's ANS-104 signature proves authorship, not payment — Irys only serves items backed by a funded account — so the node refuses to start when a bundler URL is set without this. It is sent as the `x-irys-paid-by` header on every upload. |
| `GITLAWB_BUNDLER_TOKEN` | Payment-token slug the funded account holds (e.g. `matic` on devnet, `ethereum` on mainnet). Irys bills uploads at `/tx/{token}`, so this names the token, not an API key, and is NOT sent as `x-irys-paid-by` (that header carries the account). The node refuses to start when a bundler URL is set without it. |
| `GITLAWB_ARWEAVE_GATEWAY` | Arweave gateway used to resolve anchors for `/verify` and the anchors listing. Has no default: the node refuses to start when a bundler is configured without an explicit gateway, because an anchor is only resolvable through the gateway of the network that recorded it (a devnet bundler pairs with the devnet gateway, mainnet Irys with `https://arweave.net`). |
| `GITLAWB_ARWEAVE_RATE_LIMIT` | Per-client-IP rate limit for the verify endpoint, requests per hour (defaults to 120; `0` disables). |

Production note: change the default Postgres password before exposing a node publicly.

Expand Down
24 changes: 24 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 14 d
- A supplied token's signature, audience, expiry, and proof-chain attenuation are validated.
- Tokens use a signed JSON wire format with expiry.
- Capability grants are not yet consulted by repository write authorization; see the limitations below.
**UCAN capability tokens**
- Issued at registration as a signed JSON envelope `{ "payload": {...}, "s": "<base64url Ed25519 signature over the payload JSON>" }` — not a JWT (#224 review: the policy must describe the actual wire format)
- Capability-scoped: `git/push`, `git/fetch`, `issue/create`, `pr/open`
- Expiry enforced on every verification
- The auth middleware (`require_ucan_chain`) verifies the full delegation chain when the `X-Ucan` header is present: the UCAN issuer must match the HTTP Signature identity, the audience must be this node's DID, and every proof in the chain must be cryptographically sound with no capability escalation

**Authorization**
- Every repo-scoped read and mutation binds the caller to an authorization decision before serving or mutating anything
- Per-repository read enforcement is wired: `authorize_repo_read` denies with the same 404 a missing repo returns, and content endpoints pass the specific path so a withheld subtree is denied even on an otherwise-public repo
- Owner-only mutations (visibility, webhooks, protected branches, merges) are gated to the repo owner; star/unstar, replica registration, and bounty actions have their own intended gates

**Smart contracts (Base Sepolia testnet)**
- `GitlawbDIDRegistry` — on-chain DID → document registry
Expand All @@ -57,6 +67,16 @@ These are documented limitations of the current live release. They should be pri
- `git-receive-pack` verifies HTTP Signatures, but `GITLAWB_ENFORCE_OWNER_PUSH` defaults to `false` for compatibility during rollout.
- **Impact:** With the default setting, a valid signature authenticates the pusher but does not require that DID to be the repository owner.
- **Mitigation:** Set `GITLAWB_ENFORCE_OWNER_PUSH=true` on nodes where owner-only pushes are required. Confirm that every legitimate pusher uses the owner DID before enabling it.
### UCAN chain validation is optional per request
- The middleware verifies the full UCAN delegation chain only when the client presents an `X-Ucan` header. Requests without the header pass through unchanged, so agents that predate UCAN delegation are not forced off.
- **Impact:** A client can still authenticate with a bare RFC 9421 HTTP Signature and skip delegation-chain enforcement entirely; capability delegation is enforced only for clients that opt into presenting a UCAN.
- **Mitigation:** Keep write endpoints signed, treat public nodes as public infrastructure, and treat trust scores as soft rate-limiting signals rather than authorization.
- **Fix target:** make UCAN presentation mandatory for pushes (planned together with owner-push enforcement).

### Owner-push enforcement defaults off
- `GITLAWB_ENFORCE_OWNER_PUSH` defaults to `false`: a valid did:key HTTP Signature is authentication, not authorization, so any registered agent can push to a repo until the operator enables owner-only writes.
- **Impact:** Anyone who can register an agent can push to any repo while the flag is off.
- **Mitigation:** Enable `GITLAWB_ENFORCE_OWNER_PUSH=true` in production; keep write endpoints signed in the meantime.

### UCAN delegation and revocation
- The middleware validates a supplied UCAN's complete proof chain, but a root token is accepted without an independently trusted issuer anchor. `Ucan::can` is not yet used by write handlers, so a UCAN does not grant scoped repository access.
Expand Down Expand Up @@ -85,6 +105,9 @@ These are documented limitations of the current live release. They should be pri
### GraphQL mutation coverage
- Existing GraphQL mutations require an authenticated signer, but a mutation-specific source-level guardrail has not yet been added for future mutations.
- **Impact:** A new mutation could accidentally omit its signer check without an explicit test fence.
- Per-repository private-read enforcement IS wired: `authorize_repo_read` and per-path visibility rules deny non-readers with an opaque 404, on reads and writes alike.
- **Impact:** The remaining risk is operational, not structural: a public node should still not be handed secrets, because read access is granted by the repo owner's visibility rules and any node operator can see everything stored on their own node.
- **Mitigation:** Keep secrets on isolated nodes and restrict network access at the reverse proxy or firewall layer.

### Peer route hardening rollout
- Peer announce and sync notification routes accept signed requests and verify DID matches when a signature is present.
Expand Down Expand Up @@ -112,6 +135,7 @@ These are documented limitations of the current live release. They should be pri
| Content hashing | SHA-256 via CIDv1 |
| HTTP Signatures | RFC 9421 (Ed25519 + SHA-256 Content-Digest) |
| UCAN tokens | Signed JSON object (Ed25519 signature) |
| UCAN tokens | Signed JSON envelope (Ed25519 over the payload JSON), not JWT |
| On-chain | ECDSA secp256k1 (Base L2 / Ethereum) |

---
Expand Down
Loading
Loading