this repo has no description
1use crate::api::ApiError;
2use crate::auth::BearerAuthAllowDeactivated;
3use crate::circuit_breaker::with_circuit_breaker;
4use crate::plc::{PlcClient, PlcError, PlcService, create_update_op, sign_operation};
5use crate::state::AppState;
6use axum::{
7 Json,
8 extract::State,
9 http::StatusCode,
10 response::{IntoResponse, Response},
11};
12use chrono::Utc;
13use k256::ecdsa::SigningKey;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::collections::HashMap;
17use tracing::{error, info};
18
19#[derive(Debug, Deserialize)]
20#[serde(rename_all = "camelCase")]
21pub struct SignPlcOperationInput {
22 pub token: Option<String>,
23 pub rotation_keys: Option<Vec<String>>,
24 pub also_known_as: Option<Vec<String>>,
25 pub verification_methods: Option<HashMap<String, String>>,
26 pub services: Option<HashMap<String, ServiceInput>>,
27}
28
29#[derive(Debug, Deserialize, Clone)]
30pub struct ServiceInput {
31 #[serde(rename = "type")]
32 pub service_type: String,
33 pub endpoint: String,
34}
35
36#[derive(Debug, Serialize)]
37pub struct SignPlcOperationOutput {
38 pub operation: Value,
39}
40
41pub async fn sign_plc_operation(
42 State(state): State<AppState>,
43 auth: BearerAuthAllowDeactivated,
44 Json(input): Json<SignPlcOperationInput>,
45) -> Response {
46 let auth_user = auth.0;
47 if let Err(e) = crate::auth::scope_check::check_identity_scope(
48 auth_user.is_oauth,
49 auth_user.scope.as_deref(),
50 crate::oauth::scopes::IdentityAttr::Wildcard,
51 ) {
52 return e;
53 }
54 let did = &auth_user.did;
55 if did.starts_with("did:web:") {
56 return ApiError::InvalidRequest(
57 "PLC operations are only valid for did:plc identities".into(),
58 )
59 .into_response();
60 }
61 let token = match &input.token {
62 Some(t) => t,
63 None => {
64 return ApiError::InvalidRequest(
65 "Email confirmation token required to sign PLC operations".into(),
66 )
67 .into_response();
68 }
69 };
70 let user = match sqlx::query!("SELECT id FROM users WHERE did = $1", did)
71 .fetch_optional(&state.db)
72 .await
73 {
74 Ok(Some(row)) => row,
75 _ => {
76 return ApiError::AccountNotFound.into_response();
77 }
78 };
79 let token_row = match sqlx::query!(
80 "SELECT id, expires_at FROM plc_operation_tokens WHERE user_id = $1 AND token = $2",
81 user.id,
82 token
83 )
84 .fetch_optional(&state.db)
85 .await
86 {
87 Ok(Some(row)) => row,
88 Ok(None) => {
89 return ApiError::InvalidToken(Some("Invalid or expired token".into())).into_response();
90 }
91 Err(e) => {
92 error!("DB error: {:?}", e);
93 return ApiError::InternalError(None).into_response();
94 }
95 };
96 if Utc::now() > token_row.expires_at {
97 let _ = sqlx::query!(
98 "DELETE FROM plc_operation_tokens WHERE id = $1",
99 token_row.id
100 )
101 .execute(&state.db)
102 .await;
103 return ApiError::ExpiredToken(Some("Token has expired".into())).into_response();
104 }
105 let key_row = match sqlx::query!(
106 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1",
107 user.id
108 )
109 .fetch_optional(&state.db)
110 .await
111 {
112 Ok(Some(row)) => row,
113 _ => {
114 return ApiError::InternalError(Some("User signing key not found".into()))
115 .into_response();
116 }
117 };
118 let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version)
119 {
120 Ok(k) => k,
121 Err(e) => {
122 error!("Failed to decrypt user key: {}", e);
123 return ApiError::InternalError(None).into_response();
124 }
125 };
126 let signing_key = match SigningKey::from_slice(&key_bytes) {
127 Ok(k) => k,
128 Err(e) => {
129 error!("Failed to create signing key: {:?}", e);
130 return ApiError::InternalError(None).into_response();
131 }
132 };
133 let plc_client = PlcClient::with_cache(None, Some(state.cache.clone()));
134 let did_clone = did.clone();
135 let last_op = match with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
136 plc_client.get_last_op(&did_clone).await
137 })
138 .await
139 {
140 Ok(op) => op,
141 Err(e) => return ApiError::from(e).into_response(),
142 };
143 if last_op.is_tombstone() {
144 return ApiError::from(PlcError::Tombstoned).into_response();
145 }
146 let services = input.services.map(|s| {
147 s.into_iter()
148 .map(|(k, v)| {
149 (
150 k,
151 PlcService {
152 service_type: v.service_type,
153 endpoint: v.endpoint,
154 },
155 )
156 })
157 .collect()
158 });
159 let unsigned_op = match create_update_op(
160 &last_op,
161 input.rotation_keys,
162 input.verification_methods,
163 input.also_known_as,
164 services,
165 ) {
166 Ok(op) => op,
167 Err(PlcError::Tombstoned) => {
168 return ApiError::InvalidRequest("Cannot update tombstoned DID".into()).into_response();
169 }
170 Err(e) => {
171 error!("Failed to create PLC operation: {:?}", e);
172 return ApiError::InternalError(None).into_response();
173 }
174 };
175 let signed_op = match sign_operation(&unsigned_op, &signing_key) {
176 Ok(op) => op,
177 Err(e) => {
178 error!("Failed to sign PLC operation: {:?}", e);
179 return ApiError::InternalError(None).into_response();
180 }
181 };
182 let _ = sqlx::query!(
183 "DELETE FROM plc_operation_tokens WHERE id = $1",
184 token_row.id
185 )
186 .execute(&state.db)
187 .await;
188 info!("Signed PLC operation for user {}", did);
189 (
190 StatusCode::OK,
191 Json(SignPlcOperationOutput {
192 operation: signed_op,
193 }),
194 )
195 .into_response()
196}