Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c03d84f
fix(node): harden Arweave anchoring and add verification (#26)
Gravirei Jul 20, 2026
f15b518
fix(node): improve code formatting and readability in various modules
Gravirei Jul 20, 2026
3a8005c
fix(node): use URL_SAFE_NO_PAD base64 for verify, fetch predecessor f…
Gravirei Jul 20, 2026
b9f2ee9
fix(node): allow dead code for future-use methods, fmt/clippy clean
Gravirei Jul 20, 2026
42c0475
fix(node): address review findings on Arweave anchoring and cert chai…
Gravirei Jul 22, 2026
2049f79
address review findings: backfill seq, cap verify body, --irys-url al…
Gravirei Jul 23, 2026
511d379
suppress dead_code warnings on pool-based cert methods used by tests
Gravirei Jul 23, 2026
759ef3f
include signature_input, content_digest, request_path in cert JSON re…
Gravirei Jul 23, 2026
cf51c26
address review findings: cert_id in anchor, graceful parse, arweave r…
Gravirei Jul 23, 2026
7723d9b
address review findings: repo_id cross-check, gl cert payload, pusher…
Gravirei Jul 24, 2026
9fa2577
fix: resolve certificate verification payload mismatch, update config…
Gravirei Jul 24, 2026
21c7248
style: fix formatting in arweave tests
Gravirei Jul 24, 2026
d7236d9
address third review round: P1 issuer check, payload fallback, pusher…
Gravirei Jul 25, 2026
b9b1446
address fourth review round: legacy cert fallback, pusher binding com…
Gravirei Jul 26, 2026
00f866b
address fifth review round: legacy prev, Irys gateway pairing, skip u…
Gravirei Jul 27, 2026
12f1e38
fix: renumber arweave migrations to v18/v19 after rebasing onto main
Gravirei Aug 7, 2026
86d4935
address sixth review round: legacy cert corroboration, repo/owner cro…
Gravirei Aug 10, 2026
6712fd3
Merge branch 'main' of github.com:Gitlawb/node into fix/issue-26-hard…
Gravirei Aug 11, 2026
99acf93
feat(node): anchor ref updates and manifests as signed ANS-104 data i…
Gravirei Aug 12, 2026
c6fa946
Merge remote-tracking branch 'upstream/main' into fix/issue-26-harden…
Gravirei Aug 13, 2026
33a6f24
fix(node): fund anchoring, redact gateway URLs, fail closed, keep v1 …
Gravirei Aug 13, 2026
7ef8e7c
fix(node): ANS-104 deep-hash preimage, Irys payer token, tamper rejec…
Gravirei Aug 14, 2026
4b2aeb4
style(node): rustfmt the U5 ordering gate
Gravirei Aug 14, 2026
6bf13d1
fix(node): durable post-receive jobs, restart recovery, ANS-104 flat …
Gravirei Aug 15, 2026
edd4036
fix(node): make the Arweave anchor a durable job unit and require an …
Gravirei Aug 16, 2026
30b95ac
fix(node): per-transition durable anchor outbox, distinct-signer thre…
Gravirei Aug 20, 2026
257a5a7
Merge remote-tracking branch 'upstream/main' into fix/issue-26-harden…
Gravirei Aug 20, 2026
386e0a3
chore(node): bump h2 to 0.4.16 (RUSTSEC-2026-0258)
Gravirei Aug 20, 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
19 changes: 17 additions & 2 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 }
# HMAC
hmac = "0.12"

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,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
22 changes: 17 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 14 d
- Every git object is content-addressed via CIDv1 (SHA-256)
- Tamper-evident by construction — a modified object changes its CID

**UCAN token validation**
- Bootstrap UCAN tokens are issued at registration.
- A supplied token's signature, audience, expiry, and proof-chain attenuation are validated.
- Tokens use a signed JSON wire format with expiry.
**UCAN capability tokens**
- Issued at registration as a signed JSON envelope `{ "payload": {...}, "s": "<base64url Ed25519 signature over the payload JSON>" }` — not a JWT
- 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
- Capability grants are not yet consulted by repository write authorization; see the limitations below.

**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
- `GitlawbNameRegistry` — human name → DID registry
Expand All @@ -53,6 +59,12 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 14 d

These are documented limitations of the current live release. They should be prioritized without breaking existing nodes during rolling upgrades.

### 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).

### Repository write authorization defaults
- `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.
Expand Down Expand Up @@ -111,7 +123,7 @@ These are documented limitations of the current live release. They should be pri
| Key storage | PKCS#8 PEM, 0600 permissions |
| 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