Skip to content

Commit 2527c15

Browse files
authored
feat: allow separate valkeys passwords. (#24460)
Fix A-1360. Stacked on top of #24459
1 parent 08d124b commit 2527c15

5 files changed

Lines changed: 251 additions & 27 deletions

File tree

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,8 @@ This creates keystores at:
397397
### Encrypted Keystore Passwords
398398

399399
Use `--password`, `--password-file`, or `AZTEC_KEYSTORE_PASSWORD` to write encrypted ETH JSON V3 and BLS
400-
EIP-2335 keystore files:
400+
EIP-2335 keystore files with a shared password. To use different passwords, provide role-specific sources such as
401+
`--eth-password` and `--bls-password`.
401402

402403
```bash
403404
export AZTEC_KEYSTORE_PASSWORD='your-secure-password'
@@ -408,9 +409,12 @@ aztec validator-keys new \
408409
--encrypted-keystore-dir ~/.aztec/keystore/encrypted
409410
```
410411

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.
412+
For each key type, role-specific sources take precedence over shared sources: `--eth-password`,
413+
`--eth-password-file`, and `AZTEC_ETH_KEYSTORE_PASSWORD` for ETH keystores; `--bls-password`,
414+
`--bls-password-file`, and `AZTEC_BLS_KEYSTORE_PASSWORD` for BLS keystores. Shared sources use `--password`,
415+
`--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be empty. When the command generates a mnemonic
416+
while writing encrypted keystores, it does not print the mnemonic to stdout; provide `--mnemonic` if you need deterministic
417+
recovery.
414418

415419
## Verifying Your Keystore
416420

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ aztec validator-keys new [options]
372372
| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` |
373373
| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set |
374374
| `--password-file <path>` | File containing the shared encrypted-keystore password | None |
375+
| `--eth-password <str>` | Password for encrypted ETH keystore files | `AZTEC_ETH_KEYSTORE_PASSWORD`, then shared password |
376+
| `--eth-password-file <path>` | File containing the ETH encrypted-keystore password | None |
377+
| `--bls-password <str>` | Password for encrypted BLS keystore files | `AZTEC_BLS_KEYSTORE_PASSWORD`, then shared password |
378+
| `--bls-password-file <path>` | File containing the BLS encrypted-keystore password | None |
375379
| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory |
376380
| `--data-dir <path>` | Output directory | `~/.aztec/keystore` |
377381
| `--file <name>` | Keystore filename | `key1.json` |

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,23 @@ export function injectCommands(program: Command, log: LogFn) {
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})`)
4141
.addOption(
42-
new Option('--password <str>', 'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335)').env(
42+
new Option('--password <str>', 'Shared password for writing ETH JSON V3 and BLS EIP-2335 keystore files').env(
4343
'AZTEC_KEYSTORE_PASSWORD',
4444
),
4545
)
46-
.option('--password-file <path>', 'File containing the password for writing keystore files')
46+
.option('--password-file <path>', 'File containing the shared password for writing keystore files')
47+
.addOption(
48+
new Option('--eth-password <str>', 'Password for writing ETH JSON V3 keystore files').env(
49+
'AZTEC_ETH_KEYSTORE_PASSWORD',
50+
),
51+
)
52+
.option('--eth-password-file <path>', 'File containing the password for writing ETH JSON V3 keystore files')
53+
.addOption(
54+
new Option('--bls-password <str>', 'Password for writing BLS EIP-2335 keystore files').env(
55+
'AZTEC_BLS_KEYSTORE_PASSWORD',
56+
),
57+
)
58+
.option('--bls-password-file <path>', 'File containing the password for writing BLS EIP-2335 keystore files')
4759
.option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
4860
.option('--json', 'Echo resulting JSON to stdout')
4961
.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: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ export type NewValidatorKeystoreOptions = {
4343
blsPath?: string;
4444
password?: string;
4545
passwordFile?: string;
46+
ethPassword?: string;
47+
ethPasswordFile?: string;
48+
blsPassword?: string;
49+
blsPasswordFile?: string;
4650
encryptedKeystoreDir?: string;
4751
json?: boolean;
4852
feeRecipient: AztecAddress;
@@ -54,6 +58,18 @@ export type NewValidatorKeystoreOptions = {
5458
l1ChainId?: number;
5559
};
5660

61+
type PasswordSourceOptions = Pick<
62+
NewValidatorKeystoreOptions,
63+
'password' | 'passwordFile' | 'ethPassword' | 'ethPasswordFile' | 'blsPassword' | 'blsPasswordFile'
64+
>;
65+
66+
type PasswordSource = {
67+
password?: string;
68+
passwordFile?: string;
69+
passwordOption: string;
70+
passwordFileOption: string;
71+
};
72+
5773
function validatePassword(password: string, source: string): string {
5874
if (password.length === 0) {
5975
throw new Error(`${source} cannot be empty`);
@@ -71,24 +87,57 @@ function stripOneTrailingNewline(password: string): string {
7187
return password;
7288
}
7389

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');
90+
async function resolvePasswordSource(source: PasswordSource): Promise<string | undefined> {
91+
if (source.password !== undefined && source.passwordFile !== undefined) {
92+
throw new Error(`${source.passwordOption} and ${source.passwordFileOption} cannot be used together`);
7793
}
78-
if (options.password !== undefined) {
79-
return validatePassword(options.password, '--password');
94+
if (source.password !== undefined) {
95+
return validatePassword(source.password, source.passwordOption);
8096
}
81-
if (options.passwordFile !== undefined) {
82-
if (options.passwordFile.length === 0) {
83-
throw new Error('--password-file cannot be empty');
97+
if (source.passwordFile !== undefined) {
98+
if (source.passwordFile.length === 0) {
99+
throw new Error(`${source.passwordFileOption} cannot be empty`);
84100
}
85-
const password = stripOneTrailingNewline(await readFile(options.passwordFile, 'utf-8'));
86-
return validatePassword(password, '--password-file');
101+
const password = stripOneTrailingNewline(await readFile(source.passwordFile, 'utf-8'));
102+
return validatePassword(password, source.passwordFileOption);
87103
}
88104

89105
return undefined;
90106
}
91107

108+
async function resolvePasswords(options: PasswordSourceOptions) {
109+
const sharedPassword = await resolvePasswordSource({
110+
password: options.password,
111+
passwordFile: options.passwordFile,
112+
passwordOption: '--password',
113+
passwordFileOption: '--password-file',
114+
});
115+
const ethPassword =
116+
(await resolvePasswordSource({
117+
password: options.ethPassword,
118+
passwordFile: options.ethPasswordFile,
119+
passwordOption: '--eth-password',
120+
passwordFileOption: '--eth-password-file',
121+
})) ?? sharedPassword;
122+
const blsPassword =
123+
(await resolvePasswordSource({
124+
password: options.blsPassword,
125+
passwordFile: options.blsPasswordFile,
126+
passwordOption: '--bls-password',
127+
passwordFileOption: '--bls-password-file',
128+
})) ?? sharedPassword;
129+
130+
if (ethPassword === undefined && blsPassword === undefined) {
131+
return {};
132+
}
133+
if (ethPassword === undefined || blsPassword === undefined) {
134+
throw new Error(
135+
'Both ETH and BLS passwords are required when writing encrypted keystores. Provide --password to use one password for both, or provide both --eth-password and --bls-password.',
136+
);
137+
}
138+
return { ethPassword, blsPassword };
139+
}
140+
92141
export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, log: LogFn) {
93142
// validate bls-path inputs before proceeding with key generation
94143
validateBlsPathOptions(options);
@@ -116,15 +165,26 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
116165
mnemonic: _mnemonic,
117166
password: passwordOption,
118167
passwordFile,
168+
ethPassword: ethPasswordOption,
169+
ethPasswordFile,
170+
blsPassword: blsPasswordOption,
171+
blsPasswordFile,
119172
encryptedKeystoreDir,
120173
stakerOutput,
121174
gseAddress,
122175
l1RpcUrls,
123176
l1ChainId,
124177
} = options;
125178

126-
const password = await resolvePassword({ password: passwordOption, passwordFile });
127-
const shouldEncryptKeystores = password !== undefined;
179+
const { ethPassword, blsPassword } = await resolvePasswords({
180+
password: passwordOption,
181+
passwordFile,
182+
ethPassword: ethPasswordOption,
183+
ethPasswordFile,
184+
blsPassword: blsPasswordOption,
185+
blsPasswordFile,
186+
});
187+
const shouldEncryptKeystores = ethPassword !== undefined && blsPassword !== undefined;
128188
const mnemonic = _mnemonic ?? generateMnemonic(wordlist);
129189

130190
if (!_mnemonic && !json && !shouldEncryptKeystores) {
@@ -157,8 +217,8 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
157217
if (shouldEncryptKeystores) {
158218
const encryptedKeystoreOutDir =
159219
encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir;
160-
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
161-
await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
220+
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password: ethPassword });
221+
await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password: blsPassword });
162222
}
163223

164224
const keystore = {
@@ -184,7 +244,7 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
184244
// Process each validator
185245
for (let i = 0; i < validators.length; i++) {
186246
const validator = validators[i];
187-
const outputs = await processAttesterAccounts(validator.attester, gse, password);
247+
const outputs = await processAttesterAccounts(validator.attester, gse);
188248

189249
// Collect all staker outputs
190250
for (let j = 0; j < outputs.length; j++) {

yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,31 @@ function getEncryptedAttester(keystore: KeyStore) {
3939
describe('validator keys utilities', () => {
4040
let tmp: string;
4141
let feeRecipient: AztecAddress;
42-
let originalKeystorePassword: string | undefined;
42+
let originalKeystorePasswords: Record<string, string | undefined>;
4343

4444
beforeAll(async () => {
4545
feeRecipient = await AztecAddress.random();
4646
tmp = mkdtempSync(join(tmpdir(), 'aztec-valkeys-'));
4747
});
4848

4949
beforeEach(() => {
50-
originalKeystorePassword = process.env.AZTEC_KEYSTORE_PASSWORD;
50+
originalKeystorePasswords = {
51+
AZTEC_KEYSTORE_PASSWORD: process.env.AZTEC_KEYSTORE_PASSWORD,
52+
AZTEC_ETH_KEYSTORE_PASSWORD: process.env.AZTEC_ETH_KEYSTORE_PASSWORD,
53+
AZTEC_BLS_KEYSTORE_PASSWORD: process.env.AZTEC_BLS_KEYSTORE_PASSWORD,
54+
};
5155
delete process.env.AZTEC_KEYSTORE_PASSWORD;
56+
delete process.env.AZTEC_ETH_KEYSTORE_PASSWORD;
57+
delete process.env.AZTEC_BLS_KEYSTORE_PASSWORD;
5258
});
5359

5460
afterEach(() => {
55-
if (originalKeystorePassword === undefined) {
56-
delete process.env.AZTEC_KEYSTORE_PASSWORD;
57-
} else {
58-
process.env.AZTEC_KEYSTORE_PASSWORD = originalKeystorePassword;
61+
for (const [key, value] of Object.entries(originalKeystorePasswords)) {
62+
if (value === undefined) {
63+
delete process.env[key];
64+
} else {
65+
process.env[key] = value;
66+
}
5967
}
6068
});
6169

@@ -593,6 +601,142 @@ describe('validator keys utilities', () => {
593601
).rejects.toThrow(/--password and --password-file cannot be used together/);
594602
});
595603

604+
it('writes ETH and BLS keystores with different passwords', async () => {
605+
const path = join(tmp, 'separate-passwords.json');
606+
const ethPassword = 'eth-password-123';
607+
const blsPassword = 'bls-password-123';
608+
const logs: string[] = [];
609+
const log = (s: string) => logs.push(s);
610+
611+
await newValidatorKeystore(
612+
{
613+
dataDir: tmp,
614+
file: 'separate-passwords.json',
615+
count: 1,
616+
mnemonic: TEST_MNEMONIC,
617+
ethPassword,
618+
blsPassword,
619+
encryptedKeystoreDir: tmp,
620+
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
621+
},
622+
log,
623+
);
624+
625+
const att = getEncryptedAttester(loadKeystoreFile(path));
626+
expect(att.eth.password).toBe(ethPassword);
627+
expect(att.bls.password).toBe(blsPassword);
628+
});
629+
630+
it('uses role-specific passwords over the shared password', async () => {
631+
const path = join(tmp, 'shared-with-bls-override.json');
632+
const sharedPassword = 'shared-password-123';
633+
const blsPassword = 'bls-password-override';
634+
const logs: string[] = [];
635+
const log = (s: string) => logs.push(s);
636+
637+
await newValidatorKeystore(
638+
{
639+
dataDir: tmp,
640+
file: 'shared-with-bls-override.json',
641+
count: 1,
642+
mnemonic: TEST_MNEMONIC,
643+
password: sharedPassword,
644+
blsPassword,
645+
encryptedKeystoreDir: tmp,
646+
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
647+
},
648+
log,
649+
);
650+
651+
const att = getEncryptedAttester(loadKeystoreFile(path));
652+
expect(att.eth.password).toBe(sharedPassword);
653+
expect(att.bls.password).toBe(blsPassword);
654+
});
655+
656+
it('reads ETH and BLS passwords from role-specific environment variables through command options', async () => {
657+
const path = join(tmp, 'role-env-passwords.json');
658+
const ethPassword = 'eth-env-password-123';
659+
const blsPassword = 'bls-env-password-123';
660+
process.env.AZTEC_ETH_KEYSTORE_PASSWORD = ethPassword;
661+
process.env.AZTEC_BLS_KEYSTORE_PASSWORD = blsPassword;
662+
const logs: string[] = [];
663+
const log = (s: string) => logs.push(s);
664+
const program = new Command();
665+
program.exitOverride();
666+
program
667+
.command('new')
668+
.addOption(new Option('--eth-password <str>').env('AZTEC_ETH_KEYSTORE_PASSWORD'))
669+
.addOption(new Option('--bls-password <str>').env('AZTEC_BLS_KEYSTORE_PASSWORD'))
670+
.option('--data-dir <path>')
671+
.option('--file <name>')
672+
.option('--mnemonic <mnemonic>')
673+
.option('--encrypted-keystore-dir <dir>')
674+
.action(async options => {
675+
await newValidatorKeystore({ ...options, feeRecipient }, log);
676+
});
677+
678+
await program.parseAsync(
679+
[
680+
'new',
681+
'--data-dir',
682+
tmp,
683+
'--file',
684+
'role-env-passwords.json',
685+
'--mnemonic',
686+
TEST_MNEMONIC,
687+
'--encrypted-keystore-dir',
688+
tmp,
689+
],
690+
{ from: 'user' },
691+
);
692+
693+
const att = getEncryptedAttester(loadKeystoreFile(path));
694+
expect(att.eth.password).toBe(ethPassword);
695+
expect(att.bls.password).toBe(blsPassword);
696+
});
697+
698+
it('rejects role-specific password sources without a password for the other role', async () => {
699+
const logs: string[] = [];
700+
const log = (s: string) => logs.push(s);
701+
702+
await expect(
703+
newValidatorKeystore(
704+
{
705+
dataDir: tmp,
706+
file: 'only-eth-password.json',
707+
count: 1,
708+
mnemonic: TEST_MNEMONIC,
709+
ethPassword: 'eth-password-123',
710+
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
711+
},
712+
log,
713+
),
714+
).rejects.toThrow(/Both ETH and BLS passwords are required/);
715+
});
716+
717+
it('rejects conflicting role-specific password sources', async () => {
718+
const ethPasswordFile = join(tmp, 'conflicting-eth-password.txt');
719+
writeFileSync(ethPasswordFile, 'eth-file-password', 'utf-8');
720+
const logs: string[] = [];
721+
const log = (s: string) => logs.push(s);
722+
723+
await expect(
724+
newValidatorKeystore(
725+
{
726+
dataDir: tmp,
727+
file: 'conflicting-eth-password-source.json',
728+
count: 1,
729+
mnemonic: TEST_MNEMONIC,
730+
ethPassword: 'eth-password-123',
731+
ethPasswordFile,
732+
blsPassword: 'bls-password-123',
733+
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
734+
},
735+
log,
736+
),
737+
).rejects.toThrow(/--eth-password and --eth-password-file cannot be used together/);
738+
});
739+
596740
it('does not output a generated mnemonic when writing encrypted keystores', async () => {
597741
const logs: string[] = [];
598742
const log = (s: string) => logs.push(s);

0 commit comments

Comments
 (0)