Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address Issue #171: Validate Generated Addresses Against Expected Ones in JS Client Library #184

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4b7e14e
Add setInputWitnessScript and getInputWitnessScript methods
Feb 8, 2023
5dabd24
Fix setInputWitnessScript
landabaso Feb 8, 2023
6c132e8
Create a top level package.json file so that this repo can be install…
landabaso Feb 8, 2023
ad5cc1d
Better run the build script in the child's directory
landabaso Feb 8, 2023
b0a66fd
Merge branch 'LedgerHQ:develop' into develop
landabaso Feb 9, 2023
9bd3f4a
Change amount type from Buffer to number in setInputWitnessUtxo / get…
landabaso Feb 9, 2023
7a828fc
Remove unnecessary tiny-secp256k1 dependency
landabaso Feb 9, 2023
50d382b
Add support for importing BitcoinJS Psbt for Ledger signing
landabaso Feb 10, 2023
0ca72c4
Remove tiny-secp256k1 dependency from package-lock.js
landabaso Feb 10, 2023
5633993
Return the 'this' object in fromBitcoinJS to allow chaining
landabaso Feb 10, 2023
05526e2
Merge branch 'bitcoinjslib_compat' into develop
landabaso Feb 10, 2023
8ff6495
Merge from import-from-bitcoinjs
landabaso Feb 16, 2023
96c60dd
Expose DefaultDescriptorTemplate
landabaso Feb 21, 2023
d0f9cd9
Adapt package.json so that this can be published to npm (to be used i…
landabaso Feb 22, 2023
994d959
Add proper credit to Ledger for this module
landabaso Feb 22, 2023
e03c6e1
Merge remote-tracking branch 'ledgers-remote/develop' into develop
landabaso Apr 19, 2023
079f8aa
Merge remote-tracking branch 'ledgers-remote/develop' into develop
landabaso Apr 19, 2023
ec45c0d
Sync wrt ledgers remote
landabaso Apr 19, 2023
2383790
Remove package-lock.json files (to be in synch with ledger-bitcoin pa…
landabaso Apr 19, 2023
8f441a0
Merge branch 'LedgerHQ:develop' into develop
landabaso Jul 17, 2023
056253f
Address #171: Compare generated addresses with expected ones
landabaso Jul 17, 2023
4192577
Addressed review comments: validate addresses at getWalletAddress and…
landabaso Jul 20, 2023
71f8573
Fix issue with key replacement in address derivation
landabaso Jul 20, 2023
8cf4162
Fix typo and rebase onto develop branch
landabaso Jul 26, 2023
5bda67b
Merge branch 'develop' into improved-validation
landabaso Jul 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions bitcoin_client_js/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ledger-bitcoin",
"version": "0.2.2",
"version": "0.2.3",
"description": "Ledger Hardware Wallet Bitcoin Application Client",
"main": "build/main/index.js",
"typings": "build/main/index.d.ts",
Expand Down Expand Up @@ -29,9 +29,11 @@
"node": ">=14"
},
"dependencies": {
"@bitcoinerlab/descriptors": "^1.0.2",
"@bitcoinerlab/secp256k1": "^1.0.5",
"@ledgerhq/hw-transport": "^6.20.0",
"bip32-path": "^0.4.2",
"bitcoinjs-lib": "^6.0.1"
"bitcoinjs-lib": "^6.1.3"
},
"devDependencies": {
"@ledgerhq/hw-transport-node-speculos-http": "^6.24.1",
Expand Down
47 changes: 46 additions & 1 deletion bitcoin_client_js/src/__tests__/appClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,51 @@ describe("test AppClient", () => {
expect(walletHmac.length).toEqual(32);
});

//https://wizardsardine.com/blog/ledger-vulnerability-disclosure/
it('can generate a correct address or throw on a:X', async () => {
for (const template of [
'wsh(and_b(pk(@0/**),a:1))',
'wsh(and_b(pk(@0/<0;1>/*),a:1))'
]) {
try {
const walletPolicy = new WalletPolicy('Fixed Vulnerability', template, [
"[f5acc2fd/84'/1'/0']tpubDCtKfsNyRhULjZ9XMS4VKKtVcPdVDi8MKUbcSD9MJDyjRu1A2ND5MiipozyyspBT9bg8upEp7a8EAgFxNxXn1d7QkdbL52Ty5jiSLcxPt1P"
]);

const automation = JSON.parse(
fs
.readFileSync(
'src/__tests__/automations/register_wallet_accept.json'
)
.toString()
);
await setSpeculosAutomation(transport, automation);

const [walletId, walletHmac] = await app.registerWallet(walletPolicy);

expect(walletId).toEqual(walletPolicy.getId());
expect(walletHmac.length).toEqual(32);

const address = await app.getWalletAddress(
walletPolicy,
walletHmac,
0,
0,
false
);
//version > 2.1.1
expect(address).toEqual(
'tb1q5lyn9807ygs7pc52980mdeuwl9wrq5c8n3kntlhy088h6fqw4gzspw9t9m'
);
} catch (error) {
//version <= 2.1.1
expect(error.message).toMatch(
/^Third party address validation mismatch/
);
}
}
});

it("can register a miniscript wallet", async () => {
const walletPolicy = new WalletPolicy(
"Decaying 3-of-3",
Expand Down Expand Up @@ -418,4 +463,4 @@ describe("test AppClient", () => {
const result = await app.signMessage(Buffer.from(msg, "ascii"), path)
expect(result).toEqual("H4frM6TYm5ty1MAf9o/Zz9Qiy3VEldAYFY91SJ/5nYMAZY1UUB97fiRjKW8mJit2+V4OCa1YCqjDqyFnD9Fw75k=");
});
});
});
128 changes: 87 additions & 41 deletions bitcoin_client_js/src/lib/appClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import * as descriptors from '@bitcoinerlab/descriptors';
import * as secp256k1 from '@bitcoinerlab/secp256k1';
const { Descriptor } = descriptors.DescriptorsFactory(secp256k1);
import Transport from '@ledgerhq/hw-transport';
import { networks } from 'bitcoinjs-lib';

import { pathElementsToBuffer, pathStringToArray } from './bip32';
import { ClientCommandInterpreter } from './clientCommands';
Expand Down Expand Up @@ -62,14 +66,6 @@ function makePartialSignature(pubkeyAugm: Buffer, signature: Buffer): PartialSig
}
}

