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
12 changes: 8 additions & 4 deletions docs/docs-operate/operators/keystore/creating-keystores.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,8 @@ This creates keystores at:
### Encrypted Keystore Passwords

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of using "role" here but I can't come up with something better rn 😅

`--eth-password` and `--bls-password`.

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

Password precedence is `--password`, then `--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be
empty. When the command generates a mnemonic while writing encrypted keystores, it does not print the mnemonic to stdout;
provide `--mnemonic` if you need deterministic recovery.
For each key type, role-specific sources take precedence over shared sources: `--eth-password`,
`--eth-password-file`, and `AZTEC_ETH_KEYSTORE_PASSWORD` for ETH keystores; `--bls-password`,
`--bls-password-file`, and `AZTEC_BLS_KEYSTORE_PASSWORD` for BLS keystores. Shared sources use `--password`,
`--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be empty. When the command generates a mnemonic
while writing encrypted keystores, it does not print the mnemonic to stdout; provide `--mnemonic` if you need deterministic
recovery.

## Verifying Your Keystore

Expand Down
4 changes: 4 additions & 0 deletions docs/docs-operate/operators/keystore/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ aztec validator-keys new [options]
| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` |
| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set |
| `--password-file <path>` | File containing the shared encrypted-keystore password | None |
| `--eth-password <str>` | Password for encrypted ETH keystore files | `AZTEC_ETH_KEYSTORE_PASSWORD`, then shared password |
| `--eth-password-file <path>` | File containing the ETH encrypted-keystore password | None |
| `--bls-password <str>` | Password for encrypted BLS keystore files | `AZTEC_BLS_KEYSTORE_PASSWORD`, then shared password |
| `--bls-password-file <path>` | File containing the BLS encrypted-keystore password | None |
| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory |
| `--data-dir <path>` | Output directory | `~/.aztec/keystore` |
| `--file <name>` | Keystore filename | `key1.json` |
Expand Down
16 changes: 14 additions & 2 deletions yarn-project/cli/src/cmds/validator_keys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,23 @@ export function injectCommands(program: Command, log: LogFn) {
.option('--ikm <hex>', 'Initial keying material for BLS (alternative to mnemonic)', value => parseHex(value, 32))
.option('--bls-path <path>', `EIP-2334 path (default ${defaultBlsPath})`)
.addOption(
new Option('--password <str>', 'Password for writing keystore files (ETH JSON V3 and BLS EIP-2335)').env(
new Option('--password <str>', 'Shared password for writing ETH JSON V3 and BLS EIP-2335 keystore files').env(
'AZTEC_KEYSTORE_PASSWORD',
),
)
.option('--password-file <path>', 'File containing the password for writing keystore files')
.option('--password-file <path>', 'File containing the shared password for writing keystore files')
.addOption(
new Option('--eth-password <str>', 'Password for writing ETH JSON V3 keystore files').env(
'AZTEC_ETH_KEYSTORE_PASSWORD',
),
)
.option('--eth-password-file <path>', 'File containing the password for writing ETH JSON V3 keystore files')
.addOption(
new Option('--bls-password <str>', 'Password for writing BLS EIP-2335 keystore files').env(
'AZTEC_BLS_KEYSTORE_PASSWORD',
),
)
.option('--bls-password-file <path>', 'File containing the password for writing BLS EIP-2335 keystore files')
.option('--encrypted-keystore-dir <dir>', 'Output directory for encrypted keystore file(s)')
.option('--json', 'Echo resulting JSON to stdout')
.option('--staker-output', 'Generate a single staker output JSON file with an array of validator entries')
Expand Down
90 changes: 75 additions & 15 deletions yarn-project/cli/src/cmds/validator_keys/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export type NewValidatorKeystoreOptions = {
blsPath?: string;
password?: string;
passwordFile?: string;
ethPassword?: string;
ethPasswordFile?: string;
blsPassword?: string;
blsPasswordFile?: string;
encryptedKeystoreDir?: string;
json?: boolean;
feeRecipient: AztecAddress;
Expand All @@ -54,6 +58,18 @@ export type NewValidatorKeystoreOptions = {
l1ChainId?: number;
};

type PasswordSourceOptions = Pick<
NewValidatorKeystoreOptions,
'password' | 'passwordFile' | 'ethPassword' | 'ethPasswordFile' | 'blsPassword' | 'blsPasswordFile'
>;

type PasswordSource = {
password?: string;
passwordFile?: string;
passwordOption: string;
passwordFileOption: string;
};

function validatePassword(password: string, source: string): string {
if (password.length === 0) {
throw new Error(`${source} cannot be empty`);
Expand All @@ -71,24 +87,57 @@ function stripOneTrailingNewline(password: string): string {
return password;
}

async function resolvePassword(options: Pick<NewValidatorKeystoreOptions, 'password' | 'passwordFile'>) {
if (options.password !== undefined && options.passwordFile !== undefined) {
throw new Error('--password and --password-file cannot be used together');
async function resolvePasswordSource(source: PasswordSource): Promise<string | undefined> {
if (source.password !== undefined && source.passwordFile !== undefined) {
throw new Error(`${source.passwordOption} and ${source.passwordFileOption} cannot be used together`);
}
if (options.password !== undefined) {
return validatePassword(options.password, '--password');
if (source.password !== undefined) {
return validatePassword(source.password, source.passwordOption);
}
if (options.passwordFile !== undefined) {
if (options.passwordFile.length === 0) {
throw new Error('--password-file cannot be empty');
if (source.passwordFile !== undefined) {
if (source.passwordFile.length === 0) {
throw new Error(`${source.passwordFileOption} cannot be empty`);
}
const password = stripOneTrailingNewline(await readFile(options.passwordFile, 'utf-8'));
return validatePassword(password, '--password-file');
const password = stripOneTrailingNewline(await readFile(source.passwordFile, 'utf-8'));
return validatePassword(password, source.passwordFileOption);
}

return undefined;
}

async function resolvePasswords(options: PasswordSourceOptions) {
const sharedPassword = await resolvePasswordSource({
password: options.password,
passwordFile: options.passwordFile,
passwordOption: '--password',
passwordFileOption: '--password-file',
});
const ethPassword =
(await resolvePasswordSource({
password: options.ethPassword,
passwordFile: options.ethPasswordFile,
passwordOption: '--eth-password',
passwordFileOption: '--eth-password-file',
})) ?? sharedPassword;
const blsPassword =
(await resolvePasswordSource({
password: options.blsPassword,
passwordFile: options.blsPasswordFile,
passwordOption: '--bls-password',
passwordFileOption: '--bls-password-file',
})) ?? sharedPassword;

if (ethPassword === undefined && blsPassword === undefined) {
return {};
}
if (ethPassword === undefined || blsPassword === undefined) {
throw new Error(
'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.',
);
}
return { ethPassword, blsPassword };
}

export async function newValidatorKeystore(options: NewValidatorKeystoreOptions, log: LogFn) {
// validate bls-path inputs before proceeding with key generation
validateBlsPathOptions(options);
Expand Down Expand Up @@ -116,15 +165,26 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
mnemonic: _mnemonic,
password: passwordOption,
passwordFile,
ethPassword: ethPasswordOption,
ethPasswordFile,
blsPassword: blsPasswordOption,
blsPasswordFile,
encryptedKeystoreDir,
stakerOutput,
gseAddress,
l1RpcUrls,
l1ChainId,
} = options;

const password = await resolvePassword({ password: passwordOption, passwordFile });
const shouldEncryptKeystores = password !== undefined;
const { ethPassword, blsPassword } = await resolvePasswords({
password: passwordOption,
passwordFile,
ethPassword: ethPasswordOption,
ethPasswordFile,
blsPassword: blsPasswordOption,
blsPasswordFile,
});
const shouldEncryptKeystores = ethPassword !== undefined && blsPassword !== undefined;
const mnemonic = _mnemonic ?? generateMnemonic(wordlist);

if (!_mnemonic && !json && !shouldEncryptKeystores) {
Expand Down Expand Up @@ -157,8 +217,8 @@ export async function newValidatorKeystore(options: NewValidatorKeystoreOptions,
if (shouldEncryptKeystores) {
const encryptedKeystoreOutDir =
encryptedKeystoreDir && encryptedKeystoreDir.length > 0 ? encryptedKeystoreDir : keystoreOutDir;
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password });
await writeEthJsonV3ToFile(validators, { outDir: encryptedKeystoreOutDir, password: ethPassword });
await writeBlsBn254ToFile(validators, { outDir: encryptedKeystoreOutDir, password: blsPassword });
}

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

// Collect all staker outputs
for (let j = 0; j < outputs.length; j++) {
Expand Down
156 changes: 150 additions & 6 deletions yarn-project/cli/src/cmds/validator_keys/valkeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,31 @@ function getEncryptedAttester(keystore: KeyStore) {
describe('validator keys utilities', () => {
let tmp: string;
let feeRecipient: AztecAddress;
let originalKeystorePassword: string | undefined;
let originalKeystorePasswords: Record<string, string | undefined>;

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

beforeEach(() => {
originalKeystorePassword = process.env.AZTEC_KEYSTORE_PASSWORD;
originalKeystorePasswords = {
AZTEC_KEYSTORE_PASSWORD: process.env.AZTEC_KEYSTORE_PASSWORD,
AZTEC_ETH_KEYSTORE_PASSWORD: process.env.AZTEC_ETH_KEYSTORE_PASSWORD,
AZTEC_BLS_KEYSTORE_PASSWORD: process.env.AZTEC_BLS_KEYSTORE_PASSWORD,
};
delete process.env.AZTEC_KEYSTORE_PASSWORD;
delete process.env.AZTEC_ETH_KEYSTORE_PASSWORD;
delete process.env.AZTEC_BLS_KEYSTORE_PASSWORD;
});

afterEach(() => {
if (originalKeystorePassword === undefined) {
delete process.env.AZTEC_KEYSTORE_PASSWORD;
} else {
process.env.AZTEC_KEYSTORE_PASSWORD = originalKeystorePassword;
for (const [key, value] of Object.entries(originalKeystorePasswords)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});

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

it('writes ETH and BLS keystores with different passwords', async () => {
const path = join(tmp, 'separate-passwords.json');
const ethPassword = 'eth-password-123';
const blsPassword = 'bls-password-123';
const logs: string[] = [];
const log = (s: string) => logs.push(s);

await newValidatorKeystore(
{
dataDir: tmp,
file: 'separate-passwords.json',
count: 1,
mnemonic: TEST_MNEMONIC,
ethPassword,
blsPassword,
encryptedKeystoreDir: tmp,
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
},
log,
);

const att = getEncryptedAttester(loadKeystoreFile(path));
expect(att.eth.password).toBe(ethPassword);
expect(att.bls.password).toBe(blsPassword);
});

it('uses role-specific passwords over the shared password', async () => {
const path = join(tmp, 'shared-with-bls-override.json');
const sharedPassword = 'shared-password-123';
const blsPassword = 'bls-password-override';
const logs: string[] = [];
const log = (s: string) => logs.push(s);

await newValidatorKeystore(
{
dataDir: tmp,
file: 'shared-with-bls-override.json',
count: 1,
mnemonic: TEST_MNEMONIC,
password: sharedPassword,
blsPassword,
encryptedKeystoreDir: tmp,
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
},
log,
);

const att = getEncryptedAttester(loadKeystoreFile(path));
expect(att.eth.password).toBe(sharedPassword);
expect(att.bls.password).toBe(blsPassword);
});

it('reads ETH and BLS passwords from role-specific environment variables through command options', async () => {
const path = join(tmp, 'role-env-passwords.json');
const ethPassword = 'eth-env-password-123';
const blsPassword = 'bls-env-password-123';
process.env.AZTEC_ETH_KEYSTORE_PASSWORD = ethPassword;
process.env.AZTEC_BLS_KEYSTORE_PASSWORD = blsPassword;
const logs: string[] = [];
const log = (s: string) => logs.push(s);
const program = new Command();
program.exitOverride();
program
.command('new')
.addOption(new Option('--eth-password <str>').env('AZTEC_ETH_KEYSTORE_PASSWORD'))
.addOption(new Option('--bls-password <str>').env('AZTEC_BLS_KEYSTORE_PASSWORD'))
.option('--data-dir <path>')
.option('--file <name>')
.option('--mnemonic <mnemonic>')
.option('--encrypted-keystore-dir <dir>')
.action(async options => {
await newValidatorKeystore({ ...options, feeRecipient }, log);
});

await program.parseAsync(
[
'new',
'--data-dir',
tmp,
'--file',
'role-env-passwords.json',
'--mnemonic',
TEST_MNEMONIC,
'--encrypted-keystore-dir',
tmp,
],
{ from: 'user' },
);

const att = getEncryptedAttester(loadKeystoreFile(path));
expect(att.eth.password).toBe(ethPassword);
expect(att.bls.password).toBe(blsPassword);
});

it('rejects role-specific password sources without a password for the other role', async () => {
const logs: string[] = [];
const log = (s: string) => logs.push(s);

await expect(
newValidatorKeystore(
{
dataDir: tmp,
file: 'only-eth-password.json',
count: 1,
mnemonic: TEST_MNEMONIC,
ethPassword: 'eth-password-123',
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
},
log,
),
).rejects.toThrow(/Both ETH and BLS passwords are required/);
});

it('rejects conflicting role-specific password sources', async () => {
const ethPasswordFile = join(tmp, 'conflicting-eth-password.txt');
writeFileSync(ethPasswordFile, 'eth-file-password', 'utf-8');
const logs: string[] = [];
const log = (s: string) => logs.push(s);

await expect(
newValidatorKeystore(
{
dataDir: tmp,
file: 'conflicting-eth-password-source.json',
count: 1,
mnemonic: TEST_MNEMONIC,
ethPassword: 'eth-password-123',
ethPasswordFile,
blsPassword: 'bls-password-123',
feeRecipient: ('0x' + '07'.repeat(32)) as unknown as AztecAddress,
},
log,
),
).rejects.toThrow(/--eth-password and --eth-password-file cannot be used together/);
});

it('does not output a generated mnemonic when writing encrypted keystores', async () => {
const logs: string[] = [];
const log = (s: string) => logs.push(s);
Expand Down
Loading