use std::fs;
use std::path::{Path, PathBuf};
use clap::{ArgGroup, ValueEnum};
use miden_client::auth::{AuthSchemeId, AuthSecretKey, PublicKeyCommitment};
use miden_client::crypto::{ecdsa_k256_keccak, rpo_falcon512};
use miden_client::keystore::FilesystemKeyStore;
use miden_client::utils::{ByteReader, Deserializable, hex_to_bytes};
use miden_client::{SliceReader, Word};
use crate::codecs::parse_account_id_token;
use crate::errors::CliError;
use crate::{Parser, create_dynamic_table};
const ECDSA_PUBLIC_KEY_BYTES: usize = 33;
const FALCON_PUBLIC_KEY_BYTES: usize = 897;
const FALCON_SCHEME_NAME: &str = "falcon512-poseidon2";
const ECDSA_SCHEME_NAME: &str = "ecdsa-k256-keccak";
#[derive(Clone, Copy, Debug, ValueEnum)]
enum KeyScheme {
#[value(name = FALCON_SCHEME_NAME)]
Falcon512Poseidon2,
#[value(name = ECDSA_SCHEME_NAME)]
EcdsaK256Keccak,
}
impl KeyScheme {
fn name(self) -> &'static str {
match self {
Self::Falcon512Poseidon2 => FALCON_SCHEME_NAME,
Self::EcdsaK256Keccak => ECDSA_SCHEME_NAME,
}
}
}
impl From<KeyScheme> for AuthSchemeId {
fn from(value: KeyScheme) -> Self {
match value {
KeyScheme::Falcon512Poseidon2 => Self::Falcon512Poseidon2,
KeyScheme::EcdsaK256Keccak => Self::EcdsaK256Keccak,
}
}
}
impl TryFrom<AuthSchemeId> for KeyScheme {
type Error = ();
fn try_from(value: AuthSchemeId) -> Result<Self, Self::Error> {
match value {
AuthSchemeId::Falcon512Poseidon2 => Ok(Self::Falcon512Poseidon2),
AuthSchemeId::EcdsaK256Keccak => Ok(Self::EcdsaK256Keccak),
_ => Err(()),
}
}
}
#[derive(Clone, Debug, Parser)]
#[command(
about = "Manage authentication keys. Defaults to --list",
group(ArgGroup::new("action").args([
"list",
"generate",
"import",
"commitment",
"associate",
"disassociate",
])),
group(ArgGroup::new("association_action").args(["associate", "disassociate"])),
)]
pub struct KeysCmd {
#[arg(long)]
list: bool,
#[arg(long, value_name = "SCHEME")]
generate: Option<KeyScheme>,
#[arg(long, value_name = "FILE")]
import: Option<PathBuf>,
#[arg(long, value_name = "PUBLIC_KEY")]
commitment: Option<String>,
#[arg(long, value_name = "COMMITMENT", requires = "account_id")]
associate: Option<String>,
#[arg(long, value_name = "COMMITMENT", requires = "account_id")]
disassociate: Option<String>,
#[arg(long, value_name = "ACCOUNT_ID", requires = "association_action")]
account_id: Option<String>,
}
impl KeysCmd {
pub fn execute(&self, keystore: &FilesystemKeyStore) -> Result<(), CliError> {
match self {
Self { generate: Some(scheme), .. } => generate_key(keystore, *scheme),
Self { import: Some(file), .. } => import_key(keystore, file),
Self { commitment: Some(public_key), .. } => print_commitment(public_key),
Self {
associate: Some(commitment),
account_id: Some(account_id),
..
} => associate_key(keystore, commitment, account_id),
Self {
disassociate: Some(commitment),
account_id: Some(account_id),
..
} => disassociate_key(keystore, commitment, account_id),
_ => list_keys(keystore),
}
}
}
fn list_keys(keystore: &FilesystemKeyStore) -> Result<(), CliError> {
let mut table = create_dynamic_table(&["Commitment", "Scheme", "Associated accounts"]);
for key in keystore.list_keys().map_err(CliError::KeyStore)? {
let account_ids = if key.account_ids.is_empty() {
"-".to_string()
} else {
key.account_ids
.iter()
.map(|account_id| account_id.to_hex())
.collect::<Vec<_>>()
.join(", ")
};
table.add_row(vec![
Word::from(key.commitment).to_hex(),
scheme_name(key.scheme),
account_ids,
]);
}
println!("\n{table}");
Ok(())
}
fn associate_key(
keystore: &FilesystemKeyStore,
commitment: &str,
account_id: &str,
) -> Result<(), CliError> {
let commitment = parse_commitment(commitment)?;
let account_id = parse_account_id_token(account_id)?;
keystore.associate_key(commitment, account_id).map_err(CliError::KeyStore)?;
println!(
"Associated key {} with account {}.",
Word::from(commitment).to_hex(),
account_id.to_hex()
);
Ok(())
}
fn disassociate_key(
keystore: &FilesystemKeyStore,
commitment: &str,
account_id: &str,
) -> Result<(), CliError> {
let commitment = parse_commitment(commitment)?;
let account_id = parse_account_id_token(account_id)?;
let removed = keystore.disassociate_key(commitment, account_id).map_err(CliError::KeyStore)?;
if removed {
println!(
"Association between key {} and account {} removed.",
Word::from(commitment).to_hex(),
account_id.to_hex()
);
} else {
println!(
"Key {} wasn't associated with account {}.",
Word::from(commitment).to_hex(),
account_id.to_hex()
);
}
Ok(())
}
fn generate_key(keystore: &FilesystemKeyStore, scheme: KeyScheme) -> Result<(), CliError> {
let key = AuthSecretKey::with_scheme(scheme.into())
.map_err(|err| CliError::Input(format!("failed to generate key: {err}")))?;
store_and_report_key(keystore, &key, "Generated")
}
fn import_key(keystore: &FilesystemKeyStore, file: &Path) -> Result<(), CliError> {
let bytes = fs::read(file)?;
let mut reader = SliceReader::new(&bytes);
let key = AuthSecretKey::read_from(&mut reader).map_err(|err| {
CliError::Input(format!(
"failed to decode authentication secret key from {}: {err}",
file.display()
))
})?;
if reader.has_more_bytes() {
return Err(CliError::Input(format!(
"authentication secret key in {} contains trailing bytes",
file.display()
)));
}
store_and_report_key(keystore, &key, "Imported")
}
fn store_and_report_key(
keystore: &FilesystemKeyStore,
key: &AuthSecretKey,
action: &str,
) -> Result<(), CliError> {
keystore.store_key(key).map_err(CliError::KeyStore)?;
let commitment = Word::from(key.public_key().to_commitment()).to_hex();
println!("{action} {} key.", scheme_name(key.auth_scheme()));
println!("Public key commitment: {commitment}");
Ok(())
}
fn print_commitment(public_key: &str) -> Result<(), CliError> {
let encoded_key = public_key.strip_prefix("0x").ok_or_else(|| {
CliError::Input("public key must use a 0x-prefixed hexadecimal encoding".to_string())
})?;
let scheme = match encoded_key.len() {
length if length == ECDSA_PUBLIC_KEY_BYTES * 2 => KeyScheme::EcdsaK256Keccak,
length if length == FALCON_PUBLIC_KEY_BYTES * 2 => KeyScheme::Falcon512Poseidon2,
length => {
return Err(CliError::Input(format!(
"unsupported public key length: expected {} or {} hexadecimal digits, got {}",
ECDSA_PUBLIC_KEY_BYTES * 2,
FALCON_PUBLIC_KEY_BYTES * 2,
length
)));
},
};
let commitment = match scheme {
KeyScheme::Falcon512Poseidon2 => {
let bytes = hex_to_bytes::<FALCON_PUBLIC_KEY_BYTES>(public_key)
.map_err(|err| invalid_public_key(scheme, err))?;
rpo_falcon512::PublicKey::read_from_bytes(&bytes)
.map_err(|err| invalid_public_key(scheme, err))?
.to_commitment()
},
KeyScheme::EcdsaK256Keccak => {
let bytes = hex_to_bytes::<ECDSA_PUBLIC_KEY_BYTES>(public_key)
.map_err(|err| invalid_public_key(scheme, err))?;
ecdsa_k256_keccak::PublicKey::read_from_bytes(&bytes)
.map_err(|err| invalid_public_key(scheme, err))?
.to_commitment()
},
};
println!("{}", commitment.to_hex());
Ok(())
}
fn invalid_public_key(scheme: KeyScheme, err: impl std::fmt::Display) -> CliError {
CliError::Input(format!("invalid {} public key: {err}", scheme.name()))
}
fn parse_commitment(value: &str) -> Result<PublicKeyCommitment, CliError> {
Word::try_from(value)
.map(PublicKeyCommitment::from)
.map_err(|err| CliError::Input(format!("invalid public key commitment `{value}`: {err}")))
}
fn scheme_name(scheme: AuthSchemeId) -> String {
KeyScheme::try_from(scheme).map_or_else(|()| scheme.to_string(), |scheme| scheme.name().into())
}