Skip to main content

aep_platform/
platform.rs

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}