did-beta-bch 1.3.0 → 2.0.0
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/CHANGELOG.md +16 -1
- package/PROTOCOL.md +146 -116
- package/README.md +76 -20
- package/dist/anchor.d.ts +1 -1
- package/dist/anchor.js +13 -2
- package/dist/binding.d.ts +5 -11
- package/dist/binding.js +16 -6
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.js +51 -11
- package/dist/disclosure.d.ts +8 -4
- package/dist/disclosure.js +18 -6
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/nip98.d.ts +2 -0
- package/dist/nip98.js +25 -5
- package/dist/signing.d.ts +16 -0
- package/dist/signing.js +30 -0
- package/dist/verify.d.ts +12 -13
- package/dist/verify.js +327 -99
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `cashlet-did` will be documented here.
|
|
4
4
|
|
|
5
|
+
## [2.0.0] — 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
- V2 is the only supported binding protocol; v1 bindings and raw/hex proof signatures are no longer verified.
|
|
9
|
+
- BCH proof tags now use `["bch-proof", "bitcoin-message/compact-base64", message, signatureBase64]`.
|
|
10
|
+
- Wallet signatures must be standard padded Base64 for a 65-byte Bitcoin compact-message signature with compressed-key header 31–34.
|
|
11
|
+
- Binding purpose and BCH proof domain are versioned to `identity-binding-v2` and `cashlet-did/bch-proof/v2.0.0`.
|
|
12
|
+
- Challenge payloads are versioned as `cashlet-did/challenge/v2.0.0\n<nonce>[\n<audience>]`.
|
|
13
|
+
- `nostrEventTemplate` accepts `{ message, signatureBase64 }` and sets event `created_at` to the binding timestamp.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `BCH_PROOF_FORMAT`, `BchMessageSigner`, `normalizeBchMessageSignature`, and `signBchMessage` provide strict transport-independent signing validation.
|
|
17
|
+
- `bchChallengePayload(nonce, audience?)` constructs the exact challenge text expected by `verifyChallengeProof`.
|
|
18
|
+
- Verifier now binds v2 content, proof, tags, event time, Nostr key/npub, BCH address, network, and binding ID; resolution skips v1 events.
|
|
19
|
+
|
|
5
20
|
## [1.2.0] — 2026-07-31
|
|
6
21
|
|
|
7
22
|
### Fixed
|
|
@@ -28,7 +43,7 @@ All notable changes to `cashlet-did` will be documented here.
|
|
|
28
43
|
|
|
29
44
|
### Added
|
|
30
45
|
- `buildAnchorTx(bindingId, utxo)` — build an OP_RETURN BCH transaction anchoring a binding to the chain
|
|
31
|
-
- `issueCredential(subjectNpub, type, issuerPrivkey)` — issue a third-party Nostr attestation (kind
|
|
46
|
+
- `issueCredential(subjectNpub, type, issuerPrivkey)` — issue a third-party Nostr attestation (kind 30085)
|
|
32
47
|
- `resolveCredentials(npub)` — fetch all credentials issued to an npub from relays
|
|
33
48
|
- `buildDisclosure(attrs)` — commit to a set of attributes with individual SHA-256 hashes
|
|
34
49
|
- `verifyDisclosure(revealed, commitments)` — verify selectively revealed attributes against commitments
|
package/PROTOCOL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Cashlet DID Protocol Specification
|
|
2
2
|
|
|
3
|
-
Version: `
|
|
3
|
+
Version: `2.0.0`
|
|
4
4
|
Status: Draft
|
|
5
5
|
|
|
6
|
-
This
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
This is the normative specification for Cashlet DID v2. V2 is a breaking
|
|
7
|
+
signature-format update: conforming verifiers MUST reject v1 proof tags and
|
|
8
|
+
signatures. Existing v1 bindings must be re-created and re-published.
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
@@ -14,201 +14,231 @@ client can implement compatibility by following this spec — no dependency on t
|
|
|
14
14
|
| Constant | Value |
|
|
15
15
|
|---|---|
|
|
16
16
|
| Protocol ID | `cashlet-did` |
|
|
17
|
-
| Protocol version | `
|
|
17
|
+
| Protocol version | `2.0.0` |
|
|
18
18
|
| Binding event kind | `30078` (NIP-78 addressable data) |
|
|
19
19
|
| Credential event kind | `30085` |
|
|
20
20
|
| Revocation event kind | `5` (NIP-09) |
|
|
21
21
|
| Nostr derivation path | `m/44'/1237'/<account>'/0/0` (NIP-06) |
|
|
22
22
|
|
|
23
|
-
---
|
|
24
|
-
|
|
25
23
|
## 2. Canonical binding record
|
|
26
24
|
|
|
27
|
-
A binding record
|
|
28
|
-
|
|
25
|
+
A binding record has recursively sorted object keys, no whitespace, and omits
|
|
26
|
+
undefined/absent fields:
|
|
29
27
|
|
|
30
28
|
```json
|
|
31
29
|
{
|
|
32
30
|
"bch": {
|
|
33
|
-
"address": "<cashaddr>",
|
|
31
|
+
"address": "<prefixed-cashaddr>",
|
|
34
32
|
"network": "mainnet" | "testnet" | "chipnet",
|
|
35
33
|
"paymentCode": "<BIP47 payment code>" // optional
|
|
36
34
|
},
|
|
37
35
|
"createdAt": <unix seconds>,
|
|
38
|
-
"nostr": { "pubkey": "<32-byte hex>" },
|
|
39
|
-
"nonce": "<64 hex chars
|
|
40
|
-
"previousBindingId": "<hex>",
|
|
36
|
+
"nostr": { "pubkey": "<32-byte lowercase hex>" },
|
|
37
|
+
"nonce": "<64 lowercase hex chars>",
|
|
38
|
+
"previousBindingId": "<64 lowercase hex chars>", // optional
|
|
41
39
|
"protocol": "cashlet-did",
|
|
42
|
-
"purpose": "identity-binding-
|
|
43
|
-
"version": "
|
|
40
|
+
"purpose": "identity-binding-v2",
|
|
41
|
+
"version": "2.0.0"
|
|
44
42
|
}
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
**Binding ID** = `SHA-256(canonicalize(record_without_bindingId))
|
|
45
|
+
**Binding ID** = lowercase hex `SHA-256(canonicalize(record_without_bindingId))`.
|
|
46
|
+
Arrays preserve order. `mainnet` addresses use the `bitcoincash:` prefix;
|
|
47
|
+
`testnet` and `chipnet` use `bchtest:`.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
omit keys whose value is `undefined`. Arrays preserve order.
|
|
49
|
+
## 3. BCH signing payload and wallet contract
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
The exact UTF-8 message text supplied to a wallet is:
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
cashlet-did/bch-proof/v1.0.0\n<canonicalized subset>
|
|
53
|
+
```text
|
|
54
|
+
cashlet-did/bch-proof/v2.0.0\n<canonicalized subset>
|
|
58
55
|
```
|
|
59
56
|
|
|
60
|
-
The
|
|
57
|
+
The subset contains exactly these keys:
|
|
61
58
|
|
|
62
59
|
```json
|
|
63
60
|
{
|
|
64
|
-
"bchAddress": "<cashaddr>",
|
|
61
|
+
"bchAddress": "<prefixed-cashaddr>",
|
|
65
62
|
"bchNetwork": "mainnet" | "testnet" | "chipnet",
|
|
66
|
-
"bindingId": "<hex>",
|
|
63
|
+
"bindingId": "<64 lowercase hex chars>",
|
|
67
64
|
"createdAt": <unix seconds>,
|
|
68
|
-
"nonce": "<hex>",
|
|
69
|
-
"nostrPubkey": "<hex>",
|
|
70
|
-
"purpose": "identity-binding-
|
|
65
|
+
"nonce": "<64 lowercase hex chars>",
|
|
66
|
+
"nostrPubkey": "<32-byte lowercase hex>",
|
|
67
|
+
"purpose": "identity-binding-v2"
|
|
71
68
|
}
|
|
72
69
|
```
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
Pass this exact message string to the wallet's message-signing method. Do not
|
|
72
|
+
pre-hash, add a Bitcoin prefix, or otherwise transform it in the adapter. The
|
|
73
|
+
wallet computes:
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
SHA256d( "\x18Bitcoin Signed Message:\n" + varint(len(payload_bytes)) + payload_bytes )
|
|
75
|
+
```text
|
|
76
|
+
SHA256d(0x18 || "Bitcoin Signed Message:\n" ||
|
|
77
|
+
CompactSize(UTF8(message).length) || UTF8(message))
|
|
80
78
|
```
|
|
81
79
|
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
`CompactSize` uses Bitcoin's little-endian encodings: one byte below `0xfd`,
|
|
81
|
+
`fd` plus 2 bytes through `0xffff`, `fe` plus 4 bytes through `0xffffffff`,
|
|
82
|
+
then `ff` plus 8 bytes.
|
|
85
83
|
|
|
86
|
-
The
|
|
87
|
-
`[recovery_id_byte (0–3)] + [64-byte compact sig]`, hex-encoded.
|
|
84
|
+
The wallet response is standard padded Base64 encoding of exactly 65 bytes:
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
```text
|
|
87
|
+
[header byte 31–34] || [32-byte r] || [32-byte s]
|
|
88
|
+
```
|
|
91
89
|
|
|
92
|
-
|
|
90
|
+
The header is `27 + 4 + recovery_id`; recovery IDs are `0–3`, and `+4`
|
|
91
|
+
specifies a compressed public key. V2 accepts only this compressed-key form.
|
|
92
|
+
Verifiers decode Base64, subtract 31 from the header, recover the compressed
|
|
93
|
+
public key, and derive its prefixed BCH CashAddr. Raw hex, 64-byte signatures,
|
|
94
|
+
uncompressed headers, non-canonical Base64, and v1 signatures are rejected.
|
|
95
|
+
|
|
96
|
+
### Wallet interoperability evidence
|
|
97
|
+
|
|
98
|
+
Paytaca's WalletConnect implementation signs the supplied message with the
|
|
99
|
+
Bitcoin message prefix, `bigIntToCompactUint`, double-SHA256, and recoverable
|
|
100
|
+
compact ECDSA; it adds `31` for compressed keys and returns Base64:
|
|
101
|
+
[Paytaca source](https://github.com/Paytaca/paytaca-app/blob/41c785b9f48dee337d2da3f33e082e5210d462d0/src/wallet/walletconnect2/index.js).
|
|
102
|
+
The BCH WalletConnect BCR also specifies Electron Cash-compatible signed
|
|
103
|
+
messages: [BCR](https://github.com/mainnet-pat/wc2-bch-bcr/blob/55e3e37142ddc7b30ad63c7330ef8df252fec715/README.md).
|
|
104
|
+
The library keeps this contract transport-neutral; validate each wallet
|
|
105
|
+
release's response in integration tests before advertising support.
|
|
93
106
|
|
|
94
107
|
## 4. Nostr event (kind 30078)
|
|
95
108
|
|
|
109
|
+
The event timestamp MUST equal the binding's `createdAt`. Required tags:
|
|
110
|
+
|
|
96
111
|
```json
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
["bch-proof", "<signing payload>", "<65-byte sig hex>"],
|
|
105
|
-
["protocol", "cashlet-did", "1.0.0"],
|
|
106
|
-
["t", "cashlet-did"],
|
|
107
|
-
["bch-payment-code", "<BIP47 code>"], // optional
|
|
108
|
-
["supersedes", "<previous bindingId>"], // optional, key rotation
|
|
109
|
-
["bch-anchor", "<txid>", "<block timestamp unix>"] // optional
|
|
110
|
-
],
|
|
111
|
-
"content": "<canonicalized binding JSON>",
|
|
112
|
-
"id": "<NIP-01 event id>",
|
|
113
|
-
"sig": "<Schnorr sig>"
|
|
114
|
-
}
|
|
112
|
+
[
|
|
113
|
+
["d", "<bindingId>"],
|
|
114
|
+
["bch", "<cashaddr>", "<network>"],
|
|
115
|
+
["protocol", "cashlet-did", "2.0.0"],
|
|
116
|
+
["t", "cashlet-did"],
|
|
117
|
+
["bch-proof", "bitcoin-message/compact-base64", "<exact message>", "<signature Base64>"]
|
|
118
|
+
]
|
|
115
119
|
```
|
|
116
120
|
|
|
117
|
-
|
|
121
|
+
Optional tags:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
["bch-payment-code", "<BIP47 code>"] // must equal content.bch.paymentCode
|
|
125
|
+
["supersedes", "<previous bindingId>"] // must equal content.previousBindingId
|
|
126
|
+
["bch-anchor", "<txid>", "<claimed height>"] // unverified chain claim
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`content` is the canonicalized JSON object containing `protocol`, `version`,
|
|
130
|
+
`bindingId`, `bch`, `nonce`, `createdAt`, and optional `previousBindingId`.
|
|
131
|
+
`bch` contains `address`, `network`, and optional `paymentCode`. Nostr event
|
|
132
|
+
signatures protect all event tags and content.
|
|
133
|
+
|
|
134
|
+
## 5. Discovery and revocation
|
|
118
135
|
|
|
119
|
-
|
|
136
|
+
Discover v2 records with this NIP-01 filter:
|
|
120
137
|
|
|
121
138
|
```json
|
|
122
139
|
{ "kinds": [30078], "authors": ["<pubkey hex>"], "#t": ["cashlet-did"] }
|
|
123
140
|
```
|
|
124
141
|
|
|
125
|
-
Query any public Nostr relay. Default relays: `wss://nos.lol`,
|
|
126
|
-
`wss://relay.damus.io`, `wss://relay.nostr.band`.
|
|
127
|
-
|
|
128
142
|
Revocations are kind `5` events from the same author with `["e", "<event id>"]`
|
|
129
|
-
tags. Filter revoked
|
|
130
|
-
|
|
131
|
-
---
|
|
143
|
+
tags. Filter revoked records before selecting the newest v2 binding.
|
|
132
144
|
|
|
133
145
|
## 6. Verification algorithm
|
|
134
146
|
|
|
135
|
-
1.
|
|
136
|
-
2.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
1. Verify the NIP-01 event signature and require kind `30078`.
|
|
148
|
+
2. Require exactly one correctly shaped `d`, `bch`, `protocol`, `t`, and
|
|
149
|
+
`bch-proof` tag. The proof tag MUST have exactly four elements and the v2
|
|
150
|
+
format marker. Check optional payment-code and supersedes tags against
|
|
151
|
+
content; reject duplicate or malformed known tags.
|
|
152
|
+
3. Parse content and require its canonical serialization, exact v2 fields,
|
|
153
|
+
event timestamp, `d`, `bch`, protocol/version tags, and resolved identity
|
|
154
|
+
(`npub`, Nostr pubkey, address, network, and binding ID) to agree.
|
|
155
|
+
4. Recompute the binding ID and reconstruct the exact v2 BCH signing payload.
|
|
156
|
+
The proof tag's message must be byte-for-byte identical.
|
|
157
|
+
5. Decode the canonical 65-byte Base64 compact signature, recover the
|
|
158
|
+
compressed public key from the Bitcoin signed-message hash, and derive
|
|
159
|
+
the network-appropriate BCH CashAddr. It must equal the bound address.
|
|
160
|
+
6. Report `verified` only when both the Nostr and BCH signatures are valid.
|
|
161
|
+
|
|
162
|
+
V1 tags do not satisfy the v2 proof-tag structure and MUST NOT be accepted as
|
|
163
|
+
fallbacks.
|
|
146
164
|
|
|
147
165
|
## 7. Challenge-response authentication
|
|
148
166
|
|
|
149
|
-
|
|
167
|
+
The exact v2 message is either:
|
|
150
168
|
|
|
151
|
-
```
|
|
152
|
-
cashlet-did/challenge\n<nonce
|
|
169
|
+
```text
|
|
170
|
+
cashlet-did/challenge/v2.0.0\n<nonce>
|
|
153
171
|
```
|
|
154
172
|
|
|
155
|
-
|
|
156
|
-
- `audience`: verifier's URL or identifier (optional but recommended)
|
|
173
|
+
or, with an audience:
|
|
157
174
|
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
```text
|
|
176
|
+
cashlet-did/challenge/v2.0.0\n<nonce>\n<audience>
|
|
177
|
+
```
|
|
160
178
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
179
|
+
The no-audience form has exactly two lines; the audience form has exactly three.
|
|
180
|
+
No extra lines or trailing newline are allowed. The nonce and optional audience
|
|
181
|
+
are single-line strings. The BCH wallet signs the exact text using §3's Bitcoin
|
|
182
|
+
signed-message hash and 65-byte Base64 signature.
|
|
183
|
+
`bchChallengePayload(nonce, audience?)` constructs the payload;
|
|
184
|
+
`verifyChallengeProof` checks exact equality, signature, address, and—when
|
|
185
|
+
`issuedAt` is provided—a 5-minute TTL. Applications must issue and consume
|
|
186
|
+
one-time challenges and separately verify Nostr control if required.
|
|
164
187
|
|
|
165
188
|
## 8. Key rotation
|
|
166
189
|
|
|
167
|
-
Set `previousBindingId` in the new binding
|
|
168
|
-
`["supersedes", "<previousBindingId>"]` tag
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
---
|
|
190
|
+
Set `previousBindingId` in the new binding and add the matching
|
|
191
|
+
`["supersedes", "<previousBindingId>"]` tag. Resolvers can follow references
|
|
192
|
+
backwards, but must independently verify each referenced binding and fail
|
|
193
|
+
closed if a predecessor is unavailable or invalid. A supersedes tag authored
|
|
194
|
+
by the new key does not prove that the previous Nostr key authorized rotation.
|
|
174
195
|
|
|
175
196
|
## 9. Selective disclosure
|
|
176
197
|
|
|
177
|
-
|
|
198
|
+
The library's selective-disclosure helper commits each attribute using an
|
|
199
|
+
independent random 32-byte salt:
|
|
178
200
|
|
|
179
|
-
```
|
|
180
|
-
SHA-256(
|
|
201
|
+
```text
|
|
202
|
+
SHA-256("cashlet-did/attr/v2\n" + JSON([key, value, salt]))
|
|
181
203
|
```
|
|
182
204
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
---
|
|
205
|
+
Keep salts private until revealing their associated values; include the value
|
|
206
|
+
and salt in each reveal so a verifier can recompute the commitment. The helper
|
|
207
|
+
does not publish commitments or attach them to a binding event automatically.
|
|
187
208
|
|
|
188
209
|
## 10. On-chain commitment (OP_RETURN)
|
|
189
210
|
|
|
190
211
|
Embed the binding ID in a BCH transaction output:
|
|
191
212
|
|
|
192
|
-
```
|
|
213
|
+
```text
|
|
193
214
|
OP_RETURN <"cashlet-did:" + bindingId>
|
|
194
215
|
```
|
|
195
216
|
|
|
196
|
-
The
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
---
|
|
217
|
+
The library can construct an anchor transaction, but it does not broadcast the
|
|
218
|
+
transaction or verify its inclusion in a confirmed block. A `bch-anchor` event
|
|
219
|
+
tag is only an issuer-provided claim; applications that need confirmed
|
|
220
|
+
anchoring must independently verify the transaction and block inclusion.
|
|
202
221
|
|
|
203
222
|
## 11. NIP-05 handle resolution
|
|
204
223
|
|
|
205
|
-
`user@domain` → `GET https://domain/.well-known/nostr.json?name=user`
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
---
|
|
224
|
+
`user@domain` → `GET https://domain/.well-known/nostr.json?name=user` →
|
|
225
|
+
extract `pubkey` → resolve the v2 binding via §5.
|
|
209
226
|
|
|
210
227
|
## 12. NIP-98 HTTP Auth
|
|
211
228
|
|
|
212
229
|
Build a kind `27235` event per NIP-98. The `verifyBch: true` option on the
|
|
213
|
-
server additionally resolves the signer's
|
|
214
|
-
|
|
230
|
+
server additionally resolves the signer's v2 Cashlet DID binding. The verifier
|
|
231
|
+
must receive the actual request body (`null` when absent) and compare its
|
|
232
|
+
SHA-256 hash with the event's `payload` tag. Reject missing, duplicate, or
|
|
233
|
+
mismatched payload hashes and enforce the documented 60-second event window.
|
|
234
|
+
|
|
235
|
+
## 13. Credentials
|
|
236
|
+
|
|
237
|
+
Credential events use kind `30085`. A resolver must verify the Nostr event
|
|
238
|
+
signature, require the event's `p` tag and credential payload subject to match
|
|
239
|
+
the requested pubkey, and require the `credential-type` tag to match the
|
|
240
|
+
payload type. `issuedAt` must match the event timestamp. If `expiry` is present,
|
|
241
|
+
reject the credential once that Unix timestamp has passed. A revocation is
|
|
242
|
+
effective only when a valid kind `5` event from the credential's issuer
|
|
243
|
+
references that credential event ID. Cryptographic validity does not establish
|
|
244
|
+
that an issuer is trusted; trust policy is application-specific.
|
package/README.md
CHANGED
|
@@ -4,15 +4,18 @@ A standalone, framework-agnostic library implementing the **Cashlet DID protocol
|
|
|
4
4
|
|
|
5
5
|
Any BCH wallet or Nostr client can use this library to publish, resolve, and verify BCH–Nostr identity bindings without depending on the Cashlet extension.
|
|
6
6
|
|
|
7
|
+
This release is Cashlet DID v2.0.0, a breaking proof-format update. V2 verifiers
|
|
8
|
+
do not accept v1 bindings; existing identities must be rebound and republished.
|
|
9
|
+
|
|
7
10
|
## Real-world use cases
|
|
8
11
|
|
|
9
12
|
**BCH wallet** — call `buildBinding` + `publishToRelays` once when a user sets up their identity. Anyone can then pay them by Nostr handle (`resolveAddress('npub1...')`) instead of exchanging addresses. Users never see the protocol.
|
|
10
13
|
|
|
11
14
|
**Nostr client** — call `verifyIdentity` when rendering a profile to show a cryptographically-proven "verified BCH address" badge. No issuing authority — the proof is in the signature.
|
|
12
15
|
|
|
13
|
-
**Merchant checkout** — call `resolveByNip05('alice@shop.com')` at payment time.
|
|
16
|
+
**Merchant checkout** — call `resolveByNip05('alice@shop.com')` at payment time to resolve the current NIP-05 pubkey and its verified binding. Cross-key rotation continuity is not established by this library, so merchants must update their NIP-05 record and clients must not assume a new key is authorized by the old one.
|
|
14
17
|
|
|
15
|
-
**Passwordless login** —
|
|
18
|
+
**Passwordless login** — use `verifyHttpAuth` in a server with a NIP-07 signer. The server verifies the signed Nostr request and can optionally resolve the signer's BCH binding; it still needs application-specific authorization and replay policy.
|
|
16
19
|
|
|
17
20
|
## Install
|
|
18
21
|
|
|
@@ -28,8 +31,8 @@ npm install did-beta-bch
|
|
|
28
31
|
import { resolveAddress } from 'did-beta-bch'
|
|
29
32
|
|
|
30
33
|
const address = await resolveAddress('npub1...')
|
|
31
|
-
// Returns
|
|
32
|
-
//
|
|
34
|
+
// Returns the BCH address published in the verified binding, or null
|
|
35
|
+
// Token support depends on the bound address and the wallet's payment flow
|
|
33
36
|
if (address) wallet.sendTo(address, amount)
|
|
34
37
|
```
|
|
35
38
|
|
|
@@ -42,26 +45,50 @@ const identity = await resolveIdentity('npub1...')
|
|
|
42
45
|
if (identity) {
|
|
43
46
|
const result = await verifyIdentity(identity)
|
|
44
47
|
console.log(result.status) // "verified"
|
|
45
|
-
console.log(identity.bchAddress) //
|
|
48
|
+
console.log(identity.bchAddress) // bound BCH address
|
|
46
49
|
}
|
|
47
50
|
```
|
|
48
51
|
|
|
49
52
|
### Build and publish a binding (wallet integration)
|
|
50
53
|
|
|
51
54
|
```ts
|
|
52
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
buildBinding, bchSigningPayload, signBchMessage, nostrEventTemplate,
|
|
57
|
+
publishToRelays, DEFAULT_RELAYS,
|
|
58
|
+
} from 'did-beta-bch'
|
|
53
59
|
import { finalizeEvent } from 'nostr-tools'
|
|
54
60
|
|
|
55
61
|
const binding = await buildBinding(bchAddress, 'mainnet', nostrPubkeyHex)
|
|
56
62
|
const message = bchSigningPayload(binding)
|
|
57
63
|
|
|
58
|
-
//
|
|
59
|
-
const
|
|
64
|
+
// Pass the exact text to a transport adapter; do not pre-hash or add a prefix.
|
|
65
|
+
const signatureBase64 = await signBchMessage(walletSigner, message)
|
|
60
66
|
|
|
61
|
-
const event = finalizeEvent(
|
|
67
|
+
const event = finalizeEvent(
|
|
68
|
+
nostrEventTemplate(binding, { message, signatureBase64 }),
|
|
69
|
+
nostrPrivkey,
|
|
70
|
+
)
|
|
62
71
|
await publishToRelays(DEFAULT_RELAYS, event)
|
|
63
72
|
```
|
|
64
73
|
|
|
74
|
+
`BchMessageSigner` is transport-neutral:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const walletSigner = {
|
|
78
|
+
signMessage: (message: string) => walletAdapter.signMessage({ message }),
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`signBchMessage` passes the exact string to `signMessage` and strictly validates
|
|
83
|
+
the standard 65-byte Bitcoin compact-message signature returned as Base64.
|
|
84
|
+
The response must use a compressed-key header byte from 31 through 34. Paytaca's
|
|
85
|
+
published WalletConnect implementation and the BCH WalletConnect BCR match this
|
|
86
|
+
contract; test each wallet version you integrate. The library does not import
|
|
87
|
+
WalletConnect or WizardConnect and does not provide their transport adapters.
|
|
88
|
+
For WalletConnect, the adapter maps this call to `bch_signMessage` with the
|
|
89
|
+
message in its params. WizardConnect needs a separate wallet-side signing action
|
|
90
|
+
and adapter; neither is implemented in this package.
|
|
91
|
+
|
|
65
92
|
### Key derivation (NIP-06)
|
|
66
93
|
|
|
67
94
|
```ts
|
|
@@ -75,9 +102,10 @@ const social = deriveNostrKeypair(mnemonic, 1)
|
|
|
75
102
|
### Challenge-response authentication
|
|
76
103
|
|
|
77
104
|
```ts
|
|
78
|
-
import { verifyChallengeProof } from 'did-beta-bch'
|
|
105
|
+
import { bchChallengePayload, signBchMessage, verifyChallengeProof } from 'did-beta-bch'
|
|
79
106
|
|
|
80
|
-
|
|
107
|
+
const payload = bchChallengePayload(nonce, audience)
|
|
108
|
+
const signature = await signBchMessage(walletSigner, payload)
|
|
81
109
|
// issuedAt enforces 5-minute TTL
|
|
82
110
|
const valid = verifyChallengeProof(
|
|
83
111
|
{ signature, payload, address },
|
|
@@ -95,10 +123,15 @@ import { buildBinding, resolveIdentityChain } from 'did-beta-bch'
|
|
|
95
123
|
// Rotate: reference the old binding
|
|
96
124
|
const newBinding = await buildBinding(newAddress, 'mainnet', newNostrPub, undefined, oldBindingId)
|
|
97
125
|
|
|
98
|
-
// Resolve
|
|
126
|
+
// Resolve linked, individually verified binding events, newest first
|
|
99
127
|
const chain = await resolveIdentityChain('npub1...')
|
|
100
128
|
```
|
|
101
129
|
|
|
130
|
+
The resolver verifies each event and requires every `supersedes` target to be
|
|
131
|
+
present, unrevoked, and valid. A reference signed only by the new key does not
|
|
132
|
+
prove that the previous Nostr key authorized the rotation; do not treat this
|
|
133
|
+
chain alone as proof of cross-key continuity.
|
|
134
|
+
|
|
102
135
|
### Verifiable credentials + reputation
|
|
103
136
|
|
|
104
137
|
```ts
|
|
@@ -112,19 +145,28 @@ const creds = await resolveCredentials('npub1...')
|
|
|
112
145
|
const score = await scoreReputation('npub1...')
|
|
113
146
|
```
|
|
114
147
|
|
|
148
|
+
`resolveCredentials` verifies the issuer's Nostr event signature, the subject
|
|
149
|
+
and credential tags, and any expiry. Applications must still decide which
|
|
150
|
+
issuers and credential types they trust; `scoreReputation` is a heuristic, not
|
|
151
|
+
an objective or Sybil-resistant trust measure.
|
|
152
|
+
|
|
115
153
|
### Selective disclosure
|
|
116
154
|
|
|
117
155
|
```ts
|
|
118
156
|
import { buildDisclosure, verifyDisclosure } from 'did-beta-bch'
|
|
119
157
|
|
|
120
|
-
// Commit to attributes
|
|
158
|
+
// Commit to attributes; store commitments with the identity record separately
|
|
121
159
|
const d = await buildDisclosure({ merchant: 'true', country: 'AU', kyc: 'passed' })
|
|
122
160
|
|
|
123
|
-
// Later: reveal
|
|
161
|
+
// Later: reveal selected values and their salts; hidden salts stay private
|
|
124
162
|
const revealed = d.reveal(['merchant'])
|
|
125
163
|
const { verified, failed } = await verifyDisclosure(revealed, d.commitments)
|
|
126
164
|
```
|
|
127
165
|
|
|
166
|
+
Commitments use random per-attribute salts to prevent offline guessing of
|
|
167
|
+
low-entropy values until those values are revealed. This helper does not publish
|
|
168
|
+
commitments or attach them to a binding event automatically.
|
|
169
|
+
|
|
128
170
|
### BCH-anchored identity commitment
|
|
129
171
|
|
|
130
172
|
```ts
|
|
@@ -134,6 +176,11 @@ import { buildAnchorTx } from 'did-beta-bch'
|
|
|
134
176
|
const { txHex } = await buildAnchorTx(bindingId, { txid, vout, valueSats, lockingBytecode, privateKey })
|
|
135
177
|
```
|
|
136
178
|
|
|
179
|
+
This builds transaction hex from the supplied UTXO; it
|
|
180
|
+
does not broadcast the transaction or verify its confirmation. A `bch-anchor`
|
|
181
|
+
tag is only a claim until an application independently verifies the transaction
|
|
182
|
+
and its block inclusion.
|
|
183
|
+
|
|
137
184
|
### NIP-05 handle resolution
|
|
138
185
|
|
|
139
186
|
```ts
|
|
@@ -148,16 +195,15 @@ if (address) wallet.sendTo(address, amount)
|
|
|
148
195
|
|
|
149
196
|
```ts
|
|
150
197
|
import { buildHttpAuthEventAsync, verifyHttpAuth } from 'did-beta-bch'
|
|
151
|
-
import { finalizeEvent } from 'nostr-tools'
|
|
152
198
|
|
|
153
199
|
// Client: build + sign auth event
|
|
154
200
|
const template = await buildHttpAuthEventAsync('https://api.example.com/me', 'GET')
|
|
155
|
-
const event =
|
|
201
|
+
const event = await window.nostr.signEvent(template) // NIP-07 browser extension
|
|
156
202
|
const header = 'Nostr ' + btoa(JSON.stringify(event))
|
|
157
203
|
// → send as Authorization header
|
|
158
204
|
|
|
159
205
|
// Server: verify — proves Nostr identity + optionally BCH address ownership
|
|
160
|
-
const result = await verifyHttpAuth(header, 'https://api.example.com/me', 'GET', { verifyBch: true })
|
|
206
|
+
const result = await verifyHttpAuth(header, 'https://api.example.com/me', 'GET', { body: null, verifyBch: true })
|
|
161
207
|
if (result.valid) {
|
|
162
208
|
console.log(result.pubkey) // verified Nostr pubkey
|
|
163
209
|
console.log(result.bchAddress) // verified BCH address (if verifyBch: true)
|
|
@@ -186,13 +232,23 @@ Bindings are published as Nostr events of kind `30078` (NIP-78 addressable data)
|
|
|
186
232
|
|---|---|
|
|
187
233
|
| `d` | binding ID (SHA-256 of canonical binding JSON) |
|
|
188
234
|
| `bch` | BCH address, network |
|
|
189
|
-
| `bch-proof` |
|
|
190
|
-
| `bch-payment-code` | BIP47 payment
|
|
235
|
+
| `bch-proof` | `["bch-proof", "bitcoin-message/compact-base64", message, signatureBase64]` |
|
|
236
|
+
| `bch-payment-code` | Optional BIP47 payment-code metadata; this library does not derive fresh addresses |
|
|
191
237
|
| `protocol` | `cashlet-did`, version |
|
|
192
238
|
| `supersedes` | previous binding ID (key rotation) |
|
|
193
239
|
| `t` | `cashlet-did` |
|
|
194
240
|
|
|
195
|
-
The
|
|
241
|
+
The wallet receives the exact UTF-8 v2 message text and applies the Bitcoin
|
|
242
|
+
Signed Message prefix, CompactSize byte length, and double-SHA256. Its response
|
|
243
|
+
is standard padded Base64 for a 65-byte compact signature: header 31–34
|
|
244
|
+
(compressed key plus recovery ID 0–3), followed by 64-byte `r || s`. V2
|
|
245
|
+
verifiers strictly reject v1 hex/raw signature encodings. Revocation uses
|
|
246
|
+
NIP-09 kind `5` deletion events.
|
|
247
|
+
|
|
248
|
+
The local demo previews and verifies a binding but does **not** publish or query
|
|
249
|
+
Nostr relays. Its separate HTTP verifier demo demonstrates challenge issue,
|
|
250
|
+
verification, and replay rejection locally; `/resolve` needs a published
|
|
251
|
+
binding and relay access.
|
|
196
252
|
|
|
197
253
|
## Relays
|
|
198
254
|
|
package/dist/anchor.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BCH on-chain identity commitment via OP_RETURN.
|
|
3
|
-
*
|
|
3
|
+
* Builds a transaction committing to a bindingId; it does not broadcast or verify confirmation.
|
|
4
4
|
*/
|
|
5
5
|
export declare const ANCHOR_PREFIX = "cashlet-did:";
|
|
6
6
|
export interface AnchorUtxo {
|
package/dist/anchor.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BCH on-chain identity commitment via OP_RETURN.
|
|
3
|
-
*
|
|
3
|
+
* Builds a transaction committing to a bindingId; it does not broadcast or verify confirmation.
|
|
4
4
|
*/
|
|
5
5
|
export const ANCHOR_PREFIX = "cashlet-did:";
|
|
6
6
|
const OP_RETURN = 0x6a;
|
|
7
7
|
const MIN_FEE_SATS = 500;
|
|
8
|
+
function pushData(data) {
|
|
9
|
+
if (data.length <= 0x4b)
|
|
10
|
+
return new Uint8Array([data.length, ...data]);
|
|
11
|
+
if (data.length <= 0xff)
|
|
12
|
+
return new Uint8Array([0x4c, data.length, ...data]);
|
|
13
|
+
if (data.length <= 0xffff)
|
|
14
|
+
return new Uint8Array([0x4d, data.length & 0xff, data.length >>> 8, ...data]);
|
|
15
|
+
throw new Error("Anchor data is too large for a script push");
|
|
16
|
+
}
|
|
8
17
|
/** Build an OP_RETURN tx anchoring a bindingId to the BCH chain */
|
|
9
18
|
export async function buildAnchorTx(bindingId, utxo) {
|
|
19
|
+
if (!/^[0-9a-f]{64}$/.test(bindingId))
|
|
20
|
+
throw new Error("bindingId must be 64 lowercase hex characters");
|
|
10
21
|
const feeSats = utxo.feeSats ?? MIN_FEE_SATS;
|
|
11
22
|
if (feeSats < MIN_FEE_SATS)
|
|
12
23
|
throw new Error(`feeSats ${feeSats} below minimum ${MIN_FEE_SATS}`);
|
|
@@ -16,7 +27,7 @@ export async function buildAnchorTx(bindingId, utxo) {
|
|
|
16
27
|
const data = ANCHOR_PREFIX + bindingId;
|
|
17
28
|
const dataBytes = new TextEncoder().encode(data);
|
|
18
29
|
// OP_RETURN <push> <data>
|
|
19
|
-
const opReturnScript = new Uint8Array([OP_RETURN,
|
|
30
|
+
const opReturnScript = new Uint8Array([OP_RETURN, ...pushData(dataBytes)]);
|
|
20
31
|
const template = importWalletTemplate(walletTemplateP2pkhNonHd);
|
|
21
32
|
if (typeof template === "string")
|
|
22
33
|
throw new Error(template);
|