1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
4#[serde(transparent)]
5pub struct BoundedAdmissionText<const MAX: usize>(String);
6
7impl<const MAX: usize> BoundedAdmissionText<MAX> {
8 pub fn try_new(
9 field: &'static str,
10 value: impl Into<String>,
11 ) -> Result<Self, AdmissionOperationError> {
12 let value = value.into();
13 if value.is_empty() {
14 return Err(AdmissionOperationError::Empty { field });
15 }
16 if value.len() > MAX {
17 return Err(AdmissionOperationError::TooLong {
18 field,
19 maximum: MAX,
20 });
21 }
22 if value.chars().any(char::is_control) {
23 return Err(AdmissionOperationError::ControlCharacter { field });
24 }
25 if value.trim() != value {
26 return Err(AdmissionOperationError::Padded { field });
27 }
28 Ok(Self(value))
29 }
30
31 #[must_use]
32 pub fn as_str(&self) -> &str {
33 &self.0
34 }
35}
36
37impl<'de, const MAX: usize> Deserialize<'de> for BoundedAdmissionText<MAX> {
38 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
39 where
40 D: Deserializer<'de>,
41 {
42 let value = String::deserialize(deserializer)?;
43 Self::try_new("persisted_admission_text", value).map_err(serde::de::Error::custom)
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
48#[serde(transparent)]
49pub struct AdmissionDigest(String);
50
51impl AdmissionDigest {
52 pub fn try_new(
53 field: &'static str,
54 value: impl Into<String>,
55 ) -> Result<Self, AdmissionOperationError> {
56 let value = value.into();
57 if !is_sha256_hex(&value) {
58 return Err(AdmissionOperationError::InvalidDigest { field });
59 }
60 Ok(Self(value))
61 }
62
63 #[must_use]
64 pub fn as_str(&self) -> &str {
65 &self.0
66 }
67
68 fn from_derived(value: String) -> Self {
69 debug_assert!(is_sha256_hex(&value));
70 Self(value)
71 }
72}
73
74impl<'de> Deserialize<'de> for AdmissionDigest {
75 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
76 where
77 D: Deserializer<'de>,
78 {
79 let value = String::deserialize(deserializer)?;
80 Self::try_new("persisted_admission_digest", value).map_err(serde::de::Error::custom)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
85#[serde(transparent)]
86pub struct AdmissionOperationId(AdmissionDigest);
87
88impl AdmissionOperationId {
89 pub fn from_persisted(value: impl Into<String>) -> Result<Self, AdmissionOperationError> {
90 AdmissionDigest::try_new("operation_id", value).map(Self)
91 }
92
93 #[must_use]
94 pub fn as_str(&self) -> &str {
95 self.0.as_str()
96 }
97
98 fn from_derived(value: String) -> Self {
99 Self(AdmissionDigest::from_derived(value))
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
104#[serde(transparent)]
105pub struct RequestNamespaceDigest(AdmissionDigest);
106
107impl RequestNamespaceDigest {
108 pub fn from_persisted(value: impl Into<String>) -> Result<Self, AdmissionOperationError> {
109 AdmissionDigest::try_new("request_namespace_digest", value).map(Self)
110 }
111
112 #[must_use]
113 pub fn as_str(&self) -> &str {
114 self.0.as_str()
115 }
116
117 fn from_derived(value: String) -> Self {
118 Self(AdmissionDigest::from_derived(value))
119 }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct AuthenticatedRequestNamespace {
124 coordinator_authority_id: AdmissionIdentifier,
125 authenticated_tenant_id: BoundedAdmissionText<MAX_ADMISSION_TENANT_BYTES>,
126 digest: RequestNamespaceDigest,
127}
128
129impl AuthenticatedRequestNamespace {
130 pub fn for_local_system(
131 coordinator_authority_id: AdmissionIdentifier,
132 ) -> Result<Self, AdmissionOperationError> {
133 Self::bind(
134 coordinator_authority_id,
135 BoundedAdmissionText::try_new("authenticated_tenant_id", LOCAL_SYSTEM_TENANT_ID)?,
136 )
137 }
138
139 #[allow(dead_code)]
140 pub(crate) fn from_authentication_context(
141 coordinator_authority_id: AdmissionIdentifier,
142 authenticated_tenant_id: impl Into<String>,
143 ) -> Result<Self, AdmissionOperationError> {
144 let authenticated_tenant_id = authenticated_tenant_id.into();
145 if authenticated_tenant_id == LOCAL_SYSTEM_TENANT_ID {
146 return Err(AdmissionOperationError::ReservedLocalSystemTenant);
147 }
148 Self::bind(
149 coordinator_authority_id,
150 BoundedAdmissionText::try_new("authenticated_tenant_id", authenticated_tenant_id)?,
151 )
152 }
153
154 fn bind(
155 coordinator_authority_id: AdmissionIdentifier,
156 authenticated_tenant_id: BoundedAdmissionText<MAX_ADMISSION_TENANT_BYTES>,
157 ) -> Result<Self, AdmissionOperationError> {
158 #[derive(Serialize)]
159 struct NamespaceBody<'a> {
160 authenticated_tenant_id: &'a str,
161 coordinator_authority_id: &'a str,
162 }
163
164 let body = NamespaceBody {
165 authenticated_tenant_id: authenticated_tenant_id.as_str(),
166 coordinator_authority_id: coordinator_authority_id.as_str(),
167 };
168 let digest = domain_separated_digest(ADMISSION_REQUEST_NAMESPACE_DOMAIN, &body)?;
169 Ok(Self {
170 coordinator_authority_id,
171 authenticated_tenant_id,
172 digest: RequestNamespaceDigest::from_derived(digest),
173 })
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum AdmissionOperationKind {
180 ToolDispatch,
181 GovernedActiveResponse,
182 GovernedEconomicMutation,
183}
184
185impl AdmissionOperationKind {
186 pub const ALL: [Self; 3] = [
187 Self::ToolDispatch,
188 Self::GovernedActiveResponse,
189 Self::GovernedEconomicMutation,
190 ];
191
192 pub(super) fn uses_dispatch(self) -> bool {
193 matches!(self, Self::ToolDispatch | Self::GovernedActiveResponse)
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
198#[serde(rename_all = "snake_case")]
199pub enum SideEffectClass {
200 ReadOnly,
201 SideEffecting,
202 Monetary,
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct AdmissionParticipantRequirements {
208 pub broker_attempt: bool,
209 pub budget_capture: bool,
210 pub approval: bool,
211 pub execution_nonce: bool,
212 pub outcome_eligibility: bool,
213 pub payment: bool,
214 pub authorization_consumption: bool,
215 pub observation_attempt_zero: bool,
216 pub obligation: bool,
217 #[serde(default, skip_serializing_if = "is_false")]
218 pub channel: bool,
219 #[serde(default, skip_serializing_if = "is_false")]
220 pub credit_exposure: bool,
221 #[serde(default, skip_serializing_if = "is_false")]
222 pub supplemental_authorization: bool,
223}
224
225impl AdmissionParticipantRequirements {
226 pub const NONE: Self = Self {
227 broker_attempt: false,
228 budget_capture: false,
229 approval: false,
230 execution_nonce: false,
231 outcome_eligibility: false,
232 payment: false,
233 authorization_consumption: false,
234 observation_attempt_zero: false,
235 obligation: false,
236 channel: false,
237 credit_exposure: false,
238 supplemental_authorization: false,
239 };
240
241 fn validate(self) -> Result<(), AdmissionOperationError> {
242 if (self.broker_attempt || self.payment || self.channel) && !self.budget_capture
243 || self.channel && (self.payment || !self.obligation)
244 || self.credit_exposure
245 && (!self.budget_capture || !self.obligation || self.payment || self.channel)
246 {
247 return Err(AdmissionOperationError::InvalidParticipantRequirements);
248 }
249 Ok(())
250 }
251
252 pub(super) fn validate_for_kind(
253 self,
254 kind: AdmissionOperationKind,
255 ) -> Result<(), AdmissionOperationError> {
256 self.validate()?;
257 let kind_valid = match kind {
258 AdmissionOperationKind::ToolDispatch => {
259 self.budget_capture && self.broker_attempt && !self.supplemental_authorization
260 }
261 AdmissionOperationKind::GovernedActiveResponse => {
262 self == (Self {
263 approval: true,
264 ..Self::NONE
265 })
266 }
267 AdmissionOperationKind::GovernedEconomicMutation => {
268 self == Self::NONE
269 || self
270 == (Self {
271 supplemental_authorization: true,
272 ..Self::NONE
273 })
274 }
275 };
276 if kind_valid {
277 Ok(())
278 } else {
279 Err(AdmissionOperationError::InvalidParticipantRequirements)
280 }
281 }
282}
283
284const fn is_false(value: &bool) -> bool {
285 !*value
286}
287
288#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
289pub struct AdmissionRequestBindingV1 {
290 pub(super) immutable_request_hash: AdmissionDigest,
291 pub(super) action_parameter_hash: AdmissionDigest,
292 pub(super) participant_requirements: AdmissionParticipantRequirements,
293 pub(super) request_binding_hash: AdmissionDigest,
294}
295
296impl AdmissionRequestBindingV1 {
297 pub fn new(
298 immutable_request_hash: AdmissionDigest,
299 participant_requirements: AdmissionParticipantRequirements,
300 ) -> Result<Self, AdmissionOperationError> {
301 Self::new_with_action_parameter_hash(
302 immutable_request_hash.clone(),
303 immutable_request_hash,
304 participant_requirements,
305 )
306 }
307
308 pub fn new_with_action_parameter_hash(
309 immutable_request_hash: AdmissionDigest,
310 action_parameter_hash: AdmissionDigest,
311 participant_requirements: AdmissionParticipantRequirements,
312 ) -> Result<Self, AdmissionOperationError> {
313 participant_requirements.validate()?;
314 let request_binding_hash = derive_request_binding_hash(
315 &immutable_request_hash,
316 &action_parameter_hash,
317 participant_requirements,
318 )?;
319 Ok(Self {
320 immutable_request_hash,
321 action_parameter_hash,
322 participant_requirements,
323 request_binding_hash,
324 })
325 }
326
327 fn validate(&self) -> Result<(), AdmissionOperationError> {
328 self.participant_requirements.validate()?;
329 if derive_request_binding_hash(
330 &self.immutable_request_hash,
331 &self.action_parameter_hash,
332 self.participant_requirements,
333 )? != self.request_binding_hash
334 {
335 return Err(AdmissionOperationError::RequestBindingMismatch);
336 }
337 Ok(())
338 }
339}
340
341impl<'de> Deserialize<'de> for AdmissionRequestBindingV1 {
342 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
343 where
344 D: Deserializer<'de>,
345 {
346 #[derive(Deserialize)]
347 #[serde(deny_unknown_fields)]
348 struct Persisted {
349 immutable_request_hash: AdmissionDigest,
350 action_parameter_hash: AdmissionDigest,
351 participant_requirements: AdmissionParticipantRequirements,
352 request_binding_hash: AdmissionDigest,
353 }
354 let value = Persisted::deserialize(deserializer)?;
355 let binding = Self {
356 immutable_request_hash: value.immutable_request_hash,
357 action_parameter_hash: value.action_parameter_hash,
358 participant_requirements: value.participant_requirements,
359 request_binding_hash: value.request_binding_hash,
360 };
361 binding.validate().map_err(serde::de::Error::custom)?;
362 Ok(binding)
363 }
364}
365
366#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
367#[serde(rename_all = "snake_case")]
368pub enum DurableAdmissionMode {
369 Off,
370 Monetary,
371 #[default]
372 SideEffecting,
373 All,
374}
375
376#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
377pub enum AdmissionReceiptPersistence {
378 Durable,
379 Ephemeral,
380}
381
382impl DurableAdmissionMode {
383 #[must_use]
384 pub fn covers(self, effect_class: SideEffectClass) -> bool {
385 match self {
386 Self::Off => false,
387 Self::Monetary => effect_class == SideEffectClass::Monetary,
388 Self::SideEffecting => effect_class != SideEffectClass::ReadOnly,
389 Self::All => true,
390 }
391 }
392
393 pub fn validate_configuration(
394 self,
395 unsafe_development: bool,
396 receipts: AdmissionReceiptPersistence,
397 ) -> Result<Self, AdmissionOperationError> {
398 if self == Self::Off
399 && (!unsafe_development || receipts != AdmissionReceiptPersistence::Ephemeral)
400 {
401 return Err(AdmissionOperationError::UnsafeDurableAdmissionOff);
402 }
403 Ok(self)
404 }
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
408pub struct AdmissionOperationBindingInputV1 {
409 pub kind: AdmissionOperationKind,
410 pub namespace: AuthenticatedRequestNamespace,
411 pub request_id: AdmissionIdentifier,
412 pub capability_id: AdmissionIdentifier,
413 pub authorization_capability_hash: AdmissionDigest,
414 pub request_binding: AdmissionRequestBindingV1,
415 pub policy_hash: AdmissionDigest,
416 pub effect_class: SideEffectClass,
417}
418
419#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
420#[serde(deny_unknown_fields)]
421pub struct PersistedAdmissionOperationBindingV1 {
422 pub kind: AdmissionOperationKind,
423 pub operation_id: AdmissionOperationId,
424 pub coordinator_authority_id: AdmissionIdentifier,
425 pub authenticated_tenant_id: BoundedAdmissionText<MAX_ADMISSION_TENANT_BYTES>,
426 pub request_namespace_digest: RequestNamespaceDigest,
427 pub request_id: AdmissionIdentifier,
428 pub capability_id: AdmissionIdentifier,
429 pub authorization_capability_hash: AdmissionDigest,
430 pub request_binding: AdmissionRequestBindingV1,
431 pub policy_hash: AdmissionDigest,
432 pub effect_class: SideEffectClass,
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
436pub struct AdmissionOperationBindingV1 {
437 pub(super) kind: AdmissionOperationKind,
438 pub(super) operation_id: AdmissionOperationId,
439 pub(super) coordinator_authority_id: AdmissionIdentifier,
440 pub(super) authenticated_tenant_id: BoundedAdmissionText<MAX_ADMISSION_TENANT_BYTES>,
441 pub(super) request_namespace_digest: RequestNamespaceDigest,
442 pub(super) request_id: AdmissionIdentifier,
443 pub(super) capability_id: AdmissionIdentifier,
444 pub(super) authorization_capability_hash: AdmissionDigest,
445 pub(super) request_binding: AdmissionRequestBindingV1,
446 pub(super) policy_hash: AdmissionDigest,
447 pub(super) effect_class: SideEffectClass,
448}
449
450impl AdmissionOperationBindingV1 {
451 pub fn new(input: AdmissionOperationBindingInputV1) -> Result<Self, AdmissionOperationError> {
452 input
453 .request_binding
454 .participant_requirements
455 .validate_for_kind(input.kind)?;
456 let operation_id = derive_operation_id(OperationIdInput {
457 kind: input.kind,
458 coordinator_authority_id: &input.namespace.coordinator_authority_id,
459 request_namespace_digest: &input.namespace.digest,
460 request_id: &input.request_id,
461 capability_id: &input.capability_id,
462 authorization_capability_hash: &input.authorization_capability_hash,
463 request_binding_hash: &input.request_binding.request_binding_hash,
464 policy_hash: &input.policy_hash,
465 effect_class: input.effect_class,
466 })?;
467 Ok(Self {
468 kind: input.kind,
469 operation_id,
470 coordinator_authority_id: input.namespace.coordinator_authority_id,
471 authenticated_tenant_id: input.namespace.authenticated_tenant_id,
472 request_namespace_digest: input.namespace.digest,
473 request_id: input.request_id,
474 capability_id: input.capability_id,
475 authorization_capability_hash: input.authorization_capability_hash,
476 request_binding: input.request_binding,
477 policy_hash: input.policy_hash,
478 effect_class: input.effect_class,
479 })
480 }
481
482 pub fn from_persisted(
483 input: PersistedAdmissionOperationBindingV1,
484 ) -> Result<Self, AdmissionOperationError> {
485 let namespace = AuthenticatedRequestNamespace::bind(
486 input.coordinator_authority_id.clone(),
487 input.authenticated_tenant_id.clone(),
488 )?;
489 if namespace.digest != input.request_namespace_digest {
490 return Err(AdmissionOperationError::RequestNamespaceMismatch);
491 }
492 let binding = Self {
493 kind: input.kind,
494 operation_id: input.operation_id,
495 coordinator_authority_id: input.coordinator_authority_id,
496 authenticated_tenant_id: input.authenticated_tenant_id,
497 request_namespace_digest: input.request_namespace_digest,
498 request_id: input.request_id,
499 capability_id: input.capability_id,
500 authorization_capability_hash: input.authorization_capability_hash,
501 request_binding: input.request_binding,
502 policy_hash: input.policy_hash,
503 effect_class: input.effect_class,
504 };
505 binding.validate()?;
506 Ok(binding)
507 }
508
509 pub fn validate(&self) -> Result<(), AdmissionOperationError> {
510 self.request_binding.validate()?;
511 self.request_binding
512 .participant_requirements
513 .validate_for_kind(self.kind)?;
514 let namespace = AuthenticatedRequestNamespace::bind(
515 self.coordinator_authority_id.clone(),
516 self.authenticated_tenant_id.clone(),
517 )?;
518 if namespace.digest != self.request_namespace_digest {
519 return Err(AdmissionOperationError::RequestNamespaceMismatch);
520 }
521 let expected = derive_operation_id(OperationIdInput {
522 kind: self.kind,
523 coordinator_authority_id: &self.coordinator_authority_id,
524 request_namespace_digest: &self.request_namespace_digest,
525 request_id: &self.request_id,
526 capability_id: &self.capability_id,
527 authorization_capability_hash: &self.authorization_capability_hash,
528 request_binding_hash: &self.request_binding.request_binding_hash,
529 policy_hash: &self.policy_hash,
530 effect_class: self.effect_class,
531 })?;
532 if expected != self.operation_id {
533 return Err(AdmissionOperationError::OperationIdMismatch);
534 }
535 Ok(())
536 }
537
538 #[must_use]
539 pub fn kind(&self) -> AdmissionOperationKind {
540 self.kind
541 }
542
543 #[must_use]
544 pub fn operation_id(&self) -> &AdmissionOperationId {
545 &self.operation_id
546 }
547
548 #[must_use]
549 pub fn coordinator_authority_id(&self) -> &AdmissionIdentifier {
550 &self.coordinator_authority_id
551 }
552
553 #[must_use]
554 pub fn request_namespace_digest(&self) -> &RequestNamespaceDigest {
555 &self.request_namespace_digest
556 }
557
558 #[must_use]
559 pub fn request_id(&self) -> &AdmissionIdentifier {
560 &self.request_id
561 }
562
563 #[must_use]
564 pub fn capability_id(&self) -> &AdmissionIdentifier {
565 &self.capability_id
566 }
567
568 #[must_use]
569 pub fn request_binding_hash(&self) -> &AdmissionDigest {
570 &self.request_binding.request_binding_hash
571 }
572
573 #[must_use]
574 pub fn immutable_request_hash(&self) -> &AdmissionDigest {
575 &self.request_binding.immutable_request_hash
576 }
577
578 #[must_use]
579 pub fn action_parameter_hash(&self) -> &AdmissionDigest {
580 &self.request_binding.action_parameter_hash
581 }
582
583 #[must_use]
584 pub fn participant_requirements(&self) -> AdmissionParticipantRequirements {
585 self.request_binding.participant_requirements
586 }
587
588 #[must_use]
589 pub fn policy_hash(&self) -> &AdmissionDigest {
590 &self.policy_hash
591 }
592
593 #[must_use]
594 pub fn effect_class(&self) -> SideEffectClass {
595 self.effect_class
596 }
597
598 #[must_use]
599 pub fn replay_key(&self) -> AdmissionReplayKey {
600 AdmissionReplayKey {
601 request_namespace_digest: self.request_namespace_digest.clone(),
602 request_id: self.request_id.clone(),
603 }
604 }
605
606 #[must_use]
607 pub fn to_persisted(&self) -> PersistedAdmissionOperationBindingV1 {
608 self.into()
609 }
610}
611
612impl From<&AdmissionOperationBindingV1> for PersistedAdmissionOperationBindingV1 {
613 fn from(binding: &AdmissionOperationBindingV1) -> Self {
614 Self {
615 kind: binding.kind,
616 operation_id: binding.operation_id.clone(),
617 coordinator_authority_id: binding.coordinator_authority_id.clone(),
618 authenticated_tenant_id: binding.authenticated_tenant_id.clone(),
619 request_namespace_digest: binding.request_namespace_digest.clone(),
620 request_id: binding.request_id.clone(),
621 capability_id: binding.capability_id.clone(),
622 authorization_capability_hash: binding.authorization_capability_hash.clone(),
623 request_binding: binding.request_binding.clone(),
624 policy_hash: binding.policy_hash.clone(),
625 effect_class: binding.effect_class,
626 }
627 }
628}
629
630#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
631pub struct AdmissionReplayKey {
632 pub request_namespace_digest: RequestNamespaceDigest,
633 pub request_id: AdmissionIdentifier,
634}
635
636pub(super) struct OperationIdInput<'a> {
637 pub(super) kind: AdmissionOperationKind,
638 pub(super) coordinator_authority_id: &'a AdmissionIdentifier,
639 pub(super) request_namespace_digest: &'a RequestNamespaceDigest,
640 pub(super) request_id: &'a AdmissionIdentifier,
641 pub(super) capability_id: &'a AdmissionIdentifier,
642 pub(super) authorization_capability_hash: &'a AdmissionDigest,
643 pub(super) request_binding_hash: &'a AdmissionDigest,
644 pub(super) policy_hash: &'a AdmissionDigest,
645 pub(super) effect_class: SideEffectClass,
646}
647
648pub(super) fn derive_operation_id(
649 input: OperationIdInput<'_>,
650) -> Result<AdmissionOperationId, AdmissionOperationError> {
651 #[derive(Serialize)]
652 struct OperationIdBody<'a> {
653 kind: AdmissionOperationKind,
654 coordinator_authority_id: &'a str,
655 request_namespace_digest: &'a str,
656 request_id: &'a str,
657 capability_id: &'a str,
658 authorization_capability_hash: &'a str,
659 request_binding_hash: &'a str,
660 policy_hash: &'a str,
661 effect_class: SideEffectClass,
662 }
663
664 let body = OperationIdBody {
665 kind: input.kind,
666 coordinator_authority_id: input.coordinator_authority_id.as_str(),
667 request_namespace_digest: input.request_namespace_digest.as_str(),
668 request_id: input.request_id.as_str(),
669 capability_id: input.capability_id.as_str(),
670 authorization_capability_hash: input.authorization_capability_hash.as_str(),
671 request_binding_hash: input.request_binding_hash.as_str(),
672 policy_hash: input.policy_hash.as_str(),
673 effect_class: input.effect_class,
674 };
675 domain_separated_digest(ADMISSION_OPERATION_DOMAIN, &body)
676 .map(AdmissionOperationId::from_derived)
677}
678
679fn derive_request_binding_hash(
680 immutable_request_hash: &AdmissionDigest,
681 action_parameter_hash: &AdmissionDigest,
682 participant_requirements: AdmissionParticipantRequirements,
683) -> Result<AdmissionDigest, AdmissionOperationError> {
684 #[derive(Serialize)]
685 struct RequestBindingBody<'a> {
686 immutable_request_hash: &'a str,
687 action_parameter_hash: &'a str,
688 participant_requirements: AdmissionParticipantRequirements,
689 }
690 domain_separated_digest(
691 ADMISSION_REQUEST_BINDING_DOMAIN,
692 &RequestBindingBody {
693 immutable_request_hash: immutable_request_hash.as_str(),
694 action_parameter_hash: action_parameter_hash.as_str(),
695 participant_requirements,
696 },
697 )
698 .map(AdmissionDigest::from_derived)
699}
700
701fn domain_separated_digest<T: Serialize>(
702 domain: &[u8],
703 body: &T,
704) -> Result<String, AdmissionOperationError> {
705 let canonical = canonical_json_bytes(body)
706 .map_err(|error| AdmissionOperationError::CanonicalJson(error.to_string()))?;
707 let mut bytes = Vec::with_capacity(domain.len() + canonical.len());
708 bytes.extend_from_slice(domain);
709 bytes.extend_from_slice(&canonical);
710 Ok(sha256_hex(&bytes))
711}
712
713fn is_sha256_hex(value: &str) -> bool {
714 value.len() == 64
715 && value
716 .as_bytes()
717 .iter()
718 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(byte))
719}