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

Bulk key export #13

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 79 additions & 57 deletions src/Ada.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,6 @@ export const getErrorDescription = (statusCode: number) => {
return DeviceErrorMessages[statusCode] || defaultMsg;
};

export const VersionErrors = {
UNSUPPORTED_GET_SERIAL: "getSerial not supported by device firmware",
UNSUPPORTED_POOL_REGISTRATION: "pool registration not supported by device firmware",
}

// It can happen that we try to send a message to the device
// when the device thinks it is still in a middle of previous ADPU stream.
// This happens mostly if host does abort communication for some reason
Expand Down Expand Up @@ -313,21 +308,23 @@ export default class Ada {
return this._getVersion();
}

_isGetSerialSupported(version: GetVersionResponse): boolean {
async _checkLedgerCardanoAppVersion(minMajor: number, minMinor: number) {
const version = await this._getVersion();
const major = parseInt(version.major);
const minor = parseInt(version.minor);
const patch = parseInt(version.patch);

const msg = "Operation not supported by the Ledger device, make sure to have the latest version of the Cardano app installed";

if (isNaN(major) || isNaN(minor) || isNaN(patch))
return false;
throw new Error(msg);

if (major > 1) {
return true;
} else if (major === 1) {
return minor >= 2;
} else {
return false;
}
}
if (major < minMajor)
throw new Error(msg);

if ((major === minMajor) && (minor < minMinor))
throw new Error(msg);
}

