Skip to content
Merged
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
160 changes: 160 additions & 0 deletions packages/hardware-trezor/MASTER_KEY_GENERATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Trezor Master Key Generation Types

This package now supports multiple master key generation algorithms for Trezor hardware wallets, allowing compatibility with different wallet implementations and enabling seamless integration with various existing wallet types.

## Supported Master Key Generation Types

### 1. ICARUS
- Standard CIP-3 wallet compatibility mode

### 2. ICARUS_TREZOR
- CIP-3 variant with 24-word mnemonic compatibility
- This is Trezor's internal default (SDK passes no `derivationType`)

### 3. LEDGER
- Ledger hardware wallet compatibility mode
- Use this if your wallet was originally created on a Ledger device
- Enables access to Ledger-generated accounts on Trezor hardware

## Key Concept

All types use the same derivation path but different master key generation algorithms from the mnemonic. The `derivationType` tells Trezor which algorithm to use.

## Usage

### Importing Types and Constants

All types and constants are fully exportable from their respective packages:

```typescript
// Core constants
import {
HD_WALLET_CIP_ID,
Cardano
} from '@cardano-sdk/core';

// Key management types
import {
TrezorConfig,
KeyPurpose,
MasterKeyGeneration
} from '@cardano-sdk/key-management';

// Hardware Trezor implementation
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
```



### Basic Configuration

```typescript
import { TrezorConfig, MasterKeyGeneration } from '@cardano-sdk/key-management';

const trezorConfig: TrezorConfig = {
communicationType: 'web',
manifest: {
email: '[email protected]',
appUrl: 'https://myapp.com'
},
// Use CIP-3 master key generation
derivationType: 'ICARUS'
};
```



### Creating a Trezor Key Agent

```typescript
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
import { Cardano } from '@cardano-sdk/core';
import { createBip32Ed25519 } from '@cardano-sdk/crypto';

const dependencies = {
logger: console, // or your preferred logger
bip32Ed25519: await createBip32Ed25519()
};

const keyAgent = await TrezorKeyAgent.createWithDevice({
chainId: Cardano.ChainIds.Mainnet,
accountIndex: 0,
trezorConfig: {
communicationType: 'web',
manifest: {
email: '[email protected]',
appUrl: 'https://myapp.com'
},
derivationType: 'ICARUS'
}
}, dependencies);
```



## Discovery & UX Guidelines

### Automatic Discovery Pattern

When pairing a Trezor device, follow this discovery pattern to find existing accounts:

1. **Try ICARUS first** - Most common for software wallets
2. **Try ICARUS_TREZOR** - If balance not found and user has 24-word mnemonic
3. **Try LEDGER** - If user confirms wallet was originally created on Ledger device

This is why many wallet UIs show a "Derivation type override" dropdown - to help users discover their existing accounts.

### Manual Selection During Onboarding

For applications that prefer explicit user control, consider exposing derivation type selection during the onboarding process:

```typescript
// Example onboarding flow
const derivationTypeOptions = [
{ value: 'ICARUS', label: 'Software Wallet', description: 'Most common for wallets created with software applications' },
{ value: 'ICARUS_TREZOR', label: 'Trezor Default', description: 'Trezor\'s internal default (24-word mnemonic compatible)' },
{ value: 'LEDGER', label: 'Ledger Hardware Wallet', description: 'For wallets originally created on Ledger devices' }
];

// Let user select during onboarding
const selectedDerivationType = await showDerivationTypeSelection(derivationTypeOptions);

const keyAgent = await TrezorKeyAgent.createWithDevice({
chainId: Cardano.ChainIds.Mainnet,
trezorConfig: {
communicationType: 'web',
manifest: { email: '[email protected]', appUrl: 'https://myapp.com' },
derivationType: selectedDerivationType
}
}, dependencies);
```

**Benefits of Manual Selection:**
- **User Control**: Users explicitly choose their derivation type
- **Multi-Wallet Support**: Users can create multiple wallets with different derivation types
- **Transparency**: Clear understanding of which derivation type is being used
- **No Guessing**: Eliminates the need for automatic discovery patterns