/**
* Checks whether a descriptor template contains an `a:` fragment.
*/
function containsA(descriptorTemplate: string): boolean {
const matches = descriptorTemplate.match(/[asctdvjnlu]+:/g) || [];
return matches.some(match => match.includes('a'));
}

/**
* This class encapsulates the APDU protocol documented at
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md
Expand Down Expand Up @@ -184,8 +180,6 @@ export class AppClient {
walletPolicy: WalletPolicy
): Promise<readonly [Buffer, Buffer]> {

await this.validatePolicy(walletPolicy);

const clientInterpreter = new ClientCommandInterpreter();

clientInterpreter.addKnownWalletPolicy(walletPolicy);
Expand All @@ -205,8 +199,20 @@ export class AppClient {
`Invalid response length. Expected 64 bytes, got ${response.length}`
);
}
const walletId = response.subarray(0, 32);
const walletHMAC = response.subarray(32);

// sanity check: derive and validate the first address with a 3rd party
const firstAddrDevice = await this.getWalletAddress(
walletPolicy,
walletHMAC,
0,
0,
false
);
await this.validateAddress(firstAddrDevice, walletPolicy, 0, 0);

return [response.subarray(0, 32), response.subarray(32)];
return [walletId, walletHMAC];
}

/**
Expand Down Expand Up @@ -236,8 +242,6 @@ export class AppClient {
throw new Error('Invalid HMAC length');
}

await this.validatePolicy(walletPolicy);

const clientInterpreter = new ClientCommandInterpreter();

clientInterpreter.addKnownWalletPolicy(walletPolicy);
Expand All @@ -257,7 +261,9 @@ export class AppClient {
clientInterpreter
);

return response.toString('ascii');
const address = response.toString('ascii');
await this.validateAddress(address, walletPolicy, change, addressIndex);
return address;
}

/**
Expand All @@ -279,7 +285,6 @@ export class AppClient {
walletHMAC: Buffer | null,
progressCallback?: () => void
): Promise<[number, PartialSignature][]> {
await this.validatePolicy(walletPolicy);

if (typeof psbt === 'string') {
psbt = Buffer.from(psbt, "base64");
Expand Down Expand Up @@ -402,34 +407,75 @@ export class AppClient {
return result.toString('base64');
}

/* Performs any additional checks on the policy before using it.*/
private async validatePolicy(walletPolicy: WalletPolicy) {
// TODO: Once an independent implementation of miniscript in JavaScript is available,
// we will replace the checks in this section with a generic comparison between the
// address produced by the app and the one computed locally (like the python and Rust
// clients). Until then, we filter for any known bug.

let appAndVer = undefined;

if (containsA(walletPolicy.descriptorTemplate)) {
appAndVer = appAndVer || await this.getAppAndVersion();
if (["2.1.0", "2.1.1"].includes(appAndVer.version)) {
// Versions 2.1.0 and 2.1.1 produced incorrect scripts for policies containing
// the `a:` fragment.
throw new Error("Please update your Ledger Bitcoin app.")
}
/* Performs any additional check on the generated address before returning it.*/
private async validateAddress(
address: string,
walletPolicy: WalletPolicy,
change: number,
addressIndex: number
) {
if (change !== 0 && change !== 1)
throw new Error('Change can only be 0 or 1');
const isChange: boolean = change === 1;
if (addressIndex < 0 || !Number.isInteger(addressIndex))
throw new Error('Invalid address index');
const appAndVer = await this.getAppAndVersion();
let network;
if (appAndVer.name === 'Bitcoin Test') {
network = networks.testnet;
} else if (appAndVer.name === 'Bitcoin') {
network = networks.bitcoin;
} else {
throw new Error(
`Invalid network: ${appAndVer.name}. Expected 'Bitcoin Test' or 'Bitcoin'.`
);
}

if (walletPolicy.descriptorTemplate.includes("thresh(1,")) {
appAndVer = appAndVer || await this.getAppAndVersion();
if (["2.1.0", "2.1.1", "2.1.2"].includes(appAndVer.version)) {
// Versions 2.1.0 and 2.1.1 and "2.1.2" produced incorrect scripts for policies
// containing an unusual thresh fragment with k = n = 1, that is "thresh(1,X)".
// (The check above has false positives, as it also matches "thresh" fragments
// where n > 1; however, better to be overzealous).
throw new Error("Please update your Ledger Bitcoin app.")
}
let expression = walletPolicy.descriptorTemplate;
// Replace change:
expression = expression.replace(/\/\*\*/g, `/<0;1>/*`);
const regExpMN = new RegExp(`/<(\\d+);(\\d+)>`, 'g');
let matchMN;
while ((matchMN = regExpMN.exec(expression)) !== null) {
const [M, N] = [parseInt(matchMN[1], 10), parseInt(matchMN[2], 10)];
expression = expression.replace(`/<${M};${N}>`, `/${isChange ? N : M}`);
}
// Replace index:
expression = expression.replace(/\/\*/g, `/${addressIndex}`);
// Replace origin in reverse order to prevent
// misreplacements, e.g., @10 being mistaken for @1 and leaving a 0.
for (let i = walletPolicy.keys.length - 1; i >= 0; i--)
expression = expression.replace(
new RegExp(`@${i}`, 'g'),
walletPolicy.keys[i]
);
let thirdPartyValidationApplicable = true;
let thirdPartyGeneratedAddress: string;
try {
thirdPartyGeneratedAddress = new Descriptor({
expression,
network
}).getAddress();
} catch (err) {
// Note: @bitcoinerlab/descriptors@1.0.x does not support Tapscript yet.
// These are the supported descriptors:
// - pkh(KEY)
// - wpkh(KEY)
// - sh(wpkh(KEY))
// - sh(SCRIPT)
// - wsh(SCRIPT)
// - sh(wsh(SCRIPT)), where
// SCRIPT is any of the (non-tapscript) fragments in: https://bitcoin.sipa.be/miniscript/
//
// Other expressions are not supported and third party validation would not be applicable:
thirdPartyValidationApplicable = false;
}
if (
thirdPartyValidationApplicable &&
address !== thirdPartyGeneratedAddress
)
throw new Error(
`Third party address validation mismatch: ${address} != ${thirdPartyGeneratedAddress}`
);
}
}

Expand Down