did-beta-bch 2.0.1 → 3.1.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/PROTOCOL.md +116 -154
- package/README.md +83 -30
- package/dist/anchor-client.d.ts +30 -0
- package/dist/anchor-client.js +107 -0
- package/dist/anchor-verification.d.ts +46 -0
- package/dist/anchor-verification.js +282 -0
- package/dist/binding.d.ts +12 -3
- package/dist/binding.js +51 -3
- package/dist/bip47.d.ts +50 -0
- package/dist/bip47.js +171 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -3
- package/dist/verify.d.ts +36 -1
- package/dist/verify.js +138 -7
- package/package.json +3 -3
package/dist/bip47.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BIP47 v1 reusable payment code helpers.
|
|
3
|
+
* Notification transactions and scanning are wallet-specific and out of scope.
|
|
4
|
+
*/
|
|
5
|
+
import { secp256k1, hash160, encodeCashAddress, deriveHdPublicNodeChild, deriveHdPrivateNodeChild, deriveHdPrivateNodeFromBip39Mnemonic, deriveHdPath, deriveHdPublicNode, } from "@bitauth/libauth";
|
|
6
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
7
|
+
// BIP47 payment code: [0x47][0x01][0x00][pubkey 33][chaincode 32][reserved 13][checksum 4] = 85 bytes
|
|
8
|
+
const PC_VERSION = 0x47;
|
|
9
|
+
const PC_TOTAL_BYTES = 85;
|
|
10
|
+
const PC_PAYLOAD_BYTES = 80;
|
|
11
|
+
function base58Encode(bytes) {
|
|
12
|
+
let n = 0n;
|
|
13
|
+
for (const b of bytes)
|
|
14
|
+
n = n * 256n + BigInt(b);
|
|
15
|
+
let result = "";
|
|
16
|
+
while (n > 0n) {
|
|
17
|
+
result = BASE58_ALPHABET[Number(n % 58n)] + result;
|
|
18
|
+
n /= 58n;
|
|
19
|
+
}
|
|
20
|
+
for (const b of bytes) {
|
|
21
|
+
if (b !== 0)
|
|
22
|
+
break;
|
|
23
|
+
result = "1" + result;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
function base58Decode(s) {
|
|
28
|
+
let n = 0n;
|
|
29
|
+
for (const c of s) {
|
|
30
|
+
const d = BASE58_ALPHABET.indexOf(c);
|
|
31
|
+
if (d < 0)
|
|
32
|
+
throw new Error(`Invalid Base58 character: ${c}`);
|
|
33
|
+
n = n * 58n + BigInt(d);
|
|
34
|
+
}
|
|
35
|
+
const hex = n.toString(16).padStart(PC_TOTAL_BYTES * 2, "0");
|
|
36
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
37
|
+
for (let i = 0; i < bytes.length; i++)
|
|
38
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
39
|
+
let leading = 0;
|
|
40
|
+
for (const c of s) {
|
|
41
|
+
if (c === "1")
|
|
42
|
+
leading++;
|
|
43
|
+
else
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
const result = new Uint8Array(leading + bytes.length);
|
|
47
|
+
result.set(bytes, leading);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
async function sha256Once(data) {
|
|
51
|
+
if (typeof crypto !== "undefined" && crypto.subtle)
|
|
52
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
const nc = await Function('return import("crypto")')();
|
|
55
|
+
return nc.createHash("sha256").update(data).digest();
|
|
56
|
+
}
|
|
57
|
+
async function paymentCodeChecksum(versionAndPayload) {
|
|
58
|
+
return (await sha256Once(await sha256Once(versionAndPayload))).slice(0, 4);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Encode a BIP47 v1 payment code from a public key and chain code.
|
|
62
|
+
*/
|
|
63
|
+
export async function encodePaymentCode(publicKey, chainCode) {
|
|
64
|
+
if (publicKey.length !== 33)
|
|
65
|
+
throw new Error("publicKey must be 33 bytes");
|
|
66
|
+
if (chainCode.length !== 32)
|
|
67
|
+
throw new Error("chainCode must be 32 bytes");
|
|
68
|
+
const full = new Uint8Array(PC_TOTAL_BYTES);
|
|
69
|
+
full[0] = PC_VERSION;
|
|
70
|
+
full[1] = 0x01; // payload version
|
|
71
|
+
full[2] = 0x00; // feature bits
|
|
72
|
+
full.set(publicKey, 3);
|
|
73
|
+
full.set(chainCode, 36);
|
|
74
|
+
// bytes 69-80 are reserved zeros
|
|
75
|
+
full.set(await paymentCodeChecksum(full.slice(0, PC_PAYLOAD_BYTES + 1)), PC_PAYLOAD_BYTES + 1);
|
|
76
|
+
return base58Encode(full);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Decode a BIP47 v1 Base58Check payment code string.
|
|
80
|
+
*/
|
|
81
|
+
export function parsePaymentCode(paymentCode) {
|
|
82
|
+
const decoded = base58Decode(paymentCode);
|
|
83
|
+
if (decoded.length !== PC_TOTAL_BYTES)
|
|
84
|
+
throw new Error(`Invalid payment code length: ${decoded.length}`);
|
|
85
|
+
if (decoded[0] !== PC_VERSION)
|
|
86
|
+
throw new Error(`Unsupported payment code version: 0x${decoded[0].toString(16)}`);
|
|
87
|
+
const payload = decoded.slice(1, 1 + PC_PAYLOAD_BYTES);
|
|
88
|
+
if (payload[0] !== 0x01)
|
|
89
|
+
throw new Error(`Unsupported payment code payload version: ${payload[0]}`);
|
|
90
|
+
return {
|
|
91
|
+
publicKey: payload.slice(2, 35),
|
|
92
|
+
chainCode: payload.slice(35, 67),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Derive a BIP47 payment code from a mnemonic (m/47'/0'/<account>').
|
|
97
|
+
*/
|
|
98
|
+
export async function derivePaymentCode(mnemonic, account = 0) {
|
|
99
|
+
const root = await deriveHdPrivateNodeFromBip39Mnemonic(mnemonic);
|
|
100
|
+
const node = deriveHdPath(root, `m/47'/0'/${account}'`);
|
|
101
|
+
if (typeof node === "string")
|
|
102
|
+
throw new Error(`BIP47 derivation failed: ${node}`);
|
|
103
|
+
const publicKey = secp256k1.derivePublicKeyCompressed(node.privateKey);
|
|
104
|
+
if (typeof publicKey === "string")
|
|
105
|
+
throw new Error(`Public key derivation failed: ${publicKey}`);
|
|
106
|
+
return { publicKey, chainCode: node.chainCode, encoded: await encodePaymentCode(publicKey, node.chainCode) };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Derive the nth P2PKH CashAddr payment address for sending to a recipient.
|
|
110
|
+
*
|
|
111
|
+
* @param senderScanPrivkey - sender's M/47'/0'/0'/0 private key (32 bytes)
|
|
112
|
+
* @param recipientNode - recipient's BIP47 HD public node (M/47'/0'/0')
|
|
113
|
+
* @param index - address index (0-based)
|
|
114
|
+
* @param prefix - CashAddr prefix, defaults to "bitcoincash"
|
|
115
|
+
*/
|
|
116
|
+
export async function derivePaymentAddress(senderScanPrivkey, recipientNode, index, prefix = "bitcoincash") {
|
|
117
|
+
if (index < 0 || !Number.isSafeInteger(index))
|
|
118
|
+
throw new Error("Index must be a non-negative integer");
|
|
119
|
+
const childNode = deriveHdPublicNodeChild(recipientNode, index);
|
|
120
|
+
if (typeof childNode === "string")
|
|
121
|
+
throw new Error(`HD derivation failed: ${childNode}`);
|
|
122
|
+
const sharedPoint = secp256k1.mulTweakPublicKeyCompressed(childNode.publicKey, senderScanPrivkey);
|
|
123
|
+
if (typeof sharedPoint === "string")
|
|
124
|
+
throw new Error(`ECDH failed: ${sharedPoint}`);
|
|
125
|
+
const s = await sha256Once(sharedPoint.slice(1));
|
|
126
|
+
const paymentPubkey = secp256k1.addTweakPublicKeyCompressed(childNode.publicKey, s);
|
|
127
|
+
if (typeof paymentPubkey === "string")
|
|
128
|
+
throw new Error(`Pubkey tweak failed: ${paymentPubkey}`);
|
|
129
|
+
const result = encodeCashAddress({ prefix, type: "p2pkh", payload: hash160(paymentPubkey) });
|
|
130
|
+
return typeof result === "string" ? result : result.address;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Derive the nth payment private key for a receiver to spend funds sent via BIP47.
|
|
134
|
+
*
|
|
135
|
+
* Per BIP47 spec: s = SHA256(b[i] * A) where A = sender's scan pubkey (M/47'/0'/0'/0)
|
|
136
|
+
* b' = b[i] + s
|
|
137
|
+
*
|
|
138
|
+
* @param receiverAccountNode - receiver's M/47'/0'/0' private node
|
|
139
|
+
* @param senderScanPubkey - sender's M/47'/0'/0'/0 compressed public key (33 bytes)
|
|
140
|
+
* @param index - address index (0-based)
|
|
141
|
+
*/
|
|
142
|
+
export async function derivePaymentPrivkey(receiverAccountNode, senderScanPubkey, index) {
|
|
143
|
+
if (index < 0 || !Number.isSafeInteger(index))
|
|
144
|
+
throw new Error("Index must be a non-negative integer");
|
|
145
|
+
// Receiver's child private key b[i]
|
|
146
|
+
const receiverChild = deriveHdPrivateNodeChild(receiverAccountNode, index);
|
|
147
|
+
if (typeof receiverChild === "string")
|
|
148
|
+
throw new Error(`HD child derivation failed: ${receiverChild}`);
|
|
149
|
+
// s = sha256(x-coord of b[i] * A)
|
|
150
|
+
const sharedPoint = secp256k1.mulTweakPublicKeyCompressed(senderScanPubkey, receiverChild.privateKey);
|
|
151
|
+
if (typeof sharedPoint === "string")
|
|
152
|
+
throw new Error(`ECDH failed: ${sharedPoint}`);
|
|
153
|
+
const s = await sha256Once(sharedPoint.slice(1));
|
|
154
|
+
// b' = b[i] + s (mod n)
|
|
155
|
+
const tweaked = secp256k1.addTweakPrivateKey(receiverChild.privateKey, s);
|
|
156
|
+
if (typeof tweaked === "string")
|
|
157
|
+
throw new Error(`Private key tweak failed: ${tweaked}`);
|
|
158
|
+
return tweaked;
|
|
159
|
+
}
|
|
160
|
+
export { deriveHdPublicNode, deriveHdPath, deriveHdPrivateNodeFromBip39Mnemonic };
|
|
161
|
+
/** Construct a minimal HdPublicNodeValid from a BIP47 PaymentCode for use with derivePaymentAddress/derivePaymentPrivkey. */
|
|
162
|
+
export function hdPublicNodeFromPaymentCode(pc) {
|
|
163
|
+
return {
|
|
164
|
+
publicKey: pc.publicKey,
|
|
165
|
+
chainCode: pc.chainCode,
|
|
166
|
+
depth: 0,
|
|
167
|
+
childIndex: 0,
|
|
168
|
+
parentFingerprint: new Uint8Array(4),
|
|
169
|
+
parentIdentifier: new Uint8Array(20),
|
|
170
|
+
};
|
|
171
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { buildDisclosure, verifyDisclosure, type Disclosure, type DisclosureReveal, } from "./disclosure.js";
|
|
2
|
+
export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, type PaymentCode, type EncodedPaymentCode, } from "./bip47.js";
|
|
2
3
|
export { buildAnchorTx, ANCHOR_PREFIX, type AnchorUtxo, type AnchorResult, } from "./anchor.js";
|
|
4
|
+
export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS, BCH_CHIPNET_CHECKPOINT, type BchSpvCheckpoint, type BchAsertParameters, type BchAnchorProof, type BchAnchorVerificationOptions, type BchAnchorVerificationResult, } from "./anchor-verification.js";
|
|
5
|
+
export { broadcastAnchorTx, fetchAnchorProof, wsTransport, ELECTRUM_MAINNET_TCP, ELECTRUM_MAINNET_WS, ELECTRUM_CHIPNET_TCP, ELECTRUM_CHIPNET_WS, type ElectrumTransport, } from "./anchor-client.js";
|
|
3
6
|
export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, type CredentialPayload, type IssuedCredential, } from "./credentials.js";
|
|
4
|
-
export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, type Network, type CanonicalBinding, type IdentityBinding, type NostrEvent, buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
|
|
7
|
+
export { PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, ROTATION_AUTH_KIND, type Network, type CanonicalBinding, type IdentityBinding, type NostrEvent, buildBinding, bchSigningPayload, nostrEventTemplate, rotationAuthorizationTemplate, verifyRotationAuthorization, canonicalize, sha256hex, } from "./binding.js";
|
|
5
8
|
export { BCH_PROOF_FORMAT, normalizeBchMessageSignature, signBchMessage, type BchMessageProof, type BchMessageSigner, type BchProofTag, } from "./signing.js";
|
|
6
9
|
export { publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, type RelayResult, } from "./relay.js";
|
|
7
|
-
export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, type BCHAnchorClaim, } from "./verify.js";
|
|
10
|
+
export { resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof, NOSTR_CHALLENGE_KIND, type DualChallengeProof, verifyChallengeProof, resolveIdentityChain, type VerifyResult, type VerifyStatus, type ResolvedIdentity, type BCHAnchorClaim, bitcoinSignedMessageHash, encodeBitcoinCompactSize, encodeCompactSignatureBase64, } from "./verify.js";
|
|
8
11
|
export { CashletDidError, type CashletDidErrorCode } from "./errors.js";
|
|
9
12
|
export { deriveNostrKeypair, type NostrKeypair, } from "./keygen.js";
|
|
10
13
|
export { buildHttpAuthEventAsync, verifyHttpAuth, type HttpAuthEventTemplate, type HttpAuthResult, } from "./nip98.js";
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
export { buildDisclosure, verifyDisclosure, } from "./disclosure.js";
|
|
2
|
+
export { parsePaymentCode, encodePaymentCode, derivePaymentCode, derivePaymentAddress, derivePaymentPrivkey, hdPublicNodeFromPaymentCode, } from "./bip47.js";
|
|
2
3
|
export { buildAnchorTx, ANCHOR_PREFIX, } from "./anchor.js";
|
|
4
|
+
export { verifyBchAnchorProof, BCH_MAINNET_ASERT_PARAMETERS, BCH_MAINNET_CHECKPOINT, BCH_CHIPNET_ASERT_PARAMETERS, BCH_CHIPNET_CHECKPOINT, } from "./anchor-verification.js";
|
|
5
|
+
export { broadcastAnchorTx, fetchAnchorProof, wsTransport, ELECTRUM_MAINNET_TCP, ELECTRUM_MAINNET_WS, ELECTRUM_CHIPNET_TCP, ELECTRUM_CHIPNET_WS, } from "./anchor-client.js";
|
|
3
6
|
export { issueCredential, issueRating, revokeCredential, resolveCredentials, scoreReputation, CREDENTIAL_KIND, } from "./credentials.js";
|
|
4
7
|
export {
|
|
5
8
|
// Protocol constants
|
|
6
|
-
PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND,
|
|
9
|
+
PROTOCOL_ID, PROTOCOL_VERSION, BINDING_KIND, ROTATION_AUTH_KIND,
|
|
7
10
|
// Binding construction
|
|
8
|
-
buildBinding, bchSigningPayload, nostrEventTemplate, canonicalize, sha256hex, } from "./binding.js";
|
|
11
|
+
buildBinding, bchSigningPayload, nostrEventTemplate, rotationAuthorizationTemplate, verifyRotationAuthorization, canonicalize, sha256hex, } from "./binding.js";
|
|
9
12
|
export { BCH_PROOF_FORMAT, normalizeBchMessageSignature, signBchMessage, } from "./signing.js";
|
|
10
13
|
export {
|
|
11
14
|
// Relay communication
|
|
12
15
|
publishToRelay, publishToRelays, queryRelay, queryRelays, DEFAULT_RELAYS, } from "./relay.js";
|
|
13
16
|
export {
|
|
14
17
|
// Verification
|
|
15
|
-
resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, verifyChallengeProof, resolveIdentityChain, } from "./verify.js";
|
|
18
|
+
resolveIdentity, resolveAddress, resolveByNip05, verifyIdentity, bchChallengePayload, nostrChallengeTemplate, verifyDualChallengeProof, NOSTR_CHALLENGE_KIND, verifyChallengeProof, resolveIdentityChain, bitcoinSignedMessageHash, encodeBitcoinCompactSize, encodeCompactSignatureBase64, } from "./verify.js";
|
|
16
19
|
export { CashletDidError } from "./errors.js";
|
|
17
20
|
export { deriveNostrKeypair, } from "./keygen.js";
|
|
18
21
|
export { buildHttpAuthEventAsync, verifyHttpAuth, } from "./nip98.js";
|
package/dist/verify.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalize, sha256hex, bchSigningPayload, type CanonicalBinding, type IdentityBinding, type NostrEvent } from "./binding.js";
|
|
2
2
|
import { type BchProofTag } from "./signing.js";
|
|
3
|
-
export type VerifyStatus = "verified" | "invalid_bch_sig" | "invalid_nostr_sig" | "id_mismatch" | "malformed" | "expired";
|
|
3
|
+
export type VerifyStatus = "verified" | "invalid_bch_sig" | "invalid_nostr_sig" | "id_mismatch" | "invalid_rotation" | "malformed" | "expired";
|
|
4
4
|
export interface VerifyResult {
|
|
5
5
|
status: VerifyStatus;
|
|
6
6
|
bchValid: boolean;
|
|
@@ -11,6 +11,13 @@ export interface BCHAnchorClaim {
|
|
|
11
11
|
txid: string;
|
|
12
12
|
blockHeight?: number;
|
|
13
13
|
}
|
|
14
|
+
export declare const NOSTR_CHALLENGE_KIND = 27235;
|
|
15
|
+
export interface DualChallengeProof {
|
|
16
|
+
signature: string;
|
|
17
|
+
payload: string;
|
|
18
|
+
address: string;
|
|
19
|
+
nostrEvent: NostrEvent;
|
|
20
|
+
}
|
|
14
21
|
export interface ResolvedIdentity {
|
|
15
22
|
npub: string;
|
|
16
23
|
pubkey: string;
|
|
@@ -20,15 +27,43 @@ export interface ResolvedIdentity {
|
|
|
20
27
|
createdAt: number;
|
|
21
28
|
bchProofTag: BchProofTag;
|
|
22
29
|
rawEvent: NostrEvent;
|
|
30
|
+
rotationAuthorization?: NostrEvent;
|
|
23
31
|
/** Unverified metadata claimed by the event; this library does not verify chain inclusion. */
|
|
24
32
|
anchorClaim?: BCHAnchorClaim;
|
|
25
33
|
}
|
|
34
|
+
/** Encode n as a Bitcoin CompactSize integer. */
|
|
35
|
+
export declare function encodeBitcoinCompactSize(n: number): Uint8Array;
|
|
36
|
+
/**
|
|
37
|
+
* Compute the double-SHA256 hash used by Bitcoin signed messages.
|
|
38
|
+
* Format: SHA256d("\x18Bitcoin Signed Message:\n" + varint(len) + message)
|
|
39
|
+
* Uses proper varint encoding — required for messages longer than 252 bytes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function bitcoinSignedMessageHash(message: string): Uint8Array;
|
|
42
|
+
/**
|
|
43
|
+
* Encode a 65-byte compact signature (recovery ID + 64-byte r||s) as Base64.
|
|
44
|
+
* Header byte = 31 + recoveryId (compressed key, recovery ID 0–3).
|
|
45
|
+
*/
|
|
46
|
+
export declare function encodeCompactSignatureBase64(recoveryId: number, signature: Uint8Array): string;
|
|
26
47
|
/** Resolve a BCH address from an npub via Nostr relays */
|
|
27
48
|
export declare function resolveIdentity(npub: string, relays?: string[]): Promise<ResolvedIdentity | null>;
|
|
28
49
|
/** Verify the BCH + Nostr signatures on a resolved identity */
|
|
29
50
|
export declare function verifyIdentity(resolved: ResolvedIdentity): Promise<VerifyResult>;
|
|
30
51
|
/** Build the exact versioned challenge message that a BCH wallet must sign. */
|
|
31
52
|
export declare function bchChallengePayload(nonce: string, audience?: string): string;
|
|
53
|
+
export declare function nostrChallengeTemplate(nonce: string, audience: string, endpoint: string, issuedAt: number): {
|
|
54
|
+
kind: number;
|
|
55
|
+
created_at: number;
|
|
56
|
+
tags: string[][];
|
|
57
|
+
content: string;
|
|
58
|
+
};
|
|
59
|
+
export declare function verifyDualChallengeProof(proof: DualChallengeProof, expected: {
|
|
60
|
+
nonce: string;
|
|
61
|
+
audience: string;
|
|
62
|
+
endpoint: string;
|
|
63
|
+
issuedAt: number;
|
|
64
|
+
address: string;
|
|
65
|
+
pubkey: string;
|
|
66
|
+
}): boolean;
|
|
32
67
|
/** Verify a challenge-response proof.
|
|
33
68
|
* The wallet signs the exact text produced by `bchChallengePayload`.
|
|
34
69
|
* @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
|
package/dist/verify.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { verifyEvent, nip05 } from "nostr-tools";
|
|
2
2
|
import { secp256k1, hash256, encodeCashAddress, hash160 } from "@bitauth/libauth";
|
|
3
|
-
import { canonicalize, sha256hex, bchSigningPayload, BINDING_KIND, PROTOCOL_ID, PROTOCOL_VERSION, } from "./binding.js";
|
|
3
|
+
import { canonicalize, sha256hex, bchSigningPayload, BINDING_KIND, PROTOCOL_ID, PROTOCOL_VERSION, verifyRotationAuthorization, } from "./binding.js";
|
|
4
4
|
import { queryRelays, DEFAULT_RELAYS } from "./relay.js";
|
|
5
5
|
import { CashletDidError } from "./errors.js";
|
|
6
6
|
import { nip19 } from "nostr-tools";
|
|
7
7
|
import { BCH_PROOF_FORMAT, normalizeBchMessageSignature, } from "./signing.js";
|
|
8
|
+
export const NOSTR_CHALLENGE_KIND = 27235;
|
|
8
9
|
/** Encode n as a Bitcoin CompactSize integer. */
|
|
9
|
-
function
|
|
10
|
+
export function encodeBitcoinCompactSize(n) {
|
|
10
11
|
if (n < 0xfd)
|
|
11
12
|
return new Uint8Array([n]);
|
|
12
13
|
const value = BigInt(n);
|
|
@@ -30,12 +31,28 @@ function varint(n) {
|
|
|
30
31
|
* Format: SHA256d("\x18Bitcoin Signed Message:\n" + varint(len) + message)
|
|
31
32
|
* Uses proper varint encoding — required for messages longer than 252 bytes.
|
|
32
33
|
*/
|
|
33
|
-
function
|
|
34
|
+
export function bitcoinSignedMessageHash(message) {
|
|
34
35
|
const enc = new TextEncoder();
|
|
35
36
|
const msgBytes = enc.encode(message);
|
|
36
37
|
const prefix = enc.encode("\x18Bitcoin Signed Message:\n");
|
|
37
|
-
return hash256(new Uint8Array([...prefix, ...
|
|
38
|
+
return hash256(new Uint8Array([...prefix, ...encodeBitcoinCompactSize(msgBytes.length), ...msgBytes]));
|
|
38
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Encode a 65-byte compact signature (recovery ID + 64-byte r||s) as Base64.
|
|
42
|
+
* Header byte = 31 + recoveryId (compressed key, recovery ID 0–3).
|
|
43
|
+
*/
|
|
44
|
+
export function encodeCompactSignatureBase64(recoveryId, signature) {
|
|
45
|
+
if (!Number.isInteger(recoveryId) || recoveryId < 0 || recoveryId > 3)
|
|
46
|
+
throw new RangeError("Recovery ID must be 0–3");
|
|
47
|
+
if (signature.length !== 64)
|
|
48
|
+
throw new TypeError("Signature must be 64 bytes");
|
|
49
|
+
const compact = new Uint8Array(65);
|
|
50
|
+
compact[0] = 31 + recoveryId;
|
|
51
|
+
compact.set(signature, 1);
|
|
52
|
+
return btoa(String.fromCharCode(...compact));
|
|
53
|
+
}
|
|
54
|
+
// Internal alias used within this file
|
|
55
|
+
function bitcoinMsgHash(message) { return bitcoinSignedMessageHash(message); }
|
|
39
56
|
function recoverBchAddress(signatureBase64, msgHash, expectedAddress) {
|
|
40
57
|
try {
|
|
41
58
|
normalizeBchMessageSignature(signatureBase64);
|
|
@@ -164,10 +181,12 @@ export async function verifyIdentity(resolved) {
|
|
|
164
181
|
const proofTag = exactlyOne("bch-proof");
|
|
165
182
|
const paymentCodeTag = optionalTag("bch-payment-code");
|
|
166
183
|
const supersedesTag = optionalTag("supersedes");
|
|
184
|
+
const rotationTag = optionalTag("rotation-proof");
|
|
167
185
|
const anchorTag = optionalTag("bch-anchor");
|
|
186
|
+
const rotationAuthorization = content.rotationAuthorization;
|
|
168
187
|
const previousBindingId = content.previousBindingId;
|
|
169
188
|
const contentKeys = Object.keys(content).sort().join(",");
|
|
170
|
-
const allowedContentKeys = ["bch", "bindingId", "createdAt", "nonce", "previousBindingId", "protocol", "version"]
|
|
189
|
+
const allowedContentKeys = ["bch", "bindingId", "createdAt", "nonce", "previousBindingId", "protocol", "rotationAuthorization", "version"]
|
|
171
190
|
.filter(key => content[key] !== undefined).sort().join(",");
|
|
172
191
|
const bchKeys = bch && typeof bch === "object" && !Array.isArray(bch)
|
|
173
192
|
? Object.keys(bch).sort().join(",")
|
|
@@ -197,7 +216,16 @@ export async function verifyIdentity(resolved) {
|
|
|
197
216
|
(bch.paymentCode === undefined ? paymentCodeTag !== undefined : (!paymentCodeTag || paymentCodeTag.length !== 2 || paymentCodeTag[1] !== bch.paymentCode)) ||
|
|
198
217
|
getTags("bch-payment-code").length > 1 ||
|
|
199
218
|
(previousBindingId === undefined ? supersedesTag !== undefined : (!supersedesTag || supersedesTag.length !== 2 || supersedesTag[1] !== previousBindingId)) ||
|
|
219
|
+
(rotationAuthorization === undefined ? rotationTag !== undefined : (!rotationAuthorization ||
|
|
220
|
+
typeof rotationAuthorization !== "object" ||
|
|
221
|
+
!rotationTag ||
|
|
222
|
+
rotationTag.length !== 3 ||
|
|
223
|
+
rotationTag[1] !== rotationAuthorization.id ||
|
|
224
|
+
rotationTag[2] !== rotationAuthorization.pubkey ||
|
|
225
|
+
!verifyEvent(rotationAuthorization))) ||
|
|
226
|
+
(rotationAuthorization !== undefined && previousBindingId === undefined) ||
|
|
200
227
|
getTags("supersedes").length > 1 ||
|
|
228
|
+
getTags("rotation-proof").length > 1 ||
|
|
201
229
|
getTags("bch-anchor").length > 1 ||
|
|
202
230
|
(anchorTag !== undefined && ((anchorTag.length !== 2 && anchorTag.length !== 3) ||
|
|
203
231
|
!/^[0-9a-f]{64}$/.test(anchorTag[1]) ||
|
|
@@ -232,7 +260,7 @@ export async function verifyIdentity(resolved) {
|
|
|
232
260
|
nostr: { pubkey: rawEvent.pubkey },
|
|
233
261
|
createdAt: content.createdAt,
|
|
234
262
|
nonce: content.nonce,
|
|
235
|
-
purpose: "identity-binding-
|
|
263
|
+
purpose: "identity-binding-v3",
|
|
236
264
|
...(previousBindingId !== undefined ? { previousBindingId } : {}),
|
|
237
265
|
};
|
|
238
266
|
const expectedBindingId = await sha256hex(canonicalize(partial));
|
|
@@ -263,6 +291,61 @@ export function bchChallengePayload(nonce, audience) {
|
|
|
263
291
|
throw new TypeError("Challenge audience must be a non-empty single-line string");
|
|
264
292
|
return `${PROTOCOL_ID}/challenge/v${PROTOCOL_VERSION}\n${nonce}${audience === undefined ? "" : `\n${audience}`}`;
|
|
265
293
|
}
|
|
294
|
+
export function nostrChallengeTemplate(nonce, audience, endpoint, issuedAt) {
|
|
295
|
+
const payload = bchChallengePayload(nonce, audience);
|
|
296
|
+
if (!Number.isSafeInteger(issuedAt) || issuedAt <= 0)
|
|
297
|
+
throw new TypeError("Challenge issue time must be a positive Unix timestamp");
|
|
298
|
+
if (typeof endpoint !== "string" || endpoint.length === 0 || /[\r\n]/.test(endpoint))
|
|
299
|
+
throw new TypeError("Challenge endpoint must be a non-empty single-line URL");
|
|
300
|
+
return {
|
|
301
|
+
kind: NOSTR_CHALLENGE_KIND,
|
|
302
|
+
created_at: issuedAt,
|
|
303
|
+
tags: [
|
|
304
|
+
["u", endpoint],
|
|
305
|
+
["method", "POST"],
|
|
306
|
+
["audience", audience],
|
|
307
|
+
["challenge", nonce],
|
|
308
|
+
["expiration", String(issuedAt + CHALLENGE_TTL_SECS)],
|
|
309
|
+
],
|
|
310
|
+
content: payload,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
export function verifyDualChallengeProof(proof, expected) {
|
|
314
|
+
if (!proof ||
|
|
315
|
+
proof.address !== expected.address ||
|
|
316
|
+
!verifyChallengeProof(proof, expected.nonce, expected.audience, expected.issuedAt))
|
|
317
|
+
return false;
|
|
318
|
+
const event = proof.nostrEvent;
|
|
319
|
+
if (!event ||
|
|
320
|
+
event.kind !== NOSTR_CHALLENGE_KIND ||
|
|
321
|
+
event.pubkey !== expected.pubkey ||
|
|
322
|
+
event.created_at !== expected.issuedAt ||
|
|
323
|
+
event.content !== bchChallengePayload(expected.nonce, expected.audience) ||
|
|
324
|
+
!Array.isArray(event.tags))
|
|
325
|
+
return false;
|
|
326
|
+
const uniqueTag = (name) => {
|
|
327
|
+
const matches = event.tags.filter(tag => Array.isArray(tag) && tag[0] === name);
|
|
328
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
329
|
+
};
|
|
330
|
+
const endpointTag = uniqueTag("u");
|
|
331
|
+
const methodTag = uniqueTag("method");
|
|
332
|
+
const audienceTag = uniqueTag("audience");
|
|
333
|
+
const challengeTag = uniqueTag("challenge");
|
|
334
|
+
const expirationTag = uniqueTag("expiration");
|
|
335
|
+
const expectedTags = nostrChallengeTemplate(expected.nonce, expected.audience, expected.endpoint, expected.issuedAt).tags;
|
|
336
|
+
try {
|
|
337
|
+
return endpointTag?.[1] === expected.endpoint &&
|
|
338
|
+
methodTag?.[1] === "POST" &&
|
|
339
|
+
audienceTag?.[1] === expected.audience &&
|
|
340
|
+
challengeTag?.[1] === expected.nonce &&
|
|
341
|
+
expirationTag?.[1] === String(expected.issuedAt + CHALLENGE_TTL_SECS) &&
|
|
342
|
+
event.tags.length === expectedTags.length &&
|
|
343
|
+
verifyEvent(event);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
266
349
|
/** Verify a challenge-response proof.
|
|
267
350
|
* The wallet signs the exact text produced by `bchChallengePayload`.
|
|
268
351
|
* @param issuedAt - unix timestamp when the challenge was issued; if provided, rejects proofs older than 5 minutes.
|
|
@@ -275,7 +358,9 @@ export function verifyChallengeProof(proof, nonce, audience, issuedAt) {
|
|
|
275
358
|
typeof nonce !== "string")
|
|
276
359
|
return false;
|
|
277
360
|
if (issuedAt !== undefined &&
|
|
278
|
-
(!Number.isSafeInteger(issuedAt) ||
|
|
361
|
+
(!Number.isSafeInteger(issuedAt) ||
|
|
362
|
+
issuedAt > Math.floor(Date.now() / 1000) + 60 ||
|
|
363
|
+
Math.floor(Date.now() / 1000) - issuedAt > CHALLENGE_TTL_SECS))
|
|
279
364
|
return false;
|
|
280
365
|
let expectedPayload;
|
|
281
366
|
try {
|
|
@@ -365,10 +450,53 @@ export async function resolveIdentityChain(npub, relays = DEFAULT_RELAYS) {
|
|
|
365
450
|
}
|
|
366
451
|
if (!predecessor)
|
|
367
452
|
return [];
|
|
453
|
+
const predecessorBinding = canonicalBindingFromResolved(predecessor);
|
|
454
|
+
const successorBinding = canonicalBindingFromResolved(current);
|
|
455
|
+
if (!predecessorBinding || !successorBinding)
|
|
456
|
+
return [];
|
|
457
|
+
if (predecessor.pubkey !== current.pubkey) {
|
|
458
|
+
if (!current.rotationAuthorization ||
|
|
459
|
+
!verifyRotationAuthorization(current.rotationAuthorization, predecessorBinding, successorBinding))
|
|
460
|
+
return [];
|
|
461
|
+
}
|
|
462
|
+
else if (current.rotationAuthorization &&
|
|
463
|
+
!verifyRotationAuthorization(current.rotationAuthorization, predecessorBinding, successorBinding)) {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
368
466
|
current = predecessor;
|
|
369
467
|
}
|
|
370
468
|
return chain;
|
|
371
469
|
}
|
|
470
|
+
function canonicalBindingFromResolved(identity) {
|
|
471
|
+
try {
|
|
472
|
+
const content = JSON.parse(identity.rawEvent.content);
|
|
473
|
+
const bch = content.bch;
|
|
474
|
+
if (typeof content.nonce !== "string" ||
|
|
475
|
+
typeof content.createdAt !== "number" ||
|
|
476
|
+
typeof bch.address !== "string" ||
|
|
477
|
+
typeof bch.network !== "string" ||
|
|
478
|
+
!["mainnet", "testnet", "chipnet"].includes(bch.network))
|
|
479
|
+
return null;
|
|
480
|
+
return {
|
|
481
|
+
protocol: PROTOCOL_ID,
|
|
482
|
+
version: PROTOCOL_VERSION,
|
|
483
|
+
bindingId: identity.bindingId,
|
|
484
|
+
bch: {
|
|
485
|
+
address: bch.address,
|
|
486
|
+
network: bch.network,
|
|
487
|
+
...(typeof bch.paymentCode === "string" ? { paymentCode: bch.paymentCode } : {}),
|
|
488
|
+
},
|
|
489
|
+
nostr: { pubkey: identity.pubkey },
|
|
490
|
+
createdAt: content.createdAt,
|
|
491
|
+
nonce: content.nonce,
|
|
492
|
+
purpose: "identity-binding-v3",
|
|
493
|
+
...(typeof content.previousBindingId === "string" ? { previousBindingId: content.previousBindingId } : {}),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
372
500
|
function resolvedIdentityFromEvent(event) {
|
|
373
501
|
if (event.kind !== BINDING_KIND ||
|
|
374
502
|
!/^[0-9a-f]{64}$/.test(event.pubkey) ||
|
|
@@ -408,6 +536,9 @@ function resolvedIdentityFromEvent(event) {
|
|
|
408
536
|
createdAt,
|
|
409
537
|
bchProofTag: bchProofTag,
|
|
410
538
|
rawEvent: event,
|
|
539
|
+
...(content.rotationAuthorization && typeof content.rotationAuthorization === "object"
|
|
540
|
+
? { rotationAuthorization: content.rotationAuthorization }
|
|
541
|
+
: {}),
|
|
411
542
|
...(anchorTag?.[1] ? {
|
|
412
543
|
anchorClaim: {
|
|
413
544
|
txid: anchorTag[1],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "did-beta-bch",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Decentralised identity protocol binding BCH addresses to Nostr public keys",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "tsc && npm run build:demo",
|
|
22
22
|
"build:demo": "esbuild demo/client.js --bundle --format=esm --platform=browser --target=es2022 --outfile=demo/bundle.js",
|
|
23
|
-
"typecheck": "tsc --noEmit",
|
|
24
23
|
"test": "node --test test/cashlet-did.test.mjs",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
25
|
"prepublishOnly": "npm run build"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"esbuild": "^0.28.2",
|
|
50
50
|
"typescript": "^5.4.0"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|