Skip to content

Commit 0868aa1

Browse files
committed
Address LedgerHQ#171: Compare generated addresses with expected ones
1 parent ab32733 commit 0868aa1

File tree

3 files changed

+102
-12
lines changed

3 files changed

+102
-12
lines changed

bitcoin_client_js/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
"node": ">=14"
3030
},
3131
"dependencies": {
32+
"@bitcoinerlab/descriptors": "^1.0.2",
33+
"@bitcoinerlab/secp256k1": "^1.0.5",
3234
"@ledgerhq/hw-transport": "^6.20.0",
3335
"bip32-path": "^0.4.2",
34-
"bitcoinjs-lib": "^6.0.1"
36+
"bitcoinjs-lib": "^6.1.3"
3537
},
3638
"devDependencies": {
3739
"@ledgerhq/hw-transport-node-speculos-http": "^6.24.1",

bitcoin_client_js/src/__tests__/appClient.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,36 @@ describe("test AppClient", () => {
260260
expect(walletHmac.length).toEqual(32);
261261
});
262262

263+
//https://wizardsardine.com/blog/ledger-vulnerability-disclosure/
264+
it('can generate a correct address or throw on a:X', async () => {
265+
try {
266+
const walletPolicy = new WalletPolicy('Fixed Vulnerability', 'wsh(and_b(pk(@0/**),a:1))', [
267+
"[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"
268+
]);
269+
270+
const automation = JSON.parse(fs.readFileSync('src/__tests__/automations/register_wallet_accept.json').toString());
271+
await setSpeculosAutomation(transport, automation);
272+
273+
const [walletId, walletHmac] = await app.registerWallet(walletPolicy);
274+
275+
expect(walletId).toEqual(walletPolicy.getId());
276+
expect(walletHmac.length).toEqual(32);
277+
278+
const address = await app.getWalletAddress(
279+
walletPolicy,
280+
walletHmac,
281+
0,
282+
0,
283+
false
284+
);
285+
//version > 2.1.1
286+
expect(address).toEqual('tb1q5lyn9807ygs7pc52980mdeuwl9wrq5c8n3kntlhy088h6fqw4gzspw9t9m');
287+
} catch (error) {
288+
//version <= 2.1.1
289+
expect(error.message).toMatch(/^Third party address validation mismatch/);
290+
}
291+
});
292+
263293
it("can register a miniscript wallet", async () => {
264294
const walletPolicy = new WalletPolicy(
265295
"Decaying 3-of-3",
@@ -418,4 +448,4 @@ describe("test AppClient", () => {
418448
const result = await app.signMessage(Buffer.from(msg, "ascii"), path)
419449
expect(result).toEqual("H4frM6TYm5ty1MAf9o/Zz9Qiy3VEldAYFY91SJ/5nYMAZY1UUB97fiRjKW8mJit2+V4OCa1YCqjDqyFnD9Fw75k=");
420450
});
421-
});
451+
});

bitcoin_client_js/src/lib/appClient.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import * as descriptors from '@bitcoinerlab/descriptors';
2+
import * as secp256k1 from '@bitcoinerlab/secp256k1';
3+
const { Descriptor } = descriptors.DescriptorsFactory(secp256k1);
14
import Transport from '@ledgerhq/hw-transport';
5+
import { networks } from 'bitcoinjs-lib';
26

37
import { pathElementsToBuffer, pathStringToArray } from './bip32';
48
import { ClientCommandInterpreter } from './clientCommands';
@@ -184,8 +188,6 @@ export class AppClient {
184188
walletPolicy: WalletPolicy
185189
): Promise<readonly [Buffer, Buffer]> {
186190

187-
await this.validatePolicy(walletPolicy);
188-
189191
const clientInterpreter = new ClientCommandInterpreter();
190192

191193
clientInterpreter.addKnownWalletPolicy(walletPolicy);
@@ -236,8 +238,6 @@ export class AppClient {
236238
throw new Error('Invalid HMAC length');
237239
}
238240

239-
await this.validatePolicy(walletPolicy);
240-
241241
const clientInterpreter = new ClientCommandInterpreter();
242242

243243
clientInterpreter.addKnownWalletPolicy(walletPolicy);
@@ -257,7 +257,9 @@ export class AppClient {
257257
clientInterpreter
258258
);
259259

260-
return response.toString('ascii');
260+
const address = response.toString('ascii');
261+
await this.validateAddress(address, walletPolicy, change, addressIndex);
262+
return address;
261263
}
262264

