@notabene/verify-proof 1.0.0-preview.6 → 1.0.0-preview.7

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/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@notabene/verify-proof",
3
- "version": "1.0.0-preview.6",
3
+ "version": "1.0.0-preview.7",
4
4
  "description": "Verify ownership proofs",
5
5
  "source": "src/index.ts",
6
6
  "type": "module",
7
7
  "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
8
9
  "main": "dist/index.cjs",
9
10
  "author": "Pelle Braendgaard",
10
11
  "license": "Apache-2.0",
@@ -31,16 +32,20 @@
31
32
  "microbundle": "^0.15.1",
32
33
  "tiny-secp256k1": "^2.2.3",
33
34
  "typescript": "^5.5.4",
34
- "vitest": "^2.0.5"
35
+ "vite": "^5.4.11",
36
+ "vitest": "^2.0.5",
37
+ "bitcoinjs-message": "^2.2.0",
38
+ "ox": "^0.1.4"
35
39
  },
36
40
  "dependencies": {
41
+ "@bitauth/libauth": "^3.0.0",
37
42
  "@notabene/javascript-sdk": "^2.0.2",
38
- "@solana/web3.js": "^1.95.4",
39
43
  "@stablelib/base64": "^2.0.0",
44
+ "bech32": "^2.0.0",
40
45
  "bigi": "^1.4.2",
41
- "bitcoinjs-message": "^2.2.0",
42
- "ox": "^0.1.4",
46
+ "bs58": "^6.0.0",
43
47
  "tweetnacl": "^1.0.3",
48
+ "varuint-bitcoin": "^2.0.0",
44
49
  "viem": "^2.21.44"
45
50
  }
46
51
  }
package/src/bitcoin.ts CHANGED
@@ -2,7 +2,24 @@ import {
2
2
  ProofStatus,
3
3
  SignatureProof,
4
4
  } from "@notabene/javascript-sdk/src/types";
5
- import * as bitcoinMessage from "bitcoinjs-message";
5
+ import { bech32 } from "bech32";
6
+
7
+ import {
8
+ secp256k1,
9
+ hash160,
10
+ hash256,
11
+ RecoveryId,
12
+ encodeBase58AddressFormat,
13
+ } from "@bitauth/libauth";
14
+ import { encode as encodeLength } from "varuint-bitcoin";
15
+ import { decode as decodeBase64 } from "@stablelib/base64";
16
+
17
+ enum SEGWIT_TYPES {
18
+ P2WPKH = "p2wpkh",
19
+ P2SH_P2WPKH = "p2sh(p2wpkh)",
20
+ }
21
+
22
+ const messagePrefix = "\u0018Bitcoin Signed Message:\n";
6
23
 