**When to Use Manual Selection:**
- Applications that prioritize user control and transparency
- Multi-wallet applications where users might have accounts with different derivation types
- Enterprise applications where explicit configuration is preferred
- When users have used multiple wallet types and need to access different accounts

## Implementation Details

When a non-default derivation type is specified, the SDK sets the appropriate `derivationType` in the `cardanoSignTransaction` call. For the default type, no `derivationType` is sent to Trezor.

## Backward Compatibility

Existing wallets without a `derivationType` configuration will continue to work as before. No changes are required for existing users.



## References

- **[CIP-1852](https://cips.cardano.org/cip/CIP-1852)**: HD paths and role meanings
- **[CIP-3](https://cips.cardano.org/cip/CIP-3)**: Master key generation and 24-word compatibility note
- **[Trezor Forum](https://forum.trezor.io/t/cardano-ada-transaction-signing-error/10466)**: Community discussion on derivation types
- **[Cardano Stack Exchange](https://cardano.stackexchange.com/questions/5977/how-does-ledger-generate-the-public-keys)**: Ledger key generation details
- **[Cardano Foundation](https://cardano-foundation.github.io/cardano-wallet/concepts/master-key-generation)**: Master key generation background
13 changes: 12 additions & 1 deletion packages/hardware-trezor/src/TrezorKeyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,19 @@ const isMultiSig = (tx: Omit<Trezor.CardanoSignTransaction, 'signingMode'>): boo
export class TrezorKeyAgent extends KeyAgentBase {
readonly isTrezorInitialized: Promise<boolean>;
readonly #communicationType: CommunicationType;
readonly #trezorConfig: TrezorConfig;

constructor({ isTrezorInitialized, ...serializableData }: TrezorKeyAgentProps, dependencies: KeyAgentDependencies) {
super({ ...serializableData, __typename: KeyAgentType.Trezor }, dependencies);
if (!isTrezorInitialized) {
this.isTrezorInitialized = TrezorKeyAgent.initializeTrezorTransport(serializableData.trezorConfig);
}
this.#communicationType = serializableData.trezorConfig.communicationType;
this.#trezorConfig = serializableData.trezorConfig;
}

get trezorConfig(): TrezorConfig {
return this.#trezorConfig;
}

static async initializeTrezorTransport({
Expand Down Expand Up @@ -275,7 +281,12 @@ export class TrezorKeyAgent extends KeyAgentBase {
...(signingMode === Trezor.PROTO.CardanoTxSigningMode.MULTISIG_TRANSACTION && {
additionalWitnessRequests: multiSigWitnessPaths
}),
signingMode
signingMode,
...(this.trezorConfig.derivationType
? {
derivationType: this.trezorConfig.derivationType as unknown as Trezor.PROTO.CardanoDerivationType
}
: {})
});

const expectedPublicKeys = await Promise.all(
Expand Down
6 changes: 6 additions & 0 deletions packages/key-management/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export enum KeyPurpose {
STANDARD = HD_WALLET_CIP_ID,
MULTI_SIG = MULTISIG_CIP_ID
}

/** Master key generation algorithms supported by Trezor devices */
export type MasterKeyGeneration = 'ICARUS' | 'ICARUS_TREZOR' | 'LEDGER';

export interface AccountKeyDerivationPath {
role: KeyRole;
index: number;
Expand Down Expand Up @@ -94,6 +98,8 @@ export interface TrezorConfig {
};
/** When set to true, Trezor automatically handle passphrase entry by forcing it to occur on the device */
shouldHandlePassphrase?: boolean;
/** Master key generation algorithm to use. Defaults to Trezor's default if not specified */
derivationType?: MasterKeyGeneration;
}

export interface SerializableKeyAgentDataBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,75 @@ const createWallet = async (keyAgent: KeyAgent) => {
const getAddress = async (wallet: ObservableWallet) => (await firstValueFrom(wallet.addresses$))[0].address;

describe('TrezorKeyAgent+BaseWallet', () => {
test('creating and restoring TrezorKeyAgent wallet', async () => {
const keyAgentDependencies = { bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(), logger };
let keyAgentDependencies: { bip32Ed25519: Crypto.Bip32Ed25519; logger: typeof logger };

const TEST_APP_URL = 'https://your.application.com';
const TEST_EMAIL = '[email protected]';

beforeAll(async () => {
keyAgentDependencies = { bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(), logger };
});

test('creating and restoring TrezorKeyAgent wallet with default derivation type', async () => {
const freshKeyAgent = await TrezorKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
trezorConfig: {
communicationType: CommunicationType.Node,
manifest: {
appUrl: TEST_APP_URL,
email: TEST_EMAIL
}
// No derivationType specified - uses Trezor's default (ICARUS_TREZOR)
}
},
keyAgentDependencies
);
const freshWallet = await createWallet(freshKeyAgent);

const restoredKeyAgent = await restoreKeyAgent(freshKeyAgent.serializableData, keyAgentDependencies);
const restoredWallet = await createWallet(restoredKeyAgent);

expect(await getAddress(freshWallet)).toEqual(await getAddress(restoredWallet));
freshWallet.shutdown();
restoredWallet.shutdown();
});

test('creating TrezorKeyAgent wallet with ICARUS derivation type', async () => {
const freshKeyAgent = await TrezorKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
trezorConfig: {
communicationType: CommunicationType.Node,
derivationType: 'ICARUS',
manifest: {
appUrl: TEST_APP_URL,
email: TEST_EMAIL
}
}
},
keyAgentDependencies
);
const freshWallet = await createWallet(freshKeyAgent);

const restoredKeyAgent = await restoreKeyAgent(freshKeyAgent.serializableData, keyAgentDependencies);
const restoredWallet = await createWallet(restoredKeyAgent);

expect(await getAddress(freshWallet)).toEqual(await getAddress(restoredWallet));
freshWallet.shutdown();
restoredWallet.shutdown();
});

test('creating TrezorKeyAgent wallet with LEDGER derivation type', async () => {
const freshKeyAgent = await TrezorKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
trezorConfig: {
communicationType: CommunicationType.Node,
derivationType: 'LEDGER',
manifest: {
appUrl: 'https://your.application.com',
email: '[email protected]'
appUrl: TEST_APP_URL,
email: TEST_EMAIL
}
}
},
Expand All @@ -71,4 +129,71 @@ describe('TrezorKeyAgent+BaseWallet', () => {
freshWallet.shutdown();
restoredWallet.shutdown();
});

test('different derivation types produce different addresses', async () => {
const defaultKeyAgent = await TrezorKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
trezorConfig: {
communicationType: CommunicationType.Node,
manifest: {
appUrl: TEST_APP_URL,
email: TEST_EMAIL
}
}
},
keyAgentDependencies
);

const icarusKeyAgent = await TrezorKeyAgent.createWithDevice(
{
chainId: Cardano.ChainIds.Preprod,
trezorConfig: {
communicationType: CommunicationType.Node,
derivationType: 'ICARUS',
manifest: {
appUrl: TEST_APP_URL,
email: TEST_EMAIL
}
}
},
keyAgentDependencies
);

const defaultWallet = await createWallet(defaultKeyAgent);
const icarusWallet = await createWallet(icarusKeyAgent);

const defaultAddress = await getAddress(defaultWallet);
const icarusAddress = await getAddress(icarusWallet);

// Different derivation types should produce different addresses
expect(defaultAddress).not.toEqual(icarusAddress);

defaultWallet.shutdown();
icarusWallet.shutdown();
});

test('backward compatibility - existing wallets without derivation type continue to work', async () => {
// Simulate an existing wallet configuration (no derivationType)
const existingWalletConfig = {
chainId: Cardano.ChainIds.Preprod,
trezorConfig: {
communicationType: CommunicationType.Node,
manifest: {
appUrl: 'https://your.application.com',
email: '[email protected]'
}
// No derivationType - this is how existing wallets were configured
}
};

const keyAgent = await TrezorKeyAgent.createWithDevice(existingWalletConfig, keyAgentDependencies);
const wallet = await createWallet(keyAgent);

// Should work exactly as before
expect(await getAddress(wallet)).toBeDefined();
expect(keyAgent.trezorConfig.derivationType).toBeUndefined();

wallet.shutdown();
});
});
Loading
Loading