/**
* Returns an object containing the device serial number.
Expand All @@ -340,9 +337,7 @@ export default class Ada {
*
*/
async getSerial(): Promise<GetSerialResponse> {
const version = await this._getVersion();
if (!this._isGetSerialSupported(version))
throw new Error(VersionErrors.UNSUPPORTED_GET_SERIAL);
await this._checkLedgerCardanoAppVersion(1, 2);

const _send = (p1, p2, data) =>
this.send(CLA, INS.GET_SERIAL, p1, p2, data).then(
Expand Down Expand Up @@ -371,42 +366,86 @@ export default class Ada {
}

/**
* @description Get a public key from the specified BIP 32 path.
* @description Get several public keys; one for each of the specified BIP 32 paths.
*
* @param {BIP32Path} indexes The path indexes. Path must begin with `44'/1815'/n'`, and may be up to 10 indexes long.
* @return {Promise<GetExtendedPublicKeyResponse>} The public key with chaincode for the given path.
* @param {Array<BIP32Path>} paths The paths. A path must begin with `44'/1815'/account'` or `1852'/1815'/account'`, and may be up to 10 indexes long.
* @return {Promise<Array<GetExtendedPublicKeyResponse>>} The extended public keys (i.e. with chaincode) for the given paths.
*
* @example
* const { publicKey, chainCode } = await ada.getExtendedPublicKey([ HARDENED + 44, HARDENED + 1815, HARDENED + 1 ]);
* const [{ publicKey, chainCode }] = await ada.getExtendedPublicKeys([[ HARDENED + 44, HARDENED + 1815, HARDENED + 1 ]]);
* console.log(publicKey);
*
*/
async getExtendedPublicKey(
path: BIP32Path
): Promise<GetExtendedPublicKeyResponse> {
async getExtendedPublicKeys(
paths: Array<BIP32Path>
): Promise<Array<GetExtendedPublicKeyResponse>> {

janmazak marked this conversation as resolved.
Show resolved Hide resolved
await this._checkLedgerCardanoAppVersion(2, 1);

// validate the input
Precondition.checkIsArray(paths);
for (const path of paths) {
Precondition.checkIsValidPath(path);
}

const _send = (p1, p2, data) =>
this.send(CLA, INS.GET_EXT_PUBLIC_KEY, p1, p2, data).then(
utils.stripRetcodeFromResponse
);

const P1_UNUSED = 0x00;
const P1_INIT = 0x00;
const P1_NEXT_KEY = 0x01;
const P2_UNUSED = 0x00;

const data = cardano.serializeGetExtendedPublicKeyParams(path);
const result = [];

const response = await wrapRetryStillInCall(_send)(
P1_UNUSED,
P2_UNUSED,
data
);
for (let i = 0; i < paths.length; i++) {
const pathData = cardano.serializeGetExtendedPublicKeyParams(paths[i]);

const [publicKey, chainCode, rest] = utils.chunkBy(response, [32, 32]);
Assert.assert(rest.length === 0);
let response: Buffer;
if (i === 0) {
// initial APDU
const remainingKeysData = utils.uint32_to_buf(paths.length - 1);

return {
publicKeyHex: publicKey.toString("hex"),
chainCodeHex: chainCode.toString("hex")
};
response = await wrapRetryStillInCall(_send)(
P1_INIT, P2_UNUSED,
Buffer.concat([pathData, remainingKeysData])
);
} else {
// next key APDU
response = await _send(
P1_NEXT_KEY, P2_UNUSED,
pathData
);
}

const [publicKey, chainCode, rest] = utils.chunkBy(response, [32, 32]);
Assert.assert(rest.length === 0);

result.push({
publicKeyHex: publicKey.toString("hex"),
chainCodeHex: chainCode.toString("hex")
});
}

return result;
}

/**
* @description Get a public key from the specified BIP 32 path.
*
* @param {BIP32Path} indexes The path indexes. Path must begin with `44'/1815'/n'`, and may be up to 10 indexes long.
* @return {Promise<GetExtendedPublicKeyResponse>} The public key with chaincode for the given path.
*
* @example
* const { publicKey, chainCode } = await ada.getExtendedPublicKey([ HARDENED + 44, HARDENED + 1815, HARDENED + 1 ]);
* console.log(publicKey);
*
*/
async getExtendedPublicKey(
path: BIP32Path
): Promise<GetExtendedPublicKeyResponse> {
return (await this.getExtendedPublicKeys([path]))[0];
}

/**
Expand Down Expand Up @@ -502,22 +541,6 @@ export default class Ada {
Assert.assert(response.length === 0, "response not empty");
}

_isPoolRegistrationSupported(version: GetVersionResponse): boolean {
const major = parseInt(version.major);
const minor = parseInt(version.minor);
const patch = parseInt(version.patch);
if (isNaN(major) || isNaN(minor) || isNaN(patch))
return false;

if (major >= 3) {
return true;
} else if (major === 2) {
return minor >= 1;
} else {
return false;
}
}

async signTransaction(
networkId: number,
protocolMagic: number,
Expand All @@ -542,9 +565,8 @@ export default class Ada {
cert => cert.type === CertificateTypes.STAKE_POOL_REGISTRATION
);

const version = await this._getVersion();
if (isSigningPoolRegistrationAsOwner && !this._isPoolRegistrationSupported(version))
throw new Error(VersionErrors.UNSUPPORTED_POOL_REGISTRATION);
if (isSigningPoolRegistrationAsOwner)
await this._checkLedgerCardanoAppVersion(2, 1);

const P1_STAGE_INIT = 0x01;
const P1_STAGE_INPUTS = 0x02;
Expand Down
17 changes: 11 additions & 6 deletions test/src/direct/getExtendedPublicKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,20 @@ describe("getExtendedPublicKey", async () => {
// This is a sanity check test, to make sure the API did not change
// If it is failing, you have to refactor all other tests
await send(0x00, 0x00, validDataBuffer);

const data = Buffer.concat([validDataBuffer, Buffer.from("00000000", "hex")]);
await send(0x00, 0x00, data);
});

it("Should not permit mismatch between path length and according buffer length", async () => {
const data = Buffer.concat([validDataBuffer, Buffer.from("00", "hex")]);
it("Should not permit buffer with inappropriate length", async () => {
const data1 = Buffer.concat([validDataBuffer, Buffer.from("00", "hex")]);
await checkThrows(0x00, 0x00, data1, ERRORS.INVALID_DATA);

await checkThrows(0x00, 0x00, data, ERRORS.INVALID_DATA);
const data2 = Buffer.concat([validDataBuffer, Buffer.from("1122334455", "hex")]);
await checkThrows(0x00, 0x00, data2, ERRORS.INVALID_DATA);
});

it("Should not permit mismatch between path length and according buffer length", async () => {
it("Should not permit buffer that is too short", async () => {
const data = validDataBuffer.slice(0, -3);

await checkThrows(0x00, 0x00, data, ERRORS.INVALID_DATA);
Expand All @@ -53,13 +58,13 @@ describe("getExtendedPublicKey", async () => {
await checkThrows(p1, p2, validDataBuffer, ERRORS.INVALID_REQUEST_PARAMETERS);

// Invalid P1
await testcase(0x01, 0x00);
await testcase(0x03, 0x00);

// // Invalid P2
await testcase(0x00, 0x01);
});

it("Should not permit path not starting with 44'/1815'", async () => {
it("Should not permit path with wrong/missing first three elements", async () => {
const paths = [
"44'",
"44'/132'/1'",
Expand Down
61 changes: 60 additions & 1 deletion test/src/integration/getExtendedPublicKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("getExtendedPublicKey", async () => {
await ada.t.close();
});

it("Should successfully get extended public key", async () => {
it("Should successfully get a single extended public key", async () => {
const test = async path => {
const derivation = getPathDerivationFixture({
path,
Expand All @@ -36,6 +36,65 @@ describe("getExtendedPublicKey", async () => {
await test("1852'/1815'/0'/2/0");
});

it("Should successfully get several extended public keys, starting with a usual one", async () => {
const paths = [
"44'/1815'/1'",
"44'/1815'/1'/0/10'/1/2/3",
];

const inputs = [];
const expectedResults = [];
for (const path of paths) {
const derivation = getPathDerivationFixture({
path,
});

inputs.push(str_to_path(derivation.path));

expectedResults.push({
publicKeyHex: derivation.publicKey,
chainCodeHex: derivation.chainCode
});
};

const results = await ada.getExtendedPublicKeys(inputs);
for (let i = 0; i < expectedResults.length; i++) {
expect(results[i].publicKeyHex).to.equal(expectedResults[i].publicKeyHex);
expect(results[i].chainCodeHex).to.equal(expectedResults[i].chainCodeHex);
}
});

it("Should successfully get several extended public keys, starting with an unusual one", async () => {
const paths = [
"44'/1815'/1'/0/10'/1/2/3",
"44'/1815'/1'",
"44'/1815'/1'/0/12'",
"1852'/1815'/0'/0/1",
"1852'/1815'/0'/2/0"
];

const inputs = [];
const expectedResults = [];
for (const path of paths) {
const derivation = getPathDerivationFixture({
path,
});

inputs.push(str_to_path(derivation.path));

expectedResults.push({
publicKeyHex: derivation.publicKey,
chainCodeHex: derivation.chainCode
});
};

const results = await ada.getExtendedPublicKeys(inputs);
for (let i = 0; i < expectedResults.length; i++) {
expect(results[i].publicKeyHex).to.equal(expectedResults[i].publicKeyHex);
expect(results[i].chainCodeHex).to.equal(expectedResults[i].chainCodeHex);
}
});

it("Should return the same public key with the same path consistently", async () => {
const path = str_to_path("44'/1815'/1'");

Expand Down