use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use crate::adapter::net::identity::{EntityId, MAX_TOKEN_CLOCK_SKEW_SECS};
pub const ORG_CERT_SIG_DOMAIN: &[u8] = b"net-org-cert-v1";
pub const ORG_FLOORS_SIG_DOMAIN: &[u8] = b"net-org-floors-v1";
pub const ORG_CERT_TTL_SECS_RECOMMENDED: u64 = 365 * 24 * 60 * 60;
pub const MAX_ORG_CERT_TTL_SECS: u64 = 2 * 365 * 24 * 60 * 60;
pub const MAX_REVOCATION_FLOORS_PER_BUNDLE: usize = 65_536;
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct OrgId(pub [u8; 32]);
impl OrgId {
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[inline]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn verifying_key(&self) -> Result<VerifyingKey, OrgError> {
VerifyingKey::from_bytes(&self.0).map_err(|_| OrgError::InvalidPublicKey)
}
pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), OrgError> {
let vk = self.verifying_key()?;
vk.verify_strict(message, signature)
.map_err(|_| OrgError::InvalidSignature)
}
}
impl std::fmt::Debug for OrgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "OrgId({})", hex_short(&self.0))
}
}
impl std::fmt::Display for OrgId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&hex::encode(self.0))
}
}
impl serde::Serialize for OrgId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(self.0))
} else {
serializer.serialize_bytes(&self.0)
}
}
}
impl<'de> serde::Deserialize<'de> for OrgId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = if deserializer.is_human_readable() {
let hex_str = String::deserialize(deserializer)?;
hex::decode(&hex_str).map_err(serde::de::Error::custom)?
} else {
<Vec<u8>>::deserialize(deserializer)?
};
if bytes.len() != 32 {
return Err(serde::de::Error::custom("org_id must be 32 bytes"));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(OrgId(arr))
}
}
pub struct OrgKeypair {
signing_key: SigningKey,
org_id: OrgId,
}
impl OrgKeypair {
pub fn generate() -> Self {
let mut rng_bytes = [0u8; 32];
if let Err(e) = getrandom::fill(&mut rng_bytes) {
eprintln!(
"FATAL: OrgKeypair::generate getrandom failure ({e:?}); aborting to avoid weak ed25519 secret"
);
std::process::abort();
}
let signing_key = SigningKey::from_bytes(&rng_bytes);
for byte in rng_bytes.iter_mut() {
unsafe { std::ptr::write_volatile(byte, 0) };
}
Self::from_signing_key(signing_key)
}
pub fn from_signing_key(signing_key: SigningKey) -> Self {
let org_id = OrgId::from_bytes(signing_key.verifying_key().to_bytes());
Self {
signing_key,
org_id,
}
}
pub fn from_bytes(secret: [u8; 32]) -> Self {
Self::from_signing_key(SigningKey::from_bytes(&secret))
}
#[inline]
pub fn org_id(&self) -> OrgId {
self.org_id
}
pub fn secret_bytes(&self) -> &[u8; 32] {
self.signing_key.as_bytes()
}
pub(crate) fn sign(&self, message: &[u8]) -> Signature {
self.signing_key.sign(message)
}
}
impl std::fmt::Debug for OrgKeypair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrgKeypair")
.field("org_id", &self.org_id)
.field("secret", &"[REDACTED]")
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrgError {
InvalidFormat,
InvalidPublicKey,
InvalidSignature,
ZeroTtl,
TtlTooLong,
InvalidValidityWindow,
ClockSkewTooLarge,
NotYetValid,
Expired,
TooManyFloors,
NonCanonicalFloors,
ReservedGrantId,
DiscoveryBindingMismatch,
UnknownRights,
EmptyRights,
TargetOrgNotIssuer,
}
impl std::fmt::Display for OrgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidFormat => write!(f, "invalid wire format"),
Self::InvalidPublicKey => write!(f, "invalid org public key"),
Self::InvalidSignature => write!(f, "invalid signature"),
Self::ZeroTtl => write!(f, "certificate TTL is zero"),
Self::TtlTooLong => write!(
f,
"certificate validity window exceeds MAX_ORG_CERT_TTL_SECS"
),
Self::InvalidValidityWindow => write!(
f,
"certificate validity window is zero or reversed (not_after <= not_before)"
),
Self::ClockSkewTooLarge => {
write!(f, "clock-skew tolerance exceeds MAX_TOKEN_CLOCK_SKEW_SECS")
}
Self::NotYetValid => write!(f, "certificate not yet valid"),
Self::Expired => write!(f, "certificate expired"),
Self::TooManyFloors => write!(
f,
"revocation bundle exceeds MAX_REVOCATION_FLOORS_PER_BUNDLE"
),
Self::NonCanonicalFloors => {
write!(f, "revocation bundle floors are not in canonical order")
}
Self::ReservedGrantId => {
write!(f, "grant_id zero is reserved (owner-audience sentinel)")
}
Self::DiscoveryBindingMismatch => write!(
f,
"grant violates rights ⊇ DISCOVER ⇔ discovery binding present"
),
Self::UnknownRights => write!(f, "grant rights carry unknown bits"),
Self::EmptyRights => write!(f, "grant rights are empty"),
Self::TargetOrgNotIssuer => write!(
f,
"grant AnyNodeOwnedBy target org is not the issuer org (permanently unusable)"
),
}
}
}
impl std::error::Error for OrgError {}
#[derive(Clone, PartialEq, Eq)]
pub struct OrgMembershipCert {
pub org_id: OrgId,
pub member: EntityId,
pub not_before: u64,
pub not_after: u64,
pub generation: u32,
pub nonce: u64,
pub signature: [u8; 64],
}
impl OrgMembershipCert {
const SIGNED_PAYLOAD_SIZE: usize = 32 + 32 + 8 + 8 + 4 + 8;
const SIGNING_INPUT_SIZE: usize = ORG_CERT_SIG_DOMAIN.len() + Self::SIGNED_PAYLOAD_SIZE;
pub const WIRE_SIZE: usize = Self::SIGNED_PAYLOAD_SIZE + 64;
pub fn try_issue(
org: &OrgKeypair,
member: EntityId,
generation: u32,
duration_secs: u64,
) -> Result<Self, OrgError> {
if duration_secs == 0 {
return Err(OrgError::ZeroTtl);
}
if duration_secs > MAX_ORG_CERT_TTL_SECS {
return Err(OrgError::TtlTooLong);
}
let mut nonce_bytes = [0u8; 8];
if let Err(e) = getrandom::fill(&mut nonce_bytes) {
eprintln!(
"FATAL: OrgMembershipCert nonce getrandom failure ({e:?}); aborting to avoid predictable cert nonce"
);
std::process::abort();
}
let nonce = u64::from_le_bytes(nonce_bytes);
let now = current_timestamp();
Ok(Self::issue_at(
org,
member,
generation,
now,
now.saturating_add(duration_secs),
nonce,
))
}
pub(crate) fn issue_at(
org: &OrgKeypair,
member: EntityId,
generation: u32,
not_before: u64,
not_after: u64,
nonce: u64,
) -> Self {
let mut cert = Self {
org_id: org.org_id(),
member,
not_before,
not_after,
generation,
nonce,
signature: [0u8; 64],
};
cert.signature = org.sign(&cert.signing_input()).to_bytes();
cert
}
pub(crate) fn signed_payload(&self) -> [u8; Self::SIGNED_PAYLOAD_SIZE] {
let mut buf = [0u8; Self::SIGNED_PAYLOAD_SIZE];
let mut off = 0;
buf[off..off + 32].copy_from_slice(self.org_id.as_bytes());
off += 32;
buf[off..off + 32].copy_from_slice(self.member.as_bytes());
off += 32;
buf[off..off + 8].copy_from_slice(&self.not_before.to_le_bytes());
off += 8;
buf[off..off + 8].copy_from_slice(&self.not_after.to_le_bytes());
off += 8;
buf[off..off + 4].copy_from_slice(&self.generation.to_le_bytes());
off += 4;
buf[off..off + 8].copy_from_slice(&self.nonce.to_le_bytes());
buf
}
fn signing_input(&self) -> [u8; Self::SIGNING_INPUT_SIZE] {
let mut buf = [0u8; Self::SIGNING_INPUT_SIZE];
buf[..ORG_CERT_SIG_DOMAIN.len()].copy_from_slice(ORG_CERT_SIG_DOMAIN);
buf[ORG_CERT_SIG_DOMAIN.len()..].copy_from_slice(&self.signed_payload());
buf
}
pub fn verify(&self) -> Result<(), OrgError> {
if self.not_after <= self.not_before {
return Err(OrgError::InvalidValidityWindow);
}
if self.not_after - self.not_before > MAX_ORG_CERT_TTL_SECS {
return Err(OrgError::TtlTooLong);
}
let sig = Signature::from_bytes(&self.signature);
self.org_id.verify(&self.signing_input(), &sig)
}
pub fn is_valid_with_skew(&self, skew_secs: u64) -> Result<(), OrgError> {
self.is_valid_at_with_skew(current_timestamp(), skew_secs)
}
pub fn is_valid_at_with_skew(&self, now_secs: u64, skew_secs: u64) -> Result<(), OrgError> {
if skew_secs > MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(OrgError::ClockSkewTooLarge);
}
self.verify()?;
self.check_time_bounds_at(now_secs, skew_secs)
}
fn check_time_bounds_at(&self, now: u64, skew_secs: u64) -> Result<(), OrgError> {
if now < self.not_before.saturating_sub(skew_secs) {
return Err(OrgError::NotYetValid);
}
if now >= self.not_after.saturating_add(skew_secs) {
return Err(OrgError::Expired);
}
Ok(())
}
pub fn is_expired(&self) -> bool {
current_timestamp() >= self.not_after
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(Self::WIRE_SIZE);
buf.extend_from_slice(&self.signed_payload());
buf.extend_from_slice(&self.signature);
buf
}
#[expect(
clippy::unwrap_used,
reason = "data.len() == WIRE_SIZE checked above; fixed-offset slices convert infallibly to fixed-size arrays"
)]
pub fn from_bytes(data: &[u8]) -> Result<Self, OrgError> {
if data.len() != Self::WIRE_SIZE {
return Err(OrgError::InvalidFormat);
}
let org_id = OrgId::from_bytes(data[0..32].try_into().unwrap());
let member = EntityId::from_bytes(data[32..64].try_into().unwrap());
let not_before = u64::from_le_bytes(data[64..72].try_into().unwrap());
let not_after = u64::from_le_bytes(data[72..80].try_into().unwrap());
let generation = u32::from_le_bytes(data[80..84].try_into().unwrap());
let nonce = u64::from_le_bytes(data[84..92].try_into().unwrap());
let mut signature = [0u8; 64];
signature.copy_from_slice(&data[92..156]);
Ok(Self {
org_id,
member,
not_before,
not_after,
generation,
nonce,
signature,
})
}
}
impl std::fmt::Debug for OrgMembershipCert {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrgMembershipCert")
.field("org_id", &self.org_id)
.field("member", &self.member)
.field("not_before", &self.not_before)
.field("not_after", &self.not_after)
.field("generation", &self.generation)
.field("nonce", &self.nonce)
.finish()
}
}
impl serde::Serialize for OrgMembershipCert {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let bytes = self.to_bytes();
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(&bytes))
} else {
serializer.serialize_bytes(&bytes)
}
}
}
impl<'de> serde::Deserialize<'de> for OrgMembershipCert {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = if deserializer.is_human_readable() {
let hex_str = String::deserialize(deserializer)?;
hex::decode(&hex_str).map_err(serde::de::Error::custom)?
} else {
<Vec<u8>>::deserialize(deserializer)?
};
Self::from_bytes(&bytes).map_err(serde::de::Error::custom)
}
}
pub type OrgFloor = (EntityId, u32);
#[derive(Clone, PartialEq, Eq)]
pub struct OrgRevocationBundle {
pub org_id: OrgId,
pub issued_at: u64,
floors: Vec<OrgFloor>,
pub signature: [u8; 64],
}
impl OrgRevocationBundle {
const HEADER_SIZE: usize = 32 + 8 + 4;
const FLOOR_ENTRY_SIZE: usize = 32 + 4;
pub fn try_issue(org: &OrgKeypair, floors: &BTreeMap<EntityId, u32>) -> Result<Self, OrgError> {
Self::issue_at(org, floors, current_timestamp())
}
pub(crate) fn issue_at(
org: &OrgKeypair,
floors: &BTreeMap<EntityId, u32>,
issued_at: u64,
) -> Result<Self, OrgError> {
if floors.len() > MAX_REVOCATION_FLOORS_PER_BUNDLE {
return Err(OrgError::TooManyFloors);
}
let mut bundle = Self {
org_id: org.org_id(),
issued_at,
floors: floors.iter().map(|(m, g)| (m.clone(), *g)).collect(),
signature: [0u8; 64],
};
bundle.signature = org.sign(&bundle.signing_input()).to_bytes();
Ok(bundle)
}
pub fn floors(&self) -> &[OrgFloor] {
&self.floors
}
fn signed_payload(&self) -> Vec<u8> {
let mut buf =
Vec::with_capacity(Self::HEADER_SIZE + self.floors.len() * Self::FLOOR_ENTRY_SIZE);
buf.extend_from_slice(self.org_id.as_bytes());
buf.extend_from_slice(&self.issued_at.to_le_bytes());
buf.extend_from_slice(&(self.floors.len() as u32).to_le_bytes());
for (member, floor) in &self.floors {
buf.extend_from_slice(member.as_bytes());
buf.extend_from_slice(&floor.to_le_bytes());
}
buf
}
fn signing_input(&self) -> Vec<u8> {
let payload = self.signed_payload();
let mut buf = Vec::with_capacity(ORG_FLOORS_SIG_DOMAIN.len() + payload.len());
buf.extend_from_slice(ORG_FLOORS_SIG_DOMAIN);
buf.extend_from_slice(&payload);
buf
}
pub fn verify(&self) -> Result<(), OrgError> {
if self.floors.len() > MAX_REVOCATION_FLOORS_PER_BUNDLE {
return Err(OrgError::TooManyFloors);
}
if !floors_strictly_ascending(&self.floors) {
return Err(OrgError::NonCanonicalFloors);
}
let sig = Signature::from_bytes(&self.signature);
self.org_id.verify(&self.signing_input(), &sig)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf = self.signed_payload();
buf.extend_from_slice(&self.signature);
buf
}
#[expect(
clippy::unwrap_used,
reason = "lengths are checked against the exact expected size above; fixed-offset slices convert infallibly to fixed-size arrays"
)]
pub fn from_bytes(data: &[u8]) -> Result<Self, OrgError> {
if data.len() < Self::HEADER_SIZE + 64 {
return Err(OrgError::InvalidFormat);
}
let org_id = OrgId::from_bytes(data[0..32].try_into().unwrap());
let issued_at = u64::from_le_bytes(data[32..40].try_into().unwrap());
let count = u32::from_le_bytes(data[40..44].try_into().unwrap()) as usize;
if count > MAX_REVOCATION_FLOORS_PER_BUNDLE {
return Err(OrgError::TooManyFloors);
}
let expected = Self::HEADER_SIZE + count * Self::FLOOR_ENTRY_SIZE + 64;
if data.len() != expected {
return Err(OrgError::InvalidFormat);
}
let mut floors = Vec::with_capacity(count);
let mut off = Self::HEADER_SIZE;
for _ in 0..count {
let member = EntityId::from_bytes(data[off..off + 32].try_into().unwrap());
let floor = u32::from_le_bytes(data[off + 32..off + 36].try_into().unwrap());
floors.push((member, floor));
off += Self::FLOOR_ENTRY_SIZE;
}
if !floors_strictly_ascending(&floors) {
return Err(OrgError::NonCanonicalFloors);
}
let mut signature = [0u8; 64];
signature.copy_from_slice(&data[off..off + 64]);
Ok(Self {
org_id,
issued_at,
floors,
signature,
})
}
}
impl std::fmt::Debug for OrgRevocationBundle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrgRevocationBundle")
.field("org_id", &self.org_id)
.field("issued_at", &self.issued_at)
.field("floors", &self.floors.len())
.finish()
}
}
impl serde::Serialize for OrgRevocationBundle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let bytes = self.to_bytes();
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(&bytes))
} else {
serializer.serialize_bytes(&bytes)
}
}
}
impl<'de> serde::Deserialize<'de> for OrgRevocationBundle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = if deserializer.is_human_readable() {
let hex_str = String::deserialize(deserializer)?;
hex::decode(&hex_str).map_err(serde::de::Error::custom)?
} else {
<Vec<u8>>::deserialize(deserializer)?
};
Self::from_bytes(&bytes).map_err(serde::de::Error::custom)
}
}
fn floors_strictly_ascending(floors: &[OrgFloor]) -> bool {
floors.windows(2).all(|w| w[0].0 < w[1].0)
}
fn hex_short(bytes: &[u8]) -> String {
bytes
.iter()
.take(8)
.map(|b| format!("{:02x}", b))
.collect::<String>()
+ "..."
}
pub(crate) fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use super::*;
fn org() -> OrgKeypair {
OrgKeypair::from_bytes([0x42u8; 32])
}
fn member() -> EntityId {
EntityId::from_bytes([0x24u8; 32])
}
fn valid_cert() -> OrgMembershipCert {
OrgMembershipCert::try_issue(&org(), member(), 5, ORG_CERT_TTL_SECS_RECOMMENDED)
.expect("issue")
}
#[test]
fn org_id_round_trips_serde_json_and_postcard() {
let id = org().org_id();
let json = serde_json::to_string(&id).expect("json");
assert_eq!(json, format!("\"{}\"", hex::encode(id.as_bytes())));
let back: OrgId = serde_json::from_str(&json).expect("json back");
assert_eq!(back, id);
let bin = postcard::to_allocvec(&id).expect("postcard");
let back: OrgId = postcard::from_bytes(&bin).expect("postcard back");
assert_eq!(back, id);
}
#[test]
fn org_id_rejects_wrong_length_serde() {
let short: Result<OrgId, _> = serde_json::from_str("\"5a5a\"");
assert!(short.is_err());
}
#[test]
fn org_keypair_deterministic_from_seed_and_distinct_on_generate() {
assert_eq!(
OrgKeypair::from_bytes([7u8; 32]).org_id(),
OrgKeypair::from_bytes([7u8; 32]).org_id()
);
assert_ne!(
OrgKeypair::generate().org_id(),
OrgKeypair::generate().org_id()
);
}
#[test]
fn cert_issue_verify_and_wire_round_trip() {
let cert = valid_cert();
cert.verify().expect("fresh cert verifies");
cert.is_valid_with_skew(0).expect("fresh cert in window");
let bytes = cert.to_bytes();
assert_eq!(bytes.len(), OrgMembershipCert::WIRE_SIZE);
let back = OrgMembershipCert::from_bytes(&bytes).expect("decode");
assert_eq!(back, cert);
back.verify().expect("decoded cert verifies");
}
#[test]
fn cert_wire_size_is_pinned() {
assert_eq!(OrgMembershipCert::WIRE_SIZE, 156);
}
#[test]
fn cert_from_bytes_rejects_truncation_and_trailing() {
let bytes = valid_cert().to_bytes();
for cut in 0..bytes.len() {
assert_eq!(
OrgMembershipCert::from_bytes(&bytes[..cut]),
Err(OrgError::InvalidFormat),
"truncation at {cut} must not decode"
);
}
let mut trailing = bytes.clone();
trailing.push(0);
assert_eq!(
OrgMembershipCert::from_bytes(&trailing),
Err(OrgError::InvalidFormat)
);
}
#[test]
fn cert_every_signed_field_is_tamper_evident() {
type CertMutation = (&'static str, fn(&mut OrgMembershipCert));
let mutations: [CertMutation; 6] = [
("org_id", |c| c.org_id.0[0] ^= 1),
("member", |c| c.member.0[0] ^= 1),
("not_before", |c| c.not_before ^= 1),
("not_after", |c| c.not_after ^= 1),
("generation", |c| c.generation ^= 1),
("nonce", |c| c.nonce ^= 1),
];
for (field, mutate) in mutations {
let mut tampered = valid_cert();
mutate(&mut tampered);
assert!(
tampered.verify().is_err(),
"tampered {field} must fail verification"
);
}
let mut tampered = valid_cert();
tampered.signature[0] ^= 1;
assert_eq!(tampered.verify(), Err(OrgError::InvalidSignature));
}
#[test]
fn cert_signature_domain_is_load_bearing() {
let mut cert = valid_cert();
cert.signature = org().sign(&cert.signed_payload()).to_bytes();
assert_eq!(cert.verify(), Err(OrgError::InvalidSignature));
let mut input = Vec::new();
input.extend_from_slice(ORG_FLOORS_SIG_DOMAIN);
input.extend_from_slice(&cert.signed_payload());
cert.signature = org().sign(&input).to_bytes();
assert_eq!(cert.verify(), Err(OrgError::InvalidSignature));
}
#[test]
fn cert_wrong_org_key_fails() {
let mut cert = valid_cert();
let other = OrgKeypair::from_bytes([9u8; 32]);
cert.signature = other.sign(&cert.signing_input()).to_bytes();
assert_eq!(cert.verify(), Err(OrgError::InvalidSignature));
}
#[test]
fn cert_ttl_enforced_at_issue_and_verify() {
assert_eq!(
OrgMembershipCert::try_issue(&org(), member(), 0, 0),
Err(OrgError::ZeroTtl)
);
assert_eq!(
OrgMembershipCert::try_issue(&org(), member(), 0, MAX_ORG_CERT_TTL_SECS + 1),
Err(OrgError::TtlTooLong)
);
let now = current_timestamp();
let immortal = OrgMembershipCert::issue_at(
&org(),
member(),
0,
now,
now + MAX_ORG_CERT_TTL_SECS + 1,
7,
);
assert_eq!(immortal.verify(), Err(OrgError::TtlTooLong));
let at_max =
OrgMembershipCert::issue_at(&org(), member(), 0, now, now + MAX_ORG_CERT_TTL_SECS, 7);
at_max.verify().expect("window of exactly MAX is legal");
}
#[test]
fn cert_time_bounds_and_skew() {
let now = current_timestamp();
let future = OrgMembershipCert::issue_at(&org(), member(), 0, now + 100, now + 200, 1);
assert_eq!(future.is_valid_with_skew(0), Err(OrgError::NotYetValid));
future.is_valid_with_skew(120).expect("skew admits future");
let past = OrgMembershipCert::issue_at(&org(), member(), 0, now - 200, now - 100, 1);
assert_eq!(past.is_valid_with_skew(0), Err(OrgError::Expired));
assert!(past.is_expired());
past.is_valid_with_skew(120).expect("skew admits recent");
let boundary = OrgMembershipCert::issue_at(&org(), member(), 0, now - 100, now, 1);
assert_eq!(boundary.is_valid_with_skew(0), Err(OrgError::Expired));
}
#[test]
fn skew_ceiling_enforced_at_and_above_max() {
let now = current_timestamp();
let past = OrgMembershipCert::issue_at(&org(), member(), 0, now - 200, now - 100, 1);
past.is_valid_with_skew(MAX_TOKEN_CLOCK_SKEW_SECS)
.expect("skew of exactly MAX is legal");
assert_eq!(
past.is_valid_with_skew(MAX_TOKEN_CLOCK_SKEW_SECS + 1),
Err(OrgError::ClockSkewTooLarge)
);
assert_eq!(
past.is_valid_with_skew(u64::MAX),
Err(OrgError::ClockSkewTooLarge)
);
assert_eq!(
valid_cert().is_valid_with_skew(MAX_TOKEN_CLOCK_SKEW_SECS + 1),
Err(OrgError::ClockSkewTooLarge)
);
}
#[test]
fn zero_and_reversed_windows_are_structurally_invalid() {
let now = current_timestamp();
let zero = OrgMembershipCert::issue_at(&org(), member(), 0, now, now, 1);
assert_eq!(zero.verify(), Err(OrgError::InvalidValidityWindow));
let reversed = OrgMembershipCert::issue_at(&org(), member(), 0, now + 100, now - 100, 1);
assert_eq!(reversed.verify(), Err(OrgError::InvalidValidityWindow));
assert_eq!(
reversed.is_valid_with_skew(120),
Err(OrgError::InvalidValidityWindow)
);
valid_cert().verify().expect("positive window verifies");
}
#[test]
fn cert_serde_json_and_postcard_round_trip() {
let cert = valid_cert();
let json = serde_json::to_string(&cert).expect("json");
assert_eq!(json, format!("\"{}\"", hex::encode(cert.to_bytes())));
let back: OrgMembershipCert = serde_json::from_str(&json).expect("json back");
assert_eq!(back, cert);
let bin = postcard::to_allocvec(&cert).expect("postcard");
let back: OrgMembershipCert = postcard::from_bytes(&bin).expect("postcard back");
assert_eq!(back, cert);
}
#[test]
fn cert_serde_rejects_malformed() {
let short = format!("\"{}\"", hex::encode([0u8; 10]));
assert!(serde_json::from_str::<OrgMembershipCert>(&short).is_err());
assert!(serde_json::from_str::<OrgMembershipCert>("\"zz\"").is_err());
}
fn floors_fixture() -> BTreeMap<EntityId, u32> {
let mut floors = BTreeMap::new();
floors.insert(EntityId::from_bytes([1u8; 32]), 3);
floors.insert(EntityId::from_bytes([2u8; 32]), 7);
floors.insert(EntityId::from_bytes([3u8; 32]), 1);
floors
}
#[test]
fn bundle_issue_verify_and_wire_round_trip() {
let bundle = OrgRevocationBundle::try_issue(&org(), &floors_fixture()).expect("issue");
bundle.verify().expect("fresh bundle verifies");
assert_eq!(bundle.floors().len(), 3);
let bytes = bundle.to_bytes();
assert_eq!(bytes.len(), 32 + 8 + 4 + 3 * 36 + 64);
let back = OrgRevocationBundle::from_bytes(&bytes).expect("decode");
assert_eq!(back, bundle);
back.verify().expect("decoded bundle verifies");
}
#[test]
fn bundle_empty_floors_is_legal() {
let bundle = OrgRevocationBundle::try_issue(&org(), &BTreeMap::new()).expect("issue");
bundle.verify().expect("empty bundle verifies");
let back = OrgRevocationBundle::from_bytes(&bundle.to_bytes()).expect("decode");
assert_eq!(back.floors().len(), 0);
}
#[test]
fn bundle_from_bytes_rejects_truncation_and_trailing() {
let bytes = OrgRevocationBundle::try_issue(&org(), &floors_fixture())
.expect("issue")
.to_bytes();
for cut in 0..bytes.len() {
assert!(
OrgRevocationBundle::from_bytes(&bytes[..cut]).is_err(),
"truncation at {cut} must not decode"
);
}
let mut trailing = bytes.clone();
trailing.push(0);
assert_eq!(
OrgRevocationBundle::from_bytes(&trailing),
Err(OrgError::InvalidFormat)
);
}
#[test]
fn bundle_rejects_non_canonical_floor_order() {
let bundle = OrgRevocationBundle::try_issue(&org(), &floors_fixture()).expect("issue");
let mut bytes = bundle.to_bytes();
let header = 32 + 8 + 4;
let (a, b) = (header, header + 36);
let first: Vec<u8> = bytes[a..a + 36].to_vec();
let second: Vec<u8> = bytes[b..b + 36].to_vec();
bytes[a..a + 36].copy_from_slice(&second);
bytes[b..b + 36].copy_from_slice(&first);
assert_eq!(
OrgRevocationBundle::from_bytes(&bytes),
Err(OrgError::NonCanonicalFloors)
);
let mut dup = bundle.to_bytes();
let entry: Vec<u8> = dup[a..a + 36].to_vec();
dup[b..b + 36].copy_from_slice(&entry);
assert_eq!(
OrgRevocationBundle::from_bytes(&dup),
Err(OrgError::NonCanonicalFloors)
);
}
#[test]
fn bundle_count_cap_checked_before_allocation() {
let mut data = Vec::new();
data.extend_from_slice(&[0u8; 32]); data.extend_from_slice(&0u64.to_le_bytes()); data.extend_from_slice(&((MAX_REVOCATION_FLOORS_PER_BUNDLE as u32) + 1).to_le_bytes());
data.extend_from_slice(&[0u8; 64]); assert_eq!(
OrgRevocationBundle::from_bytes(&data),
Err(OrgError::TooManyFloors)
);
}
#[test]
fn bundle_tamper_evident() {
let bundle = OrgRevocationBundle::try_issue(&org(), &floors_fixture()).expect("issue");
let bytes = bundle.to_bytes();
for pos in 0..bytes.len() - 64 {
let mut tampered = bytes.clone();
tampered[pos] ^= 1;
let ok = OrgRevocationBundle::from_bytes(&tampered)
.and_then(|b| b.verify())
.is_ok();
assert!(!ok, "flipped byte {pos} must not survive decode+verify");
}
}
#[test]
fn bundle_signature_domain_is_load_bearing() {
let mut bundle = OrgRevocationBundle::try_issue(&org(), &floors_fixture()).expect("issue");
bundle.signature = org().sign(&bundle.signed_payload()).to_bytes();
assert_eq!(bundle.verify(), Err(OrgError::InvalidSignature));
let mut input = Vec::new();
input.extend_from_slice(ORG_CERT_SIG_DOMAIN);
input.extend_from_slice(&bundle.signed_payload());
bundle.signature = org().sign(&input).to_bytes();
assert_eq!(bundle.verify(), Err(OrgError::InvalidSignature));
}
#[test]
fn bundle_too_many_floors_rejected_at_issue() {
let mut floors = BTreeMap::new();
for i in 0..=MAX_REVOCATION_FLOORS_PER_BUNDLE {
let mut m = [0u8; 32];
m[0..8].copy_from_slice(&(i as u64).to_le_bytes());
floors.insert(EntityId::from_bytes(m), 1);
}
assert_eq!(
OrgRevocationBundle::try_issue(&org(), &floors),
Err(OrgError::TooManyFloors)
);
}
#[test]
fn bundle_serde_json_round_trip() {
let bundle = OrgRevocationBundle::try_issue(&org(), &floors_fixture()).expect("issue");
let json = serde_json::to_string(&bundle).expect("json");
let back: OrgRevocationBundle = serde_json::from_str(&json).expect("json back");
assert_eq!(back, bundle);
}
#[test]
fn golden_vector_cert() {
let cert = OrgMembershipCert::issue_at(
&org(),
member(),
5,
1_700_000_000,
1_731_536_000,
0x1122_3344_5566_7788,
);
assert_eq!(hex::encode(cert.to_bytes()), GOLDEN_CERT_HEX);
let decoded = OrgMembershipCert::from_bytes(
&hex::decode(GOLDEN_CERT_HEX).expect("golden hex decodes"),
)
.expect("golden bytes decode");
decoded.verify().expect("golden cert verifies");
}
#[test]
fn golden_vector_bundle() {
let bundle =
OrgRevocationBundle::issue_at(&org(), &floors_fixture(), 1_700_000_000).expect("issue");
assert_eq!(hex::encode(bundle.to_bytes()), GOLDEN_BUNDLE_HEX);
let decoded = OrgRevocationBundle::from_bytes(
&hex::decode(GOLDEN_BUNDLE_HEX).expect("golden hex decodes"),
)
.expect("golden bytes decode");
decoded.verify().expect("golden bundle verifies");
}
const GOLDEN_CERT_HEX: &str = "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12242424242424242424242424242424242424242424242424242424242424242400f15365000000008024356700000000050000008877665544332211b94f91f5cac0026eb101b68e5eed16d9e3f8d516d9b81add1de32ccf7508fe40f597be8370ba6ee871154348a5d2ea86335714277a60e8146de3b5576acd300c";
const GOLDEN_BUNDLE_HEX: &str = "2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db1200f153650000000003000000010101010101010101010101010101010101010101010101010101010101010103000000020202020202020202020202020202020202020202020202020202020202020207000000030303030303030303030303030303030303030303030303030303030303030301000000e9da4985e5d915871c1713e5c6ec815bf97c5c69ac943595f51c9c9faf7d26231220f9aaa5b5f42db19b0cf60a05502549c486a08b7c2495d3f38e7a453b0803";
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0
}
fn bytes32(&mut self) -> [u8; 32] {
let mut out = [0u8; 32];
for chunk in out.chunks_mut(8) {
chunk.copy_from_slice(&self.next().to_le_bytes()[..chunk.len()]);
}
out
}
}
#[test]
fn random_certs_and_bundles_round_trip() {
let mut rng = Lcg(0xDEAD_BEEF);
for _ in 0..64 {
let org = OrgKeypair::from_bytes(rng.bytes32());
let cert = OrgMembershipCert::issue_at(
&org,
EntityId::from_bytes(rng.bytes32()),
rng.next() as u32,
rng.next(),
rng.next(),
rng.next(),
);
let back = OrgMembershipCert::from_bytes(&cert.to_bytes()).expect("cert decode");
assert_eq!(back, cert);
let sig = Signature::from_bytes(&cert.signature);
cert.org_id
.verify(&cert.signing_input(), &sig)
.expect("sig verifies");
let mut floors = BTreeMap::new();
for _ in 0..(rng.next() % 8) {
floors.insert(EntityId::from_bytes(rng.bytes32()), rng.next() as u32);
}
let bundle = OrgRevocationBundle::issue_at(&org, &floors, rng.next()).expect("issue");
let back = OrgRevocationBundle::from_bytes(&bundle.to_bytes()).expect("decode");
assert_eq!(back, bundle);
back.verify().expect("bundle verifies");
}
}
}