1mod credentials;
20mod names;
21mod oidc;
22mod parked;
23mod proof;
24
25pub(crate) use credentials::{update_credentials, update_credentials_from_dashboard};
26pub use names::RESERVED_LABELS;
27pub use oidc::{JwksCache, DEFAULT_OIDC_FRESHNESS};
28pub use parked::{
29 fingerprint, ParkedKey, ParkedKeys, DEFAULT_PARK_TTL, PARK_RATE_LIMIT, PARK_RATE_WINDOW,
30};
31pub use proof::{NotSpent, Proof, Replay, MAX_PROOF_LIFETIME};
32
33use std::collections::{BTreeMap, HashMap};
34use std::sync::Mutex;
35use std::time::Instant;
36
37use axum::extract::{Query, State};
38use axum::http::{header, HeaderMap, StatusCode};
39use axum::response::{IntoResponse, Response};
40use axum::Json;
41use didbot_pds::{Creator, EstopRefusal, ProvisionError, ProvisionRequest};
42use serde_json::json;
43use time::{Duration, OffsetDateTime};
44use tracing::{debug, info, warn};
45
46use crate::error::ApiError;
47use crate::rate_limit::{caller_key, Peer};
48use crate::routes::{blocking_task_failed, AppState, XrpcBody};
49use crate::wire::{parse_kind, CreateAccountRequest, ParkKeyRequest, ParkedKeyQuery, SessionQuery};
50
51use names::split_name;
52use proof::{invalid_proof, prove, Named, NamedBy};
53
54const TOP_EDGE_TTL: std::time::Duration = std::time::Duration::from_secs(60);
57
58pub struct TreeState {
60 pub parked: ParkedKeys,
62 pub replay: Replay,
64 pub jwks: JwksCache,
66 top_edges: Mutex<HashMap<String, (bool, Instant)>>,
69 name_read_failed: Mutex<Option<Instant>>,
72 pub client: reqwest::Client,
74 pub issuers: Vec<String>,
77 pub freshness: Duration,
79 pub loopback_fetches: bool,
86 pub reserved_labels: Vec<String>,
89}
90
91impl TreeState {
92 pub fn new(park_ttl: Duration) -> Self {
95 Self {
96 parked: ParkedKeys::new(park_ttl),
97 replay: Replay::default(),
98 jwks: JwksCache::default(),
99 top_edges: Mutex::new(HashMap::new()),
100 name_read_failed: Mutex::new(None),
101 client: didbot_http::client(),
102 issuers: Vec::new(),
103 freshness: DEFAULT_OIDC_FRESHNESS,
104 loopback_fetches: false,
105 reserved_labels: RESERVED_LABELS.iter().map(|s| (*s).to_owned()).collect(),
106 }
107 }
108
109 pub fn with_spent_store(
116 mut self,
117 store: didbot_pds::SpentStore,
118 ) -> Result<Self, didbot_pds::SideStoreError> {
119 self.replay = Replay::kept_in(store)?;
120 Ok(self)
121 }
122
123 #[must_use]
126 pub fn with_reserved_labels(mut self, labels: Vec<String>) -> Self {
127 self.reserved_labels = labels;
128 self
129 }
130
131 #[must_use]
133 pub fn with_loopback_fetches(mut self, allowed: bool) -> Self {
134 self.loopback_fetches = allowed;
135 self
136 }
137
138 pub(crate) fn private_addresses(&self) -> didbot_http::PrivateAddresses {
140 if self.loopback_fetches {
141 didbot_http::PrivateAddresses::Allowed
142 } else {
143 didbot_http::PrivateAddresses::Refused
144 }
145 }
146
147 #[must_use]
149 pub fn with_issuers(mut self, issuers: Vec<String>) -> Self {
150 self.issuers = issuers;
151 self
152 }
153
154 #[must_use]
156 pub fn with_freshness(mut self, freshness: Duration) -> Self {
157 self.freshness = freshness;
158 self
159 }
160
161 fn top_edge(&self, did: &str) -> Option<bool> {
164 self.top_edges
165 .lock()
166 .unwrap_or_else(|p| p.into_inner())
167 .get(did)
168 .filter(|(_, at)| at.elapsed() < TOP_EDGE_TTL)
169 .map(|(stands, _)| *stands)
170 }
171
172 fn remember_top_edge(&self, did: &str, stands: bool) {
173 self.top_edges
174 .lock()
175 .unwrap_or_else(|p| p.into_inner())
176 .insert(did.to_owned(), (stands, Instant::now()));
177 }
178
179 fn name_read_failed_lately(&self) -> bool {
182 self.name_read_failed
183 .lock()
184 .unwrap_or_else(|p| p.into_inner())
185 .is_some_and(|at| at.elapsed() < TOP_EDGE_TTL)
186 }
187
188 fn remember_name_read_failed(&self) {
189 *self
190 .name_read_failed
191 .lock()
192 .unwrap_or_else(|p| p.into_inner()) = Some(Instant::now());
193 }
194}
195
196impl Default for TreeState {
197 fn default() -> Self {
198 Self::new(DEFAULT_PARK_TTL)
199 }
200}
201
202const ALLOWANCE_REREAD_WAIT: std::time::Duration = std::time::Duration::from_secs(8);
208
209pub(crate) async fn create_account(
225 State(state): State<AppState>,
226 peer: Peer,
227 headers: HeaderMap,
228 XrpcBody(body): XrpcBody,
229) -> Response {
230 match create_account_inner(&state, peer, &headers, &body).await {
231 Ok(response) => response,
232 Err(error) => error.into_response(),
233 }
234}
235
236async fn create_account_inner(
237 state: &AppState,
238 peer: Peer,
239 headers: &HeaderMap,
240 body: &axum::body::Bytes,
241) -> Result<Response, ApiError> {
242 state.estop.check_issue(EstopRefusal::Account)?;
243 crate::routes::require_lifecycle(state, |p| p.provisions_accounts, "bot.did.createAccount")?;
244 let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
245 if !state.provision_rate_limiter.check(&key) {
246 info!("provisioning refused: too many attempts from this address");
247 return Ok(crate::routes::too_many_provisioning_attempts());
248 }
249 crate::routes::require_account_capacity(state).inspect_err(|_| {
250 info!("provisioning refused: this deployment is at its account cap");
251 })?;
252 let request: CreateAccountRequest = crate::routes::parse_body(body)?;
253 let kind = parse_kind(&request.kind)?;
254 let fragments = fragments_of(&request)?;
255 let (label, zone) = match request.name.as_deref() {
258 Some(name) => {
259 let (label, zone) =
260 split_name(state.registry.as_ref(), &state.tree.reserved_labels, name)?;
261 (label, Some(zone))
262 }
263 None => (String::new(), None),
264 };
265 if request.key.is_some() && request.parked_key.is_some() {
266 return Err(ApiError::bad_request(
267 "name one of `key` and `parkedKey`, not both",
268 ));
269 }
270 let creator = if crate::surface::session_cookie(headers).is_some() {
271 if let Err(refusal) = crate::surface::signed_in(state, headers, true) {
276 info!("provisioning refused: no live operator session");
277 return Ok(*refusal);
278 }
279 if request.parent.is_some() {
280 return Err(ApiError::bad_request(
281 "the operator creates at the top of the tree, with no `parent`",
282 ));
283 }
284 let Some(name) = request.name.as_deref() else {
285 return Err(ApiError::new(
286 StatusCode::BAD_REQUEST,
287 "NameRequired",
288 "the operator's record is keyed by the name, so the operator names what \
289 they create",
290 ));
291 };
292 operator_creator(state, &state.registry.operator_did(), name).await?
293 } else {
294 let proof = prove(
295 state,
296 headers,
297 "bot.did.createAccount",
298 state.unauthenticated_creates,
299 Named {
300 did: request.parent.as_deref(),
301 field: NamedBy::Parent,
302 },
303 )
304 .await?;
305 match proof.did() {
306 None if request.parent.is_some() => {
307 return Err(ApiError::bad_request(
308 "`parent` needs a credential for that account",
309 ));
310 }
311 None => Creator::Server,
312 Some(did) => {
313 let Some(account) = state.registry.account(did) else {
314 return Err(invalid_proof(format!(
315 "{did} is not an account this server hosts"
316 )));
317 };
318 require_top_edge(state, &account).await?;
319 Creator::Account(did.to_owned())
320 }
321 }
322 };
323
324 let by_operator = matches!(creator, Creator::Operator { .. });
325 if let (Some(name), false) = (request.name.as_deref(), by_operator) {
326 refuse_operators_name(state, name).await?;
327 }
328
329 let now = OffsetDateTime::now_utc();
330 let account_key = match request.parked_key.as_deref() {
331 None => request.key.clone(),
332 Some(fingerprint) => {
333 let Some(name) = request.name.as_deref() else {
334 return Err(ApiError::new(
335 StatusCode::BAD_REQUEST,
336 "NameRequired",
337 "a parked key is admitted under the name it was parked for",
338 ));
339 };
340 let parked = state
341 .tree
342 .parked
343 .take(name, fingerprint, now)
344 .ok_or_else(|| {
345 ApiError::new(
346 StatusCode::NOT_FOUND,
347 "ParkedKeyNotFound",
348 format!("no key with fingerprint {fingerprint} is parked for {name:?}"),
349 )
350 })?;
351 if parked.kind != kind {
352 return Err(ApiError::new(
353 StatusCode::BAD_REQUEST,
354 "ParkedKeyKindMismatch",
355 format!(
356 "the key was parked for {}, and this creates {}",
357 parked.kind.with_article(),
358 kind.with_article()
359 ),
360 ));
361 }
362 if let Creator::Operator { claim, .. } = &creator {
363 if parked.parked_at >= claim.created_at {
364 return Err(ApiError::new(
365 StatusCode::CONFLICT,
366 "ParkedKeyNewerThanRecord",
367 "the key was parked after the operator's record was written; a record \
368 admits only a key parked before it",
369 ));
370 }
371 }
372 Some(parked.key)
373 }
374 };
375
376 let mut provision = ProvisionRequest::new(label, None)
377 .with_kind(kind)
378 .with_fragments(fragments)
379 .created_by(creator);
380 if let Some(pool) = request.pool.clone() {
381 provision = provision.in_pool(pool);
382 }
383 if let Some(zone) = zone {
384 provision = provision.in_zone(zone);
385 }
386 if let Some(key) = account_key {
387 provision = provision.with_account_key(key);
388 }
389 if let Some(oidc) = request.oidc {
390 provision = provision.with_oidc(oidc);
391 }
392 let name = request.name.clone().unwrap_or_default();
393 let provision_now = |provision: ProvisionRequest| {
394 let registry = state.registry.clone();
395 tokio::task::spawn_blocking(move || registry.provision(provision))
396 };
397 let mut outcome = provision_now(provision.clone()).await;
398 let mut reread = false;
403 if matches!(outcome, Ok(Err(ProvisionError::NotAllowed { .. }))) {
404 debug!(name, "no allowance yet; re-reading the operator's records");
405 if state.nudge.nudge_and_wait(ALLOWANCE_REREAD_WAIT).await {
406 reread = true;
407 outcome = provision_now(provision).await;
408 }
409 }
410 match outcome {
411 Ok(Ok(provisioned)) => Ok(Json(json!({
412 "did": provisioned.account.did.as_str(),
413 "handle": provisioned.document.handle(),
414 "didDocument": provisioned.document,
415 "accountToken": provisioned.account_token,
418 "operator": provisioned.operator,
419 "parent": provisioned.account.parent.as_ref().map(|parent| parent.as_str()),
420 }))
421 .into_response()),
422 Ok(Err(err)) => {
423 info!(name, error = %err, "provisioning refused");
424 if let (ProvisionError::Store(didbot_pds::StoreError::AlreadyExists { did }), true) =
427 (&err, by_operator)
428 {
429 if let Some(parent) = state.registry.account(did).and_then(|held| held.parent) {
430 let refusal = ApiError::from(&err);
431 return Err(ApiError::new(
432 StatusCode::CONFLICT,
433 refusal.error(),
434 format!("{}; {parent} created it", refusal.message()),
435 ));
436 }
437 }
438 let refusal = ApiError::from(&err);
439 if reread && matches!(err, ProvisionError::NotAllowed { .. }) {
440 return Err(ApiError::new(
441 StatusCode::FORBIDDEN,
442 refusal.error(),
443 format!(
444 "{}; the operator's records were just re-read",
445 refusal.message()
446 ),
447 ));
448 }
449 Err(refusal)
450 }
451 Err(join_err) => {
452 Ok(blocking_task_failed("bot.did.createAccount", join_err).into_response())
453 }
454 }
455}
456
457fn fragments_of(request: &CreateAccountRequest) -> Result<BTreeMap<String, String>, ApiError> {
460 if request.name.is_some() && (request.pool.is_some() || !request.fragments.is_empty()) {
461 return Err(ApiError::bad_request(
462 "give a `name`, or a `pool` and its `fragments`, not both",
463 ));
464 }
465 if request.pool.is_none() && !request.fragments.is_empty() {
466 return Err(ApiError::bad_request(
467 "`fragments` fill a pool's naming slots, so they need a `pool`",
468 ));
469 }
470 let mut fragments = BTreeMap::new();
471 for fragment in &request.fragments {
472 if fragments
473 .insert(fragment.slot.clone(), fragment.value.clone())
474 .is_some()
475 {
476 return Err(ApiError::bad_request(format!(
477 "the {:?} slot is given twice",
478 fragment.slot
479 )));
480 }
481 }
482 Ok(fragments)
483}
484
485async fn operator_creator(state: &AppState, did: &str, name: &str) -> Result<Creator, ApiError> {
488 let (endpoint, pinned) = crate::operator_poll::resolve_endpoint(
489 &state.tree.client,
490 did,
491 state.tree.private_addresses(),
492 )
493 .await
494 .map_err(|error| operator_unreachable(did, &error))?;
495 let claim = crate::operator_poll::fetch_claim_from(&pinned, &endpoint, did, name)
496 .await
497 .map_err(|error| operator_unreachable(did, &error))?
498 .ok_or_else(|| {
499 info!(
500 creator = did,
501 name, "refused: no operator record for the name"
502 );
503 ApiError::new(
504 StatusCode::FORBIDDEN,
505 "OperatorRecordMissing",
506 format!("{did}'s repository holds no bot.did.operator record at {name:?}"),
507 )
508 })?;
509 Ok(Creator::Operator {
510 did: did.to_owned(),
511 claim,
512 })
513}
514
515async fn require_top_edge(
524 state: &AppState,
525 creator: &didbot_pds::HostedAccount,
526) -> Result<(), ApiError> {
527 let mut root = creator.clone();
528 while let Some(parent) = &root.parent {
529 match state.registry.account(parent.as_str()) {
530 Some(parent) => root = parent,
531 None => return Ok(()),
533 }
534 }
535 if root.did.as_str() == state.registry.service_did() {
536 return Ok(());
537 }
538 let account = &root;
539 let did = account.did.as_str();
540 let stands = match state.tree.top_edge(did) {
541 Some(stands) => stands,
542 None => {
543 let operator = state.registry.operator_did();
544 let read = match crate::operator_poll::resolve_endpoint(
545 &state.tree.client,
546 &operator,
547 state.tree.private_addresses(),
548 )
549 .await
550 {
551 Ok((endpoint, pinned)) => {
552 crate::operator_poll::fetch_claim_from(
553 &pinned,
554 &endpoint,
555 &operator,
556 &account.did.did().authority(),
557 )
558 .await
559 }
560 Err(error) => Err(error),
561 };
562 match read {
563 Ok(found) => {
564 let stands = found.is_some_and(|claim| claim.subject == did);
565 state.tree.remember_top_edge(did, stands);
566 stands
567 }
568 Err(error) => {
569 warn!(
570 did,
571 %error,
572 "could not re-read the operator's record for a root creator; keeping the poll's verdict"
573 );
574 state.tree.remember_top_edge(did, true);
578 true
579 }
580 }
581 }
582 };
583 if stands {
584 return Ok(());
585 }
586 info!(
587 creator = creator.did.as_str(),
588 root = did,
589 "refused: the operator's record for the root above this creator is gone"
590 );
591 Err(ApiError::new(
592 StatusCode::FORBIDDEN,
593 "OperatorRecordMissing",
594 format!("the operator's repository no longer holds a bot.did.operator record naming {did}"),
595 ))
596}
597
598async fn refuse_operators_name(state: &AppState, name: &str) -> Result<(), ApiError> {
608 let Ok(rkey) = didbot_claim_check::record_key(&didbot_identity::did::did_web_for_host(name))
609 else {
610 return Ok(());
612 };
613 let operator = state.registry.operator_did();
614 let stands = if state.tree.name_read_failed_lately() {
615 listing_holds(state, &rkey)
616 } else {
617 let read = match crate::operator_poll::resolve_endpoint(
618 &state.tree.client,
619 &operator,
620 state.tree.private_addresses(),
621 )
622 .await
623 {
624 Ok((endpoint, pinned)) => {
625 crate::operator_poll::fetch_claim_from(&pinned, &endpoint, &operator, &rkey).await
626 }
627 Err(error) => Err(error),
628 };
629 match read {
630 Ok(found) => found.is_some(),
631 Err(error) => {
632 warn!(
633 operator,
634 name,
635 %error,
636 "could not read the operator's record for a requested name; the last listing decides"
637 );
638 state.tree.remember_name_read_failed();
639 listing_holds(state, &rkey)
640 }
641 }
642 };
643 if !stands {
644 return Ok(());
645 }
646 info!(
647 name,
648 "refused: the operator's repository holds a record for the name"
649 );
650 Err(ApiError::new(
651 StatusCode::FORBIDDEN,
652 "OperatorRecordExists",
653 format!("the operator's repository holds bot.did.operator/{rkey}; only the operator creates {name}"),
654 ))
655}
656
657fn listing_holds(state: &AppState, rkey: &str) -> bool {
660 state.operator_records.read().is_some_and(|lines| {
661 lines
662 .iter()
663 .any(|line| line.uri.rsplit('/').next() == Some(rkey))
664 })
665}
666
667fn operator_unreachable(did: &str, error: &didbot_pds::ClaimFetchError) -> ApiError {
670 warn!(operator = did, %error, "could not read the operator's repository for a creation");
671 ApiError::new(
672 StatusCode::BAD_GATEWAY,
673 "OperatorUnreachable",
674 format!("could not read {did}'s repository: {error}"),
675 )
676}
677
678fn require_park_budget(state: &AppState, headers: &HeaderMap, peer: Peer) -> Result<(), ApiError> {
680 let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
681 if state.park_rate_limiter.check(&key) {
682 return Ok(());
683 }
684 info!(caller = %key, "park refused: too many from this address");
685 Err(ApiError::rate_limited("too many parkKey calls; slow down")
686 .with_header(header::RETRY_AFTER, PARK_RATE_WINDOW.as_secs().to_string()))
687}
688
689pub(crate) async fn park_key(
699 State(state): State<AppState>,
700 peer: Peer,
701 headers: HeaderMap,
702 XrpcBody(body): XrpcBody,
703) -> Response {
704 match park_key_inner(&state, peer, &headers, &body) {
705 Ok(response) => response,
706 Err(error) => error.into_response(),
707 }
708}
709
710fn park_key_inner(
711 state: &AppState,
712 peer: Peer,
713 headers: &HeaderMap,
714 body: &axum::body::Bytes,
715) -> Result<Response, ApiError> {
716 require_park_budget(state, headers, peer)?;
717 let request: ParkKeyRequest = crate::routes::parse_body(body)?;
718 let kind = parse_kind(&request.kind)?;
719 let (label, zone) = split_name(
720 state.registry.as_ref(),
721 &state.tree.reserved_labels,
722 &request.name,
723 )?;
724 state
727 .registry
728 .check_name(&label, &zone, kind)
729 .map_err(|error| {
730 info!(name = request.name, %error, "park refused: the name cannot be created");
731 ApiError::from(&error)
732 })?;
733 didbot_pds::account_key_curve(&request.key)
734 .map_err(|error| ApiError::bad_request(format!("key: {error}")))?;
735 let now = OffsetDateTime::now_utc();
736 match state
737 .tree
738 .parked
739 .park(&request.name, kind, &request.key, now)
740 {
741 Ok(parked) => {
742 info!(
743 name = parked.name,
744 fingerprint = parked.fingerprint,
745 "parked a key"
746 );
747 Ok(Json(json!({
748 "fingerprint": parked.fingerprint,
749 "expiresAt": didbot_schema::format::datetime::render(parked.expires_at),
750 }))
751 .into_response())
752 }
753 Err(expires_at) => {
754 debug!(
755 name = request.name,
756 "park refused: this key is already held"
757 );
758 Err(ApiError::new(
759 StatusCode::CONFLICT,
760 "KeyAlreadyParked",
761 format!(
762 "this key is already parked for {:?} until {}",
763 request.name,
764 didbot_schema::format::datetime::render(expires_at)
765 ),
766 ))
767 }
768 }
769}
770
771pub(crate) async fn describe_parked_key(
776 State(state): State<AppState>,
777 peer: Peer,
778 headers: HeaderMap,
779 query: Result<Query<ParkedKeyQuery>, axum::extract::rejection::QueryRejection>,
780) -> Response {
781 if let Err(error) = require_park_budget(&state, &headers, peer) {
782 return error.into_response();
783 }
784 let Query(query) = match query {
785 Ok(query) => query,
786 Err(rejection) => return ApiError::bad_request(rejection.body_text()).into_response(),
787 };
788 match state.tree.parked.describe(
789 &query.name,
790 query.fingerprint.as_deref(),
791 OffsetDateTime::now_utc(),
792 ) {
793 Some(parked) => Json(json!({
796 "name": parked.name,
797 "kind": parked.kind,
798 "fingerprint": parked.fingerprint,
799 "expiresAt": didbot_schema::format::datetime::render(parked.expires_at),
800 }))
801 .into_response(),
802 None => ApiError::new(
803 StatusCode::NOT_FOUND,
804 "ParkedKeyNotFound",
805 match &query.fingerprint {
806 Some(fingerprint) => format!(
807 "no key with fingerprint {fingerprint} is parked for {:?}",
808 query.name
809 ),
810 None => format!("no key is parked for {:?}", query.name),
811 },
812 )
813 .into_response(),
814 }
815}
816
817pub(crate) async fn create_session(
830 State(state): State<AppState>,
831 peer: Peer,
832 headers: HeaderMap,
833 query: Result<Query<SessionQuery>, axum::extract::rejection::QueryRejection>,
834) -> Response {
835 match create_session_inner(&state, peer, &headers, query).await {
836 Ok(response) => response,
837 Err(error) => error.into_response(),
838 }
839}
840
841async fn create_session_inner(
842 state: &AppState,
843 peer: Peer,
844 headers: &HeaderMap,
845 query: Result<Query<SessionQuery>, axum::extract::rejection::QueryRejection>,
846) -> Result<Response, ApiError> {
847 state.estop.check_issue(EstopRefusal::Token)?;
848 crate::routes::require_lifecycle(state, |p| p.provisions_accounts, "bot.did.createSession")?;
849 let key = caller_key(headers, peer.0, state.trust_forwarded_headers);
850 if !state.provision_rate_limiter.check(&key) {
851 info!(caller = %key, "session refused: too many attempts from this address");
852 return Err(ApiError::rate_limited(
853 "too many createAccount and createSession calls; slow down",
854 ));
855 }
856 let Query(query) = query.map_err(|rejection| ApiError::bad_request(rejection.body_text()))?;
857 let proof = prove(
858 state,
859 headers,
860 "bot.did.createSession",
861 false,
862 Named {
863 did: query.did.as_deref(),
864 field: NamedBy::Did,
865 },
866 )
867 .await?;
868 let did = match proof {
869 Proof::Key(did) | Proof::Oidc(did) => did,
870 Proof::Session(_) | Proof::Server => {
871 return Err(invalid_proof(
872 "createSession takes a key-signed JWT or an OpenID Connect token",
873 ))
874 }
875 };
876 if state.registry.account(&did).is_none() {
877 return Err(invalid_proof(format!(
878 "{did} is not an account this server hosts"
879 )));
880 }
881 let registry = state.registry.clone();
882 let issued = registry
883 .issue_session(&did)
884 .map_err(|error| ApiError::from(&error))?;
885 Ok(Json(json!({ "did": did, "accountToken": issued.token })).into_response())
886}