1use metamorphic_crypto::{SignatureLevel, Suite};
94
95use crate::coniks::Namespace;
96use crate::error::{Error, Result};
97use crate::leaf::{ContextLabel, content_hash};
98use crate::merkle::{Hash, hash_leaf};
99
100pub const POLICY_FORMAT_VERSION: u32 = 1;
105
106pub const SIGNED_POLICY_FORMAT_VERSION: u32 = 1;
108
109pub const POLICY_HASH_LEN: usize = 64;
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
119pub enum SecurityLevel {
120 Cat3,
122 Cat5,
124}
125
126impl SecurityLevel {
127 const TAG_CAT3: u8 = 0x03;
128 const TAG_CAT5: u8 = 0x05;
129
130 fn tag(self) -> u8 {
131 match self {
132 SecurityLevel::Cat3 => Self::TAG_CAT3,
133 SecurityLevel::Cat5 => Self::TAG_CAT5,
134 }
135 }
136
137 fn from_tag(tag: u8) -> Result<Self> {
138 match tag {
139 Self::TAG_CAT3 => Ok(SecurityLevel::Cat3),
140 Self::TAG_CAT5 => Ok(SecurityLevel::Cat5),
141 other => Err(Error::MalformedPolicy(format!(
142 "unknown security_level tag 0x{other:02x}"
143 ))),
144 }
145 }
146
147 fn rank(self) -> u8 {
149 match self {
150 SecurityLevel::Cat3 => 0,
151 SecurityLevel::Cat5 => 1,
152 }
153 }
154
155 #[must_use]
158 pub fn signature_level(self) -> SignatureLevel {
159 match self {
160 SecurityLevel::Cat3 => SignatureLevel::Cat3,
161 SecurityLevel::Cat5 => SignatureLevel::Cat5,
162 }
163 }
164
165 #[must_use]
167 pub fn derived_commitment_hash(self) -> CommitmentHash {
168 match self {
169 SecurityLevel::Cat3 => CommitmentHash::Sha3_256,
170 SecurityLevel::Cat5 => CommitmentHash::Sha3_512,
171 }
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
179pub enum CheckpointSuite {
180 Hybrid,
182 HybridMatched,
185 PureCnsa2,
187}
188
189impl CheckpointSuite {
190 const TAG_HYBRID: u8 = 0x01;
191 const TAG_HYBRID_MATCHED: u8 = 0x02;
192 const TAG_PURE_CNSA2: u8 = 0x03;
193
194 fn tag(self) -> u8 {
195 match self {
196 CheckpointSuite::Hybrid => Self::TAG_HYBRID,
197 CheckpointSuite::HybridMatched => Self::TAG_HYBRID_MATCHED,
198 CheckpointSuite::PureCnsa2 => Self::TAG_PURE_CNSA2,
199 }
200 }
201
202 fn from_tag(tag: u8) -> Result<Self> {
203 match tag {
204 Self::TAG_HYBRID => Ok(CheckpointSuite::Hybrid),
205 Self::TAG_HYBRID_MATCHED => Ok(CheckpointSuite::HybridMatched),
206 Self::TAG_PURE_CNSA2 => Ok(CheckpointSuite::PureCnsa2),
207 other => Err(Error::MalformedPolicy(format!(
208 "unknown checkpoint_suite tag 0x{other:02x}"
209 ))),
210 }
211 }
212
213 #[must_use]
216 pub fn crypto_suite(self) -> Suite {
217 match self {
218 CheckpointSuite::Hybrid => Suite::Hybrid,
219 CheckpointSuite::HybridMatched => Suite::HybridMatched,
220 CheckpointSuite::PureCnsa2 => Suite::PureCnsa2,
221 }
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
228pub enum CommitmentHash {
229 Sha3_256,
231 Sha3_512,
233}
234
235impl CommitmentHash {
236 const TAG_SHA3_256: u8 = 0x01;
237 const TAG_SHA3_512: u8 = 0x02;
238
239 fn tag(self) -> u8 {
240 match self {
241 CommitmentHash::Sha3_256 => Self::TAG_SHA3_256,
242 CommitmentHash::Sha3_512 => Self::TAG_SHA3_512,
243 }
244 }
245
246 fn from_tag(tag: u8) -> Result<Self> {
247 match tag {
248 Self::TAG_SHA3_256 => Ok(CommitmentHash::Sha3_256),
249 Self::TAG_SHA3_512 => Ok(CommitmentHash::Sha3_512),
250 other => Err(Error::MalformedPolicy(format!(
251 "unknown commitment_hash tag 0x{other:02x}"
252 ))),
253 }
254 }
255
256 fn rank(self) -> u8 {
257 match self {
258 CommitmentHash::Sha3_256 => 0,
259 CommitmentHash::Sha3_512 => 1,
260 }
261 }
262}
263
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
268pub enum VrfMode {
269 Classical,
272 HybridOutput,
275 PurePqExperimental,
277}
278
279impl VrfMode {
280 const TAG_CLASSICAL: u8 = 0x01;
281 const TAG_HYBRID_OUTPUT: u8 = 0x02;
282 const TAG_PURE_PQ: u8 = 0x03;
283
284 fn tag(self) -> u8 {
285 match self {
286 VrfMode::Classical => Self::TAG_CLASSICAL,
287 VrfMode::HybridOutput => Self::TAG_HYBRID_OUTPUT,
288 VrfMode::PurePqExperimental => Self::TAG_PURE_PQ,
289 }
290 }
291
292 fn from_tag(tag: u8) -> Result<Self> {
293 match tag {
294 Self::TAG_CLASSICAL => Ok(VrfMode::Classical),
295 Self::TAG_HYBRID_OUTPUT => Ok(VrfMode::HybridOutput),
296 Self::TAG_PURE_PQ => Ok(VrfMode::PurePqExperimental),
297 other => Err(Error::MalformedPolicy(format!(
298 "unknown vrf_mode tag 0x{other:02x}"
299 ))),
300 }
301 }
302
303 fn rank(self) -> u8 {
304 match self {
305 VrfMode::Classical => 0,
306 VrfMode::HybridOutput => 1,
307 VrfMode::PurePqExperimental => 2,
308 }
309 }
310
311 #[must_use]
315 pub fn expected_vrf_suite_id(self) -> Option<u8> {
316 match self {
317 VrfMode::Classical => Some(metamorphic_crypto::ECVRF_EDWARDS25519_SHA512_TAI_SUITE),
318 VrfMode::HybridOutput | VrfMode::PurePqExperimental => None,
319 }
320 }
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
330pub struct NamespacePolicy {
331 namespace: Namespace,
332 policy_schema_version: u32,
333 security_level: SecurityLevel,
334 checkpoint_suite: CheckpointSuite,
335 commitment_hash: CommitmentHash,
336 vrf_mode: VrfMode,
337 effective_from: u64,
338 created_at: u64,
339 prev_policy_hash: Option<[u8; POLICY_HASH_LEN]>,
340}
341
342impl NamespacePolicy {
343 pub const RECORD_TYPE: &'static str = "namespace-policy";
345
346 #[allow(clippy::too_many_arguments)]
356 pub fn new(
357 namespace: Namespace,
358 policy_schema_version: u32,
359 security_level: SecurityLevel,
360 checkpoint_suite: CheckpointSuite,
361 commitment_hash: CommitmentHash,
362 vrf_mode: VrfMode,
363 effective_from: u64,
364 created_at: u64,
365 prev_policy_hash: Option<[u8; POLICY_HASH_LEN]>,
366 ) -> Result<Self> {
367 let policy = Self {
368 namespace,
369 policy_schema_version,
370 security_level,
371 checkpoint_suite,
372 commitment_hash,
373 vrf_mode,
374 effective_from,
375 created_at,
376 prev_policy_hash,
377 };
378 policy.validate()?;
379 Ok(policy)
380 }
381
382 pub fn genesis(
390 namespace: Namespace,
391 security_level: SecurityLevel,
392 checkpoint_suite: CheckpointSuite,
393 effective_from: u64,
394 created_at: u64,
395 ) -> Result<Self> {
396 Self::new(
397 namespace,
398 1,
399 security_level,
400 checkpoint_suite,
401 security_level.derived_commitment_hash(),
402 VrfMode::Classical,
403 effective_from,
404 created_at,
405 None,
406 )
407 }
408
409 fn validate(&self) -> Result<()> {
410 if self.policy_schema_version == 0 {
411 return Err(Error::MalformedPolicy(
412 "policy_schema_version must be >= 1".into(),
413 ));
414 }
415 if self.commitment_hash != self.security_level.derived_commitment_hash() {
417 return Err(Error::MalformedPolicy(format!(
418 "commitment_hash {:?} does not match the one derived from security_level {:?}",
419 self.commitment_hash, self.security_level
420 )));
421 }
422 if self.vrf_mode != VrfMode::Classical {
424 return Err(Error::MalformedPolicy(format!(
425 "vrf_mode {:?} is not legal in v0.1 (only Classical)",
426 self.vrf_mode
427 )));
428 }
429 if self.checkpoint_suite == CheckpointSuite::PureCnsa2
431 && self.security_level != SecurityLevel::Cat5
432 {
433 return Err(Error::MalformedPolicy(
434 "PureCnsa2 checkpoint_suite requires security_level Cat5".into(),
435 ));
436 }
437 if matches!(self.prev_policy_hash.as_ref(), Some(h) if h.len() != POLICY_HASH_LEN) {
438 return Err(Error::MalformedPolicy(
439 "prev_policy_hash must be 64 bytes".into(),
440 ));
441 }
442 Ok(())
443 }
444
445 #[must_use]
447 pub fn namespace(&self) -> &Namespace {
448 &self.namespace
449 }
450
451 #[must_use]
453 pub fn policy_schema_version(&self) -> u32 {
454 self.policy_schema_version
455 }
456
457 #[must_use]
459 pub fn security_level(&self) -> SecurityLevel {
460 self.security_level
461 }
462
463 #[must_use]
465 pub fn checkpoint_suite(&self) -> CheckpointSuite {
466 self.checkpoint_suite
467 }
468
469 #[must_use]
471 pub fn commitment_hash(&self) -> CommitmentHash {
472 self.commitment_hash
473 }
474
475 #[must_use]
477 pub fn vrf_mode(&self) -> VrfMode {
478 self.vrf_mode
479 }
480
481 #[must_use]
483 pub fn effective_from(&self) -> u64 {
484 self.effective_from
485 }
486
487 #[must_use]
489 pub fn created_at(&self) -> u64 {
490 self.created_at
491 }
492
493 #[must_use]
495 pub fn prev_policy_hash(&self) -> Option<&[u8; POLICY_HASH_LEN]> {
496 self.prev_policy_hash.as_ref()
497 }
498
499 #[must_use]
502 pub fn declared_checkpoint_posture(&self) -> (Suite, SignatureLevel) {
503 (
504 self.checkpoint_suite.crypto_suite(),
505 self.security_level.signature_level(),
506 )
507 }
508
509 pub fn context_label(&self) -> Result<ContextLabel> {
515 ContextLabel::parse(&format!(
516 "{}/{}/v{}",
517 self.namespace.as_str(),
518 Self::RECORD_TYPE,
519 POLICY_FORMAT_VERSION
520 ))
521 }
522
523 #[must_use]
539 pub fn canonical_bytes(&self) -> Vec<u8> {
540 let ns = self.namespace.as_str().as_bytes();
541 let prev: &[u8] = self.prev_policy_hash.as_ref().map_or(&[], |h| h.as_slice());
542 let mut out = Vec::with_capacity(4 + 4 + ns.len() + 4 + 4 + 8 + 8 + 4 + prev.len());
543 out.extend_from_slice(&POLICY_FORMAT_VERSION.to_be_bytes());
544 push_lp(&mut out, ns);
545 out.extend_from_slice(&self.policy_schema_version.to_be_bytes());
546 out.push(self.security_level.tag());
547 out.push(self.checkpoint_suite.tag());
548 out.push(self.commitment_hash.tag());
549 out.push(self.vrf_mode.tag());
550 out.extend_from_slice(&self.effective_from.to_be_bytes());
551 out.extend_from_slice(&self.created_at.to_be_bytes());
552 push_lp(&mut out, prev);
553 out
554 }
555
556 pub fn parse(bytes: &[u8]) -> Result<Self> {
564 let mut cur = Cursor::new(bytes);
565 let format_version = cur.u32()?;
566 if format_version != POLICY_FORMAT_VERSION {
567 return Err(Error::MalformedPolicy(format!(
568 "unknown policy format version {format_version}"
569 )));
570 }
571 let ns_bytes = cur.lp()?;
572 let namespace = core::str::from_utf8(ns_bytes)
573 .map_err(|_| Error::MalformedPolicy("namespace is not valid UTF-8".into()))
574 .and_then(Namespace::parse)?;
575 let policy_schema_version = cur.u32()?;
576 let security_level = SecurityLevel::from_tag(cur.u8()?)?;
577 let checkpoint_suite = CheckpointSuite::from_tag(cur.u8()?)?;
578 let commitment_hash = CommitmentHash::from_tag(cur.u8()?)?;
579 let vrf_mode = VrfMode::from_tag(cur.u8()?)?;
580 let effective_from = cur.u64()?;
581 let created_at = cur.u64()?;
582 let prev = cur.lp()?;
583 let prev_policy_hash = match prev.len() {
584 0 => None,
585 POLICY_HASH_LEN => {
586 let mut h = [0u8; POLICY_HASH_LEN];
587 h.copy_from_slice(prev);
588 Some(h)
589 }
590 other => {
591 return Err(Error::MalformedPolicy(format!(
592 "prev_policy_hash is {other} bytes, want 0 (genesis) or {POLICY_HASH_LEN}"
593 )));
594 }
595 };
596 if !cur.is_empty() {
597 return Err(Error::MalformedPolicy(
598 "trailing bytes after policy record".into(),
599 ));
600 }
601
602 Self::new(
603 namespace,
604 policy_schema_version,
605 security_level,
606 checkpoint_suite,
607 commitment_hash,
608 vrf_mode,
609 effective_from,
610 created_at,
611 prev_policy_hash,
612 )
613 }
614
615 pub fn policy_hash(&self) -> Result<[u8; POLICY_HASH_LEN]> {
625 let label = self.context_label()?;
626 Ok(content_hash(&label, &self.canonical_bytes()))
627 }
628
629 #[must_use]
633 pub fn rfc6962_leaf_hash(&self) -> Hash {
634 hash_leaf(&self.canonical_bytes())
635 }
636
637 pub fn enforce_checkpoint_signing_key(&self, public_key_b64: &str) -> Result<()> {
651 let observed = metamorphic_crypto::signature_posture(public_key_b64).map_err(|e| {
652 Error::PostureMismatch {
653 declared: posture_str(self.declared_checkpoint_posture()),
654 observed: format!("undecodable checkpoint key ({e})"),
655 }
656 })?;
657 self.check_checkpoint_posture(observed)
658 }
659
660 pub fn enforce_checkpoint_signature(&self, signature_b64: &str) -> Result<()> {
668 let observed = metamorphic_crypto::signature_posture_from_signature(signature_b64)
669 .map_err(|e| Error::PostureMismatch {
670 declared: posture_str(self.declared_checkpoint_posture()),
671 observed: format!("undecodable checkpoint signature ({e})"),
672 })?;
673 self.check_checkpoint_posture(observed)
674 }
675
676 fn check_checkpoint_posture(&self, observed: (Suite, SignatureLevel)) -> Result<()> {
677 let declared = self.declared_checkpoint_posture();
678 if observed == declared {
679 Ok(())
680 } else {
681 Err(Error::PostureMismatch {
682 declared: posture_str(declared),
683 observed: posture_str(observed),
684 })
685 }
686 }
687
688 pub fn enforce_vrf_suite_id(&self, observed_suite_id: u8) -> Result<()> {
697 match self.vrf_mode.expected_vrf_suite_id() {
698 Some(expected) if expected == observed_suite_id => Ok(()),
699 expected => Err(Error::PostureMismatch {
700 declared: expected.map_or_else(
701 || format!("vrf_mode {:?} (no built suite)", self.vrf_mode),
702 |e| format!("vrf_mode {:?} (suite_id 0x{e:02x})", self.vrf_mode),
703 ),
704 observed: format!("vrf suite_id 0x{observed_suite_id:02x}"),
705 }),
706 }
707 }
708
709 pub fn enforce_commitment_hash(&self, observed: CommitmentHash) -> Result<()> {
715 if observed == self.commitment_hash {
716 Ok(())
717 } else {
718 Err(Error::PostureMismatch {
719 declared: format!("commitment_hash {:?}", self.commitment_hash),
720 observed: format!("commitment_hash {observed:?}"),
721 })
722 }
723 }
724}
725
726#[derive(Debug, Clone, PartialEq, Eq)]
729pub struct ObservedPosture {
730 pub checkpoint: (Suite, SignatureLevel),
733 pub vrf_suite_id: u8,
735 pub commitment_hash: CommitmentHash,
737}
738
739impl NamespacePolicy {
740 pub fn enforce_observed(&self, observed: &ObservedPosture) -> Result<()> {
747 self.check_checkpoint_posture(observed.checkpoint)?;
748 self.enforce_vrf_suite_id(observed.vrf_suite_id)?;
749 self.enforce_commitment_hash(observed.commitment_hash)?;
750 Ok(())
751 }
752}
753
754fn posture_str(p: (Suite, SignatureLevel)) -> String {
755 format!("{:?}/{:?}", p.0, p.1)
756}
757
758#[derive(Debug, Clone, PartialEq, Eq)]
766pub struct SignedPolicy {
767 policy: NamespacePolicy,
768 signing_public_key: Vec<u8>,
769 signature: Vec<u8>,
770}
771
772impl SignedPolicy {
773 pub fn sign(policy: NamespacePolicy, secret_key_b64: &str) -> Result<Self> {
782 let ctx = policy.context_label()?;
783 let canonical = policy.canonical_bytes();
784 let public_key_b64 = metamorphic_crypto::derive_public_key(secret_key_b64)
785 .map_err(|e| Error::HybridSignature(format!("invalid policy signing key: {e}")))?;
786 let signing_public_key = metamorphic_crypto::b64::decode(&public_key_b64)
787 .map_err(|e| Error::HybridSignature(format!("undecodable policy public key: {e}")))?;
788 let sig_b64 = metamorphic_crypto::sign(&canonical, ctx.as_str(), secret_key_b64)
789 .map_err(|e| Error::HybridSignature(format!("policy signing failed: {e}")))?;
790 let signature = metamorphic_crypto::b64::decode(&sig_b64)
791 .map_err(|e| Error::HybridSignature(format!("undecodable policy signature: {e}")))?;
792 Ok(Self {
793 policy,
794 signing_public_key,
795 signature,
796 })
797 }
798
799 #[must_use]
802 pub fn from_parts(
803 policy: NamespacePolicy,
804 signing_public_key: Vec<u8>,
805 signature: Vec<u8>,
806 ) -> Self {
807 Self {
808 policy,
809 signing_public_key,
810 signature,
811 }
812 }
813
814 #[must_use]
816 pub fn policy(&self) -> &NamespacePolicy {
817 &self.policy
818 }
819
820 #[must_use]
823 pub fn signing_public_key(&self) -> &[u8] {
824 &self.signing_public_key
825 }
826
827 #[must_use]
829 pub fn signature(&self) -> &[u8] {
830 &self.signature
831 }
832
833 pub fn verify(&self) -> Result<&NamespacePolicy> {
847 let ctx = self.policy.context_label()?;
848 let canonical = self.policy.canonical_bytes();
849 let sig_b64 = metamorphic_crypto::b64::encode(&self.signature);
850 let pk_b64 = metamorphic_crypto::b64::encode(&self.signing_public_key);
851 let ok = metamorphic_crypto::verify(&canonical, ctx.as_str(), &sig_b64, &pk_b64)
852 .unwrap_or(false);
853 if ok {
854 Ok(&self.policy)
855 } else {
856 Err(Error::InvalidSignature {
857 name: format!("{}/namespace-policy", self.policy.namespace.as_str()),
858 key_id: 0,
859 })
860 }
861 }
862
863 #[must_use]
875 pub fn canonical_bytes(&self) -> Vec<u8> {
876 let policy = self.policy.canonical_bytes();
877 let mut out = Vec::with_capacity(
878 4 + 12 + policy.len() + self.signing_public_key.len() + self.signature.len(),
879 );
880 out.extend_from_slice(&SIGNED_POLICY_FORMAT_VERSION.to_be_bytes());
881 push_lp(&mut out, &policy);
882 push_lp(&mut out, &self.signing_public_key);
883 push_lp(&mut out, &self.signature);
884 out
885 }
886
887 pub fn parse(bytes: &[u8]) -> Result<Self> {
895 let mut cur = Cursor::new(bytes);
896 let format_version = cur.u32()?;
897 if format_version != SIGNED_POLICY_FORMAT_VERSION {
898 return Err(Error::MalformedPolicy(format!(
899 "unknown signed-policy format version {format_version}"
900 )));
901 }
902 let policy = NamespacePolicy::parse(cur.lp()?)?;
903 let signing_public_key = cur.lp()?.to_vec();
904 let signature = cur.lp()?.to_vec();
905 if signing_public_key.is_empty() || signature.is_empty() {
906 return Err(Error::MalformedPolicy(
907 "signed policy must carry a non-empty key and signature".into(),
908 ));
909 }
910 if !cur.is_empty() {
911 return Err(Error::MalformedPolicy(
912 "trailing bytes after signed policy envelope".into(),
913 ));
914 }
915 Ok(Self {
916 policy,
917 signing_public_key,
918 signature,
919 })
920 }
921}
922
923#[derive(Debug, Clone, PartialEq, Eq)]
930pub struct PolicyChain {
931 versions: Vec<NamespacePolicy>,
932}
933
934impl PolicyChain {
935 pub fn genesis(policy: NamespacePolicy) -> Result<Self> {
941 if policy.prev_policy_hash.is_some() {
942 return Err(Error::PolicyMigrationRejected(
943 "genesis policy must not carry a prev_policy_hash".into(),
944 ));
945 }
946 Ok(Self {
947 versions: vec![policy],
948 })
949 }
950
951 #[must_use]
953 pub fn versions(&self) -> &[NamespacePolicy] {
954 &self.versions
955 }
956
957 #[must_use]
959 pub fn latest(&self) -> &NamespacePolicy {
960 self.versions
961 .last()
962 .expect("a PolicyChain always has at least the genesis version")
963 }
964
965 pub fn push(&mut self, next: NamespacePolicy) -> Result<()> {
977 let prev = self.latest();
978
979 if next.namespace != prev.namespace {
980 return Err(Error::PolicyMigrationRejected(format!(
981 "namespace changed from {:?} to {:?}",
982 prev.namespace.as_str(),
983 next.namespace.as_str()
984 )));
985 }
986 if next.policy_schema_version != prev.policy_schema_version + 1 {
987 return Err(Error::PolicyMigrationRejected(format!(
988 "policy_schema_version must increment by 1 ({} -> {}), got {}",
989 prev.policy_schema_version,
990 prev.policy_schema_version + 1,
991 next.policy_schema_version
992 )));
993 }
994 if next.effective_from <= prev.effective_from {
995 return Err(Error::PolicyMigrationRejected(format!(
996 "effective_from must strictly increase ({} -> {})",
997 prev.effective_from, next.effective_from
998 )));
999 }
1000 let expected_prev = prev.policy_hash()?;
1001 match next.prev_policy_hash {
1002 Some(h) if h == expected_prev => {}
1003 Some(_) => {
1004 return Err(Error::PolicyMigrationRejected(
1005 "prev_policy_hash does not chain to the prior version".into(),
1006 ));
1007 }
1008 None => {
1009 return Err(Error::PolicyMigrationRejected(
1010 "migration must carry a prev_policy_hash".into(),
1011 ));
1012 }
1013 }
1014 if next.security_level.rank() < prev.security_level.rank()
1015 || next.commitment_hash.rank() < prev.commitment_hash.rank()
1016 || next.vrf_mode.rank() < prev.vrf_mode.rank()
1017 {
1018 return Err(Error::PolicyMigrationRejected(format!(
1019 "migration would weaken posture (prev {:?}/{:?}/{:?} -> next {:?}/{:?}/{:?})",
1020 prev.security_level,
1021 prev.commitment_hash,
1022 prev.vrf_mode,
1023 next.security_level,
1024 next.commitment_hash,
1025 next.vrf_mode
1026 )));
1027 }
1028
1029 self.versions.push(next);
1030 Ok(())
1031 }
1032
1033 pub fn active_at(&self, position: u64) -> Result<&NamespacePolicy> {
1040 if position < self.versions[0].effective_from {
1041 return Err(Error::UnknownNamespacePolicy(format!(
1042 "tree position {position} precedes the genesis effective_from {}",
1043 self.versions[0].effective_from
1044 )));
1045 }
1046 let active = self
1049 .versions
1050 .iter()
1051 .rev()
1052 .find(|p| p.effective_from <= position)
1053 .expect("position >= genesis effective_from guarantees a match");
1054 Ok(active)
1055 }
1056}
1057
1058fn push_lp(out: &mut Vec<u8>, bytes: &[u8]) {
1062 out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
1063 out.extend_from_slice(bytes);
1064}
1065
1066struct Cursor<'a> {
1068 buf: &'a [u8],
1069 pos: usize,
1070}
1071
1072impl<'a> Cursor<'a> {
1073 fn new(buf: &'a [u8]) -> Self {
1074 Self { buf, pos: 0 }
1075 }
1076
1077 fn is_empty(&self) -> bool {
1078 self.pos >= self.buf.len()
1079 }
1080
1081 fn take(&mut self, n: usize) -> Result<&'a [u8]> {
1082 let end = self
1083 .pos
1084 .checked_add(n)
1085 .filter(|&e| e <= self.buf.len())
1086 .ok_or_else(|| {
1087 Error::MalformedPolicy(format!(
1088 "field of {n} bytes overruns the {}-byte buffer at offset {}",
1089 self.buf.len(),
1090 self.pos
1091 ))
1092 })?;
1093 let out = &self.buf[self.pos..end];
1094 self.pos = end;
1095 Ok(out)
1096 }
1097
1098 fn u8(&mut self) -> Result<u8> {
1099 Ok(self.take(1)?[0])
1100 }
1101
1102 fn u32(&mut self) -> Result<u32> {
1103 let b = self.take(4)?;
1104 Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
1105 }
1106
1107 fn u64(&mut self) -> Result<u64> {
1108 let b = self.take(8)?;
1109 Ok(u64::from_be_bytes([
1110 b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
1111 ]))
1112 }
1113
1114 fn lp(&mut self) -> Result<&'a [u8]> {
1115 let len = self.u32()? as usize;
1116 self.take(len)
1117 }
1118}
1119
1120#[cfg(all(test, not(target_arch = "wasm32")))]
1121mod tests {
1122 use super::*;
1123
1124 fn ns() -> Namespace {
1125 Namespace::parse("acme").unwrap()
1126 }
1127
1128 fn cat5_pure() -> NamespacePolicy {
1129 NamespacePolicy::genesis(
1130 ns(),
1131 SecurityLevel::Cat5,
1132 CheckpointSuite::PureCnsa2,
1133 0,
1134 1_700_000,
1135 )
1136 .unwrap()
1137 }
1138
1139 #[test]
1140 fn genesis_derives_commitment_hash_and_classical_vrf() {
1141 let p = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 0, 0)
1142 .unwrap();
1143 assert_eq!(p.commitment_hash(), CommitmentHash::Sha3_256);
1144 assert_eq!(p.vrf_mode(), VrfMode::Classical);
1145 assert_eq!(p.policy_schema_version(), 1);
1146 assert!(p.prev_policy_hash().is_none());
1147
1148 let p5 = cat5_pure();
1149 assert_eq!(p5.commitment_hash(), CommitmentHash::Sha3_512);
1150 }
1151
1152 #[test]
1153 fn canonical_round_trips_byte_for_byte() {
1154 let p = cat5_pure();
1155 let bytes = p.canonical_bytes();
1156 let parsed = NamespacePolicy::parse(&bytes).unwrap();
1157 assert_eq!(parsed, p);
1158 assert_eq!(parsed.canonical_bytes(), bytes);
1159 }
1160
1161 #[test]
1162 fn parse_rejects_malformed() {
1163 assert!(matches!(
1165 NamespacePolicy::parse(&[0, 0, 0, 1]),
1166 Err(Error::MalformedPolicy(_))
1167 ));
1168 let mut b = cat5_pure().canonical_bytes();
1170 b.push(0xff);
1171 assert!(matches!(
1172 NamespacePolicy::parse(&b),
1173 Err(Error::MalformedPolicy(_))
1174 ));
1175 }
1176
1177 #[test]
1178 fn rejects_commitment_hash_not_matching_level() {
1179 let r = NamespacePolicy::new(
1180 ns(),
1181 1,
1182 SecurityLevel::Cat5,
1183 CheckpointSuite::Hybrid,
1184 CommitmentHash::Sha3_256, VrfMode::Classical,
1186 0,
1187 0,
1188 None,
1189 );
1190 assert!(matches!(r, Err(Error::MalformedPolicy(_))));
1191 }
1192
1193 #[test]
1194 fn rejects_non_classical_vrf_and_purecnsa2_below_cat5() {
1195 assert!(matches!(
1196 NamespacePolicy::new(
1197 ns(),
1198 1,
1199 SecurityLevel::Cat5,
1200 CheckpointSuite::Hybrid,
1201 CommitmentHash::Sha3_512,
1202 VrfMode::HybridOutput,
1203 0,
1204 0,
1205 None,
1206 ),
1207 Err(Error::MalformedPolicy(_))
1208 ));
1209 assert!(matches!(
1210 NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::PureCnsa2, 0, 0),
1211 Err(Error::MalformedPolicy(_))
1212 ));
1213 }
1214
1215 #[test]
1216 fn policy_hash_is_stable_and_context_bound() {
1217 let p = cat5_pure();
1218 assert_eq!(p.policy_hash().unwrap(), p.policy_hash().unwrap());
1219 let other = NamespacePolicy::genesis(
1221 Namespace::parse("other").unwrap(),
1222 SecurityLevel::Cat5,
1223 CheckpointSuite::PureCnsa2,
1224 0,
1225 1_700_000,
1226 )
1227 .unwrap();
1228 assert_ne!(p.policy_hash().unwrap(), other.policy_hash().unwrap());
1229 }
1230
1231 #[test]
1232 fn enforce_vrf_suite_id_classical() {
1233 let p = cat5_pure();
1234 assert!(p.enforce_vrf_suite_id(0x03).is_ok());
1235 assert!(matches!(
1236 p.enforce_vrf_suite_id(0x04),
1237 Err(Error::PostureMismatch { .. })
1238 ));
1239 }
1240
1241 #[test]
1242 fn enforce_commitment_hash() {
1243 let p = cat5_pure();
1244 assert!(p.enforce_commitment_hash(CommitmentHash::Sha3_512).is_ok());
1245 assert!(matches!(
1246 p.enforce_commitment_hash(CommitmentHash::Sha3_256),
1247 Err(Error::PostureMismatch { .. })
1248 ));
1249 }
1250
1251 #[test]
1252 fn migration_strengthen_ok_weaken_rejected() {
1253 let g = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 0, 0)
1254 .unwrap();
1255 let mut chain = PolicyChain::genesis(g.clone()).unwrap();
1256
1257 let v2 = NamespacePolicy::new(
1259 ns(),
1260 2,
1261 SecurityLevel::Cat5,
1262 CheckpointSuite::Hybrid,
1263 CommitmentHash::Sha3_512,
1264 VrfMode::Classical,
1265 100,
1266 1,
1267 Some(g.policy_hash().unwrap()),
1268 )
1269 .unwrap();
1270 chain.push(v2.clone()).unwrap();
1271 assert_eq!(chain.versions().len(), 2);
1272
1273 let weak = NamespacePolicy::new(
1275 ns(),
1276 3,
1277 SecurityLevel::Cat3,
1278 CheckpointSuite::Hybrid,
1279 CommitmentHash::Sha3_256,
1280 VrfMode::Classical,
1281 200,
1282 2,
1283 Some(v2.policy_hash().unwrap()),
1284 )
1285 .unwrap();
1286 assert!(matches!(
1287 chain.push(weak),
1288 Err(Error::PolicyMigrationRejected(_))
1289 ));
1290 }
1291
1292 #[test]
1293 fn migration_rejects_bad_chain_links() {
1294 let g = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 0, 0)
1295 .unwrap();
1296 let mut chain = PolicyChain::genesis(g.clone()).unwrap();
1297
1298 let bad_prev = NamespacePolicy::new(
1300 ns(),
1301 2,
1302 SecurityLevel::Cat3,
1303 CheckpointSuite::Hybrid,
1304 CommitmentHash::Sha3_256,
1305 VrfMode::Classical,
1306 10,
1307 1,
1308 Some([0u8; POLICY_HASH_LEN]),
1309 )
1310 .unwrap();
1311 assert!(matches!(
1312 chain.push(bad_prev),
1313 Err(Error::PolicyMigrationRejected(_))
1314 ));
1315
1316 let bad_ver = NamespacePolicy::new(
1318 ns(),
1319 3,
1320 SecurityLevel::Cat3,
1321 CheckpointSuite::Hybrid,
1322 CommitmentHash::Sha3_256,
1323 VrfMode::Classical,
1324 10,
1325 1,
1326 Some(g.policy_hash().unwrap()),
1327 )
1328 .unwrap();
1329 assert!(matches!(
1330 chain.push(bad_ver),
1331 Err(Error::PolicyMigrationRejected(_))
1332 ));
1333 }
1334
1335 #[test]
1336 fn active_at_resolves_half_open_ranges() {
1337 let g = NamespacePolicy::genesis(ns(), SecurityLevel::Cat3, CheckpointSuite::Hybrid, 5, 0)
1338 .unwrap();
1339 let mut chain = PolicyChain::genesis(g.clone()).unwrap();
1340 let v2 = NamespacePolicy::new(
1341 ns(),
1342 2,
1343 SecurityLevel::Cat5,
1344 CheckpointSuite::Hybrid,
1345 CommitmentHash::Sha3_512,
1346 VrfMode::Classical,
1347 10,
1348 1,
1349 Some(g.policy_hash().unwrap()),
1350 )
1351 .unwrap();
1352 chain.push(v2).unwrap();
1353
1354 assert!(matches!(
1355 chain.active_at(4),
1356 Err(Error::UnknownNamespacePolicy(_))
1357 ));
1358 assert_eq!(chain.active_at(5).unwrap().policy_schema_version(), 1);
1359 assert_eq!(chain.active_at(9).unwrap().policy_schema_version(), 1);
1360 assert_eq!(chain.active_at(10).unwrap().policy_schema_version(), 2);
1361 assert_eq!(chain.active_at(1000).unwrap().policy_schema_version(), 2);
1362 }
1363}