extern crate alloc;
use alloc::vec::Vec;
use crate::identity_error::IdentityError;
use crate::plp::{pad_tau, EphemeralProjection, Verifier, ZkIdentityProof};
pub const WIRE_MAGIC: &[u8; 4] = crate::EIAB_MAGIC;
pub const WIRE_VERSION_PLP1: u8 = 1;
pub const CODEC_NAME: &str = "aethel-plp-1";
const HEADER_LEN: usize = 4 + 1 + 1 + 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Kind {
Projection = 0x01,
Proof = 0x02,
}
fn encode_envelope(kind: Kind, body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(HEADER_LEN + body.len());
out.extend_from_slice(WIRE_MAGIC);
out.push(WIRE_VERSION_PLP1);
out.push(kind as u8);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(body);
out
}
fn decode_envelope(bytes: &[u8], expected_kind: Kind) -> Result<&[u8], IdentityError> {
if bytes.len() < HEADER_LEN {
return Err(IdentityError::WireLengthMismatch);
}
if &bytes[0..4] != WIRE_MAGIC.as_slice() {
return Err(IdentityError::WireBadMagic);
}
if bytes[4] != WIRE_VERSION_PLP1 {
return Err(IdentityError::WireBadVersion);
}
if bytes[5] != expected_kind as u8 {
return Err(IdentityError::SerializationError);
}
let declared_len = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]) as usize;
let body = &bytes[HEADER_LEN..];
if body.len() != declared_len {
return Err(IdentityError::WireLengthMismatch);
}
Ok(body)
}
pub fn encode_projection(projection: &EphemeralProjection) -> Vec<u8> {
encode_envelope(Kind::Projection, &projection.to_bytes())
}
pub fn decode_projection(bytes: &[u8]) -> Result<EphemeralProjection, IdentityError> {
let body = decode_envelope(bytes, Kind::Projection)?;
EphemeralProjection::from_bytes(body)
}
pub fn encode_proof(proof: &ZkIdentityProof) -> Vec<u8> {
encode_envelope(Kind::Proof, &proof.to_bytes())
}
pub fn decode_proof(bytes: &[u8]) -> Result<ZkIdentityProof, IdentityError> {
let body = decode_envelope(bytes, Kind::Proof)?;
ZkIdentityProof::from_bytes(body)
}
pub fn verify_projection(
projection: &[u8],
proof: &[u8],
context: &[u8],
) -> Result<bool, IdentityError> {
let proj = decode_projection(projection)?;
let zk = decode_proof(proof)?;
if proj.tau != pad_tau(context) {
return Ok(false);
}
Ok(Verifier::verify(&proj, &zk))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plp::{MasterIdentity, Prover};
fn honest_projection_and_proof(
seed: &[u8; 32],
tau: &[u8],
rho: &[u8; 32],
) -> (EphemeralProjection, ZkIdentityProof) {
let identity = MasterIdentity::from_seed(seed);
let proj = identity.project_at_context(tau, rho);
let proof = Prover::prove_identity(&identity, &proj, seed).expect("honest proving");
(proj, proof)
}
#[test]
fn projection_round_trips_through_the_envelope() {
let (proj, _) = honest_projection_and_proof(&[0x11u8; 32], b"wire-ctx", &[0x22u8; 32]);
let encoded = encode_projection(&proj);
assert_eq!(&encoded[0..4], WIRE_MAGIC.as_slice());
assert_eq!(encoded[4], WIRE_VERSION_PLP1);
assert_eq!(encoded[5], Kind::Projection as u8);
let decoded = decode_projection(&encoded).expect("decode");
assert_eq!(decoded.tau, proj.tau);
assert_eq!(decoded.salt, proj.salt);
}
#[test]
fn proof_round_trips_through_the_envelope() {
let (_, proof) = honest_projection_and_proof(&[0x33u8; 32], b"wire-ctx-2", &[0x44u8; 32]);
let encoded = encode_proof(&proof);
assert_eq!(encoded[5], Kind::Proof as u8);
let decoded = decode_proof(&encoded).expect("decode");
assert_eq!(decoded.challenge_c.coeffs(), proof.challenge_c.coeffs());
}
#[test]
fn verify_projection_accepts_an_honest_pair() {
let seed = [0x55u8; 32];
let tau = b"verify-projection-honest";
let (proj, proof) = honest_projection_and_proof(&seed, tau, &[0x66u8; 32]);
let result = verify_projection(&encode_projection(&proj), &encode_proof(&proof), tau);
assert_eq!(result, Ok(true));
}
#[test]
fn verify_projection_rejects_the_wrong_context() {
let seed = [0x55u8; 32];
let tau = b"verify-projection-honest";
let (proj, proof) = honest_projection_and_proof(&seed, tau, &[0x66u8; 32]);
let result = verify_projection(
&encode_projection(&proj),
&encode_proof(&proof),
b"a-different-context",
);
assert_eq!(result, Ok(false));
}
#[test]
fn verify_projection_rejects_a_tampered_proof() {
let seed = [0x77u8; 32];
let tau = b"verify-projection-tamper";
let (proj, proof) = honest_projection_and_proof(&seed, tau, &[0x88u8; 32]);
let mut proof_bytes = encode_proof(&proof);
let last = proof_bytes.len() - 1;
proof_bytes[last] ^= 0x01;
if let Ok(v) = verify_projection(&encode_projection(&proj), &proof_bytes, tau) {
assert!(!v, "a tampered proof verified");
}
}
#[test]
fn wrong_magic_is_rejected() {
let (proj, _) = honest_projection_and_proof(&[0x99u8; 32], b"magic-ctx", &[0xAAu8; 32]);
let mut encoded = encode_projection(&proj);
encoded[0] ^= 0xFF;
assert_eq!(
decode_projection(&encoded).err(),
Some(IdentityError::WireBadMagic)
);
}
#[test]
fn wrong_version_is_rejected() {
let (proj, _) = honest_projection_and_proof(&[0xBBu8; 32], b"version-ctx", &[0xCCu8; 32]);
let mut encoded = encode_projection(&proj);
encoded[4] = 0xFF;
assert_eq!(
decode_projection(&encoded).err(),
Some(IdentityError::WireBadVersion)
);
}
#[test]
fn declared_length_mismatch_is_rejected() {
let (proj, _) = honest_projection_and_proof(&[0xDDu8; 32], b"length-ctx", &[0xEEu8; 32]);
let mut encoded = encode_projection(&proj);
encoded[6] = 0xFF;
encoded[7] = 0xFF;
assert_eq!(
decode_projection(&encoded).err(),
Some(IdentityError::WireLengthMismatch)
);
}
#[test]
fn truncated_envelope_is_rejected() {
assert_eq!(
decode_projection(&[0u8; 3]).err(),
Some(IdentityError::WireLengthMismatch)
);
}
#[test]
fn decode_projection_refuses_proof_shaped_bytes() {
let (_, proof) = honest_projection_and_proof(&[0x12u8; 32], b"kind-ctx", &[0x34u8; 32]);
let proof_bytes = encode_proof(&proof);
assert_eq!(
decode_projection(&proof_bytes).err(),
Some(IdentityError::SerializationError)
);
}
}