@solidus-network/auth 0.6.0 → 0.6.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 +128 -18
- package/demo/unlinkability-demo.mjs +159 -0
- package/dist/challenge.d.ts +35 -0
- package/dist/challenge.d.ts.map +1 -1
- package/dist/challenge.js +39 -0
- package/dist/challenge.js.map +1 -1
- package/dist/envelope.d.ts +72 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +153 -0
- package/dist/envelope.js.map +1 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/pairwise.d.ts +36 -0
- package/dist/pairwise.d.ts.map +1 -0
- package/dist/pairwise.js +115 -0
- package/dist/pairwise.js.map +1 -0
- package/dist/rp.d.ts +34 -0
- package/dist/rp.d.ts.map +1 -0
- package/dist/rp.js +59 -0
- package/dist/rp.js.map +1 -0
- package/dist/verify.d.ts +76 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +137 -0
- package/dist/verify.js.map +1 -1
- package/package.json +39 -7
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @solidus-network/auth
|
|
2
2
|
|
|
3
|
-
DID-based authentication
|
|
3
|
+
DID-based authentication and DID-less BBS+ selective-disclosure verification for the [Solidus Network](https://solidus.network) protocol.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Two independent surfaces, one package:
|
|
6
|
+
|
|
7
|
+
- **DID-bound auth** — Ed25519 challenge / signature / verification, for backends that authenticate the holder of a known DID without operating a user database.
|
|
8
|
+
- **DID-less presentations** — the relying-party surface for Solidus's unlinkable credential presentations: verify "a valid KYC credential from this issuer, disclosing exactly these claims" without ever learning who presented it.
|
|
6
9
|
|
|
7
10
|
## Install
|
|
8
11
|
|
|
@@ -12,39 +15,146 @@ npm install @solidus-network/auth
|
|
|
12
15
|
pnpm add @solidus-network/auth
|
|
13
16
|
```
|
|
14
17
|
|
|
15
|
-
## Quick start
|
|
18
|
+
## Quick start — DID-bound challenge
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createChallenge, verifyPresentation } from '@solidus-network/auth'
|
|
22
|
+
|
|
23
|
+
// Server: issue a challenge for a known DID (TTL in seconds, default 300)
|
|
24
|
+
const challenge = createChallenge('did:solidus:testnet:abc123', 300)
|
|
25
|
+
|
|
26
|
+
// Client answers with a W3C Verifiable Presentation whose proof signs
|
|
27
|
+
// the challenge nonce; the server verifies:
|
|
28
|
+
const result = await verifyPresentation(challenge, presentation, getPublicKey)
|
|
29
|
+
|
|
30
|
+
if (result.valid) {
|
|
31
|
+
// authenticated — result.checks itemizes expiry/holder/nonce/signature
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## DID-less selective-disclosure presentations (BBS+)
|
|
36
|
+
|
|
37
|
+
The verifier surface for Solidus's **unlinkable** presentations: a wallet
|
|
38
|
+
proves it holds a valid KYC credential from a trusted issuer and disclosed
|
|
39
|
+
exactly the claims you asked for — without revealing the holder's DID, a
|
|
40
|
+
credential ID, or anything two relying parties could later compare to
|
|
41
|
+
discover they served the same person.
|
|
42
|
+
|
|
43
|
+
### How it works
|
|
44
|
+
|
|
45
|
+
- The issuer signs an 8-message KYC vector with BBS+ under a **header that
|
|
46
|
+
is constant per (issuer, credential-type, epoch)** — no per-credential
|
|
47
|
+
entropy, so the header places a holder in a cohort, never identifies one.
|
|
48
|
+
- The issuer publishes its per-epoch public keys at
|
|
49
|
+
`https://verify.solidus.network/.well-known/solidus-bbs-issuer.json`.
|
|
50
|
+
Credentials live as long as their epoch (+ a grace window); revocation is
|
|
51
|
+
bounded-lifetime — a lapsed epoch's key disappears from the document and
|
|
52
|
+
every verifier fails closed.
|
|
53
|
+
- The wallet answers a verifier's challenge with a **randomized proof**
|
|
54
|
+
bound to that verifier's domain + nonce. Replaying it under any other
|
|
55
|
+
challenge fails cryptographically.
|
|
56
|
+
- The envelope (`BbsSelectiveDisclosurePresentation`) has **no `holder`
|
|
57
|
+
field by construction**, and the verifier rejects any envelope that
|
|
58
|
+
discloses `subject_did`, `verification_id`, or `document_hash` even when
|
|
59
|
+
the proof itself is valid.
|
|
60
|
+
|
|
61
|
+
### Verifier quickstart (a complete relying party)
|
|
16
62
|
|
|
17
63
|
```ts
|
|
18
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
createBbsChallenge,
|
|
66
|
+
verifyBbsPresentation,
|
|
67
|
+
createIssuerKeyResolver,
|
|
68
|
+
parseBbsPresentation,
|
|
69
|
+
decodeDisclosedKycFields,
|
|
70
|
+
} from '@solidus-network/auth'
|
|
19
71
|
|
|
20
|
-
//
|
|
21
|
-
const challenge =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
72
|
+
// 1. Issue a DID-less challenge (store it; enforce single use).
|
|
73
|
+
const challenge = createBbsChallenge({
|
|
74
|
+
domain: 'your-app.example.com', // your authenticated origin
|
|
75
|
+
credentialType: 'kyc',
|
|
76
|
+
requiredIndices: [4, 5], // kyc_level, country
|
|
25
77
|
})
|
|
26
78
|
|
|
27
|
-
//
|
|
79
|
+
// 2. The wallet answers with a serialized envelope.
|
|
80
|
+
const presentation = parseBbsPresentation(envelopeJson)
|
|
28
81
|
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
// 3. Verify — stateless, against the issuer's published epoch keys.
|
|
83
|
+
const resolveIssuerKey = createIssuerKeyResolver({
|
|
84
|
+
issuerBaseUrl: 'https://verify.solidus.network',
|
|
85
|
+
})
|
|
86
|
+
const result = await verifyBbsPresentation(challenge, presentation, {
|
|
87
|
+
issuerDid: 'did:solidus:testnet:2FDp7gH5qyb66jjsXAbFQYwLygqQ',
|
|
88
|
+
resolveIssuerKey,
|
|
89
|
+
used: false, // your storage-side single-use flag
|
|
34
90
|
})
|
|
35
91
|
|
|
36
|
-
if (result.
|
|
37
|
-
|
|
92
|
+
if (result.valid) {
|
|
93
|
+
const fields = decodeDisclosedKycFields(presentation.disclosed)
|
|
94
|
+
// e.g. { issuer_did: '…', kyc_level: '2', country: 'TR' } — and nothing else
|
|
38
95
|
}
|
|
39
96
|
```
|
|
40
97
|
|
|
98
|
+
The resolver is fail-closed: an unknown or lapsed epoch, an unreachable
|
|
99
|
+
issuer, or a malformed key document all resolve to `null` and the
|
|
100
|
+
presentation is rejected — never accepted on error.
|
|
101
|
+
|
|
102
|
+
### Verify the unlinkability claim yourself
|
|
103
|
+
|
|
104
|
+
Don't take our word for it — the demonstration ships in this package and
|
|
105
|
+
runs in under a minute:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
node node_modules/@solidus-network/auth/demo/unlinkability-demo.mjs
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
It issues one credential, presents it to two relying parties, verifies
|
|
112
|
+
both presentations, then pools everything both RPs received and shows the
|
|
113
|
+
complete correlation surface: cohort-level constants and the disclosed
|
|
114
|
+
claims — no per-holder value in common, randomized proofs sharing no
|
|
115
|
+
bytes, and cross-challenge replay failing.
|
|
116
|
+
|
|
117
|
+
### Re-recognition (pairwise DIDs)
|
|
118
|
+
|
|
119
|
+
When a returning user *wants* to be recognized by one relying party, the
|
|
120
|
+
wallet can attach a **pairwise DID** — deterministically derived from the
|
|
121
|
+
holder's seed *and that verifier's identity*, so the same user shows a
|
|
122
|
+
different, uncorrelatable pairwise DID to every other verifier. See
|
|
123
|
+
`buildPairwiseAttachment` / `buildPairwiseDid`. Two caveats, stated
|
|
124
|
+
plainly: the wallet must derive against the verifier's *authenticated*
|
|
125
|
+
origin (never an ID copied blindly from the challenge), and the pairwise
|
|
126
|
+
signature sits *beside* the credential proof rather than being
|
|
127
|
+
cryptographically bound to the same seed — assurance is "a returning
|
|
128
|
+
controller of this key who also holds a valid credential", which is right
|
|
129
|
+
for convenience and analytics, not for access-control-grade identity.
|
|
130
|
+
|
|
131
|
+
### What this does and does not protect
|
|
132
|
+
|
|
133
|
+
**Protected (cryptographic presentation layer):** two relying parties —
|
|
134
|
+
or the same one twice — cannot link presentations by anything in the
|
|
135
|
+
envelopes: no subject DID, no credential ID, no document hash, randomized
|
|
136
|
+
proofs, cohort-constant headers, challenge-bound replay protection.
|
|
137
|
+
|
|
138
|
+
**NOT protected (out of scope — be honest with your users):** network-level
|
|
139
|
+
correlation (IP address, timing, TLS/device fingerprints, cookies), and
|
|
140
|
+
relying parties pooling out-of-band data they each collected themselves
|
|
141
|
+
(email, phone, payment details). If your application hands every RP the
|
|
142
|
+
user's email address, unlinkable credentials will not make them unlinkable.
|
|
143
|
+
|
|
144
|
+
**Status:** the underlying `@solidus-network/bbs` implementation is
|
|
145
|
+
**testnet-grade and audit-pending** (external audit via NLnet NGI Zero,
|
|
146
|
+
H2 2026 target). `did:solidus` is submitted to the W3C DID method registry
|
|
147
|
+
and under review. This runs on the Solidus testnet — do not represent it
|
|
148
|
+
as audited or production-assured until those complete.
|
|
149
|
+
|
|
41
150
|
## Features
|
|
42
151
|
|
|
43
152
|
- Ed25519 signature verification via [@noble/ed25519](https://www.npmjs.com/package/@noble/ed25519)
|
|
44
153
|
- BLAKE3 hashing for challenge digests via [@noble/hashes](https://www.npmjs.com/package/@noble/hashes)
|
|
45
154
|
- Time-bounded challenges with TTL enforcement
|
|
46
155
|
- Domain-bound challenges (prevents replay across origins)
|
|
47
|
-
-
|
|
156
|
+
- DID-less BBS+ selective-disclosure verification with fail-closed epoch-key resolution
|
|
157
|
+
- A runnable unlinkability demonstration (`demo/unlinkability-demo.mjs`)
|
|
48
158
|
|
|
49
159
|
## License
|
|
50
160
|
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Solidus unlinkability demonstration — runnable by anyone, from npm alone.
|
|
2
|
+
//
|
|
3
|
+
// npm i @solidus-network/auth @solidus-network/bbs
|
|
4
|
+
// node node_modules/@solidus-network/auth/demo/unlinkability-demo.mjs
|
|
5
|
+
//
|
|
6
|
+
// You play every role: issuer, holder, and two relying parties who later
|
|
7
|
+
// collude. The issuer keypair is generated locally; the FORMAT — header v2,
|
|
8
|
+
// the 8-message KYC vector, the envelope, the presentation-header binding —
|
|
9
|
+
// is exactly what verify.solidus.network issues in production (Part 1
|
|
10
|
+
// fetches its live epoch-key document so you can see that for yourself).
|
|
11
|
+
// Production differs only in whose secret key signs, never in what a
|
|
12
|
+
// relying party receives — so the correlation analysis below transfers.
|
|
13
|
+
//
|
|
14
|
+
// HONESTY BOUNDARY (read before quoting this): this demonstrates
|
|
15
|
+
// unlinkability at the cryptographic presentation layer ONLY. It does NOT
|
|
16
|
+
// defend against network-level correlation (IP address, timing, device
|
|
17
|
+
// fingerprint, cookies) or relying parties pooling out-of-band data they
|
|
18
|
+
// collected themselves (email, phone number). @solidus-network/bbs is
|
|
19
|
+
// testnet-grade; external audit pending (NLnet NGI Zero, H2 2026 target).
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createBbsChallenge,
|
|
23
|
+
verifyBbsPresentation,
|
|
24
|
+
buildBbsPresentation,
|
|
25
|
+
createIssuerKeyResolver,
|
|
26
|
+
KYC_MESSAGE_FIELDS,
|
|
27
|
+
} from '@solidus-network/auth'
|
|
28
|
+
import {
|
|
29
|
+
BbsSecretKey,
|
|
30
|
+
buildV2Header,
|
|
31
|
+
derivePresentationHeader,
|
|
32
|
+
utf8,
|
|
33
|
+
} from '@solidus-network/bbs'
|
|
34
|
+
|
|
35
|
+
const hex = (b) => Buffer.from(b).toString('hex')
|
|
36
|
+
const line = (s = '') => console.log(s)
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// PART 1 — the production issuer publishes epoch keys at a public URL.
|
|
40
|
+
// Anyone can resolve them; unknown or lapsed epochs fail closed to null.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
line('== PART 1: production issuer key document (public URL) ==')
|
|
43
|
+
const PROD = 'https://verify.solidus.network'
|
|
44
|
+
try {
|
|
45
|
+
const doc = await (await fetch(`${PROD}/.well-known/solidus-bbs-issuer.json`)).json()
|
|
46
|
+
line(`issuer_did: ${doc.issuer_did}`)
|
|
47
|
+
line(`current epoch: ${doc.current.epoch} (notAfter ${doc.current.notAfter})`)
|
|
48
|
+
const resolve = createIssuerKeyResolver({ issuerBaseUrl: PROD })
|
|
49
|
+
const key = await resolve(doc.current.epoch)
|
|
50
|
+
line(`resolver → epoch ${doc.current.epoch}: ${key ? key.slice(0, 32) + '… (96-byte BLS12-381 G2 key)' : 'null'}`)
|
|
51
|
+
const lapsed = await resolve(9999)
|
|
52
|
+
line(`resolver → epoch 9999 (unknown): ${lapsed === null ? 'null (fail-closed)' : 'UNEXPECTED KEY'}`)
|
|
53
|
+
} catch {
|
|
54
|
+
line('(offline — skipping the live fetch; the local demonstration below is unaffected)')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// PART 2 — one credential, two relying parties.
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
line()
|
|
61
|
+
line('== PART 2: one credential, presented to two relying parties ==')
|
|
62
|
+
const issuerDid = 'did:solidus:testnet:demo-issuer'
|
|
63
|
+
const credentialType = 'kyc'
|
|
64
|
+
const epoch = 2
|
|
65
|
+
const sk = await BbsSecretKey.generate()
|
|
66
|
+
const pk = await sk.publicKey()
|
|
67
|
+
const header = buildV2Header(issuerDid, credentialType, epoch)
|
|
68
|
+
|
|
69
|
+
// The production message vector (order frozen). Demo values only — no PII.
|
|
70
|
+
const messages = [
|
|
71
|
+
utf8('did:solidus:testnet:demoHolderSubjectDid0000'), // 0 subject_did — ALWAYS hidden
|
|
72
|
+
utf8(issuerDid), // 1 issuer_did
|
|
73
|
+
utf8('vrf_demo_0001'), // 2 verification_id — ALWAYS hidden
|
|
74
|
+
utf8('2026-07-01T09:30:00Z'), // 3 verified_at
|
|
75
|
+
utf8('2'), // 4 kyc_level
|
|
76
|
+
utf8('TR'), // 5 country
|
|
77
|
+
utf8('passport'), // 6 document_type
|
|
78
|
+
utf8('deadbeefdeadbeefdeadbeefdeadbeef'), // 7 document_hash — ALWAYS hidden
|
|
79
|
+
]
|
|
80
|
+
const sig = await sk.sign(header, messages)
|
|
81
|
+
line(`issued: 1 credential, 8 messages, header v2 = "${Buffer.from(header).toString()}"`)
|
|
82
|
+
line('(header v2 is issuer/type/epoch-constant — identical for every holder in the cohort)')
|
|
83
|
+
|
|
84
|
+
const resolveLocal = async (e) => (e === epoch ? pk.toHex() : null)
|
|
85
|
+
const rpA = createBbsChallenge({ domain: 'rp-a.example.com', credentialType, requiredIndices: [4, 5] })
|
|
86
|
+
const rpB = createBbsChallenge({ domain: 'rp-b.example.com', credentialType, requiredIndices: [4] })
|
|
87
|
+
|
|
88
|
+
async function present(challenge) {
|
|
89
|
+
const ph = derivePresentationHeader(challenge.domain, Buffer.from(challenge.nonce, 'hex'))
|
|
90
|
+
const disclosedIndices = [1, ...challenge.requiredIndices].sort((x, y) => x - y)
|
|
91
|
+
const proof = await sig.createProof({ pk, header, presentationHeader: ph, messages, disclosedIndices })
|
|
92
|
+
return buildBbsPresentation({
|
|
93
|
+
challengeId: challenge.id,
|
|
94
|
+
epoch,
|
|
95
|
+
proofHex: proof.toHex(),
|
|
96
|
+
totalMessageCount: messages.length,
|
|
97
|
+
disclosed: disclosedIndices.map((i) => ({ index: i, message: hex(messages[i]) })),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
const presA = await present(rpA)
|
|
101
|
+
const presB = await present(rpB)
|
|
102
|
+
|
|
103
|
+
const opts = () => ({ issuerDid, resolveIssuerKey: resolveLocal, used: false, network: 'testnet' })
|
|
104
|
+
const resA = await verifyBbsPresentation(rpA, presA, opts())
|
|
105
|
+
const resB = await verifyBbsPresentation(rpB, presB, opts())
|
|
106
|
+
line(`RP A verify: valid=${resA.valid}`)
|
|
107
|
+
line(`RP B verify: valid=${resB.valid}`)
|
|
108
|
+
if (!resA.valid || !resB.valid) throw new Error('verification failed — demo aborted')
|
|
109
|
+
|
|
110
|
+
const replay = await verifyBbsPresentation(rpB, { ...presA, challengeId: rpB.id }, opts())
|
|
111
|
+
line(`RP B replaying A's proof under its own challenge: valid=${replay.valid} (must be false)`)
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// PART 3 — the collusion attempt: both RPs pool everything they received.
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
line()
|
|
117
|
+
line('== PART 3: RP A + RP B collude — the complete correlation surface ==')
|
|
118
|
+
const jsonA = JSON.stringify(presA)
|
|
119
|
+
const jsonB = JSON.stringify(presB)
|
|
120
|
+
|
|
121
|
+
for (const [name, needle] of [
|
|
122
|
+
['subject DID', 'demoHolderSubjectDid0000'],
|
|
123
|
+
['verification_id', hex(utf8('vrf_demo_0001'))],
|
|
124
|
+
['document_hash', hex(utf8('deadbeefdeadbeefdeadbeefdeadbeef'))],
|
|
125
|
+
]) {
|
|
126
|
+
const found = jsonA.includes(needle) || jsonB.includes(needle)
|
|
127
|
+
line(`per-holder ${name} present in either envelope: ${found ? 'YES — LINKABLE (FAIL)' : 'no'}`)
|
|
128
|
+
if (found) throw new Error('linkable identifier leaked')
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const a = Buffer.from(presA.proof, 'hex')
|
|
132
|
+
const b = Buffer.from(presB.proof, 'hex')
|
|
133
|
+
let equalBytes = 0
|
|
134
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] === b[i]) equalBytes++
|
|
135
|
+
line(`proof bytes equal at the same offset: ${equalBytes}/${Math.min(a.length, b.length)} (random baseline ≈ ${(Math.min(a.length, b.length) / 256).toFixed(1)})`)
|
|
136
|
+
|
|
137
|
+
const presA2 = await present(rpA)
|
|
138
|
+
line(`regenerated proof for the SAME challenge identical to the first: ${presA2.proof === presA.proof ? 'YES (FAIL)' : 'no (randomized)'}`)
|
|
139
|
+
|
|
140
|
+
line('fields present in BOTH envelopes:')
|
|
141
|
+
for (const [k, v] of Object.entries(presA)) {
|
|
142
|
+
if (JSON.stringify(v) === JSON.stringify(presB[k])) line(` ${k} = ${JSON.stringify(v)} ← cohort-constant, shared by every holder of this (issuer, type, epoch)`)
|
|
143
|
+
}
|
|
144
|
+
const inA = Object.fromEntries(presA.disclosed.map((d) => [d.index, d.message]))
|
|
145
|
+
for (const d of presB.disclosed) {
|
|
146
|
+
if (inA[d.index]) line(` disclosed[${d.index}] (${KYC_MESSAGE_FIELDS[d.index]}) = "${Buffer.from(d.message, 'hex')}" ← disclosed by policy to both; a cohort value, not a per-user handle`)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
line()
|
|
150
|
+
line('RESULT: both presentations verified; the pooled correlation surface contains only')
|
|
151
|
+
line('cohort-level constants and the claims each RP itself requested. No per-holder')
|
|
152
|
+
line('value appears in both envelopes.')
|
|
153
|
+
line()
|
|
154
|
+
line('HONESTY BOUNDARY — read before quoting this result:')
|
|
155
|
+
line(' This demonstrates unlinkability at the cryptographic PRESENTATION LAYER only.')
|
|
156
|
+
line(' It does NOT defend against network-level correlation (IP address, timing,')
|
|
157
|
+
line(' device fingerprints, cookies) or relying parties pooling out-of-band data')
|
|
158
|
+
line(' they collected themselves (email, phone). @solidus-network/bbs is testnet-grade;')
|
|
159
|
+
line(' external audit pending (NLnet NGI Zero, H2 2026 target).')
|
package/dist/challenge.d.ts
CHANGED
|
@@ -17,4 +17,39 @@ export interface Challenge {
|
|
|
17
17
|
* @param ttlSeconds - How long the challenge is valid (default: 300 = 5 minutes)
|
|
18
18
|
*/
|
|
19
19
|
export declare function createChallenge(did: string, ttlSeconds?: number): Challenge;
|
|
20
|
+
export interface BbsChallenge {
|
|
21
|
+
/** UUID, for idempotency and storage. */
|
|
22
|
+
id: string;
|
|
23
|
+
/**
|
|
24
|
+
* The verifier's AUTHENTICATED origin (e.g. its TLS hostname). Folded
|
|
25
|
+
* into the presentation header, so a proof made for this verifier
|
|
26
|
+
* fails at every other one.
|
|
27
|
+
*/
|
|
28
|
+
domain: string;
|
|
29
|
+
/** The credential type this challenge accepts (pins the header cohort). */
|
|
30
|
+
credentialType: string;
|
|
31
|
+
/** Message indices the presenter must disclose. */
|
|
32
|
+
requiredIndices: number[];
|
|
33
|
+
/** 32-byte random hex — folded into the presentation header. */
|
|
34
|
+
nonce: string;
|
|
35
|
+
/** ISO 8601 */
|
|
36
|
+
issuedAt: string;
|
|
37
|
+
/** ISO 8601 */
|
|
38
|
+
expiresAt: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a DID-less selective-disclosure challenge. Carries no DID by
|
|
42
|
+
* construction — the verifier learns only what the disclosed indices say.
|
|
43
|
+
*
|
|
44
|
+
* The CALLER stores the challenge and must enforce single-use (mark it
|
|
45
|
+
* used once a presentation verifies) — the cryptographic ph binding kills
|
|
46
|
+
* cross-challenge replay, storage-side bookkeeping kills same-challenge
|
|
47
|
+
* re-submission.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createBbsChallenge(opts: {
|
|
50
|
+
domain: string;
|
|
51
|
+
credentialType: string;
|
|
52
|
+
requiredIndices: number[];
|
|
53
|
+
ttlSeconds?: number;
|
|
54
|
+
}): BbsChallenge;
|
|
20
55
|
//# sourceMappingURL=challenge.d.ts.map
|
package/dist/challenge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,eAAe;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAM,GAAG,SAAS,CAUxE"}
|
|
1
|
+
{"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,SAAS;IACxB,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,wCAAwC;IACxC,GAAG,EAAE,MAAM,CAAA;IACX,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,eAAe;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAM,GAAG,SAAS,CAUxE;AASD,MAAM,WAAW,YAAY;IAC3B,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAA;IACd,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAA;IACtB,mDAAmD;IACnD,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAA;IACb,eAAe;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,cAAc,EAAE,MAAM,CAAA;IACtB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,GAAG,YAAY,CA0Bf"}
|
package/dist/challenge.js
CHANGED
|
@@ -16,4 +16,43 @@ export function createChallenge(did, ttlSeconds = 300) {
|
|
|
16
16
|
expiresAt: expiresAt.toISOString(),
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// DID-less BBS challenge (privacy rebuild §5.3) — additive beside the
|
|
21
|
+
// DID-bound challenge above; never a replacement.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
import { ALWAYS_HIDDEN_KYC_INDICES } from './envelope.js';
|
|
24
|
+
/**
|
|
25
|
+
* Create a DID-less selective-disclosure challenge. Carries no DID by
|
|
26
|
+
* construction — the verifier learns only what the disclosed indices say.
|
|
27
|
+
*
|
|
28
|
+
* The CALLER stores the challenge and must enforce single-use (mark it
|
|
29
|
+
* used once a presentation verifies) — the cryptographic ph binding kills
|
|
30
|
+
* cross-challenge replay, storage-side bookkeeping kills same-challenge
|
|
31
|
+
* re-submission.
|
|
32
|
+
*/
|
|
33
|
+
export function createBbsChallenge(opts) {
|
|
34
|
+
if (!opts.domain)
|
|
35
|
+
throw new Error('domain must be non-empty');
|
|
36
|
+
if (!opts.credentialType)
|
|
37
|
+
throw new Error('credentialType must be non-empty');
|
|
38
|
+
const forbidden = opts.requiredIndices.filter((i) => ALWAYS_HIDDEN_KYC_INDICES.includes(i));
|
|
39
|
+
if (forbidden.length > 0) {
|
|
40
|
+
throw new Error(`requiredIndices ${forbidden.join(',')} are always-hidden fields ` +
|
|
41
|
+
`(subject_did/verification_id/document_hash) and can never be requested`);
|
|
42
|
+
}
|
|
43
|
+
if (opts.requiredIndices.some((i) => !Number.isInteger(i) || i < 0)) {
|
|
44
|
+
throw new Error('requiredIndices must be non-negative integers');
|
|
45
|
+
}
|
|
46
|
+
const now = new Date();
|
|
47
|
+
const ttl = opts.ttlSeconds ?? 300;
|
|
48
|
+
return {
|
|
49
|
+
id: randomUUID(),
|
|
50
|
+
domain: opts.domain,
|
|
51
|
+
credentialType: opts.credentialType,
|
|
52
|
+
requiredIndices: [...opts.requiredIndices],
|
|
53
|
+
nonce: randomBytes(32).toString('hex'),
|
|
54
|
+
issuedAt: now.toISOString(),
|
|
55
|
+
expiresAt: new Date(now.getTime() + ttl * 1_000).toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
19
58
|
//# sourceMappingURL=challenge.js.map
|
package/dist/challenge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAerD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,UAAU,GAAG,GAAG;IAC3D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,UAAU,GAAG,KAAK,CAAC,CAAA;IAC9D,OAAO;QACL,EAAE,EAAE,UAAU,EAAE;QAChB,GAAG;QACH,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtC,QAAQ,EAAE,GAAG,CAAC,WAAW,EAAE;QAC3B,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;KACnC,CAAA;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAerD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,UAAU,GAAG,GAAG;IAC3D,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,UAAU,GAAG,KAAK,CAAC,CAAA;IAC9D,OAAO;QACL,EAAE,EAAE,UAAU,EAAE;QAChB,GAAG;QACH,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtC,QAAQ,EAAE,GAAG,CAAC,WAAW,EAAE;QAC3B,SAAS,EAAE,SAAS,CAAC,WAAW,EAAE;KACnC,CAAA;AACH,CAAC;AAED,8EAA8E;AAC9E,sEAAsE;AACtE,kDAAkD;AAClD,8EAA8E;AAE9E,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AAuBzD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAKlC;IACC,IAAI,CAAC,IAAI,CAAC,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAA;IAC7D,IAAI,CAAC,IAAI,CAAC,cAAc;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAA;IAC7E,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CACjD,yBAA+C,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC7D,CAAA;IACD,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,mBAAmB,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,4BAA4B;YAChE,wEAAwE,CAC3E,CAAA;IACH,CAAC;IACD,IAAI,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAA;IAClE,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,CAAA;IAClC,OAAO;QACL,EAAE,EAAE,UAAU,EAAE;QAChB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,eAAe,EAAE,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC;QAC1C,KAAK,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QACtC,QAAQ,EAAE,GAAG,CAAC,WAAW,EAAE;QAC3B,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,WAAW,EAAE;KAC/D,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DID-less selective-disclosure presentation envelope (privacy rebuild §5.3).
|
|
3
|
+
*
|
|
4
|
+
* A `BbsSelectiveDisclosurePresentation` is what a wallet sends a relying
|
|
5
|
+
* party: a randomized BBS+ proof over the issuer-signed credential,
|
|
6
|
+
* disclosing exactly the requested message indices. By construction it has
|
|
7
|
+
* NO `holder` field, no subject DID, no per-credential identifier — the
|
|
8
|
+
* only things a verifier learns are "a valid credential from this issuer's
|
|
9
|
+
* (type, epoch) cohort" plus the disclosed claim values.
|
|
10
|
+
*
|
|
11
|
+
* DELIBERATE OMISSION vs the design sketch: the envelope does NOT carry
|
|
12
|
+
* the BBS header. The verifier RECONSTRUCTS the header from its own
|
|
13
|
+
* trusted (issuerDid, credentialType, epoch) — a header taken from the
|
|
14
|
+
* envelope would let a credential of a different cohort (e.g. a lower KYC
|
|
15
|
+
* level) satisfy a check whose policy expects another, and invites
|
|
16
|
+
* third-party RP implementations to skip policy binding entirely.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: the wallet builds envelopes client-side — no node imports.
|
|
19
|
+
*/
|
|
20
|
+
/** Canonical KYC message order (frozen — mirrors verify's `buildKycMessageVector`). */
|
|
21
|
+
export declare const KYC_MESSAGE_FIELDS: readonly ["subject_did", "issuer_did", "verification_id", "verified_at", "kyc_level", "country", "document_type", "document_hash"];
|
|
22
|
+
/**
|
|
23
|
+
* Indices that must NEVER be disclosed (§5.2 disposition): the subject
|
|
24
|
+
* DID (the T2 handle), the per-credential verification id (the old
|
|
25
|
+
* header leak), and the per-document hash. Verifiers reject envelopes
|
|
26
|
+
* disclosing any of these even when the proof is cryptographically valid.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ALWAYS_HIDDEN_KYC_INDICES: readonly [0, 2, 7];
|
|
29
|
+
export declare const BBS_PRESENTATION_TYPE = "BbsSelectiveDisclosurePresentation";
|
|
30
|
+
export interface DisclosedMessage {
|
|
31
|
+
/** Zero-based index in the original signed vector. */
|
|
32
|
+
index: number;
|
|
33
|
+
/** Hex-encoded message bytes. */
|
|
34
|
+
message: string;
|
|
35
|
+
}
|
|
36
|
+
export interface BbsSelectiveDisclosurePresentation {
|
|
37
|
+
type: typeof BBS_PRESENTATION_TYPE;
|
|
38
|
+
/** Credential-format version marker. */
|
|
39
|
+
v: 'bbs-v2';
|
|
40
|
+
/** The challenge this presentation answers. */
|
|
41
|
+
challengeId: string;
|
|
42
|
+
/** Issuer key-rotation epoch (cohort label — selects the pubkey). */
|
|
43
|
+
epoch: number;
|
|
44
|
+
/** Hex-encoded randomized BBS+ proof. */
|
|
45
|
+
proof: string;
|
|
46
|
+
totalMessageCount: number;
|
|
47
|
+
disclosed: DisclosedMessage[];
|
|
48
|
+
/** Re-recognition (mechanism B) only — §5.4. Null in one-shot mode. */
|
|
49
|
+
pairwiseDid: string | null;
|
|
50
|
+
/** VP signed by the pairwise key — §5.4. Null in one-shot mode. */
|
|
51
|
+
pairwiseProof: Record<string, unknown> | null;
|
|
52
|
+
}
|
|
53
|
+
export interface BuildBbsPresentationInput {
|
|
54
|
+
challengeId: string;
|
|
55
|
+
epoch: number;
|
|
56
|
+
proofHex: string;
|
|
57
|
+
totalMessageCount: number;
|
|
58
|
+
disclosed: DisclosedMessage[];
|
|
59
|
+
pairwiseDid?: string | null;
|
|
60
|
+
pairwiseProof?: Record<string, unknown> | null;
|
|
61
|
+
}
|
|
62
|
+
/** Build (and validate) a presentation envelope — wallet side. */
|
|
63
|
+
export declare function buildBbsPresentation(input: BuildBbsPresentationInput): BbsSelectiveDisclosurePresentation;
|
|
64
|
+
/** Parse + strictly validate a serialized envelope — verifier side. */
|
|
65
|
+
export declare function parseBbsPresentation(json: string): BbsSelectiveDisclosurePresentation;
|
|
66
|
+
/**
|
|
67
|
+
* Decode disclosed messages to named KYC fields (utf8). Only disclosed
|
|
68
|
+
* indices appear; always-hidden fields can never appear in a valid
|
|
69
|
+
* envelope (the verifier rejects them).
|
|
70
|
+
*/
|
|
71
|
+
export declare function decodeDisclosedKycFields(disclosed: DisclosedMessage[]): Partial<Record<(typeof KYC_MESSAGE_FIELDS)[number], string>>;
|
|
72
|
+
//# sourceMappingURL=envelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.d.ts","sourceRoot":"","sources":["../src/envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,uFAAuF;AACvF,eAAO,MAAM,kBAAkB,oIASrB,CAAA;AAEV;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,oBAAqB,CAAA;AAE3D,eAAO,MAAM,qBAAqB,uCAAuC,CAAA;AAEzE,MAAM,WAAW,gBAAgB;IAC/B,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAA;IACb,iCAAiC;IACjC,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,kCAAkC;IACjD,IAAI,EAAE,OAAO,qBAAqB,CAAA;IAClC,wCAAwC;IACxC,CAAC,EAAE,QAAQ,CAAA;IACX,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAA;IACb,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,gBAAgB,EAAE,CAAA;IAC7B,uEAAuE;IACvE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC9C;AAQD,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,gBAAgB,EAAE,CAAA;IAC7B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC/C;AAED,kEAAkE;AAClE,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,yBAAyB,GAC/B,kCAAkC,CAcpC;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,kCAAkC,CAwBrF;AAmDD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE,GAC5B,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAa9D"}
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DID-less selective-disclosure presentation envelope (privacy rebuild §5.3).
|
|
3
|
+
*
|
|
4
|
+
* A `BbsSelectiveDisclosurePresentation` is what a wallet sends a relying
|
|
5
|
+
* party: a randomized BBS+ proof over the issuer-signed credential,
|
|
6
|
+
* disclosing exactly the requested message indices. By construction it has
|
|
7
|
+
* NO `holder` field, no subject DID, no per-credential identifier — the
|
|
8
|
+
* only things a verifier learns are "a valid credential from this issuer's
|
|
9
|
+
* (type, epoch) cohort" plus the disclosed claim values.
|
|
10
|
+
*
|
|
11
|
+
* DELIBERATE OMISSION vs the design sketch: the envelope does NOT carry
|
|
12
|
+
* the BBS header. The verifier RECONSTRUCTS the header from its own
|
|
13
|
+
* trusted (issuerDid, credentialType, epoch) — a header taken from the
|
|
14
|
+
* envelope would let a credential of a different cohort (e.g. a lower KYC
|
|
15
|
+
* level) satisfy a check whose policy expects another, and invites
|
|
16
|
+
* third-party RP implementations to skip policy binding entirely.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: the wallet builds envelopes client-side — no node imports.
|
|
19
|
+
*/
|
|
20
|
+
/** Canonical KYC message order (frozen — mirrors verify's `buildKycMessageVector`). */
|
|
21
|
+
export const KYC_MESSAGE_FIELDS = [
|
|
22
|
+
'subject_did',
|
|
23
|
+
'issuer_did',
|
|
24
|
+
'verification_id',
|
|
25
|
+
'verified_at',
|
|
26
|
+
'kyc_level',
|
|
27
|
+
'country',
|
|
28
|
+
'document_type',
|
|
29
|
+
'document_hash',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Indices that must NEVER be disclosed (§5.2 disposition): the subject
|
|
33
|
+
* DID (the T2 handle), the per-credential verification id (the old
|
|
34
|
+
* header leak), and the per-document hash. Verifiers reject envelopes
|
|
35
|
+
* disclosing any of these even when the proof is cryptographically valid.
|
|
36
|
+
*/
|
|
37
|
+
export const ALWAYS_HIDDEN_KYC_INDICES = [0, 2, 7];
|
|
38
|
+
export const BBS_PRESENTATION_TYPE = 'BbsSelectiveDisclosurePresentation';
|
|
39
|
+
const HEX_RE = /^[0-9a-f]*$/i;
|
|
40
|
+
function isHex(s) {
|
|
41
|
+
return HEX_RE.test(s) && s.length % 2 === 0;
|
|
42
|
+
}
|
|
43
|
+
/** Build (and validate) a presentation envelope — wallet side. */
|
|
44
|
+
export function buildBbsPresentation(input) {
|
|
45
|
+
const envelope = {
|
|
46
|
+
type: BBS_PRESENTATION_TYPE,
|
|
47
|
+
v: 'bbs-v2',
|
|
48
|
+
challengeId: input.challengeId,
|
|
49
|
+
epoch: input.epoch,
|
|
50
|
+
proof: input.proofHex,
|
|
51
|
+
totalMessageCount: input.totalMessageCount,
|
|
52
|
+
disclosed: input.disclosed.map((d) => ({ index: d.index, message: d.message })),
|
|
53
|
+
pairwiseDid: input.pairwiseDid ?? null,
|
|
54
|
+
pairwiseProof: input.pairwiseProof ?? null,
|
|
55
|
+
};
|
|
56
|
+
assertEnvelopeShape(envelope);
|
|
57
|
+
return envelope;
|
|
58
|
+
}
|
|
59
|
+
/** Parse + strictly validate a serialized envelope — verifier side. */
|
|
60
|
+
export function parseBbsPresentation(json) {
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = JSON.parse(json);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
throw new Error('presentation is not valid JSON');
|
|
67
|
+
}
|
|
68
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
69
|
+
throw new Error('presentation must be a JSON object');
|
|
70
|
+
}
|
|
71
|
+
const e = raw;
|
|
72
|
+
const envelope = {
|
|
73
|
+
type: e['type'],
|
|
74
|
+
v: e['v'],
|
|
75
|
+
challengeId: e['challengeId'],
|
|
76
|
+
epoch: e['epoch'],
|
|
77
|
+
proof: e['proof'],
|
|
78
|
+
totalMessageCount: e['totalMessageCount'],
|
|
79
|
+
disclosed: e['disclosed'],
|
|
80
|
+
pairwiseDid: (e['pairwiseDid'] ?? null),
|
|
81
|
+
pairwiseProof: (e['pairwiseProof'] ?? null),
|
|
82
|
+
};
|
|
83
|
+
assertEnvelopeShape(envelope);
|
|
84
|
+
return envelope;
|
|
85
|
+
}
|
|
86
|
+
function assertEnvelopeShape(e) {
|
|
87
|
+
if (e.type !== BBS_PRESENTATION_TYPE) {
|
|
88
|
+
throw new Error(`presentation type must be ${BBS_PRESENTATION_TYPE}`);
|
|
89
|
+
}
|
|
90
|
+
if (e.v !== 'bbs-v2')
|
|
91
|
+
throw new Error('unsupported presentation version');
|
|
92
|
+
if (typeof e.challengeId !== 'string' || e.challengeId.length === 0) {
|
|
93
|
+
throw new Error('challengeId must be a non-empty string');
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isInteger(e.epoch) || e.epoch < 0) {
|
|
96
|
+
throw new Error('epoch must be a non-negative integer');
|
|
97
|
+
}
|
|
98
|
+
if (typeof e.proof !== 'string' || e.proof.length === 0 || !isHex(e.proof)) {
|
|
99
|
+
throw new Error('proof must be non-empty hex');
|
|
100
|
+
}
|
|
101
|
+
if (!Number.isInteger(e.totalMessageCount) ||
|
|
102
|
+
e.totalMessageCount < 1 ||
|
|
103
|
+
e.totalMessageCount > 64) {
|
|
104
|
+
throw new Error('totalMessageCount out of range');
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(e.disclosed))
|
|
107
|
+
throw new Error('disclosed must be an array');
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
for (const d of e.disclosed) {
|
|
110
|
+
if (typeof d !== 'object' ||
|
|
111
|
+
d === null ||
|
|
112
|
+
!Number.isInteger(d.index) ||
|
|
113
|
+
d.index < 0 ||
|
|
114
|
+
d.index >= e.totalMessageCount ||
|
|
115
|
+
typeof d.message !== 'string' ||
|
|
116
|
+
!isHex(d.message)) {
|
|
117
|
+
throw new Error('disclosed entries must be {index within range, hex message}');
|
|
118
|
+
}
|
|
119
|
+
if (seen.has(d.index))
|
|
120
|
+
throw new Error(`duplicate disclosed index ${d.index}`);
|
|
121
|
+
seen.add(d.index);
|
|
122
|
+
}
|
|
123
|
+
if (e.pairwiseDid !== null && (typeof e.pairwiseDid !== 'string' || e.pairwiseDid.length === 0)) {
|
|
124
|
+
throw new Error('pairwiseDid must be null or a non-empty string');
|
|
125
|
+
}
|
|
126
|
+
if (e.pairwiseProof !== null && (typeof e.pairwiseProof !== 'object' || Array.isArray(e.pairwiseProof))) {
|
|
127
|
+
throw new Error('pairwiseProof must be null or an object');
|
|
128
|
+
}
|
|
129
|
+
if ((e.pairwiseDid === null) !== (e.pairwiseProof === null)) {
|
|
130
|
+
throw new Error('pairwiseDid and pairwiseProof must be provided together');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Decode disclosed messages to named KYC fields (utf8). Only disclosed
|
|
135
|
+
* indices appear; always-hidden fields can never appear in a valid
|
|
136
|
+
* envelope (the verifier rejects them).
|
|
137
|
+
*/
|
|
138
|
+
export function decodeDisclosedKycFields(disclosed) {
|
|
139
|
+
const out = {};
|
|
140
|
+
const decoder = new TextDecoder();
|
|
141
|
+
for (const d of disclosed) {
|
|
142
|
+
const field = KYC_MESSAGE_FIELDS[d.index];
|
|
143
|
+
if (!field)
|
|
144
|
+
continue;
|
|
145
|
+
const bytes = new Uint8Array(d.message.length / 2);
|
|
146
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
147
|
+
bytes[i] = parseInt(d.message.slice(i * 2, i * 2 + 2), 16);
|
|
148
|
+
}
|
|
149
|
+
out[field] = decoder.decode(bytes);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=envelope.js.map
|