@notabene/verify-proof 1.8.0 → 1.9.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.
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notabene/verify-proof",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Verify ownership proofs",
5
5
  "source": "src/index.ts",
6
6
  "type": "module",
@@ -44,8 +44,9 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@cardano-foundation/cardano-verify-datasignature": "^1.0.11",
47
+ "@cosmjs/amino": "^0.34.0",
47
48
  "@noble/curves": "^1.7.0",
48
- "@notabene/javascript-sdk": "^2.11.0",
49
+ "@notabene/javascript-sdk": "2.11.0-next.2",
49
50
  "@scure/base": "^1.2.1",
50
51
  "@stellar/stellar-sdk": "^13.1.0",
51
52
  "bip322-js": "^2.0.0",
package/src/bitcoin.ts CHANGED
@@ -117,7 +117,6 @@ export async function verifyBTCSignature(
117
117
  status: ProofStatus.FAILED,
118
118
  };
119
119
  }
120
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
121
120
  } catch (error) {
122
121
  console.error("error verifying proof", error);
123
122
  return {
@@ -169,10 +168,16 @@ function verifyBIP322(address: string, proof: SignatureProof) {
169
168
  }
170
169
 
171
170
  function verifyBIP137(address: string, proof: SignatureProof, chainConfig: ChainConfig) {
172
- const segwit = Boolean(chainConfig.bech32Prefix && [DerivationMode.SEGWIT, DerivationMode.NATIVE].includes(
173
- getDerivationMode(address)
174
- ));
175
- const verified = verify(proof.attestation, address, proof.proof, segwit, chainConfig);
171
+ const derivationMode = getDerivationMode(address);
172
+
173
+ // For legacy addresses (starting with "1"), never use SegWit encoding
174
+ // For P2SH addresses (starting with "3"), use SegWit encoding if they have bech32 support
175
+ // For native SegWit addresses (bc1, tb1, ltc1), always use SegWit encoding
176
+ const useSegwitEncoding = Boolean(chainConfig.bech32Prefix &&
177
+ (derivationMode === DerivationMode.NATIVE ||
178
+ (derivationMode === DerivationMode.SEGWIT && !address.startsWith("1"))));
179
+
180
+ const verified = verify(proof.attestation, address, proof.proof, useSegwitEncoding, chainConfig);
176
181
 
177
182
  return {
178
183
  ...proof,
@@ -191,6 +196,10 @@ function getDerivationMode(address: string) {
191
196
  return DerivationMode.DOGECOIN;
192
197
  } else if (address.match("^(q).*")) {
193
198
  return DerivationMode.BCH;
199
+ } else if (address.match("^(t1|t3).*")) {
200
+ return DerivationMode.LEGACY; // Zcash addresses
201
+ } else if (address.match("^[X7].*")) {
202
+ return DerivationMode.LEGACY; // Dash addresses
194
203
  } else {
195
204
  throw new Error(
196
205
  "INVALID ADDRESS: "
@@ -271,7 +280,22 @@ function verify(
271
280
  }
272
281
  }
273
282
  } else {
274
- if (checkSegwitAlways && chainConfig.bech32Prefix) {
283
+ // For addresses starting with "3" (P2SH), try both P2SH-P2WPKH and legacy P2SH encodings if segwitType is undefined
284
+ if (address.startsWith("3") && !segwitType) {
285
+ // P2SH-P2WPKH: script hash of the redeem script (OP_0 <pubkeyhash>)
286
+ const redeemScript = new Uint8Array(22);
287
+ redeemScript[0] = 0x00; // OP_0
288
+ redeemScript[1] = 0x14; // push 20 bytes
289
+ redeemScript.set(publicKeyHash, 2);
290
+ const redeemScriptHash = hash160(redeemScript);
291
+ const p2shP2wpkh = encodeBase58AddressFormat(chainConfig.scriptHashVersion, redeemScriptHash);
292
+ // Legacy P2SH: script hash of the public key
293
+ const legacyP2sh = encodeBase58AddressFormat(chainConfig.scriptHashVersion, publicKeyHash);
294
+ if (address === p2shP2wpkh || address === legacyP2sh) {
295
+ return true;
296
+ }
297
+ actual = legacyP2sh; // fallback for error reporting
298
+ } else if (checkSegwitAlways && chainConfig.bech32Prefix) {
275
299
  try {
276
300
  actual = encodeBech32Address(publicKeyHash, chainConfig.bech32Prefix);
277
301
  // if address is bech32 it is not p2sh
package/src/cosmos.ts ADDED
@@ -0,0 +1,88 @@
1
+ import {
2
+ CosmosMetadata,
3
+ ProofStatus,
4
+ SignatureProof,
5
+ } from "@notabene/javascript-sdk";
6
+ import { bech32 } from "bech32";
7
+ import { StdSignDoc, serializeSignDoc } from "@cosmjs/amino";
8
+ import { secp256k1 } from "@noble/curves/secp256k1";
9
+ import { ripemd160 } from "@noble/hashes/legacy";
10
+ import { sha256 } from "@noble/hashes/sha2";
11
+
12
+ // Base64 decode
13
+ function base64ToUint8Array(base64: string): Uint8Array {
14
+ return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
15
+ }
16
+
17
+ // Base64 encode UTF-8 bytes
18
+ function toBase64Bytes(str: string): string {
19
+ const utf8 = new TextEncoder().encode(str);
20
+ let binary = "";
21
+ for (let i = 0; i < utf8.length; i++) {
22
+ binary += String.fromCharCode(utf8[i]);
23
+ }
24
+ return btoa(binary);
25
+ }
26
+
27
+ // Cosmos address derivation (ADR-36)
28
+ // https://github.com/cosmos/cosmos-sdk/blob/214b11dcbaa129f7b4c0013b2103db9d54b85e9e/docs/architecture/adr-036-arbitrary-signature.md
29
+ function deriveCosmosAddress(pubkey: Uint8Array, prefix = "cosmos"): string {
30
+ const sha = sha256(pubkey);
31
+ const rip = ripemd160(sha);
32
+ return bech32.encode(prefix, bech32.toWords(rip));
33
+ }
34
+
35
+ export async function verifyCosmosSignature(
36
+ proof: SignatureProof
37
+ ): Promise<SignatureProof> {
38
+ try {
39
+ const [ns, , address] = proof.address.split(/:/);
40
+ if (ns !== "cosmos") {
41
+ return { ...proof, status: ProofStatus.FAILED };
42
+ }
43
+
44
+ // Parse the proof JSON
45
+ const pubKeyBytes = base64ToUint8Array(
46
+ (proof.chainSpecificData as CosmosMetadata).pub_key.value
47
+ );
48
+ const signatureBytes = base64ToUint8Array(proof.proof);
49
+
50
+ // Step 1: Derive address from pubkey
51
+ const derivedAddress = deriveCosmosAddress(pubKeyBytes);
52
+ if (derivedAddress !== address) {
53
+ return { ...proof, status: ProofStatus.FAILED };
54
+ }
55
+
56
+ // Step 2: Build MsgSignData + StdSignDoc
57
+ const signDoc: StdSignDoc = {
58
+ chain_id: "", // must match the signing process
59
+ account_number: "0",
60
+ sequence: "0",
61
+ fee: { amount: [], gas: "0" },
62
+ msgs: [
63
+ {
64
+ type: "sign/MsgSignData",
65
+ value: {
66
+ signer: address,
67
+ data: toBase64Bytes(proof.attestation),
68
+ },
69
+ },
70
+ ],
71
+ memo: "",
72
+ };
73
+
74
+ // Step 3: Serialize + hash
75
+ const signBytes = await serializeSignDoc(signDoc);
76
+ const digest = sha256(signBytes);
77
+
78
+ // Step 4: Verify using secp256k1
79
+ const verified = secp256k1.verify(signatureBytes, digest, pubKeyBytes);
80
+
81
+ return {
82
+ ...proof,
83
+ status: verified ? ProofStatus.VERIFIED : ProofStatus.FAILED,
84
+ };
85
+ } catch {
86
+ return { ...proof, status: ProofStatus.FAILED };
87
+ }
88
+ }
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ import { verifyCIP8Signature } from "./cardano";
14
14
  import { verifyPersonalSignXRPL } from "./xrpl";
15
15
  import { verifyStellarSignature } from "./xlm";
16
16
  import { verifyConcordiumSignature } from "./concordium";
17
+ import { verifyCosmosSignature } from "./cosmos";
17
18
 
18
19
  export async function verifyProof(
19
20
  proof: OwnershipProof,
@@ -46,9 +47,10 @@ export async function verifyProof(
46
47
  return verifyPersonalSignXRPL(proof as SignatureProof, publicKey);
47
48
  case ProofTypes.XLM_ED25519:
48
49
  return verifyStellarSignature(proof as SignatureProof);
49
- case ProofTypes.CONCORDIUM: {
50
+ case ProofTypes.CONCORDIUM:
50
51
  return verifyConcordiumSignature(proof as SignatureProof);
51
- }
52
+ case ProofTypes.COSMOS:
53
+ return verifyCosmosSignature(proof as SignatureProof);
52
54
  case ProofTypes.EIP712:
53
55
  case ProofTypes.BIP137:
54
56
  case ProofTypes.BIP322:
@@ -7,6 +7,26 @@ import {
7
7
  ProofTypes,
8
8
  } from "@notabene/javascript-sdk";
9
9
 
10
+ const bitcoinP2WPKHProof: SignatureProof = {
11
+ type: ProofTypes.BIP137,
12
+ wallet_provider: "Manual Wallet Signature",
13
+ status: ProofStatus.PENDING,
14
+ address: "bip122:000000000019d6689c085ae165831e93:3Q9jfjfbMDuatto7wZ6Tz2aKaDCRHqWWys",
15
+ did: "did:pkh:bip122:000000000019d6689c085ae165831e93:3Q9jfjfbMDuatto7wZ6Tz2aKaDCRHqWWys",
16
+ attestation: "I certify that the blockchain address 3Q9jfjfbMDuatto7wZ6Tz2aKaDCRHqWWys belongs to did:pkh:bip122:000000000019d6689c085ae165831e93:3Q9jfjfbMDuatto7wZ6Tz2aKaDCRHqWWys on Wed, 09 Jul 2025 20:17:12 GMT",
17
+ proof: "HxPmbzvEnvgu0RYIPYl5bWySkFNXwOF/Jegq3NvzjFZ/Ik/koTdV9rh2A7osXefhzTlniUw8YbZNmCeXB9V9qC8=",
18
+ }
19
+
20
+ const bitcoinP2shProof: SignatureProof = {
21
+ type: ProofTypes.BIP137,
22
+ wallet_provider: "Manual Wallet Signature",
23
+ status: ProofStatus.PENDING,
24
+ address: "bip122:000000000019d6689c085ae165831e93:1ANiqVALaKwadxA9nvmCUHpBhaopVniuVS",
25
+ did: "did:pkh:bip122:000000000019d6689c085ae165831e93:1ANiqVALaKwadxA9nvmCUHpBhaopVniuVS",
26
+ attestation: "I certify that the blockchain address 1ANiqVALaKwadxA9nvmCUHpBhaopVniuVS belongs to did:pkh:bip122:000000000019d6689c085ae165831e93:1ANiqVALaKwadxA9nvmCUHpBhaopVniuVS on Wed, 02 Jul 2025 16:01:44 GMT",
27
+ proof: "IBo2Im6O5NyuXzBJ+coiTMqmUsqG9bv8NzM3+B5e+5XMB2Xp1n/sIsE/73Jy0EdSvcb334t49+3tdjE/X+sXjFw="
28
+ }
29
+
10
30
  const bip322SegwitTestnetProof: SignatureProof = {
11
31
  type: ProofTypes.BIP137,
12
32
  address: "bip122:000000000019d6689c085ae165831e93:tb1q0apvgsh48f3rmw224na3ye5rrg37fd796cm0xf",
@@ -89,6 +109,16 @@ const zcashProof: SignatureProof = {
89
109
  };
90
110
 
91
111
  describe("verifyBTCSignature", () => {
112
+ it("handles bitcoin p2sh addresses", async () => {
113
+ const result = await verifyBTCSignature(bitcoinP2shProof);
114
+ expect(result).toEqual({ ...bitcoinP2shProof, status: ProofStatus.VERIFIED });
115
+ });
116
+
117
+ it("handles bitcoin p2wpkh addresses", async () => {
118
+ const result = await verifyBTCSignature(bitcoinP2WPKHProof);
119
+ expect(result).toEqual({ ...bitcoinP2WPKHProof, status: ProofStatus.VERIFIED });
120
+ });
121
+
92
122
  it("handles bip322 segwit testnet addresses", async () => {
93
123
  const result = await verifyBTCSignature(bip322SegwitTestnetProof);
94
124
  expect(result).toEqual({
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ ProofStatus,
4
+ ProofTypes,
5
+ SignatureProof,
6
+ } from "@notabene/javascript-sdk";
7
+ import { verifyCosmosSignature } from "../cosmos";
8
+
9
+ describe("verifyCosmosSignature", () => {
10
+ it("returns verified proof when valid message and address", async () => {
11
+ const proof: SignatureProof = {
12
+ type: ProofTypes.COSMOS,
13
+ did: "did:pkh:cosmos:cosmoshub-4:cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8",
14
+ status: ProofStatus.PENDING,
15
+ attestation:
16
+ "I certify that\n\ncosmos:cosmoshub-4 account cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8\n\nbelonged to did:key:z6MksamQq4oVRktkeSDxs6pbixYFUaUca8xgCvnTVLQA5PC5\n\non Mon, 14 Jul 2025 15:09:56 GMT",
17
+ address:
18
+ "cosmos:cosmoshub-4:cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8",
19
+ proof:
20
+ "r0L+4864I3p2jPRBt8IAnhhvYmemMkKGg9fcKM0GOBIScgIpmyX/+ObPT/09VMSCzxlzZkH6LQGLNiD2sudAZw==",
21
+ wallet_provider: "Keplr (Wallet Connect)",
22
+ chainSpecificData: {
23
+ pub_key: {
24
+ type: "tendermint/PubKeySecp256k1",
25
+ value: "Ak8k3jYgH4srYi1ygG0GWLrdbF7VwQgjiLqjifuJSsmU",
26
+ },
27
+ },
28
+ };
29
+ const result = await verifyCosmosSignature(proof);
30
+
31
+ expect(result.status).toBe(ProofStatus.VERIFIED);
32
+ });
33
+
34
+ it("returns failed proof when invalid message and address", async () => {
35
+ const proof: SignatureProof = {
36
+ type: ProofTypes.COSMOS,
37
+ did: "did:pkh:cosmos:cosmoshub-4:cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8",
38
+ status: ProofStatus.PENDING,
39
+ attestation:
40
+ "I certify that\n\ncosmos:cosmoshub-4 account cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8\n\nbelonged to did:key:z6MksamQq4oVRktkeSDxs6pbixYFUaUca8xgCvnTVLQA5PC5\n\non Mon, 14 Jul 2025 15:09:56 GMT",
41
+ address:
42
+ "cosmos:cosmoshub-4:cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8",
43
+ proof:
44
+ '{"pub_key":{"type":"tendermint/PubKeySecp256k1","value":"Ak8k3jYgH4srYi1ygG0GWLrdbF7VwQgjiLqjifuJSsmU"},"signature":"invalid_signature_that_will_fail_verification"}',
45
+ wallet_provider: "Keplr (Wallet Connect)",
46
+ };
47
+ const result = await verifyCosmosSignature(proof);
48
+
49
+ expect(result.status).toBe(ProofStatus.FAILED);
50
+ });
51
+ });
@@ -342,4 +342,27 @@ describe("verifyProof", () => {
342
342
  expect(result.status).toBe(ProofStatus.VERIFIED);
343
343
  });
344
344
  })
345
+
346
+ describe("Cosmos", () => {
347
+ const proof: SignatureProof = {
348
+ type: ProofTypes.COSMOS,
349
+ did: "did:pkh:cosmos:cosmoshub-4:cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8",
350
+ status: ProofStatus.PENDING,
351
+ attestation: "I certify that\n\ncosmos:cosmoshub-4 account cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8\n\nbelonged to did:key:z6MksamQq4oVRktkeSDxs6pbixYFUaUca8xgCvnTVLQA5PC5\n\non Mon, 14 Jul 2025 15:09:56 GMT",
352
+ address: "cosmos:cosmoshub-4:cosmos1tzxpcdsszyfuj852msx85c24u37jd7rly8skc8",
353
+ proof: "r0L+4864I3p2jPRBt8IAnhhvYmemMkKGg9fcKM0GOBIScgIpmyX/+ObPT/09VMSCzxlzZkH6LQGLNiD2sudAZw==",
354
+ wallet_provider: "Keplr (Wallet Connect)",
355
+ chainSpecificData: {
356
+ pub_key: {
357
+ type: "tendermint/PubKeySecp256k1",
358
+ value: "Ak8k3jYgH4srYi1ygG0GWLrdbF7VwQgjiLqjifuJSsmU",
359
+ },
360
+ }
361
+ };
362
+
363
+ it("should verify proof", async () => {
364
+ const result = await verifyProof(proof);
365
+ expect(result.status).toBe(ProofStatus.VERIFIED);
366
+ });
367
+ });
345
368
  });