7
24
  export enum DerivationMode {
8
25
  LEGACY = "Legacy",
@@ -25,13 +42,7 @@ export async function verifyBTCSignature(
25
42
  const segwit = [DerivationMode.SEGWIT, DerivationMode.NATIVE].includes(
26
43
  getDerivationMode(address),
27
44
  );
28
- const verified = bitcoinMessage.verify(
29
- proof.attestation,
30
- address,
31
- proof.proof,
32
- undefined,
33
- segwit,
34
- );
45
+ const verified = verify(proof.attestation, address, proof.proof, segwit);
35
46
 
36
47
  return {
37
48
  ...proof,
@@ -59,3 +70,97 @@ function getDerivationMode(address: string) {
59
70
  );
60
71
  }
61
72
  }
73
+
74
+ type DecodedSignature = {
75
+ compressed: boolean;
76
+ segwitType?: SEGWIT_TYPES;
77
+ recovery: RecoveryId;
78
+ signature: Uint8Array;
79
+ };
80
+ function decodeSignature(proof: string): DecodedSignature {
81
+ const signature = decodeBase64(proof);
82
+ if (signature.length !== 65) throw new Error("Invalid signature length");
83
+
84
+ const flagByte = signature[0] - 27;
85
+ if (flagByte > 15 || flagByte < 0) {
86
+ throw new Error("Invalid signature parameter");
87
+ }
88
+
89
+ return {
90
+ compressed: !!(flagByte & 12),
91
+ segwitType: !(flagByte & 8)
92
+ ? undefined
93
+ : !(flagByte & 4)
94
+ ? SEGWIT_TYPES.P2SH_P2WPKH
95
+ : SEGWIT_TYPES.P2WPKH,
96
+ recovery: (flagByte & 3) as RecoveryId,
97
+ signature: signature.slice(1),
98
+ };
99
+ }
100
+
101
+ function verify(
102
+ attestation: string,
103
+ address: string,
104
+ proof: string,
105
+ checkSegwitAlways: boolean,
106
+ ) {
107
+ const { compressed, segwitType, recovery, signature } =
108
+ decodeSignature(proof);
109
+ if (checkSegwitAlways && !compressed) {
110
+ throw new Error(
111
+ "checkSegwitAlways can only be used with a compressed pubkey signature flagbyte",
112
+ );
113
+ }
114
+
115
+ const hash = magicHash(attestation);
116
+ const publicKey: Uint8Array | string = compressed
117
+ ? secp256k1.recoverPublicKeyCompressed(signature, recovery, hash)
118
+ : secp256k1.recoverPublicKeyUncompressed(signature, recovery, hash);
119
+ if (typeof publicKey === "string") throw new Error(publicKey);
120
+ const publicKeyHash = hash160(publicKey);
121
+ let actual: string = "";
122
+
123
+ if (segwitType) {
124
+ if (segwitType === SEGWIT_TYPES.P2SH_P2WPKH) {
125
+ actual = encodeBech32Address(publicKeyHash);
126
+ } else {
127
+ // parsed.segwitType === SEGWIT_TYPES.P2WPKH
128
+ // must be true since we only return null, P2SH_P2WPKH, or P2WPKH
129
+ // from the decodeSignature function.
130
+ actual = encodeBech32Address(publicKeyHash);
131
+ }
132
+ } else {
133
+ if (checkSegwitAlways) {
134
+ try {
135
+ actual = encodeBech32Address(publicKeyHash);
136
+ // if address is bech32 it is not p2sh
137
+ } catch (e) {
138
+ actual = encodeBech32Address(publicKeyHash);
139
+ // base58 can be p2pkh or p2sh-p2wpkh
140
+ }
141
+ } else {
142
+ actual = encodeBase58AddressFormat(0, publicKeyHash);
143
+ }
144
+ }
145
+
146
+ return actual === address;
147
+ }
148
+
149
+ export function magicHash(attestation: string) {
150
+ const prefix = new TextEncoder().encode(messagePrefix);
151
+ const message = new TextEncoder().encode(attestation);
152
+ const length = encodeLength(message.length).buffer;
153
+ const buffer = new Uint8Array(
154
+ prefix.length + length.byteLength + message.length,
155
+ );
156
+ buffer.set(prefix);
157
+ buffer.set(new Uint8Array(length), prefix.length);
158
+ buffer.set(message, prefix.length + length.byteLength);
159
+ return hash256(buffer);
160
+ }
161
+
162
+ function encodeBech32Address(publicKeyHash: Uint8Array): string {
163
+ const bwords = bech32.toWords(publicKeyHash);
164
+ bwords.unshift(0);
165
+ return bech32.encode("bc", bwords);
166
+ }
package/src/solana.ts CHANGED
@@ -1,24 +1,23 @@
1
- import { PublicKey } from "@solana/web3.js";
2
1
  import nacl from "tweetnacl";
3
2
  import {
4
3
  ProofStatus,
5
4
  SignatureProof,
6
5
  } from "@notabene/javascript-sdk/src/types";
7
6
  import { decode as decodeBase64 } from "@stablelib/base64";
8
-
7
+ import bs58 from "bs58";
9
8
  export async function verifySolanaSignature(
10
9
  proof: SignatureProof,
11
10
  ): Promise<SignatureProof> {
12
11
  const [ns, _, address] = proof.address.split(/:/);
13
12
  if (ns !== "solana") return { ...proof, status: ProofStatus.FAILED };
14
13
  try {
15
- const publicKey = new PublicKey(address);
14
+ const publicKey = bs58.decode(address);
16
15
  const messageBytes = new TextEncoder().encode(proof.attestation);
17
16
  const signatureBytes = decodeBase64(proof.proof);
18
17
  const verified = nacl.sign.detached.verify(
19
18
  messageBytes,
20
19
  signatureBytes,
21
- publicKey.toBytes(),
20
+ publicKey,
22
21
  );
23
22
 
24
23
  return {
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi } from "vitest";
1
+ import { describe, it, expect } from "vitest";
2
2
  import * as bitcoin from "bitcoinjs-lib";
3
3
  import * as bitcoinMessage from "bitcoinjs-message";
4
4
  import { Buffer } from "node:buffer";
@@ -19,10 +19,13 @@ function signMessage(
19
19
  ): SignatureProof {
20
20
  const keyPair = bitcoin.ECPair.makeRandom({ rng: rng });
21
21
  const address = toAddress(keyPair);
22
+ const sigOptions: bitcoinMessage.SignatureOptions | undefined =
23
+ address.startsWith("bc1") ? { segwitType: "p2wpkh" } : undefined;
22
24
  const privateKey = keyPair.d.toBuffer(32);
23
25
  var attestation = "This is an example of a signed message.";
26
+
24
27
  var proof = bitcoinMessage
25
- .sign(attestation, privateKey, keyPair.compressed)
28
+ .sign(attestation, privateKey, keyPair.compressed, sigOptions)
26
29
  .toString("base64");
27
30
  // console.log("signMessage", { proof, address });
28
31
  return {
@@ -67,6 +70,7 @@ describe("verifyBTCSignature", () => {
67
70
  const proof: SignatureProof = {
68
71
  type: ProofTypes.BIP137,
69
72
  did: "did:pkh:bip122:000000000019d6689c085ae165831e93:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
73
+ wallet_provider: "ledger",
70
74
  address:
71
75
  "bip122:000000000019d6689c085ae165831e93:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
72
76
  attestation: "test message",
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi } from "vitest";
1
+ import { describe, it, expect } from "vitest";
2
2
  import { verifyPersonalSignEIP191 } from "../eth";
3
3
  import {
4
4
  type SignatureProof,
@@ -45,6 +45,7 @@ describe("verifyPersonalSignEIP191", () => {
45
45
  const proof: SignatureProof = {
46
46
  type: ProofTypes.EIP191,
47
47
  did: `did:pkh:bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6`,
48
+ wallet_provider: "metamask",
48
49
  address:
49
50
  "bip122:000000000019d6689c085ae165831e93:128Lkh3S7CkDTBZ8W7BbpsN3YYizJMp8p6",
50
51
  attestation: "test message",
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { Keypair } from "@solana/web3.js";
2
+ import bs58 from "bs58";
3
3
  import nacl from "tweetnacl";
4
4
  import {
5
5
  ProofStatus,
@@ -9,17 +9,18 @@ import {
9
9
  import { verifySolanaSignature } from "../solana";
10
10
 
11
11
  describe("verifySolanaSignature", () => {
12
+ const keypair = nacl.sign.keyPair();
13
+ const address = bs58.encode(keypair.publicKey);
12
14
  it("verifies valid Solana signature", async () => {
13
15
  // Generate a test keypair
14
- const keypair = Keypair.generate();
15
16
  const message = "Test message";
16
17
  const messageBytes = new TextEncoder().encode(message);
17
18
  const signature = nacl.sign.detached(messageBytes, keypair.secretKey);
18
19
  const proof: SignatureProof = {
19
20
  type: ProofTypes.ED25519,
20
21
  status: ProofStatus.PENDING,
21
- did: `did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${keypair.publicKey.toString()}`,
22
- address: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${keypair.publicKey.toString()}`,
22
+ did: `did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${address}`,
23
+ address: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${address}`,
23
24
  attestation: message,
24
25
  proof: Buffer.from(signature).toString("base64"),
25
26
  wallet_provider: "Phantom",
@@ -29,12 +30,11 @@ describe("verifySolanaSignature", () => {
29
30
  });
30
31
 
31
32
  it("fails for invalid signature", async () => {
32
- const keypair = Keypair.generate();
33
33
  const proof: SignatureProof = {
34
34
  type: ProofTypes.ED25519,
35
35
  status: ProofStatus.PENDING,
36
- did: `did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${keypair.publicKey.toString()}`,
37
- address: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${keypair.publicKey.toString()}`,
36
+ did: `did:pkh:solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${address}`,
37
+ address: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${address}`,
38
38
  attestation: "Test message",
39
39
  proof: "invalid_signature",
40
40
  wallet_provider: "Phantom",