use sha2::{Digest, Sha256, Sha512};
use std::collections::BTreeSet;
pub const REPORTDATA_DOMAIN: &[u8] = b"aspens-signer/reportdata/v1";
pub const MANIFEST_DOMAIN: &[u8] = b"aspens-signer/pubkey-manifest/v1";
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum CurveTag {
Secp256k1 = 0x01,
Ed25519 = 0x02,
}
impl CurveTag {
pub fn as_u8(self) -> u8 {
self as u8
}
}
pub fn manifest_bytes(entries: &[(CurveTag, Vec<u8>)]) -> Vec<u8> {
let set: BTreeSet<(u8, Vec<u8>)> = entries
.iter()
.filter(|(_, pk)| !pk.is_empty())
.map(|(tag, pk)| (tag.as_u8(), pk.clone()))
.collect();
let mut out = Vec::with_capacity(MANIFEST_DOMAIN.len() + 4 + set.len() * 70);
out.extend_from_slice(MANIFEST_DOMAIN);
out.extend_from_slice(&(set.len() as u32).to_be_bytes());
for (tag, pk) in &set {
out.push(*tag);
out.extend_from_slice(&(pk.len() as u32).to_be_bytes());
out.extend_from_slice(pk);
}
out
}
pub fn reconstruct_reportdata(
pubkey_manifest: &[u8],
image_digests: &[u8],
report_data: &[u8],
) -> [u8; 64] {
let mut h = Sha512::new();
h.update(REPORTDATA_DOMAIN);
h.update(Sha256::digest(pubkey_manifest));
h.update(Sha256::digest(image_digests));
h.update(Sha256::digest(report_data));
let out = h.finalize();
let mut rd = [0u8; 64];
rd.copy_from_slice(&out);
rd
}
pub fn expected_reportdata(
pubkeys: &[(CurveTag, Vec<u8>)],
image_digests: &[u8],
report_data: &[u8],
) -> [u8; 64] {
reconstruct_reportdata(&manifest_bytes(pubkeys), image_digests, report_data)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn manifest_empty_is_domain_plus_zero_count() {
let mut expected = Vec::new();
expected.extend_from_slice(MANIFEST_DOMAIN);
expected.extend_from_slice(&[0, 0, 0, 0]);
assert_eq!(manifest_bytes(&[]), expected);
}
#[test]
fn manifest_one_key_exact_bytes() {
let pk = b"key!".to_vec();
let mut expected = Vec::new();
expected.extend_from_slice(MANIFEST_DOMAIN);
expected.extend_from_slice(&1u32.to_be_bytes()); expected.push(0x01); expected.extend_from_slice(&4u32.to_be_bytes()); expected.extend_from_slice(&pk);
assert_eq!(manifest_bytes(&[(CurveTag::Secp256k1, pk)]), expected);
}
#[test]
fn manifest_is_order_independent_and_dedups() {
let a = (CurveTag::Secp256k1, b"aaaa".to_vec());
let b = (CurveTag::Ed25519, b"bbbb".to_vec());
let one = manifest_bytes(&[a.clone(), b.clone()]);
let two = manifest_bytes(&[b.clone(), a.clone(), a.clone()]); assert_eq!(one, two);
assert_eq!(
&two[MANIFEST_DOMAIN.len()..MANIFEST_DOMAIN.len() + 4],
&[0, 0, 0, 2]
);
}
#[test]
fn manifest_distinguishes_curve_tag() {
let m = manifest_bytes(&[
(CurveTag::Secp256k1, b"same".to_vec()),
(CurveTag::Ed25519, b"same".to_vec()),
]);
assert_eq!(
&m[MANIFEST_DOMAIN.len()..MANIFEST_DOMAIN.len() + 4],
&[0, 0, 0, 2]
);
}
#[test]
fn manifest_skips_empty_pubkeys() {
assert_eq!(
manifest_bytes(&[(CurveTag::Ed25519, Vec::new())]),
manifest_bytes(&[])
);
}
#[test]
fn reportdata_matches_independent_sha_assembly() {
let manifest = b"manifest-bytes";
let images = b"img-digests";
let rdata = b"nonce";
let got = reconstruct_reportdata(manifest, images, rdata);
let mut h = Sha512::new();
h.update(REPORTDATA_DOMAIN);
h.update(Sha256::digest(manifest));
h.update(Sha256::digest(images));
h.update(Sha256::digest(rdata));
let want: [u8; 64] = h.finalize().into();
assert_eq!(got, want);
}
#[test]
fn reportdata_is_sensitive_to_each_input_and_deterministic() {
let base = expected_reportdata(&[(CurveTag::Secp256k1, b"k".to_vec())], b"img", b"n");
assert_eq!(
base,
expected_reportdata(&[(CurveTag::Secp256k1, b"k".to_vec())], b"img", b"n")
);
assert_ne!(
base,
expected_reportdata(&[(CurveTag::Secp256k1, b"K".to_vec())], b"img", b"n")
);
assert_ne!(
base,
expected_reportdata(&[(CurveTag::Secp256k1, b"k".to_vec())], b"IMG", b"n")
);
assert_ne!(
base,
expected_reportdata(&[(CurveTag::Secp256k1, b"k".to_vec())], b"img", b"N")
);
}
}