From 2ce139b1c9a39e6b4104b279d3ad25ea625bcd41 Mon Sep 17 00:00:00 2001 From: Ripperoni Date: Mon, 3 Feb 2025 21:07:55 -0800 Subject: [PATCH] Redid createAccount API call - Now accepts already existing DIDs - Better error handling on it - Easier to understand --- rsky-pds/Cargo.toml | 2 + .../apis/com/atproto/server/create_account.rs | 369 +++++++++++------- rsky-pds/src/apis/com/atproto/server/mod.rs | 131 +------ rsky-pds/src/apis/mod.rs | 37 +- rsky-pds/src/main.rs | 3 + rsky-pds/src/plc/mod.rs | 2 +- rsky-pds/src/plc/operations.rs | 64 ++- 7 files changed, 323 insertions(+), 285 deletions(-) diff --git a/rsky-pds/Cargo.toml b/rsky-pds/Cargo.toml index 93859c8..03bbb77 100644 --- a/rsky-pds/Cargo.toml +++ b/rsky-pds/Cargo.toml @@ -75,6 +75,8 @@ webpki-roots = { version = "0.26.0-alpha.1" } lexicon_cid = { package = "cid", version = "0.10.1", features = ["serde-codec"] } async-recursion = "1.1.1" once_cell = "1.19.0" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" [dependencies.rocket_sync_db_pools] diff --git a/rsky-pds/src/apis/com/atproto/server/create_account.rs b/rsky-pds/src/apis/com/atproto/server/create_account.rs index 0599f19..e081b40 100644 --- a/rsky-pds/src/apis/com/atproto/server/create_account.rs +++ b/rsky-pds/src/apis/com/atproto/server/create_account.rs @@ -1,15 +1,17 @@ use crate::account_manager::helpers::account::AccountStatus; use crate::account_manager::{AccountManager, CreateAccountOpts}; -use crate::apis::com::atproto::server::safe_resolve_did_doc; +use crate::apis::com::atproto::server::{ + encode_did_key, get_keys_from_private_key_str, safe_resolve_did_doc, +}; use crate::apis::ApiError; use crate::auth_verifier::UserDidAuthOptional; use crate::config::ServerConfig; use crate::handle::{normalize_and_validate_handle, HandleValidationContext, HandleValidationOpts}; +use crate::plc::operations::{create_op, CreateAtprotoOpInput}; +use crate::plc::types::{CompatibleOpOrTombstone, OpOrTombstone, Operation}; use crate::repo::aws::s3::S3BlobStore; use crate::repo::ActorStore; -use crate::storage::readable_blockstore::ReadableBlockstore; -use crate::storage::sql_repo::SqlRepoReader; -use crate::SharedIdResolver; +use crate::{plc, SharedIdResolver}; use crate::SharedSequencer; use aws_config::SdkConfig; use email_address::*; @@ -18,79 +20,111 @@ use rocket::State; use rsky_lexicon::com::atproto::server::{CreateAccountInput, CreateAccountOutput}; use secp256k1::{Keypair, Secp256k1, SecretKey}; use std::env; -use std::fmt::Debug; +use crate::common::env::env_str; -#[allow(unused_assignments)] -async fn inner_server_create_account( - mut body: CreateAccountInput, +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct TransformedCreateAccountInput { + pub email: String, + pub handle: String, + pub did: String, + pub invite_code: Option, + pub password: String, + pub signing_key: Keypair, + pub plc_op: Option, + pub deactivated: bool, +} + +//TODO: Potential for taking advantage of async better +#[tracing::instrument(skip_all)] +#[rocket::post( + "/xrpc/com.atproto.server.createAccount", + format = "json", + data = "" +)] +pub async fn server_create_account( + body: Json, + auth: UserDidAuthOptional, sequencer: &State, s3_config: &State, + cfg: &State, id_resolver: &State, -) -> Result { - let CreateAccountInput { +) -> Result, ApiError> { + tracing::info!("Creating new user account"); + let requester = match auth.access { + Some(access) if access.credentials.is_some() => access.credentials.unwrap().iss, + _ => None, + }; + let TransformedCreateAccountInput { email, handle, - mut did, // @TODO: Allow people to bring their own DID + did, invite_code, password, - .. - } = body.clone(); - let deactivated = false; - if let Some(input_recovery_key) = &body.recovery_key { - body.recovery_key = Some(input_recovery_key.to_owned()); - } - - let secp = Secp256k1::new(); - let private_key = env::var("PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX").unwrap(); - let secret_key = SecretKey::from_slice(&hex::decode(private_key.as_bytes()).unwrap()).unwrap(); - let signing_key = Keypair::from_secret_key(&secp, &secret_key); - match super::create_did_and_plc_op(&handle, &body, signing_key).await { - Ok(did_resp) => { - did = Some(did_resp); - } - Err(error) => { - eprintln!("Failed to create DID\n{:?}", error); - return Err(ApiError::RuntimeError); - } - } - let did = did.unwrap(); + deactivated, + plc_op, + signing_key, + } = validate_inputs_for_local_pds(cfg, id_resolver, body.into_inner(), requester).await?; - let actor_store = ActorStore::new(did.clone(), S3BlobStore::new(did.clone(), s3_config)); + // Create new actor repo TODO: Proper rollback + let mut actor_store = ActorStore::new(did.clone(), S3BlobStore::new(did.clone(), s3_config)); let commit = match actor_store.create_repo(signing_key, Vec::new()).await { Ok(commit) => commit, Err(error) => { - eprintln!("Failed to create account\n{:?}", error); + tracing::error!("Failed to create repo\n{:?}", error); + actor_store.destroy().await?; return Err(ApiError::RuntimeError); } }; + // Generate a real did with PLC + match plc_op { + None => {} + Some(op) => { + let plc_url = env_str("PDS_DID_PLC_URL").unwrap_or("https://plc.directory".to_owned()); + let plc_client = plc::Client::new(plc_url); + match plc_client.send_operation(&did, &OpOrTombstone::Operation(op)).await { + Ok(_) => { + tracing::info!("Succesfully sent PLC Operation") + } + Err(_) => { + tracing::error!("Failed to create did:plc"); + actor_store.destroy().await?; + return Err(ApiError::RuntimeError); + } + } + } + } + let did_doc; match safe_resolve_did_doc(id_resolver, &did, Some(true)).await { Ok(res) => did_doc = res, Err(error) => { - eprintln!("Error resolving DID Doc\n{error}"); + tracing::error!("Error resolving DID Doc\n{error}"); + actor_store.destroy().await?; return Err(ApiError::RuntimeError); } } + // Create Account let (access_jwt, refresh_jwt); match AccountManager::create_account(CreateAccountOpts { did: did.clone(), handle: handle.clone(), - email, - password, + email: Some(email), + password: Some(password), repo_cid: commit.cid, repo_rev: commit.rev.clone(), invite_code, deactivated: Some(deactivated), }) - .await + .await { Ok(res) => { (access_jwt, refresh_jwt) = res; } Err(error) => { - eprintln!("Error creating account\n{error}"); + tracing::error!("Error creating account\n{error}"); + actor_store.destroy().await.unwrap(); return Err(ApiError::RuntimeError); } } @@ -101,9 +135,11 @@ async fn inner_server_create_account {} + Ok(_) => { + tracing::debug!("Sequenece identity event succeeded"); + } Err(error) => { - eprintln!("Sequence Identity Event failed\n{error}"); + tracing::error!("Sequence Identity Event failed\n{error}"); return Err(ApiError::RuntimeError); } } @@ -111,9 +147,11 @@ async fn inner_server_create_account {} + Ok(_) => { + tracing::debug!("Sequence account event succeeded"); + } Err(error) => { - eprintln!("Sequence Account Event failed\n{error}"); + tracing::error!("Sequence Account Event failed\n{error}"); return Err(ApiError::RuntimeError); } } @@ -121,17 +159,21 @@ async fn inner_server_create_account {} + Ok(_) => { + tracing::debug!("Sequence commit succeeded"); + } Err(error) => { - eprintln!("Sequence Commit failed\n{error}"); + tracing::error!("Sequence Commit failed\n{error}"); return Err(ApiError::RuntimeError); } } } match AccountManager::update_repo_root(did.clone(), commit.cid, commit.rev) { - Ok(_) => {} + Ok(_) => { + tracing::debug!("Successfully updated repo root"); + } Err(error) => { - eprintln!("Update Repo Root failed\n{error}"); + tracing::error!("Update Repo Root failed\n{error}"); return Err(ApiError::RuntimeError); } } @@ -142,133 +184,172 @@ async fn inner_server_create_account match serde_json::to_value(did_doc) { Ok(res) => converted_did_doc = Some(res), Err(error) => { - eprintln!("Did Doc failed conversion\n{error}"); + tracing::error!("Did Doc failed conversion\n{error}"); return Err(ApiError::RuntimeError); } }, } - Ok(CreateAccountOutput { + + Ok(Json(CreateAccountOutput { access_jwt, refresh_jwt, handle, did, did_doc: converted_did_doc, - }) -} - -#[rocket::post( - "/xrpc/com.atproto.server.createAccount", - format = "json", - data = "" -)] -pub async fn server_create_account( - body: Json, - auth: UserDidAuthOptional, - sequencer: &State, - s3_config: &State, - cfg: &State, - id_resolver: &State, -) -> Result, ApiError> { - let requester = match auth.access { - Some(access) if access.credentials.is_some() => access.credentials.unwrap().iss, - _ => None, - }; - let input = - match validate_inputs_for_local_pds(cfg, id_resolver, body.clone().into_inner(), requester) - .await - { - Ok(res) => res, - Err(e) => return Err(e), // @TODO this needs better error logging - }; - - match inner_server_create_account::(input, sequencer, s3_config, id_resolver) - .await - { - Ok(response) => Ok(Json(response)), - Err(error) => Err(error), - } + })) } +/// Validates Create Account Parameters and builds PLC Operation if needed pub async fn validate_inputs_for_local_pds( cfg: &State, id_resolver: &State, input: CreateAccountInput, requester: Option, -) -> Result { - let CreateAccountInput { - email, - handle, - did, - invite_code, - verification_code, - verification_phone, - password, - recovery_key, - plc_op, - } = input; +) -> Result { + let did: String; + let plc_op; + let deactivated: bool; + let email; + let password; + let invite_code; - if plc_op.is_some() { + //PLC Op Validation + if input.plc_op.is_some() { return Err(ApiError::InvalidRequest( "Unsupported input: `plcOp`".to_string(), )); } - if cfg.invites.required && invite_code.is_none() { + + //Invite Code Validation + if cfg.invites.required && input.invite_code.is_none() { return Err(ApiError::InvalidInviteCode); + } else { + invite_code = input.invite_code.clone(); } - if email.is_none() { + + //Email Validation + if input.email.is_none() { return Err(ApiError::InvalidEmail); }; - match email { - None => Err(ApiError::InvalidEmail), - Some(email) => { - let e_slice: &str = &email[..]; // take a full slice of the string + match input.email { + None => return Err(ApiError::InvalidEmail), + Some(ref input_email) => { + let e_slice: &str = &input_email[..]; // take a full slice of the string if !EmailAddress::is_valid(e_slice) { return Err(ApiError::InvalidEmail); + } else { + email = input_email.clone(); } - if password.is_none() { - return Err(ApiError::InvalidPassword); - }; - //TODO Not yet allowing people to bring their own DID - if did.is_some() { - return Err(ApiError::InvalidRequest( - "Not yet allowing people to bring their own DID".to_string(), - )); - }; - let opts = HandleValidationOpts { - handle: handle.clone(), - did: requester.clone(), - allow_reserved: None, - }; - let validation_ctx = HandleValidationContext { - server_config: cfg, - id_resolver, - }; - let handle = normalize_and_validate_handle(opts, validation_ctx).await?; + } + } - if !super::validate_handle(&handle) { - return Err(ApiError::InvalidHandle); - }; - if cfg.invites.required && invite_code.is_some() { - AccountManager::ensure_invite_is_available(invite_code.clone().unwrap()).await?; - } - let handle_accnt = AccountManager::get_account(&handle, None).await?; - let email_accnt = AccountManager::get_account_by_email(&email, None).await?; - if handle_accnt.is_some() { - return Err(ApiError::HandleNotAvailable); - } else if email_accnt.is_some() { - return Err(ApiError::EmailNotAvailable); + // Normalize and Ensure Valid Handle + let opts = HandleValidationOpts { + handle: input.handle.clone(), + did: requester.clone(), + allow_reserved: None, + }; + let validation_ctx = HandleValidationContext { + server_config: cfg, + id_resolver, + }; + let handle = normalize_and_validate_handle(opts, validation_ctx).await?; + if !super::validate_handle(&handle) { + return Err(ApiError::InvalidHandle); + }; + + // Check Handle and Email are still available + let handle_accnt = AccountManager::get_account(&handle, None).await?; + let email_accnt = AccountManager::get_account_by_email(&email, None).await?; + if handle_accnt.is_some() { + return Err(ApiError::HandleNotAvailable); + } else if email_accnt.is_some() { + return Err(ApiError::EmailNotAvailable); + } + + // Check password exists + match input.password { + None => return Err(ApiError::InvalidPassword), + Some(ref pass) => password = pass.clone() + }; + + // Get Signing Key + let secp = Secp256k1::new(); + let private_key = env::var("PDS_REPO_SIGNING_KEY_K256_PRIVATE_KEY_HEX").unwrap(); + let secret_key = SecretKey::from_slice(&hex::decode(private_key.as_bytes()).unwrap()).unwrap(); + let signing_key = Keypair::from_secret_key(&secp, &secret_key); + + match input.did { + Some(input_did) => { + if input_did == requester.unwrap_or("n/a".to_string()) { + return Err(ApiError::AuthRequiredError(format!( + "Missing auth to create account with did: {input_did}" + ))); } - Ok(CreateAccountInput { - email: Some(email), - handle, - did, - invite_code, - verification_code, - verification_phone, - password, - recovery_key, - plc_op, - }) + did = input_did; + plc_op = None; + deactivated = true; + } + None => { + let res = format_did_and_plc_op(input, signing_key).await?; + did = res.0; + plc_op = Some(res.1); + deactivated = false; + } + }; + + Ok(TransformedCreateAccountInput { + email, + handle, + did, + invite_code, + password, + signing_key, + plc_op, + deactivated, + }) +} + +#[tracing::instrument(skip_all)] +async fn format_did_and_plc_op( + input: CreateAccountInput, + signing_key: Keypair, +) -> Result<(String, Operation), ApiError> { + let mut rotation_keys: Vec = Vec::new(); + + //Add user provided rotation key + if let Some(recovery_key) = &input.recovery_key { + rotation_keys.push(recovery_key.clone()); + } + + //Add PDS rotation key + let secp = Secp256k1::new(); + let private_rotation_key = env::var("PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX").unwrap(); + let private_secret_key = + SecretKey::from_slice(&hex::decode(private_rotation_key.as_bytes()).unwrap()).unwrap(); + let rotation_keypair = Keypair::from_secret_key(&secp, &private_secret_key); + rotation_keys.push(encode_did_key(&rotation_keypair.public_key())); + + //Build PLC Create Operation + let response; + let create_op_input = CreateAtprotoOpInput { + signing_key: encode_did_key(&signing_key.public_key()), + handle: input.handle, + pds: format!( + "https://{}", + env::var("PDS_HOSTNAME").unwrap_or("localhost".to_owned()) + ), + rotation_keys, + }; + match create_op(create_op_input, rotation_keypair.secret_key()).await { + Ok(res) => { + response = res; + } + Err(error) => { + tracing::error!("{error}"); + return Err(ApiError::RuntimeError); } } + + Ok(response) } diff --git a/rsky-pds/src/apis/com/atproto/server/mod.rs b/rsky-pds/src/apis/com/atproto/server/mod.rs index 301ebd9..1ff17b8 100644 --- a/rsky-pds/src/apis/com/atproto/server/mod.rs +++ b/rsky-pds/src/apis/com/atproto/server/mod.rs @@ -1,63 +1,21 @@ extern crate unsigned_varint; use crate::common::env::{env_int, env_str}; -use crate::common::sign::atproto_sign; -use crate::models::*; -use crate::{plc, SharedIdResolver, APP_USER_AGENT}; +use crate::{plc, SharedIdResolver}; use anyhow::{bail, Result}; -use data_encoding::BASE32; use diesel::prelude::*; -use diesel::PgConnection; -use indexmap::IndexMap; use multibase::Base::Base58Btc; use rand::{distributions::Alphanumeric, Rng}; use reqwest; use rocket::form::validate::Contains; use rocket::State; use rsky_identity::types::DidDocument; -use rsky_lexicon::com::atproto::server::CreateAccountInput; -use secp256k1::{Keypair, PublicKey, Secp256k1, SecretKey}; -use serde_json::Value; -use sha2::{Digest, Sha256}; +use secp256k1::{PublicKey, Secp256k1, SecretKey}; +use sha2::Digest; use std::env; use unsigned_varint::encode::u16 as encode_varint; const DID_KEY_PREFIX: &str = "did:key:"; -/// Important to user `preserve_order` with serde_json so these bytes are ordered -/// correctly when encoding. -#[derive(Debug, Deserialize, Serialize)] -pub struct AtprotoPdsService { - #[serde(rename(deserialize = "type", serialize = "type"))] - pub r#type: String, - pub endpoint: String, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct PlcGenesisServices { - pub atproto_pds: AtprotoPdsService, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct PlcGenesisVerificationMethods { - pub atproto: String, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct PlcGenesisOperation { - #[serde(rename(deserialize = "type", serialize = "type"))] - pub r#type: String, - #[serde(rename(deserialize = "rotationKeys", serialize = "rotationKeys"))] - pub rotation_keys: Vec, - #[serde(rename(deserialize = "verificationMethods", serialize = "verificationMethods"))] - pub verification_methods: PlcGenesisVerificationMethods, - #[serde(rename(deserialize = "alsoKnownAs", serialize = "alsoKnownAs"))] - pub also_known_as: Vec, - pub services: PlcGenesisServices, - pub prev: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sig: Option, -} - #[derive(Debug, Deserialize, Serialize)] pub struct AssertionContents { pub signing_key: Option, @@ -122,27 +80,6 @@ pub fn validate_handle(handle: &str) -> bool { // Need to check suffix here and need to make sure handle doesn't include "." after trumming it } -pub fn lookup_user_by_handle(handle: &str, conn: &mut PgConnection) -> Result { - use crate::schema::pds::actor::dsl as ActorSchema; - - let result = ActorSchema::actor - .filter(ActorSchema::handle.eq(handle)) - .select(Actor::as_select()) - .first(conn) - .map_err(|error| { - let context = format!("no user found with handle '{}'", handle); - anyhow::Error::new(error).context(context) - })?; - Ok(result) -} - -pub fn sign(mut genesis: PlcGenesisOperation, private_key: &SecretKey) -> PlcGenesisOperation { - let genesis_sig = atproto_sign(&genesis, private_key).unwrap(); - // Base 64 encode signature bytes - genesis.sig = Some(base64_url::encode(&genesis_sig).replace("=", "")); - genesis -} - /// https://github.com/gnunicorn/rust-multicodec/blob/master/src/lib.rs#L249-L260 pub fn multicodec_wrap(bytes: Vec) -> Vec { let mut buf = [0u8; 3]; @@ -180,68 +117,6 @@ pub fn get_keys_from_private_key_str(private_key: String) -> Result<(SecretKey, Ok((secret_key, public_key)) } -pub async fn create_did_and_plc_op( - handle: &str, - input: &CreateAccountInput, - signing_key: Keypair, -) -> Result { - let private_key: String; - if let Some(recovery_key) = &input.recovery_key { - private_key = recovery_key.clone(); - } else { - private_key = env::var("PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX").unwrap(); - } - let (secret_key, public_key) = get_keys_from_private_key_str(private_key)?; - - println!("Generating and signing PLC directory genesis operation..."); - let mut create_op = PlcGenesisOperation { - r#type: "plc_operation".to_owned(), - rotation_keys: vec![encode_did_key(&public_key)], - verification_methods: PlcGenesisVerificationMethods { - atproto: encode_did_key(&signing_key.public_key()), - }, - also_known_as: vec![format!("at://{handle}")], - services: PlcGenesisServices { - atproto_pds: AtprotoPdsService { - r#type: "AtprotoPersonalDataServer".to_owned(), - endpoint: format!( - "https://{}", - env::var("PDS_HOSTNAME").unwrap_or("localhost".to_owned()) - ), - }, - }, - prev: None, - sig: None, - }; - create_op = sign(create_op, &secret_key); - let json = serde_json::to_string(&create_op).unwrap(); - let hashmap_genesis: IndexMap = serde_json::from_str(&json).unwrap(); - let signed_genesis_bytes = serde_ipld_dagcbor::to_vec(&hashmap_genesis).unwrap(); - let mut hasher: Sha256 = Digest::new(); - hasher.update(signed_genesis_bytes.as_slice()); - let hash = hasher.finalize(); - let did_plc = &format!("did:plc:{}", BASE32.encode(&hash[..]))[..32].to_lowercase(); - println!("Created DID {did_plc:#}"); - println!("publishing......"); - - // @TODO: Use plc::Client instead - let plc_url = format!( - "{0}/{1}", - env::var("PDS_DID_PLC_URL").unwrap_or("https://plc.directory".to_owned()), - did_plc - ); - println!("Publishing to {plc_url}"); - let client = reqwest::Client::builder() - .user_agent(APP_USER_AGENT) - .build()?; - let response = client.post(plc_url).json(&create_op).send().await?; - let res = &response; - match res.error_for_status_ref() { - Ok(_res) => Ok(did_plc.into()), - Err(error) => Err(anyhow::Error::new(error).context(response.text().await?)), - } -} - pub async fn is_valid_did_doc_for_service(did: String) -> Result { match assert_valid_did_documents_for_service(did).await { Ok(()) => Ok(true), diff --git a/rsky-pds/src/apis/mod.rs b/rsky-pds/src/apis/mod.rs index 71f03bd..c452c34 100644 --- a/rsky-pds/src/apis/mod.rs +++ b/rsky-pds/src/apis/mod.rs @@ -80,9 +80,11 @@ pub enum ApiError { UnsupportedDomain, UnresolvableDid, IncompatibleDidDoc, + WellKnownNotFound, AccountNotFound, BlobNotFound, BadRequest(String, String), + AuthRequiredError(String), } #[derive(Serialize)] @@ -116,7 +118,7 @@ impl<'r, 'o: 'r> ::rocket::response::Responder<'r, 'o> for ApiError { }); let mut res = as ::rocket::response::Responder>::respond_to(body, __req)?; - res.set_header(ContentType(rocket::http::MediaType::const_new( + res.set_header(ContentType(::rocket::http::MediaType::const_new( "application", "json", &[], @@ -131,7 +133,7 @@ impl<'r, 'o: 'r> ::rocket::response::Responder<'r, 'o> for ApiError { }); let mut res = as ::rocket::response::Responder>::respond_to(body, __req)?; - res.set_header(ContentType(rocket::http::MediaType::const_new( + res.set_header(ContentType(::rocket::http::MediaType::const_new( "application", "json", &[], @@ -348,7 +350,22 @@ impl<'r, 'o: 'r> ::rocket::response::Responder<'r, 'o> for ApiError { ))); res.set_status(Status { code: 400u16 }); Ok(res) - } + }, + ApiError::WellKnownNotFound => { + let body = Json(ErrorBody { + error: "WellKnownNotFound".to_string(), + message: "User not found".to_string(), + }); + let mut res = + as ::rocket::response::Responder>::respond_to(body, __req)?; + res.set_header(ContentType(::rocket::http::MediaType::const_new( + "application", + "json", + &[], + ))); + res.set_status(Status { code: 404u16 }); + Ok(res) + }, ApiError::BadRequest(error, message) => { let body = Json(ErrorBody { error, message }); let mut res = @@ -360,7 +377,19 @@ impl<'r, 'o: 'r> ::rocket::response::Responder<'r, 'o> for ApiError { ))); res.set_status(Status { code: 400u16 }); Ok(res) - } + }, + ApiError::AuthRequiredError(message) => { + let body = Json(ErrorBody { error: "AuthRequiredError".to_string(), message }); + let mut res = + as ::rocket::response::Responder>::respond_to(body, __req)?; + res.set_header(ContentType(::rocket::http::MediaType::const_new( + "application", + "json", + &[], + ))); + res.set_status(Status { code: 401u16 }); + Ok(res) + }, ApiError::RecordNotFound => { let body = Json(ErrorBody { error: "RecordNotFound".to_string(), diff --git a/rsky-pds/src/main.rs b/rsky-pds/src/main.rs index abd9344..146db85 100644 --- a/rsky-pds/src/main.rs +++ b/rsky-pds/src/main.rs @@ -120,6 +120,9 @@ impl Fairing for CORS { async fn rocket() -> _ { dotenv().ok(); + let subscriber = tracing_subscriber::FmtSubscriber::new(); + tracing::subscriber::set_global_default(subscriber).unwrap(); + let db_url = env::var("DATABASE_URL").unwrap_or("".into()); let db: Map<_, Value> = map! { diff --git a/rsky-pds/src/plc/mod.rs b/rsky-pds/src/plc/mod.rs index 65fb5d8..88655a9 100644 --- a/rsky-pds/src/plc/mod.rs +++ b/rsky-pds/src/plc/mod.rs @@ -40,7 +40,7 @@ impl Client { Ok(res.json().await?) } - async fn send_operation(&self, did: &String, op: &OpOrTombstone) -> Result<()> { + pub async fn send_operation(&self, did: &String, op: &OpOrTombstone) -> Result<()> { let client = reqwest::Client::builder() .user_agent(APP_USER_AGENT) .build()?; diff --git a/rsky-pds/src/plc/operations.rs b/rsky-pds/src/plc/operations.rs index 7a7c179..5da719d 100644 --- a/rsky-pds/src/plc/operations.rs +++ b/rsky-pds/src/plc/operations.rs @@ -5,8 +5,10 @@ use anyhow::Result; use indexmap::IndexMap; use libipld::Cid; use secp256k1::SecretKey; -use serde_json::Value as JsonValue; +use serde_json::{Value as JsonValue, Value}; use std::collections::BTreeMap; +use data_encoding::BASE32; +use sha2::{Digest, Sha256}; #[derive(Debug, Clone)] pub struct CreateAtprotoUpdateOpOpts { @@ -16,6 +18,45 @@ pub struct CreateAtprotoUpdateOpOpts { pub rotation_keys: Option>, } +#[derive(Debug, Clone)] +pub struct CreateAtprotoOpInput { + pub signing_key: String, + pub handle: String, + pub pds: String, + pub rotation_keys: Vec, +} + +pub async fn create_op(opts: CreateAtprotoOpInput, secret_key: SecretKey) -> Result<(String, Operation)> { + //Build Operation + let mut create_op = Operation { + r#type: "plc_operation".to_string(), + rotation_keys: opts.rotation_keys, + verification_methods: BTreeMap::from([("atproto".to_string(), opts.signing_key)]), + also_known_as: vec![ensure_atproto_prefix(opts.handle)], + services: BTreeMap::from([( + "atproto_pds".to_string(), + Service { + r#type: "AtprotoPersonalDataServer".to_string(), + endpoint: ensure_http_prefix(opts.pds), + }, + )]), + prev: None, + sig: None, + }; + + //Sign and get DID + create_op = sign(create_op, &secret_key); + let json = serde_json::to_string(&create_op)?; + let hashmap_genesis: IndexMap = serde_json::from_str(&json)?; + let signed_genesis_bytes = serde_ipld_dagcbor::to_vec(&hashmap_genesis)?; + let mut hasher: Sha256 = Digest::new(); + hasher.update(signed_genesis_bytes.as_slice()); + let hash = hasher.finalize(); + let did_plc = format!("did:plc:{}", BASE32.encode(&hash[..]))[..32].to_lowercase(); + + Ok((did_plc, create_op)) +} + pub async fn update_atproto_key_op( last_op: CompatibleOp, signer: &SecretKey, @@ -31,7 +72,7 @@ pub async fn update_atproto_key_op( rotation_keys: None, }, ) - .await + .await } pub async fn update_handle_op( @@ -49,7 +90,7 @@ pub async fn update_handle_op( rotation_keys: None, }, ) - .await + .await } pub async fn update_pds_op( @@ -67,7 +108,7 @@ pub async fn update_pds_op( rotation_keys: None, }, ) - .await + .await } pub async fn update_rotation_keys_op( @@ -85,7 +126,7 @@ pub async fn update_rotation_keys_op( rotation_keys: Some(rotation_keys), }, ) - .await + .await } pub async fn create_atproto_update_op( @@ -117,7 +158,7 @@ pub async fn create_atproto_update_op( [formatted].as_slice(), &normalized.also_known_as[handle_i + 1..], ] - .concat() + .concat() } } } @@ -136,7 +177,7 @@ pub async fn create_atproto_update_op( } updated }) - .await + .await } pub async fn create_update_op( @@ -172,7 +213,7 @@ pub async fn tombstone_op(prev: Cid, key: &SecretKey) -> Result { }), key, ) - .await? + .await? { CompatibleOpOrTombstone::Tombstone(op) => Ok(op), _ => panic!("Enum type changed"), @@ -233,3 +274,10 @@ pub fn ensure_atproto_prefix(str: String) -> String { let stripped = str.replace("http://", "").replace("https://", ""); format!("at://{stripped}") } + +fn sign(mut op: Operation, private_key: &SecretKey) -> Operation { + let op_sig = atproto_sign(&op, private_key).unwrap(); + // Base 64 encode signature bytes + op.sig = Some(base64_url::encode(&op_sig).replace("=", "")); + op +} \ No newline at end of file