signet-protocol 1.11.1 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/docs/bot-ownership-v1.md +43 -0
- package/docs/implementation-levels.md +235 -0
- package/docs/private-vault-v1.md +104 -0
- package/docs/signet-in-5-minutes.md +181 -0
- package/examples/full-flow.ts +260 -0
- package/examples/jurisdictions/brazil-lawyer.ts +186 -0
- package/examples/jurisdictions/eu-doctor.ts +160 -0
- package/examples/jurisdictions/india-doctor.ts +207 -0
- package/examples/jurisdictions/japan-notary.ts +157 -0
- package/examples/jurisdictions/multi-jurisdiction.ts +174 -0
- package/examples/jurisdictions/uae-professional.ts +190 -0
- package/examples/jurisdictions/uk-solicitor.ts +170 -0
- package/examples/jurisdictions/us-attorney.ts +161 -0
- package/examples/signet-me.ts +253 -0
- package/llms-full.txt +51 -20
- package/llms.txt +29 -11
- package/package.json +7 -1
- package/spec/protocol.md +2628 -0
- package/spec/voting.md +563 -0
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ const events = await fetchFromRelay('wss://relay.example.com', filters);
|
|
|
39
39
|
const badge = await computeBadge('<hex-pubkey>', events, { verifySignatures: true });
|
|
40
40
|
|
|
41
41
|
// badge.tier => 3, badge.score => 106, badge.displayLabel => "Verified (Tier 3)"
|
|
42
|
+
// (illustrative values; badge.score is a Level 1 approximation, see the notes below)
|
|
42
43
|
if (meetsMinimumTier(badge, 2)) {
|
|
43
44
|
// allow posting in verified-only community
|
|
44
45
|
}
|
|
@@ -46,9 +47,15 @@ if (meetsMinimumTier(badge, 2)) {
|
|
|
46
47
|
|
|
47
48
|
`fetchFromRelay(relayUrl, filters)` is the package's own minimal relay client and returns `Promise<NostrEvent[]>`. Any other Nostr client works too: pass `buildBadgeFilters` output as the REQ filters and hand the resulting events to `computeBadge`. `computeBadge` is async and returns `Promise<BadgeInfo>`.
|
|
48
49
|
|
|
50
|
+
Three things to know before you copy this:
|
|
51
|
+
|
|
52
|
+
- `verifySignatures` defaults to `false`. A call such as `computeBadge(pubkey, events)` checks no signatures and trusts every event it is given, so keep `{ verifySignatures: true }` unless the events were already verified. `fetchFromRelay` drops events with an invalid signature or id by default; events from any other source are not checked for you.
|
|
53
|
+
- `badge.score` is on the 0-200 Signet IQ scale but is a Level 1 approximation of `computeTrustScore(...).score`: it ignores account age and identity bridges and applies none of the per-type caps (1 professional verification, 3 in-person vouches, 5 online vouches). Use `computeTrustScore` when you need the full score.
|
|
54
|
+
- `computeBadge` does not check who issued a Tier 3 or Tier 4 credential, and `buildBadgeFilters` does not fetch verifier or revocation events. Any key can sign an event that carries `tier: 3`, and it will count. To require that the issuer is an activated, unrevoked verifier, fetch the verifier credentials, their vouches and revocations yourself, apply `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and drop other issuers' Tier 3 and Tier 4 credentials before calling `computeBadge`. [llms.txt](llms.txt) has the steps.
|
|
55
|
+
|
|
49
56
|
See [Signet in 5 Minutes](docs/signet-in-5-minutes.md) for a full developer overview, or the [full flow example](examples/full-flow.ts) for all 4 tiers, trust scoring, policies, and verifier lifecycle.
|
|
50
57
|
|
|
51
|
-
For LLM and agent tooling, [llms.txt](llms.txt) is a concise API overview and [llms-full.txt](llms-full.txt) adds the full protocol and voting specifications and the developer guides in one file. Both ship in the npm package.
|
|
58
|
+
For LLM and agent tooling, [llms.txt](llms.txt) is a concise API overview and [llms-full.txt](llms-full.txt) adds the full protocol and voting specifications and the developer guides in one file. Both ship in the npm package, as do `spec/`, `examples/` and the four `docs/` pages linked from this README and `llms.txt` (`signet-in-5-minutes.md`, `implementation-levels.md`, `private-vault-v1.md` and `bot-ownership-v1.md`), so those relative links resolve inside `node_modules/signet-protocol/`. Other files under `docs/`, `legal/` and `tests/` stay in the repository only.
|
|
52
59
|
|
|
53
60
|
## Protocol
|
|
54
61
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Draft bot ownership profile
|
|
2
|
+
|
|
3
|
+
Unstable. Import from `signet-protocol/experimental`; nothing here is exported
|
|
4
|
+
from the package root, and any of it may change without a deprecation period.
|
|
5
|
+
|
|
6
|
+
This unreleased profile uses nostr-attestations kind 31000 with type
|
|
7
|
+
`bot-ownership`. Its replaceable d-tag is `bot-ownership:<bot pubkey>`, p-tag is
|
|
8
|
+
the bot, and the event author is the selected owner persona. It cannot replace
|
|
9
|
+
a credential or vouch. Ownership never grants the bot the owner's trust.
|
|
10
|
+
|
|
11
|
+
A live claim has `valid_from` equal to `created_at`, matching `valid_to` and
|
|
12
|
+
`expiration`, and content `{ "v": 1, "label": "…" }` (100 characters maximum).
|
|
13
|
+
Labels refuse C0/C1 controls, DEL, zero-width and bidi marks (LRM, RLM, ALM),
|
|
14
|
+
line/paragraph separators and bidi embeddings, overrides and isolates, lone
|
|
15
|
+
surrogates, and every other Unicode format or default-ignorable code point,
|
|
16
|
+
except the narrow in-context forms emoji sequences need: the zero-width
|
|
17
|
+
joiner between emoji elements, the VS15/VS16 variation selectors right after
|
|
18
|
+
an emoji or a keycap base, and the England/Scotland/Wales RGI subdivision
|
|
19
|
+
flag tag sequences (no other tag sequence, and no hidden tag payload).
|
|
20
|
+
The event supplies principal, agent, issue date and expiry without requiring a
|
|
21
|
+
raw-hash signature: ordinary NIP-46 sign_event works on hardware. Revocation is
|
|
22
|
+
the existing same-address attestation with `status=revoked` and empty content.
|
|
23
|
+
|
|
24
|
+
Builders return unsigned events and never publish. Product integration must ask
|
|
25
|
+
for creation consent every time, sign with the chosen persona, and keep the
|
|
26
|
+
signed claim private until publication is explicitly chosen. Hardware-derived
|
|
27
|
+
bots must be registered on the device; this module does not register them.
|
|
28
|
+
|
|
29
|
+
Default expiry is 30 days, permitted creation range 1–90 days. Readers apply the
|
|
30
|
+
earliest of signed expiry, 90 days and an optional stricter per-bot ceiling,
|
|
31
|
+
with five minutes of clock tolerance. Lapsed is different from revoked.
|
|
32
|
+
Renewal becomes due with ten days left; retry at most daily. The scheduling
|
|
33
|
+
helper never signs: local policy and hardware approvals remain in force.
|
|
34
|
+
|
|
35
|
+
Consumers pin the expected owner and bot, verify signatures, and select the
|
|
36
|
+
newest same-address event while retaining their previously seen high-water mark.
|
|
37
|
+
A stale ownership claim must not resurrect a newer revocation. This module
|
|
38
|
+
validates one event; it does not implement relay freshness or fetch ordering.
|
|
39
|
+
`readBotOwnershipSync` applies identical validation for synchronous storage and
|
|
40
|
+
transport codecs; `readBotOwnership` is the asynchronous form of the same check.
|
|
41
|
+
|
|
42
|
+
Protocol review, app lifecycle/UI, isolated bots contacts, downstream adoption,
|
|
43
|
+
and hardware acceptance are still required before release.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Signet Implementation Levels
|
|
2
|
+
|
|
3
|
+
A guide for Nostr client developers integrating the Signet identity protocol. There are three progressive integration levels. Start at Level 1 and add depth as your use case demands.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
| Level | Effort | Event Kinds | New Crypto | What Users Get |
|
|
8
|
+
|-------|--------|-------------|------------|----------------|
|
|
9
|
+
| 1 — Read Trust Badges | ~1 weekend | 31000 (credential, vouch) | None | Tier badges and Signet Score on profiles |
|
|
10
|
+
| 2 — Issue Vouches | A few days | 31000, 30078 | None | Peer vouching, Tier 2 networks, policy checks |
|
|
11
|
+
| 3 — Full Protocol | Weeks to months | 31000, 30078, 30482-30484 | Merkle, Pedersen range proofs, ring signatures | Professional verification, ZK proofs, elections |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Level 1 — Read Trust Badges
|
|
16
|
+
|
|
17
|
+
**Effort:** A weekend
|
|
18
|
+
**Event kinds:** 31000 (attestation — filter by `type` tag: `credential`, `vouch`)
|
|
19
|
+
**New crypto required:** None — Nostr already verifies Schnorr signatures
|
|
20
|
+
|
|
21
|
+
This is the NIP-05 equivalent of Signet integration. You read one event kind (filtered by type tag), call one function, and display a badge. No cryptographic work on your part beyond what your Nostr client already does.
|
|
22
|
+
|
|
23
|
+
### What to build
|
|
24
|
+
|
|
25
|
+
1. Subscribe to kind `31000` events with `type` tags `credential` and `vouch` for the pubkeys you want to display.
|
|
26
|
+
2. Pass the events to `computeBadge()`.
|
|
27
|
+
3. Show the result on the user's profile.
|
|
28
|
+
|
|
29
|
+
### Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install signet-protocol
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Code example
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { computeBadge, buildBadgeFilters, fetchFromRelay } from 'signet-protocol';
|
|
39
|
+
|
|
40
|
+
const pubkey = '<hex-pubkey>';
|
|
41
|
+
|
|
42
|
+
// 1. Fetch events from a relay (fetchFromRelay is the package's own client; any Nostr client works)
|
|
43
|
+
const filters = buildBadgeFilters([pubkey]);
|
|
44
|
+
// The filters request kind 31000 with credential and vouch type tags.
|
|
45
|
+
const events = await fetchFromRelay('wss://relay.example.com', filters);
|
|
46
|
+
|
|
47
|
+
// 2. Compute badge from the events you receive
|
|
48
|
+
const badge = await computeBadge(pubkey, events, { verifySignatures: true }); // async: returns Promise<BadgeInfo>
|
|
49
|
+
|
|
50
|
+
// 3. Display
|
|
51
|
+
if (badge.isVerified) {
|
|
52
|
+
console.log(badge.tierLabel, badge.score); // replace with your own rendering
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The `badge` object includes:
|
|
57
|
+
|
|
58
|
+
- `badge.isVerified` — boolean, true if the pubkey has any Signet verification
|
|
59
|
+
- `badge.tier` — number 1–4, the highest verified tier
|
|
60
|
+
- `badge.tierLabel` — human-readable string (e.g. `"Vouched"`, `"Verified"`, `"Verified (Child Safety)"`)
|
|
61
|
+
- `badge.score`: integer 0–200 on the Signet IQ scale (100 = government standard), derived from credentials and vouches only. It is a Level 1 approximation of `computeTrustScore(...).score`: it ignores account age and identity bridges and applies no per-type caps
|
|
62
|
+
|
|
63
|
+
### Trust tiers at a glance
|
|
64
|
+
|
|
65
|
+
| Tier | Meaning |
|
|
66
|
+
|------|---------|
|
|
67
|
+
| 1 | Self-declared — the user has made identity claims |
|
|
68
|
+
| 2 | Web-of-trust — other users have vouched for this identity |
|
|
69
|
+
| 3 | Professional adult — verified by a recognised professional body |
|
|
70
|
+
| 4 | Professional adult + child safeguarding — extended professional verification |
|
|
71
|
+
|
|
72
|
+
### What you do NOT need to handle at this level
|
|
73
|
+
|
|
74
|
+
- Generating or validating zero-knowledge proofs
|
|
75
|
+
- Nullifier computation
|
|
76
|
+
- Merkle trees
|
|
77
|
+
- Any cryptographic primitives beyond standard Nostr signature verification
|
|
78
|
+
|
|
79
|
+
Two things Level 1 does not do for you:
|
|
80
|
+
|
|
81
|
+
- Signature checking is opt-in. `verifySignatures` defaults to `false`, so `computeBadge(pubkey, events)` with no options trusts every event it is given. Keep `{ verifySignatures: true }` unless the events were already verified (`fetchFromRelay` drops events with an invalid signature or id by default; events from other sources are not checked for you).
|
|
82
|
+
- Issuer standing is not checked. `computeBadge` does not look at who signed a Tier 3 or Tier 4 credential, and `buildBadgeFilters` fetches no verifier or revocation events, so any key can sign an event that carries `tier: 3` and it will count. To require an activated, unrevoked verifier, fetch the verifier credentials, their vouches and revocations yourself, apply `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and drop other issuers' Tier 3 and Tier 4 credentials before calling `computeBadge` (see `llms.txt`).
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Level 2 — Issue Vouches
|
|
87
|
+
|
|
88
|
+
**Effort:** A few days
|
|
89
|
+
**Event kinds:** 31000 (attestation — credential, vouch types), 30078 (policy)
|
|
90
|
+
**New crypto required:** None
|
|
91
|
+
|
|
92
|
+
Level 2 adds the ability for your users to vouch for each other and for your client to respect community policies. This is the viral layer of the protocol — peer vouching is what builds Tier 2 trust networks and creates network effects across clients.
|
|
93
|
+
|
|
94
|
+
### What to build
|
|
95
|
+
|
|
96
|
+
Everything from Level 1, plus:
|
|
97
|
+
|
|
98
|
+
1. UI for a user to select a contact and publish a kind `31000` vouch attestation (with `['type', 'vouch']` tag).
|
|
99
|
+
2. Read kind `30078` (NIP-78 app-specific data) policies from community relays and check whether a pubkey meets the policy threshold before allowing sensitive actions.
|
|
100
|
+
|
|
101
|
+
### Creating a vouch
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { createVouch } from 'signet-protocol';
|
|
105
|
+
|
|
106
|
+
// createVouch takes the voucher's private key and a VouchParams object.
|
|
107
|
+
// It returns a signed Nostr event ready to publish.
|
|
108
|
+
const vouchEvent = await createVouch(myPrivkey, {
|
|
109
|
+
subjectPubkey: theirPubkey,
|
|
110
|
+
method: 'in-person',
|
|
111
|
+
context: 'I have met this person in person and verified their identity.',
|
|
112
|
+
voucherTier: 2,
|
|
113
|
+
voucherScore: 106,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Publish to your relay as a standard Nostr event.
|
|
117
|
+
await publishEvent(vouchEvent);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Parsing incoming vouches
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { parseVouch, countQualifyingVouches, hasEnoughVouches } from 'signet-protocol';
|
|
124
|
+
|
|
125
|
+
// Parse a raw Nostr event into a structured vouch object
|
|
126
|
+
const vouch = parseVouch(rawEvent);
|
|
127
|
+
|
|
128
|
+
// Count how many qualifying vouches a pubkey has received
|
|
129
|
+
const count = countQualifyingVouches(allVouchEvents, pubkey);
|
|
130
|
+
|
|
131
|
+
// Check whether a pubkey clears the threshold required for Tier 2
|
|
132
|
+
const qualifies = hasEnoughVouches(allVouchEvents, pubkey, 3);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Reference: `src/vouches.ts`
|
|
136
|
+
|
|
137
|
+
### Checking policy compliance
|
|
138
|
+
|
|
139
|
+
Policies (kind `30078`, NIP-78) let community operators define trust thresholds for specific actions — for example, requiring a minimum score of 40 before a user can post to a paid relay, or requiring Tier 2 before joining a group.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { parsePolicy, checkPolicyCompliance, PolicyChecker } from 'signet-protocol';
|
|
143
|
+
|
|
144
|
+
// Parse a policy event
|
|
145
|
+
const policy = parsePolicy(policyEvent);
|
|
146
|
+
|
|
147
|
+
// One-shot check: does this user meet the policy?
|
|
148
|
+
const result = checkPolicyCompliance(policy, userTier, userScore);
|
|
149
|
+
if (!result.allowed) {
|
|
150
|
+
showError(`This action requires Tier ${result.requiredTier} or higher.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// For repeated checks across many pubkeys, use PolicyChecker (more efficient)
|
|
154
|
+
const checker = new PolicyChecker(policyEvent);
|
|
155
|
+
const adultResult = checker.checkAdult(userTier, userScore);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Reference: `src/policies.ts`
|
|
159
|
+
|
|
160
|
+
### What you do NOT need to handle at this level
|
|
161
|
+
|
|
162
|
+
- Zero-knowledge proofs of any kind
|
|
163
|
+
- Professional body verification flows
|
|
164
|
+
- Two-credential ceremonies
|
|
165
|
+
- Nullifiers or document hashing
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Level 3 — Full Protocol
|
|
170
|
+
|
|
171
|
+
**Effort:** Weeks to months
|
|
172
|
+
**Event kinds:** 31000 (all attestation types), 30078 (policies), 30482-30484 (voting)
|
|
173
|
+
**New crypto required:** Merkle trees, Pedersen range proofs (age range proofs), linkable ring signatures
|
|
174
|
+
|
|
175
|
+
Level 3 is full reference implementation. This is appropriate for clients that want to act as professional verifiers, run elections, or issue and receive Tier 3/4 credentials with zero-knowledge proofs. Elections themselves (kinds 30482–30484) live in the separate `nostr-anon-vote` package, not in `signet-protocol`.
|
|
176
|
+
|
|
177
|
+
### What to build (beyond Level 2)
|
|
178
|
+
|
|
179
|
+
- **Two-credential ceremony:** Professional verifiers issue a Natural Person credential (kind `31000` with `type: credential`, nullifier and Merkle root) and an anonymous Persona credential simultaneously. The Natural Person credential binds to a document-based nullifier (`SHA-256(docType || country || docNumber || salt)`) to prevent duplicate identity claims. The Persona credential reveals only an age range, not the full identity.
|
|
180
|
+
- **Merkle selective disclosure:** Verified attributes are stored as private leaves of a Merkle tree. Only the root is published on-chain. Users disclose individual leaves with inclusion proofs when required.
|
|
181
|
+
- **Age range proofs:** Pedersen range proofs prove that a user is within an age range without revealing their exact date of birth.
|
|
182
|
+
- **Professional verifier onboarding:** Verifier accounts (kind `31000` with `type: verifier`) are backed by recognised professional bodies (law societies, medical boards, notary commissions). The protocol has no central authority — trust anchors are these external bodies.
|
|
183
|
+
- **Ring signatures and elections:** The voting extension (kinds `30482`–`30484`) uses linkable ring signatures for anonymous voting with double-spend prevention. It is specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package; `signet-protocol` ships only the linkable ring signature primitives (`lsagSign`, `lsagVerify`, `computeKeyImage`).
|
|
184
|
+
- **Guardian delegation:** Delegation attestations (kind `31000` with `type: delegation`) with scopes (`full`, `activity-approval`, `content-management`, `contact-approval`) support family structures and guardian-linked sub-accounts.
|
|
185
|
+
- **Revocation and lifecycle:** Revocation attestations (kind `31000` with `type: revocation`) and credential chains using `supersedes`/`superseded-by` tags handle name changes, document renewal, and tier upgrades.
|
|
186
|
+
- **Identity bridging:** Identity bridge attestations (kind `31000` with `type: identity-bridge`) link verified identities across protocols or jurisdictions.
|
|
187
|
+
- **Anomaly detection:** The library includes heuristics for detecting suspicious vouching patterns (e.g. vouch rings, rapid bulk vouching).
|
|
188
|
+
|
|
189
|
+
### Key modules in signet-protocol
|
|
190
|
+
|
|
191
|
+
All modules are re-exported from the package root. You can also import them directly for tree-shaking.
|
|
192
|
+
|
|
193
|
+
| Module | Source | Responsibility |
|
|
194
|
+
|--------|--------|----------------|
|
|
195
|
+
| Credentials | `src/credentials.ts` | Issue, parse, verify credentials including Merkle roots and nullifiers |
|
|
196
|
+
| Ring signatures | `src/ring-signature.ts` | Ring signature generation and verification (via `@forgesworn/ring-sig`) |
|
|
197
|
+
| Range proofs | `src/range-proof.ts` | Pedersen range proofs for age verification (via `@forgesworn/range-proof`) |
|
|
198
|
+
| Merkle trees | `src/merkle.ts` | Build attribute trees, generate and verify inclusion proofs |
|
|
199
|
+
| Signet Score | `src/trust-score.ts` | Weighted signal aggregation, 0–200 Signet Score computation |
|
|
200
|
+
| Anomaly detection | `src/anomaly.ts` | Detect vouch ring attacks and other manipulation patterns |
|
|
201
|
+
| Jurisdictions | `jurisdiction-kit` | Jurisdiction-specific rules for document types and professional bodies |
|
|
202
|
+
|
|
203
|
+
All modules are re-exported from the package root (`signet-protocol`). Import from the root entry point:
|
|
204
|
+
|
|
205
|
+
### Event kind reference
|
|
206
|
+
|
|
207
|
+
All Signet identity attestations use a single generic kind, differentiated by `type` tag:
|
|
208
|
+
|
|
209
|
+
| Kind | Type Tag | Name | Description |
|
|
210
|
+
|------|----------|------|-------------|
|
|
211
|
+
| 31000 | `credential` | Credential | Identity claim with optional Merkle root and nullifier |
|
|
212
|
+
| 31000 | `vouch` | Vouch | Peer attestation of another pubkey's identity |
|
|
213
|
+
| 31000 | `verifier` | Verifier | Professional verifier registration |
|
|
214
|
+
| 31000 | `challenge` | Challenge | Challenge against a verifier's legitimacy |
|
|
215
|
+
| 31000 | `revocation` | Revocation | Credential or vouch revocation |
|
|
216
|
+
| 31000 | `identity-bridge` | Identity Bridge | Cross-protocol or cross-jurisdiction identity link |
|
|
217
|
+
| 31000 | `delegation` | Delegation | Guardian or organisational delegation with scopes |
|
|
218
|
+
| 30078 | — | Policy | Community trust threshold definitions (NIP-78) |
|
|
219
|
+
| 30482 | — | Election | Voting election definition (separate NIP) |
|
|
220
|
+
| 30483 | — | Ballot | Anonymous signed ballot |
|
|
221
|
+
| 30484 | — | Election Result | Tallied election result |
|
|
222
|
+
|
|
223
|
+
**Note:** Kind 31000 is the NIP-VA (Verifiable Attestations) kind. The `type` tag differentiates attestation types within the single kind.
|
|
224
|
+
|
|
225
|
+
### Where to start
|
|
226
|
+
|
|
227
|
+
The `signet-protocol` npm package contains everything. The protocol specification at `spec/protocol.md` is the authoritative source of truth for all behaviour.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Choosing your level
|
|
232
|
+
|
|
233
|
+
Start at Level 1 unless you have a specific reason to go further. Most social clients only need Level 1 — displaying trust context on profiles. Level 2 is appropriate if your community has a peer moderation culture. Level 3 is for infrastructure builders: relay operators, professional verification portals, and election platforms.
|
|
234
|
+
|
|
235
|
+
You can ship Level 1 in a weekend and upgrade later. The three levels are strictly additive — nothing you build at Level 1 needs to be undone to reach Level 2 or 3.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Private vault checkpoint v1 (staged, not released)
|
|
2
|
+
|
|
3
|
+
Unstable. Import from `signet-protocol/experimental`; nothing here is exported
|
|
4
|
+
from the package root, and any of it may change without a deprecation period.
|
|
5
|
+
|
|
6
|
+
Keys derive flat from the tree root using `vaultPurpose`; index means rotation.
|
|
7
|
+
Legacy Natural Person rails remain permanently readable. No writer is switched
|
|
8
|
+
until both the app and Sapwood can restore the new dataset.
|
|
9
|
+
|
|
10
|
+
The existing v2 AES-GCM/NIP-44 envelope is retained. Vault keys wrap content keys
|
|
11
|
+
and sign only control events below 12 KiB of UTF-8 content. Every chunk is sealed
|
|
12
|
+
to the vault key but signed by an installation device key. A checkpoint authorises
|
|
13
|
+
that device and lists exact chunk event IDs, encrypted-content SHA-256 hashes,
|
|
14
|
+
byte lengths and order. The complete plaintext SHA-256 is the revision. Recovery discovers and merges all
|
|
15
|
+
device heads rather than selecting one global latest snapshot, so concurrent
|
|
16
|
+
offline writers cannot replace each other. The reader fails closed beyond 16
|
|
17
|
+
heads instead of dropping devices; head compaction is future work. Readers
|
|
18
|
+
verify signatures, scope, bounds, sequence, every chunk and the revision before
|
|
19
|
+
returning any data. Missing chunks never mean an empty dataset.
|
|
20
|
+
|
|
21
|
+
All events use existing kind 30078. Each installation retains its own checkpoint under
|
|
22
|
+
`vaultCheckpointTag(author, publisherDevicePubkey)`;
|
|
23
|
+
chunks use a content-derived tag and are fetched by exact event ID. This reuses
|
|
24
|
+
the existing event kind rather than allocating a new kind. Content-addressed
|
|
25
|
+
chunk replacement cannot alter an already signed checkpoint reference. No public
|
|
26
|
+
tags name the dataset or connect device chunks to the vault author. Routing tags
|
|
27
|
+
are opaque, not secret: knowing a vault pubkey lets an observer compute its tag.
|
|
28
|
+
|
|
29
|
+
Head discovery reads only kind-30078 events by the vault author whose single
|
|
30
|
+
d-tag has the checkpoint shape (32 lowercase hex characters); other events by
|
|
31
|
+
the vault key are ignored (by the relay reader and again by recovery) before
|
|
32
|
+
any limit applies and are never decrypted. The
|
|
33
|
+
shape is necessary, not sufficient, because the publisher is encrypted: the
|
|
34
|
+
vault key must sign no other kind-30078 event with a 32-hex d-tag, or that
|
|
35
|
+
event is read as a malformed head and the read is unusable.
|
|
36
|
+
|
|
37
|
+
Checkpoints dated more than `VAULT_CLOCK_TOLERANCE_SECONDS` (300) after the
|
|
38
|
+
reader's clock are rejected; every reader takes `now` as a parameter. A device
|
|
39
|
+
whose clock ran ahead therefore cannot outrank its own later honest heads, but
|
|
40
|
+
its future-dated head is ignored until the clock catches up. Of two events with
|
|
41
|
+
the same created_at under one tag, the lower event ID is the newer (NIP-01).
|
|
42
|
+
|
|
43
|
+
Checkpoint plaintext shape is exported as `VaultCheckpoint`. It contains purpose,
|
|
44
|
+
rotation, sequence, revision, authorised device pubkeys and ordered chunk refs.
|
|
45
|
+
`nextRotation` is optional and, when present, exactly `rotation + 1`; any other
|
|
46
|
+
value makes the checkpoint invalid. Recovery begins at zero. A rotation
|
|
47
|
+
must not become canonical before the new copy is fetched, decrypted and verified.
|
|
48
|
+
|
|
49
|
+
Rotation is a revocation boundary. Recovery reads only the newest rotation:
|
|
50
|
+
the first rotation n whose successor n + 1 holds no authentic checkpoint-shaped
|
|
51
|
+
event. Before reading rotation n it always probes rotation n + 1 (one extra
|
|
52
|
+
resolve and query). Once n + 1 exists, rotation n is never queried, opened,
|
|
53
|
+
counted or merged, so a holder of a retired key can neither inject or tombstone
|
|
54
|
+
data (backdated or not, under any publisher tag) nor block recovery with
|
|
55
|
+
malformed, over-cap or junk-tag events, nor hide the successor by overwriting
|
|
56
|
+
the pointer head. The newest rotation is read strictly; a pointer there to a
|
|
57
|
+
rotation with no authentic checkpoint is damage (`unusable`). The rotating
|
|
58
|
+
writer must carry every head's state into the new rotation before publishing
|
|
59
|
+
there; nothing written under an older rotation is recovered. Sequence floors
|
|
60
|
+
apply to the rotation that is read.
|
|
61
|
+
|
|
62
|
+
A rotation revokes only a holder of THAT rotation's key, not the tree root: a
|
|
63
|
+
holder of the root key can derive every rotation regardless. A caller that
|
|
64
|
+
walks forward and finds every relay withholding the true newest rotation's
|
|
65
|
+
successor cannot tell that from a vault that never rotated further, so any
|
|
66
|
+
caller tracking the highest rotation it has already reached must treat a walk
|
|
67
|
+
that lands below it as a rollback, not as ready.
|
|
68
|
+
|
|
69
|
+
Rotation write order matters. The rotating writer first publishes the merged
|
|
70
|
+
state under rotation n + 1 (every chunk, then the checkpoint) and only then
|
|
71
|
+
declares `nextRotation` in rotation n. A pointer published first makes reads
|
|
72
|
+
`unusable` until rotation n + 1 is complete, because a pointer to a rotation
|
|
73
|
+
with no authentic checkpoint is damage. Once any authentic checkpoint-shaped
|
|
74
|
+
event exists under rotation n + 1, rotation n is never read: if that event (or
|
|
75
|
+
its chunks) is broken, the read is fail-closed `unusable` with no fallback to
|
|
76
|
+
rotation n. Recovery walks at most 32 rotations (`MAX_ROTATION_HOPS`); a vault
|
|
77
|
+
rotated more often reads `unusable`.
|
|
78
|
+
|
|
79
|
+
Restore results distinguish absent, unavailable, unusable and ready. Legacy may
|
|
80
|
+
be the canonical source only when the vault is absent, never merely because a
|
|
81
|
+
relay is offline or a new checkpoint fails to decrypt. A previously observed
|
|
82
|
+
sequence is a rollback floor. A fresh device cannot detect a relay withholding
|
|
83
|
+
all newer state without another trusted source; do not claim otherwise.
|
|
84
|
+
|
|
85
|
+
`createVaultRelayReader` queries at most eight `wss://` relays (plain `ws://`
|
|
86
|
+
only for `localhost` and `127.0.0.1`, matching `RelayClient`). A checkpoint
|
|
87
|
+
query, empty or not, completes only when every queried relay returned EOSE and
|
|
88
|
+
none failed: one relay's "nothing" is not evidence of absence, and one relay's
|
|
89
|
+
heads may omit a device head that another relay holds. Otherwise the query
|
|
90
|
+
throws a `VaultRelayError` whose `failedRelays` names the relays that failed,
|
|
91
|
+
and recovery returns `{ state: 'unavailable', failedRelays }`. A permanently
|
|
92
|
+
dead configured relay therefore keeps reads `unavailable` until it is removed
|
|
93
|
+
from the relay set; that is deliberate, since `unavailable` is safe and a false
|
|
94
|
+
`absent` is not. A chunk fetch by exact ID succeeds once any relay returns it
|
|
95
|
+
(its hash is pinned by the checkpoint); "not found" again needs every relay.
|
|
96
|
+
Each relay's events are re-checked against the requested author, kind, ID and
|
|
97
|
+
d-tag, checkpoint queries also drop events without a checkpoint-shaped d-tag,
|
|
98
|
+
and duplicates are removed, all before a per-relay cap of 128; a relay over the
|
|
99
|
+
cap counts as failed.
|
|
100
|
+
|
|
101
|
+
The SDK provides stateless read/validation and forward rotation traversal.
|
|
102
|
+
Writer scheduling, dataset import and durable canonical markers belong to
|
|
103
|
+
consumers. The candidate Signet app implements scheduling and confirmed-copy
|
|
104
|
+
state; this does not establish equivalent Sapwood behaviour.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Signet in 5 Minutes
|
|
2
|
+
|
|
3
|
+
Signet is an open-source, decentralised identity verification protocol for Nostr. It lets users prove claims
|
|
4
|
+
about who they are — human, professional, adult, organisation — without exposing personal data. Verification
|
|
5
|
+
is anchored to professional bodies (law societies, medical boards, notary commissions) rather than any central
|
|
6
|
+
authority. Trust accumulates as a continuous Signet Score (0–200, where 100 represents the current government standard) assembled from weighted signals, and is expressed as a tier badge any Nostr client can display.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## How Trust Flows
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Person
|
|
13
|
+
|
|
|
14
|
+
| signs a kind 31000 self-declaration (`type: credential`)
|
|
15
|
+
v
|
|
16
|
+
Tier 1 — Self-declared
|
|
17
|
+
|
|
|
18
|
+
| three or more peers sign kind 31000 vouches (`type: vouch`)
|
|
19
|
+
v
|
|
20
|
+
Tier 2 — Web-of-trust
|
|
21
|
+
|
|
|
22
|
+
| licensed professional issues kind 31000 credential (`type: credential`)
|
|
23
|
+
| with zero-knowledge age proof and document nullifier
|
|
24
|
+
v
|
|
25
|
+
Tier 3 — Professionally verified (adult)
|
|
26
|
+
|
|
|
27
|
+
| same ceremony, scope extended to child supervision
|
|
28
|
+
v
|
|
29
|
+
Tier 4 — Professionally verified (adult + child)
|
|
30
|
+
|
|
|
31
|
+
v
|
|
32
|
+
Trust signals accumulated (professional verif, vouches, account age, identity bridges)
|
|
33
|
+
|
|
|
34
|
+
v
|
|
35
|
+
Signet Score computed (0-200, where 100 = government standard)
|
|
36
|
+
|
|
|
37
|
+
v
|
|
38
|
+
Badge displayed by client e.g. "Verified (Tier 3) — Signet Score 106"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## The Four Tiers
|
|
43
|
+
|
|
44
|
+
| Tier | Name | Who issues | What it proves | Score contribution |
|
|
45
|
+
|------|-----------------------------|-----------------------------------------|-----------------------------------------------------|--------------------|
|
|
46
|
+
| 1 | Self-declared | Subject themselves | "I exist and I own this key" | — |
|
|
47
|
+
| 2 | Web-of-trust | Peers (kind 31000, `type: vouch`) | Enough known people can vouch for this identity | +2–8 per vouch |
|
|
48
|
+
| 3 | Professionally verified | Licensed professional (kind 31000, `type: credential`) | Government document seen; adult age confirmed | +40 |
|
|
49
|
+
| 4 | Professional + child safety | Licensed professional (kind 31000, `type: credential`) | Tier 3 plus authorisation to interact with minors | +40 |
|
|
50
|
+
|
|
51
|
+
Tier 2 requires at least 3 vouches from Tier 2+ peers (configurable per community via kind 30078 (NIP-78) policy).
|
|
52
|
+
Tiers 3 and 4 are issued in a two-credential ceremony: one Natural Person credential (carries document
|
|
53
|
+
nullifier and Merkle root) and one anonymous Persona credential (carries age range only, no identifying data).
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Three Implementation Levels
|
|
57
|
+
|
|
58
|
+
### Level 1 — Display badges (a weekend)
|
|
59
|
+
|
|
60
|
+
Read kind 31000 (`type: credential` and `type: vouch`) from relays, call `computeBadge`, show a label.
|
|
61
|
+
No cryptography required beyond Schnorr signature verification (optional).
|
|
62
|
+
|
|
63
|
+
### Level 2 — Issue vouches (a few days)
|
|
64
|
+
|
|
65
|
+
Add kind 31000 (`type: vouch`) vouch issuance. Requires Schnorr signing, relay write access, and basic
|
|
66
|
+
kind 30078 (NIP-78) policy parsing so your client respects community verification thresholds.
|
|
67
|
+
|
|
68
|
+
### Level 3 — Full protocol (weeks)
|
|
69
|
+
|
|
70
|
+
Issue professional credentials, manage verifier lifecycle (kind 31000 types: verifier, challenge, revocation), build
|
|
71
|
+
guardian delegation (kind 31000, `type: delegation`), support credential chains (supersedes / superseded-by),
|
|
72
|
+
and integrate the voting extension (kinds 30482–30484). The voting extension is specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package, not in `signet-protocol`; this package ships only the linkable ring signature primitives it builds on (`lsagSign`, `lsagVerify`, `computeKeyImage`).
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## How Verification Works (Tier 3 / 4)
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Subject Verifier
|
|
79
|
+
| |
|
|
80
|
+
|--- presents document -->|
|
|
81
|
+
| |
|
|
82
|
+
| Verifier computes nullifier:
|
|
83
|
+
| SHA-256(docType || country || docNumber || salt)
|
|
84
|
+
| (prevents duplicate identity; document never stored)
|
|
85
|
+
| |
|
|
86
|
+
| Verifier builds Merkle tree:
|
|
87
|
+
| leaves = verified attributes (name, DOB, address, ...)
|
|
88
|
+
| only root is published on-chain
|
|
89
|
+
| |
|
|
90
|
+
| Verifier creates zero-knowledge age range proof
|
|
91
|
+
| (proves "subject is 18+" without revealing birth date)
|
|
92
|
+
| |
|
|
93
|
+
| Two credentials signed and published:
|
|
94
|
+
| kind 31000 Natural Person (nullifier + Merkle root, type: credential)
|
|
95
|
+
| kind 31000 Persona (age range only, anonymous, type: credential)
|
|
96
|
+
| |
|
|
97
|
+
v v
|
|
98
|
+
Both events published to Nostr relays
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Professional verifiers are themselves credentialed (kind 31000, `type: verifier`) and require cross-verification
|
|
102
|
+
by two other licensed professionals before becoming active. A challenge / revocation mechanism
|
|
103
|
+
(kind 31000 types: `challenge` and `revocation`) allows the community to remove a fraudulent verifier.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## Event Kinds Reference
|
|
107
|
+
|
|
108
|
+
| Kind | Type tag | Purpose |
|
|
109
|
+
|-------|-------------------|----------------------------------------------------|
|
|
110
|
+
| 31000 | `credential` | Verification credential for any tier |
|
|
111
|
+
| 31000 | `vouch` | Peer attestation (Tier 2 building block) |
|
|
112
|
+
| 30078 | — | Policy: community verification requirements (NIP-78) |
|
|
113
|
+
| 31000 | `verifier` | Professional verifier credential |
|
|
114
|
+
| 31000 | `challenge` | Report a suspected fraudulent verifier |
|
|
115
|
+
| 31000 | `revocation` | Remove a verifier after threshold confirmations |
|
|
116
|
+
| 31000 | `identity-bridge` | Link two keypairs via ring signature |
|
|
117
|
+
| 31000 | `delegation` | Guardian / agent delegation with scoped permission |
|
|
118
|
+
| 30482 | — | Voting extension: define an election |
|
|
119
|
+
| 30483 | — | Voting extension: cast an anonymous ballot |
|
|
120
|
+
| 30484 | — | Voting extension: publish tallied result |
|
|
121
|
+
|
|
122
|
+
Kinds 30482–30484 are specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package, not in `signet-protocol`.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Integrating at Level 1
|
|
126
|
+
|
|
127
|
+
Install the library:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
npm install signet-protocol
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Fetch events from a relay, then call `computeBadge`:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { computeBadge, buildBadgeFilters, fetchFromRelay, meetsMinimumTier } from 'signet-protocol';
|
|
137
|
+
import type { NostrEvent } from 'signet-protocol';
|
|
138
|
+
|
|
139
|
+
// 1. Build relay filters for one or more pubkeys
|
|
140
|
+
const pubkeys = ['<hex-pubkey>'];
|
|
141
|
+
const filters = buildBadgeFilters(pubkeys);
|
|
142
|
+
// filters = [{ kinds: [31000], '#d': ['credential:<hex-pubkey>', 'vouch:<hex-pubkey>'] }]
|
|
143
|
+
|
|
144
|
+
// 2. Fetch events from your relay (fetchFromRelay is the package's own client; any Nostr client works)
|
|
145
|
+
const events: NostrEvent[] = await fetchFromRelay('wss://relay.example.com', filters);
|
|
146
|
+
|
|
147
|
+
// 3. Compute the badge
|
|
148
|
+
const badge = await computeBadge(pubkeys[0], events, { verifySignatures: true });
|
|
149
|
+
|
|
150
|
+
// badge.displayLabel => "Verified (Tier 3)"
|
|
151
|
+
// badge.score => 106 (illustrative; see the score note below)
|
|
152
|
+
// badge.tier => 3
|
|
153
|
+
// badge.vouchCount => 5
|
|
154
|
+
|
|
155
|
+
// 4. Gate a feature on minimum tier
|
|
156
|
+
if (meetsMinimumTier(badge, 2)) {
|
|
157
|
+
// allow posting in a verified-only community
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`computeBadge` accepts any mix of kind 31000 (`type: credential` and `type: vouch`) events. It handles expiry, deduplication
|
|
162
|
+
(one vouch counted per voucher pubkey), and optional Schnorr signature verification. The result is
|
|
163
|
+
a plain object — no rendering assumptions are made.
|
|
164
|
+
|
|
165
|
+
- `verifySignatures` defaults to `false`. `computeBadge(pubkey, events)` with no options checks no signatures, so keep `{ verifySignatures: true }` unless the events were already verified. `fetchFromRelay` drops events with an invalid signature or id by default; events from any other source are not checked for you.
|
|
166
|
+
- `badge.score` is on the 0–200 Signet IQ scale but is a Level 1 approximation of `computeTrustScore(...).score`: it ignores account age and identity bridges and applies no per-type caps (1 professional verification, 3 in-person vouches, 5 online vouches). Use `computeTrustScore` for the full score.
|
|
167
|
+
- `computeBadge` does not check who issued a Tier 3 or Tier 4 credential, and `buildBadgeFilters` fetches no verifier or revocation events. Any key can sign an event that carries `tier: 3` and it will count. To require an activated, unrevoked verifier, fetch the verifier credentials, their vouches and revocations yourself, apply `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and drop other issuers' Tier 3 and Tier 4 credentials before calling `computeBadge` (see `llms.txt`).
|
|
168
|
+
|
|
169
|
+
The `buildBadgeFilters` helper returns a standard Nostr REQ filter array ready for use with any
|
|
170
|
+
relay client library.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## Further Reading
|
|
174
|
+
|
|
175
|
+
- Full protocol specification: `spec/protocol.md`
|
|
176
|
+
- Voting extension: `spec/voting.md`
|
|
177
|
+
- Level 1 badge library: `src/badge.ts`
|
|
178
|
+
- Signet Score details: `src/trust-score.ts`
|
|
179
|
+
- Full working example: `examples/full-flow.ts`
|
|
180
|
+
- Jurisdiction-specific examples: `examples/jurisdictions/`
|
|
181
|
+
- "Signet me" peer verification: `examples/signet-me.ts`
|