use alloc::vec::Vec;
use sha2::{Digest, Sha256};
pub const SIGNING_CONTEXT_DOMAIN: &[u8] = b"frostito/signing-context/v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SigningContext<'a> {
pub epoch: u64,
pub manifest_hash: [u8; 32],
pub message: &'a [u8],
}
impl<'a> SigningContext<'a> {
#[inline]
pub fn new(epoch: u64, manifest_hash: [u8; 32], message: &'a [u8]) -> Self {
Self {
epoch,
manifest_hash,
message,
}
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + SIGNING_CONTEXT_DOMAIN.len() + 8 + 32 + 8 + self.message.len());
self.encode_into(&mut out);
out
}
pub fn encode_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&(SIGNING_CONTEXT_DOMAIN.len() as u64).to_le_bytes());
out.extend_from_slice(SIGNING_CONTEXT_DOMAIN);
out.extend_from_slice(&self.epoch.to_le_bytes());
out.extend_from_slice(&self.manifest_hash);
out.extend_from_slice(&(self.message.len() as u64).to_le_bytes());
out.extend_from_slice(self.message);
}
pub fn digest(&self) -> [u8; 32] {
let mut h = Sha256::new();
h.update(self.encode());
h.finalize().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encoding_is_deterministic() {
let a = SigningContext::new(3, [1u8; 32], b"pay alice");
let b = SigningContext::new(3, [1u8; 32], b"pay alice");
assert_eq!(a.encode(), b.encode());
assert_eq!(a.digest(), b.digest());
}
#[test]
fn epoch_changes_the_signed_bytes() {
let m = [7u8; 32];
assert_ne!(
SigningContext::new(1, m, b"same message").encode(),
SigningContext::new(2, m, b"same message").encode()
);
}
#[test]
fn manifest_changes_the_signed_bytes() {
assert_ne!(
SigningContext::new(1, [7u8; 32], b"same message").encode(),
SigningContext::new(1, [8u8; 32], b"same message").encode()
);
}
#[test]
fn encoding_is_injective_across_field_boundaries() {
let a = SigningContext::new(1, [0u8; 32], b"ab");
let b = SigningContext::new(1, [0u8; 32], b"a");
let c = SigningContext::new(1, [0u8; 32], b"");
assert_ne!(a.encode(), b.encode());
assert_ne!(b.encode(), c.encode());
let mut forged = Vec::new();
forged.extend_from_slice(&2u64.to_le_bytes());
forged.extend_from_slice(&[9u8; 32]);
forged.extend_from_slice(&4u64.to_le_bytes());
forged.extend_from_slice(b"evil");
assert_ne!(
SigningContext::new(1, [0u8; 32], &forged).encode(),
SigningContext::new(2, [9u8; 32], b"evil").encode()
);
}
#[test]
fn encode_into_matches_encode() {
let ctx = SigningContext::new(9, [4u8; 32], b"buffered");
let mut buf = Vec::from(&b"prefix"[..]);
ctx.encode_into(&mut buf);
assert_eq!(&buf[..6], b"prefix");
assert_eq!(&buf[6..], &ctx.encode()[..]);
}
#[test]
fn encoding_layout_is_stable() {
let ctx = SigningContext::new(0x0102_0304_0506_0708, [0xaa; 32], b"hi");
let e = ctx.encode();
let d = SIGNING_CONTEXT_DOMAIN.len();
assert_eq!(&e[..8], &(d as u64).to_le_bytes());
assert_eq!(&e[8..8 + d], SIGNING_CONTEXT_DOMAIN);
assert_eq!(&e[8 + d..16 + d], &0x0102_0304_0506_0708u64.to_le_bytes());
assert_eq!(&e[16 + d..48 + d], &[0xaa; 32]);
assert_eq!(&e[48 + d..56 + d], &2u64.to_le_bytes());
assert_eq!(&e[56 + d..], b"hi");
assert_eq!(e.len(), 56 + d + 2);
}
}
#[cfg(all(test, feature = "ristretto255", feature = "std"))]
mod frost_tests {
use super::*;
use crate::frost::{self, Signature, SigningPackage};
use crate::{CurvePoint, CurveScalar, SecretShare};
use alloc::vec::Vec;
use curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar};
fn sign_2_of_3(message: &[u8]) -> (RistrettoPoint, Signature<RistrettoPoint>) {
use rand::rngs::OsRng;
let mut rng = OsRng;
let secret = <Scalar as CurveScalar>::random(&mut rng);
let a1 = <Scalar as CurveScalar>::random(&mut rng);
let eval = |x: u32| {
let xs = <Scalar as CurveScalar>::from_u32(x);
secret.add(&a1.mul(&xs))
};
let group_pubkey = RistrettoPoint::generator().mul_scalar(&secret);
let active: [u32; 2] = [1, 2];
let mut nonces = Vec::new();
let mut commitments = Vec::new();
for &i in &active {
let (n, c) = frost::commit::<RistrettoPoint, _>(i, &mut rng).expect("index is 1-indexed by construction");
nonces.push((i, n));
commitments.push(c);
}
let package =
SigningPackage::<RistrettoPoint>::new(message.to_vec(), commitments).unwrap();
let mut sig_shares = Vec::new();
for (i, n) in nonces {
let share = SecretShare::new(i, eval(i)).expect("index is 1-indexed by construction");
sig_shares
.push(frost::sign::<RistrettoPoint>(&package, n, &share, &group_pubkey).unwrap());
}
let sig =
frost::aggregate::<RistrettoPoint>(&package, &sig_shares, &group_pubkey, None).unwrap();
(group_pubkey, sig)
}
#[test]
fn signature_is_bound_to_its_epoch() {
let manifest = [0x5a; 32];
let msg = b"release the escrow";
let epoch_7 = SigningContext::new(7, manifest, msg).encode();
let (pubkey, sig) = sign_2_of_3(&epoch_7);
assert!(frost::verify_signature::<RistrettoPoint>(&pubkey, &epoch_7, &sig));
let epoch_8 = SigningContext::new(8, manifest, msg).encode();
assert!(!frost::verify_signature::<RistrettoPoint>(&pubkey, &epoch_8, &sig));
let other_manifest = SigningContext::new(7, [0x5b; 32], msg).encode();
assert!(!frost::verify_signature::<RistrettoPoint>(&pubkey, &other_manifest, &sig));
assert!(!frost::verify_signature::<RistrettoPoint>(&pubkey, msg, &sig));
}
}