1use crate::context::{EvaluationContext, FieldValue};
4use crate::decision::Decision;
5use crate::EvaluationRequest;
6use crue_dsl::compiler::Bytecode;
7use serde::Serialize;
8
9const PROOF_BINDING_SERIALIZATION_VERSION: u8 = 1;
10const PROOF_BINDING_SCHEMA_ID: &str = "rsrp.proof.binding.v1";
11const PROOF_ENVELOPE_SERIALIZATION_VERSION: u8 = 1;
12const PROOF_ENVELOPE_SCHEMA_ID: &str = "rsrp.proof.envelope.v1";
13pub const PROOF_ENVELOPE_V1_VERSION: u8 = 1;
14pub const PROOF_ENVELOPE_V1_ENCODING_VERSION: u8 = 1;
15#[cfg(feature = "pq-proof")]
16const PQ_PROOF_ENVELOPE_SERIALIZATION_VERSION: u8 = 1;
17#[cfg(feature = "pq-proof")]
18const PQ_PROOF_ENVELOPE_SCHEMA_ID: &str = "rsrp.proof.envelope.pq-hybrid.v1";
19
20#[derive(Debug, Clone, Serialize, serde::Deserialize)]
21pub struct ProofBinding {
22 pub serialization_version: u8,
23 pub schema_id: String,
24 pub runtime_version: String,
25 pub crypto_backend_id: String,
26 pub policy_hash: String,
27 pub bytecode_hash: String,
28 pub input_hash: String,
29 pub state_hash: String,
30 pub decision: Decision,
31}
32
33#[derive(Debug, Clone, Serialize, serde::Deserialize, PartialEq, Eq)]
34pub struct ProofEnvelope {
35 pub serialization_version: u8,
36 pub schema_id: String,
37 pub signature_algorithm: String,
38 pub signer_key_id: String,
39 pub binding: ProofBinding,
40 pub signature: Vec<u8>,
41}
42
43#[cfg(feature = "pq-proof")]
44#[derive(Clone, Serialize, serde::Deserialize)]
45pub struct PqProofEnvelope {
46 pub serialization_version: u8,
47 pub schema_id: String,
48 pub signature_algorithm: String,
49 pub signer_key_id: String,
50 pub pq_backend_id: String,
51 pub level: pqcrypto::DilithiumLevel,
52 pub binding: ProofBinding,
53 pub signature: pqcrypto::hybrid::HybridSignature,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
58#[repr(u8)]
59pub enum DecisionCodeV1 {
60 Allow = 1,
61 Block = 2,
62 Warn = 3,
63 ApprovalRequired = 4,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
68#[repr(u8)]
69pub enum SignatureAlgorithmCodeV1 {
70 Ed25519 = 1,
71 #[cfg(feature = "pq-proof")]
72 HybridEd25519Mldsa = 2,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
76pub struct Ed25519SignatureV1 {
77 pub key_id_hash: [u8; 32],
78 pub signature: Vec<u8>,
79}
80
81#[cfg(feature = "pq-proof")]
82#[derive(Clone, Serialize, serde::Deserialize)]
83pub struct HybridSignatureV1 {
84 pub key_id_hash: [u8; 32],
85 pub backend_id_hash: [u8; 32],
86 pub level_code: u8,
87 pub signature: pqcrypto::hybrid::HybridSignature,
88}
89
90#[derive(Clone, Serialize, serde::Deserialize)]
91pub enum SignatureV1 {
92 Ed25519(Ed25519SignatureV1),
93 #[cfg(feature = "pq-proof")]
94 Hybrid(HybridSignatureV1),
95}
96
97#[derive(Clone, Serialize, serde::Deserialize)]
99pub struct ProofEnvelopeV1 {
100 pub version: u8,
101 pub encoding_version: u8,
102 pub runtime_version: u16,
103 pub policy_hash: [u8; 32],
104 pub bytecode_hash: [u8; 32],
105 pub input_hash: [u8; 32],
106 pub state_hash: [u8; 32],
107 pub decision_code: u8,
108 pub signature: SignatureV1,
109}
110
111impl ProofBinding {
112 pub fn create(
113 bytecode: &Bytecode,
114 request: &EvaluationRequest,
115 ctx: &EvaluationContext,
116 decision: Decision,
117 crypto_backend_id: &str,
118 ) -> Result<Self, String> {
119 Self::create_with_policy_hash(
120 bytecode,
121 request,
122 ctx,
123 decision,
124 crypto_backend_id,
125 None,
126 )
127 }
128
129 pub fn create_with_policy_hash(
130 bytecode: &Bytecode,
131 request: &EvaluationRequest,
132 ctx: &EvaluationContext,
133 decision: Decision,
134 crypto_backend_id: &str,
135 policy_hash_hex: Option<&str>,
136 ) -> Result<Self, String> {
137 let bytecode_hash = sha256_hex(&canonical_json_bytes(bytecode)?);
138 let policy_hash = policy_hash_hex.unwrap_or(&bytecode_hash).to_string();
139 Ok(Self {
140 serialization_version: PROOF_BINDING_SERIALIZATION_VERSION,
141 schema_id: PROOF_BINDING_SCHEMA_ID.to_string(),
142 runtime_version: env!("CARGO_PKG_VERSION").to_string(),
143 crypto_backend_id: crypto_backend_id.to_string(),
144 policy_hash,
145 bytecode_hash,
146 input_hash: sha256_hex(&canonical_json_bytes(request)?),
147 state_hash: sha256_hex(&canonical_json_bytes(&state_snapshot(ctx))?),
148 decision,
149 })
150 }
151
152 pub fn verify_recompute(
153 &self,
154 bytecode: &Bytecode,
155 request: &EvaluationRequest,
156 ctx: &EvaluationContext,
157 decision: Decision,
158 crypto_backend_id: &str,
159 ) -> Result<bool, String> {
160 let recomputed = Self::create_with_policy_hash(
161 bytecode,
162 request,
163 ctx,
164 decision,
165 crypto_backend_id,
166 Some(&self.policy_hash),
167 )?;
168 Ok(self.serialization_version == PROOF_BINDING_SERIALIZATION_VERSION
169 && self.schema_id == PROOF_BINDING_SCHEMA_ID
170 && self == &recomputed)
171 }
172
173 pub fn canonical_bytes(&self) -> Result<Vec<u8>, String> {
174 let json = canonical_json_bytes(self)?;
175 let schema_len: u16 = self
176 .schema_id
177 .len()
178 .try_into()
179 .map_err(|_| "schema_id too long".to_string())?;
180 let payload_len: u32 = json
181 .len()
182 .try_into()
183 .map_err(|_| "payload too long".to_string())?;
184 let mut out = Vec::with_capacity(1 + 2 + self.schema_id.len() + 4 + json.len());
185 out.push(self.serialization_version);
186 out.extend_from_slice(&schema_len.to_be_bytes());
187 out.extend_from_slice(self.schema_id.as_bytes());
188 out.extend_from_slice(&payload_len.to_be_bytes());
189 out.extend_from_slice(&json);
190 Ok(out)
191 }
192}
193
194impl PartialEq for ProofBinding {
195 fn eq(&self, other: &Self) -> bool {
196 self.serialization_version == other.serialization_version
197 && self.schema_id == other.schema_id
198 && self.runtime_version == other.runtime_version
199 && self.crypto_backend_id == other.crypto_backend_id
200 && self.policy_hash == other.policy_hash
201 && self.bytecode_hash == other.bytecode_hash
202 && self.input_hash == other.input_hash
203 && self.state_hash == other.state_hash
204 && self.decision == other.decision
205 }
206}
207
208impl Eq for ProofBinding {}
209
210impl ProofEnvelope {
211 pub fn sign_ed25519(
212 binding: ProofBinding,
213 signer_key_id: impl Into<String>,
214 key_pair: &crypto_core::signature::Ed25519KeyPair,
215 ) -> Result<Self, String> {
216 let payload = binding.canonical_bytes()?;
217 let signature = key_pair.sign(&payload);
218 Ok(Self {
219 serialization_version: PROOF_ENVELOPE_SERIALIZATION_VERSION,
220 schema_id: PROOF_ENVELOPE_SCHEMA_ID.to_string(),
221 signature_algorithm: "ED25519".to_string(),
222 signer_key_id: signer_key_id.into(),
223 binding,
224 signature,
225 })
226 }
227
228 pub fn verify_ed25519(&self, public_key: &[u8]) -> Result<bool, String> {
229 if self.serialization_version != PROOF_ENVELOPE_SERIALIZATION_VERSION
230 || self.schema_id != PROOF_ENVELOPE_SCHEMA_ID
231 || self.signature_algorithm != "ED25519"
232 {
233 return Ok(false);
234 }
235 let payload = self.binding.canonical_bytes()?;
236 crypto_core::signature::verify(
237 &payload,
238 &self.signature,
239 public_key,
240 crypto_core::SignatureAlgorithm::Ed25519,
241 )
242 .map_err(|e| e.to_string())
243 }
244}
245
246#[cfg(feature = "pq-proof")]
247impl PqProofEnvelope {
248 pub fn sign_hybrid(
249 binding: ProofBinding,
250 signer_key_id: impl Into<String>,
251 signer: &pqcrypto::hybrid::HybridSigner,
252 keypair: &pqcrypto::hybrid::HybridKeyPair,
253 ) -> Result<Self, String> {
254 let payload = binding.canonical_bytes()?;
255 let signature = signer
256 .sign(keypair, &payload)
257 .map_err(|e| e.to_string())?;
258
259 Ok(Self {
260 serialization_version: PQ_PROOF_ENVELOPE_SERIALIZATION_VERSION,
261 schema_id: PQ_PROOF_ENVELOPE_SCHEMA_ID.to_string(),
262 signature_algorithm: "HYBRID-ED25519+ML-DSA".to_string(),
263 signer_key_id: signer_key_id.into(),
264 pq_backend_id: signer.backend_id().to_string(),
265 level: keypair.level,
266 binding,
267 signature,
268 })
269 }
270
271 pub fn verify_hybrid(
272 &self,
273 public_key: &pqcrypto::hybrid::HybridPublicKey,
274 ) -> Result<bool, String> {
275 if self.serialization_version != PQ_PROOF_ENVELOPE_SERIALIZATION_VERSION
276 || self.schema_id != PQ_PROOF_ENVELOPE_SCHEMA_ID
277 || self.signature_algorithm != "HYBRID-ED25519+ML-DSA"
278 || self.level != public_key.level
279 || self.signature.quantum.level != self.level
280 {
281 return Ok(false);
282 }
283
284 let payload = self.binding.canonical_bytes()?;
285 let verifier = pqcrypto::hybrid::HybridVerifier::new(self.level);
286 verifier
287 .verify_public(public_key, &payload, &self.signature)
288 .map_err(|e| e.to_string())
289 }
290}
291
292impl ProofEnvelopeV1 {
293 pub fn sign_ed25519(
294 binding: &ProofBinding,
295 signer_key_id: impl AsRef<str>,
296 key_pair: &crypto_core::signature::Ed25519KeyPair,
297 ) -> Result<Self, String> {
298 let mut envelope = Self::unsigned_from_binding(binding, SignatureV1::Ed25519(Ed25519SignatureV1 {
299 key_id_hash: sha256_fixed(signer_key_id.as_ref().as_bytes()),
300 signature: Vec::new(),
301 }))?;
302 let payload = envelope.signing_bytes()?;
303 match &mut envelope.signature {
304 SignatureV1::Ed25519(sig) => sig.signature = key_pair.sign(&payload),
305 #[cfg(feature = "pq-proof")]
306 SignatureV1::Hybrid(_) => return Err("invalid signature variant for ed25519 signing".to_string()),
307 }
308 Ok(envelope)
309 }
310
311 #[cfg(feature = "pq-proof")]
312 pub fn sign_hybrid(
313 binding: &ProofBinding,
314 signer_key_id: impl AsRef<str>,
315 signer: &pqcrypto::hybrid::HybridSigner,
316 keypair: &pqcrypto::hybrid::HybridKeyPair,
317 ) -> Result<Self, String> {
318 let mut envelope = Self::unsigned_from_binding(
319 binding,
320 SignatureV1::Hybrid(HybridSignatureV1 {
321 key_id_hash: sha256_fixed(signer_key_id.as_ref().as_bytes()),
322 backend_id_hash: sha256_fixed(signer.backend_id().as_bytes()),
323 level_code: dilithium_level_code(keypair.level),
324 signature: pqcrypto::hybrid::HybridSignature::new(
325 Vec::new(),
326 pqcrypto::signature::DilithiumSignature {
327 level: keypair.level,
328 signature: Vec::new(),
329 },
330 ),
331 }),
332 )?;
333 let payload = envelope.signing_bytes()?;
334 let sig = signer.sign(keypair, &payload).map_err(|e| e.to_string())?;
335 if let SignatureV1::Hybrid(h) = &mut envelope.signature {
336 h.signature = sig;
337 }
338 Ok(envelope)
339 }
340
341 pub fn verify_ed25519(&self, public_key: &[u8]) -> Result<bool, String> {
342 let sig = match &self.signature {
343 SignatureV1::Ed25519(sig) => sig,
344 #[cfg(feature = "pq-proof")]
345 SignatureV1::Hybrid(_) => return Ok(false),
346 };
347 let payload = self.signing_bytes()?;
348 crypto_core::signature::verify(
349 &payload,
350 &sig.signature,
351 public_key,
352 crypto_core::SignatureAlgorithm::Ed25519,
353 )
354 .map_err(|e| e.to_string())
355 }
356
357 #[cfg(feature = "pq-proof")]
358 pub fn verify_hybrid(
359 &self,
360 public_key: &pqcrypto::hybrid::HybridPublicKey,
361 ) -> Result<bool, String> {
362 let SignatureV1::Hybrid(sig) = &self.signature else {
363 return Ok(false);
364 };
365 if sig.level_code != dilithium_level_code(public_key.level) {
366 return Ok(false);
367 }
368 if sig.backend_id_hash == [0u8; 32] {
369 return Ok(false);
370 }
371 let payload = self.signing_bytes()?;
372 let verifier = pqcrypto::hybrid::HybridVerifier::new(public_key.level);
373 verifier
374 .verify_public(public_key, &payload, &sig.signature)
375 .map_err(|e| e.to_string())
376 }
377
378 pub fn signing_bytes(&self) -> Result<Vec<u8>, String> {
380 let mut out = Vec::with_capacity(1 + 1 + 2 + (32 * 4) + 1);
381 out.push(self.version);
382 out.push(self.encoding_version);
383 out.extend_from_slice(&self.runtime_version.to_be_bytes());
384 out.extend_from_slice(&self.policy_hash);
385 out.extend_from_slice(&self.bytecode_hash);
386 out.extend_from_slice(&self.input_hash);
387 out.extend_from_slice(&self.state_hash);
388 out.push(self.decision_code);
389 let sig_meta = self.signature.meta_bytes()?;
390 let sig_meta_len: u16 = sig_meta
391 .len()
392 .try_into()
393 .map_err(|_| "signature metadata too large".to_string())?;
394 out.extend_from_slice(&sig_meta_len.to_be_bytes());
395 out.extend_from_slice(&sig_meta);
396 Ok(out)
397 }
398
399 pub fn canonical_bytes(&self) -> Result<Vec<u8>, String> {
401 let mut out = self.signing_bytes()?;
402 let sig_bytes = self.signature.signature_owned_bytes();
403 let sig_len: u32 = sig_bytes
404 .len()
405 .try_into()
406 .map_err(|_| "signature too large".to_string())?;
407 out.extend_from_slice(&sig_len.to_be_bytes());
408 out.extend_from_slice(&sig_bytes);
409 Ok(out)
410 }
411
412 pub fn decision(&self) -> Result<Decision, String> {
413 decision_from_code(self.decision_code)
414 }
415
416 fn unsigned_from_binding(binding: &ProofBinding, signature: SignatureV1) -> Result<Self, String> {
417 Ok(Self {
418 version: PROOF_ENVELOPE_V1_VERSION,
419 encoding_version: PROOF_ENVELOPE_V1_ENCODING_VERSION,
420 runtime_version: pack_runtime_version_u16(&binding.runtime_version)?,
421 policy_hash: hex32(&binding.policy_hash)?,
422 bytecode_hash: hex32(&binding.bytecode_hash)?,
423 input_hash: hex32(&binding.input_hash)?,
424 state_hash: hex32(&binding.state_hash)?,
425 decision_code: decision_to_code(binding.decision) as u8,
426 signature,
427 })
428 }
429}
430
431impl SignatureV1 {
432 fn meta_bytes(&self) -> Result<Vec<u8>, String> {
433 match self {
434 SignatureV1::Ed25519(sig) => {
435 let mut out = Vec::with_capacity(1 + 32);
436 out.push(SignatureAlgorithmCodeV1::Ed25519 as u8);
437 out.extend_from_slice(&sig.key_id_hash);
438 Ok(out)
439 }
440 #[cfg(feature = "pq-proof")]
441 SignatureV1::Hybrid(sig) => {
442 let mut out = Vec::with_capacity(1 + 32 + 32 + 1);
443 out.push(SignatureAlgorithmCodeV1::HybridEd25519Mldsa as u8);
444 out.extend_from_slice(&sig.key_id_hash);
445 out.extend_from_slice(&sig.backend_id_hash);
446 out.push(sig.level_code);
447 Ok(out)
448 }
449 }
450 }
451
452 fn signature_owned_bytes(&self) -> Vec<u8> {
453 match self {
454 SignatureV1::Ed25519(sig) => sig.signature.clone(),
455 #[cfg(feature = "pq-proof")]
456 SignatureV1::Hybrid(sig) => sig.signature.to_bytes(),
457 }
458 }
459}
460
461#[derive(Serialize)]
462struct StateField<'a> {
463 key: &'a str,
464 value: &'a FieldValue,
465}
466
467fn state_snapshot(ctx: &EvaluationContext) -> Vec<StateField<'_>> {
468 let mut items: Vec<_> = ctx.fields().iter().collect();
469 items.sort_by(|(ka, _), (kb, _)| ka.cmp(kb));
470 items.into_iter()
471 .map(|(key, value)| StateField {
472 key: key.as_str(),
473 value,
474 })
475 .collect()
476}
477
478fn canonical_json_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>, String> {
479 serde_json::to_vec(value).map_err(|e| e.to_string())
480}
481
482fn sha256_hex(data: &[u8]) -> String {
483 use crypto_core::hash::{hex_encode, sha256};
484 hex_encode(&sha256(data))
485}
486
487fn sha256_fixed(data: &[u8]) -> [u8; 32] {
488 let digest = crypto_core::hash::sha256(data);
489 let mut out = [0u8; 32];
490 out.copy_from_slice(&digest[..32]);
491 out
492}
493
494fn hex32(hex: &str) -> Result<[u8; 32], String> {
495 let decoded = crypto_core::hash::hex_decode(hex).map_err(|e| e.to_string())?;
496 if decoded.len() != 32 {
497 return Err(format!("expected 32-byte hash, got {}", decoded.len()));
498 }
499 let mut out = [0u8; 32];
500 out.copy_from_slice(&decoded);
501 Ok(out)
502}
503
504fn pack_runtime_version_u16(runtime_version: &str) -> Result<u16, String> {
505 let mut parts = runtime_version.split('.');
507 let major: u16 = parts
508 .next()
509 .ok_or_else(|| "missing major runtime version".to_string())?
510 .parse()
511 .map_err(|_| "invalid major runtime version".to_string())?;
512 let minor: u16 = parts
513 .next()
514 .ok_or_else(|| "missing minor runtime version".to_string())?
515 .parse()
516 .map_err(|_| "invalid minor runtime version".to_string())?;
517 if major > 0xFF || minor > 0xFF {
518 return Err("runtime_version major/minor exceed u8 packing".to_string());
519 }
520 Ok((major << 8) | minor)
521}
522
523fn decision_to_code(decision: Decision) -> DecisionCodeV1 {
524 match decision {
525 Decision::Allow => DecisionCodeV1::Allow,
526 Decision::Block => DecisionCodeV1::Block,
527 Decision::Warn => DecisionCodeV1::Warn,
528 Decision::ApprovalRequired => DecisionCodeV1::ApprovalRequired,
529 }
530}
531
532fn decision_from_code(code: u8) -> Result<Decision, String> {
533 match code {
534 x if x == DecisionCodeV1::Allow as u8 => Ok(Decision::Allow),
535 x if x == DecisionCodeV1::Block as u8 => Ok(Decision::Block),
536 x if x == DecisionCodeV1::Warn as u8 => Ok(Decision::Warn),
537 x if x == DecisionCodeV1::ApprovalRequired as u8 => Ok(Decision::ApprovalRequired),
538 _ => Err(format!("invalid decision code {}", code)),
539 }
540}
541
542#[cfg(feature = "pq-proof")]
543fn dilithium_level_code(level: pqcrypto::DilithiumLevel) -> u8 {
544 match level {
545 pqcrypto::DilithiumLevel::Dilithium2 => 2,
546 pqcrypto::DilithiumLevel::Dilithium3 => 3,
547 pqcrypto::DilithiumLevel::Dilithium5 => 5,
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use crate::context::EvaluationContext;
555
556 fn fixed_hash_hex(byte: u8) -> String {
557 crypto_core::hash::hex_encode(&[byte; 32])
558 }
559
560 #[test]
561 fn test_proof_binding_recompute_detects_bytecode_change() {
562 let src = r#"
563RULE CRUE_001 VERSION 1.0
564WHEN
565 agent.requests_last_hour >= 50
566THEN
567 BLOCK WITH CODE "VOLUME_EXCEEDED"
568"#;
569 let ast = crue_dsl::parser::parse(src).unwrap();
570 let mut bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
571
572 let req = crate::EvaluationRequest {
573 request_id: "r".into(),
574 agent_id: "a".into(),
575 agent_org: "o".into(),
576 agent_level: "l".into(),
577 mission_id: None,
578 mission_type: None,
579 query_type: None,
580 justification: None,
581 export_format: None,
582 result_limit: None,
583 requests_last_hour: 60,
584 requests_last_24h: 10,
585 results_last_query: 1,
586 account_department: None,
587 allowed_departments: vec![],
588 request_hour: 9,
589 is_within_mission_hours: true,
590 };
591 let ctx = EvaluationContext::from_request(&req);
592 let proof = ProofBinding::create(&bytecode, &req, &ctx, Decision::Block, "mock-crypto").unwrap();
593 assert!(proof
594 .verify_recompute(&bytecode, &req, &ctx, Decision::Block, "mock-crypto")
595 .unwrap());
596
597 bytecode.instructions.push(0x00);
598 assert!(!proof
599 .verify_recompute(&bytecode, &req, &ctx, Decision::Block, "mock-crypto")
600 .unwrap());
601 }
602
603 #[test]
604 fn test_proof_envelope_ed25519_sign_verify() {
605 let src = r#"
606RULE CRUE_002 VERSION 1.0
607WHEN
608 agent.requests_last_hour >= 10
609THEN
610 BLOCK WITH CODE "TEST"
611"#;
612 let ast = crue_dsl::parser::parse(src).unwrap();
613 let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
614 let req = crate::EvaluationRequest {
615 request_id: "r".into(),
616 agent_id: "a".into(),
617 agent_org: "o".into(),
618 agent_level: "l".into(),
619 mission_id: None,
620 mission_type: None,
621 query_type: None,
622 justification: None,
623 export_format: None,
624 result_limit: None,
625 requests_last_hour: 12,
626 requests_last_24h: 10,
627 results_last_query: 1,
628 account_department: None,
629 allowed_departments: vec![],
630 request_hour: 9,
631 is_within_mission_hours: true,
632 };
633 let ctx = EvaluationContext::from_request(&req);
634 let binding =
635 ProofBinding::create(&bytecode, &req, &ctx, Decision::Block, "mock-crypto").unwrap();
636 let kp = crypto_core::signature::Ed25519KeyPair::generate().unwrap();
637 let envelope = ProofEnvelope::sign_ed25519(binding, "proof-key-1", &kp).unwrap();
638 let pk = kp.verifying_key();
639 assert!(envelope.verify_ed25519(&pk).unwrap());
640 }
641
642 #[test]
643 fn test_proof_envelope_v1_ed25519_vector_fixture() {
644 let binding = ProofBinding {
645 serialization_version: 1,
646 schema_id: "rsrp.proof.binding.v1".to_string(),
647 runtime_version: "0.9.1".to_string(),
648 crypto_backend_id: "mock-crypto".to_string(),
649 policy_hash: fixed_hash_hex(0x11),
650 bytecode_hash: fixed_hash_hex(0x22),
651 input_hash: fixed_hash_hex(0x33),
652 state_hash: fixed_hash_hex(0x44),
653 decision: Decision::Block,
654 };
655 let kp = crypto_core::signature::Ed25519KeyPair::derive_from_secret(
656 b"rsrp-proof-envelope-v1-ed25519-test-vector",
657 Some("fixture-ed25519-key".into()),
658 );
659 let pk = kp.verifying_key();
660 let env = ProofEnvelopeV1::sign_ed25519(&binding, "fixture-ed25519-key", &kp).unwrap();
661 assert_eq!(env.decision().unwrap(), Decision::Block);
662 assert!(env.verify_ed25519(&pk).unwrap());
663
664 let signing_hex = crypto_core::hash::hex_encode(&env.signing_bytes().unwrap());
665 let canonical_hex = crypto_core::hash::hex_encode(&env.canonical_bytes().unwrap());
666
667 assert_eq!(
668 signing_hex,
669 "01010009111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444402002101e7e331964026891ae93f6f0d4b20c19f95cf20d6c6ba87fd73e287b081a46201"
670 );
671 assert_eq!(
672 canonical_hex,
673 "01010009111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444402002101e7e331964026891ae93f6f0d4b20c19f95cf20d6c6ba87fd73e287b081a4620100000040ec3e14a8311ebc1d76c65054b7b011cbf9b10d6796417b9e69bc3cb28fd6aab41228c26d034d52b6690680ea27617a35db24993cd24dd296c3905b1338272d05"
674 );
675 }
676
677 #[test]
678 fn test_pack_runtime_version_u16_ignores_patch() {
679 assert_eq!(pack_runtime_version_u16("0.9.4").unwrap(), pack_runtime_version_u16("0.9.99").unwrap());
680 assert_eq!(pack_runtime_version_u16("1.2.3").unwrap(), 0x0102);
681 }
682
683 #[cfg(feature = "pq-proof")]
684 #[test]
685 fn test_pq_proof_envelope_hybrid_sign_verify() {
686 let src = r#"
687RULE CRUE_003 VERSION 1.0
688WHEN
689 agent.requests_last_hour >= 10
690THEN
691 BLOCK WITH CODE "TEST"
692"#;
693 let ast = crue_dsl::parser::parse(src).unwrap();
694 let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
695 let req = crate::EvaluationRequest {
696 request_id: "r".into(),
697 agent_id: "a".into(),
698 agent_org: "o".into(),
699 agent_level: "l".into(),
700 mission_id: None,
701 mission_type: None,
702 query_type: None,
703 justification: None,
704 export_format: None,
705 result_limit: None,
706 requests_last_hour: 12,
707 requests_last_24h: 10,
708 results_last_query: 1,
709 account_department: None,
710 allowed_departments: vec![],
711 request_hour: 9,
712 is_within_mission_hours: true,
713 };
714 let ctx = EvaluationContext::from_request(&req);
715 let binding =
716 ProofBinding::create(&bytecode, &req, &ctx, Decision::Block, "mock-crypto").unwrap();
717
718 let signer = pqcrypto::hybrid::HybridSigner::new(pqcrypto::DilithiumLevel::Dilithium2);
719 let kp = signer.generate_keypair().unwrap();
720 let pk = kp.public_key();
721 let envelope = PqProofEnvelope::sign_hybrid(binding, "pq-proof-key-1", &signer, &kp).unwrap();
722 assert_eq!(envelope.pq_backend_id, signer.backend_id());
723 assert!(envelope.verify_hybrid(&pk).unwrap());
724 }
725}