@notabene/verify-proof 1.0.0-preview.6 → 1.0.0-preview.7
Sign up to get free protection for your applications and to get access to all the features.
- 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",
|