Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
162dd33
feat(node): implement reconciliation sweep as durability backstop (#218)
Gravirei Aug 27, 2026
818ba0a
fix: add missing parameters to pinning functions and adjust timeout s…
Gravirei Aug 27, 2026
3e0b01b
fix(tests): update CID assertions to handle Option type and adjust pa…
Gravirei Aug 27, 2026
4941af5
fix(node): close review findings on reconciliation sweep (#218)
Gravirei Aug 27, 2026
31457cf
feat: implement per-backend continuation offset for missing OIDs
Gravirei Aug 28, 2026
c4babfe
fix(node): trace provenance/structural invariants through every consumer
Gravirei Aug 29, 2026
91d0578
fix(node): structural tree gate at every depth + non-commit ref toler…
Gravirei Aug 29, 2026
f28fa18
test(node): ignore pre-existing RED replication-tail test on CI
Gravirei Aug 29, 2026
8fd6302
fix(node): align encrypted manifest anchor log with one-shot contract
Gravirei Aug 30, 2026
af36c20
fix(node): address eight reviewer findings on #218 reconciliation swe…
Gravirei Aug 31, 2026
1f915ef
fix(node): close audit.toml array and drop inline comment
Gravirei Aug 31, 2026
8803067
fix(node): repair the stale for-each-ref fake-git fixture (round 8 P1)
kevincodex1 Aug 31, 2026
ea95f10
fix(node): peel annotated tags in the blob_paths ref walk (round 8 P1)
kevincodex1 Aug 31, 2026
d1f5fe6
fix(node): one empty-path decision for both blob_paths consumers (rou…
kevincodex1 Aug 31, 2026
d565f06
test(node): bind the phase-2 ref tests to a commit-unreachable blob (…
kevincodex1 Aug 31, 2026
7fd5d22
fix(node): advance the reconciliation continuation only for dispatche…
kevincodex1 Aug 31, 2026
5670f36
fix(gl): render the Pinata provider CID in `gl ipfs list` (round 8 P2)
kevincodex1 Aug 31, 2026
cf8cb39
fix(node): implement third fence check for IPFS pinning and enhance v…
Gravirei Aug 31, 2026
4e9729f
fix(node): enhance documentation and add tests for ProgressState and …
Gravirei Aug 31, 2026
4bcb88d
fix(node): address round-9 reviewer findings on #218 reconciliation s…
Gravirei Sep 1, 2026
5243547
fix(node): correct consumer-4 recipient test for call-invariant set
Gravirei Sep 1, 2026
594c7b3
fix(git): fail closed on non-UTF-8 tree listing in walk_tree_oids_inn…
Gravirei Sep 3, 2026
d9992a4
fix(recon): run encrypted recovery even when public list is empty (#2…
Gravirei Sep 3, 2026
0c4f123
fix(db): rename migration v12 test to v32 (#218 round 10 P3)
Gravirei Sep 3, 2026
1be9a70
fix(git): index OID membership before quadratic classifier scan (#218…
Gravirei Sep 3, 2026
75053af
fix(git): bound tree walk by ls-tree invocation count, not just depth…
Gravirei Sep 3, 2026
074ac26
fix(pinata): suppress push when DB record is not durable (#218 round …
Gravirei Sep 3, 2026
1634a04
fix(node): serialize policy narrow with irreversible upload via in-pr…
Gravirei Sep 3, 2026
7b9ebfc
fix(pinata): update post-upload stalled record test for round 10 P2 c…
Gravirei Sep 4, 2026
244ba36
fix: clippy allow + rustfmt on round 10 commits
Gravirei Sep 4, 2026
34b5979
fix(node): address round-11 review findings on #218 reconciliation sweep
Gravirei Sep 4, 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
17 changes: 17 additions & 0 deletions .cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,21 @@ ignore = [
# ever built with the `mysql` feature, or any other consumer of rsa enters
# the build, at which point it becomes a real reachable advisory.
"RUSTSEC-2023-0071", # rsa Marvin attack (no fix; not linked in our build)

# lru 0.12.5 (use-after-free in pop()). Reachable via aws-sdk-s3
# (reverted from the upgrade that pulled in lru 0.12.5). The
# lockfile currently has 0.12.5 (via aws-sdk-s3) AND 0.16.3 (via
# alloy). The RUSTSEC-2026-0253 advisory covers both ranges, and
# this ignore is still load-bearing for 0.12.5 — a future alloy
# release that bumps 0.16.x to a fixed version would let this
# ignore retire for the 0.16 range. REMOVE only when BOTH
# lockfile versions are fixed (or removed).
"RUSTSEC-2026-0253",
#
# Round-3 P2 (reviewer 2): the previous version of this file
# ignored RUSTSEC-2026-0258 for h2 0.4.13, but the lockfile
# already has h2 0.4.18 (verified 2026-08-31: cargo tree -p h2
# shows 0.4.18) — the fix is in. The ignore was stale and would
# have silently re-accepted the advisory if h2 dropped back to
# 0.4.13. Removed.
]
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,20 @@ GITLAWB_TRUSTED_PROXY=
# Enable automatic background sync from known peers
GITLAWB_AUTO_SYNC=false

# ── Reconciliation sweep ─────────────────────────────────────────────────
# Periodic durability sweep: re-derives the public pin set and the withheld-blob
# recovery set each hour and fills gaps so a dropped replication job never means
# data loss. Defaults to true; set to false to disable the sweep even when a pin
# backend (IPFS/Pinata) is configured.
#
# Phase-capability matrix:
# - Public pin repair: IPFS-only, Pinata-only, or both (requires the
# respective backend to be configured).
# - Encrypted recovery repair: requires local IPFS (GITLAWB_IPFS_API).
# Pinata-only nodes reconcile public pins only; encrypted recovery
# reconciliation is not performed.
GITLAWB_RECONCILIATION_SWEEP=true

# ── iCaptcha proof-of-intelligence gate ───────────────────────────────────
# Optional gate on create_repo + register: require callers to present an
# iCaptcha proof (X-ICaptcha-Proof header) earned at icaptcha.gitlawb.com.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ Important node settings:
| `GITLAWB_REQUIRE_SIGNED_PEER_WRITES` | Require signed peer announce/sync writes. Defaults to `false` during the staged rollout below. |
| `GITLAWB_ENFORCE_OWNER_PUSH` | Require the authenticated pusher to be the repo owner on `git-receive-pack`. **Defaults to `true`.** A `did:key` signature is authentication, not authorization — anyone can mint a key and sign — so with this off every signed caller may push to every repository, private ones included. Delegated and CI keys count as non-owners: a UCAN `git/push` capability is verified but not yet honored for authorization, so they cannot push while this is on. Set `false` only for a rolling upgrade; see [`docs/RUN-A-NODE.md`](docs/RUN-A-NODE.md). |
| `GITLAWB_AUTO_SYNC` | Enable automatic sync from known peers. |
| `GITLAWB_RECONCILIATION_SWEEP` | Enable the hourly durability sweep that re-pins/backstops missing objects (default `true`; disabled when no IPFS/Pinata backend is configured). Public pin repair runs against any configured backend (IPFS, Pinata, or both). Encrypted recovery repair requires local IPFS (`GITLAWB_IPFS_API`); Pinata-only nodes reconcile public pins only. |
| `GITLAWB_MAX_PACK_BYTES` | Max git pack body size for smart-HTTP routes. |
| `GITLAWB_GIT_SERVICE_TIMEOUT_SECS` | Max seconds a served git upload-pack, receive-pack, or `info/refs` advertisement may run before it is aborted (504). Default 600. Also bounds the withheld-blob classification walk (on both the upload-pack serve and receive-pack replication paths) and the push-side pin-candidate discovery (`rev-list` / `cat-file`), each reaped via process-group teardown at the deadline. On the path-scoped upload-pack path the classification walk and the pack serve share ONE deadline, so this value bounds their combined duration rather than granting each stage a full budget: a walk that consumes it leaves the serve nothing and the clone gets a 504. Serving large path-scoped repos may therefore need a higher value than they did when each stage was budgeted separately. Accepted range is 1 to 3153600000 (100 years), since the node derives deadlines from this value and a larger one cannot be represented. |
| `GITLAWB_GIT_ACQUIRE_TIMEOUT_SECS` | Max seconds the storage-acquisition phase (Tigris HEAD/GET, push advisory-lock) of a served git op may run before the request is shed with a 503, separate from the git-run timeout. The concurrency permit is released on expiry so a stalled backend cannot pin the pool. Default 30. |
Expand Down
31 changes: 31 additions & 0 deletions crates/gitlawb-attest/src/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,37 @@ mod tests {
assert!(matches!(err, Error::Signature(_)));
}

/// The identity-point forgery must be rejected: the shared attestation
/// verifier is a cert-bound provenance gate, so accepting the weak-key
/// signature would let anyone mint a forged attestation that verifies.
/// Strict verification rejects small-order public keys and R (the identity
/// point here), which ordinary verification does not.
#[test]
fn verify_rejects_identity_point_forgery() {
let cert_hash = sample_cert_hash();
let mut att = dummy_attestation(&fresh(), cert_hash);

// Public key A = identity point (0,1); signature R = identity, S = 0.
// The equation `[S]B = R + [k]A` then holds for any k and any message.
let identity = [
1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
];
let mut buf = Vec::with_capacity(34);
buf.extend_from_slice(&ED25519_MULTICODEC);
buf.extend_from_slice(&identity);
att.signer = format!(
"did:key:{}",
multibase::encode(multibase::Base::Base58Btc, &buf)
);
let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&identity);
att.sig = B64U.encode(sig);

let err = att.verify_signature(cert_hash).unwrap_err();
assert!(matches!(err, Error::Signature(_)));
}

/// A payload that happens to contain a `cert_hash` field of its own does
/// not interfere with the outer binding: the attestation envelope's
/// `cert_hash` is the only field consulted by `verify_signature`, and the
Expand Down
40 changes: 40 additions & 0 deletions crates/gitlawb-core/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ impl Keypair {
}

/// Verify an Ed25519 signature.
///
/// Strict verification: rejects small-order `R` and small-order public keys
/// (the identity point, and any point of low order). Ordinary `verify` accepts
/// a signature forged with the identity point as the public key plus
/// `R = identity, S = 0`, which verifies for *any* message. `identity::verify`
/// is the shared primitive behind HTTP request authentication, UCANs, and
/// certificates, so weak-key acceptance is an authentication bypass, not a
/// malleability nuance.
pub fn verify(verifying_key: &VerifyingKey, msg: &[u8], sig_bytes: &[u8; 64]) -> Result<()> {
let sig = Signature::from_bytes(sig_bytes);
verifying_key
Expand Down Expand Up @@ -208,6 +216,38 @@ mod tests {
);
}

/// The identity-point forgery: with public key A = identity, R = identity,
/// and S = 0, the equation `[S]B = R + [k]A` holds for every message,
/// because `[k]·identity = identity`. Ordinary (non-strict) Ed25519
/// verification accepts it, so the shared `verify` primitive must use
/// strict verification, which rejects small-order R and public keys.
#[test]
fn verify_rejects_identity_point_forgery() {
use ed25519_dalek::Verifier;
// The identity point (0,1) compresses to y = 1 with sign bit 0.
let identity = [
1u8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0,
];
let vk = VerifyingKey::from_bytes(&identity).expect("identity point is on the curve");
let mut sig_bytes = [0u8; 64];
sig_bytes[..32].copy_from_slice(&identity);
let msg = b"arbitrary message the key owner never signed";

// Prove the forged signature satisfies the ordinary verification
// equation, so the strict check below is what actually defends the
// boundary (not a signature that was already invalid everywhere).
assert!(
vk.verify(msg, &Signature::from_bytes(&sig_bytes)).is_ok(),
"identity-point forgery must satisfy ordinary verification (this is why strict is needed)"
);

assert!(
verify(&vk, msg, &sig_bytes).is_err(),
"strict verification must reject the identity-point forgery"
);
}

#[test]
fn verify_rejects_weak_key_signature() {
// Regression guard for strict verification: a signature forged under a
Expand Down
1 change: 1 addition & 0 deletions crates/gitlawb-node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ alloy = { version = "1", default-features = false, features = [
"rpc-types-eth",
] }
libp2p-dns = { version = "0.44.0", features = ["tokio"] }
rand = { workspace = true }

[dev-dependencies]
mockito = "1"
Expand Down
Loading
Loading