Skip to content

Commit 4ddfd74

Browse files
committed
fix: improve valkeys password handling (A-1359)
1 parent 831e809 commit 4ddfd74

5 files changed

Lines changed: 289 additions & 17 deletions

File tree

docs/docs-operate/operators/keystore/creating-keystores.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,24 @@ This creates keystores at:
394394
- **Directory**: `~/.aztec/keystore/`
395395
- **Filename**: `key1.json`, `key2.json`, etc. (auto-increments)
396396

397+
### Encrypted Keystore Passwords
398+
399+
Use `--password`, `--password-file`, or `AZTEC_KEYSTORE_PASSWORD` to write encrypted ETH JSON V3 and BLS
400+
EIP-2335 keystore files:
401+
402+
```bash
403+
export AZTEC_KEYSTORE_PASSWORD='your-secure-password'
404+
aztec validator-keys new \
405+
--fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \
406+
--count 5 \
407+
--publishers 0x7988a4a779f058a0 \
408+
--encrypted-keystore-dir ~/.aztec/keystore/encrypted
409+
```
410+
411+
Password precedence is `--password`, then `--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be
412+
empty. When the command generates a mnemonic while writing encrypted keystores, it does not print the mnemonic to stdout;
413+
provide `--mnemonic` if you need deterministic recovery.
414+
397415
## Verifying Your Keystore
398416

399417
Verify the keystore is valid JSON:

docs/docs-operate/operators/keystore/troubleshooting.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,9 @@ aztec validator-keys new [options]
370370
| `--gse-address <address>` | GSE contract address (required with --staker-output) | None |
371371
| `--l1-rpc-urls <urls>` | L1 RPC endpoints (required with --staker-output) | None |
372372
| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` |
373+
| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set |
374+
| `--password-file <path>` | File containing the shared encrypted-keystore password | None |
375+
| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory |
373376
| `--data-dir <path>` | Output directory | `~/.aztec/keystore` |
374377
| `--file <name>` | Keystore filename | `key1.json` |
375378

yarn-project/cli/src/cmds/validator_keys/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LogFn } from '@aztec/foundation/log';
22

3-
import { Command } from 'commander';
3+
import { Command, Option } from 'commander';
44

55
import { parseAztecAddress, parseEthereumAddress, parseHex, parseOptionalInteger } from '../../utils/commands.js';
66
import { defaultBlsPath } from './utils.js';
@@ -38,10 +38,12 @@ export function injectCommands(program: Command, log: LogFn) {
3838
.option('--remote-signer <url>', 'Default remote signer URL for accounts in this file')
3939
.option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
4040
.option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
41-
.option(
42-
'--password <str>',
43-
'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335). Empty string allowed',
41+
.addOption(
42+
new Option('--password <str>', 'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335)').env(
43+
'AZTEC_KEYSTORE_PASSWORD',
44+
),
4445
)
46+
.option('--password-file <path>', 'File containing the password for writing keystore files')
4547
.option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
4648
.option('--json', 'Echo resulting JSON to stdout')
4749
.option('--staker-output', 'Generate a single staker output JSON file with an array of validator entries')

yarn-project/cli/src/cmds/validator_keys/new.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { LogFn } from '@aztec/foundation/log';
66
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
77

88
import { wordlist } from '@scure/bip39/wordlists/english.js';
9-
import { writeFile } from 'fs/promises';
9+
import { readFile, writeFile } from 'fs/promises';
1010
import { basename, dirname, join } from 'path';
1111
import { createPublicClient, fallback, http } from 'viem';
1212
import { generateMnemonic, mnemonicToAccount } from 'viem/accounts';
@@ -42,6 +42,7 @@ export type NewValidatorKeystoreOptions = {
4242
ikm?: string;
4343
blsPath?: string;
4444
password?: string;
45+
passwordFile?: string;
4546
encryptedKeystoreDir?: string;
4647
json?: boolean;
4748
feeRecipient: AztecAddress;
@@ -53,6 +54,41 @@ export type NewValidatorKeystoreOptions = {
5354
l1ChainId?: number;
5455
};
5556

57+
function validatePassword(password: string, source: string): string {
58+
if (password.length === 0) {
59+
throw new Error(`${source} cannot be empty`);
60+
}
61+
return password;
62+
}
63+
64+
function stripOneTrailingNewline(password: string): string {
65+
if (password.endsWith('\n')) {
66+
password = password.slice(0, -1);
67+
}
68+
if (password.endsWith('\r')) {
69+
password = password.slice(0, -1);
70+
}
71+
return password;
72+
}
73+
74+
async function resolvePassword(options: Pick<NewValidatorKeystoreOptions, 'password' | 'passwordFile'>) {
75+
if (options.password !== undefined && options.passwordFile !== undefined) {
76+
throw new Error('--password and --password-file cannot be used together');
77+
}
78+
if (options.password !== undefined) {
79+
return validatePassword(options.password, '--password');
80+
}
81+
if (options.passwordFile !== undefined) {
82+
if (options.passwordFile.length === 0) {
83+
throw new Error('--password-file cannot be empty');
84+
}
85+
const password = stripOneTrailingNewline(await readFile(options.passwordFile, 'utf-8'));
86+
return validatePassword(password, '--password-file');
87+
}
88+
89+
return undefined;
90+
}
91+
5692
export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, log: LogFn) {
5793
// validate bls-path inputs before proceeding with key generation
5894
validateBlsPathOptions(options);
@@ -78,17 +114,20 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
78114
blsPath,
79115
ikm,
80116
mnemonic: _mnemonic,
81-
password,
117+
password: passwordOption,
118+
passwordFile,
82119
encryptedKeystoreDir,
83120
stakerOutput,
84121
gseAddress,
85122
l1RpcUrls,
86123
l1ChainId,
87124
} = options;
88125

126+
const password = await resolvePassword({ password: passwordOption, passwordFile });
127+
const shouldEncryptKeystores = password !== undefined;
89128
const mnemonic = _mnemonic ?? generateMnemonic(wordlist);
90129

91-
if (!_mnemonic && !json) {
130+
if (!_mnemonic && !json && !shouldEncryptKeystores) {
92131
log('No mnemonic provided, generating new one...');
93132
log(`Using new mnemonic:`);
94133
log('');
@@ -115,7 +154,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
115154
});
116155

117156
// If password provided, write ETH JSON V3 and BLS BN254 keystores and replace plaintext
118-
if (password !== undefined) {
157+
if (shouldEncryptKeystores) {
119158
const encryptedKeystoreOutDir =
120159
encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir;
121160
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
@@ -160,7 +199,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
160199
}
161200
}
162201

163-
const outputData = !_mnemonic ? { ...keystore, generatedMnemonic: mnemonic } : keystore;
202+
const outputData = !_mnemonic && !shouldEncryptKeystores ? { ...keystore, generatedMnemonic: mnemonic } : keystore;
164203

165204
// Handle JSON output
166205
if (json) {

0 commit comments

Comments
 (0)