/** * YubiKey signer that handles OpenPGP smart card operations */ import createDebug from 'debug'; import { DEFAULT_OPENPGP_PIN, DEFAULT_SIGNING_TIMEOUT } from '../constants.js'; import { hexToUint8Array, normalizeS, recoverV, uint8ArrayToHex } from './utils.js'; // Import OpenPGPSlot from parent package import type { OpenPGPSlot } from '@l8d/hardhat-yubikey/dist/src/internal/openpgp-apdu.js'; // Import parent package types and functions import type pcsclite from 'pcsclite'; const debug = createDebug('viem-account-yubikey:signer'); /** * YubiKey signer configuration */ export interface YubikeySignerConfig { pin?: string; timeout?: number; } /** * Signature result from YubiKey */ export interface SignatureResult { r: Uint8Array; s: Uint8Array; v: number; } /** * YubiKey signer that uses PC/SC to communicate with the OpenPGP applet */ export class YubikeySigner { private pin: string; private timeout: number; private connection: any = null; // Will hold YubikeyConnection instance constructor(config: YubikeySignerConfig = {}) { this.pin = config.pin || DEFAULT_OPENPGP_PIN; this.timeout = config.timeout || DEFAULT_SIGNING_TIMEOUT; } /** * Connect to YubiKey device */ async connect(): Promise { if (this.connection) { debug('Already connected'); return; } debug('Importing YubiKey connection from parent package...'); try { // Dynamically import from parent package const { YubikeyConnection } = await import( '@l8d/hardhat-yubikey/dist/src/internal/yubikey-connection.js' ); debug('Creating new YubiKey connection...'); this.connection = new YubikeyConnection({ connectionTimeout: this.timeout, }); debug('Connecting to YubiKey...'); await this.connection.connect(); debug('✓ Connected to YubiKey'); } catch (error) { debug('Failed to connect to YubiKey:', error); throw new Error( `Failed to connect to YubiKey: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Disconnect from YubiKey */ async disconnect(): Promise { if (this.connection) { debug('Disconnecting from YubiKey...'); await this.connection.close(); this.connection = null; debug('✓ Disconnected'); } } /** * Sign a hash using the specified OpenPGP slot * @param hash The 32-byte hash to sign * @param slot The OpenPGP slot to use for signing * @param address The Ethereum address (used for v recovery) * @returns Signature with r, s, and v components */ async signHash( hash: `0x${string}`, slot: OpenPGPSlot, address: `0x${string}` ): Promise { if (!this.connection) { throw new Error('Not connected to YubiKey. Call connect() first.'); } debug(`Signing hash with slot ${slot}...`); debug(`Hash: ${hash}`); debug(`Address: ${address}`); try { // Import APDU functions from parent package const { createVerifyPinCommand, createSignCommand, parseECDSASignature, isSuccess, } = await import('@l8d/hardhat-yubikey/dist/src/internal/openpgp-apdu.js'); // Convert hash to Buffer const hashBuffer = Buffer.from(hash.slice(2), 'hex'); if (hashBuffer.length !== 32) { throw new Error(`Invalid hash length: expected 32 bytes, got ${hashBuffer.length}`); } // Verify PIN debug('Verifying PIN...'); const verifyCmd = createVerifyPinCommand(this.pin); const verifyResponse = await this.connection.transmit(verifyCmd); if (!isSuccess(verifyResponse)) { throw new Error( `PIN verification failed: ${verifyResponse.sw1.toString(16)}${verifyResponse.sw2.toString(16)}` ); } debug('✓ PIN verified'); // Create sign command debug('Creating sign command...'); const signCmd = createSignCommand(hashBuffer, slot); // Transmit to YubiKey debug('Transmitting sign command...'); debug('⚠️ Please confirm the operation on your YubiKey if prompted'); const response = await this.connection.transmit(signCmd); debug('✓ Received signature response'); // Parse signature debug('Parsing ECDSA signature...'); let { r: rBuffer, s: sBuffer } = parseECDSASignature(response); // Convert Buffers to Uint8Array const r = new Uint8Array(rBuffer) as Uint8Array; const sOriginal = new Uint8Array(sBuffer) as Uint8Array; debug(`r: ${uint8ArrayToHex(r)}`); debug(`s (original): ${uint8ArrayToHex(sOriginal)}`); // Normalize s to lower half of curve order const s = normalizeS(sOriginal) as Uint8Array; debug(`s (normalized): ${uint8ArrayToHex(s)}`); // Recover v value debug('Recovering v value...'); const hashBytes = hexToUint8Array(hash) as Uint8Array; const v = recoverV(hashBytes, r, s, address); debug(`✓ Recovered v: ${v}`); return { r, s, v }; } catch (error) { debug('Error during signing:', error); throw new Error( `Failed to sign with YubiKey: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get the public key from the specified OpenPGP slot * @param slot The OpenPGP slot to read from * @returns The public key as hex string */ async getPublicKey(slot: OpenPGPSlot): Promise<`0x${string}`> { if (!this.connection) { throw new Error('Not connected to YubiKey. Call connect() first.'); } debug(`Getting public key from slot ${slot}...`); try { // Import APDU functions from parent package const { createGetPublicKeyCommand, isSuccess } = await import( '@l8d/hardhat-yubikey/dist/src/internal/openpgp-apdu.js' ); const cmd = createGetPublicKeyCommand(slot); const response = await this.connection.transmit(cmd); if (!isSuccess(response)) { throw new Error( `Failed to get public key: ${response.sw1.toString(16)}${response.sw2.toString(16)}` ); } // Parse public key from response // The response contains TLV-encoded data with the public key const publicKeyHex = `0x${response.data.toString('hex')}` as `0x${string}`; debug(`✓ Got public key: ${publicKeyHex.slice(0, 20)}...`); return publicKeyHex; } catch (error) { debug('Error getting public key:', error); throw new Error( `Failed to get public key from YubiKey: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Check if currently connected */ isConnected(): boolean { return this.connection !== null && this.connection.isConnected(); } }