1use crate::context::{EvaluationContext, FieldValue};
4use crate::decision::Decision;
5use crate::EvaluationRequest;
6use crue_dsl::compiler::{ActionDecision, ActionInstruction, Bytecode, Constant};
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: u32,
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(bytecode, request, ctx, decision, crypto_backend_id, None)
120 }
121
122 pub fn create_with_policy_hash(
123 bytecode: &Bytecode,
124 request: &EvaluationRequest,
125 ctx: &EvaluationContext,
126 decision: Decision,
127 crypto_backend_id: &str,
128 policy_hash_hex: Option<&str>,
129 ) -> Result<Self, String> {
130 let bytecode_hash = sha256_hex(&canonical_bytecode_bytes(bytecode)?);
131 let policy_hash = policy_hash_hex.unwrap_or(&bytecode_hash).to_string();
132 Ok(Self {
133 serialization_version: PROOF_BINDING_SERIALIZATION_VERSION,
134 schema_id: PROOF_BINDING_SCHEMA_ID.to_string(),
135 runtime_version: env!("CARGO_PKG_VERSION").to_string(),
136 crypto_backend_id: crypto_backend_id.to_string(),
137 policy_hash,
138 bytecode_hash,
139 input_hash: sha256_hex(&canonical_request_bytes(request)?),
140 state_hash: sha256_hex(&canonical_state_bytes(&state_snapshot(ctx))?),
141 decision,
142 })
143 }
144
145 pub fn verify_recompute(
146 &self,
147 bytecode: &Bytecode,
148 request: &EvaluationRequest,
149 ctx: &EvaluationContext,
150 decision: Decision,
151 crypto_backend_id: &str,
152 ) -> Result<bool, String> {
153 let recomputed = Self::create_with_policy_hash(
154 bytecode,
155 request,
156 ctx,
157 decision,
158 crypto_backend_id,
159 Some(&self.policy_hash),
160 )?;
161 Ok(
162 self.serialization_version == PROOF_BINDING_SERIALIZATION_VERSION
163 && self.schema_id == PROOF_BINDING_SCHEMA_ID
164 && self == &recomputed,
165 )
166 }
167
168 pub fn canonical_bytes(&self) -> Result<Vec<u8>, String> {
169 let payload = self.canonical_payload_bytes()?;
170 let schema_len: u16 = self
171 .schema_id
172 .len()
173 .try_into()
174 .map_err(|_| "schema_id too long".to_string())?;
175 let payload_len: u32 = payload
176 .len()
177 .try_into()
178 .map_err(|_| "payload too long".to_string())?;
179 let mut out = Vec::with_capacity(1 + 2 + self.schema_id.len() + 4 + payload.len());
180 out.push(self.serialization_version);
181 out.extend_from_slice(&schema_len.to_be_bytes());
182 out.extend_from_slice(self.schema_id.as_bytes());
183 out.extend_from_slice(&payload_len.to_be_bytes());
184 out.extend_from_slice(&payload);
185 Ok(out)
186 }
187
188 fn canonical_payload_bytes(&self) -> Result<Vec<u8>, String> {
189 let mut out = Vec::with_capacity(
190 1 + 2 + self.runtime_version.len() + 2 + self.crypto_backend_id.len() + (32 * 4) + 1,
191 );
192 encode_len_prefixed_str(&mut out, &self.runtime_version)?;
193 encode_len_prefixed_str(&mut out, &self.crypto_backend_id)?;
194 out.extend_from_slice(&hex32(&self.policy_hash)?);
195 out.extend_from_slice(&hex32(&self.bytecode_hash)?);
196 out.extend_from_slice(&hex32(&self.input_hash)?);
197 out.extend_from_slice(&hex32(&self.state_hash)?);
198 out.push(decision_to_code(self.decision) as u8);
199 Ok(out)
200 }
201}
202
203impl PartialEq for ProofBinding {
204 fn eq(&self, other: &Self) -> bool {
205 self.serialization_version == other.serialization_version
206 && self.schema_id == other.schema_id
207 && self.runtime_version == other.runtime_version
208 && self.crypto_backend_id == other.crypto_backend_id
209 && self.policy_hash == other.policy_hash
210 && self.bytecode_hash == other.bytecode_hash
211 && self.input_hash == other.input_hash
212 && self.state_hash == other.state_hash
213 && self.decision == other.decision
214 }
215}
216
217impl Eq for ProofBinding {}
218
219impl ProofEnvelope {
220 pub fn sign_ed25519(
221 binding: ProofBinding,
222 signer_key_id: impl Into<String>,
223 key_pair: &crypto_core::signature::Ed25519KeyPair,
224 ) -> Result<Self, String> {
225 let payload = binding.canonical_bytes()?;
226 let signature = key_pair.sign(&payload);
227 Ok(Self {
228 serialization_version: PROOF_ENVELOPE_SERIALIZATION_VERSION,
229 schema_id: PROOF_ENVELOPE_SCHEMA_ID.to_string(),
230 signature_algorithm: "ED25519".to_string(),
231 signer_key_id: signer_key_id.into(),
232 binding,
233 signature,
234 })
235 }
236
237 pub fn verify_ed25519(&self, public_key: &[u8]) -> Result<bool, String> {
238 if self.serialization_version != PROOF_ENVELOPE_SERIALIZATION_VERSION
239 || self.schema_id != PROOF_ENVELOPE_SCHEMA_ID
240 || self.signature_algorithm != "ED25519"
241 {
242 return Ok(false);
243 }
244 let payload = self.binding.canonical_bytes()?;
245 crypto_core::signature::verify(
246 &payload,
247 &self.signature,
248 public_key,
249 crypto_core::SignatureAlgorithm::Ed25519,
250 )
251 .map_err(|e| e.to_string())
252 }
253}
254
255#[cfg(feature = "pq-proof")]
256impl PqProofEnvelope {
257 pub fn sign_hybrid(
258 binding: ProofBinding,
259 signer_key_id: impl Into<String>,
260 signer: &pqcrypto::hybrid::HybridSigner,
261 keypair: &pqcrypto::hybrid::HybridKeyPair,
262 ) -> Result<Self, String> {
263 let payload = binding.canonical_bytes()?;
264 let signature = signer.sign(keypair, &payload).map_err(|e| e.to_string())?;
265
266 Ok(Self {
267 serialization_version: PQ_PROOF_ENVELOPE_SERIALIZATION_VERSION,
268 schema_id: PQ_PROOF_ENVELOPE_SCHEMA_ID.to_string(),
269 signature_algorithm: "HYBRID-ED25519+ML-DSA".to_string(),
270 signer_key_id: signer_key_id.into(),
271 pq_backend_id: signer.backend_id().to_string(),
272 level: keypair.level,
273 binding,
274 signature,
275 })
276 }
277
278 pub fn verify_hybrid(
279 &self,
280 public_key: &pqcrypto::hybrid::HybridPublicKey,
281 ) -> Result<bool, String> {
282 if self.serialization_version != PQ_PROOF_ENVELOPE_SERIALIZATION_VERSION
283 || self.schema_id != PQ_PROOF_ENVELOPE_SCHEMA_ID
284 || self.signature_algorithm != "HYBRID-ED25519+ML-DSA"
285 || self.level != public_key.level
286 || self.signature.quantum.level != self.level
287 {
288 return Ok(false);
289 }
290
291 let payload = self.binding.canonical_bytes()?;
292 let verifier = pqcrypto::hybrid::HybridVerifier::new(self.level);
293 verifier
294 .verify_public(public_key, &payload, &self.signature)
295 .map_err(|e| e.to_string())
296 }
297}
298
299impl ProofEnvelopeV1 {
300 pub fn sign_ed25519(
301 binding: &ProofBinding,
302 signer_key_id: impl AsRef<str>,
303 key_pair: &crypto_core::signature::Ed25519KeyPair,
304 ) -> Result<Self, String> {
305 let mut envelope = Self::unsigned_from_binding(
306 binding,
307 SignatureV1::Ed25519(Ed25519SignatureV1 {
308 key_id_hash: sha256_fixed(signer_key_id.as_ref().as_bytes()),
309 signature: Vec::new(),
310 }),
311 )?;
312 let payload = envelope.signing_bytes()?;
313 match &mut envelope.signature {
314 SignatureV1::Ed25519(sig) => sig.signature = key_pair.sign(&payload),
315 #[cfg(feature = "pq-proof")]
316 SignatureV1::Hybrid(_) => {
317 return Err("invalid signature variant for ed25519 signing".to_string())
318 }
319 }
320 Ok(envelope)
321 }
322
323 #[cfg(feature = "pq-proof")]
324 pub fn sign_hybrid(
325 binding: &ProofBinding,
326 signer_key_id: impl AsRef<str>,
327 signer: &pqcrypto::hybrid::HybridSigner,
328 keypair: &pqcrypto::hybrid::HybridKeyPair,
329 ) -> Result<Self, String> {
330 let mut envelope = Self::unsigned_from_binding(
331 binding,
332 SignatureV1::Hybrid(HybridSignatureV1 {
333 key_id_hash: sha256_fixed(signer_key_id.as_ref().as_bytes()),
334 backend_id_hash: sha256_fixed(signer.backend_id().as_bytes()),
335 level_code: dilithium_level_code(keypair.level),
336 signature: pqcrypto::hybrid::HybridSignature::new(
337 Vec::new(),
338 pqcrypto::signature::DilithiumSignature {
339 level: keypair.level,
340 signature: Vec::new(),
341 },
342 ),
343 }),
344 )?;
345 let payload = envelope.signing_bytes()?;
346 let sig = signer.sign(keypair, &payload).map_err(|e| e.to_string())?;
347 if let SignatureV1::Hybrid(h) = &mut envelope.signature {
348 h.signature = sig;
349 }
350 Ok(envelope)
351 }
352
353 pub fn verify_ed25519(&self, public_key: &[u8]) -> Result<bool, String> {
354 let sig = match &self.signature {
355 SignatureV1::Ed25519(sig) => sig,
356 #[cfg(feature = "pq-proof")]
357 SignatureV1::Hybrid(_) => return Ok(false),
358 };
359 let payload = self.signing_bytes()?;
360 crypto_core::signature::verify(
361 &payload,
362 &sig.signature,
363 public_key,
364 crypto_core::SignatureAlgorithm::Ed25519,
365 )
366 .map_err(|e| e.to_string())
367 }
368
369 #[cfg(feature = "pq-proof")]
370 pub fn verify_hybrid(
371 &self,
372 public_key: &pqcrypto::hybrid::HybridPublicKey,
373 ) -> Result<bool, String> {
374 let SignatureV1::Hybrid(sig) = &self.signature else {
375 return Ok(false);
376 };
377 if sig.level_code != dilithium_level_code(public_key.level) {
378 return Ok(false);
379 }
380 if sig.backend_id_hash == [0u8; 32] {
381 return Ok(false);
382 }
383 let payload = self.signing_bytes()?;
384 let verifier = pqcrypto::hybrid::HybridVerifier::new(public_key.level);
385 verifier
386 .verify_public(public_key, &payload, &sig.signature)
387 .map_err(|e| e.to_string())
388 }
389
390 pub fn signing_bytes(&self) -> Result<Vec<u8>, String> {
392 let mut out = Vec::with_capacity(1 + 1 + 4 + (32 * 4) + 1);
393 out.push(self.version);
394 out.push(self.encoding_version);
395 out.extend_from_slice(&self.runtime_version.to_be_bytes());
396 out.extend_from_slice(&self.policy_hash);
397 out.extend_from_slice(&self.bytecode_hash);
398 out.extend_from_slice(&self.input_hash);
399 out.extend_from_slice(&self.state_hash);
400 out.push(self.decision_code);
401 let sig_meta = self.signature.meta_bytes()?;
402 let sig_meta_len: u16 = sig_meta
403 .len()
404 .try_into()
405 .map_err(|_| "signature metadata too large".to_string())?;
406 out.extend_from_slice(&sig_meta_len.to_be_bytes());
407 out.extend_from_slice(&sig_meta);
408 Ok(out)
409 }
410
411 pub fn canonical_bytes(&self) -> Result<Vec<u8>, String> {
413 let mut out = self.signing_bytes()?;
414 let sig_bytes = self.signature.signature_owned_bytes();
415 let sig_len: u32 = sig_bytes
416 .len()
417 .try_into()
418 .map_err(|_| "signature too large".to_string())?;
419 out.extend_from_slice(&sig_len.to_be_bytes());
420 out.extend_from_slice(&sig_bytes);
421 Ok(out)
422 }
423
424 pub fn from_canonical_bytes(bytes: &[u8]) -> Result<Self, String> {
426 let mut cursor = 0usize;
427 let version = read_u8(bytes, &mut cursor)?;
428 let encoding_version = read_u8(bytes, &mut cursor)?;
429 let runtime_version = read_u32_be(bytes, &mut cursor)?;
430 let policy_hash = read_fixed_32(bytes, &mut cursor)?;
431 let bytecode_hash = read_fixed_32(bytes, &mut cursor)?;
432 let input_hash = read_fixed_32(bytes, &mut cursor)?;
433 let state_hash = read_fixed_32(bytes, &mut cursor)?;
434 let decision_code = read_u8(bytes, &mut cursor)?;
435
436 let sig_meta_len = read_u16_be(bytes, &mut cursor)? as usize;
437 let sig_meta = read_slice(bytes, &mut cursor, sig_meta_len)?;
438 let mut signature = SignatureV1::from_meta_bytes(sig_meta)?;
439
440 let sig_len = read_u32_be(bytes, &mut cursor)? as usize;
441 let sig_bytes = read_slice(bytes, &mut cursor, sig_len)?;
442 signature.attach_signature_bytes(sig_bytes)?;
443
444 if cursor != bytes.len() {
445 return Err("unexpected trailing bytes in ProofEnvelopeV1".to_string());
446 }
447
448 Ok(Self {
449 version,
450 encoding_version,
451 runtime_version,
452 policy_hash,
453 bytecode_hash,
454 input_hash,
455 state_hash,
456 decision_code,
457 signature,
458 })
459 }
460
461 pub fn decision(&self) -> Result<Decision, String> {
462 decision_from_code(self.decision_code)
463 }
464
465 fn unsigned_from_binding(
466 binding: &ProofBinding,
467 signature: SignatureV1,
468 ) -> Result<Self, String> {
469 Ok(Self {
470 version: PROOF_ENVELOPE_V1_VERSION,
471 encoding_version: PROOF_ENVELOPE_V1_ENCODING_VERSION,
472 runtime_version: pack_runtime_version_u32(&binding.runtime_version)?,
473 policy_hash: hex32(&binding.policy_hash)?,
474 bytecode_hash: hex32(&binding.bytecode_hash)?,
475 input_hash: hex32(&binding.input_hash)?,
476 state_hash: hex32(&binding.state_hash)?,
477 decision_code: decision_to_code(binding.decision) as u8,
478 signature,
479 })
480 }
481}
482
483impl SignatureV1 {
484 fn meta_bytes(&self) -> Result<Vec<u8>, String> {
485 match self {
486 SignatureV1::Ed25519(sig) => {
487 let mut out = Vec::with_capacity(1 + 32);
488 out.push(SignatureAlgorithmCodeV1::Ed25519 as u8);
489 out.extend_from_slice(&sig.key_id_hash);
490 Ok(out)
491 }
492 #[cfg(feature = "pq-proof")]
493 SignatureV1::Hybrid(sig) => {
494 let mut out = Vec::with_capacity(1 + 32 + 32 + 1);
495 out.push(SignatureAlgorithmCodeV1::HybridEd25519Mldsa as u8);
496 out.extend_from_slice(&sig.key_id_hash);
497 out.extend_from_slice(&sig.backend_id_hash);
498 out.push(sig.level_code);
499 Ok(out)
500 }
501 }
502 }
503
504 fn signature_owned_bytes(&self) -> Vec<u8> {
505 match self {
506 SignatureV1::Ed25519(sig) => sig.signature.clone(),
507 #[cfg(feature = "pq-proof")]
508 SignatureV1::Hybrid(sig) => sig.signature.to_bytes(),
509 }
510 }
511
512 fn from_meta_bytes(meta: &[u8]) -> Result<Self, String> {
513 if meta.is_empty() {
514 return Err("missing signature metadata".to_string());
515 }
516 match meta[0] {
517 x if x == SignatureAlgorithmCodeV1::Ed25519 as u8 => {
518 if meta.len() != 1 + 32 {
519 return Err("invalid Ed25519 signature metadata length".to_string());
520 }
521 let mut key_id_hash = [0u8; 32];
522 key_id_hash.copy_from_slice(&meta[1..33]);
523 Ok(SignatureV1::Ed25519(Ed25519SignatureV1 {
524 key_id_hash,
525 signature: Vec::new(),
526 }))
527 }
528 #[cfg(feature = "pq-proof")]
529 x if x == SignatureAlgorithmCodeV1::HybridEd25519Mldsa as u8 => {
530 if meta.len() != 1 + 32 + 32 + 1 {
531 return Err("invalid Hybrid signature metadata length".to_string());
532 }
533 let mut key_id_hash = [0u8; 32];
534 key_id_hash.copy_from_slice(&meta[1..33]);
535 let mut backend_id_hash = [0u8; 32];
536 backend_id_hash.copy_from_slice(&meta[33..65]);
537 let level_code = meta[65];
538 let level = dilithium_level_from_code(level_code)?;
539 Ok(SignatureV1::Hybrid(HybridSignatureV1 {
540 key_id_hash,
541 backend_id_hash,
542 level_code,
543 signature: pqcrypto::hybrid::HybridSignature::new(
544 Vec::new(),
545 pqcrypto::signature::DilithiumSignature {
546 level,
547 signature: Vec::new(),
548 },
549 ),
550 }))
551 }
552 _ => Err(format!("unknown signature algorithm code {}", meta[0])),
553 }
554 }
555
556 fn attach_signature_bytes(&mut self, signature_bytes: &[u8]) -> Result<(), String> {
557 match self {
558 SignatureV1::Ed25519(sig) => {
559 if signature_bytes.len() != 64 {
560 return Err("invalid Ed25519 signature length".to_string());
561 }
562 sig.signature = signature_bytes.to_vec();
563 Ok(())
564 }
565 #[cfg(feature = "pq-proof")]
566 SignatureV1::Hybrid(sig) => {
567 let level = dilithium_level_from_code(sig.level_code)?;
568 sig.signature =
569 pqcrypto::hybrid::HybridSignature::from_bytes(level, signature_bytes)
570 .map_err(|e| e.to_string())?;
571 Ok(())
572 }
573 }
574 }
575}
576
577#[derive(Serialize)]
578struct StateField<'a> {
579 key: &'a str,
580 value: &'a FieldValue,
581}
582
583fn state_snapshot(ctx: &EvaluationContext) -> Vec<StateField<'_>> {
584 let mut items: Vec<_> = ctx.fields().iter().collect();
585 items.sort_by(|(ka, _), (kb, _)| ka.cmp(kb));
586 items
587 .into_iter()
588 .map(|(key, value)| StateField {
589 key: key.as_str(),
590 value,
591 })
592 .collect()
593}
594
595fn canonical_bytecode_bytes(bytecode: &Bytecode) -> Result<Vec<u8>, String> {
596 let mut out = Vec::new();
597 out.extend_from_slice(b"rsrp.bytecode.v1");
598
599 encode_len_prefixed_bytes(&mut out, &bytecode.instructions)?;
600
601 encode_u32(
602 &mut out,
603 bytecode
604 .constants
605 .len()
606 .try_into()
607 .map_err(|_| "too many constants".to_string())?,
608 );
609 for c in &bytecode.constants {
610 match c {
611 Constant::Number(n) => {
612 out.push(1);
613 out.extend_from_slice(&n.to_be_bytes());
614 }
615 Constant::String(s) => {
616 out.push(2);
617 encode_len_prefixed_bytes(&mut out, s.as_bytes())?;
618 }
619 Constant::Boolean(b) => {
620 out.push(3);
621 out.push(u8::from(*b));
622 }
623 }
624 }
625
626 encode_u32(
627 &mut out,
628 bytecode
629 .fields
630 .len()
631 .try_into()
632 .map_err(|_| "too many fields".to_string())?,
633 );
634 for field in &bytecode.fields {
635 encode_len_prefixed_bytes(&mut out, field.as_bytes())?;
636 }
637
638 encode_u32(
639 &mut out,
640 bytecode
641 .action_instructions
642 .len()
643 .try_into()
644 .map_err(|_| "too many action instructions".to_string())?,
645 );
646 for instruction in &bytecode.action_instructions {
647 encode_action_instruction(&mut out, instruction)?;
648 }
649
650 Ok(out)
651}
652
653fn canonical_request_bytes(request: &EvaluationRequest) -> Result<Vec<u8>, String> {
654 let mut out = Vec::new();
655 out.extend_from_slice(b"rsrp.request.v1");
656 encode_len_prefixed_bytes(&mut out, request.request_id.as_bytes())?;
657 encode_len_prefixed_bytes(&mut out, request.agent_id.as_bytes())?;
658 encode_len_prefixed_bytes(&mut out, request.agent_org.as_bytes())?;
659 encode_len_prefixed_bytes(&mut out, request.agent_level.as_bytes())?;
660 encode_opt_str(&mut out, request.mission_id.as_deref())?;
661 encode_opt_str(&mut out, request.mission_type.as_deref())?;
662 encode_opt_str(&mut out, request.query_type.as_deref())?;
663 encode_opt_str(&mut out, request.justification.as_deref())?;
664 encode_opt_str(&mut out, request.export_format.as_deref())?;
665 encode_opt_u32(&mut out, request.result_limit);
666 encode_u32(&mut out, request.requests_last_hour);
667 encode_u32(&mut out, request.requests_last_24h);
668 encode_u32(&mut out, request.results_last_query);
669 encode_opt_str(&mut out, request.account_department.as_deref())?;
670 encode_u32(
671 &mut out,
672 request
673 .allowed_departments
674 .len()
675 .try_into()
676 .map_err(|_| "too many departments".to_string())?,
677 );
678 for dept in &request.allowed_departments {
679 encode_u32(&mut out, *dept);
680 }
681 encode_u32(&mut out, request.request_hour);
682 out.push(u8::from(request.is_within_mission_hours));
683 Ok(out)
684}
685
686fn canonical_state_bytes(fields: &[StateField<'_>]) -> Result<Vec<u8>, String> {
687 let mut out = Vec::new();
688 out.extend_from_slice(b"rsrp.state.v1");
689 encode_u32(
690 &mut out,
691 fields
692 .len()
693 .try_into()
694 .map_err(|_| "too many state fields".to_string())?,
695 );
696 for field in fields {
697 encode_len_prefixed_bytes(&mut out, field.key.as_bytes())?;
698 encode_field_value(&mut out, field.value)?;
699 }
700 Ok(out)
701}
702
703fn encode_action_instruction(
704 out: &mut Vec<u8>,
705 instruction: &ActionInstruction,
706) -> Result<(), String> {
707 match instruction {
708 ActionInstruction::SetDecision(d) => {
709 out.push(1);
710 let code = match d {
711 ActionDecision::Allow => 1,
712 ActionDecision::Block => 2,
713 ActionDecision::Warn => 3,
714 ActionDecision::ApprovalRequired => 4,
715 };
716 out.push(code);
717 }
718 ActionInstruction::SetErrorCode(v) => {
719 out.push(2);
720 encode_len_prefixed_bytes(out, v.as_bytes())?;
721 }
722 ActionInstruction::SetMessage(v) => {
723 out.push(3);
724 encode_len_prefixed_bytes(out, v.as_bytes())?;
725 }
726 ActionInstruction::SetApprovalTimeout(v) => {
727 out.push(4);
728 encode_u32(out, *v);
729 }
730 ActionInstruction::SetAlertSoc(v) => {
731 out.push(5);
732 out.push(u8::from(*v));
733 }
734 ActionInstruction::Halt => out.push(6),
735 }
736 Ok(())
737}
738
739fn encode_field_value(out: &mut Vec<u8>, value: &FieldValue) -> Result<(), String> {
740 match value {
741 FieldValue::Number(n) => {
742 out.push(1);
743 out.extend_from_slice(&n.to_be_bytes());
744 }
745 FieldValue::Float(f) => {
746 out.push(2);
747 out.extend_from_slice(&f.to_bits().to_be_bytes());
748 }
749 FieldValue::String(s) => {
750 out.push(3);
751 encode_len_prefixed_bytes(out, s.as_bytes())?;
752 }
753 FieldValue::Boolean(b) => {
754 out.push(4);
755 out.push(u8::from(*b));
756 }
757 }
758 Ok(())
759}
760
761fn encode_u32(out: &mut Vec<u8>, value: u32) {
762 out.extend_from_slice(&value.to_be_bytes());
763}
764
765fn encode_opt_u32(out: &mut Vec<u8>, value: Option<u32>) {
766 match value {
767 Some(v) => {
768 out.push(1);
769 encode_u32(out, v);
770 }
771 None => out.push(0),
772 }
773}
774
775fn encode_opt_str(out: &mut Vec<u8>, value: Option<&str>) -> Result<(), String> {
776 match value {
777 Some(v) => {
778 out.push(1);
779 encode_len_prefixed_bytes(out, v.as_bytes())?;
780 }
781 None => out.push(0),
782 }
783 Ok(())
784}
785
786fn encode_len_prefixed_bytes(out: &mut Vec<u8>, value: &[u8]) -> Result<(), String> {
787 let len: u32 = value
788 .len()
789 .try_into()
790 .map_err(|_| "field too large".to_string())?;
791 encode_u32(out, len);
792 out.extend_from_slice(value);
793 Ok(())
794}
795
796fn encode_len_prefixed_str(out: &mut Vec<u8>, value: &str) -> Result<(), String> {
797 let len: u16 = value
798 .len()
799 .try_into()
800 .map_err(|_| "string field too long".to_string())?;
801 out.extend_from_slice(&len.to_be_bytes());
802 out.extend_from_slice(value.as_bytes());
803 Ok(())
804}
805
806fn sha256_hex(data: &[u8]) -> String {
807 use crypto_core::hash::{hex_encode, sha256};
808 hex_encode(&sha256(data))
809}
810
811fn sha256_fixed(data: &[u8]) -> [u8; 32] {
812 let digest = crypto_core::hash::sha256(data);
813 let mut out = [0u8; 32];
814 out.copy_from_slice(&digest[..32]);
815 out
816}
817
818fn hex32(hex: &str) -> Result<[u8; 32], String> {
819 let decoded = crypto_core::hash::hex_decode(hex).map_err(|e| e.to_string())?;
820 if decoded.len() != 32 {
821 return Err(format!("expected 32-byte hash, got {}", decoded.len()));
822 }
823 let mut out = [0u8; 32];
824 out.copy_from_slice(&decoded);
825 Ok(out)
826}
827
828fn pack_runtime_version_u32(runtime_version: &str) -> Result<u32, String> {
829 let mut parts = runtime_version.split('.');
831 let major: u32 = parts
832 .next()
833 .ok_or_else(|| "missing major runtime version".to_string())?
834 .parse()
835 .map_err(|_| "invalid major runtime version".to_string())?;
836 let minor: u32 = parts
837 .next()
838 .ok_or_else(|| "missing minor runtime version".to_string())?
839 .parse()
840 .map_err(|_| "invalid minor runtime version".to_string())?;
841 let patch: u32 = match parts.next() {
842 Some(p) if !p.is_empty() => p
843 .parse()
844 .map_err(|_| "invalid patch runtime version".to_string())?,
845 _ => 0,
846 };
847 if major > 0xFF || minor > 0xFF || patch > 0xFFFF {
848 return Err("runtime_version component exceeds u32 packing limits".to_string());
849 }
850 Ok((major << 24) | (minor << 16) | patch)
851}
852
853fn decision_to_code(decision: Decision) -> DecisionCodeV1 {
854 match decision {
855 Decision::Allow => DecisionCodeV1::Allow,
856 Decision::Block => DecisionCodeV1::Block,
857 Decision::Warn => DecisionCodeV1::Warn,
858 Decision::ApprovalRequired => DecisionCodeV1::ApprovalRequired,
859 }
860}
861
862fn decision_from_code(code: u8) -> Result<Decision, String> {
863 match code {
864 x if x == DecisionCodeV1::Allow as u8 => Ok(Decision::Allow),
865 x if x == DecisionCodeV1::Block as u8 => Ok(Decision::Block),
866 x if x == DecisionCodeV1::Warn as u8 => Ok(Decision::Warn),
867 x if x == DecisionCodeV1::ApprovalRequired as u8 => Ok(Decision::ApprovalRequired),
868 _ => Err(format!("invalid decision code {}", code)),
869 }
870}
871
872#[cfg(feature = "pq-proof")]
873fn dilithium_level_code(level: pqcrypto::DilithiumLevel) -> u8 {
874 match level {
875 pqcrypto::DilithiumLevel::Dilithium2 => 2,
876 pqcrypto::DilithiumLevel::Dilithium3 => 3,
877 pqcrypto::DilithiumLevel::Dilithium5 => 5,
878 }
879}
880
881#[cfg(feature = "pq-proof")]
882fn dilithium_level_from_code(code: u8) -> Result<pqcrypto::DilithiumLevel, String> {
883 match code {
884 2 => Ok(pqcrypto::DilithiumLevel::Dilithium2),
885 3 => Ok(pqcrypto::DilithiumLevel::Dilithium3),
886 5 => Ok(pqcrypto::DilithiumLevel::Dilithium5),
887 _ => Err(format!("invalid Dilithium level code {}", code)),
888 }
889}
890
891fn read_u8(data: &[u8], cursor: &mut usize) -> Result<u8, String> {
892 if *cursor >= data.len() {
893 return Err("unexpected EOF reading u8".to_string());
894 }
895 let v = data[*cursor];
896 *cursor += 1;
897 Ok(v)
898}
899
900fn read_u16_be(data: &[u8], cursor: &mut usize) -> Result<u16, String> {
901 let slice = read_slice(data, cursor, 2)?;
902 Ok(u16::from_be_bytes([slice[0], slice[1]]))
903}
904
905fn read_u32_be(data: &[u8], cursor: &mut usize) -> Result<u32, String> {
906 let slice = read_slice(data, cursor, 4)?;
907 Ok(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
908}
909
910fn read_fixed_32(data: &[u8], cursor: &mut usize) -> Result<[u8; 32], String> {
911 let slice = read_slice(data, cursor, 32)?;
912 let mut out = [0u8; 32];
913 out.copy_from_slice(slice);
914 Ok(out)
915}
916
917fn read_slice<'a>(data: &'a [u8], cursor: &mut usize, len: usize) -> Result<&'a [u8], String> {
918 if data.len().saturating_sub(*cursor) < len {
919 return Err("unexpected EOF reading bytes".to_string());
920 }
921 let start = *cursor;
922 *cursor += len;
923 Ok(&data[start..start + len])
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929 use crate::context::EvaluationContext;
930
931 fn fixed_hash_hex(byte: u8) -> String {
932 crypto_core::hash::hex_encode(&[byte; 32])
933 }
934
935 #[test]
936 fn test_proof_binding_recompute_detects_bytecode_change() {
937 let src = r#"
938RULE CRUE_001 VERSION 1.0
939WHEN
940 agent.requests_last_hour >= 50
941THEN
942 BLOCK WITH CODE "VOLUME_EXCEEDED"
943"#;
944 let ast = crue_dsl::parser::parse(src).unwrap();
945 let mut bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
946
947 let req = crate::EvaluationRequest {
948 request_id: "r".into(),
949 agent_id: "a".into(),
950 agent_org: "o".into(),
951 agent_level: "l".into(),
952 mission_id: None,
953 mission_type: None,
954 query_type: None,
955 justification: None,
956 export_format: None,
957 result_limit: None,
958 requests_last_hour: 60,
959 requests_last_24h: 10,
960 results_last_query: 1,
961 account_department: None,
962 allowed_departments: vec![],
963 request_hour: 9,
964 is_within_mission_hours: true,
965 };
966 let ctx = EvaluationContext::from_request(&req);
967 let proof =
968 ProofBinding::create(&bytecode, &req, &ctx, Decision::Block, "mock-crypto").unwrap();
969 assert!(proof
970 .verify_recompute(&bytecode, &req, &ctx, Decision::Block, "mock-crypto")
971 .unwrap());
972
973 bytecode.instructions.push(0x00);
974 assert!(!proof
975 .verify_recompute(&bytecode, &req, &ctx, Decision::Block, "mock-crypto")
976 .unwrap());
977 }
978
979 #[test]
980 fn test_proof_envelope_ed25519_sign_verify() {
981 let src = r#"
982RULE CRUE_002 VERSION 1.0
983WHEN
984 agent.requests_last_hour >= 10
985THEN
986 BLOCK WITH CODE "TEST"
987"#;
988 let ast = crue_dsl::parser::parse(src).unwrap();
989 let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
990 let req = crate::EvaluationRequest {
991 request_id: "r".into(),
992 agent_id: "a".into(),
993 agent_org: "o".into(),
994 agent_level: "l".into(),
995 mission_id: None,
996 mission_type: None,
997 query_type: None,
998 justification: None,
999 export_format: None,
1000 result_limit: None,
1001 requests_last_hour: 12,
1002 requests_last_24h: 10,
1003 results_last_query: 1,
1004 account_department: None,
1005 allowed_departments: vec![],
1006 request_hour: 9,
1007 is_within_mission_hours: true,
1008 };
1009 let ctx = EvaluationContext::from_request(&req);
1010 let binding =
1011 ProofBinding::create(&bytecode, &req, &ctx, Decision::Block, "mock-crypto").unwrap();
1012 let kp = crypto_core::signature::Ed25519KeyPair::generate().unwrap();
1013 let envelope = ProofEnvelope::sign_ed25519(binding, "proof-key-1", &kp).unwrap();
1014 let pk = kp.verifying_key();
1015 assert!(envelope.verify_ed25519(&pk).unwrap());
1016 }
1017
1018 #[test]
1019 fn test_proof_envelope_v1_ed25519_vector_fixture() {
1020 let binding = ProofBinding {
1021 serialization_version: 1,
1022 schema_id: "rsrp.proof.binding.v1".to_string(),
1023 runtime_version: "0.9.1".to_string(),
1024 crypto_backend_id: "mock-crypto".to_string(),
1025 policy_hash: fixed_hash_hex(0x11),
1026 bytecode_hash: fixed_hash_hex(0x22),
1027 input_hash: fixed_hash_hex(0x33),
1028 state_hash: fixed_hash_hex(0x44),
1029 decision: Decision::Block,
1030 };
1031 let kp = crypto_core::signature::Ed25519KeyPair::derive_from_secret(
1032 b"rsrp-proof-envelope-v1-ed25519-test-vector",
1033 Some("fixture-ed25519-key".into()),
1034 );
1035 let pk = kp.verifying_key();
1036 let env = ProofEnvelopeV1::sign_ed25519(&binding, "fixture-ed25519-key", &kp).unwrap();
1037 assert_eq!(env.decision().unwrap(), Decision::Block);
1038 assert!(env.verify_ed25519(&pk).unwrap());
1039
1040 let signing_hex = crypto_core::hash::hex_encode(&env.signing_bytes().unwrap());
1041 let canonical_hex = crypto_core::hash::hex_encode(&env.canonical_bytes().unwrap());
1042
1043 assert_eq!(
1044 signing_hex,
1045 "010100090001111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444402002101e7e331964026891ae93f6f0d4b20c19f95cf20d6c6ba87fd73e287b081a46201"
1046 );
1047 assert_eq!(
1048 canonical_hex,
1049 "010100090001111111111111111111111111111111111111111111111111111111111111111122222222222222222222222222222222222222222222222222222222222222223333333333333333333333333333333333333333333333333333333333333333444444444444444444444444444444444444444444444444444444444444444402002101e7e331964026891ae93f6f0d4b20c19f95cf20d6c6ba87fd73e287b081a46201000000406dfc53cce34237ad8fdd62a3fc35b1221d18d7503971bdf73ec1f37d0cacfe002cc3405dfa2c046b66a68760c29c55a2fb8c130cc3d926a54645c771989dc000"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_proof_envelope_v1_canonical_decode_roundtrip() {
1055 let binding = ProofBinding {
1056 serialization_version: 1,
1057 schema_id: "rsrp.proof.binding.v1".to_string(),
1058 runtime_version: "0.9.3".to_string(),
1059 crypto_backend_id: "mock-crypto".to_string(),
1060 policy_hash: fixed_hash_hex(0xAA),
1061 bytecode_hash: fixed_hash_hex(0xBB),
1062 input_hash: fixed_hash_hex(0xCC),
1063 state_hash: fixed_hash_hex(0xDD),
1064 decision: Decision::Allow,
1065 };
1066 let kp = crypto_core::signature::Ed25519KeyPair::derive_from_secret(
1067 b"rsrp-proof-envelope-v1-roundtrip",
1068 Some("fixture-ed25519-key".into()),
1069 );
1070 let pk = kp.verifying_key();
1071 let env = ProofEnvelopeV1::sign_ed25519(&binding, "fixture-ed25519-key", &kp).unwrap();
1072 let bytes = env.canonical_bytes().unwrap();
1073 let decoded = ProofEnvelopeV1::from_canonical_bytes(&bytes).unwrap();
1074
1075 assert_eq!(decoded.canonical_bytes().unwrap(), bytes);
1076 assert_eq!(decoded.decision().unwrap(), Decision::Allow);
1077 assert!(decoded.verify_ed25519(&pk).unwrap());
1078 }
1079
1080 #[test]
1081 fn test_pack_runtime_version_u32_includes_patch() {
1082 assert_ne!(
1083 pack_runtime_version_u32("0.9.4").unwrap(),
1084 pack_runtime_version_u32("0.9.99").unwrap()
1085 );
1086 assert_eq!(pack_runtime_version_u32("1.2.3").unwrap(), 0x01020003);
1087 }
1088
1089 #[cfg(feature = "pq-proof")]
1090 #[test]
1091 fn test_pq_proof_envelope_hybrid_sign_verify() {
1092 let src = r#"
1093RULE CRUE_003 VERSION 1.0
1094WHEN
1095 agent.requests_last_hour >= 10
1096THEN
1097 BLOCK WITH CODE "TEST"
1098"#;
1099 let ast = crue_dsl::parser::parse(src).unwrap();
1100 let bytecode = crue_dsl::compiler::Compiler::compile(&ast).unwrap();
1101 let req = crate::EvaluationRequest {
1102 request_id: "r".into(),
1103 agent_id: "a".into(),
1104 agent_org: "o".into(),
1105 agent_level: "l".into(),
1106 mission_id: None,
1107 mission_type: None,
1108 query_type: None,
1109 justification: None,
1110 export_format: None,
1111 result_limit: None,
1112 requests_last_hour: 12,
1113 requests_last_24h: 10,
1114 results_last_query: 1,
1115 account_department: None,
1116 allowed_departments: vec![],
1117 request_hour: 9,
1118 is_within_mission_hours: true,
1119 };
1120 let ctx = EvaluationContext::from_request(&req);
1121 let binding =
1122 ProofBinding::create(&bytecode, &req, &ctx, Decision::Block, "mock-crypto").unwrap();
1123
1124 let signer = pqcrypto::hybrid::HybridSigner::new(pqcrypto::DilithiumLevel::Dilithium2);
1125 let kp = signer.generate_keypair().unwrap();
1126 let pk = kp.public_key();
1127 let envelope =
1128 PqProofEnvelope::sign_hybrid(binding, "pq-proof-key-1", &signer, &kp).unwrap();
1129 assert_eq!(envelope.pq_backend_id, signer.backend_id());
1130 assert!(envelope.verify_hybrid(&pk).unwrap());
1131 }
1132}