From 19a202873bcae403c26d64eea2c19c841afa01b2 Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Wed, 24 May 2023 09:52:59 +0900 Subject: [PATCH] [zk-token-sdk] Generalize encryption key derivation from signers (#31784) * generalize ElGamal keypair derivation from signer * generalize AeKey derivation from signer * add `tiny-bip39` as a dev dependency for tests --- Cargo.lock | 1 + zk-token-sdk/Cargo.toml | 3 + .../src/encryption/auth_encryption.rs | 39 ++++---- zk-token-sdk/src/encryption/elgamal.rs | 96 +++++++++++-------- zk-token-sdk/src/sigma_proofs/pubkey_proof.rs | 3 +- 5 files changed, 86 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cb22b170..427dc8840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7129,6 +7129,7 @@ dependencies = [ "solana-sdk", "subtle", "thiserror", + "tiny-bip39", "zeroize", ] diff --git a/zk-token-sdk/Cargo.toml b/zk-token-sdk/Cargo.toml index c9cc39dfa..1abc29720 100644 --- a/zk-token-sdk/Cargo.toml +++ b/zk-token-sdk/Cargo.toml @@ -14,6 +14,9 @@ num-derive = { workspace = true } num-traits = { workspace = true } solana-program = { workspace = true } +[dev-dependencies] +tiny-bip39 = { workspace = true } + [target.'cfg(not(target_os = "solana"))'.dependencies] aes-gcm-siv = { workspace = true } arrayref = { workspace = true } diff --git a/zk-token-sdk/src/encryption/auth_encryption.rs b/zk-token-sdk/src/encryption/auth_encryption.rs index f89dc4201..e2d979d38 100644 --- a/zk-token-sdk/src/encryption/auth_encryption.rs +++ b/zk-token-sdk/src/encryption/auth_encryption.rs @@ -16,9 +16,6 @@ use { sha3::{Digest, Sha3_512}, solana_sdk::{ derivation_path::DerivationPath, - instruction::Instruction, - message::Message, - pubkey::Pubkey, signature::Signature, signer::{ keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, SeedDerivable, @@ -85,20 +82,26 @@ impl AuthenticatedEncryption { #[derive(Debug, Zeroize)] pub struct AeKey([u8; 16]); impl AeKey { - pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result { - let message = Message::new( - &[Instruction::new_with_bytes(*address, b"AeKey", vec![])], - Some(&signer.try_pubkey()?), - ); - let signature = signer.try_sign_message(&message.serialize())?; + pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result> { + let seed = Self::seed_from_signer(signer, tag)?; + Self::from_seed(&seed) + } + + pub fn seed_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result, SignerError> { + let message = [b"AeKey", tag].concat(); + let signature = signer.try_sign_message(&message)?; // Some `Signer` implementations return the default signature, which is not suitable for // use as key material if bool::from(signature.as_ref().ct_eq(Signature::default().as_ref())) { - Err(SignerError::Custom("Rejecting default signature".into())) - } else { - Ok(AeKey(signature.as_ref()[..16].try_into().unwrap())) + return Err(SignerError::Custom("Rejecting default signature".into())); } + + let mut hasher = Sha3_512::new(); + hasher.update(signature.as_ref()); + let result = hasher.finalize(); + + Ok(result.to_vec()) } pub fn random(rng: &mut T) -> Self { @@ -209,7 +212,7 @@ impl fmt::Display for AeCiphertext { mod tests { use { super::*, - solana_sdk::{signature::Keypair, signer::null_signer::NullSigner}, + solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::null_signer::NullSigner}, }; #[test] @@ -229,11 +232,15 @@ mod tests { let keypair2 = Keypair::new(); assert_ne!( - AeKey::new(&keypair1, &Pubkey::default()).unwrap().0, - AeKey::new(&keypair2, &Pubkey::default()).unwrap().0, + AeKey::new_from_signer(&keypair1, Pubkey::default().as_ref()) + .unwrap() + .0, + AeKey::new_from_signer(&keypair2, Pubkey::default().as_ref()) + .unwrap() + .0, ); let null_signer = NullSigner::new(&Pubkey::default()); - assert!(AeKey::new(&null_signer, &Pubkey::default()).is_err()); + assert!(AeKey::new_from_signer(&null_signer, Pubkey::default().as_ref()).is_err()); } } diff --git a/zk-token-sdk/src/encryption/elgamal.rs b/zk-token-sdk/src/encryption/elgamal.rs index b1f05b50a..0e747b398 100644 --- a/zk-token-sdk/src/encryption/elgamal.rs +++ b/zk-token-sdk/src/encryption/elgamal.rs @@ -28,9 +28,6 @@ use { serde::{Deserialize, Serialize}, solana_sdk::{ derivation_path::DerivationPath, - instruction::Instruction, - message::Message, - pubkey::Pubkey, signature::Signature, signer::{ keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, EncodableKeypair, @@ -45,7 +42,7 @@ use { #[cfg(not(target_os = "solana"))] use { rand::rngs::OsRng, - sha3::Sha3_512, + sha3::{Digest, Sha3_512}, std::{ error, fmt, io::{Read, Write}, @@ -162,24 +159,22 @@ pub struct ElGamalKeypair { } impl ElGamalKeypair { - /// Deterministically derives an ElGamal keypair from an Ed25519 signing key and a Solana - /// address. + /// Deterministically derives an ElGamal keypair from a Solana signer and a tag. /// - /// This function exists for applications where a user may not wish to maintin a Solana - /// (Ed25519) keypair and an ElGamal keypair separately. A user may wish to solely maintain the - /// Solana keypair and then derive the ElGamal keypair on-the-fly whenever - /// encryption/decryption is needed. + /// This function exists for applications where a user may not wish to maintain a Solana signer + /// and an ElGamal keypair separately. Instead, a user can derive the ElGamal keypair + /// on-the-fly whenever encryption/decryption is needed. /// - /// For the spl token-2022 confidential extension application, the ElGamal encryption public - /// key is specified in a token account address. A natural way to derive an ElGamal keypair is - /// then to define it from the hash of a Solana keypair and a Solana address. However, for - /// general hardware wallets, the signing key is not exposed in the API. Therefore, this - /// function uses a signer to sign a pre-specified message with respect to a Solana address. - /// The resulting signature is then hashed to derive an ElGamal keypair. + /// For the spl-token-2022 confidential extension, the ElGamal public key is + /// specified in a token account. A natural way to derive an ElGamal keypair is to define it + /// from the hash of a Solana keypair and a Solana address as the tag. However, for general + /// hardware wallets, the signing key is not exposed in the API. Therefore, this function uses + /// a signer to sign a pre-specified message with respect to a Solana address. The resulting + /// signature is then hashed to derive an ElGamal keypair. #[cfg(not(target_os = "solana"))] #[allow(non_snake_case)] - pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result { - let secret = ElGamalSecretKey::new(signer, address)?; + pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result> { + let secret = ElGamalSecretKey::new_from_signer(signer, tag)?; let public = ElGamalPubkey::new(&secret); Ok(ElGamalKeypair { public, secret }) } @@ -367,20 +362,18 @@ impl fmt::Display for ElGamalPubkey { #[zeroize(drop)] pub struct ElGamalSecretKey(Scalar); impl ElGamalSecretKey { - /// Deterministically derives an ElGamal keypair from an Ed25519 signing key and a Solana - /// address. + /// Deterministically derives an ElGamal secret key from a Solana signer and a tag. /// - /// See `ElGamalKeypair::new` for more context on the key derivation. - pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result { - let message = Message::new( - &[Instruction::new_with_bytes( - *address, - b"ElGamalSecretKey", - vec![], - )], - Some(&signer.try_pubkey()?), - ); - let signature = signer.try_sign_message(&message.serialize())?; + /// See `ElGamalKeypair::new_from_signer` for more context on the key derivation. + pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result> { + let seed = Self::seed_from_signer(signer, tag)?; + Self::from_seed(&seed) + } + + /// Derive a seed from a Solana signer used to generate an ElGamal secret key. + pub fn seed_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result, SignerError> { + let message = [b"ElGamalSecretKey", tag].concat(); + let signature = signer.try_sign_message(&message)?; // Some `Signer` implementations return the default signature, which is not suitable for // use as key material @@ -388,9 +381,11 @@ impl ElGamalSecretKey { return Err(SignerError::Custom("Rejecting default signatures".into())); } - Ok(ElGamalSecretKey(Scalar::hash_from_bytes::( - signature.as_ref(), - ))) + let mut hasher = Sha3_512::new(); + hasher.update(signature.as_ref()); + let result = hasher.finalize(); + + Ok(result.to_vec()) } /// Randomly samples an ElGamal secret key. @@ -714,7 +709,8 @@ mod tests { use { super::*, crate::encryption::pedersen::Pedersen, - solana_sdk::{signature::Keypair, signer::null_signer::NullSigner}, + bip39::{Language, Mnemonic, MnemonicType, Seed}, + solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::null_signer::NullSigner}, std::fs::{self, File}, }; @@ -949,21 +945,43 @@ mod tests { } #[test] - fn test_secret_key_new() { + fn test_secret_key_new_from_signer() { let keypair1 = Keypair::new(); let keypair2 = Keypair::new(); assert_ne!( - ElGamalSecretKey::new(&keypair1, &Pubkey::default()) + ElGamalSecretKey::new_from_signer(&keypair1, Pubkey::default().as_ref()) .unwrap() .0, - ElGamalSecretKey::new(&keypair2, &Pubkey::default()) + ElGamalSecretKey::new_from_signer(&keypair2, Pubkey::default().as_ref()) .unwrap() .0, ); let null_signer = NullSigner::new(&Pubkey::default()); - assert!(ElGamalSecretKey::new(&null_signer, &Pubkey::default()).is_err()); + assert!( + ElGamalSecretKey::new_from_signer(&null_signer, Pubkey::default().as_ref()).is_err() + ); + } + + #[test] + fn test_keypair_from_seed() { + let good_seed = vec![0; 32]; + assert!(ElGamalKeypair::from_seed(&good_seed).is_ok()); + + let too_short_seed = vec![0; 31]; + assert!(ElGamalKeypair::from_seed(&too_short_seed).is_err()); + } + + #[test] + fn test_keypair_from_seed_phrase_and_passphrase() { + let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); + let passphrase = "42"; + let seed = Seed::new(&mnemonic, passphrase); + let expected_keypair = ElGamalKeypair::from_seed(seed.as_bytes()).unwrap(); + let keypair = + ElGamalKeypair::from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap(); + assert_eq!(keypair.public, expected_keypair.public); } #[test] diff --git a/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs b/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs index 400e719e4..be00010b7 100644 --- a/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs @@ -157,7 +157,8 @@ mod test { .is_ok()); // derived ElGamal keypair - let keypair = ElGamalKeypair::new(&Keypair::new(), &Pubkey::default()).unwrap(); + let keypair = + ElGamalKeypair::new_from_signer(&Keypair::new(), Pubkey::default().as_ref()).unwrap(); let mut prover_transcript = Transcript::new(b"test"); let mut verifier_transcript = Transcript::new(b"test");