Ledger ECDSA signing support (#1986)

* Ledger ECDSA signing support

* Updates inline docs

* linter
This commit is contained in:
Tarik Gul
2025-05-18 21:12:41 -07:00
committed by GitHub
parent 802174d6ad
commit 35f9f4468b
3 changed files with 103 additions and 53 deletions

View File

@@ -22,7 +22,7 @@
"dependencies": {
"@polkadot/hw-ledger-transports": "13.4.4",
"@polkadot/util": "13.4.4",
"@zondax/ledger-substrate": "1.0.0",
"@zondax/ledger-substrate": "1.1.1",
"tslib": "^2.8.0"
}
}

View File

@@ -46,8 +46,8 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
return result;
}
/** @internal Wraps a sign/signRaw call and returns the associated signature */
function sign (method: 'sign' | 'signRaw', message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
/** @internal Wraps a signEd25519/signRawEd25519 call and returns the associated signature */
function sign (method: 'signEd25519' | 'signRawEd25519', message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
const bip42Path = `m/44'/${slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
return async (app: PolkadotGenericApp): Promise<LedgerSignature> => {
@@ -59,7 +59,22 @@ function sign (method: 'sign' | 'signRaw', message: Uint8Array, slip44: number,
};
}
/** @internal Wraps a signWithMetadata call and returns the associated signature */
/** @internal Wraps a signEcdsa/signRawEcdsa call and returns the associated signature */
function signEcdsa (method: 'signEcdsa' | 'signRawEcdsa', message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
const bip42Path = `m/44'/${slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
return async (app: PolkadotGenericApp): Promise<LedgerSignature> => {
const { r, s, v } = await wrapError(app[method](bip42Path, u8aToBuffer(message)));
const signature = Buffer.concat([r, s, v]);
return {
signature: hexAddPrefix(signature.toString('hex'))
};
};
}
/** @internal Wraps a signWithMetadataEd25519 call and returns the associated signature */
function signWithMetadata (message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0, { metadata }: Partial<AccountOptionsGeneric> = {}): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
const bip42Path = `m/44'/${slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
@@ -70,7 +85,28 @@ function signWithMetadata (message: Uint8Array, slip44: number, accountIndex = 0
const bufferMsg = Buffer.from(message);
const { signature } = await wrapError(app.signWithMetadata(bip42Path, bufferMsg, metadata));
const { signature } = await wrapError(app.signWithMetadataEd25519(bip42Path, bufferMsg, metadata));
return {
signature: hexAddPrefix(signature.toString('hex'))
};
};
}
/** @internal Wraps a signWithMetadataEcdsa call and returns the associated signature */
function signWithMetadataEcdsa (message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0, { metadata }: Partial<AccountOptionsGeneric> = {}): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
const bip42Path = `m/44'/${slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
return async (app: PolkadotGenericApp): Promise<LedgerSignature> => {
if (!metadata) {
throw new Error('The metadata option must be present when using signWithMetadata');
}
const bufferMsg = Buffer.from(message);
const { r, s, v } = await wrapError(app.signWithMetadataEcdsa(bip42Path, bufferMsg, metadata));
const signature = Buffer.concat([r, s, v]);
return {
signature: hexAddPrefix(signature.toString('hex'))
@@ -119,14 +155,31 @@ export class LedgerGeneric {
}
/**
* @description Returns the address associated with a specific account & address offset. Optionally
* @description Returns the address associated with a specific Ed25519 account & address offset. Optionally
* asks for on-device confirmation
*/
public async getAddress (ss58Prefix: number, confirm = false, accountIndex = 0, addressOffset = 0): Promise<LedgerAddress> {
const bip42Path = `m/44'/${this.#slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
return this.withApp(async (app: PolkadotGenericApp): Promise<LedgerAddress> => {
const { address, pubKey } = await wrapError(app.getAddress(bip42Path, ss58Prefix, confirm));
const { address, pubKey } = await wrapError(app.getAddressEd25519(bip42Path, ss58Prefix, confirm));
return {
address,
publicKey: hexAddPrefix(pubKey)
};
});
}
/**
* @description Returns the address associated with a specific ecdsa account & address offset. Optionally
* asks for on-device confirmation
*/
public async getAddressEcdsa (confirm = false, accountIndex = 0, addressOffset = 0) {
const bip42Path = `m/44'/${this.#slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
return this.withApp(async (app: PolkadotGenericApp): Promise<LedgerAddress> => {
const { address, pubKey } = await wrapError(app.getAddressEcdsa(bip42Path, confirm));
return {
address,
@@ -154,14 +207,28 @@ export class LedgerGeneric {
* @description Signs a transaction on the Ledger device. This requires the LedgerGeneric class to be instantiated with `chainId`, and `metaUrl`
*/
public async sign (message: Uint8Array, accountIndex?: number, addressOffset?: number): Promise<LedgerSignature> {
return this.withApp(sign('sign', message, this.#slip44, accountIndex, addressOffset));
return this.withApp(sign('signEd25519', message, this.#slip44, accountIndex, addressOffset));
}
/**
* @description Signs a message (non-transactional) on the Ledger device
*/
public async signRaw (message: Uint8Array, accountIndex?: number, addressOffset?: number): Promise<LedgerSignature> {
return this.withApp(sign('signRaw', u8aWrapBytes(message), this.#slip44, accountIndex, addressOffset));
return this.withApp(sign('signRawEd25519', u8aWrapBytes(message), this.#slip44, accountIndex, addressOffset));
}
/**
* @description Signs a transaction on the Ledger device with Ecdsa. This requires the LedgerGeneric class to be instantiated with `chainId`, and `metaUrl`
*/
public async signEcdsa (message: Uint8Array, accountIndex?: number, addressOffset?: number): Promise<LedgerSignature> {
return this.withApp(signEcdsa('signEcdsa', u8aWrapBytes(message), this.#slip44, accountIndex, addressOffset));
}
/**
* @description Signs a message with Ecdsa (non-transactional) on the Ledger device
*/
public async signRawEcdsa (message: Uint8Array, accountIndex?: number, addressOffset?: number): Promise<LedgerSignature> {
return this.withApp(signEcdsa('signRawEcdsa', u8aWrapBytes(message), this.#slip44, accountIndex, addressOffset));
}
/**
@@ -171,6 +238,13 @@ export class LedgerGeneric {
return this.withApp(signWithMetadata(message, this.#slip44, accountIndex, addressOffset, options));
}
/**
* @description Signs a transaction on the ledger device for an ecdsa signature provided some metadata.
*/
public async signWithMetadataEcdsa (message: Uint8Array, accountIndex?: number, addressOffset?: number, options?: Partial<AccountOptionsGeneric>) {
return this.withApp(signWithMetadataEcdsa(message, this.#slip44, accountIndex, addressOffset, options));
}
/**
* @internal
*

View File

@@ -1207,7 +1207,7 @@ __metadata:
languageName: node
linkType: hard
"@ledgerhq/devices@npm:^8.3.0, @ledgerhq/devices@npm:^8.4.2, @ledgerhq/devices@npm:^8.4.4":
"@ledgerhq/devices@npm:^8.4.4":
version: 8.4.4
resolution: "@ledgerhq/devices@npm:8.4.4"
dependencies:
@@ -1219,7 +1219,7 @@ __metadata:
languageName: node
linkType: hard
"@ledgerhq/errors@npm:^6.16.4, @ledgerhq/errors@npm:^6.18.0, @ledgerhq/errors@npm:^6.19.1":
"@ledgerhq/errors@npm:^6.19.1":
version: 6.19.1
resolution: "@ledgerhq/errors@npm:6.19.1"
checksum: 10/8265c6d73c314a4aabbe060ec29e2feebb4e904fe811bf7a9c53cde08e713dcbceded9d927ebb2f0ffc47a7b16524379d4a7e9aa3d61945b8a832be7cd5cf69b
@@ -1278,31 +1278,7 @@ __metadata:
languageName: node
linkType: hard
"@ledgerhq/hw-transport@npm:6.30.6":
version: 6.30.6
resolution: "@ledgerhq/hw-transport@npm:6.30.6"
dependencies:
"@ledgerhq/devices": "npm:^8.3.0"
"@ledgerhq/errors": "npm:^6.16.4"
"@ledgerhq/logs": "npm:^6.12.0"
events: "npm:^3.3.0"
checksum: 10/4fc71409e1ff906e479f2e45c386a9db2d062271908a86d09a59587652d485b80200a5335e7e9dbaf5264c0af4b8e6c36d24e7db921b10f4cc420b51e9ca3c00
languageName: node
linkType: hard
"@ledgerhq/hw-transport@npm:6.31.2":
version: 6.31.2
resolution: "@ledgerhq/hw-transport@npm:6.31.2"
dependencies:
"@ledgerhq/devices": "npm:^8.4.2"
"@ledgerhq/errors": "npm:^6.18.0"
"@ledgerhq/logs": "npm:^6.12.0"
events: "npm:^3.3.0"
checksum: 10/b712b0c5a409d8bca254daa16b2867110132d44382dfb758e4ab0d710656d85c6b3fd9e42565ec9959db9a7f57b430d04d9db5556fb64727d7dc0a8e7a92d165
languageName: node
linkType: hard
"@ledgerhq/hw-transport@npm:^6.31.4":
"@ledgerhq/hw-transport@npm:6.31.4, @ledgerhq/hw-transport@npm:^6.31.4":
version: 6.31.4
resolution: "@ledgerhq/hw-transport@npm:6.31.4"
dependencies:
@@ -1620,7 +1596,7 @@ __metadata:
dependencies:
"@polkadot/hw-ledger-transports": "npm:13.4.4"
"@polkadot/util": "npm:13.4.4"
"@zondax/ledger-substrate": "npm:1.0.0"
"@zondax/ledger-substrate": "npm:1.1.1"
tslib: "npm:^2.8.0"
languageName: unknown
linkType: soft
@@ -3213,23 +3189,23 @@ __metadata:
languageName: node
linkType: hard
"@zondax/ledger-js@npm:^0.11.0":
version: 0.11.0
resolution: "@zondax/ledger-js@npm:0.11.0"
"@zondax/ledger-js@npm:^1.2.0":
version: 1.2.0
resolution: "@zondax/ledger-js@npm:1.2.0"
dependencies:
"@ledgerhq/hw-transport": "npm:6.30.6"
checksum: 10/52a5e2aa66db1554365d733b3d38f286a0bbb192e1a2911c526f44f058a2e6c1fc6e0ca67a6dc824bb89ce694bd4554c39809d0290141de97675bb48677c3985
"@ledgerhq/hw-transport": "npm:6.31.4"
checksum: 10/5852b340efd421f96d4e52d0a93d4e8464604c070939dd42fade5a495010a08d730b03e1b7ab3ff0f26f13075d8a0a5226a748a41986f2ed087fca4471f24e4e
languageName: node
linkType: hard
"@zondax/ledger-substrate@npm:1.0.0":
version: 1.0.0
resolution: "@zondax/ledger-substrate@npm:1.0.0"
"@zondax/ledger-substrate@npm:1.1.1":
version: 1.1.1
resolution: "@zondax/ledger-substrate@npm:1.1.1"
dependencies:
"@ledgerhq/hw-transport": "npm:6.31.2"
"@zondax/ledger-js": "npm:^0.11.0"
axios: "npm:^1.7.4"
checksum: 10/5324da85746c160dd2400286c38ff0f90fae0147ad7e275b382b20b50acdee99c57941366531dc663931da5cb41144770888e3c6250b3f632b097d8bb7c7182f
"@ledgerhq/hw-transport": "npm:6.31.4"
"@zondax/ledger-js": "npm:^1.2.0"
axios: "npm:^1.8.4"
checksum: 10/039ffef55b31870f82c6b0f20b518089ad9ce0295d695f880640cb586256adacd4a326c10157ccaf82e897abde7358f9e4e02c069dac672ec90881e21d250722
languageName: node
linkType: hard
@@ -3771,14 +3747,14 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.7.4":
version: 1.7.7
resolution: "axios@npm:1.7.7"
"axios@npm:^1.8.4":
version: 1.9.0
resolution: "axios@npm:1.9.0"
dependencies:
follow-redirects: "npm:^1.15.6"
form-data: "npm:^4.0.0"
proxy-from-env: "npm:^1.1.0"
checksum: 10/7f875ea13b9298cd7b40fd09985209f7a38d38321f1118c701520939de2f113c4ba137832fe8e3f811f99a38e12c8225481011023209a77b0c0641270e20cde1
checksum: 10/a2f90bba56820883879f32a237e2b9ff25c250365dcafd41cec41b3406a3df334a148f90010182dfdadb4b41dc59f6f0b3e8898ff41b666d1157b5f3f4523497
languageName: node
linkType: hard