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 = "zf-ristretto255", feature = "std"))]
mod frost_tests {
use super::*;
use alloc::collections::BTreeMap;
use frost_core::{
aggregate,
keys::{self, IdentifierList, KeyPackage},
round1, round2, Signature, SigningPackage,
};
use frost_ristretto255::Ristretto255Sha512 as C;
use frost_core::VerifyingKey;
fn sign_2_of_3(message: &[u8]) -> (VerifyingKey<C>, Signature<C>) {
use rand::rngs::OsRng;
let mut rng = OsRng;
let (shares, pubkeys) =
keys::generate_with_dealer::<C, _>(3, 2, IdentifierList::Default, &mut rng)
.expect("dealer keygen");
let packages: BTreeMap<_, _> = shares
.into_iter()
.map(|(id, s)| (id, KeyPackage::try_from(s).expect("key package")))
.collect();
let signers: Vec<_> = packages.keys().copied().take(2).collect();
let mut nonces = BTreeMap::new();
let mut commitments = BTreeMap::new();
for id in &signers {
let (n, c) = round1::commit(packages[id].signing_share(), &mut rng);
nonces.insert(*id, n);
commitments.insert(*id, c);
}
let package = SigningPackage::new(commitments, message);
let sig_shares: BTreeMap<_, _> = signers
.iter()
.map(|id| {
(
*id,
round2::sign(&package, &nonces[id], &packages[id]).expect("sign"),
)
})
.collect();
let sig = aggregate(&package, &sig_shares, &pubkeys).expect("aggregate");
(*pubkeys.verifying_key(), 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!(pubkey.verify(&epoch_7, &sig).is_ok());
let epoch_8 = SigningContext::new(8, manifest, msg).encode();
assert!(pubkey.verify(&epoch_8, &sig).is_err());
let other_manifest = SigningContext::new(7, [0x5b; 32], msg).encode();
assert!(pubkey.verify(&other_manifest, &sig).is_err());
assert!(pubkey.verify(msg, &sig).is_err());
}
}