[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
This commit is contained in:
samkim-crypto 2023-05-24 09:52:59 +09:00 committed by GitHub
parent 8e8b2f1671
commit 19a202873b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 86 additions and 56 deletions

1
Cargo.lock generated
View File

@ -7129,6 +7129,7 @@ dependencies = [
"solana-sdk", "solana-sdk",
"subtle", "subtle",
"thiserror", "thiserror",
"tiny-bip39",
"zeroize", "zeroize",
] ]

View File

@ -14,6 +14,9 @@ num-derive = { workspace = true }
num-traits = { workspace = true } num-traits = { workspace = true }
solana-program = { workspace = true } solana-program = { workspace = true }
[dev-dependencies]
tiny-bip39 = { workspace = true }
[target.'cfg(not(target_os = "solana"))'.dependencies] [target.'cfg(not(target_os = "solana"))'.dependencies]
aes-gcm-siv = { workspace = true } aes-gcm-siv = { workspace = true }
arrayref = { workspace = true } arrayref = { workspace = true }

View File

@ -16,9 +16,6 @@ use {
sha3::{Digest, Sha3_512}, sha3::{Digest, Sha3_512},
solana_sdk::{ solana_sdk::{
derivation_path::DerivationPath, derivation_path::DerivationPath,
instruction::Instruction,
message::Message,
pubkey::Pubkey,
signature::Signature, signature::Signature,
signer::{ signer::{
keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, SeedDerivable, keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, SeedDerivable,
@ -85,20 +82,26 @@ impl AuthenticatedEncryption {
#[derive(Debug, Zeroize)] #[derive(Debug, Zeroize)]
pub struct AeKey([u8; 16]); pub struct AeKey([u8; 16]);
impl AeKey { impl AeKey {
pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result<Self, SignerError> { pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result<Self, Box<dyn error::Error>> {
let message = Message::new( let seed = Self::seed_from_signer(signer, tag)?;
&[Instruction::new_with_bytes(*address, b"AeKey", vec![])], Self::from_seed(&seed)
Some(&signer.try_pubkey()?), }
);
let signature = signer.try_sign_message(&message.serialize())?; pub fn seed_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result<Vec<u8>, 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 // Some `Signer` implementations return the default signature, which is not suitable for
// use as key material // use as key material
if bool::from(signature.as_ref().ct_eq(Signature::default().as_ref())) { if bool::from(signature.as_ref().ct_eq(Signature::default().as_ref())) {
Err(SignerError::Custom("Rejecting default signature".into())) return Err(SignerError::Custom("Rejecting default signature".into()));
} else {
Ok(AeKey(signature.as_ref()[..16].try_into().unwrap()))
} }
let mut hasher = Sha3_512::new();
hasher.update(signature.as_ref());
let result = hasher.finalize();
Ok(result.to_vec())
} }
pub fn random<T: RngCore + CryptoRng>(rng: &mut T) -> Self { pub fn random<T: RngCore + CryptoRng>(rng: &mut T) -> Self {
@ -209,7 +212,7 @@ impl fmt::Display for AeCiphertext {
mod tests { mod tests {
use { use {
super::*, super::*,
solana_sdk::{signature::Keypair, signer::null_signer::NullSigner}, solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::null_signer::NullSigner},
}; };
#[test] #[test]
@ -229,11 +232,15 @@ mod tests {
let keypair2 = Keypair::new(); let keypair2 = Keypair::new();
assert_ne!( assert_ne!(
AeKey::new(&keypair1, &Pubkey::default()).unwrap().0, AeKey::new_from_signer(&keypair1, Pubkey::default().as_ref())
AeKey::new(&keypair2, &Pubkey::default()).unwrap().0, .unwrap()
.0,
AeKey::new_from_signer(&keypair2, Pubkey::default().as_ref())
.unwrap()
.0,
); );
let null_signer = NullSigner::new(&Pubkey::default()); 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());
} }
} }

View File

@ -28,9 +28,6 @@ use {
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
solana_sdk::{ solana_sdk::{
derivation_path::DerivationPath, derivation_path::DerivationPath,
instruction::Instruction,
message::Message,
pubkey::Pubkey,
signature::Signature, signature::Signature,
signer::{ signer::{
keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, EncodableKeypair, keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, EncodableKeypair,
@ -45,7 +42,7 @@ use {
#[cfg(not(target_os = "solana"))] #[cfg(not(target_os = "solana"))]
use { use {
rand::rngs::OsRng, rand::rngs::OsRng,
sha3::Sha3_512, sha3::{Digest, Sha3_512},
std::{ std::{
error, fmt, error, fmt,
io::{Read, Write}, io::{Read, Write},
@ -162,24 +159,22 @@ pub struct ElGamalKeypair {
} }
impl ElGamalKeypair { impl ElGamalKeypair {
/// Deterministically derives an ElGamal keypair from an Ed25519 signing key and a Solana /// Deterministically derives an ElGamal keypair from a Solana signer and a tag.
/// address.
/// ///
/// This function exists for applications where a user may not wish to maintin a Solana /// This function exists for applications where a user may not wish to maintain a Solana signer
/// (Ed25519) keypair and an ElGamal keypair separately. A user may wish to solely maintain the /// and an ElGamal keypair separately. Instead, a user can derive the ElGamal keypair
/// Solana keypair and then derive the ElGamal keypair on-the-fly whenever /// on-the-fly whenever encryption/decryption is needed.
/// encryption/decryption is needed.
/// ///
/// For the spl token-2022 confidential extension application, the ElGamal encryption public /// For the spl-token-2022 confidential extension, the ElGamal public key is
/// key is specified in a token account address. A natural way to derive an ElGamal keypair is /// specified in a token account. A natural way to derive an ElGamal keypair is to define it
/// then to define it from the hash of a Solana keypair and a Solana address. However, for /// from the hash of a Solana keypair and a Solana address as the tag. However, for general
/// general hardware wallets, the signing key is not exposed in the API. Therefore, this /// hardware wallets, the signing key is not exposed in the API. Therefore, this function uses
/// function uses a signer to sign a pre-specified message with respect to a Solana address. /// a signer to sign a pre-specified message with respect to a Solana address. The resulting
/// The resulting signature is then hashed to derive an ElGamal keypair. /// signature is then hashed to derive an ElGamal keypair.
#[cfg(not(target_os = "solana"))] #[cfg(not(target_os = "solana"))]
#[allow(non_snake_case)] #[allow(non_snake_case)]
pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result<Self, SignerError> { pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result<Self, Box<dyn error::Error>> {
let secret = ElGamalSecretKey::new(signer, address)?; let secret = ElGamalSecretKey::new_from_signer(signer, tag)?;
let public = ElGamalPubkey::new(&secret); let public = ElGamalPubkey::new(&secret);
Ok(ElGamalKeypair { public, secret }) Ok(ElGamalKeypair { public, secret })
} }
@ -367,20 +362,18 @@ impl fmt::Display for ElGamalPubkey {
#[zeroize(drop)] #[zeroize(drop)]
pub struct ElGamalSecretKey(Scalar); pub struct ElGamalSecretKey(Scalar);
impl ElGamalSecretKey { impl ElGamalSecretKey {
/// Deterministically derives an ElGamal keypair from an Ed25519 signing key and a Solana /// Deterministically derives an ElGamal secret key from a Solana signer and a tag.
/// address.
/// ///
/// See `ElGamalKeypair::new` for more context on the key derivation. /// See `ElGamalKeypair::new_from_signer` for more context on the key derivation.
pub fn new(signer: &dyn Signer, address: &Pubkey) -> Result<Self, SignerError> { pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result<Self, Box<dyn error::Error>> {
let message = Message::new( let seed = Self::seed_from_signer(signer, tag)?;
&[Instruction::new_with_bytes( Self::from_seed(&seed)
*address, }
b"ElGamalSecretKey",
vec![], /// 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<Vec<u8>, SignerError> {
Some(&signer.try_pubkey()?), let message = [b"ElGamalSecretKey", tag].concat();
); let signature = signer.try_sign_message(&message)?;
let signature = signer.try_sign_message(&message.serialize())?;
// Some `Signer` implementations return the default signature, which is not suitable for // Some `Signer` implementations return the default signature, which is not suitable for
// use as key material // use as key material
@ -388,9 +381,11 @@ impl ElGamalSecretKey {
return Err(SignerError::Custom("Rejecting default signatures".into())); return Err(SignerError::Custom("Rejecting default signatures".into()));
} }
Ok(ElGamalSecretKey(Scalar::hash_from_bytes::<Sha3_512>( let mut hasher = Sha3_512::new();
signature.as_ref(), hasher.update(signature.as_ref());
))) let result = hasher.finalize();
Ok(result.to_vec())
} }
/// Randomly samples an ElGamal secret key. /// Randomly samples an ElGamal secret key.
@ -714,7 +709,8 @@ mod tests {
use { use {
super::*, super::*,
crate::encryption::pedersen::Pedersen, 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}, std::fs::{self, File},
}; };
@ -949,21 +945,43 @@ mod tests {
} }
#[test] #[test]
fn test_secret_key_new() { fn test_secret_key_new_from_signer() {
let keypair1 = Keypair::new(); let keypair1 = Keypair::new();
let keypair2 = Keypair::new(); let keypair2 = Keypair::new();
assert_ne!( assert_ne!(
ElGamalSecretKey::new(&keypair1, &Pubkey::default()) ElGamalSecretKey::new_from_signer(&keypair1, Pubkey::default().as_ref())
.unwrap() .unwrap()
.0, .0,
ElGamalSecretKey::new(&keypair2, &Pubkey::default()) ElGamalSecretKey::new_from_signer(&keypair2, Pubkey::default().as_ref())
.unwrap() .unwrap()
.0, .0,
); );
let null_signer = NullSigner::new(&Pubkey::default()); 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] #[test]

View File

@ -157,7 +157,8 @@ mod test {
.is_ok()); .is_ok());
// derived ElGamal keypair // 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 prover_transcript = Transcript::new(b"test");
let mut verifier_transcript = Transcript::new(b"test"); let mut verifier_transcript = Transcript::new(b"test");