diff --git a/Cargo.lock b/Cargo.lock index 5d80a37..41610ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,6 +1048,41 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9" +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.70", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.70", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -1550,7 +1585,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1569,7 +1604,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -1946,6 +1981,12 @@ dependencies = [ "syn 2.0.70", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1968,6 +2009,17 @@ dependencies = [ "utf8_iter", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -1976,6 +2028,7 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", + "serde", ] [[package]] @@ -2368,6 +2421,7 @@ dependencies = [ "serde", "serde-email", "serde_json", + "serde_with", "sha2", "thiserror", "tokio", @@ -2385,6 +2439,7 @@ dependencies = [ "sea-orm", "serde", "server-migrations", + "thiserror", ] [[package]] @@ -2698,7 +2753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.2.6", ] [[package]] @@ -3577,7 +3632,7 @@ version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -3604,6 +3659,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.70", +] + [[package]] name = "server-migrations" version = "0.1.0" @@ -3786,7 +3871,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.2.6", "log", "memchr", "once_cell", @@ -4342,7 +4427,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] @@ -4353,7 +4438,7 @@ version = "0.22.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", diff --git a/crates/naja-cli/src/commands/aspe/delete.rs b/crates/naja-cli/src/commands/aspe/delete.rs index 883efe9..be915d5 100644 --- a/crates/naja-cli/src/commands/aspe/delete.rs +++ b/crates/naja-cli/src/commands/aspe/delete.rs @@ -1,9 +1,11 @@ +use std::str::FromStr; + use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit as _}; use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _}; use naja_lib::{ aspe::{ - requests::{AspeRequest, AspeRequestType, AspeRequestVariant}, + requests::{AspeRequest, AspeRequestType, AspeRequestVariant, AspeUri}, AspeRequestFailure, AspeServer, }, keys::AspKey, @@ -82,7 +84,7 @@ impl NajaSubcommand for AspeDeleteCommand { version: 0, r#type: AspeRequestType::Request, request: AspeRequestVariant::Delete { - aspe_uri: format!("aspe:{}:{}", server.host, key.fingerprint), + aspe_uri: AspeUri::from_str(&format!("aspe:{}:{}", server.host, key.fingerprint))?, }, }; diff --git a/crates/naja-cli/src/commands/aspe/upload.rs b/crates/naja-cli/src/commands/aspe/upload.rs index 5ce6acf..355f343 100644 --- a/crates/naja-cli/src/commands/aspe/upload.rs +++ b/crates/naja-cli/src/commands/aspe/upload.rs @@ -1,9 +1,11 @@ +use std::str::FromStr as _; + use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit as _}; use anyhow::{anyhow, bail, Context}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher as _}; use naja_lib::{ aspe::{ - requests::{AspeRequest, AspeRequestType, AspeRequestVariant}, + requests::{AspeRequest, AspeRequestType, AspeRequestVariant, AspeUri}, AspeFetchFailure, AspeRequestFailure, AspeServer, }, hex_color::HexColor, @@ -149,7 +151,7 @@ impl NajaSubcommand for AspeUploadCommand { r#type: AspeRequestType::Request, request: AspeRequestVariant::Update { profile_jws: encoded_profile, - aspe_uri: format!("aspe:{}:{}", server.host, fingerprint), + aspe_uri: AspeUri::from_str(&format!("aspe:{}:{}", server.host, key.fingerprint))?, }, }, }; diff --git a/crates/naja-cli/src/commands/profiles/import.rs b/crates/naja-cli/src/commands/profiles/import.rs index 87ebe15..82a4742 100644 --- a/crates/naja-cli/src/commands/profiles/import.rs +++ b/crates/naja-cli/src/commands/profiles/import.rs @@ -61,7 +61,7 @@ impl NajaSubcommand for ProfilesImportCommand { _ => (self.profile, None), }; - let (key, profile) = AriadneSignatureProfile::decode_and_verify(&profile, fingerprint) + let (key, profile) = AriadneSignatureProfile::decode_and_verify(&profile, fingerprint.as_deref()) .context("The provided or fetched profile was invalid and unable to be imported")?; let txn = state.db.begin().await?; diff --git a/crates/naja-lib/Cargo.toml b/crates/naja-lib/Cargo.toml index 697967d..95b1610 100644 --- a/crates/naja-lib/Cargo.toml +++ b/crates/naja-lib/Cargo.toml @@ -17,6 +17,7 @@ reqwest = "0.12.5" serde = { version = "1.0.204", features = ["derive"] } serde-email = "3.0.1" serde_json = { version = "1.0.120", features = ["preserve_order"] } +serde_with = "3.9.0" sha2 = "0.10.8" thiserror = "1.0.61" tokio = { version = "1.39.1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/naja-lib/src/aspe/requests.rs b/crates/naja-lib/src/aspe/requests.rs index 409f9cd..ebac8af 100644 --- a/crates/naja-lib/src/aspe/requests.rs +++ b/crates/naja-lib/src/aspe/requests.rs @@ -1,7 +1,59 @@ +use std::{fmt::Display, str::FromStr}; + +use anyhow::bail; use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use url::{Host, Url}; use crate::utils::jwt::JwtSerializable; +const BASE32_CHARS: [char; 32] = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','2','3','4','5','6','7']; + +#[derive(Debug, PartialEq, SerializeDisplay, DeserializeFromStr)] +pub struct AspeUri { + /// The domain of the ASPE server to fetch this profile from + domain: Host, + /// The fingerprint of the profile to fetch (in uppercase) + fingerprint: String +} + +impl AspeUri { + pub fn domain(&self) -> &Host { &self.domain } + pub fn fingerprint(&self) -> &String { &self.fingerprint } +} + +impl Display for AspeUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "aspe:{}:{}", self.domain, self.fingerprint) + } +} + +impl FromStr for AspeUri { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let url = Url::parse(s)?; + + if url.scheme() != "aspe" { + bail!("Provided uri did not have a scheme of 'aspe'"); + } + + let Some((domain, fingerprint)) = url.path().split_once(':') else { + bail!("Uri did not have the correct amount of parts"); + }; + let fingerprint = fingerprint.to_uppercase(); + + if fingerprint.len() != 26 || !fingerprint.chars().all(|c| BASE32_CHARS.contains(&c)) { + bail!("Fingerprint was not formatted correctly"); + } + + Ok(Self { + domain: Host::parse(domain)?, + fingerprint + }) + } +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub enum AspeRequestType { @@ -19,11 +71,11 @@ pub enum AspeRequestVariant { #[serde(rename = "http://ariadne.id/profile_jws")] profile_jws: String, #[serde(rename = "http://ariadne.id/aspe_uri")] - aspe_uri: String, + aspe_uri: AspeUri, }, Delete { #[serde(rename = "http://ariadne.id/aspe_uri")] - aspe_uri: String, + aspe_uri: AspeUri, }, } @@ -41,9 +93,11 @@ impl JwtSerializable for AspeRequest {} #[cfg(test)] mod tests { - use josekit::jwk::Jwk; + use std::str::FromStr; - use crate::{keys::AspKey, utils::jwt::JwtSerialize}; +use josekit::jwk::Jwk; + + use crate::{aspe::requests::AspeUri, keys::AspKey, utils::jwt::JwtSerialize}; use super::{AspeRequest, AspeRequestType, AspeRequestVariant}; @@ -107,7 +161,7 @@ mod tests { r#type: AspeRequestType::Request, request: AspeRequestVariant::Update { profile_jws: crate::test_constants::PROFILE.to_string(), - aspe_uri: crate::test_constants::ASPE_URI.to_string(), + aspe_uri: AspeUri::from_str(crate::test_constants::ASPE_URI).unwrap(), }, }; let encoded = request.encode_and_sign(&key); @@ -141,7 +195,7 @@ mod tests { r#type: AspeRequestType::Request, request: AspeRequestVariant::Update { profile_jws: crate::test_constants::PROFILE.to_string(), - aspe_uri: crate::test_constants::ASPE_URI.to_string(), + aspe_uri: AspeUri::from_str(crate::test_constants::ASPE_URI).unwrap(), }, }, "Decoded update request should be correct" @@ -157,7 +211,7 @@ mod tests { version: 0, r#type: AspeRequestType::Request, request: AspeRequestVariant::Delete { - aspe_uri: crate::test_constants::ASPE_URI.to_string(), + aspe_uri: AspeUri::from_str(crate::test_constants::ASPE_URI).unwrap(), }, }; let encoded = request.encode_and_sign(&key); @@ -190,7 +244,7 @@ mod tests { version: 0, r#type: AspeRequestType::Request, request: AspeRequestVariant::Delete { - aspe_uri: crate::test_constants::ASPE_URI.to_string(), + aspe_uri: AspeUri::from_str(crate::test_constants::ASPE_URI).unwrap(), }, }, "Decoded delete request should be correct" diff --git a/crates/naja-lib/src/utils/jwt.rs b/crates/naja-lib/src/utils/jwt.rs index 3ff8614..1ba2efd 100644 --- a/crates/naja-lib/src/utils/jwt.rs +++ b/crates/naja-lib/src/utils/jwt.rs @@ -22,13 +22,12 @@ pub trait JwtSerializable {} pub trait JwtSerialize { fn encode_and_sign(&self, key: &AspKey) -> Result; - fn decode_and_verify( + fn decode_and_verify( jwt: &str, - expected_fingerprint: Option, + expected_fingerprint: Option<&str>, ) -> Result<(AspKey, Box), JwtDeserializationError> where - Self: for<'de> Deserialize<'de> + JwtSerializable, - S: AsRef; + Self: for<'de> Deserialize<'de> + JwtSerializable; } #[derive(Error, Debug)] @@ -88,13 +87,12 @@ impl Deserialize<'de>> JwtSerialize fo .or(Err(JwtSerializationError::SerializationError)) } - fn decode_and_verify( + fn decode_and_verify( jwt: &str, - expected_fingerprint: Option, + expected_fingerprint: Option<&str>, ) -> Result<(AspKey, Box), JwtDeserializationError> where Self: for<'de> serde::Deserialize<'de> + JwtSerializable, - S: AsRef, { // Decode the header and check if the key id is correct let header = jwt::decode_header(jwt).or(Err(JwtDeserializationError::HeaderDecodeError))?; @@ -124,7 +122,7 @@ impl Deserialize<'de>> JwtSerialize fo if key.fingerprint != key_id { return Err(JwtDeserializationError::MalformedJwkError); } - if expected_fingerprint.is_some_and(|e| e.as_ref() != key_id) { + if expected_fingerprint.is_some_and(|e| e != key_id) { return Err(JwtDeserializationError::WrongJwkError); }; diff --git a/crates/naja-server/Cargo.toml b/crates/naja-server/Cargo.toml index 9bf08e7..c1fbac9 100644 --- a/crates/naja-server/Cargo.toml +++ b/crates/naja-server/Cargo.toml @@ -14,3 +14,4 @@ serde = { version = "1.0.204", features = ["derive"] } naja-lib = { path = "../naja-lib" } migrations = { path = "../server-migrations", package = "server-migrations" } env_logger = "0.11.3" +thiserror = "1.0.63" diff --git a/crates/naja-server/src/extractors.rs b/crates/naja-server/src/extractors.rs new file mode 100644 index 0000000..b8a60f0 --- /dev/null +++ b/crates/naja-server/src/extractors.rs @@ -0,0 +1,86 @@ +use std::{future::Future, pin::Pin}; + +use actix_web::{ + http::{header, StatusCode}, FromRequest, ResponseError +}; +use naja_lib::{aspe::requests::AspeRequest, keys::AspKey, utils::jwt::{JwtDeserializationError, JwtSerialize}}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AspeRequestParseError { + #[error(r#"Request content type was not set to "application/asp+jwt; charset=utf-8""#)] + ContentTypeMismatch, + #[error("Unable to parse request body as string")] + StringParseFailure, + #[error("The request JWT header was formatted incorrectly")] + InvalidJwtHeader, + #[error("The request JWT was unable to be verified")] + VerificationError, + #[error("The request JWT was unable to be decoded")] + DecodeError, + #[error("The request JWT had a different key fingerprint and key id")] + KeyIdMismatch, + #[error("An unknown error has occurred while parsing the request body JWT")] + UnknownParsingFailure, +} + +impl ResponseError for AspeRequestParseError { + fn status_code(&self) -> actix_web::http::StatusCode { + match self { + AspeRequestParseError::ContentTypeMismatch => StatusCode::UNSUPPORTED_MEDIA_TYPE, + AspeRequestParseError::StringParseFailure => StatusCode::BAD_REQUEST, + AspeRequestParseError::InvalidJwtHeader => StatusCode::BAD_REQUEST, + AspeRequestParseError::VerificationError => StatusCode::BAD_REQUEST, + AspeRequestParseError::DecodeError => StatusCode::BAD_REQUEST, + AspeRequestParseError::KeyIdMismatch => StatusCode::BAD_REQUEST, + AspeRequestParseError::UnknownParsingFailure => StatusCode::BAD_REQUEST, + } + } + + // TODO: Implement RFC9457 +} + +pub struct AspeRequestBody { + pub request: AspeRequest, + pub key: AspKey +} + +impl FromRequest for AspeRequestBody { + type Error = AspeRequestParseError; + + type Future = Pin>>>; + + fn from_request( + req: &actix_web::HttpRequest, + payload: &mut actix_web::dev::Payload, + ) -> Self::Future { + let req = req.clone(); + let mut payload = payload.take(); + + Box::pin(async move { + if req + .headers() + .get(header::CONTENT_TYPE) + .is_some_and(|v| v == "application/asp+jwt; charset=utf-8") + { + return Err(AspeRequestParseError::ContentTypeMismatch); + } + + let Ok(string_body) = String::from_request(&req, &mut payload).await else { + return Err(AspeRequestParseError::StringParseFailure); + }; + + match AspeRequest::decode_and_verify( + &string_body, + None + ) { + Ok((key, request)) => Ok(Self { key, request: *request }), + Err(JwtDeserializationError::HeaderDecodeError) => Err(AspeRequestParseError::InvalidJwtHeader), + Err(JwtDeserializationError::JwkUsageError) => Err(AspeRequestParseError::VerificationError), + Err(JwtDeserializationError::JwtDecodeError) => Err(AspeRequestParseError::DecodeError), + Err(JwtDeserializationError::MalformedJwkError) => Err(AspeRequestParseError::KeyIdMismatch), + Err(JwtDeserializationError::WrongJwkError) => Err(AspeRequestParseError::UnknownParsingFailure), + } + }) + } +} diff --git a/crates/naja-server/src/main copy.rs b/crates/naja-server/src/main copy.rs new file mode 100644 index 0000000..31754ab --- /dev/null +++ b/crates/naja-server/src/main copy.rs @@ -0,0 +1,233 @@ +#[allow(warnings)] +mod entities; + +use std::{fs, io, path::PathBuf}; + +use actix_web::{get, http::header, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; +use clap::Parser; +use entities::{prelude::*, profiles}; +use env_logger::Env; +use migrations::{Migrator, MigratorTrait as _}; +use naja_lib::{ + aspe::requests::{AspeRequest, AspeRequestVariant}, + profiles::AriadneSignatureProfile, + utils::jwt::{JwtDeserializationError, JwtSerialize}, +}; +use sea_orm::{ + ActiveValue, Database, DatabaseConnection, DbErr, EntityTrait, +}; +use serde::Serialize; + +#[derive(Serialize)] +struct VersionResponse { + name: &'static str, + version: &'static str, + repository: &'static str, + homepage: &'static str, +} + +#[derive(Debug, Clone)] +struct ServerState { + db: DatabaseConnection, + domain: String, +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// The path to store data in + #[arg(long, env = "NAJA_DATA_PATH")] + data_path: PathBuf, + /// The bind address to listen on + #[arg(long, env = "NAJA_BIND_ADDRESS")] + bind_address: String, + /// The port to listen on + #[arg(long, env = "NAJA_BIND_PORT", default_value = "80")] + port: u16, + /// The front-facing domain for this server + #[arg(long, env = "NAJA_DOMAIN")] + domain: String, +} + +#[actix_web::main] +async fn main() -> io::Result<()> { + let args = Args::parse(); + + fs::create_dir_all(&args.data_path)?; + let db = Database::connect(format!( + "sqlite://{}?mode=rwc", + args.data_path.join("db.sqlite").to_str().unwrap() + )) + .await + .expect("Unable to connect to sqlite database, are permissions correct?"); + + Migrator::up(&db, None) + .await + .expect("Unable to migrate database"); + + let state = ServerState { + db, + domain: args.domain, + }; + + env_logger::init_from_env(Env::default().default_filter_or("info")); + HttpServer::new(move || { + App::new() + .wrap(Logger::default()) + .app_data(web::Data::new(state.clone())) + .service(version) + .service(get_by_fingerprint) + .service(post_request) + }) + .bind_auto_h2c((args.bind_address, args.port))? + .run() + .await +} + +#[get("/.well-known/aspe/version")] +async fn version(accept: web::Header) -> impl Responder { + match accept.to_string().as_str() { + "text/html" | "text/plain" => HttpResponse::Ok().body(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )), + _ => HttpResponse::Ok().json(VersionResponse { + name: env!("CARGO_PKG_NAME"), + version: env!("CARGO_PKG_VERSION"), + homepage: env!("CARGO_PKG_HOMEPAGE"), + repository: env!("CARGO_PKG_REPOSITORY"), + }), + } +} + +#[get("/.well-known/aspe/id/{fingerprint}")] +async fn get_by_fingerprint( + state: web::Data, + fingerprint: web::Path, +) -> impl Responder { + match Profiles::find_by_id(fingerprint.as_str()) + .one(&state.db) + .await + { + Ok(Some(profile)) => HttpResponse::Ok() + .content_type("application/asp+jwt; charset=UTF-8") + .body(profile.jwt), + Ok(None) => HttpResponse::NotFound().finish(), + Err(e) => { + eprintln!("{}", e); + HttpResponse::InternalServerError().finish() + } + } +} + +#[post("/.well-known/aspe/post")] +async fn post_request( + state: web::Data, + data: String, + content_type: web::Header, +) -> impl Responder { + if content_type.to_string().as_str() != "application/asp+jwt; charset=utf-8" { + return HttpResponse::BadRequest() + .body("Content type header was not set to \"application/asp+jwt; charset=UTF-8\""); + } + + match AspeRequest::decode_and_verify::(&data, None) { + Ok((key, request)) => { + if let AspeRequestVariant::Update { aspe_uri, .. } + | AspeRequestVariant::Delete { aspe_uri } = &request.request + { + if aspe_uri != &format!("aspe:{}:{}", &state.domain, &key.fingerprint) { + return HttpResponse::BadRequest().body("ASPE uri did not match key and domain"); + } + } + + match &request.request { + AspeRequestVariant::Create { profile_jws } + | AspeRequestVariant::Update { profile_jws, .. } => { + match AriadneSignatureProfile::decode_and_verify( + profile_jws, + Some(&key.fingerprint), + ) { + Ok(_) if matches!(request.request, AspeRequestVariant::Create { .. }) => { + match Profiles::insert(profiles::ActiveModel { + fingerprint: ActiveValue::Set(key.fingerprint), + jwt: ActiveValue::Set(profile_jws.to_owned()), + }) + .exec(&state.db) + .await + { + Ok(_) => HttpResponse::Created().finish(), + Err(e) => { + eprintln!("{e}"); + HttpResponse::InternalServerError().finish() + } + } + } + Ok(_) => { + // Must be AspeRequestVariant::Update { .. } + match Profiles::update(profiles::ActiveModel { + fingerprint: ActiveValue::Unchanged(key.fingerprint.to_owned()), + jwt: ActiveValue::Set(profile_jws.to_owned()), + }) + .exec(&state.db) + .await + { + Ok(_) => HttpResponse::Created().finish(), + Err(DbErr::RecordNotUpdated) => { + HttpResponse::BadRequest() + .body("Profile does not already exist") + } + Err(e) => { + eprintln!("{e}"); + HttpResponse::InternalServerError().finish() + } + } + } + Err(e) => HttpResponse::BadRequest().body(match e { + JwtDeserializationError::HeaderDecodeError => { + "Unable to decode profile JWT header" + } + JwtDeserializationError::MalformedJwkError => { + "Profile JWT JWK was malformed" + } + JwtDeserializationError::WrongJwkError => { + "The wrong JWK was encoded inside the profile JWT" + } + JwtDeserializationError::JwtDecodeError => { + "Unable to decoded profile JWT" + } + JwtDeserializationError::JwkUsageError => { + "Profile JWT JWK had invalid usage claim(s)" + } + }), + } + } + AspeRequestVariant::Delete { .. } => { + match Profiles::delete_by_id(&key.fingerprint) + .exec(&state.db) + .await + { + Ok(_) => HttpResponse::Ok().finish(), + Err(DbErr::RecordNotFound(_)) => { + HttpResponse::NotFound().body("Profile does not exist") + } + Err(e) => { + eprintln!("{e}"); + HttpResponse::InternalServerError().finish() + } + } + } + } + } + Err(e) => HttpResponse::BadRequest().body(match e { + JwtDeserializationError::HeaderDecodeError => "Unable to decode request JWT header", + JwtDeserializationError::MalformedJwkError => "Request JWT JWK was malformed", + JwtDeserializationError::WrongJwkError => { + "The wrong JWK was encoded inside the request JWT" + } + JwtDeserializationError::JwtDecodeError => "Unable to decoded request JWT", + JwtDeserializationError::JwkUsageError => "Request JWT JWK had invalid usage claim(s)", + }), + } +} diff --git a/crates/naja-server/src/main.rs b/crates/naja-server/src/main.rs index 115c30d..2ab51a0 100644 --- a/crates/naja-server/src/main.rs +++ b/crates/naja-server/src/main.rs @@ -1,9 +1,12 @@ #[allow(warnings)] mod entities; +mod extractors; use std::{fs, io, path::PathBuf}; -use actix_web::{get, http::header, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; +use actix_web::{ + get, http::header, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder, +}; use clap::Parser; use entities::{prelude::*, profiles}; use env_logger::Env; @@ -13,9 +16,7 @@ use naja_lib::{ profiles::AriadneSignatureProfile, utils::jwt::{JwtDeserializationError, JwtSerialize}, }; -use sea_orm::{ - ActiveValue, Database, DatabaseConnection, DbErr, EntityTrait, -}; +use sea_orm::{ActiveValue, Database, DatabaseConnection, DbErr, EntityTrait}; use serde::Serialize; #[derive(Serialize)] @@ -124,7 +125,7 @@ async fn get_by_fingerprint( #[post("/.well-known/aspe/post")] async fn post_request( state: web::Data, - data: String, + aspe_body: extractors::AspeRequestBody, content_type: web::Header, ) -> impl Responder { if content_type.to_string().as_str() != "application/asp+jwt; charset=utf-8" { @@ -132,102 +133,15 @@ async fn post_request( .body("Content type header was not set to \"application/asp+jwt; charset=UTF-8\""); } - match AspeRequest::decode_and_verify::(&data, None) { - Ok((key, request)) => { - if let AspeRequestVariant::Update { aspe_uri, .. } - | AspeRequestVariant::Delete { aspe_uri } = &request.request - { - if aspe_uri != &format!("aspe:{}:{}", &state.domain, &key.fingerprint) { - return HttpResponse::BadRequest().body("ASPE uri did not match key and domain"); - } - } - - match &request.request { - AspeRequestVariant::Create { profile_jws } - | AspeRequestVariant::Update { profile_jws, .. } => { - match AriadneSignatureProfile::decode_and_verify( - profile_jws, - Some(&key.fingerprint), - ) { - Ok(_) if matches!(request.request, AspeRequestVariant::Create { .. }) => { - match Profiles::insert(profiles::ActiveModel { - fingerprint: ActiveValue::Set(key.fingerprint), - jwt: ActiveValue::Set(profile_jws.to_owned()), - }) - .exec(&state.db) - .await - { - Ok(_) => HttpResponse::Created().finish(), - Err(e) => { - eprintln!("{e}"); - HttpResponse::InternalServerError().finish() - } - } - } - Ok(_) => { - // Must be AspeRequestvariant::Update { .. } - match Profiles::update(profiles::ActiveModel { - fingerprint: ActiveValue::Unchanged(key.fingerprint.to_owned()), - jwt: ActiveValue::Set(profile_jws.to_owned()), - }) - .exec(&state.db) - .await - { - Ok(_) => HttpResponse::Created().finish(), - Err(DbErr::RecordNotUpdated) => { - HttpResponse::BadRequest() - .body("Profile does not already exist") - } - Err(e) => { - eprintln!("{e}"); - HttpResponse::InternalServerError().finish() - } - } - } - Err(e) => HttpResponse::BadRequest().body(match e { - JwtDeserializationError::HeaderDecodeError => { - "Unable to decode profile JWT header" - } - JwtDeserializationError::MalformedJwkError => { - "Profile JWT JWK was malformed" - } - JwtDeserializationError::WrongJwkError => { - "The wrong JWK was encoded inside the profile JWT" - } - JwtDeserializationError::JwtDecodeError => { - "Unable to decoded profile JWT" - } - JwtDeserializationError::JwkUsageError => { - "Profile JWT JWK had invalid usage claim(s)" - } - }), - } - } - AspeRequestVariant::Delete { .. } => { - match Profiles::delete_by_id(&key.fingerprint) - .exec(&state.db) - .await - { - Ok(_) => HttpResponse::Ok().finish(), - Err(DbErr::RecordNotFound(_)) => { - HttpResponse::NotFound().body("Profile does not exist") - } - Err(e) => { - eprintln!("{e}"); - HttpResponse::InternalServerError().finish() - } - } - } - } + if let AspeRequestVariant::Update { aspe_uri, .. } | AspeRequestVariant::Delete { aspe_uri } = + &aspe_body.request.request + { + if aspe_uri.domain().to_string() != state.domain || *aspe_uri.fingerprint() != aspe_body.key.fingerprint { + return HttpResponse::BadRequest().body("ASPE uri did not match key and domain"); } - Err(e) => HttpResponse::BadRequest().body(match e { - JwtDeserializationError::HeaderDecodeError => "Unable to decode request JWT header", - JwtDeserializationError::MalformedJwkError => "Request JWT JWK was malformed", - JwtDeserializationError::WrongJwkError => { - "The wrong JWK was encoded inside the request JWT" - } - JwtDeserializationError::JwtDecodeError => "Unable to decoded request JWT", - JwtDeserializationError::JwkUsageError => "Request JWT JWK had invalid usage claim(s)", - }), } + + + + todo!(); }