1use std::{collections::BTreeMap, sync::Arc, time::Duration};
2
3use aep_core::{
4 AssertionOperation, ClientAssertionClaims, ErrorCode, ProblemDetails, SigningAlgorithm,
5 VerifyClientAssertionOptions, decode_jwt_unverified, new_problem_details,
6 verify_client_assertion,
7};
8use futures::FutureExt as _;
9use http::{HeaderMap, HeaderValue, header};
10use serde::{Serialize, de::DeserializeOwned};
11use sha2::{Digest as _, Sha256};
12use time::{OffsetDateTime, format_description::well_known::Rfc3339};
13use uuid::Uuid;
14
15use crate::{
16 AgentIdentity, AgentIdentityListResponse, AuthorizationRequest, Authorizer, Clock,
17 DID_MEDIA_TYPE, DefaultLifecyclePolicy, DidDocument, DiscoveryDocument, IdempotencyInput,
18 IdempotencyResult, IdempotencyStore, IdempotentOperation, IdentityListQuery, IdentityRecord,
19 IdentityStore, KeyStore, LifecyclePolicy, LifecycleRequest, ManagedAgentStatus,
20 MemoryIdempotencyStore, MemoryIdentityStore, PlatformError, PlatformOptions, PlatformResponse,
21 ReplayStore, RequestContext, ResponseBody, ServiceDidResolver, SignHandler, SignRequest,
22 SignResponse, SignStatus, StoredResponse, SystemClock, VerificationRequest,
23 VerificationResponse, create_did_document, create_discovery_document,
24 create_service_scoped_agent_did, is_absolute_https_url, is_did, lifecycle_error_code,
25 render_did_url,
26};
27
28const MAXIMUM_LIST_LIMIT: usize = 100;
29
30pub struct Platform {
31 agent_did_id_generator: Arc<dyn Fn() -> Result<String, PlatformError> + Send + Sync>,
32 authorizer: Arc<dyn Authorizer>,
33 clock: Arc<dyn Clock>,
34 default_lifetime: Duration,
35 did_host: String,
36 did_path_prefix: String,
37 did_url_template: String,
38 discovery: DiscoveryDocument,
39 hosted_verification: bool,
40 identifier: Arc<dyn Fn() -> Result<String, PlatformError> + Send + Sync>,
41 idempotency_store: Arc<dyn IdempotencyStore>,
42 identity_store: Arc<dyn IdentityStore>,
43 key_store: Arc<dyn KeyStore>,
44 lifecycle_policy: Arc<dyn LifecyclePolicy>,
45 maximum_lifetime: Duration,
46 pub(crate) replay_store: Option<Arc<dyn ReplayStore>>,
47 service_did_resolver: Arc<dyn ServiceDidResolver>,
48 sign_handler: Option<Arc<dyn SignHandler>>,
49 signing_algorithms: Vec<SigningAlgorithm>,
50}
51
52impl Platform {
53 pub fn new(options: PlatformOptions) -> Result<Arc<Self>, PlatformError> {
54 create_service_scoped_agent_did(&options.did_host, &options.did_path_prefix, "validation")?;
55 validate_signing_algorithms(&options.signing_algorithms)?;
56 validate_lifetime(
57 "maximum",
58 options.maximum_lifetime,
59 aep_core::MAX_ASSERTION_LIFETIME,
60 )?;
61 validate_lifetime(
62 "default",
63 options.default_lifetime,
64 options.maximum_lifetime,
65 )?;
66 if options.hosted_verification && options.replay_store.is_none() {
67 return Err(PlatformError::InvalidConfiguration(
68 "AEP Platform hosted verification requires a replay store".to_owned(),
69 ));
70 }
71 let discovery = create_discovery_document(
72 &options.discovery,
73 &options.did_url_template,
74 options.hosted_verification,
75 options.signing_algorithms.clone(),
76 options.default_lifetime,
77 )?;
78 let clock = options.clock.unwrap_or_else(|| Arc::new(SystemClock));
79 let idempotency_store = options
80 .idempotency_store
81 .unwrap_or_else(|| Arc::new(MemoryIdempotencyStore::new(clock.clone())));
82 Ok(Arc::new(Self {
83 agent_did_id_generator: options
84 .agent_did_id_generator
85 .unwrap_or_else(|| Arc::new(random_identifier)),
86 authorizer: options.authorizer,
87 clock,
88 default_lifetime: options.default_lifetime,
89 did_host: options.did_host,
90 did_path_prefix: options.did_path_prefix,
91 did_url_template: options.did_url_template,
92 discovery,
93 hosted_verification: options.hosted_verification,
94 identifier: options
95 .identifier
96 .unwrap_or_else(|| Arc::new(random_identifier)),
97 idempotency_store,
98 identity_store: options
99 .identity_store
100 .unwrap_or_else(|| Arc::new(MemoryIdentityStore::default())),
101 key_store: options.key_store,
102 lifecycle_policy: options
103 .lifecycle_policy
104 .unwrap_or_else(|| Arc::new(DefaultLifecyclePolicy)),
105 maximum_lifetime: options.maximum_lifetime,
106 replay_store: options.replay_store,
107 service_did_resolver: options.service_did_resolver,
108 sign_handler: options.sign_handler,
109 signing_algorithms: options.signing_algorithms,
110 }))
111 }
112
113 pub fn discovery(&self) -> PlatformResponse<DiscoveryDocument> {
114 let mut response = success(200, self.discovery.clone());
115 response.headers.insert(
116 header::CACHE_CONTROL,
117 HeaderValue::from_static("max-age=300"),
118 );
119 response
120 }
121
122 pub async fn did_document(
123 &self,
124 agent_did_id: &str,
125 ) -> Result<PlatformResponse<DidDocument>, PlatformError> {
126 let Some(identity) = self
127 .identity_store
128 .find_by_agent_did_id(agent_did_id)
129 .await?
130 else {
131 return Ok(problem(404, ErrorCode::NotRecognized));
132 };
133 if identity.status != ManagedAgentStatus::Active {
134 return Ok(problem(404, ErrorCode::NotRecognized));
135 }
136 let method = self.key_store.did_verification_method(&identity).await?;
137 let document = create_did_document(&identity, method)?;
138 let mut response = success(200, document);
139 response.headers.insert(
140 header::CONTENT_TYPE,
141 HeaderValue::from_static(DID_MEDIA_TYPE),
142 );
143 response.headers.insert(
144 header::CACHE_CONTROL,
145 HeaderValue::from_static("max-age=300"),
146 );
147 Ok(response)
148 }
149
150 pub async fn get_identity(
151 &self,
152 agent_identity_id: &str,
153 context: &RequestContext,
154 ) -> Result<PlatformResponse<AgentIdentity>, PlatformError> {
155 let Some(identity) = self
156 .authorized_identity(agent_identity_id, context, |identity| {
157 AuthorizationRequest::GetIdentity { identity }
158 })
159 .await?
160 else {
161 return Ok(problem(404, ErrorCode::NotRecognized));
162 };
163 Ok(success(200, agent_identity(&identity)?))
164 }
165
166 pub async fn list(
167 &self,
168 mut query: IdentityListQuery,
169 context: &RequestContext,
170 ) -> Result<PlatformResponse<AgentIdentityListResponse>, PlatformError> {
171 if query.limit > MAXIMUM_LIST_LIMIT
172 || query
173 .service_did
174 .as_ref()
175 .is_some_and(|service_did| !is_did(service_did))
176 {
177 return Ok(problem(400, ErrorCode::InvalidRequest));
178 }
179 if query.limit == 0 {
180 query.limit = MAXIMUM_LIST_LIMIT;
181 }
182 let authorized = self
183 .authorizer
184 .authorize(
185 &AuthorizationRequest::ListIdentities {
186 query: query.clone(),
187 },
188 context,
189 )
190 .await?;
191 if !authorized || context.principal.is_empty() {
192 return Ok(problem(404, ErrorCode::NotRecognized));
193 }
194 let result = self.identity_store.list(&context.principal, &query).await?;
195 let data = result
196 .identities
197 .iter()
198 .map(agent_identity)
199 .collect::<Result<Vec<_>, _>>()?;
200 Ok(success(
201 200,
202 AgentIdentityListResponse {
203 additional: BTreeMap::new(),
204 count: data.len().to_string(),
205 data,
206 total: result.total.to_string(),
207 },
208 ))
209 }
210
211 pub async fn provision(
212 self: &Arc<Self>,
213 request: crate::ProvisionRequest,
214 context: RequestContext,
215 ) -> Result<PlatformResponse<AgentIdentity>, PlatformError> {
216 if !is_did(&request.service_did) {
217 return Ok(problem(400, ErrorCode::InvalidRequest));
218 }
219 let material = request.clone();
220 let platform = self.clone();
221 self.execute_idempotent(
222 IdempotentOperation::Provision,
223 &material,
224 context.clone(),
225 move || async move {
226 let authorized = platform
227 .authorizer
228 .authorize(
229 &AuthorizationRequest::Provision {
230 request: request.clone(),
231 },
232 &context,
233 )
234 .await?;
235 if !authorized {
236 return Ok(problem(404, ErrorCode::NotRecognized));
237 }
238 if !platform
239 .service_did_resolver
240 .resolve(&request.service_did)
241 .await?
242 {
243 return Ok(problem(400, ErrorCode::InvalidRequest));
244 }
245 let creation_platform = platform.clone();
246 let principal = context.principal.clone();
247 let service_did = request.service_did.clone();
248 let creation_principal = principal.clone();
249 let creation_service_did = service_did.clone();
250 let identity = platform
251 .identity_store
252 .find_or_create(
253 &principal,
254 &service_did,
255 Box::new(move || {
256 async move {
257 creation_platform
258 .create_identity(creation_principal, creation_service_did)
259 .await
260 }
261 .boxed()
262 }),
263 )
264 .await?;
265 Ok(success(200, agent_identity(&identity)?))
266 },
267 )
268 .await
269 }
270
271 pub async fn sign(
272 self: &Arc<Self>,
273 agent_identity_id: &str,
274 request: SignRequest,
275 context: RequestContext,
276 ) -> Result<PlatformResponse<SignResponse>, PlatformError> {
277 let lifetime = match self.sign_lifetime(&request) {
278 Ok(lifetime) => lifetime,
279 Err(_) => return Ok(problem(400, ErrorCode::InvalidRequest)),
280 };
281 #[derive(Serialize)]
282 struct SignMaterial {
283 agent_identity_id: String,
284 request: SignRequest,
285 }
286 let material = SignMaterial {
287 agent_identity_id: agent_identity_id.to_owned(),
288 request: request.clone(),
289 };
290 let platform = self.clone();
291 let agent_identity_id = agent_identity_id.to_owned();
292 self.execute_idempotent(
293 IdempotentOperation::Sign,
294 &material,
295 context.clone(),
296 move || async move {
297 let Some(identity) = platform
298 .authorized_identity(&agent_identity_id, &context, |identity| {
299 AuthorizationRequest::Sign {
300 identity,
301 request: request.clone(),
302 }
303 })
304 .await?
305 else {
306 return Ok(problem(404, ErrorCode::NotRecognized));
307 };
308 if identity.service_did != request.service_did {
309 return Ok(problem(404, ErrorCode::NotRecognized));
310 }
311 if !platform
312 .lifecycle_policy
313 .can_sign(&identity, &context)
314 .await?
315 {
316 return Ok(problem(403, lifecycle_error_code(identity.status)));
317 }
318 if let Some(handler) = &platform.sign_handler
319 && let Some(response) = handler.sign(&identity, &request, &context).await?
320 {
321 validate_sign_response(&response, &identity, &request)?;
322 return Ok(response);
323 }
324 let now = context.now.unwrap_or_else(|| platform.clock.now());
325 let expires_at = now + lifetime;
326 let claims = ClientAssertionClaims {
327 additional: BTreeMap::new(),
328 aud: request.service_did.clone(),
329 exp: expires_at.unix_timestamp(),
330 iat: now.unix_timestamp(),
331 iss: identity.agent_did.clone(),
332 jti: request.jti.clone(),
333 op: request.op,
334 resource: request.resource.clone(),
335 sub: identity.agent_did.clone(),
336 };
337 let assertion = platform.key_store.sign(&identity, &claims).await?;
338 if assertion.is_empty() {
339 return Err(PlatformError::Handler(
340 "AEP Platform key store returned an empty assertion".to_owned(),
341 ));
342 }
343 Ok(success(
344 200,
345 SignResponse {
346 additional: BTreeMap::new(),
347 agent_did: Some(identity.agent_did),
348 client_assertion: Some(assertion),
349 expires_at: Some(format_time(expires_at)?),
350 issued_at: Some(format_time(now)?),
351 jti: Some(request.jti),
352 platform_context: request.platform_context,
353 retry_after_seconds: None,
354 service_did: Some(request.service_did),
355 status: SignStatus::Completed,
356 },
357 ))
358 },
359 )
360 .await
361 }
362
363 pub async fn update_identity(
364 &self,
365 agent_identity_id: &str,
366 request: LifecycleRequest,
367 context: &RequestContext,
368 ) -> Result<PlatformResponse<AgentIdentity>, PlatformError> {
369 let Some(identity) = self
370 .authorized_identity(agent_identity_id, context, |identity| {
371 AuthorizationRequest::UpdateIdentity {
372 identity,
373 request: request.clone(),
374 }
375 })
376 .await?
377 else {
378 return Ok(problem(404, ErrorCode::NotRecognized));
379 };
380 if !self
381 .lifecycle_policy
382 .can_transition(&identity, request.status, context)
383 .await?
384 {
385 return Ok(problem(403, lifecycle_error_code(identity.status)));
386 }
387 let Some(updated) = self
388 .identity_store
389 .update_status(agent_identity_id, request.status, self.clock.now())
390 .await?
391 else {
392 return Ok(problem(404, ErrorCode::NotRecognized));
393 };
394 Ok(success(200, agent_identity(&updated)?))
395 }
396
397 pub async fn verify(
398 self: &Arc<Self>,
399 request: VerificationRequest,
400 context: RequestContext,
401 ) -> Result<PlatformResponse<VerificationResponse>, PlatformError> {
402 if !self.hosted_verification {
403 return Ok(problem(404, ErrorCode::NotRecognized));
404 }
405 if request.client_assertion.is_empty()
406 || !is_did(&request.service_did)
407 || match (&request.op, &request.resource) {
408 (AssertionOperation::Authenticate, Some(resource)) => {
409 !is_absolute_https_url(resource)
410 }
411 (AssertionOperation::Authenticate, None) => true,
412 (_, Some(_)) => true,
413 (_, None) => false,
414 }
415 {
416 return Ok(problem(400, ErrorCode::InvalidRequest));
417 }
418 let material = request.clone();
419 let platform = self.clone();
420 self.execute_idempotent(
421 IdempotentOperation::HostedVerification,
422 &material,
423 context.clone(),
424 move || async move { platform.verify_assertion(request, context).await },
425 )
426 .await
427 }
428
429 async fn verify_assertion(
430 &self,
431 request: VerificationRequest,
432 context: RequestContext,
433 ) -> Result<PlatformResponse<VerificationResponse>, PlatformError> {
434 let unrecognized = || {
435 success(
436 200,
437 VerificationResponse {
438 additional: BTreeMap::new(),
439 agent_did: None,
440 agent_identity_id: None,
441 op: None,
442 reason: "not_recognized".to_owned(),
443 service_did: request.service_did.clone(),
444 status: None,
445 verified: false,
446 },
447 )
448 };
449 let Ok(decoded) = decode_jwt_unverified(&request.client_assertion) else {
450 return Ok(unrecognized());
451 };
452 let Some(agent_did) = decoded
453 .payload
454 .get("iss")
455 .and_then(serde_json::Value::as_str)
456 else {
457 return Ok(unrecognized());
458 };
459 let subject = decoded
460 .payload
461 .get("sub")
462 .and_then(serde_json::Value::as_str);
463 if subject != Some(agent_did) || decoded.header.key_id.as_deref() != Some(agent_did) {
464 return Ok(unrecognized());
465 }
466 let Some(identity) = self.identity_store.find_by_agent_did(agent_did).await? else {
467 return Ok(unrecognized());
468 };
469 if identity.service_did != request.service_did
470 || identity.principal != context.principal
471 || context.principal.is_empty()
472 {
473 return Ok(unrecognized());
474 }
475 let authorized = self
476 .authorizer
477 .authorize(
478 &AuthorizationRequest::Verify {
479 identity: identity.clone(),
480 request: request.clone(),
481 },
482 &context,
483 )
484 .await?;
485 if !authorized
486 || !self
487 .lifecycle_policy
488 .can_verify(&identity, &context)
489 .await?
490 {
491 return Ok(unrecognized());
492 }
493 let key = self.key_store.verification_key(&identity).await?;
494 let now = context.now.unwrap_or_else(|| self.clock.now());
495 let verified = verify_client_assertion(
496 &request.client_assertion,
497 &key,
498 &VerifyClientAssertionOptions {
499 algorithms: identity.signing_algorithms.clone(),
500 audience: Some(request.service_did.clone()),
501 current_time: Some(now.unix_timestamp()),
502 issuer: Some(agent_did.to_owned()),
503 operation: Some(request.op),
504 resource: request.resource.clone(),
505 subject: Some(agent_did.to_owned()),
506 ..VerifyClientAssertionOptions::default()
507 },
508 );
509 let Ok(claims) = verified else {
510 return Ok(unrecognized());
511 };
512 if request.op != AssertionOperation::Authenticate && claims.resource.is_some() {
513 return Ok(unrecognized());
514 }
515 let replay_key = format!(
516 "{}\0{}\0{}\0{}",
517 request.service_did,
518 request.op.as_str(),
519 agent_did,
520 claims.jti
521 );
522 let Some(replay_store) = &self.replay_store else {
523 return Err(PlatformError::InvalidConfiguration(
524 "AEP Platform hosted verification requires a replay store".to_owned(),
525 ));
526 };
527 let expires_at = OffsetDateTime::from_unix_timestamp(claims.exp)
528 .map_err(|error| PlatformError::Handler(error.to_string()))?;
529 if !replay_store.consume(&replay_key, expires_at, now).await? {
530 return Ok(unrecognized());
531 }
532 Ok(success(
533 200,
534 VerificationResponse {
535 additional: BTreeMap::new(),
536 agent_did: Some(identity.agent_did),
537 agent_identity_id: Some(identity.agent_identity_id),
538 op: Some(request.op),
539 reason: "verified".to_owned(),
540 service_did: request.service_did,
541 status: Some(identity.status),
542 verified: true,
543 },
544 ))
545 }
546
547 async fn create_identity(
548 &self,
549 principal: String,
550 service_did: String,
551 ) -> Result<IdentityRecord, PlatformError> {
552 let mut agent_identity_id = (self.identifier)()?;
553 if agent_identity_id.is_empty() {
554 return Err(PlatformError::Handler(
555 "AEP Platform identity generator returned an empty identifier".to_owned(),
556 ));
557 }
558 if !agent_identity_id.starts_with("pai_") {
559 agent_identity_id = format!("pai_{agent_identity_id}");
560 }
561 let agent_did_id = (self.agent_did_id_generator)()?;
562 let agent_did =
563 create_service_scoped_agent_did(&self.did_host, &self.did_path_prefix, &agent_did_id)?;
564 let now = self.clock.now();
565 let identity = IdentityRecord {
566 agent_did: agent_did.clone(),
567 agent_did_id: agent_did_id.clone(),
568 agent_identity_id,
569 created_at: now,
570 did_document_url: render_did_url(&self.did_url_template, &agent_did_id)?,
571 key_id: agent_did,
572 principal,
573 service_did,
574 signing_algorithms: self.signing_algorithms.clone(),
575 status: ManagedAgentStatus::Active,
576 updated_at: now,
577 };
578 self.key_store.create_key(&identity).await?;
579 Ok(identity)
580 }
581
582 async fn authorized_identity(
583 &self,
584 agent_identity_id: &str,
585 context: &RequestContext,
586 request: impl FnOnce(IdentityRecord) -> AuthorizationRequest,
587 ) -> Result<Option<IdentityRecord>, PlatformError> {
588 let Some(identity) = self.identity_store.get(agent_identity_id).await? else {
589 return Ok(None);
590 };
591 let authorized = self
592 .authorizer
593 .authorize(&request(identity.clone()), context)
594 .await?;
595 if !authorized || context.principal.is_empty() || identity.principal != context.principal {
596 return Ok(None);
597 }
598 Ok(Some(identity))
599 }
600
601 fn sign_lifetime(&self, request: &SignRequest) -> Result<Duration, PlatformError> {
602 if request.jti.is_empty() || !is_did(&request.service_did) {
603 return Err(PlatformError::InvalidConfiguration(
604 "AEP Platform signing request is invalid".to_owned(),
605 ));
606 }
607 match (&request.op, &request.resource) {
608 (AssertionOperation::Authenticate, Some(resource))
609 if is_absolute_https_url(resource) => {}
610 (AssertionOperation::Authenticate, _) => {
611 return Err(PlatformError::InvalidConfiguration(
612 "AEP Platform authenticate signing requires an HTTPS resource".to_owned(),
613 ));
614 }
615 (_, None) => {}
616 (_, Some(_)) => {
617 return Err(PlatformError::InvalidConfiguration(
618 "AEP Platform resource is only permitted for authenticate".to_owned(),
619 ));
620 }
621 }
622 let Some(seconds) = &request.lifetime_seconds else {
623 return Ok(self.default_lifetime);
624 };
625 let seconds = seconds.parse::<u64>().map_err(|_| {
626 PlatformError::InvalidConfiguration(
627 "AEP Platform lifetime_seconds must be a positive integer string".to_owned(),
628 )
629 })?;
630 let lifetime = Duration::from_secs(seconds);
631 validate_lifetime("requested", lifetime, self.maximum_lifetime)?;
632 Ok(lifetime)
633 }
634
635 async fn execute_idempotent<T, M, Execute, Future>(
636 &self,
637 operation: IdempotentOperation,
638 material: &M,
639 context: RequestContext,
640 execute: Execute,
641 ) -> Result<PlatformResponse<T>, PlatformError>
642 where
643 T: DeserializeOwned + Serialize + Send + 'static,
644 M: Serialize + ?Sized,
645 Execute: FnOnce() -> Future + Send + 'static,
646 Future: std::future::Future<Output = Result<PlatformResponse<T>, PlatformError>>
647 + Send
648 + 'static,
649 {
650 let Some(idempotency_key) = context.idempotency_key.clone() else {
651 return Ok(problem(400, ErrorCode::InvalidRequest));
652 };
653 if idempotency_key.is_empty() || context.principal.is_empty() {
654 return Ok(problem(400, ErrorCode::InvalidRequest));
655 }
656 let request_hash = format!("{:x}", Sha256::digest(serde_json::to_vec(material)?));
657 let result = self
658 .idempotency_store
659 .execute(
660 IdempotencyInput {
661 idempotency_key,
662 operation,
663 principal: context.principal,
664 request_hash,
665 },
666 Box::new(move || {
667 async move {
668 let response = execute().await?;
669 store_response(response)
670 }
671 .boxed()
672 }),
673 )
674 .await?;
675 match result {
676 IdempotencyResult::Conflict => Ok(problem(409, ErrorCode::IdempotencyConflict)),
677 IdempotencyResult::Created(response) | IdempotencyResult::Replayed(response) => {
678 restore_response(response)
679 }
680 }
681 }
682}
683
684pub(crate) fn success<T>(status: u16, body: T) -> PlatformResponse<T> {
685 let mut headers = HeaderMap::new();
686 headers.insert(
687 header::CONTENT_TYPE,
688 HeaderValue::from_static(aep_core::MEDIA_TYPE),
689 );
690 PlatformResponse {
691 body: ResponseBody::Success(body),
692 headers,
693 status,
694 }
695}
696
697pub(crate) fn problem<T>(status: u16, code: ErrorCode) -> PlatformResponse<T> {
698 let title = code
699 .as_str()
700 .split('_')
701 .map(|part| {
702 let mut characters = part.chars();
703 characters.next().map_or_else(String::new, |first| {
704 first.to_uppercase().collect::<String>() + characters.as_str()
705 })
706 })
707 .collect::<Vec<_>>()
708 .join(" ");
709 let mut headers = HeaderMap::new();
710 headers.insert(
711 header::CONTENT_TYPE,
712 HeaderValue::from_static(aep_core::PROBLEM_MEDIA_TYPE),
713 );
714 PlatformResponse {
715 body: ResponseBody::Problem(Box::new(new_problem_details(
716 code,
717 title,
718 i64::from(status),
719 ))),
720 headers,
721 status,
722 }
723}
724
725fn store_response<T: Serialize>(
726 response: PlatformResponse<T>,
727) -> Result<StoredResponse, PlatformError> {
728 let content_type = response
729 .headers
730 .get(header::CONTENT_TYPE)
731 .and_then(|value| value.to_str().ok())
732 .unwrap_or(aep_core::MEDIA_TYPE)
733 .to_owned();
734 let body = match response.body {
735 ResponseBody::Success(body) => serde_json::to_vec(&body)?,
736 ResponseBody::Problem(body) => serde_json::to_vec(&body)?,
737 };
738 Ok(StoredResponse {
739 body,
740 content_type,
741 created_at: OffsetDateTime::UNIX_EPOCH,
742 headers: response.headers,
743 status: response.status,
744 })
745}
746
747fn restore_response<T: DeserializeOwned>(
748 stored: StoredResponse,
749) -> Result<PlatformResponse<T>, PlatformError> {
750 let body = if stored.content_type == aep_core::PROBLEM_MEDIA_TYPE {
751 ResponseBody::Problem(Box::new(serde_json::from_slice::<ProblemDetails>(
752 &stored.body,
753 )?))
754 } else {
755 ResponseBody::Success(serde_json::from_slice(&stored.body)?)
756 };
757 Ok(PlatformResponse {
758 body,
759 headers: stored.headers,
760 status: stored.status,
761 })
762}
763
764fn agent_identity(identity: &IdentityRecord) -> Result<AgentIdentity, PlatformError> {
765 Ok(AgentIdentity {
766 additional: BTreeMap::new(),
767 agent_did: identity.agent_did.clone(),
768 agent_identity_id: identity.agent_identity_id.clone(),
769 created_at: format_time(identity.created_at)?,
770 did_document_url: identity.did_document_url.clone(),
771 key_id: identity.key_id.clone(),
772 service_did: identity.service_did.clone(),
773 signing_algorithms: identity.signing_algorithms.clone(),
774 status: identity.status,
775 updated_at: format_time(identity.updated_at)?,
776 })
777}
778
779fn validate_signing_algorithms(algorithms: &[SigningAlgorithm]) -> Result<(), PlatformError> {
780 if algorithms.is_empty()
781 || algorithms.iter().enumerate().any(|(index, algorithm)| {
782 !matches!(algorithm, SigningAlgorithm::EdDsa | SigningAlgorithm::Es256)
783 || algorithms[..index].contains(algorithm)
784 })
785 {
786 return Err(PlatformError::InvalidConfiguration(
787 "AEP Platform requires unique supported signing algorithms".to_owned(),
788 ));
789 }
790 Ok(())
791}
792
793fn validate_lifetime(
794 name: &str,
795 lifetime: Duration,
796 maximum: Duration,
797) -> Result<(), PlatformError> {
798 if lifetime.is_zero()
799 || lifetime > maximum
800 || lifetime > aep_core::MAX_ASSERTION_LIFETIME
801 || lifetime.subsec_nanos() != 0
802 {
803 return Err(PlatformError::InvalidConfiguration(format!(
804 "AEP Platform {name} assertion lifetime must be whole seconds within the supported maximum"
805 )));
806 }
807 Ok(())
808}
809
810fn validate_sign_response(
811 response: &PlatformResponse<SignResponse>,
812 identity: &IdentityRecord,
813 request: &SignRequest,
814) -> Result<(), PlatformError> {
815 match &response.body {
816 ResponseBody::Problem(problem) => {
817 if response.status != u16::try_from(problem.status).unwrap_or_default() {
818 return Err(PlatformError::Handler(
819 "AEP Platform sign handler returned invalid Problem Details".to_owned(),
820 ));
821 }
822 }
823 ResponseBody::Success(body) if body.status == SignStatus::Pending => {
824 let retry = body
825 .retry_after_seconds
826 .as_deref()
827 .and_then(|value| value.parse::<u16>().ok());
828 if response.status != 202
829 || !matches!(retry, Some(1..=300))
830 || response.headers.contains_key(header::RETRY_AFTER)
831 || body.agent_did.is_some()
832 || body.client_assertion.is_some()
833 || body.expires_at.is_some()
834 || body.issued_at.is_some()
835 || body.jti.is_some()
836 || body.service_did.is_some()
837 {
838 return Err(PlatformError::Handler(
839 "AEP Platform sign handler returned an invalid pending response".to_owned(),
840 ));
841 }
842 }
843 ResponseBody::Success(body) => {
844 let issued_at = body
845 .issued_at
846 .as_deref()
847 .and_then(|value| OffsetDateTime::parse(value, &Rfc3339).ok());
848 let expires_at = body
849 .expires_at
850 .as_deref()
851 .and_then(|value| OffsetDateTime::parse(value, &Rfc3339).ok());
852 if response.status != 200
853 || body.status != SignStatus::Completed
854 || body.agent_did.as_deref() != Some(&identity.agent_did)
855 || body.client_assertion.as_deref().is_none_or(str::is_empty)
856 || body.jti.as_deref() != Some(&request.jti)
857 || body.service_did.as_deref() != Some(&request.service_did)
858 || !matches!((issued_at, expires_at), (Some(issued), Some(expires)) if expires > issued && expires - issued <= aep_core::MAX_ASSERTION_LIFETIME)
859 || body.retry_after_seconds.is_some()
860 {
861 return Err(PlatformError::Handler(
862 "AEP Platform sign handler returned an invalid completed response".to_owned(),
863 ));
864 }
865 }
866 }
867 Ok(())
868}
869
870fn format_time(value: OffsetDateTime) -> Result<String, PlatformError> {
871 value
872 .format(&Rfc3339)
873 .map_err(|error| PlatformError::Handler(error.to_string()))
874}
875
876fn random_identifier() -> Result<String, PlatformError> {
877 Ok(Uuid::new_v4().simple().to_string())
878}