-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ (keyring-btc) [DSDK-464]: Implement GetExtendedPublicKeyCommand (#282)
- Loading branch information
Showing
6 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@ledgerhq/keyring-btc": patch | ||
--- | ||
|
||
Implement GetExtendedPublicKeyCommand |
160 changes: 160 additions & 0 deletions
160
...es/signer/keyring-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import { | ||
ApduResponse, | ||
CommandResultFactory, | ||
InvalidStatusWordError, | ||
isSuccessCommandResult, | ||
UnknownDeviceExchangeError, | ||
} from "@ledgerhq/device-sdk-core"; | ||
|
||
import { | ||
GetExtendedPublicKeyCommand, | ||
type GetExtendedPublicKeyCommandArgs, | ||
} from "./GetExtendedPublicKeyCommand"; | ||
|
||
const GET_EXTENDED_PUBLIC_KEY_APDU_WITH_DISPLAY = new Uint8Array([ | ||
0xe1, 0x00, 0x00, 0x00, 0x0e, 0x01, 0x03, 0x80, 0x00, 0x00, 0x54, 0x80, 0x00, | ||
0x00, 0x00, 0x80, 0x00, 0x00, 0x00, | ||
]); | ||
|
||
const GET_EXTENDED_PUBLIC_KEY_APDU_WITHOUT_DISPLAY = new Uint8Array([ | ||
0xe1, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x03, 0x80, 0x00, 0x00, 0x54, 0x80, 0x00, | ||
0x00, 0x00, 0x80, 0x00, 0x00, 0x00, | ||
]); | ||
|
||
const GET_EXTENDED_PUBLIC_KEY_APDU_WITH_OTHER_DERIVATION_PATH = new Uint8Array([ | ||
0xe1, 0x00, 0x00, 0x00, 0x12, 0x01, 0x04, 0x80, 0x00, 0x00, 0x31, 0x80, 0x00, | ||
0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, | ||
]); | ||
|
||
const GET_EXTENDED_PUBLIC_KEY_VALID_RESPONSE = new Uint8Array([ | ||
0x78, 0x70, 0x75, 0x62, 0x36, 0x44, 0x39, 0x50, 0x70, 0x34, 0x72, 0x46, 0x76, | ||
0x77, 0x54, 0x47, 0x78, 0x38, 0x38, 0x59, 0x44, 0x34, 0x43, 0x44, 0x61, 0x31, | ||
0x6e, 0x42, 0x45, 0x70, 0x63, 0x4b, 0x45, 0x5a, 0x54, 0x43, 0x4e, 0x46, 0x7a, | ||
0x43, 0x46, 0x37, 0x67, 0x56, 0x50, 0x7a, 0x36, 0x54, 0x68, 0x39, 0x42, 0x61, | ||
0x56, 0x68, 0x68, 0x50, 0x4a, 0x44, 0x75, 0x67, 0x39, 0x59, 0x59, 0x46, 0x50, | ||
0x59, 0x6d, 0x6b, 0x53, 0x48, 0x4c, 0x66, 0x52, 0x31, 0x56, 0x51, 0x59, 0x6a, | ||
0x35, 0x6a, 0x61, 0x79, 0x71, 0x77, 0x53, 0x59, 0x41, 0x52, 0x6e, 0x75, 0x42, | ||
0x4a, 0x69, 0x50, 0x53, 0x44, 0x61, 0x62, 0x79, 0x79, 0x54, 0x69, 0x43, 0x44, | ||
0x37, 0x42, 0x33, 0x63, 0x6a, 0x50, 0x71, 0x90, 0x00, | ||
]); | ||
|
||
describe("GetExtendedPublicKeyCommand", () => { | ||
let command: GetExtendedPublicKeyCommand; | ||
const defaultArgs: GetExtendedPublicKeyCommandArgs = { | ||
displayOnDevice: true, | ||
derivationPath: "84'/0'/0'", | ||
}; | ||
|
||
beforeEach(() => {}); | ||
|
||
describe("getApdu", () => { | ||
it("should return the correct APDU", () => { | ||
// GIVEN | ||
command = new GetExtendedPublicKeyCommand(defaultArgs); | ||
|
||
// WHEN | ||
const apdu = command.getApdu(); | ||
|
||
//THEN | ||
expect(apdu.getRawApdu()).toEqual( | ||
GET_EXTENDED_PUBLIC_KEY_APDU_WITH_DISPLAY, | ||
); | ||
}); | ||
|
||
it("should return the correct APDU without display", () => { | ||
// GIVEN | ||
command = new GetExtendedPublicKeyCommand({ | ||
...defaultArgs, | ||
displayOnDevice: false, | ||
}); | ||
|
||
// WHEN | ||
const apdu = command.getApdu(); | ||
|
||
//THEN | ||
expect(apdu.getRawApdu()).toEqual( | ||
GET_EXTENDED_PUBLIC_KEY_APDU_WITHOUT_DISPLAY, | ||
); | ||
}); | ||
|
||
it("should return the correct APDU with different derivation path", () => { | ||
// GIVEN | ||
command = new GetExtendedPublicKeyCommand({ | ||
...defaultArgs, | ||
derivationPath: "49'/0'/0'/0", | ||
}); | ||
|
||
// WHEN | ||
const apdu = command.getApdu(); | ||
|
||
//THEN | ||
expect(apdu.getRawApdu()).toEqual( | ||
GET_EXTENDED_PUBLIC_KEY_APDU_WITH_OTHER_DERIVATION_PATH, | ||
); | ||
}); | ||
}); | ||
|
||
describe("parseResponse", () => { | ||
it("should return the extended public key", () => { | ||
// GIVEN | ||
command = new GetExtendedPublicKeyCommand(defaultArgs); | ||
const response = new ApduResponse({ | ||
data: GET_EXTENDED_PUBLIC_KEY_VALID_RESPONSE, | ||
statusCode: new Uint8Array([0x90, 0x00]), | ||
}); | ||
|
||
// WHEN | ||
const result = command.parseResponse(response); | ||
|
||
// THEN | ||
expect(result).toEqual( | ||
CommandResultFactory({ | ||
data: { | ||
extendedPublicKey: | ||
"xpub6D9Pp4rFvwTGx88YD4CDa1nBEpcKEZTCNFzCF7gVPz6Th9BaVhhPJDug9YYFPYmkSHLfR1VQYj5jayqwSYARnuBJiPSDabyyTiCD7B3cjPq", | ||
}, | ||
}), | ||
); | ||
}); | ||
|
||
it("should return an error if the response is not successful", () => { | ||
// GIVEN | ||
command = new GetExtendedPublicKeyCommand(defaultArgs); | ||
const response = new ApduResponse({ | ||
statusCode: Uint8Array.from([0x6d, 0x00]), | ||
data: new Uint8Array(0), | ||
}); | ||
|
||
// WHEN | ||
const result = command.parseResponse(response); | ||
|
||
// THEN | ||
if (!isSuccessCommandResult(result)) { | ||
expect(result.error).toBeInstanceOf(UnknownDeviceExchangeError); | ||
} else { | ||
fail("Expected an error, but the result was successful"); | ||
} | ||
}); | ||
|
||
it("should return an error if the response is too short", () => { | ||
// GIVEN | ||
command = new GetExtendedPublicKeyCommand(defaultArgs); | ||
const response = new ApduResponse({ | ||
data: GET_EXTENDED_PUBLIC_KEY_VALID_RESPONSE.slice(0, 2), | ||
statusCode: new Uint8Array([0x90, 0x00]), | ||
}); | ||
|
||
// WHEN | ||
const result = command.parseResponse(response); | ||
|
||
// THEN | ||
if (!isSuccessCommandResult(result)) { | ||
expect(result.error).toEqual( | ||
new InvalidStatusWordError("Invalid response length"), | ||
); | ||
} else { | ||
fail("Expected an error, but the result was successful"); | ||
} | ||
}); | ||
}); | ||
}); |
89 changes: 89 additions & 0 deletions
89
packages/signer/keyring-btc/src/internal/app-binder/command/GetExtendedPublicKeyCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/bitcoin.md#get_extended_pubkey | ||
import { | ||
Apdu, | ||
ApduBuilder, | ||
type ApduBuilderArgs, | ||
ApduParser, | ||
ApduResponse, | ||
type Command, | ||
type CommandResult, | ||
CommandResultFactory, | ||
CommandUtils, | ||
GlobalCommandErrorHandler, | ||
} from "@ledgerhq/device-sdk-core"; | ||
import { InvalidStatusWordError } from "@ledgerhq/device-sdk-core"; | ||
|
||
import { DerivationPathUtils } from "@internal/shared/utils/DerivationPathUtils"; | ||
|
||
const STATUS_CODE_LENGTH = 2; | ||
|
||
export type GetExtendedPublicKeyCommandArgs = { | ||
displayOnDevice: boolean; | ||
derivationPath: string; | ||
}; | ||
|
||
export type GetExtendedPublicKeyCommandResponse = { | ||
extendedPublicKey: string; | ||
}; | ||
|
||
export class GetExtendedPublicKeyCommand | ||
implements | ||
Command< | ||
GetExtendedPublicKeyCommandResponse, | ||
GetExtendedPublicKeyCommandArgs | ||
> | ||
{ | ||
constructor(private readonly args: GetExtendedPublicKeyCommandArgs) {} | ||
|
||
getApdu(): Apdu { | ||
const { displayOnDevice, derivationPath } = this.args; | ||
|
||
const getExtendedPublicKeyArgs: ApduBuilderArgs = { | ||
cla: 0xe1, | ||
ins: 0x00, | ||
p1: 0x00, | ||
p2: 0x00, | ||
}; | ||
const builder = new ApduBuilder(getExtendedPublicKeyArgs).add8BitUIntToData( | ||
displayOnDevice ? 0x01 : 0x00, | ||
); | ||
|
||
const path = DerivationPathUtils.splitPath(derivationPath); | ||
builder.add8BitUIntToData(path.length); | ||
path.forEach((element) => { | ||
builder.add32BitUIntToData(element); | ||
}); | ||
|
||
return builder.build(); | ||
} | ||
|
||
parseResponse( | ||
response: ApduResponse, | ||
): CommandResult<GetExtendedPublicKeyCommandResponse> { | ||
const parser = new ApduParser(response); | ||
|
||
if (!CommandUtils.isSuccessResponse(response)) { | ||
return CommandResultFactory({ | ||
error: GlobalCommandErrorHandler.handle(response), | ||
}); | ||
} | ||
|
||
const length = parser.getUnparsedRemainingLength() - STATUS_CODE_LENGTH; | ||
|
||
if (length <= 0) { | ||
return CommandResultFactory({ | ||
error: new InvalidStatusWordError("Invalid response length"), | ||
}); | ||
} | ||
|
||
const extendedPublicKey = parser.encodeToString( | ||
parser.extractFieldByLength(length), | ||
); | ||
|
||
return CommandResultFactory({ | ||
data: { | ||
extendedPublicKey, | ||
}, | ||
}); | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
packages/signer/keyring-btc/src/internal/shared/utils/DerivationPathUtils.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { DerivationPathUtils } from "./DerivationPathUtils"; | ||
|
||
const PADDING = 0x80000000; | ||
|
||
describe("DerivationPathUtils", () => { | ||
it("should split the derivation path", () => { | ||
// GIVEN | ||
const path = "44'/60/0/0/0"; | ||
|
||
// WHEN | ||
const result = DerivationPathUtils.splitPath(path); | ||
|
||
// THEN | ||
expect(result).toStrictEqual([44 + PADDING, 60, 0, 0, 0]); | ||
}); | ||
|
||
it("should split the derivation path with hardened path", () => { | ||
// GIVEN | ||
const path = "44'/60'/0'/0'/1"; | ||
|
||
// WHEN | ||
const result = DerivationPathUtils.splitPath(path); | ||
|
||
// THEN | ||
expect(result).toStrictEqual([ | ||
44 + PADDING, | ||
60 + PADDING, | ||
0 + PADDING, | ||
0 + PADDING, | ||
1, | ||
]); | ||
}); | ||
|
||
it("should split the derivation path with custom path", () => { | ||
// GIVEN | ||
const path = "44'/60'/5/4/3"; | ||
|
||
// WHEN | ||
const result = DerivationPathUtils.splitPath(path); | ||
|
||
// THEN | ||
expect(result).toStrictEqual([44 + PADDING, 60 + PADDING, 5, 4, 3]); | ||
}); | ||
|
||
it("should throw an error if invalid number provided", () => { | ||
// GIVEN | ||
const path = "44'/60'/zzz/4/3"; | ||
|
||
// WHEN | ||
const result = () => DerivationPathUtils.splitPath(path); | ||
|
||
// THEN | ||
expect(result).toThrow(new Error("Invalid number provided")); | ||
}); | ||
}); |
21 changes: 21 additions & 0 deletions
21
packages/signer/keyring-btc/src/internal/shared/utils/DerivationPathUtils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// TODO: Move to shared package and use in both keyring-btc and keyring-eth | ||
|
||
export class DerivationPathUtils { | ||
private static readonly PADDING = 0x80000000; | ||
|
||
static splitPath(path: string): number[] { | ||
const result: number[] = []; | ||
const components = path.split("/"); | ||
components.forEach((element) => { | ||
let number = parseInt(element, 10); | ||
if (isNaN(number)) { | ||
throw new Error("Invalid number provided"); | ||
} | ||
if (element.length > 1 && element[element.length - 1] === "'") { | ||
number += this.PADDING; | ||
} | ||
result.push(number); | ||
}); | ||
return result; | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
packages/signer/keyring-eth/src/internal/shared/utils/DerivationPathUtils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters