use base64::prelude::*;
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::CompressedRistretto;
use curve25519_dalek::scalar::Scalar;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha512};
pub const ZK_DOMAIN_SEPARATOR: &[u8] = b"SBM_ZK_PLATFORM_ATTESTATION_V1";
pub const STANDARD_POLICY_ROOT: [u8; 32] = [
0x53, 0x42, 0x4d, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x52, 0x4f, 0x4f, 0x54, 0x5f, 0x53, 0x45, 0x43, 0x55, 0x52, 0x45, 0x5f, 0x42, 0x4f, 0x4f, 0x54, 0x5f, 0x56, 0x30, 0x30, 0x31, ];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ZkProof {
pub commitment_r: [u8; 32],
pub response_s: [u8; 32],
pub platform_commitment_x: [u8; 32],
pub policy_root: [u8; 32],
pub epoch: u64,
}
impl ZkProof {
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(104);
buf.extend_from_slice(&self.commitment_r);
buf.extend_from_slice(&self.response_s);
buf.extend_from_slice(&self.platform_commitment_x);
buf.extend_from_slice(&self.policy_root);
buf.extend_from_slice(&self.epoch.to_be_bytes());
buf
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
if bytes.len() < 136 {
return Err(format!(
"Invalid ZK proof length: expected 136 bytes, got {}",
bytes.len()
));
}
let mut r = [0u8; 32];
let mut s = [0u8; 32];
let mut x = [0u8; 32];
let mut root = [0u8; 32];
let mut epoch_bytes = [0u8; 8];
r.copy_from_slice(&bytes[0..32]);
s.copy_from_slice(&bytes[32..64]);
x.copy_from_slice(&bytes[64..96]);
root.copy_from_slice(&bytes[96..128]);
epoch_bytes.copy_from_slice(&bytes[128..136]);
Ok(Self {
commitment_r: r,
response_s: s,
platform_commitment_x: x,
policy_root: root,
epoch: u64::from_be_bytes(epoch_bytes),
})
}
pub fn to_base64(&self) -> String {
BASE64_STANDARD.encode(self.to_bytes())
}
}
pub struct PlatformWitness {
pub platform_secret: Scalar,
pub integrity_measurement: [u8; 32],
}
impl PlatformWitness {
pub fn generate_fresh() -> Self {
let mut rng = OsRng;
let secret = Scalar::random(&mut rng);
Self {
platform_secret: secret,
integrity_measurement: STANDARD_POLICY_ROOT,
}
}
pub fn platform_commitment(&self) -> CompressedRistretto {
(self.platform_secret * RISTRETTO_BASEPOINT_POINT).compress()
}
}
pub struct VerificationStatement<'a> {
pub target_peer_pubkey: &'a [u8; 32],
pub session_nonce: u64,
pub expected_policy_root: &'a [u8; 32],
}
pub fn generate_zk_proof(
witness: &PlatformWitness,
peer_pubkey: &[u8; 32],
session_nonce: u64,
epoch: u64,
) -> ZkProof {
let mut rng = OsRng;
let r = Scalar::random(&mut rng);
let big_r = (r * RISTRETTO_BASEPOINT_POINT).compress();
let big_x = witness.platform_commitment();
let challenge = compute_fiat_shamir_challenge(
&big_x.to_bytes(),
&big_r.to_bytes(),
&witness.integrity_measurement,
peer_pubkey,
session_nonce,
epoch,
);
let s = r + (challenge * witness.platform_secret);
ZkProof {
commitment_r: big_r.to_bytes(),
response_s: s.to_bytes(),
platform_commitment_x: big_x.to_bytes(),
policy_root: witness.integrity_measurement,
epoch,
}
}
pub fn verify_zk_proof(proof: &ZkProof, statement: &VerificationStatement) -> bool {
if proof.policy_root != *statement.expected_policy_root {
return false;
}
let r_point = match CompressedRistretto(proof.commitment_r).decompress() {
Some(p) => p,
None => return false,
};
let x_point = match CompressedRistretto(proof.platform_commitment_x).decompress() {
Some(p) => p,
None => return false,
};
let s_scalar: Option<Scalar> = Scalar::from_canonical_bytes(proof.response_s).into();
let s_scalar = match s_scalar {
Some(s) => s,
None => return false,
};
let challenge = compute_fiat_shamir_challenge(
&proof.platform_commitment_x,
&proof.commitment_r,
&proof.policy_root,
statement.target_peer_pubkey,
statement.session_nonce,
proof.epoch,
);
let lhs = s_scalar * RISTRETTO_BASEPOINT_POINT;
let rhs = r_point + (challenge * x_point);
lhs == rhs
}
fn compute_fiat_shamir_challenge(
x_bytes: &[u8; 32],
r_bytes: &[u8; 32],
policy_root: &[u8; 32],
peer_pubkey: &[u8; 32],
session_nonce: u64,
epoch: u64,
) -> Scalar {
let mut hasher = Sha512::new();
hasher.update(ZK_DOMAIN_SEPARATOR);
hasher.update(x_bytes);
hasher.update(r_bytes);
hasher.update(policy_root);
hasher.update(peer_pubkey);
hasher.update(session_nonce.to_be_bytes());
hasher.update(epoch.to_be_bytes());
let hash: [u8; 64] = hasher.finalize().into();
Scalar::from_bytes_mod_order_wide(&hash)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zk_proof_lifecycle() {
let witness = PlatformWitness::generate_fresh();
let peer_pub = [0x42u8; 32];
let session_nonce = 1001u64;
let epoch = 1;
let proof = generate_zk_proof(&witness, &peer_pub, session_nonce, epoch);
let statement = VerificationStatement {
target_peer_pubkey: &peer_pub,
session_nonce,
expected_policy_root: &STANDARD_POLICY_ROOT,
};
assert!(verify_zk_proof(&proof, &statement));
}
#[test]
fn test_zk_tampering_rejection() {
let witness = PlatformWitness::generate_fresh();
let peer_pub = [0x42u8; 32];
let session_nonce = 1001u64;
let epoch = 1;
let proof = generate_zk_proof(&witness, &peer_pub, session_nonce, epoch);
let wrong_peer = [0x99u8; 32];
let bad_statement_peer = VerificationStatement {
target_peer_pubkey: &wrong_peer,
session_nonce,
expected_policy_root: &STANDARD_POLICY_ROOT,
};
assert!(!verify_zk_proof(&proof, &bad_statement_peer));
let bad_statement_nonce = VerificationStatement {
target_peer_pubkey: &peer_pub,
session_nonce: 1002u64,
expected_policy_root: &STANDARD_POLICY_ROOT,
};
assert!(!verify_zk_proof(&proof, &bad_statement_nonce));
let mut tampered_proof = proof.clone();
tampered_proof.response_s[0] ^= 0xFF;
let valid_statement = VerificationStatement {
target_peer_pubkey: &peer_pub,
session_nonce,
expected_policy_root: &STANDARD_POLICY_ROOT,
};
assert!(!verify_zk_proof(&tampered_proof, &valid_statement));
}
}