Signet — Reputation & Trust Model (Design v0.1)
How Signet keeps attestation data trustworthy when both humans and AI agents can write to it. Covers threat model, identity tiers, the reputation algorithm, verification weighting, Sybil resistance, and trust-on-read semantics. Related: requirements.md (FR-3, NFR-4)
1. First Principles
Reputation in Signet exists to answer exactly one question on the read path:
**"Given this vetted-clean attestation, what is the probability it is correct?"**Everything else (scores, badges, upvotes) is instrumentation toward that estimate. Four principles fall out of this:
- Reputation must be predictive, not decorative. A score is only useful if it
correlates with attestation accuracy. Activity volume must never raise a score — only *validated* outcomes may.
- Faking reputation must cost more than the value it unlocks. An AI-agent swarm
can produce unlimited attestations at near-zero cost. Every scoring input must therefore be gated by something scarce: verified identity, staked reputation, or elapsed time.
- Security audits have delayed objective ground truth. Unlike social voting,
a vetted-clean claim is eventually *falsifiable* — a CVE or confirmed vulnerability disclosed in the attested scope proves it wrong. The model must exploit this asymmetry: slow to reward, brutal on falsification.
- Trust is subjective in a decentralized system (NFR-3). There is no global
admin. Consumers must be able to choose their own trust anchors and minimum thresholds, the same way PGP web-of-trust and TUF delegations work.
2. Threat Model
| # | Attack | Actor | Goal |
|---|---|---|---|
| T1 | Attestation spam | Low-quality AI agents | Flood registry with cheap vetted-clean claims to farm activity |
| T2 | Sybil swarm | One operator, many identities | Create N fake identities that upvote each other into credibility |
| T3 | Collusion ring | Small group of real identities | Mutual verification to inflate scores without independent review |
| T4 | Poisoning / supply-chain assist | Malicious actor | Publish false vetted-clean on code containing a known backdoor so CI pipelines skip scanning it |
| T5 | Reputation laundering | Established account | Build score on trivial audits, spend it on one malicious attestation |
| T6 | Lazy verification | Careless expert | Rubber-stamp verifications without review, diluting signal |
| T7 | Griefing / false flags | Competitor or troll | Flag legitimate attestations to damage a rival's score |
| T8 | Stale-scope drift | Nobody (entropy) | Old attestations imply coverage on code that has since changed |
T4 is the existential threat. A registry that convinces pipelines to skip scanning a backdoor is worse than no registry. The design below treats false vetted-clean as the maximum-severity event.
3. Identity Tiers (Sybil cost floor)
Reputation math is meaningless if identities are free. Every identity gets a tier that sets its *prior* reputation and its rate limits (NFR-4). Tiers map directly to FR-3's identity providers.
| Tier | Identity proof | Prior (R₀) | Submission rate limit |
|---|---|---|---|
T0 anonymous-key | Self-generated key pair only | 0.00 | Minimal (queued, low priority) |
T1 linked-account | GitHub OAuth (account age & activity checked) | 0.05 | Low |
T2 verified-identity | Sigstore/OIDC token from an established IdP, or PGP key with WoT signatures | 0.10 | Moderate |
T3 vouched | T2 plus endorsement by ≥ 2 existing contributors above reputation θ_vouch | 0.20 | Standard |
T4 organization | Signed org attestation (company/foundation security team, domain-verified) | 0.30 | High |
Rules:
- Tier sets the floor, behavior sets the ceiling. A T4 org that ships bad
attestations decays below a diligent T1 individual. Tier is a prior, never a cap on penalties.
- **AI agents are first-class but must declare
auditor.type: "agent"** and bind to
an *operator identity* (the human/org that runs them). Agent reputation is sub-keyed under the operator: R(agent) ≤ R(operator). An operator's agents share one rate-limit pool — 1,000 agents give an operator zero extra throughput. This single rule neutralizes most of T2 at the agent layer.
- New identities enter a probation window: their attestations are stored and
queryable but marked unweighted (excluded from coverage-status computation) until either (a) k₁ of their attestations are verified by established auditors, or (b) the identity is vouched to T3. *Implementation:* shipped behind signet.trust.probation-enabled (default off — the M2 bootstrap keeps tier-prior semantics until a deployment opts in); "established auditor" is a verifier with R ≥ θ_verify, and seed anchors are exempt by definition.
4. Reputation Algorithm
Two layers, multiplied together. Layer A measures *evidence about this identity's own track record*; Layer B measures *how the web of trust regards them*, computed Sybil-resistantly.
R(a) = E(a) × G(a), floored at the tier prior R₀ until the identity is penalizedThe tier prior R₀ (§3) is a *prior*, not a cap: it is the starting reputation a fresh or purely-positive identity reads, so it floors E·G only while the identity carries no active (un-decayed) negative evidence. The moment a penalty lands (β > β₀), the floor lifts and R = E·G sinks freely — a falsified attestation must decay an identity *below* a diligent lower-tier peer (§3, "tier is a prior, never a cap on penalties"). This is what makes the T4/T5 slashing defenses bite; a hard floor would pin a proven-malicious high-tier identity at its starting reputation, indistinguishable from a clean one. As the slash decays (λ, ~12 mo) and positive evidence accrues, E·G recovers on its own — the prior does not snap back.
4.1 Layer A — Evidence score E(a) (Beta reputation with decay)
Based on the Jøsang–Ismail beta reputation system: model each auditor as a Bernoulli process and estimate their accuracy from observed outcomes.
E(a) = α / (α + β)
α = Σ w_pos(event) · λ^Δt (positive evidence)
β = β₀ + Σ w_neg(event) · λ^Δt (negative evidence, β₀ = pessimistic prior)λ^Δtis exponential time decay (half-life ≈ 12 months): reputation must be
maintained, countering T5 laundering with stale credit.
β₀ > 0encodes "innocent until proven, but not trusted until proven."
Positive evidence events (slow accrual):
| Event | Weight |
|---|---|
| Attestation verified by independent auditor with rep R_v | + R_v · independence(a, v) |
| Attestation survives a formal dispute (flag resolved in author's favor) | + 2.0 |
active-vulnerability-found attestation confirmed by upstream fix/CVE | + 5.0 (finding real bugs is the strongest skill signal) |
| Verification the auditor *gave* is later corroborated | + 0.5 (rewards diligent reviewing) |
Negative evidence events (fast, heavy):
| Event | Weight |
|---|---|
CVE/confirmed vuln disclosed in scope of their vetted-clean attestation, matching an analyzed_vulnerabilities class they claimed to check | − 20.0 (falsification — the T4 case) |
Same, but vuln class was *outside* declared analyzed_vulnerabilities | − 2.0 (scoped honesty is punished lightly — this incentivizes narrow, precise scopes) |
| Flag upheld against their attestation | − 8.0 |
| Verification they gave on an attestation later falsified | − 10.0 (verifier slashing — see §5) |
| Flag they raised is rejected as bad-faith | − 4.0 (counters T7) |
Design note: the asymmetry (+1-ish vs −20) is deliberate. With ground truth available only occasionally, a single falsification must erase months of farming (T1, T5). An auditor at 95% accuracy stays healthy; a spammer producing bulk unverified claims gains nothing (unverified attestations contribute zero to α).
4.2 Layer B — Graph trust G(a) (personalized EigenTrust)
Layer A can be gamed by a collusion ring (T3): members verify each other, all gain α. Layer B breaks this by asking *who* is doing the verifying, globally.
- Build the endorsement graph: edge
v → aweighted by verifications and vouches
v has given a (normalized per-voucher, so each identity distributes a fixed total of 1.0 trust — endorsing everyone means endorsing no one).
- Compute EigenTrust with a pre-trusted seed set P: trust propagates from P
through the graph with damping factor d (≈ 0.85, PageRank-style teleport back to P).
G⁽ᵗ⁺¹⁾ = (1 − d) · p + d · Cᵀ · G⁽ᵗ⁾- Seed set P = named, public, auditable anchor identities (e.g., OpenSSF,
distro security teams, well-known researchers) — governed openly, like browser root-CA programs but lower stakes because *seeds only accelerate trust, never gate participation*.
- A Sybil ring with no inbound path from P gets
G ≈ (1 − d) · p_min ≈ 0, so its
internally-farmed E scores are multiplied to nothing. This is the standard, well-studied defense for T2/T3.
- Decentralization escape hatch (NFR-3): the seed set is a *parameter, not a
constant*. Mirrors and downstream consumers can recompute G from the exported dump with their own seeds. The default seeds are a convenience for the hosted API, not a central authority.
4.3 Independence factor
Every verification carries independence(a, v) ∈ [0, 1], discounting:
- same operator or org → 0 (self-verification is void)
- reciprocity (v verifies a, a verifies v repeatedly) → decays toward 0.2
- repeated pairings (v's 10th verification of a is worth far less than the 1st) →
1/√n schedule
- graph proximity (v and a trust-connected only through each other) → discounted
*Implementation:* the same-operator/org component ships mechanically — a verification from the author's operator, from one of the author's agents, or from a sibling agent under the same operator is void in both evidence and confidence. Reciprocity, the 1/√n schedule, and graph-proximity discounts arrive with the full engine; the M2 first-per-verifier cutoff bounds repeated pairings harder than 1/√n in the interim.
5. Verification: Skin in the Game
FR-3's "Upvote/Verify" is redefined as a staked co-signature, not a like button:
- A verification is itself a signed attestation:
{verifier, attestation_id, depth: "acknowledged" | "reviewed" | "reproduced", signature}.
- Only
reviewedandreproducedverifications add evidence weight
(reproduced counts ×1.5). acknowledged is display-only.
- Verifier slashing: if the underlying attestation is falsified, every
reviewed/reproduced verifier takes the −10.0 hit (§4.1). Verification is lending your reputation, with recourse. This is the primary defense against T6 rubber-stamping — and it makes T3 rings *self-destructive*: one falsified attestation slashes the whole ring.
- Verifications require
R(v) ≥ θ_verify— below that they're recorded but
unweighted ("authorized experts" per FR-3).
Flags are the symmetric negative: staked challenges with evidence attached, routed to a dispute queue reviewed by high-reputation auditors in the relevant ecosystem. Upheld → author slashed; rejected as bad-faith → flagger slashed (T7).
6. Trust-on-Read: Attestation Confidence & Coverage Status
The read path (FR-2) never returns a bare boolean. Each attestation gets a confidence score, combined per-file into coverage status.
6.1 Attestation confidence
Author and verifiers are treated as noisy independent endorsers (noisy-OR), attenuated by age and scope drift:
C(att) = [1 − (1 − R(author)) · Π_v (1 − R(v) · depth_v · indep_v)] · fresh(att)
fresh(att) = 1 if file sha256 at queried commit == attested sha256
= 0 otherwise (content changed → attestation void for this query)
× λ_age^Δt (slow decay even for identical hashes; tooling and
vulnerability knowledge advance — a 2019 "clean" is weaker
evidence in 2026)The hash-match rule kills T8 mechanically: attestations bind to content, so scope drift can't leak coverage. Line-range attestations void if the file hash changes (v0.1 conservative choice; smarter diff-aware carry-forward is future work).
6.2 Coverage status (the skip-scan contract)
GET /v1/coverage?purl=...&commit=...&min_confidence=0.8&trust_root=default
→
{
"status": "fully-audited" | "partially-audited" | "no-audit-history" | "disputed",
"files": [
{ "path": "lib/router/index.js", "confidence": 0.93,
"attestations": ["urn:audit:..."], "vuln_classes_covered": ["CWE-79","CWE-89"] }
]
}- **
min_confidenceis caller-chosen.** Signet reports evidence; the *consumer*
decides their risk tolerance (principle 4). A hobby project may skip-scan at 0.6; a bank pins 0.95 + trust_root=own-seed-list.
- **
disputedis a first-class status**: any open flag on a covering attestation
surfaces immediately, and pipelines should treat disputed as uncovered.
- Coverage is also per vulnerability class:
vetted-cleanfor CWE-89 says
nothing about CWE-79. Skip-scan logic can suppress only the rule families actually covered — this is what makes narrow-scope honesty (§4.1) usable.
7. Attack ↔ Mitigation Matrix
| Threat | Primary mitigations |
|---|---|
| T1 spam | Unverified attestations add zero score; rate limits by tier; probation window |
| T2 Sybil swarm | Identity-tier cost; operator-pooled agent rate limits; EigenTrust ≈ 0 without inbound trust from seeds |
| T3 collusion ring | Layer B graph trust; independence/reciprocity discounts; ring-wide verifier slashing |
| T4 poisoning | −20 falsification slash; verifier slashing; disputed status on read; consumer min_confidence; per-CWE coverage |
| T5 laundering | Time decay; per-attestation confidence uses *current* R; disproportionate slash erases farmed credit |
| T6 lazy verification | Staked co-signature with slashing; depth levels; θ_verify gate |
| T7 false flags | Flags are staked; bad-faith flag slash; dispute review by domain experts |
| T8 stale scope | Hash-bound freshness (mechanical); λ_age decay |
8. System Flow
%%{init: {'theme': 'base', 'themeVariables': { 'fontSize': '14px' }, 'flowchart': { 'useMaxWidth': true }}}%%
flowchart TB
subgraph W["Write path"]
A["Signed attestation<br/>(auditor, scope, hashes)"]:::write
B["Identity tier check<br/>+ rate limit"]:::gate
C["Registry store<br/>(probation if new)"]:::store
end
subgraph T["Trust engine"]
D["Layer A: Beta evidence<br/>E(a) with decay"]:::trust
E["Layer B: EigenTrust<br/>G(a) from seed set"]:::graph
F["Ground truth ingest<br/>(CVE/OSV feeds → slashing)"]:::truth
end
subgraph R["Read path"]
G["Confidence C(att)<br/>noisy-OR × freshness"]:::read
H["Coverage status<br/>(caller min_confidence)"]:::api
end
A --> B --> C
C --> D
C --> E
F --> D
D --> G
E --> G
G --> H
classDef write fill:#dbeafe,stroke:#2563eb
classDef gate fill:#fef3c7,stroke:#d97706
classDef store fill:#e0e7ff,stroke:#4f46e5
classDef trust fill:#dcfce7,stroke:#16a34a
classDef graph fill:#ccfbf1,stroke:#0d9488
classDef truth fill:#fee2e2,stroke:#dc2626
classDef read fill:#f3e8ff,stroke:#9333ea
classDef api fill:#fce7f3,stroke:#db2777
class A write
class B gate
class C store
class D trust
class E graph
class F truth
class G read
class H apiNote the ground-truth ingest component: Signet continuously matches CVE/OSV/GHSA disclosures against attested scopes (repo + commit ancestry + file paths) to fire falsification events automatically. This closes the loop that makes the whole model predictive rather than social.
9. Parameters to Tune (v0.1 defaults)
| Param | Default | Meaning |
|---|---|---|
| λ half-life | 12 mo | evidence decay |
| λ_age half-life | 24 mo | attestation freshness decay |
| β₀ | 3.0 | pessimistic prior |
| d | 0.85 | EigenTrust damping |
| θ_verify | 0.40 | min rep to give weighted verification |
| θ_vouch | 0.60 | min rep to vouch T3 |
| k₁ | 3 | verified attestations to exit probation |
| falsification slash | −20.0 | in-scope, in-class miss |
All parameters ship in a signed, versioned config so mirrors reproduce identical scores from the same dump (NFR-3 determinism).
10. Open Questions / Flagged Uncertainties
- Seed-set governance. Who curates the default EigenTrust anchors, and what's
the rotation/removal process? Proposal: public governance repo + signed seed manifest, modeled on CT log lists. Needs its own design doc.
- Slashing appeals. A CVE in attested scope may itself be disputed (bogus CVEs
exist). Falsification events need the same dispute pipeline as flags before the −20 lands — otherwise CVE-spam becomes a griefing vector against auditors.
- Cold-start. Before the graph has density, EigenTrust starves honest
newcomers. Mitigation: probation exit via k₁ verifications works without graph trust; consider bootstrap-era relaxed θ_verify. Needs simulation.
- Per-ecosystem reputation. Should R(a) be sub-scored by ecosystem
(npm vs crates.io vs C)? Expertise doesn't transfer cleanly. v0.1 keeps a global score for simplicity; revisit with data.
- Simulation before launch. ✅ *Built* —
crates/signet-simis the
agent-based harness (honest auditors, collusion rings, Sybil-backed poisoning attacker) with the acceptance criteria wired as tests (cargo test -p signet-sim). Results at the §9 defaults:
| Scenario | Metric | Result | Criterion |
|---|---|---|---|
| Collusion ring (10, max self-dealing) under EigenTrust | ring confidence | 0.00 | < 0.10 ✅ |
| *Same ring under the M2 stub (G=1)* | ring confidence | 1.00 | demonstrates Layer B is required |
| Honest newcomer, 4 weekly verifications from seed-trusted reviewers | R | 0.226 | ≥ 0.20 (T3) ✅ |
| Poisoning attestation + 5-node Sybil ring | confidence | 0.00 | < 0.50 ✅ |
| One upheld flag on a healthy auditor | R drop | > 50% | erases standing ✅ |
The harness includes a personalized EigenTrust (power iteration with teleport to the seed set). The stub-vs-EigenTrust contrast (1.00 → 0.00 ring confidence) was the concrete evidence that graph trust is a launch blocker, not a nicety — and it has since been **ported into the server TrustEngine** (EigenTrust.java, gated on signet.trust.seeds; the signet-sim collusion-ring scenario is replicated as a JUnit test). With no seeds configured, G stays 1.0 and the engine reduces to beta-evidence only, so pre-Layer-B behavior is preserved exactly.
- Privacy tension. Operator binding for AI agents (§3) conflicts with
pseudonymous research culture. Possible middle ground: zero-knowledge proof of distinct-operator without revealing identity. Deferred.