Skip to content

Commit

Permalink
fix: reject identity keys and commitments in signatures (#217)
Browse files Browse the repository at this point in the history
Signature verification currently allows identity elements:
- `SchnorrSignature` allows an identity public key
- `CommitmentSignature` allows an identity commitment
- `CommitmentAndPublicKeySignature` allows both

This doesn't strictly break soundness, but does remove message binding.
While it shouldn't be problematic for unforgeability, it's a case that
shouldn't arise from an honest signer and is easy to check for.

This PR fails signature verification if such an identity element is
provided, and adds tests for each case. It does _not_ return an error if
the corresponding identity elements are provided by the signer, since
this precludes useful partial signature operations.
  • Loading branch information
AaronFeickert authored Feb 7, 2024
1 parent a6cef07 commit e755b26
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 3 deletions.
69 changes: 68 additions & 1 deletion src/ristretto/ristretto_com_and_pub_sig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ pub type RistrettoComAndPubSig = CommitmentAndPublicKeySignature<RistrettoPublic
mod test {
use blake2::Blake2b;
use digest::{consts::U64, Digest};
use rand_core::RngCore;
use tari_utilities::ByteArray;

use crate::{
commitment::HomomorphicCommitmentFactory,
commitment::{HomomorphicCommitment, HomomorphicCommitmentFactory},
keys::{PublicKey, SecretKey},
ristretto::{
pedersen::{commitment_factory::PedersenCommitmentFactory, PedersenCommitment},
Expand Down Expand Up @@ -359,4 +360,70 @@ mod test {
assert_eq!(bytes.capacity(), bytes.len());
assert!(bytes.iter().all(|b| *b == 0x00));
}

#[test]
fn zero_commitment() {
let mut rng = rand::thread_rng();
let factory = PedersenCommitmentFactory::default();

// Generate a zero commitment opening and a random key
let a = RistrettoSecretKey::default();
let x = RistrettoSecretKey::default();
let commitment = factory.commit(&x, &a);
assert_eq!(commitment, HomomorphicCommitment::<RistrettoPublicKey>::default());

let y = RistrettoSecretKey::random(&mut rng);
let public_key = RistrettoPublicKey::from_secret_key(&y);

// Generate a signature with the zero opening and key
let mut challenge = [0u8; RistrettoSecretKey::WIDE_REDUCTION_LEN];
rng.fill_bytes(&mut challenge);
let sig = RistrettoComAndPubSig::sign(
&a,
&x,
&y,
&RistrettoSecretKey::random(&mut rng),
&RistrettoSecretKey::random(&mut rng),
&RistrettoSecretKey::random(&mut rng),
&challenge,
&factory,
)
.unwrap();

// The signature should fail to verify
assert!(!sig.verify_challenge(&commitment, &public_key, &challenge, &factory, &mut rng));
}

#[test]
fn zero_public_key() {
let mut rng = rand::thread_rng();
let factory = PedersenCommitmentFactory::default();

// Generate a random commitment opening and a zero key
let a = RistrettoSecretKey::random(&mut rng);
let x = RistrettoSecretKey::random(&mut rng);
let commitment = factory.commit(&x, &a);

let y = RistrettoSecretKey::default();
let public_key = RistrettoPublicKey::from_secret_key(&y);
assert_eq!(public_key, RistrettoPublicKey::default());

// Generate a signature with the opening and zero key
let mut challenge = [0u8; RistrettoSecretKey::WIDE_REDUCTION_LEN];
rng.fill_bytes(&mut challenge);
let sig = RistrettoComAndPubSig::sign(
&a,
&x,
&y,
&RistrettoSecretKey::random(&mut rng),
&RistrettoSecretKey::random(&mut rng),
&RistrettoSecretKey::random(&mut rng),
&challenge,
&factory,
)
.unwrap();

// The signature should fail to verify
assert!(!sig.verify_challenge(&commitment, &public_key, &challenge, &factory, &mut rng));
}
}
31 changes: 30 additions & 1 deletion src/ristretto/ristretto_com_sig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,11 @@ pub type RistrettoComSig = CommitmentSignature<RistrettoPublicKey, RistrettoSecr
mod test {
use blake2::Blake2b;
use digest::{consts::U64, Digest};
use rand_core::RngCore;
use tari_utilities::ByteArray;

use crate::{
commitment::HomomorphicCommitmentFactory,
commitment::{HomomorphicCommitment, HomomorphicCommitmentFactory},
keys::{PublicKey, SecretKey},
ristretto::{
pedersen::{commitment_factory::PedersenCommitmentFactory, PedersenCommitment},
Expand Down Expand Up @@ -228,4 +229,32 @@ mod test {
assert_eq!(bytes.capacity(), bytes.len());
assert!(bytes.iter().all(|b| *b == 0x00));
}

#[test]
fn zero_commitment() {
let mut rng = rand::thread_rng();
let factory = PedersenCommitmentFactory::default();

// Generate a zero commitment opening
let secret_a = RistrettoSecretKey::default();
let secret_x = RistrettoSecretKey::default();
let commitment = factory.commit(&secret_x, &secret_a);
assert_eq!(commitment, HomomorphicCommitment::<RistrettoPublicKey>::default());

// Generate a signature with the zero opening
let mut challenge = [0u8; RistrettoSecretKey::WIDE_REDUCTION_LEN];
rng.fill_bytes(&mut challenge);
let sig = RistrettoComSig::sign(
&secret_a,
&secret_x,
&RistrettoSecretKey::random(&mut rng),
&RistrettoSecretKey::random(&mut rng),
&challenge,
&factory,
)
.unwrap();

// The signature should fail to verify
assert!(!sig.verify_challenge(&commitment, &challenge, &factory));
}
}
17 changes: 17 additions & 0 deletions src/ristretto/ristretto_sig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,21 @@ mod test {
assert!(!sig.verify(&P, "Qs are things that happen to other people"));
assert!(!sig.verify(&(&P + &P), "Queues are things that happen to other people"));
}

#[test]
fn zero_public_key() {
let mut rng = rand::thread_rng();

// Generate a zero key
let secret_key = RistrettoSecretKey::default();
let public_key = RistrettoPublicKey::from_secret_key(&secret_key);
assert_eq!(public_key, RistrettoPublicKey::default());

// Sign a message with the zero key
let message = "A secret message";
let sig = RistrettoSchnorr::sign(&secret_key, message, &mut rng).unwrap();

// The signature should fail to verify
assert!(!sig.verify(&public_key, message,));
}
}
5 changes: 5 additions & 0 deletions src/signatures/commitment_and_public_key_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ where
C: HomomorphicCommitmentFactory<P = P>,
R: RngCore + CryptoRng,
{
// Reject a zero commitment and public key
if commitment.as_public_key() == &P::default() || pubkey == &P::default() {
return false;
}

// The challenge cannot be zero
if *challenge == K::default() {
return false;
Expand Down
5 changes: 5 additions & 0 deletions src/signatures/commitment_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ where
for<'b> &'b HomomorphicCommitment<P>: Add<&'b HomomorphicCommitment<P>, Output = HomomorphicCommitment<P>>,
C: HomomorphicCommitmentFactory<P = P>,
{
// Reject a zero commitment
if public_commitment.as_public_key() == &P::default() {
return false;
}

// v*H + u*G
let lhs = self.calc_signature_verifier(factory);
// R + e.C
Expand Down
8 changes: 7 additions & 1 deletion src/signatures/schnorr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ use crate::{
keys::{PublicKey, SecretKey},
};

// Define the hashing domain for Schnorr signatures
// Define a default hashing domain for Schnorr signatures
// You almost certainly want to define your own that is specific to signature context!
hash_domain!(SchnorrSigChallenge, "com.tari.schnorr_signature", 1);

/// An error occurred during construction of a SchnorrSignature
Expand Down Expand Up @@ -231,6 +232,11 @@ where
for<'b> &'b K: Mul<&'a P, Output = P>,
for<'b> &'b P: Add<P, Output = P>,
{
// Reject a zero key
if public_key == &P::default() {
return false;
}

let lhs = self.calc_signature_verifier();
let rhs = &self.public_nonce + challenge * public_key;
// Implementors should make this a constant time comparison
Expand Down

0 comments on commit e755b26

Please sign in to comment.