UNPKG

@taquito/ledger-signer

Version:

Ledger signer provider

239 lines (219 loc) 8.15 kB
/** * @packageDocumentation * @module @taquito/ledger-signer */ import { Signer } from '@taquito/taquito'; import Transport from '@ledgerhq/hw-transport'; import { b58cencode, invalidDetail, prefix, Prefix, ValidationResult } from '@taquito/utils'; import { appendWatermark, transformPathToBuffer, compressPublicKey, chunkOperation, validateResponse, extractValue, } from './utils'; import { hash } from '@stablelib/blake2b'; import { PublicKeyHashRetrievalError, PublicKeyRetrievalError, InvalidLedgerResponseError, InvalidDerivationTypeError, } from './errors'; import { InvalidDerivationPathError, ProhibitedActionError } from '@taquito/core'; export { InvalidDerivationPathError } from '@taquito/core'; export type LedgerTransport = Pick<Transport, 'send' | 'decorateAppAPIMethods' | 'setScrambleKey'>; export enum DerivationType { ED25519 = 0x00, // tz1 SECP256K1 = 0x01, // tz2 P256 = 0x02, // tz3 BIP32_ED25519 = 0x03, // tz1 BIP32 } export { InvalidDerivationTypeError } from './errors'; export const HDPathTemplate = (account: number) => { return `44'/1729'/${account}'/0'`; }; export { VERSION } from './version'; /** * * @description Implementation of the Signer interface that will allow signing operation from a Ledger Nano device * * @param transport A transport instance from LedgerJS libraries depending on the platform used (e.g. Web, Node) * @param path The ledger derivation path (default is "44'/1729'/0'/0'") * @param prompt Whether to prompt the ledger for public key (default is true) * @param derivationType The value which defines the curve to use (DerivationType.ED25519(default), DerivationType.SECP256K1, DerivationType.P256, DerivationType.BIP32_ED25519) * * @example * ``` * import TransportNodeHid from "@ledgerhq/hw-transport-node-hid"; * const transport = await TransportNodeHid.create(); * const ledgerSigner = new LedgerSigner(transport, "44'/1729'/0'/0'", false, DerivationType.ED25519); * ``` * * @example * ``` * import TransportU2F from "@ledgerhq/hw-transport-u2f"; * const transport = await TransportU2F.create(); * const ledgerSigner = new LedgerSigner(transport, "44'/1729'/0'/0'", true, DerivationType.SECP256K1); * ``` * * @example * ``` * import TransportU2F from "@ledgerhq/hw-transport-u2f"; * const transport = await TransportU2F.create(); * const ledgerSigner = new LedgerSigner(transport, "44'/1729'/6'/0'", true, DerivationType.BIP32_ED25519); * ``` */ export class LedgerSigner implements Signer { // constants for APDU requests (https://github.com/obsidiansystems/ledger-app-tezos/blob/master/APDUs.md) private readonly CLA = 0x80; // Instruction class (always 0x80) private readonly INS_GET_PUBLIC_KEY = 0x02; // Instruction code to get the ledger’s internal public key without prompt private readonly INS_PROMPT_PUBLIC_KEY = 0x03; // Instruction code to get the ledger’s internal public key with prompt private readonly INS_SIGN = 0x04; // Sign a message with the ledger’s key private readonly FIRST_MESSAGE_SEQUENCE = 0x00; private readonly LAST_MESSAGE_SEQUENCE = 0x81; private readonly OTHER_MESSAGE_SEQUENCE = 0x01; private _publicKey?: string; private _publicKeyHash?: string; constructor( private transport: LedgerTransport, private path: string = "44'/1729'/0'/0'", private prompt: boolean = true, private derivationType: DerivationType = DerivationType.ED25519 ) { this.transport.setScrambleKey('XTZ'); if (!path.startsWith(`44'/1729'`)) { throw new InvalidDerivationPathError( path, `${invalidDetail(ValidationResult.NO_PREFIX_MATCHED)} expecting prefix "44'/1729'".` ); } if (!Object.values(DerivationType).includes(derivationType)) { throw new InvalidDerivationTypeError(derivationType.toString()); } } async publicKeyHash(): Promise<string> { if (!this._publicKeyHash) { await this.publicKey(); } if (this._publicKeyHash) { return this._publicKeyHash; } throw new PublicKeyHashRetrievalError(); } async publicKey(): Promise<string> { if (this._publicKey) { return this._publicKey; } const responseLedger = await this.getLedgerPublicKey(); const publicKeyLength = responseLedger[0]; const rawPublicKey = responseLedger.slice(1, 1 + publicKeyLength); const compressedPublicKey = compressPublicKey(rawPublicKey, this.derivationType); const prefixes = this.getPrefixes(); const publicKey = b58cencode(compressedPublicKey, prefixes.prefPk); const publicKeyHash = b58cencode(hash(compressedPublicKey, 20), prefixes.prefPkh); this._publicKey = publicKey; this._publicKeyHash = publicKeyHash; return publicKey; } private async getLedgerPublicKey(): Promise<Buffer> { try { let ins = this.INS_PROMPT_PUBLIC_KEY; if (this.prompt === false) { ins = this.INS_GET_PUBLIC_KEY; } const responseLedger = await this.transport.send( this.CLA, ins, this.FIRST_MESSAGE_SEQUENCE, this.derivationType, transformPathToBuffer(this.path) ); return responseLedger; } catch (error) { throw new PublicKeyRetrievalError(error); } } async secretKey(): Promise<string> { throw new ProhibitedActionError('Secret key cannot be exposed'); } async sign(bytes: string, watermark?: Uint8Array) { const watermarkedBytes = appendWatermark(bytes, watermark); const watermarkedBytes2buff = Buffer.from(watermarkedBytes, 'hex'); let messageToSend = []; messageToSend.push(transformPathToBuffer(this.path)); messageToSend = chunkOperation(messageToSend, watermarkedBytes2buff); const ledgerResponse = await this.signWithLedger(messageToSend); let signature; if ( this.derivationType === DerivationType.ED25519 || this.derivationType === DerivationType.BIP32_ED25519 ) { signature = ledgerResponse.slice(0, ledgerResponse.length - 2).toString('hex'); } else { if (!validateResponse(ledgerResponse)) { throw new InvalidLedgerResponseError( 'Invalid signature return by ledger unable to parse the response' ); } const idxLengthRVal = 3; // Third element of response is length of r value const rValue = extractValue(idxLengthRVal, ledgerResponse); const idxLengthSVal = rValue.idxValueStart + rValue.length + 1; const sValue = extractValue(idxLengthSVal, ledgerResponse); const signatureBuffer = Buffer.concat([rValue.buffer, sValue.buffer]); signature = signatureBuffer.toString('hex'); } return { bytes, sig: b58cencode(signature, prefix[Prefix.SIG]), prefixSig: b58cencode(signature, this.getPrefixes().prefSig), sbytes: bytes + signature, }; } private async signWithLedger(message: Buffer[]): Promise<Buffer> { // first element of the message represents the path let ledgerResponse = await this.transport.send( this.CLA, this.INS_SIGN, this.FIRST_MESSAGE_SEQUENCE, this.derivationType, message[0] ); for (let i = 1; i < message.length; i++) { const p1 = i === message.length - 1 ? this.LAST_MESSAGE_SEQUENCE : this.OTHER_MESSAGE_SEQUENCE; ledgerResponse = await this.transport.send( this.CLA, this.INS_SIGN, p1, this.derivationType, message[i] ); } return ledgerResponse; } private getPrefixes() { if ( this.derivationType === DerivationType.ED25519 || this.derivationType === DerivationType.BIP32_ED25519 ) { return { prefPk: prefix[Prefix.EDPK], prefPkh: prefix[Prefix.TZ1], prefSig: prefix[Prefix.EDSIG], }; } else if (this.derivationType === DerivationType.SECP256K1) { return { prefPk: prefix[Prefix.SPPK], prefPkh: prefix[Prefix.TZ2], prefSig: prefix[Prefix.SPSIG], }; } else { return { prefPk: prefix[Prefix.P2PK], prefPkh: prefix[Prefix.TZ3], prefSig: prefix[Prefix.P2SIG], }; } } }