this repo has no description
at main 6.0 kB view raw
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}