Skip to content

Commit b71b169

Browse files
authored
Merge pull request #1650 from input-output-hk/feature/trezor-derivation-schemes
feat: support all Trezor derivation types
2 parents 7af19bc + e6cd80c commit b71b169

File tree

6 files changed

+446
-5
lines changed

6 files changed

+446
-5
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Trezor Master Key Generation Types
2+
3+
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.
4+
5+
## Supported Master Key Generation Types
6+
7+
### 1. ICARUS
8+
- Standard CIP-3 wallet compatibility mode
9+
10+
### 2. ICARUS_TREZOR
11+
- CIP-3 variant with 24-word mnemonic compatibility
12+
- This is Trezor's internal default (SDK passes no `derivationType`)
13+
14+
### 3. LEDGER
15+
- Ledger hardware wallet compatibility mode
16+
- Use this if your wallet was originally created on a Ledger device
17+
- Enables access to Ledger-generated accounts on Trezor hardware
18+
19+
## Key Concept
20+
21+
All types use the same derivation path but different master key generation algorithms from the mnemonic. The `derivationType` tells Trezor which algorithm to use.
22+
23+
## Usage
24+
25+
### Importing Types and Constants
26+
27+
All types and constants are fully exportable from their respective packages:
28+
29+
```typescript
30+
// Core constants
31+
import {
32+
HD_WALLET_CIP_ID,
33+
Cardano
34+
} from '@cardano-sdk/core';
35+
36+
// Key management types
37+
import {
38+
TrezorConfig,
39+
KeyPurpose,
40+
MasterKeyGeneration
41+
} from '@cardano-sdk/key-management';
42+
43+
// Hardware Trezor implementation
44+
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
45+
```
46+
47+
48+
49+
### Basic Configuration
50+
51+
```typescript
52+
import { TrezorConfig, MasterKeyGeneration } from '@cardano-sdk/key-management';
53+
54+
const trezorConfig: TrezorConfig = {
55+
communicationType: 'web',
56+
manifest: {
57+
58+
appUrl: 'https://myapp.com'
59+
},
60+
// Use CIP-3 master key generation
61+
derivationType: 'ICARUS'
62+
};
63+
```
64+
65+
66+
67+
### Creating a Trezor Key Agent
68+
69+
```typescript
70+
import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor';
71+
import { Cardano } from '@cardano-sdk/core';
72+
import { createBip32Ed25519 } from '@cardano-sdk/crypto';
73+
74+
const dependencies = {
75+
logger: console, // or your preferred logger
76+
bip32Ed25519: await createBip32Ed25519()
77+
};
78+
79+
const keyAgent = await TrezorKeyAgent.createWithDevice({
80+
chainId: Cardano.ChainIds.Mainnet,
81+
accountIndex: 0,
82+
trezorConfig: {
83+
communicationType: 'web',
84+
manifest: {
85+
86+
appUrl: 'https://myapp.com'
87+
},
88+
derivationType: 'ICARUS'
89+
}
90+
}, dependencies);
91+
```
92+
93+
94+
95+
## Discovery & UX Guidelines
96+
97+
### Automatic Discovery Pattern
98+
99+
When pairing a Trezor device, follow this discovery pattern to find existing accounts:
100+
101+
1. **Try ICARUS first** - Most common for software wallets
102+
2. **Try ICARUS_TREZOR** - If balance not found and user has 24-word mnemonic
103+
3. **Try LEDGER** - If user confirms wallet was originally created on Ledger device
104+
105+
This is why many wallet UIs show a "Derivation type override" dropdown - to help users discover their existing accounts.
106+
107+
### Manual Selection During Onboarding
108+
109+
For applications that prefer explicit user control, consider exposing derivation type selection during the onboarding process:
110+
111+
```typescript
112+
// Example onboarding flow
113+
const derivationTypeOptions = [
114+
{ value: 'ICARUS', label: 'Software Wallet', description: 'Most common for wallets created with software applications' },
115+
{ value: 'ICARUS_TREZOR', label: 'Trezor Default', description: 'Trezor\'s internal default (24-word mnemonic compatible)' },
116+
{ value: 'LEDGER', label: 'Ledger Hardware Wallet', description: 'For wallets originally created on Ledger devices' }
117+
];
118+
119+
// Let user select during onboarding
120+
const selectedDerivationType = await showDerivationTypeSelection(derivationTypeOptions);
121+
122+
const keyAgent = await TrezorKeyAgent.createWithDevice({
123+
chainId: Cardano.ChainIds.Mainnet,
124+
trezorConfig: {
125+
communicationType: 'web',
126+
manifest: { email: '[email protected]', appUrl: 'https://myapp.com' },
127+
derivationType: selectedDerivationType
128+
}
129+
}, dependencies);
130+
```
131+
132+
**Benefits of Manual Selection:**
133+
- **User Control**: Users explicitly choose their derivation type
134+
- **Multi-Wallet Support**: Users can create multiple wallets with different derivation types
135+
- **Transparency**: Clear understanding of which derivation type is being used
136+
- **No Guessing**: Eliminates the need for automatic discovery patterns
137+
138+
**When to Use Manual Selection:**
139+
- Applications that prioritize user control and transparency
140+
- Multi-wallet applications where users might have accounts with different derivation types
141+
- Enterprise applications where explicit configuration is preferred
142+
- When users have used multiple wallet types and need to access different accounts
143+
144+
## Implementation Details
145+
146+
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.
147+
148+
## Backward Compatibility
149+
150+
Existing wallets without a `derivationType` configuration will continue to work as before. No changes are required for existing users.
151+
152+
153+
154+
## References
155+
156+
- **[CIP-1852](https://cips.cardano.org/cip/CIP-1852)**: HD paths and role meanings
157+
- **[CIP-3](https://cips.cardano.org/cip/CIP-3)**: Master key generation and 24-word compatibility note
158+
- **[Trezor Forum](https://forum.trezor.io/t/cardano-ada-transaction-signing-error/10466)**: Community discussion on derivation types
159+
- **[Cardano Stack Exchange](https://cardano.stackexchange.com/questions/5977/how-does-ledger-generate-the-public-keys)**: Ledger key generation details
160+
- **[Cardano Foundation](https://cardano-foundation.github.io/cardano-wallet/concepts/master-key-generation)**: Master key generation background

packages/hardware-trezor/src/TrezorKeyAgent.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,19 @@ const isMultiSig = (tx: Omit<Trezor.CardanoSignTransaction, 'signingMode'>): boo
9393
export class TrezorKeyAgent extends KeyAgentBase {
9494
readonly isTrezorInitialized: Promise<boolean>;
9595
readonly #communicationType: CommunicationType;
96+
readonly #trezorConfig: TrezorConfig;
9697

9798
constructor({ isTrezorInitialized, ...serializableData }: TrezorKeyAgentProps, dependencies: KeyAgentDependencies) {
9899
super({ ...serializableData, __typename: KeyAgentType.Trezor }, dependencies);
99100
if (!isTrezorInitialized) {
100101
this.isTrezorInitialized = TrezorKeyAgent.initializeTrezorTransport(serializableData.trezorConfig);
101102
}
102103
this.#communicationType = serializableData.trezorConfig.communicationType;
104+
this.#trezorConfig = serializableData.trezorConfig;
105+
}
106+
107+
get trezorConfig(): TrezorConfig {
108+
return this.#trezorConfig;
103109
}
104110

105111
static async initializeTrezorTransport({
@@ -275,7 +281,12 @@ export class TrezorKeyAgent extends KeyAgentBase {
275281
...(signingMode === Trezor.PROTO.CardanoTxSigningMode.MULTISIG_TRANSACTION && {
276282
additionalWitnessRequests: multiSigWitnessPaths
277283
}),
278-
signingMode
284+
signingMode,
285+
...(this.trezorConfig.derivationType
286+
? {
287+
derivationType: this.trezorConfig.derivationType as unknown as Trezor.PROTO.CardanoDerivationType
288+
}
289+
: {})
279290
});
280291

281292
const expectedPublicKeys = await Promise.all(

packages/key-management/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export enum KeyPurpose {
4343
STANDARD = HD_WALLET_CIP_ID,
4444
MULTI_SIG = MULTISIG_CIP_ID
4545
}
46+
47+
/** Master key generation algorithms supported by Trezor devices */
48+
export type MasterKeyGeneration = 'ICARUS' | 'ICARUS_TREZOR' | 'LEDGER';
49+
4650
export interface AccountKeyDerivationPath {
4751
role: KeyRole;
4852
index: number;
@@ -94,6 +98,8 @@ export interface TrezorConfig {
9498
};
9599
/** When set to true, Trezor automatically handle passphrase entry by forcing it to occur on the device */
96100
shouldHandlePassphrase?: boolean;
101+
/** Master key generation algorithm to use. Defaults to Trezor's default if not specified */
102+
derivationType?: MasterKeyGeneration;
97103
}
98104

99105
export interface SerializableKeyAgentDataBase {

packages/wallet/test/hardware/trezor/TrezorKeyAgent.integration.test.ts

Lines changed: 129 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,75 @@ const createWallet = async (keyAgent: KeyAgent) => {
4646
const getAddress = async (wallet: ObservableWallet) => (await firstValueFrom(wallet.addresses$))[0].address;
4747

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

51+
const TEST_APP_URL = 'https://your.application.com';
52+
const TEST_EMAIL = '[email protected]';
53+
54+
beforeAll(async () => {
55+
keyAgentDependencies = { bip32Ed25519: await Crypto.SodiumBip32Ed25519.create(), logger };
56+
});
57+
58+
test('creating and restoring TrezorKeyAgent wallet with default derivation type', async () => {
59+
const freshKeyAgent = await TrezorKeyAgent.createWithDevice(
60+
{
61+
chainId: Cardano.ChainIds.Preprod,
62+
trezorConfig: {
63+
communicationType: CommunicationType.Node,
64+
manifest: {
65+
appUrl: TEST_APP_URL,
66+
email: TEST_EMAIL
67+
}
68+
// No derivationType specified - uses Trezor's default (ICARUS_TREZOR)
69+
}
70+
},
71+
keyAgentDependencies
72+
);
73+
const freshWallet = await createWallet(freshKeyAgent);
74+
75+
const restoredKeyAgent = await restoreKeyAgent(freshKeyAgent.serializableData, keyAgentDependencies);
76+
const restoredWallet = await createWallet(restoredKeyAgent);
77+
78+
expect(await getAddress(freshWallet)).toEqual(await getAddress(restoredWallet));
79+
freshWallet.shutdown();
80+
restoredWallet.shutdown();
81+
});
82+
83+
test('creating TrezorKeyAgent wallet with ICARUS derivation type', async () => {
84+
const freshKeyAgent = await TrezorKeyAgent.createWithDevice(
85+
{
86+
chainId: Cardano.ChainIds.Preprod,
87+
trezorConfig: {
88+
communicationType: CommunicationType.Node,
89+
derivationType: 'ICARUS',
90+
manifest: {
91+
appUrl: TEST_APP_URL,
92+
email: TEST_EMAIL
93+
}
94+
}
95+
},
96+
keyAgentDependencies
97+
);
98+
const freshWallet = await createWallet(freshKeyAgent);
99+
100+
const restoredKeyAgent = await restoreKeyAgent(freshKeyAgent.serializableData, keyAgentDependencies);
101+
const restoredWallet = await createWallet(restoredKeyAgent);
102+
103+
expect(await getAddress(freshWallet)).toEqual(await getAddress(restoredWallet));
104+
freshWallet.shutdown();
105+
restoredWallet.shutdown();
106+
});
107+
108+
test('creating TrezorKeyAgent wallet with LEDGER derivation type', async () => {
52109
const freshKeyAgent = await TrezorKeyAgent.createWithDevice(
53110
{
54111
chainId: Cardano.ChainIds.Preprod,
55112
trezorConfig: {
56113
communicationType: CommunicationType.Node,
114+
derivationType: 'LEDGER',
57115
manifest: {
58-
appUrl: 'https://your.application.com',
59-
116+
appUrl: TEST_APP_URL,
117+
email: TEST_EMAIL
60118
}
61119
}
62120
},
@@ -71,4 +129,71 @@ describe('TrezorKeyAgent+BaseWallet', () => {
71129
freshWallet.shutdown();
72130
restoredWallet.shutdown();
73131
});
132+
133+
test('different derivation types produce different addresses', async () => {
134+
const defaultKeyAgent = await TrezorKeyAgent.createWithDevice(
135+
{
136+
chainId: Cardano.ChainIds.Preprod,
137+
trezorConfig: {
138+
communicationType: CommunicationType.Node,
139+
manifest: {
140+
appUrl: TEST_APP_URL,
141+
email: TEST_EMAIL
142+
}
143+
}
144+
},
145+
keyAgentDependencies
146+
);
147+
148+
const icarusKeyAgent = await TrezorKeyAgent.createWithDevice(
149+
{
150+
chainId: Cardano.ChainIds.Preprod,
151+
trezorConfig: {
152+
communicationType: CommunicationType.Node,
153+
derivationType: 'ICARUS',
154+
manifest: {
155+
appUrl: TEST_APP_URL,
156+
email: TEST_EMAIL
157+
}
158+
}
159+
},
160+
keyAgentDependencies
161+
);
162+
163+
const defaultWallet = await createWallet(defaultKeyAgent);
164+
const icarusWallet = await createWallet(icarusKeyAgent);
165+
166+
const defaultAddress = await getAddress(defaultWallet);
167+
const icarusAddress = await getAddress(icarusWallet);
168+
169+
// Different derivation types should produce different addresses
170+
expect(defaultAddress).not.toEqual(icarusAddress);
171+
172+
defaultWallet.shutdown();
173+
icarusWallet.shutdown();
174+
});
175+
176+
test('backward compatibility - existing wallets without derivation type continue to work', async () => {
177+
// Simulate an existing wallet configuration (no derivationType)
178+
const existingWalletConfig = {
179+
chainId: Cardano.ChainIds.Preprod,
180+
trezorConfig: {
181+
communicationType: CommunicationType.Node,
182+
manifest: {
183+
appUrl: 'https://your.application.com',
184+
185+
}
186+
// No derivationType - this is how existing wallets were configured
187+
}
188+
};
189+
190+
const keyAgent = await TrezorKeyAgent.createWithDevice(existingWalletConfig, keyAgentDependencies);
191+
const wallet = await createWallet(keyAgent);
192+
193+
// Should work exactly as before
194+
expect(await getAddress(wallet)).toBeDefined();
195+
expect(keyAgent.trezorConfig.derivationType).toBeUndefined();
196+
197+
wallet.shutdown();
198+
});
74199
});

0 commit comments

Comments
 (0)