263265
/**
@@ -279,7 +281,6 @@ export class AppClient {
279281
walletHMAC: Buffer | null,
280282
progressCallback?: () => void
281283
): Promise<[number, PartialSignature][]> {
282-
await this.validatePolicy(walletPolicy);
283284

284285
if (typeof psbt === 'string') {
285286
psbt = Buffer.from(psbt, "base64");
@@ -402,12 +403,69 @@ export class AppClient {
402403
return result.toString('base64');
403404
}
404405

406+
/* Performs any additional check on the genetated address before returning it.*/
407+
private async validateAddress(
408+
address: string,
409+
walletPolicy: WalletPolicy,
410+
change: number,
411+
addressIndex: number
412+
) {
413+
if (change !== 0 && change !== 1)
414+
throw new Error('Change can only be 0 or 1');
415+
if (addressIndex < 0 || !Number.isInteger(addressIndex))
416+
throw new Error('Invalid address index');
417+
const appAndVer = await this.getAppAndVersion();
418+
let network;
419+
if (appAndVer.name === 'Bitcoin Test') {
420+
network = networks.testnet;
421+
} else if (appAndVer.name === 'Bitcoin') {
422+
network = networks.bitcoin;
423+
} else {
424+
throw new Error(
425+
`Invalid network: ${appAndVer.name}. Expected 'Bitcoin Test' or 'Bitcoin'.`
426+
);
427+
}
428+
let expression = walletPolicy.descriptorTemplate;
429+
for (let i = 0; i < walletPolicy.keys.length; i++) {
430+
const keyPath = walletPolicy.keys[i] + '/' + change + '/' + addressIndex;
431+
expression = expression
432+
.replace('@' + i + '/**', keyPath)
433+
.replace('@' + i + '/<0;1>', keyPath);
434+
}
435+
let thirdPartyValidationApplicable = true;
436+
let thirdPartyGeneratedAddress: string;
437+
try {
438+
thirdPartyGeneratedAddress = new Descriptor({
439+
expression,
440+
network
441+
}).getAddress();
442+
} catch(err) {
443+
// Note: @bitcoinerlab/[email protected] does not support Tapscript yet.
444+
// These are the supported descriptors:
445+
// - pkh(KEY)
446+
// - wpkh(KEY)
447+
// - sh(wpkh(KEY))
448+
// - sh(SCRIPT)
449+
// - wsh(SCRIPT)
450+
// - sh(wsh(SCRIPT)), where
451+
// SCRIPT is any of the (non-tapscript) fragments in: https://bitcoin.sipa.be/miniscript/
452+
//
453+
// Other expressions are not supported and third party validation would not be applicable:
454+
thirdPartyValidationApplicable = false;
455+
}
456+
if (thirdPartyValidationApplicable) {
457+
if (address !== thirdPartyGeneratedAddress) {
458+
throw new Error(
459+
`Third party address validation mismatch: ${address} != ${thirdPartyGeneratedAddress}`
460+
);
461+
}
462+
} else {
463+
await this.validatePolicy(walletPolicy);
464+
}
465+
}
466+
405467
/* Performs any additional checks on the policy before using it.*/
406468
private async validatePolicy(walletPolicy: WalletPolicy) {
407-
// TODO: Once an independent implementation of miniscript in JavaScript is available,
408-
// we will replace the checks in this section with a generic comparison between the
409-
// address produced by the app and the one computed locally (like the python and Rust
410-
// clients). Until then, we filter for any known bug.
411469

412470
let appAndVer = undefined;
413471

0 commit comments

Comments
 (0)