@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 +10 -5
- package/src/bitcoin.ts +113 -8
- package/src/solana.ts +3 -4
- package/src/tests/bitcoin.test.ts +6 -2
- package/src/tests/eth.test.ts +2 -1
- package/src/tests/solana.test.ts +7 -7
package/package.json
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
{
|
2
2
|
"name": "@notabene/verify-proof",
|
3
|
-
"version": "1.0.0-preview.
|
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
|
-
"
|
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
|
-
"
|
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
|
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 =
|
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 =
|
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
|
20
|
+
publicKey,
|
22
21
|
);
|
23
22
|
|
24
23
|
return {
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { describe, it, expect
|
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",
|
package/src/tests/eth.test.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { describe, it, expect
|
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",
|
package/src/tests/solana.test.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
2
|
-
import
|
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:${
|
22
|
-
address: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${
|
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:${
|
37
|
-
address: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:${
|
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",
|