[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",
"subtle",
"thiserror",
"tiny-bip39",
"zeroize",
]

View File

@ -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 }

View File

@ -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<Self, SignerError> {
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<Self, Box<dyn error::Error>> {
let seed = Self::seed_from_signer(signer, tag)?;
Self::from_seed(&seed)
}
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
// 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<T: RngCore + CryptoRng>(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());
}
}

View File

@ -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<Self, SignerError> {
let secret = ElGamalSecretKey::new(signer, address)?;
pub fn new_from_signer(signer: &dyn Signer, tag: &[u8]) -> Result<Self, Box<dyn error::Error>> {
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<Self, SignerError> {
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<Self, Box<dyn error::Error>> {
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<Vec<u8>, 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::<Sha3_512>(
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]

View File

@ -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");