Skip to content

Commit

Permalink
k256: add support for non-32-byte BIP340 signatures (RustCrypto#1041)
Browse files Browse the repository at this point in the history
This was an extension to BIP340 that was introduced in 2022.

The official BIP340 test vectors do contain data for this case (tests 15-18) which are included here.

Fixes RustCrypto#1040
  • Loading branch information
randombit authored and altkdf committed Jun 5, 2024
1 parent e158ce5 commit 01d6e70
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 41 deletions.
63 changes: 62 additions & 1 deletion k256/src/schnorr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ mod tests {
assert_eq!(sk.verifying_key().to_bytes().as_slice(), &vector.public_key);

let sig = sk
.sign_prehash_with_aux_rand(&vector.message, &vector.aux_rand)
.sign_raw(&vector.message, &vector.aux_rand)
.unwrap_or_else(|_| {
panic!(
"low-level Schnorr signing failure for index {}",
Expand All @@ -287,6 +287,67 @@ mod tests {
}
}

#[test]
fn bip340_ext_sign_vectors() {
// Test indexes 15-18 from https://github.com/bitcoin/bips/blob/master/bip-0340/test-vectors.csv
//
// These tests all use the same key and aux
let sk = SigningKey::from_bytes(&hex!(
"0340034003400340034003400340034003400340034003400340034003400340"
))
.unwrap();

let aux_rand = [0u8; 32];

struct Bip340ExtTest {
index: usize,
msg: alloc::vec::Vec<u8>,
signature: [u8; 64],
}

let bip340_ext_sign_vectors = [
Bip340ExtTest {
index: 15,
msg: vec![],
signature: hex!(
"71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF
6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63"
)
},
Bip340ExtTest {
index: 16,
msg: hex!("11").to_vec(),
signature: hex!("08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF")
},
Bip340ExtTest {
index: 17,
msg: hex!("0102030405060708090A0B0C0D0E0F1011").to_vec(),
signature: hex!("5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5"),
},
Bip340ExtTest {
index: 18,
msg: vec![0x99; 100],
signature: hex!("403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367"),
},
];

for vector in bip340_ext_sign_vectors {
let sig = sk.sign_raw(&vector.msg, &aux_rand).unwrap_or_else(|_| {
panic!(
"low-level Schnorr signing failure for index {}",
vector.index
)
});

assert_eq!(
vector.signature,
sig.to_bytes(),
"wrong signature for index {}",
vector.index
);
}
}

/// Verification test vector
struct VerifyVector {
/// Index of test case
Expand Down
27 changes: 11 additions & 16 deletions k256/src/schnorr/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,12 @@ impl SigningKey {
/// # ⚠️ Warning
///
/// This is a low-level interface intended only for unusual use cases
/// involving signing pre-hashed messages.
/// involving signing pre-hashed messages, or "raw" messages where the
/// message is not hashed at all prior to being used to generate the
/// Schnorr signature.
///
/// The preferred interfaces are the [`Signer`] or [`RandomizedSigner`] traits.
pub fn sign_prehash_with_aux_rand(
&self,
msg_digest: &[u8; 32],
aux_rand: &[u8; 32],
) -> Result<Signature> {
pub fn sign_raw(&self, msg: &[u8], aux_rand: &[u8; 32]) -> Result<Signature> {
let mut t = tagged_hash(AUX_TAG).chain_update(aux_rand).finalize();

for (a, b) in t.iter_mut().zip(self.secret_key.to_bytes().iter()) {
Expand All @@ -90,7 +88,7 @@ impl SigningKey {
let rand = tagged_hash(NONCE_TAG)
.chain_update(t)
.chain_update(self.verifying_key.as_affine().x.to_bytes())
.chain_update(msg_digest)
.chain_update(msg)
.finalize();

let k = NonZeroScalar::try_from(&*rand)
Expand All @@ -105,7 +103,7 @@ impl SigningKey {
&tagged_hash(CHALLENGE_TAG)
.chain_update(r.to_bytes())
.chain_update(self.verifying_key.to_bytes())
.chain_update(msg_digest)
.chain_update(msg)
.finalize(),
);

Expand All @@ -114,7 +112,7 @@ impl SigningKey {
let sig = Signature { r, s };

#[cfg(debug_assertions)]
self.verifying_key.verify_prehash(msg_digest, &sig)?;
self.verifying_key.verify_prehash(msg, &sig)?;

Ok(sig)
}
Expand Down Expand Up @@ -164,14 +162,13 @@ where
D: Digest + FixedOutput<OutputSize = U32>,
{
fn try_sign_digest(&self, digest: D) -> Result<Signature> {
self.sign_prehash_with_aux_rand(&digest.finalize_fixed().into(), &Default::default())
self.sign_raw(&digest.finalize_fixed(), &Default::default())
}
}

impl PrehashSigner<Signature> for SigningKey {
fn sign_prehash(&self, prehash: &[u8]) -> Result<Signature> {
let prehash = prehash.try_into().map_err(|_| Error::new())?;
self.sign_prehash_with_aux_rand(&prehash, &Default::default())
self.sign_raw(prehash, &Default::default())
}
}

Expand All @@ -186,7 +183,7 @@ where
) -> Result<Signature> {
let mut aux_rand = [0u8; 32];
rng.fill_bytes(&mut aux_rand);
self.sign_prehash_with_aux_rand(&digest.finalize_fixed().into(), &aux_rand)
self.sign_raw(&digest.finalize_fixed(), &aux_rand)
}
}

Expand All @@ -202,12 +199,10 @@ impl RandomizedPrehashSigner<Signature> for SigningKey {
rng: &mut impl CryptoRngCore,
prehash: &[u8],
) -> Result<Signature> {
let prehash = prehash.try_into().map_err(|_| Error::new())?;

let mut aux_rand = [0u8; 32];
rng.fill_bytes(&mut aux_rand);

self.sign_prehash_with_aux_rand(&prehash, &aux_rand)
self.sign_raw(prehash, &aux_rand)
}
}

Expand Down
65 changes: 41 additions & 24 deletions k256/src/schnorr/verifying.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,46 @@ impl VerifyingKey {
self.as_affine().x.to_bytes()
}

/// Compute Schnorr signature.
///
/// # ⚠️ Warning
///
/// This is a low-level interface intended only for unusual use cases
/// involving verifying pre-hashed messages, or "raw" messages where the
/// message is not hashed at all prior to being used to generate the
/// Schnorr signature.
///
/// The preferred interfaces are the [`DigestVerifier`] or [`PrehashVerifier`] traits.
pub fn verify_raw(
&self,
message: &[u8],
signature: &Signature,
) -> core::result::Result<(), Error> {
let (r, s) = signature.split();

let e = <Scalar as Reduce<U256>>::reduce_bytes(
&tagged_hash(CHALLENGE_TAG)
.chain_update(signature.r.to_bytes())
.chain_update(self.to_bytes())
.chain_update(message)
.finalize(),
);

let R = ProjectivePoint::lincomb(
&ProjectivePoint::GENERATOR,
s,
&self.inner.to_projective(),
&-e,
)
.to_affine();

if R.is_identity().into() || R.y.normalize().is_odd().into() || R.x.normalize() != *r {
return Err(Error::new());
}

Ok(())
}

/// Parse verifying key from big endian-encoded x-coordinate.
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let maybe_affine_point = AffinePoint::decompact(FieldBytes::from_slice(bytes));
Expand Down Expand Up @@ -64,30 +104,7 @@ impl PrehashVerifier<Signature> for VerifyingKey {
prehash: &[u8],
signature: &Signature,
) -> core::result::Result<(), Error> {
let prehash: [u8; 32] = prehash.try_into().map_err(|_| Error::new())?;
let (r, s) = signature.split();

let e = <Scalar as Reduce<U256>>::reduce_bytes(
&tagged_hash(CHALLENGE_TAG)
.chain_update(signature.r.to_bytes())
.chain_update(self.to_bytes())
.chain_update(prehash)
.finalize(),
);

let R = ProjectivePoint::lincomb(
&ProjectivePoint::GENERATOR,
s,
&self.inner.to_projective(),
&-e,
)
.to_affine();

if R.is_identity().into() || R.y.normalize().is_odd().into() || R.x.normalize() != *r {
return Err(Error::new());
}

Ok(())
self.verify_raw(prehash, signature)
}
}

Expand Down

0 comments on commit 01d6e70

Please sign in to comment.