use metamorphic_crypto::{SignatureLevel, Suite};
use crate::coniks::Namespace;
use crate::error::{Error, Result};
use crate::leaf::{ContextLabel, content_hash};
use crate::merkle::{Hash, hash_leaf};
pub const POLICY_FORMAT_VERSION: u32 = 1;
pub const SIGNED_POLICY_FORMAT_VERSION: u32 = 1;
pub const POLICY_HASH_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SecurityLevel {
Cat3,
Cat5,
}
impl SecurityLevel {
const TAG_CAT3: u8 = 0x03;
const TAG_CAT5: u8 = 0x05;
fn tag(self) -> u8 {
match self {
SecurityLevel::Cat3 => Self::TAG_CAT3,
SecurityLevel::Cat5 => Self::TAG_CAT5,
}
}
fn from_tag(tag: u8) -> Result<Self> {
match tag {
Self::TAG_CAT3 => Ok(SecurityLevel::Cat3),
Self::TAG_CAT5 => Ok(SecurityLevel::Cat5),
other => Err(Error::MalformedPolicy(format!(
"unknown security_level tag 0x{other:02x}"
))),
}
}
fn rank(self) -> u8 {
match self {
SecurityLevel::Cat3 => 0,
SecurityLevel::Cat5 => 1,
}
}
#[must_use]
pub fn signature_level(self) -> SignatureLevel {
match self {
SecurityLevel::Cat3 => SignatureLevel::Cat3,
SecurityLevel::Cat5 => SignatureLevel::Cat5,
}
}
#[must_use]
pub fn derived_commitment_hash(self) -> CommitmentHash {
match self {
SecurityLevel::Cat3 => CommitmentHash::Sha3_256,
SecurityLevel::Cat5 => CommitmentHash::Sha3_512,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CheckpointSuite {
Hybrid,
HybridMatched,
PureCnsa2,
}
impl CheckpointSuite {
const TAG_HYBRID: u8 = 0x01;
const TAG_HYBRID_MATCHED: u8 = 0x02;
const TAG_PURE_CNSA2: u8 = 0x03;
fn tag(self) -> u8 {
match self {
CheckpointSuite::Hybrid => Self::TAG_HYBRID,
CheckpointSuite::HybridMatched => Self::TAG_HYBRID_MATCHED,
CheckpointSuite::PureCnsa2 => Self::TAG_PURE_CNSA2,
}
}
fn from_tag(tag: u8) -> Result<Self> {
match tag {
Self::TAG_HYBRID => Ok(CheckpointSuite::Hybrid),
Self::TAG_HYBRID_MATCHED => Ok(CheckpointSuite::HybridMatched),
Self::TAG_PURE_CNSA2 => Ok(CheckpointSuite::PureCnsa2),
other => Err(Error::MalformedPolicy(format!(
"unknown checkpoint_suite tag 0x{other:02x}"
))),
}
}
#[must_use]
pub fn crypto_suite(self) -> Suite {
match self {
CheckpointSuite::Hybrid => Suite::Hybrid,
CheckpointSuite::HybridMatched => Suite::HybridMatched,
CheckpointSuite::PureCnsa2 => Suite::PureCnsa2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommitmentHash {
Sha3_256,
Sha3_512,
}
impl CommitmentHash {
const TAG_SHA3_256: u8 = 0x01;
const TAG_SHA3_512: u8 = 0x02;
fn tag(self) -> u8 {
match self {
CommitmentHash::Sha3_256 => Self::TAG_SHA3_256,
CommitmentHash::Sha3_512 => Self::TAG_SHA3_512,
}
}
fn from_tag(tag: u8) -> Result<Self> {
match tag {
Self::TAG_SHA3_256 => Ok(CommitmentHash::Sha3_256),
Self::TAG_SHA3_512 => Ok(CommitmentHash::Sha3_512),
other => Err(Error::MalformedPolicy(format!(
"unknown commitment_hash tag 0x{other:02x}"
))),
}
}
fn rank(self) -> u8 {
match self {
CommitmentHash::Sha3_256 => 0,
CommitmentHash::Sha3_512 => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VrfMode {
Classical,
HybridOutput,
PurePqExperimental,
}
impl VrfMode {
const TAG_CLASSICAL: u8 = 0x01;
const TAG_HYBRID_OUTPUT: u8 = 0x02;
const TAG_PURE_PQ: u8 = 0x03;
fn tag(self) -> u8 {
match self {
VrfMode::Classical => Self::TAG_CLASSICAL,
VrfMode::HybridOutput => Self::TAG_HYBRID_OUTPUT,
VrfMode::PurePqExperimental => Self::TAG_PURE_PQ,
}
}
fn from_tag(tag: u8) -> Result<Self> {
match tag {
Self::TAG_CLASSICAL => Ok(VrfMode::Classical),
Self::TAG_HYBRID_OUTPUT => Ok(VrfMode::HybridOutput),
Self::TAG_PURE_PQ => Ok(VrfMode::PurePqExperimental),
other => Err(Error::MalformedPolicy(format!(
"unknown vrf_mode tag 0x{other:02x}"
))),
}
}
fn rank(self) -> u8 {
match self {
VrfMode::Classical => 0,
VrfMode::HybridOutput => 1,
VrfMode::PurePqExperimental => 2,
}
}
#[must_use]
pub fn expected_vrf_suite_id(self) -> Option<u8> {
match self {
VrfMode::Classical => Some(metamorphic_crypto::ECVRF_EDWARDS25519_SHA512_TAI_SUITE),
VrfMode::HybridOutput | VrfMode::PurePqExperimental => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NamespacePolicy {
namespace: Namespace,
policy_schema_version: u32,
security_level: SecurityLevel,
checkpoint_suite: CheckpointSuite,
commitment_hash: CommitmentHash,
vrf_mode: VrfMode,
effective_from: u64,
created_at: u64,
prev_policy_hash: Option<[u8; POLICY_HASH_LEN]>,
}
impl NamespacePolicy {
pub const RECORD_TYPE: &'static str = "namespace-policy";
#[allow(clippy::too_many_arguments)]
pub fn new(
namespace: Namespace,
policy_schema_version: u32,
security_level: SecurityLevel,
checkpoint_suite: CheckpointSuite,
commitment_hash: CommitmentHash,
vrf_mode: VrfMode,
effective_from: u64,
created_at: u64,
prev_policy_hash: Option<[u8; POLICY_HASH_LEN]>,
) -> Result<Self> {
let policy = Self {
namespace,
policy_schema_version,
security_level,
checkpoint_suite,
commitment_hash,
vrf_mode,
effective_from,
created_at,
prev_policy_hash,
};
policy.validate()?;
Ok(policy)
}
pub fn genesis(
namespace: Namespace,
security_level: SecurityLevel,
checkpoint_suite: CheckpointSuite,
effective_from: u64,
created_at: u64,
) -> Result<Self> {
Self::new(
namespace,
1,
security_level,
checkpoint_suite,
security_level.derived_commitment_hash(),
VrfMode::Classical,
effective_from,
created_at,
None,
)
}
fn validate(&self) -> Result<()> {
if self.policy_schema_version == 0 {
return Err(Error::MalformedPolicy(
"policy_schema_version must be >= 1".into(),
));
}
if self.commitment_hash != self.security_level.derived_commitment_hash() {
return Err(Error::MalformedPolicy(format!(
"commitment_hash {:?} does not match the one derived from security_level {:?}",
self.commitment_hash, self.security_level
)));
}
if self.vrf_mode != VrfMode::Classical {
return Err(Error::MalformedPolicy(format!(
"vrf_mode {:?} is not legal in v0.1 (only Classical)",
self.vrf_mode
)));
}
if self.checkpoint_suite == CheckpointSuite::PureCnsa2
&& self.security_level != SecurityLevel::Cat5
{
return Err(Error::MalformedPolicy(
"PureCnsa2 checkpoint_suite requires security_level Cat5".into(),
));
}
if matches!(self.prev_policy_hash.as_ref(), Some(h) if h.len() != POLICY_HASH_LEN) {
return Err(Error::MalformedPolicy(
"prev_policy_hash must be 64 bytes".into(),
));
}
Ok(())
}
#[must_use]
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
#[must_use]
pub fn policy_schema_version(&self) -> u32 {
self.policy_schema_version
}
#[must_use]
pub fn security_level(&self) -> SecurityLevel {
self.security_level
}
#[must_use]
pub fn checkpoint_suite(&self) -> CheckpointSuite {
self.checkpoint_suite
}
#[must_use]
pub fn commitment_hash(&self) -> CommitmentHash {
self.commitment_hash
}
#[must_use]
pub fn vrf_mode(&self) -> VrfMode {
self.vrf_mode
}
#[must_use]
pub fn effective_from(&self) -> u64 {
self.effective_from
}
#[must_use]
pub fn created_at(&self) -> u64 {
self.created_at
}
#[must_use]
pub fn prev_policy_hash(&self) -> Option<&[u8; POLICY_HASH_LEN]> {
self.prev_policy_hash.as_ref()
}
#[must_use]
pub fn declared_checkpoint_posture(&self) -> (Suite, SignatureLevel) {
(
self.checkpoint_suite.crypto_suite(),
self.security_level.signature_level(),
)
}
pub fn context_label(&self) -> Result<ContextLabel> {
ContextLabel::parse(&format!(
"{}/{}/v{}",
self.namespace.as_str(),
Self::RECORD_TYPE,
POLICY_FORMAT_VERSION
))
}
#[must_use]
pub fn canonical_bytes(&self) -> Vec<u8> {
let ns = self.namespace.as_str().as_bytes();
let prev: &[u8] = self.prev_policy_hash.as_ref().map_or(&[], |h| h.as_slice());
let mut out = Vec::with_capacity(4 + 4 + ns.len() + 4 + 4 + 8 + 8 + 4 + prev.len());
out.extend_from_slice(&POLICY_FORMAT_VERSION.to_be_bytes());
push_lp(&mut out, ns);
out.extend_from_slice(&self.policy_schema_version.to_be_bytes());
out.push(self.security_level.tag());
out.push(self.checkpoint_suite.tag());
out.push(self.commitment_hash.tag());
out.push(self.vrf_mode.tag());
out.extend_from_slice(&self.effective_from.to_be_bytes());
out.extend_from_slice(&self.created_at.to_be_bytes());
push_lp(&mut out, prev);
out
}
pub fn parse(bytes: &[u8]) -> Result<Self> {
let mut cur = Cursor::new(bytes);
let format_version = cur.u32()?;
if format_version != POLICY_FORMAT_VERSION {
return Err(Error::MalformedPolicy(format!(
"unknown policy format version {format_version}"
)));
}
let ns_bytes = cur.lp()?;
let namespace = core::str::from_utf8(ns_bytes)
.map_err(|_| Error::MalformedPolicy("namespace is not valid UTF-8".into()))
.and_then(Namespace::parse)?;
let policy_schema_version = cur.u32()?;
let security_level = SecurityLevel::from_tag(cur.u8()?)?;
let checkpoint_suite = CheckpointSuite::from_tag(cur.u8()?)?;
let commitment_hash = CommitmentHash::from_tag(cur.u8()?)?;
let vrf_mode = VrfMode::from_tag(cur.u8()?)?;
let effective_from = cur.u64()?;
let created_at = cur.u64()?;
let prev = cur.lp()?;
let prev_policy_hash = match prev.len() {
0 => None,
POLICY_HASH_LEN => {
let mut h = [0u8; POLICY_HASH_LEN];
h.copy_from_slice(prev);
Some(h)
}
other => {
return Err(Error::MalformedPolicy(format!(
"prev_policy_hash is {other} bytes, want 0 (genesis) or {POLICY_HASH_LEN}"
)));
}
};
if !cur.is_empty() {
return Err(Error::MalformedPolicy(
"trailing bytes after policy record".into(),
));
}
Self::new(
namespace,
policy_schema_version,
security_level,
checkpoint_suite,
commitment_hash,
vrf_mode,
effective_from,
created_at,
prev_policy_hash,
)
}
pub fn policy_hash(&self) -> Result<[u8; POLICY_HASH_LEN]> {
let label = self.context_label()?;
Ok(content_hash(&label, &self.canonical_bytes()))
}
#[must_use]
pub fn rfc6962_leaf_hash(&self) -> Hash {
hash_leaf(&self.canonical_bytes())
}
pub fn enforce_checkpoint_signing_key(&self, public_key_b64: &str) -> Result<()> {
let observed = metamorphic_crypto::signature_posture(public_key_b64).map_err(|e| {
Error::PostureMismatch {
declared: posture_str(self.declared_checkpoint_posture()),
observed: format!("undecodable checkpoint key ({e})"),
}
})?;
self.check_checkpoint_posture(observed)
}
pub fn enforce_checkpoint_signature(&self, signature_b64: &str) -> Result<()> {
let observed = metamorphic_crypto::signature_posture_from_signature(signature_b64)
.map_err(|e| Error::PostureMismatch {
declared: posture_str(self.declared_checkpoint_posture()),
observed: format!("undecodable checkpoint signature ({e})"),
})?;
self.check_checkpoint_posture(observed)
}
fn check_checkpoint_posture(&self, observed: (Suite, SignatureLevel)) -> Result<()> {
let declared = self.declared_checkpoint_posture();
if observed == declared {
Ok(())
} else {
Err(Error::PostureMismatch {
declared: posture_str(declared),
observed: posture_str(observed),
})
}
}
pub fn enforce_vrf_suite_id(&self, observed_suite_id: u8) -> Result<()> {
match self.vrf_mode.expected_vrf_suite_id() {
Some(expected) if expected == observed_suite_id => Ok(()),
expected => Err(Error::PostureMismatch {
declared: expected.map_or_else(
|| format!("vrf_mode {:?} (no built suite)", self.vrf_mode),
|e| format!("vrf_mode {:?} (suite_id 0x{e:02x})", self.vrf_mode),
),
observed: format!("vrf suite_id 0x{observed_suite_id:02x}"),
}),
}
}
pub fn enforce_commitment_hash(&self, observed: CommitmentHash) -> Result<()> {
if observed == self.commitment_hash {
Ok(())
} else {
Err(Error::PostureMismatch {
declared: format!("commitment_hash {:?}", self.commitment_hash),
observed: format!("commitment_hash {observed:?}"),
})
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObservedPosture {
pub checkpoint: (Suite, SignatureLevel),
pub vrf_suite_id: u8,
pub commitment_hash: CommitmentHash,
}
impl NamespacePolicy {
pub fn enforce_observed(&self, observed: &ObservedPosture) -> Result<()> {
self.check_checkpoint_posture(observed.checkpoint)?;
self.enforce_vrf_suite_id(observed.vrf_suite_id)?;
self.enforce_commitment_hash(observed.commitment_hash)?;
Ok(())
}
}
fn posture_str(p: (Suite, SignatureLevel)) -> String {
format!("{:?}/{:?}", p.0, p.1)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedPolicy {
policy: NamespacePolicy,
signing_public_key: Vec<u8>,
signature: Vec<u8>,
}
impl SignedPolicy {
pub fn sign(policy: NamespacePolicy, secret_key_b64: &str) -> Result<Self> {
let ctx = policy.context_label()?;
let canonical = policy.canonical_bytes();
let public_key_b64 = metamorphic_crypto::derive_public_key(secret_key_b64)
.map_err(|e| Error::HybridSignature(format!("invalid policy signing key: {e}")))?;
let signing_public_key = metamorphic_crypto::b64::decode(&public_key_b64)
.map_err(|e| Error::HybridSignature(format!("undecodable policy public key: {e}")))?;
let sig_b64 = metamorphic_crypto::sign(&canonical, ctx.as_str(), secret_key_b64)
.map_err(|e| Error::HybridSignature(format!("policy signing failed: {e}")))?;
let signature = metamorphic_crypto::b64::decode(&sig_b64)
.map_err(|e| Error::HybridSignature(format!("undecodable policy signature: {e}")))?;
Ok(Self {
policy,
signing_public_key,
signature,
})
}
#[must_use]
pub fn from_parts(
policy: NamespacePolicy,
signing_public_key: Vec<u8>,
signature: Vec<u8>,
) -> Self {
Self {
policy,
signing_public_key,
signature,
}
}
#[must_use]
pub fn policy(&self) -> &NamespacePolicy {
&self.policy
}
#[must_use]
pub fn signing_public_key(&self) -> &[u8] {
&self.signing_public_key
}
#[must_use]
pub fn signature(&self) -> &[u8] {
&self.signature
}
pub fn verify(&self) -> Result<&NamespacePolicy> {
let ctx = self.policy.context_label()?;
let canonical = self.policy.canonical_bytes();
let sig_b64 = metamorphic_crypto::b64::encode(&self.signature);
let pk_b64 = metamorphic_crypto::b64::encode(&self.signing_public_key);
let ok = metamorphic_crypto::verify(&canonical, ctx.as_str(), &sig_b64, &pk_b64)
.unwrap_or(false);
if ok {
Ok(&self.policy)
} else {
Err(Error::InvalidSignature {
name: format!("{}/namespace-policy", self.policy.namespace.as_str()),
key_id: 0,
})
}
}
#[must_use]
pub fn canonical_bytes(&self) -> Vec<u8> {
let policy = self.policy.canonical_bytes();
let mut out = Vec::with_capacity(
4 + 12 + policy.len() + self.signing_public_key.len() + self.signature.len(),
);
out.extend_from_slice(&SIGNED_POLICY_FORMAT_VERSION.to_be_bytes());
push_lp(&mut out, &policy);
push_lp(&mut out, &self.signing_public_key);
push_lp(&mut out, &self.signature);
out
}
pub fn parse(bytes: &[u8]) -> Result<Self> {
let mut cur = Cursor::new(bytes);
let format_version = cur.u32()?;
if format_version != SIGNED_POLICY_FORMAT_VERSION {
return Err(Error::MalformedPolicy(format!(
"unknown signed-policy format version {format_version}"
)));
}
let policy = NamespacePolicy::parse(cur.lp()?)?;
let signing_public_key = cur.lp()?.to_vec();
let signature = cur.lp()?.to_vec();
if signing_public_key.is_empty() || signature.is_empty() {
return Err(Error::MalformedPolicy(
"signed policy must carry a non-empty key and signature".into(),
));
}
if !cur.is_empty() {
return Err(Error::MalformedPolicy(
"trailing bytes after signed policy envelope".into(),
));
}
Ok(Self {
policy,
signing_public_key,
signature,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PolicyChain {
versions: Vec<NamespacePolicy>,
}
impl PolicyChain {
pub fn genesis(policy: NamespacePolicy) -> Result<Self> {
if policy.prev_policy_hash.is_some() {
return Err(Error::PolicyMigrationRejected(
"genesis policy must not carry a prev_policy_hash".into(),
));
}
Ok(Self {
versions: vec![policy],
})
}
#[must_use]
pub fn versions(&self) -> &[NamespacePolicy] {
&self.versions
}
#[must_use]
pub fn latest(&self) -> &NamespacePolicy {
self.versions
.last()
.expect("a PolicyChain always has at least the genesis version")
}
pub fn push(&mut self, next: NamespacePolicy) -> Result<()> {
let prev = self.latest();
if next.namespace != prev.namespace {
return Err(Error::PolicyMigrationRejected(format!(
"namespace changed from {:?} to {:?}",
prev.namespace.as_str(),
next.namespace.as_str()
)));
}
if next.policy_schema_version != prev.policy_schema_version + 1 {
return Err(Error::PolicyMigrationRejected(format!(
"policy_schema_version must increment by 1 ({} -> {}), got {}",
prev.policy_schema_version,
prev.policy_schema_version + 1,
next.policy_schema_version
)));
}
if next.effective_from <= prev.effective_from {
return Err(Error::PolicyMigrationRejected(format!(
"effective_from must strictly increase ({} -> {})",
prev.effective_from, next.effective_from
)));
}
let expected_prev = prev.policy_hash()?;
match next.prev_policy_hash {
Some(h) if h == expected_prev => {}
Some(_) => {
return Err(Error::PolicyMigrationRejected(
"prev_policy_hash does not chain to the prior version".into(),
));
}
None => {
return Err(Error::PolicyMigrationRejected(
"migration must carry a prev_policy_hash".into(),
));
}
}
if next.security_level.rank() < prev.security_level.rank()
|| next.commitment_hash.rank() < prev.commitment_hash.rank()
|| next.vrf_mode.rank() < prev.vrf_mode.rank()
{
return Err(Error::PolicyMigrationRejected(format!(
"migration would weaken posture (prev {:?}/{:?}/{:?} -> next {:?}/{:?}/{:?})",
prev.security_level,
prev.commitment_hash,
prev.vrf_mode,
next.security_level,
next.commitment_hash,
next.vrf_mode
)));
}
self.versions.push(next);
Ok(())
}
pub fn active_at(&self, position: u64) -> Result<&NamespacePolicy> {
if position < self.versions[0].effective_from {
return Err(Error::UnknownNamespacePolicy(format!(
"tree position {position} precedes the genesis effective_from {}",
self.versions[0].effective_from
)));
}
let active = self
.versions
.iter()
.rev()
.find(|p| p.effective_from <= position)
.expect("position >= genesis effective_from guarantees a match");
Ok(active)
}
}
fn push_lp(out: &mut Vec<u8>, bytes: &[u8]) {
out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
out.extend_from_slice(bytes);
}
struct Cursor<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn is_empty(&self) -> bool {
self.pos >= self.buf.len()
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.pos
.checked_add(n)
.filter(|&e| e <= self.buf.len())
.ok_or_else(|| {
Error::MalformedPolicy(format!(
"field of {n} bytes overruns the {}-byte buffer at offset {}",
self.buf.len(),
self.pos
))
})?;
let out = &self.buf[self.pos..end];
self.pos = end;
Ok(out)
}
fn u8(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
fn u32(&mut self) -> Result<u32> {
let b = self.take(4)?;
Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn u64(&mut self) -> Result<u64> {
let b = self.take(8)?;
Ok(u64::from_be_bytes([
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
]))
}
fn lp(&mut self) -> Result<&'a [u8]> {
let len = self.u32()? as usize;
self.take(len)
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
fn ns() -> Namespace {
Namespace::parse("acme").unwrap()
}
fn cat5_pure() -> NamespacePolicy {
NamespacePolicy::genesis(
ns(),
SecurityLevel::Cat5,
CheckpointSuite::PureCnsa2,
0,
1_700_000,
)
.unwrap()
}
#[test]
fn genesis_derives_commitment_hash_and_classical_vrf() {
let p = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 0, 0)
.unwrap();
assert_eq!(p.commitment_hash(), CommitmentHash::Sha3_256);
assert_eq!(p.vrf_mode(), VrfMode::Classical);
assert_eq!(p.policy_schema_version(), 1);
assert!(p.prev_policy_hash().is_none());
let p5 = cat5_pure();
assert_eq!(p5.commitment_hash(), CommitmentHash::Sha3_512);
}
#[test]
fn canonical_round_trips_byte_for_byte() {
let p = cat5_pure();
let bytes = p.canonical_bytes();
let parsed = NamespacePolicy::parse(&bytes).unwrap();
assert_eq!(parsed, p);
assert_eq!(parsed.canonical_bytes(), bytes);
}
#[test]
fn parse_rejects_malformed() {
assert!(matches!(
NamespacePolicy::parse(&[0, 0, 0, 1]),
Err(Error::MalformedPolicy(_))
));
let mut b = cat5_pure().canonical_bytes();
b.push(0xff);
assert!(matches!(
NamespacePolicy::parse(&b),
Err(Error::MalformedPolicy(_))
));
}
#[test]
fn rejects_commitment_hash_not_matching_level() {
let r = NamespacePolicy::new(
ns(),
1,
SecurityLevel::Cat5,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_256, VrfMode::Classical,
0,
0,
None,
);
assert!(matches!(r, Err(Error::MalformedPolicy(_))));
}
#[test]
fn rejects_non_classical_vrf_and_purecnsa2_below_cat5() {
assert!(matches!(
NamespacePolicy::new(
ns(),
1,
SecurityLevel::Cat5,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_512,
VrfMode::HybridOutput,
0,
0,
None,
),
Err(Error::MalformedPolicy(_))
));
assert!(matches!(
NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::PureCnsa2, 0, 0),
Err(Error::MalformedPolicy(_))
));
}
#[test]
fn policy_hash_is_stable_and_context_bound() {
let p = cat5_pure();
assert_eq!(p.policy_hash().unwrap(), p.policy_hash().unwrap());
let other = NamespacePolicy::genesis(
Namespace::parse("other").unwrap(),
SecurityLevel::Cat5,
CheckpointSuite::PureCnsa2,
0,
1_700_000,
)
.unwrap();
assert_ne!(p.policy_hash().unwrap(), other.policy_hash().unwrap());
}
#[test]
fn enforce_vrf_suite_id_classical() {
let p = cat5_pure();
assert!(p.enforce_vrf_suite_id(0x03).is_ok());
assert!(matches!(
p.enforce_vrf_suite_id(0x04),
Err(Error::PostureMismatch { .. })
));
}
#[test]
fn enforce_commitment_hash() {
let p = cat5_pure();
assert!(p.enforce_commitment_hash(CommitmentHash::Sha3_512).is_ok());
assert!(matches!(
p.enforce_commitment_hash(CommitmentHash::Sha3_256),
Err(Error::PostureMismatch { .. })
));
}
#[test]
fn migration_strengthen_ok_weaken_rejected() {
let g = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 0, 0)
.unwrap();
let mut chain = PolicyChain::genesis(g.clone()).unwrap();
let v2 = NamespacePolicy::new(
ns(),
2,
SecurityLevel::Cat5,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_512,
VrfMode::Classical,
100,
1,
Some(g.policy_hash().unwrap()),
)
.unwrap();
chain.push(v2.clone()).unwrap();
assert_eq!(chain.versions().len(), 2);
let weak = NamespacePolicy::new(
ns(),
3,
SecurityLevel::Cat3,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_256,
VrfMode::Classical,
200,
2,
Some(v2.policy_hash().unwrap()),
)
.unwrap();
assert!(matches!(
chain.push(weak),
Err(Error::PolicyMigrationRejected(_))
));
}
#[test]
fn migration_rejects_bad_chain_links() {
let g = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 0, 0)
.unwrap();
let mut chain = PolicyChain::genesis(g.clone()).unwrap();
let bad_prev = NamespacePolicy::new(
ns(),
2,
SecurityLevel::Cat3,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_256,
VrfMode::Classical,
10,
1,
Some([0u8; POLICY_HASH_LEN]),
)
.unwrap();
assert!(matches!(
chain.push(bad_prev),
Err(Error::PolicyMigrationRejected(_))
));
let bad_ver = NamespacePolicy::new(
ns(),
3,
SecurityLevel::Cat3,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_256,
VrfMode::Classical,
10,
1,
Some(g.policy_hash().unwrap()),
)
.unwrap();
assert!(matches!(
chain.push(bad_ver),
Err(Error::PolicyMigrationRejected(_))
));
}
#[test]
fn active_at_resolves_half_open_ranges() {
let g = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 5, 0)
.unwrap();
let mut chain = PolicyChain::genesis(g.clone()).unwrap();
let v2 = NamespacePolicy::new(
ns(),
2,
SecurityLevel::Cat5,
CheckpointSuite::Hybrid,
CommitmentHash::Sha3_512,
VrfMode::Classical,
10,
1,
Some(g.policy_hash().unwrap()),
)
.unwrap();
chain.push(v2).unwrap();
assert!(matches!(
chain.active_at(4),
Err(Error::UnknownNamespacePolicy(_))
));
assert_eq!(chain.active_at(5).unwrap().policy_schema_version(), 1);
assert_eq!(chain.active_at(9).unwrap().policy_schema_version(), 1);
assert_eq!(chain.active_at(10).unwrap().policy_schema_version(), 2);
assert_eq!(chain.active_at(1000).unwrap().policy_schema_version(), 2);
}
}