1use chrono::{DateTime, Utc};
4use ed25519_dalek::SigningKey;
5use serde::{Deserialize, Serialize};
6use std::path::Path;
7
8use super::{
9 ARTIFACT_REF_VERSION, CAPABILITY_GRANT_REF_VERSION, DIGEST_HEX_LEN, DIGEST_PREFIX,
10 MAX_ARTIFACT_REFS, MAX_AUTHORITY_CLOCK_SKEW_SECONDS, MAX_CANCEL_REASON_BYTES,
11 MAX_CONTROL_NONCE_BYTES, MAX_DESCRIPTION_BYTES, MAX_DESCRIPTOR_STRING_BYTES,
12 MAX_IDEMPOTENCY_KEY_BYTES, MAX_POLICY_LIFETIME_SECONDS, MAX_TASK_CONTROL_LIFETIME_SECONDS,
13 MAX_TASK_DESCRIPTOR_BYTES, MAX_TASK_LIFETIME_SECONDS, MAX_TRUST_ENTRIES, TASK_ACTION_CANCEL,
14 TASK_ACTION_GET, TASK_ACTION_SEND, TASK_AUTHORITY_CONFIG_VERSION,
15 TASK_CONTROL_DESCRIPTOR_VERSION, TASK_CONTROL_SIGNING_DOMAIN, TASK_DESCRIPTOR_SIGNING_DOMAIN,
16 TASK_DESCRIPTOR_VERSION, delivery_authority, delivery_delegation, policy_file,
17};
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(deny_unknown_fields)]
21pub struct AgentArtifactRefV1 {
22 pub schema_version: u32,
23 pub artifact_id: String,
24 pub digest: String,
25 pub media_type: String,
26 pub size_bytes: u64,
27 pub uri: Option<String>,
28}
29
30impl AgentArtifactRefV1 {
31 fn validate(&self) -> Result<(), TaskAuthorityError> {
32 if self.schema_version != ARTIFACT_REF_VERSION
33 || self.size_bytes > MAX_TASK_DESCRIPTOR_BYTES as u64
34 {
35 return Err(TaskAuthorityError::MalformedBounds);
36 }
37 validate_identifier(&self.artifact_id, MAX_DESCRIPTOR_STRING_BYTES)?;
38 validate_identifier(&self.digest, MAX_DESCRIPTOR_STRING_BYTES)?;
39 validate_identifier(&self.media_type, MAX_DESCRIPTOR_STRING_BYTES)?;
40 if let Some(uri) = &self.uri {
41 validate_identifier(uri, MAX_DESCRIPTOR_STRING_BYTES)?;
42 }
43 Ok(())
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
48#[serde(deny_unknown_fields)]
49pub struct CapabilityGrantRefV1 {
50 pub schema_version: u32,
51 pub grant_id: String,
52}
53
54impl CapabilityGrantRefV1 {
55 pub(super) fn validate(&self) -> Result<(), TaskAuthorityError> {
56 if self.schema_version != CAPABILITY_GRANT_REF_VERSION {
57 return Err(TaskAuthorityError::MalformedBounds);
58 }
59 validate_identifier(&self.grant_id, MAX_DESCRIPTOR_STRING_BYTES)
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(deny_unknown_fields)]
65pub struct TaskScopeV1 {
66 pub schema_version: u32,
67 pub tenant_id: String,
68 pub project_id: String,
69}
70
71impl TaskScopeV1 {
72 fn validate(&self) -> Result<(), TaskAuthorityError> {
73 if self.schema_version != TASK_AUTHORITY_CONFIG_VERSION {
74 return Err(TaskAuthorityError::MalformedBounds);
75 }
76 validate_identifier(&self.tenant_id, MAX_DESCRIPTOR_STRING_BYTES)?;
77 validate_identifier(&self.project_id, MAX_DESCRIPTOR_STRING_BYTES)
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(deny_unknown_fields)]
83pub struct TaskPeerTrustV1 {
84 pub schema_version: u32,
85 pub key_id: String,
86 pub agent_id: String,
87 pub public_key: String,
88 pub allowed_actions: Vec<String>,
89 pub allowed_scopes: Vec<TaskScopeV1>,
90 pub not_before: DateTime<Utc>,
91 pub expires_at: DateTime<Utc>,
92 pub revoked: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
96#[serde(deny_unknown_fields)]
97pub struct TaskCapabilityGrantV1 {
98 pub schema_version: u32,
99 pub grant_id: String,
100 pub key_id: String,
101 pub action: String,
102 pub tenant_id: String,
103 pub project_id: String,
104 pub not_before: DateTime<Utc>,
105 pub expires_at: DateTime<Utc>,
106 pub revoked: bool,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110#[serde(deny_unknown_fields)]
111pub struct TaskAuthorityConfigV1 {
112 pub schema_version: u32,
113 pub peers: Vec<TaskPeerTrustV1>,
114 pub grants: Vec<TaskCapabilityGrantV1>,
115}
116
117impl Default for TaskAuthorityConfigV1 {
118 fn default() -> Self {
119 Self {
120 schema_version: TASK_AUTHORITY_CONFIG_VERSION,
121 peers: Vec::new(),
122 grants: Vec::new(),
123 }
124 }
125}
126
127impl TaskAuthorityConfigV1 {
128 pub fn from_file(path: &Path) -> Result<Self, String> {
130 Self::from_json(&policy_file::read_bounded(path)?)
131 }
132
133 pub fn from_json(raw: &str) -> Result<Self, String> {
137 let config: Self = serde_json::from_str(raw).map_err(|error| error.to_string())?;
138 config.validate().map_err(|error| error.to_string())?;
139 Ok(config)
140 }
141
142 pub fn validate(&self) -> Result<(), TaskAuthorityError> {
149 if self.schema_version != TASK_AUTHORITY_CONFIG_VERSION
150 || self.peers.len() > MAX_TRUST_ENTRIES
151 || self.grants.len() > MAX_TRUST_ENTRIES
152 {
153 return Err(TaskAuthorityError::MalformedBounds);
154 }
155 for (index, peer) in self.peers.iter().enumerate() {
156 validate_peer(peer)?;
157 if self.peers[..index].iter().any(|other| {
158 other.key_id == peer.key_id
159 || other.public_key.eq_ignore_ascii_case(&peer.public_key)
160 }) {
161 return Err(TaskAuthorityError::DuplicateTrustEntry);
162 }
163 }
164 for (index, grant) in self.grants.iter().enumerate() {
165 validate_grant(grant)?;
166 if self.grants[..index]
167 .iter()
168 .any(|other| other.grant_id == grant.grant_id)
169 {
170 return Err(TaskAuthorityError::DuplicateTrustEntry);
171 }
172 if !self.peers.iter().any(|peer| peer.key_id == grant.key_id) {
173 return Err(TaskAuthorityError::UnknownKey);
174 }
175 }
176 Ok(())
177 }
178
179 pub fn reject_shared_secret(&self, secret: &str) -> Result<(), TaskAuthorityError> {
184 let secret = secret.trim();
185 if secret.is_empty() {
186 return Ok(());
187 }
188 if self
189 .peers
190 .iter()
191 .any(|peer| peer.public_key.eq_ignore_ascii_case(secret) || peer.key_id == secret)
192 {
193 return Err(TaskAuthorityError::SharedSecretReuse);
194 }
195 Ok(())
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum TaskAuthorityError {
201 MalformedBounds,
202 UnsupportedAction,
203 UnknownKey,
204 RevokedKey,
205 InvalidKey,
206 InvalidSignature,
207 WrongSender,
208 WrongRecipient,
209 WrongScope,
210 KeyNotYetValid,
211 KeyExpired,
212 MissingGrant,
213 RevokedGrant,
214 GrantNotYetValid,
215 GrantExpired,
216 GrantMismatch,
217 DescriptorNotYetValid,
218 DescriptorExpired,
219 DuplicateTrustEntry,
220 SharedSecretReuse,
221}
222
223impl std::fmt::Display for TaskAuthorityError {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.write_str(match self {
226 Self::MalformedBounds => "malformed task authority bounds",
227 Self::UnsupportedAction => "unsupported task action",
228 Self::UnknownKey => "unknown task authority key",
229 Self::RevokedKey => "revoked task authority key",
230 Self::InvalidKey => "invalid task authority key",
231 Self::InvalidSignature => "invalid task descriptor signature",
232 Self::WrongSender => "task descriptor sender mismatch",
233 Self::WrongRecipient => "task descriptor recipient mismatch",
234 Self::WrongScope => "task descriptor scope mismatch",
235 Self::KeyNotYetValid => "task authority key is not yet valid",
236 Self::KeyExpired => "task authority key is expired",
237 Self::MissingGrant => "task capability grant is missing",
238 Self::RevokedGrant => "task capability grant is revoked",
239 Self::GrantNotYetValid => "task capability grant is not yet valid",
240 Self::GrantExpired => "task capability grant is expired",
241 Self::GrantMismatch => "task capability grant does not authorize descriptor",
242 Self::DescriptorNotYetValid => "task descriptor is not yet valid",
243 Self::DescriptorExpired => "task descriptor is expired",
244 Self::DuplicateTrustEntry => "duplicate task authority trust entry",
245 Self::SharedSecretReuse => {
246 "task authority key must not reuse a bearer or channel secret"
247 }
248 })
249 }
250}
251
252impl std::error::Error for TaskAuthorityError {}
253
254#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
255#[serde(deny_unknown_fields)]
256pub struct TaskDescriptorV1 {
257 pub schema_version: u32,
258 pub sender: String,
259 pub recipient: String,
260 pub tenant_id: String,
261 pub project_id: String,
262 pub action: String,
263 pub issued_at: DateTime<Utc>,
264 pub expires_at: DateTime<Utc>,
265 pub idempotency_key: String,
266 pub description: String,
267 pub artifact_refs: Vec<AgentArtifactRefV1>,
268 pub grant_ref: CapabilityGrantRefV1,
269 pub key_id: String,
270 pub signature: String,
271}
272
273impl TaskDescriptorV1 {
274 #[allow(clippy::too_many_arguments)]
275 pub fn new(
276 sender: &str,
277 recipient: &str,
278 tenant_id: &str,
279 project_id: &str,
280 action: &str,
281 issued_at: DateTime<Utc>,
282 expires_at: DateTime<Utc>,
283 idempotency_key: &str,
284 description: &str,
285 artifact_refs: Vec<AgentArtifactRefV1>,
286 grant_id: &str,
287 key_id: &str,
288 ) -> Self {
289 Self {
290 schema_version: TASK_DESCRIPTOR_VERSION,
291 sender: sender.to_string(),
292 recipient: recipient.to_string(),
293 tenant_id: tenant_id.to_string(),
294 project_id: project_id.to_string(),
295 action: action.to_string(),
296 issued_at,
297 expires_at,
298 idempotency_key: idempotency_key.to_string(),
299 description: description.to_string(),
300 artifact_refs,
301 grant_ref: CapabilityGrantRefV1 {
302 schema_version: CAPABILITY_GRANT_REF_VERSION,
303 grant_id: grant_id.to_string(),
304 },
305 key_id: key_id.to_string(),
306 signature: String::new(),
307 }
308 }
309
310 pub fn sign(&mut self, signing_key: &SigningKey) {
313 self.signature.clear();
314 let bytes = self.signing_bytes();
315 self.signature = crate::core::agent_identity::hex_encode(
316 &crate::core::agent_identity::sign_bytes_with(signing_key, &bytes),
317 );
318 }
319
320 #[must_use]
325 pub fn signing_bytes(&self) -> Vec<u8> {
326 let mut unsigned = self.clone();
327 unsigned.signature.clear();
328 let mut bytes = Vec::from(TASK_DESCRIPTOR_SIGNING_DOMAIN);
329 bytes.extend_from_slice(&crate::core::canonical::canonical_serialize(&unsigned));
330 bytes
331 }
332
333 #[must_use]
339 pub fn content_digest(&self) -> String {
340 use sha2::{Digest, Sha256};
341 let digest = Sha256::digest(self.signing_bytes());
342 format!(
343 "sha256:{}",
344 crate::core::agent_identity::hex_encode(&digest)
345 )
346 }
347
348 pub fn validate_shape(&self) -> Result<(), TaskAuthorityError> {
349 if self.schema_version != TASK_DESCRIPTOR_VERSION
350 || self.action != TASK_ACTION_SEND
351 || self.artifact_refs.len() > MAX_ARTIFACT_REFS
352 || self.description.len() > MAX_DESCRIPTION_BYTES
353 || self.idempotency_key.len() > MAX_IDEMPOTENCY_KEY_BYTES
354 || self.expires_at <= self.issued_at
355 || (self.expires_at - self.issued_at).num_seconds() > MAX_TASK_LIFETIME_SECONDS
356 {
357 return Err(if self.action == TASK_ACTION_SEND {
358 TaskAuthorityError::MalformedBounds
359 } else {
360 TaskAuthorityError::UnsupportedAction
361 });
362 }
363 for value in [
364 &self.sender,
365 &self.recipient,
366 &self.tenant_id,
367 &self.project_id,
368 &self.action,
369 &self.idempotency_key,
370 &self.key_id,
371 ] {
372 validate_identifier(value, MAX_DESCRIPTOR_STRING_BYTES)?;
373 }
374 if self.description.is_empty()
375 || self.idempotency_key.is_empty()
376 || self.signature.len() != 128
377 || !self.signature.bytes().all(|byte| byte.is_ascii_hexdigit())
378 {
379 return Err(TaskAuthorityError::MalformedBounds);
380 }
381 self.grant_ref.validate()?;
382 for (index, artifact) in self.artifact_refs.iter().enumerate() {
383 artifact.validate()?;
384 if self.artifact_refs[..index]
385 .iter()
386 .any(|other| other.artifact_id == artifact.artifact_id)
387 {
388 return Err(TaskAuthorityError::DuplicateTrustEntry);
389 }
390 }
391 if self.signing_bytes().len() > MAX_TASK_DESCRIPTOR_BYTES {
392 return Err(TaskAuthorityError::MalformedBounds);
393 }
394 Ok(())
395 }
396
397 pub fn verify_authority(
404 &self,
405 policy: &TaskAuthorityConfigV1,
406 expected: &TaskAuthorityExpectationV1<'_>,
407 ) -> Result<(), TaskAuthorityError> {
408 self.validate_shape()?;
409 authorize(
410 &AuthorityClaimV1 {
411 sender: &self.sender,
412 recipient: &self.recipient,
413 tenant_id: &self.tenant_id,
414 project_id: &self.project_id,
415 action: &self.action,
416 issued_at: self.issued_at,
417 expires_at: self.expires_at,
418 grant_id: &self.grant_ref.grant_id,
419 key_id: &self.key_id,
420 signature: &self.signature,
421 signing_bytes: self.signing_bytes(),
422 },
423 policy,
424 expected,
425 )
426 }
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
448#[serde(deny_unknown_fields)]
449pub struct TaskControlDescriptorV1 {
450 pub schema_version: u32,
451 pub sender: String,
452 pub recipient: String,
453 pub tenant_id: String,
454 pub project_id: String,
455 pub action: String,
456 pub task_id: String,
457 pub task_descriptor_digest: String,
458 pub issued_at: DateTime<Utc>,
459 pub expires_at: DateTime<Utc>,
460 pub nonce: String,
461 pub reason: Option<String>,
462 pub grant_ref: CapabilityGrantRefV1,
463 pub key_id: String,
464 pub signature: String,
465}
466
467impl TaskControlDescriptorV1 {
468 #[allow(clippy::too_many_arguments)]
469 pub fn new(
470 sender: &str,
471 recipient: &str,
472 tenant_id: &str,
473 project_id: &str,
474 action: &str,
475 task_id: &str,
476 task_descriptor_digest: &str,
477 issued_at: DateTime<Utc>,
478 expires_at: DateTime<Utc>,
479 nonce: &str,
480 reason: Option<&str>,
481 grant_id: &str,
482 key_id: &str,
483 ) -> Self {
484 Self {
485 schema_version: TASK_CONTROL_DESCRIPTOR_VERSION,
486 sender: sender.to_string(),
487 recipient: recipient.to_string(),
488 tenant_id: tenant_id.to_string(),
489 project_id: project_id.to_string(),
490 action: action.to_string(),
491 task_id: task_id.to_string(),
492 task_descriptor_digest: task_descriptor_digest.to_string(),
493 issued_at,
494 expires_at,
495 nonce: nonce.to_string(),
496 reason: reason.map(str::to_string),
497 grant_ref: CapabilityGrantRefV1 {
498 schema_version: CAPABILITY_GRANT_REF_VERSION,
499 grant_id: grant_id.to_string(),
500 },
501 key_id: key_id.to_string(),
502 signature: String::new(),
503 }
504 }
505
506 pub fn sign(&mut self, signing_key: &SigningKey) {
509 self.signature.clear();
510 let bytes = self.signing_bytes();
511 self.signature = crate::core::agent_identity::hex_encode(
512 &crate::core::agent_identity::sign_bytes_with(signing_key, &bytes),
513 );
514 }
515
516 #[must_use]
523 pub fn signing_bytes(&self) -> Vec<u8> {
524 let mut unsigned = self.clone();
525 unsigned.signature.clear();
526 let mut bytes = Vec::from(TASK_CONTROL_SIGNING_DOMAIN);
527 bytes.extend_from_slice(&crate::core::canonical::canonical_serialize(&unsigned));
528 bytes
529 }
530
531 #[must_use]
535 pub fn content_digest(&self) -> String {
536 use sha2::{Digest, Sha256};
537 let digest = Sha256::digest(self.signing_bytes());
538 format!(
539 "sha256:{}",
540 crate::core::agent_identity::hex_encode(&digest)
541 )
542 }
543
544 pub fn validate_shape(&self) -> Result<(), TaskAuthorityError> {
545 if !is_control_action(&self.action) {
546 return Err(TaskAuthorityError::UnsupportedAction);
547 }
548 if self.schema_version != TASK_CONTROL_DESCRIPTOR_VERSION
549 || self.expires_at <= self.issued_at
550 || (self.expires_at - self.issued_at)
551 > chrono::Duration::seconds(MAX_TASK_CONTROL_LIFETIME_SECONDS)
552 || self.signature.len() != 128
553 || !self.signature.bytes().all(|byte| byte.is_ascii_hexdigit())
554 {
555 return Err(TaskAuthorityError::MalformedBounds);
556 }
557 for value in [
558 &self.sender,
559 &self.recipient,
560 &self.tenant_id,
561 &self.project_id,
562 &self.action,
563 &self.task_id,
564 &self.key_id,
565 ] {
566 validate_identifier(value, MAX_DESCRIPTOR_STRING_BYTES)?;
567 }
568 validate_identifier(&self.nonce, MAX_CONTROL_NONCE_BYTES)?;
569 validate_digest(&self.task_descriptor_digest)?;
570 validate_control_reason(&self.action, self.reason.as_deref())?;
571 self.grant_ref.validate()?;
572 if self.signing_bytes().len() > MAX_TASK_DESCRIPTOR_BYTES {
573 return Err(TaskAuthorityError::MalformedBounds);
574 }
575 Ok(())
576 }
577
578 pub fn verify_authority(
592 &self,
593 policy: &TaskAuthorityConfigV1,
594 expected: &TaskAuthorityExpectationV1<'_>,
595 ) -> Result<(), TaskAuthorityError> {
596 self.validate_shape()?;
597 authorize(
598 &AuthorityClaimV1 {
599 sender: &self.sender,
600 recipient: &self.recipient,
601 tenant_id: &self.tenant_id,
602 project_id: &self.project_id,
603 action: &self.action,
604 issued_at: self.issued_at,
605 expires_at: self.expires_at,
606 grant_id: &self.grant_ref.grant_id,
607 key_id: &self.key_id,
608 signature: &self.signature,
609 signing_bytes: self.signing_bytes(),
610 },
611 policy,
612 expected,
613 )
614 }
615}
616
617#[derive(Debug, Clone, Copy)]
620pub struct TaskAuthorityExpectationV1<'a> {
621 pub sender: &'a str,
622 pub recipient: &'a str,
623 pub tenant_id: &'a str,
624 pub project_id: &'a str,
625 pub now: DateTime<Utc>,
626}
627
628pub(super) struct AuthorityClaimV1<'a> {
634 pub(super) sender: &'a str,
635 pub(super) recipient: &'a str,
636 pub(super) tenant_id: &'a str,
637 pub(super) project_id: &'a str,
638 pub(super) action: &'a str,
639 pub(super) issued_at: DateTime<Utc>,
640 pub(super) expires_at: DateTime<Utc>,
641 pub(super) grant_id: &'a str,
642 pub(super) key_id: &'a str,
643 pub(super) signature: &'a str,
644 pub(super) signing_bytes: Vec<u8>,
645}
646
647pub(super) fn authorize(
655 claim: &AuthorityClaimV1<'_>,
656 policy: &TaskAuthorityConfigV1,
657 expected: &TaskAuthorityExpectationV1<'_>,
658) -> Result<(), TaskAuthorityError> {
659 policy.validate()?;
660 if claim.sender != expected.sender {
661 return Err(TaskAuthorityError::WrongSender);
662 }
663 if claim.recipient != expected.recipient {
664 return Err(TaskAuthorityError::WrongRecipient);
665 }
666 if claim.tenant_id != expected.tenant_id || claim.project_id != expected.project_id {
667 return Err(TaskAuthorityError::WrongScope);
668 }
669
670 let now = expected.now;
671 let peer = policy
672 .peers
673 .iter()
674 .find(|peer| peer.key_id == claim.key_id)
675 .ok_or(TaskAuthorityError::UnknownKey)?;
676 if peer.revoked {
677 return Err(TaskAuthorityError::RevokedKey);
678 }
679 if peer.agent_id != claim.sender {
680 return Err(TaskAuthorityError::WrongSender);
681 }
682 if !peer
683 .allowed_actions
684 .iter()
685 .any(|action| action == claim.action)
686 {
687 return Err(TaskAuthorityError::UnsupportedAction);
688 }
689 if !peer
690 .allowed_scopes
691 .iter()
692 .any(|scope| scope.tenant_id == claim.tenant_id && scope.project_id == claim.project_id)
693 {
694 return Err(TaskAuthorityError::WrongScope);
695 }
696 if !verify_signed_transcript(claim.signature, &claim.signing_bytes, peer)? {
698 return Err(TaskAuthorityError::InvalidSignature);
699 }
700 if claim.issued_at > now + chrono::Duration::seconds(MAX_AUTHORITY_CLOCK_SKEW_SECONDS) {
701 return Err(TaskAuthorityError::DescriptorNotYetValid);
702 }
703 if now >= claim.expires_at {
704 return Err(TaskAuthorityError::DescriptorExpired);
705 }
706 if now < peer.not_before || claim.issued_at < peer.not_before {
707 return Err(TaskAuthorityError::KeyNotYetValid);
708 }
709 if now >= peer.expires_at {
710 return Err(TaskAuthorityError::KeyExpired);
711 }
712
713 let grant = policy
714 .grants
715 .iter()
716 .find(|grant| grant.grant_id == claim.grant_id)
717 .ok_or(TaskAuthorityError::MissingGrant)?;
718 if grant.revoked {
719 return Err(TaskAuthorityError::RevokedGrant);
720 }
721 if grant.key_id != claim.key_id
722 || grant.action != claim.action
723 || grant.tenant_id != claim.tenant_id
724 || grant.project_id != claim.project_id
725 {
726 return Err(TaskAuthorityError::GrantMismatch);
727 }
728 if now < grant.not_before || claim.issued_at < grant.not_before {
729 return Err(TaskAuthorityError::GrantNotYetValid);
730 }
731 if now >= grant.expires_at {
732 return Err(TaskAuthorityError::GrantExpired);
733 }
734 if claim.expires_at > grant.expires_at || claim.expires_at > peer.expires_at {
736 return Err(TaskAuthorityError::GrantMismatch);
737 }
738 Ok(())
739}
740
741fn verify_signed_transcript(
742 signature: &str,
743 transcript: &[u8],
744 peer: &TaskPeerTrustV1,
745) -> Result<bool, TaskAuthorityError> {
746 let public_key = crate::core::agent_identity::hex_decode(&peer.public_key)
747 .map_err(|_| TaskAuthorityError::InvalidKey)?;
748 let signature = crate::core::agent_identity::hex_decode(signature)
749 .map_err(|_| TaskAuthorityError::InvalidSignature)?;
750 Ok(crate::core::agent_identity::verify_signature(
751 &public_key,
752 transcript,
753 &signature,
754 ))
755}
756
757fn is_control_action(action: &str) -> bool {
761 action == TASK_ACTION_GET || action == TASK_ACTION_CANCEL
762}
763
764fn is_supported_task_action(action: &str) -> bool {
768 action == TASK_ACTION_SEND
769 || is_control_action(action)
770 || delivery_authority::is_delivery_action(action)
771 || action == delivery_delegation::DELIVERY_DELEGATE
772}
773
774pub(super) fn validate_digest(value: &str) -> Result<(), TaskAuthorityError> {
775 let hex = value
776 .strip_prefix(DIGEST_PREFIX)
777 .ok_or(TaskAuthorityError::MalformedBounds)?;
778 if hex.len() != DIGEST_HEX_LEN || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
779 return Err(TaskAuthorityError::MalformedBounds);
780 }
781 Ok(())
782}
783
784fn validate_control_reason(action: &str, reason: Option<&str>) -> Result<(), TaskAuthorityError> {
790 let Some(reason) = reason else {
791 return Ok(());
792 };
793 if action != TASK_ACTION_CANCEL
794 || reason.is_empty()
795 || reason.len() > MAX_CANCEL_REASON_BYTES
796 || !reason
797 .bytes()
798 .all(|byte| byte.is_ascii_graphic() || byte == b' ')
799 {
800 return Err(TaskAuthorityError::MalformedBounds);
801 }
802 Ok(())
803}
804
805fn validate_peer(peer: &TaskPeerTrustV1) -> Result<(), TaskAuthorityError> {
806 if peer.schema_version != TASK_AUTHORITY_CONFIG_VERSION
807 || peer.allowed_actions.is_empty()
808 || peer.allowed_scopes.is_empty()
809 || peer.expires_at <= peer.not_before
810 || (peer.expires_at - peer.not_before).num_seconds() > MAX_POLICY_LIFETIME_SECONDS
811 {
812 return Err(TaskAuthorityError::MalformedBounds);
813 }
814 validate_identifier(&peer.key_id, MAX_DESCRIPTOR_STRING_BYTES)?;
815 validate_identifier(&peer.agent_id, MAX_DESCRIPTOR_STRING_BYTES)?;
816 if peer.public_key.len() != 64 || !peer.public_key.bytes().all(|byte| byte.is_ascii_hexdigit())
817 {
818 return Err(TaskAuthorityError::InvalidKey);
819 }
820 for action in &peer.allowed_actions {
821 if !is_supported_task_action(action) {
822 return Err(TaskAuthorityError::UnsupportedAction);
823 }
824 }
825 for scope in &peer.allowed_scopes {
826 scope.validate()?;
827 }
828 Ok(())
829}
830
831fn validate_grant(grant: &TaskCapabilityGrantV1) -> Result<(), TaskAuthorityError> {
832 if grant.schema_version != TASK_AUTHORITY_CONFIG_VERSION
833 || grant.expires_at <= grant.not_before
834 || (grant.expires_at - grant.not_before).num_seconds() > MAX_POLICY_LIFETIME_SECONDS
835 {
836 return Err(TaskAuthorityError::MalformedBounds);
837 }
838 validate_identifier(&grant.grant_id, MAX_DESCRIPTOR_STRING_BYTES)?;
839 validate_identifier(&grant.key_id, MAX_DESCRIPTOR_STRING_BYTES)?;
840 validate_identifier(&grant.action, MAX_DESCRIPTOR_STRING_BYTES)?;
841 validate_identifier(&grant.tenant_id, MAX_DESCRIPTOR_STRING_BYTES)?;
842 validate_identifier(&grant.project_id, MAX_DESCRIPTOR_STRING_BYTES)?;
843 if !is_supported_task_action(&grant.action) {
844 return Err(TaskAuthorityError::UnsupportedAction);
845 }
846 Ok(())
847}
848
849pub(super) fn validate_identifier(value: &str, max_bytes: usize) -> Result<(), TaskAuthorityError> {
850 if value.is_empty()
851 || value.len() > max_bytes
852 || !value.is_ascii()
853 || !value
854 .bytes()
855 .all(|byte| byte.is_ascii_alphanumeric() || b"._:/-".contains(&byte))
856 {
857 return Err(TaskAuthorityError::MalformedBounds);
858 }
859 Ok(())
860}