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