From b6274bb3c49c2093d76b8172ba73c72cb936c62f Mon Sep 17 00:00:00 2001 From: Lewis Date: Sun, 19 Jul 2026 17:19:22 +0300 Subject: [PATCH] api: validated newtypes from their checked constructors Lewis: May this revision serve well! --- crates/tranquil-api/src/common.rs | 3 +- crates/tranquil-api/src/delegation.rs | 2 +- crates/tranquil-api/src/discord_webhook.rs | 25 +++++++------ crates/tranquil-api/src/identity/did.rs | 4 +- crates/tranquil-api/src/identity/provision.rs | 4 +- crates/tranquil-api/src/moderation/mod.rs | 7 +++- crates/tranquil-api/src/repo/blob.rs | 6 ++- .../src/server/passkey_account.rs | 5 ++- crates/tranquil-api/src/server/password.rs | 26 ++++++++----- crates/tranquil-api/src/server/signing_key.rs | 3 +- crates/tranquil-api/src/telegram_webhook.rs | 16 +++++++- crates/tranquil-api/src/temp.rs | 21 +++++------ .../src/endpoints/authorize/registration.rs | 15 ++++++-- .../src/sso_endpoints.rs | 8 ++-- crates/tranquil-pds/src/api/responses.rs | 4 +- crates/tranquil-pds/src/auth/email_token.rs | 16 ++++---- crates/tranquil-pds/src/auth/mod.rs | 2 +- crates/tranquil-pds/src/plc/mod.rs | 5 ++- crates/tranquil-pds/src/sync/import.rs | 18 ++++++--- crates/tranquil-pds/src/sync/verify.rs | 3 +- .../tests/firehose_inline_blocks.rs | 4 +- crates/tranquil-pds/tests/jwt_security.rs | 14 +++---- .../tranquil-pds/tests/record_validation.rs | 2 +- crates/tranquil-pds/tests/repo_batch.rs | 6 +-- crates/tranquil-pds/tests/scope_edge_cases.rs | 2 +- crates/tranquil-pds/tests/signing_key.rs | 12 ++++-- .../src/metastore/backlink_ops.rs | 37 ++++++++++--------- .../tranquil-store/src/metastore/blob_ops.rs | 17 ++++++--- .../src/metastore/session_ops.rs | 3 +- crates/tranquil-store/src/metastore/users.rs | 2 +- 30 files changed, 181 insertions(+), 111 deletions(-) diff --git a/crates/tranquil-api/src/common.rs b/crates/tranquil-api/src/common.rs index c5df7c4..adbb445 100644 --- a/crates/tranquil-api/src/common.rs +++ b/crates/tranquil-api/src/common.rs @@ -200,7 +200,8 @@ pub fn create_self_hosted_did_web(handle: &str) -> Result { return Err(ApiError::SelfHostedDidWebDisabled); } let encoded_handle = handle.replace(':', "%3A"); - Ok(Did::from(format!("did:web:{}", encoded_handle))) + Did::new(format!("did:web:{}", encoded_handle)) + .map_err(|_| ApiError::InvalidHandle(Some("Handle is not a valid did:web".into()))) } pub enum CredentialMatch { diff --git a/crates/tranquil-api/src/delegation.rs b/crates/tranquil-api/src/delegation.rs index f4390af..2e52499 100644 --- a/crates/tranquil-api/src/delegation.rs +++ b/crates/tranquil-api/src/delegation.rs @@ -392,7 +392,7 @@ pub async fn create_delegated_account( controller_scopes: input.controller_scopes.as_str().to_string(), encrypted_key_bytes: repo.encrypted_key_bytes, encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, - commit_cid: CidLink::from(repo.commit_cid.to_string()), + commit_cid: CidLink::from(&repo.commit_cid), repo_rev: repo.repo_rev.clone(), genesis_block_cids: repo.genesis_block_cids, }; diff --git a/crates/tranquil-api/src/discord_webhook.rs b/crates/tranquil-api/src/discord_webhook.rs index 2740750..c5e1cb3 100644 --- a/crates/tranquil-api/src/discord_webhook.rs +++ b/crates/tranquil-api/src/discord_webhook.rs @@ -149,17 +149,21 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response { } }; - let handle = parse_start_handle(interaction.data.as_ref().and_then(|d| d.options.as_deref())); - - if let Some(ref h) = handle - && Handle::new(h).is_err() + let handle = match parse_start_handle( + interaction.data.as_ref().and_then(|d| d.options.as_deref()), + ) + .map(Handle::new) + .transpose() { - return Json(json!({ - "type": 4, - "data": {"content": "Invalid handle format. Handle should look like: alice.example.com", "flags": 64} - })) - .into_response(); - } + Ok(h) => h, + Err(_) => { + return Json(json!({ + "type": 4, + "data": {"content": "Invalid handle format. Handle should look like: nel.oyster.cafe", "flags": 64} + })) + .into_response(); + } + }; debug!( discord_username = %discord_username, @@ -168,7 +172,6 @@ async fn handle_command(state: AppState, interaction: Interaction) -> Response { "Received /start from Discord user" ); - let handle = handle.map(Handle::from); match state .repos .user diff --git a/crates/tranquil-api/src/identity/did.rs b/crates/tranquil-api/src/identity/did.rs index 3d4e7e8..34ec86f 100644 --- a/crates/tranquil-api/src/identity/did.rs +++ b/crates/tranquil-api/src/identity/did.rs @@ -51,7 +51,9 @@ pub async fn resolve_handle( } }; let cache_key = tranquil_pds::cache_keys::handle_key(&handle); - if let Some(did) = state.cache.get(&cache_key).await { + if let Some(cached) = state.cache.get(&cache_key).await + && let Ok(did) = tranquil_pds::Did::new(cached) + { return DidResponse::response(did).into_response(); } let user = state.repos.user.get_by_handle(&handle).await; diff --git a/crates/tranquil-api/src/identity/provision.rs b/crates/tranquil-api/src/identity/provision.rs index 3e1c7dd..23edb79 100644 --- a/crates/tranquil-api/src/identity/provision.rs +++ b/crates/tranquil-api/src/identity/provision.rs @@ -109,7 +109,7 @@ pub async fn init_genesis_repo( encrypted_key_bytes, commit_cid, mst_root_cid: mst_root, - repo_rev: Tid::from(rev.as_ref().to_string()), + repo_rev: Tid::from(rev.clone()), genesis_block_cids: vec![mst_root.to_bytes(), commit_cid.to_bytes()], }) } @@ -198,7 +198,7 @@ pub async fn sequence_new_account( if let Err(e) = tranquil_pds::repo_ops::sequence_sync_event( state, did, - &CidLink::from(repo.commit_cid.to_string()), + &CidLink::from(&repo.commit_cid), Some(&repo.repo_rev), ) .await diff --git a/crates/tranquil-api/src/moderation/mod.rs b/crates/tranquil-api/src/moderation/mod.rs index ae6d604..88045f1 100644 --- a/crates/tranquil-api/src/moderation/mod.rs +++ b/crates/tranquil-api/src/moderation/mod.rs @@ -6,6 +6,7 @@ use axum::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; +use std::sync::LazyLock; use tracing::{error, info, warn}; use tranquil_pds::api::ApiError; use tranquil_pds::api::proxy_client::{is_ssrf_safe, proxy_client}; @@ -13,6 +14,9 @@ use tranquil_pds::auth::{AnyUser, Auth}; use tranquil_pds::state::AppState; use tranquil_pds::types::{Did, Nsid}; +static CREATE_REPORT_NSID: LazyLock = + LazyLock::new(|| "com.atproto.moderation.createReport".parse().unwrap()); + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ReportReasonType { #[serde(rename = "com.atproto.moderation.defs#reasonSpam")] @@ -145,11 +149,10 @@ async fn proxy_to_report_service( }, }; - let report_lxm = Nsid::from("com.atproto.moderation.createReport".to_string()); let service_token = match tranquil_pds::auth::create_service_token( &auth_user.did, service_did, - Some(&report_lxm), + Some(&CREATE_REPORT_NSID), &key_bytes, ) { Ok(t) => t, diff --git a/crates/tranquil-api/src/repo/blob.rs b/crates/tranquil-api/src/repo/blob.rs index dd68092..aa0f7b1 100644 --- a/crates/tranquil-api/src/repo/blob.rs +++ b/crates/tranquil-api/src/repo/blob.rs @@ -11,6 +11,7 @@ use multihash::Multihash; use serde::{Deserialize, Serialize}; use serde_json::json; use std::pin::Pin; +use std::sync::LazyLock; use tracing::{debug, error, info, warn}; use tranquil_pds::api::error::{ApiError, DbResultExt}; use tranquil_pds::auth::{Auth, AuthAny, NotTakendown, Permissive, VerifyScope}; @@ -19,6 +20,9 @@ use tranquil_pds::state::AppState; use tranquil_pds::types::{CidLink, Did, Nsid}; use tranquil_pds::util::get_header_str; +static UPLOAD_BLOB_NSID: LazyLock = + LazyLock::new(|| "com.atproto.repo.uploadBlob".parse().unwrap()); + fn detect_mime_type(data: &[u8], client_hint: &str) -> String { if let Some(kind) = infer::get(data) { let detected = kind.mime_type().to_string(); @@ -48,7 +52,7 @@ pub async fn upload_blob( ) -> Result { let (did, controller_did): (Did, Option) = match &auth { AuthAny::Service(service) => { - service.require_lxm(&Nsid::from("com.atproto.repo.uploadBlob".to_string()))?; + service.require_lxm(&UPLOAD_BLOB_NSID)?; (service.did.clone(), None) } AuthAny::User(user) => { diff --git a/crates/tranquil-api/src/server/passkey_account.rs b/crates/tranquil-api/src/server/passkey_account.rs index bddfb30..33d96b9 100644 --- a/crates/tranquil-api/src/server/passkey_account.rs +++ b/crates/tranquil-api/src/server/passkey_account.rs @@ -69,7 +69,8 @@ pub async fn create_passkey_account( let token = extracted.token; if is_service_token(&token) { let verifier = ServiceTokenVerifier::new(); - let create_account_lxm = Nsid::from("com.atproto.server.createAccount".to_string()); + let create_account_lxm = Nsid::new("com.atproto.server.createAccount") + .expect("com.atproto.server.createAccount is a valid NSID"); match verifier .verify_service_token(&token, Some(&create_account_lxm)) .await @@ -313,7 +314,7 @@ pub async fn create_passkey_account( encrypted_key_bytes: repo.encrypted_key_bytes, encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, reserved_key_id, - commit_cid: tranquil_types::CidLink::from(repo.commit_cid.to_string()), + commit_cid: tranquil_types::CidLink::from(&repo.commit_cid), repo_rev: repo.repo_rev.clone(), genesis_block_cids: repo.genesis_block_cids, invite_code: invite_registration.into_invite_code(), diff --git a/crates/tranquil-api/src/server/password.rs b/crates/tranquil-api/src/server/password.rs index 68c3788..db8f327 100644 --- a/crates/tranquil-api/src/server/password.rs +++ b/crates/tranquil-api/src/server/password.rs @@ -45,15 +45,23 @@ pub async fn request_password_reset( None }; - let user_id = match state - .repos - .user - .get_id_by_email_or_handle( - normalized, - &Handle::from(normalized_handle.as_str().to_string()), - ) - .await - { + let lookup = match Handle::new(normalized_handle.as_str()) { + Ok(handle) => { + state + .repos + .user + .get_id_by_email_or_handle(normalized, &handle) + .await + } + Err(_) => state + .repos + .user + .get_by_email(normalized) + .await + .map(|user| user.map(|user| user.id)), + }; + + let user_id = match lookup { Ok(Some(id)) => id, Ok(None) => { info!("Password reset requested for unknown identifier"); diff --git a/crates/tranquil-api/src/server/signing_key.rs b/crates/tranquil-api/src/server/signing_key.rs index 0ac514f..60d4226 100644 --- a/crates/tranquil-api/src/server/signing_key.rs +++ b/crates/tranquil-api/src/server/signing_key.rs @@ -21,7 +21,8 @@ fn public_key_to_did_key(signing_key: &SigningKey) -> Did { multicodec_key.extend_from_slice(&SECP256K1_MULTICODEC_PREFIX); multicodec_key.extend_from_slice(&compressed_pubkey); let encoded = multibase::encode(multibase::Base::Base58Btc, &multicodec_key); - Did::from(format!("did:key:{}", encoded)) + Did::new(format!("did:key:{}", encoded)) + .expect("did:key with a multibase-encoded secp256k1 key is a valid DID") } #[derive(Deserialize)] diff --git a/crates/tranquil-api/src/telegram_webhook.rs b/crates/tranquil-api/src/telegram_webhook.rs index 004f2a8..0227c8f 100644 --- a/crates/tranquil-api/src/telegram_webhook.rs +++ b/crates/tranquil-api/src/telegram_webhook.rs @@ -62,8 +62,20 @@ pub async fn handle_telegram_webhook( && let Some(from) = message.from && let Some(username) = from.username { - let handle = - parse_start_handle(message.text.as_deref()).map(tranquil_types::Handle::from); + let handle = match parse_start_handle(message.text.as_deref()) + .map(tranquil_types::Handle::new) + .transpose() + { + Ok(h) => h, + Err(e) => { + warn!( + telegram_username = %username, + error = %e, + "Ignoring /start with an invalid handle" + ); + return StatusCode::OK; + } + }; debug!( telegram_username = %username, diff --git a/crates/tranquil-api/src/temp.rs b/crates/tranquil-api/src/temp.rs index 437a261..868d6f6 100644 --- a/crates/tranquil-api/src/temp.rs +++ b/crates/tranquil-api/src/temp.rs @@ -58,9 +58,16 @@ pub async fn dereference_scope( for part in scope_parts { if let Some(cid_str) = part.strip_prefix("ref:") { - let cache_key = tranquil_pds::cache_keys::scope_ref_key( - &tranquil_types::CidLink::from(cid_str.to_string()), - ); + let cid = match Cid::from_str(cid_str) { + Ok(c) => c, + Err(_) => { + tracing::warn!("Invalid CID in scope ref: {}", cid_str); + continue; + } + }; + + let cache_key = + tranquil_pds::cache_keys::scope_ref_key(&tranquil_types::CidLink::from(&cid)); if let Some(cached) = state.cache.get(&cache_key).await { for s in cached.split_whitespace() { if !resolved_scopes.contains(&s.to_string()) { @@ -70,14 +77,6 @@ pub async fn dereference_scope( continue; } - let cid = match Cid::from_str(cid_str) { - Ok(c) => c, - Err(_) => { - tracing::warn!("Invalid CID in scope ref: {}", cid_str); - continue; - } - }; - let block_bytes = match state.block_store.get(&cid).await { Ok(Some(b)) => b, Ok(None) => { diff --git a/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs b/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs index 9e8cea1..4993728 100644 --- a/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs +++ b/crates/tranquil-oauth-server/src/endpoints/authorize/registration.rs @@ -12,7 +12,16 @@ pub async fn register_complete( _rate_limit: OAuthRateLimited, Json(form): Json, ) -> Response { - let did = Did::from(form.did.clone()); + let Ok(did) = Did::new(form.did.as_str()) else { + return ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": "invalid_request", + "error_description": "Invalid DID." + })), + ) + .into_response(); + }; let request_id = RequestId::from(form.request_uri.clone()); let request_data = match state @@ -97,12 +106,12 @@ pub async fn register_complete( } if let Some(existing_did) = &request_data.did - && existing_did != &form.did + && existing_did != &did { tracing::warn!( request_uri = %form.request_uri, existing_did = %existing_did, - attempted_did = %form.did, + attempted_did = %did, "register_complete attempted with different DID than already bound" ); return ( diff --git a/crates/tranquil-oauth-server/src/sso_endpoints.rs b/crates/tranquil-oauth-server/src/sso_endpoints.rs index a4c3662..1f3b08a 100644 --- a/crates/tranquil-oauth-server/src/sso_endpoints.rs +++ b/crates/tranquil-oauth-server/src/sso_endpoints.rs @@ -1023,7 +1023,9 @@ pub async fn complete_registration( } let encoded_handle = handle.replace(':', "%3A"); let self_hosted_did = - tranquil_pds::types::Did::from(format!("did:web:{}", encoded_handle)); + tranquil_pds::types::Did::new(format!("did:web:{}", encoded_handle)).map_err( + |_| ApiError::InvalidHandle(Some("Handle is not a valid did:web".into())), + )?; tracing::info!(did = %self_hosted_did, "Creating self-hosted did:web SSO account"); self_hosted_did } @@ -1151,8 +1153,8 @@ pub async fn complete_registration( .filter(|s| !s.is_empty()), encrypted_key_bytes: encrypted_key_bytes.clone(), encryption_version: tranquil_pds::config::ENCRYPTION_VERSION, - commit_cid: tranquil_types::CidLink::from(commit_cid.to_string()), - repo_rev: tranquil_types::Tid::from(rev.as_ref().to_string()), + commit_cid: tranquil_types::CidLink::from_cid(&commit_cid), + repo_rev: tranquil_types::Tid::from(rev.clone()), genesis_block_cids, invite_code: invite_registration.into_invite_code(), birthdate_pref, diff --git a/crates/tranquil-pds/src/api/responses.rs b/crates/tranquil-pds/src/api/responses.rs index 937761b..0ea52d2 100644 --- a/crates/tranquil-pds/src/api/responses.rs +++ b/crates/tranquil-pds/src/api/responses.rs @@ -28,8 +28,8 @@ pub struct DidResponse { } impl DidResponse { - pub fn response(did: impl Into) -> impl IntoResponse { - Json(Self { did: did.into() }) + pub fn response(did: Did) -> impl IntoResponse { + Json(Self { did }) } } diff --git a/crates/tranquil-pds/src/auth/email_token.rs b/crates/tranquil-pds/src/auth/email_token.rs index 2522473..2441a29 100644 --- a/crates/tranquil-pds/src/auth/email_token.rs +++ b/crates/tranquil-pds/src/auth/email_token.rs @@ -189,7 +189,7 @@ mod tests { #[tokio::test] async fn test_create_and_validate_token() { let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) .await @@ -206,7 +206,7 @@ mod tests { #[tokio::test] async fn test_token_consumed_after_use() { let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) .await @@ -224,7 +224,7 @@ mod tests { #[tokio::test] async fn test_invalid_token_rejected() { let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); let _token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) .await @@ -238,7 +238,7 @@ mod tests { #[tokio::test] async fn test_wrong_purpose_rejected() { let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) .await @@ -253,7 +253,7 @@ mod tests { async fn test_token_format() { // The emitted token is the display form: uppercase `XXXXX-XXXXX`. let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); (0..50).for_each(|_| { let token = futures::executor::block_on(create_email_token( &cache, @@ -270,7 +270,7 @@ mod tests { #[tokio::test] async fn test_case_insensitive_validation() { let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) .await @@ -285,7 +285,7 @@ mod tests { #[tokio::test] async fn test_hyphen_insensitive_validation() { let cache = MockCache::new(); - let did = Did::from("did:plc:teq".to_string()); + let did = Did::new("did:plc:teq").expect("valid DID"); let token = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail) .await @@ -300,7 +300,7 @@ mod tests { #[tokio::test] async fn test_noop_cache_returns_unavailable() { let cache = crate::cache::NoOpCache; - let did = Did::from("did:plc:whelk".to_string()); + let did = Did::new("did:plc:whelk").expect("valid DID"); let result = create_email_token(&cache, &did, EmailTokenPurpose::UpdateEmail).await; assert_eq!(result.unwrap_err(), TokenError::CacheUnavailable); diff --git a/crates/tranquil-pds/src/auth/mod.rs b/crates/tranquil-pds/src/auth/mod.rs index 5a6e64b..4d3c5b1 100644 --- a/crates/tranquil-pds/src/auth/mod.rs +++ b/crates/tranquil-pds/src/auth/mod.rs @@ -624,7 +624,7 @@ mod tests { use super::*; fn n(s: &str) -> crate::types::Nsid { - crate::types::Nsid::from(s.to_string()) + crate::types::Nsid::new(s).expect("valid NSID") } #[test] diff --git a/crates/tranquil-pds/src/plc/mod.rs b/crates/tranquil-pds/src/plc/mod.rs index 8422407..4561e00 100644 --- a/crates/tranquil-pds/src/plc/mod.rs +++ b/crates/tranquil-pds/src/plc/mod.rs @@ -561,7 +561,8 @@ pub fn did_for_genesis_op(signed_op: &Value) -> Result { let hash = hasher.finalize(); let encoded = base32::encode(Alphabet::Rfc4648Lower { padding: false }, &hash); let truncated = &encoded[..24]; - Ok(Did::from(format!("did:plc:{}", truncated))) + Ok(Did::new(format!("did:plc:{}", truncated)) + .expect("did:plc with 24 base32 characters of a sha256 digest is a valid DID")) } pub fn validate_plc_operation(op: &Value) -> Result { @@ -784,7 +785,7 @@ mod tests { let result = create_genesis_operation( &key, Some(operator_key), - &crate::types::Handle::from("whelk.nel.pet".to_string()), + &crate::types::Handle::new("whelk.nel.pet").expect("valid handle"), "https://nel.pet", ) .unwrap(); diff --git a/crates/tranquil-pds/src/sync/import.rs b/crates/tranquil-pds/src/sync/import.rs index 7512c45..ca98856 100644 --- a/crates/tranquil-pds/src/sync/import.rs +++ b/crates/tranquil-pds/src/sync/import.rs @@ -227,15 +227,23 @@ fn walk_mst_node( { let blob_refs = find_blob_refs_ipld(&record_value, 0); let parts: Vec<&str> = full_key.split('/').collect(); - if parts.len() >= 2 { - let collection = Nsid::from(parts[..parts.len() - 1].join("/")); - let rkey = Rkey::from(parts[parts.len() - 1].to_string()); - records.push(ImportedRecord { + let parsed = match parts.len() >= 2 { + true => Nsid::new(parts[..parts.len() - 1].join("/")) + .ok() + .zip(Rkey::new(parts[parts.len() - 1].to_string()).ok()), + false => None, + }; + match parsed { + Some((collection, rkey)) => records.push(ImportedRecord { collection, rkey, cid: record_cid, blob_refs, - }); + }), + None => tracing::warn!( + key = %full_key, + "skipping a CAR record whose MST key isn't a valid collection/rkey pair" + ), } } diff --git a/crates/tranquil-pds/src/sync/verify.rs b/crates/tranquil-pds/src/sync/verify.rs index 6c11579..d661a37 100644 --- a/crates/tranquil-pds/src/sync/verify.rs +++ b/crates/tranquil-pds/src/sync/verify.rs @@ -100,7 +100,8 @@ impl CarVerifier { .ok_or_else(|| VerifyError::BlockNotFound(root_cid.to_string()))?; let commit = Commit::from_cbor(root_block).map_err(|e| VerifyError::InvalidCommit(e.to_string()))?; - let commit_did = commit.did().to_string().into(); + let commit_did = tranquil_types::Did::new(commit.did().to_string()) + .map_err(|e| VerifyError::InvalidCommit(e.to_string()))?; let data_cid = commit.data(); self.verify_mst_structure(data_cid, blocks)?; debug!("MST structure verified for commit: {:?}", commit); diff --git a/crates/tranquil-pds/tests/firehose_inline_blocks.rs b/crates/tranquil-pds/tests/firehose_inline_blocks.rs index 9cd4e4f..6df81a4 100644 --- a/crates/tranquil-pds/tests/firehose_inline_blocks.rs +++ b/crates/tranquil-pds/tests/firehose_inline_blocks.rs @@ -145,7 +145,7 @@ async fn sync_event_carries_inline_commit_block() { let commit_bytes = b"synthetic sync commit block payload".to_vec(); let commit_cid = synthetic_cid(&commit_bytes); let cid_link: CidLink = (&commit_cid).into(); - let rev = tranquil_types::Tid::from("3kabcdefghij2".to_string()); + let rev = tranquil_types::Tid::new("3kabcdefghij2").expect("valid TID"); let baseline = repos.repo.get_max_seq().await.expect("get_max_seq"); repos @@ -192,7 +192,7 @@ async fn genesis_commit_event_carries_inline_blocks() { let mst_root_cid = synthetic_cid(&mst_root_bytes); let commit_link: CidLink = (&commit_cid).into(); let mst_link: CidLink = (&mst_root_cid).into(); - let rev = tranquil_types::Tid::from("3kabcdefghij3".to_string()); + let rev = tranquil_types::Tid::new("3kabcdefghij3").expect("valid TID"); let baseline = repos.repo.get_max_seq().await.expect("get_max_seq"); repos diff --git a/crates/tranquil-pds/tests/jwt_security.rs b/crates/tranquil-pds/tests/jwt_security.rs index 1d3fd70..e462df3 100644 --- a/crates/tranquil-pds/tests/jwt_security.rs +++ b/crates/tranquil-pds/tests/jwt_security.rs @@ -40,7 +40,7 @@ fn create_unsigned_jwt(header: &Value, claims: &Value) -> String { #[test] fn test_signature_attacks() { let key_bytes = generate_user_key(); - let did = Did::from("did:plc:whelk".to_string()); + let did = Did::new("did:plc:whelk").expect("valid DID"); let token = create_access_token(&did, &key_bytes).expect("create token"); let parts: Vec<&str> = token.split('.').collect(); @@ -143,7 +143,7 @@ fn test_algorithm_substitution_attacks() { #[test] fn test_token_type_confusion() { let key_bytes = generate_user_key(); - let did = Did::from("did:plc:whelk".to_string()); + let did = Did::new("did:plc:whelk").expect("valid DID"); let refresh_token = create_refresh_token(&did, &key_bytes).expect("create refresh token"); let result = verify_access_token(&refresh_token, &key_bytes); @@ -169,8 +169,8 @@ fn test_token_type_confusion() { let service_token = create_service_token( &did, - &Did::from("did:web:nel.pet".to_string()), - Some(&Nsid::from("cafe.oyster.method".to_string())), + &Did::new("did:web:nel.pet").expect("valid DID"), + Some(&Nsid::new("cafe.oyster.method").expect("valid NSID")), &key_bytes, ) .unwrap(); @@ -434,7 +434,7 @@ fn test_claim_validation() { #[test] fn test_did_and_jti_extraction() { let key_bytes = generate_user_key(); - let did = Did::from("did:plc:limpet".to_string()); + let did = Did::new("did:plc:limpet").expect("valid DID"); let token = create_access_token(&did, &key_bytes).expect("create token"); assert_eq!(get_did_from_token(&token).unwrap(), did); @@ -459,7 +459,7 @@ fn test_did_and_jti_extraction() { #[test] fn test_header_injection_and_constant_time() { let key_bytes = generate_user_key(); - let did = Did::from("did:plc:whelk".to_string()); + let did = Did::new("did:plc:whelk").expect("valid DID"); let header = json!({ "alg": "ES256K", "typ": TokenType::Access.as_str(), @@ -501,7 +501,7 @@ async fn test_server_rejects_invalid_tokens() { let key_bytes = generate_user_key(); let forged_token = - create_access_token(&Did::from("did:plc:lyna".to_string()), &key_bytes).unwrap(); + create_access_token(&Did::new("did:plc:lyna").expect("valid DID"), &key_bytes).unwrap(); let res = http_client .get(format!("{}/xrpc/com.atproto.server.getSession", url)) .header("Authorization", format!("Bearer {}", forged_token)) diff --git a/crates/tranquil-pds/tests/record_validation.rs b/crates/tranquil-pds/tests/record_validation.rs index 102b875..1fa087b 100644 --- a/crates/tranquil-pds/tests/record_validation.rs +++ b/crates/tranquil-pds/tests/record_validation.rs @@ -10,7 +10,7 @@ fn now() -> String { } fn c(s: &str) -> Nsid { - Nsid::from(s.to_string()) + Nsid::new(s).expect("valid NSID") } #[test] diff --git a/crates/tranquil-pds/tests/repo_batch.rs b/crates/tranquil-pds/tests/repo_batch.rs index 9821134..b86f98c 100644 --- a/crates/tranquil-pds/tests/repo_batch.rs +++ b/crates/tranquil-pds/tests/repo_batch.rs @@ -512,9 +512,9 @@ async fn follow_uris_pointing_to(repo_id: uuid::Uuid, target_did: &str) -> Vec Nsid { - Nsid::from(s.to_string()) + Nsid::new(s).expect("valid NSID") } #[test] diff --git a/crates/tranquil-pds/tests/signing_key.rs b/crates/tranquil-pds/tests/signing_key.rs index 2eaaae6..e87cd21 100644 --- a/crates/tranquil-pds/tests/signing_key.rs +++ b/crates/tranquil-pds/tests/signing_key.rs @@ -48,7 +48,9 @@ async fn test_reserve_signing_key_with_did() { assert!(signing_key.starts_with("did:key:z")); let row = repos .infra - .get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string())) + .get_reserved_signing_key_full( + &tranquil_types::Did::new(signing_key.to_string()).expect("valid DID"), + ) .await .expect("db error") .expect("Reserved key not found in database"); @@ -75,7 +77,9 @@ async fn test_reserve_signing_key_stores_private_key() { let signing_key = body["signingKey"].as_str().unwrap(); let row = repos .infra - .get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string())) + .get_reserved_signing_key_full( + &tranquil_types::Did::new(signing_key.to_string()).expect("valid DID"), + ) .await .expect("db error") .expect("Reserved key not found in database"); @@ -185,7 +189,9 @@ async fn test_create_account_with_reserved_signing_key() { assert!(!access_jwt.is_empty()); let reserved = repos .infra - .get_reserved_signing_key_full(&tranquil_types::Did::from(signing_key.to_string())) + .get_reserved_signing_key_full( + &tranquil_types::Did::new(signing_key.to_string()).expect("valid DID"), + ) .await .expect("db error") .expect("Reserved key not found"); diff --git a/crates/tranquil-store/src/metastore/backlink_ops.rs b/crates/tranquil-store/src/metastore/backlink_ops.rs index 65353c9..20abbf8 100644 --- a/crates/tranquil-store/src/metastore/backlink_ops.rs +++ b/crates/tranquil-store/src/metastore/backlink_ops.rs @@ -174,7 +174,8 @@ impl BacklinkOps { MetastoreError::CorruptData("corrupt backlink value in indexes partition"), )?; - let uri: AtUri = val.source_uri.into(); + let uri = AtUri::new(val.source_uri) + .map_err(|_| MetastoreError::CorruptData("corrupt backlink source_uri"))?; let matches_collection = uri.collection().is_some_and(|c| c == collection_str); let matches_path = match discriminant_to_path(val.path) { Some(p) => p == bl.path, @@ -236,13 +237,6 @@ mod tests { } } - fn test_cid_link(seed: u8) -> tranquil_types::CidLink { - let digest: [u8; 32] = std::array::from_fn(|i| seed.wrapping_add(i as u8)); - let mh = multihash::Multihash::<64>::wrap(0x12, &digest).unwrap(); - let c = cid::Cid::new_v1(0x71, mh); - tranquil_types::CidLink::from_cid(&c) - } - fn test_rev(seq: u64) -> tranquil_types::Tid { const ALPHABET: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; let s: String = (0..13) @@ -252,10 +246,17 @@ mod tests { tranquil_types::Tid::new(s).expect("generated TID is valid") } + fn test_cid_link(seed: u8) -> tranquil_types::CidLink { + let digest: [u8; 32] = std::array::from_fn(|i| seed.wrapping_add(i as u8)); + let mh = multihash::Multihash::<64>::wrap(0x12, &digest).unwrap(); + let c = cid::Cid::new_v1(0x71, mh); + tranquil_types::CidLink::from_cid(&c) + } + fn create_repo(h: &TestHarness, name: &str, seed: u8) -> (Uuid, UserHash) { let user_id = Uuid::new_v4(); - let did = Did::from(format!("did:plc:{name}")); - let handle = Handle::from(format!("{name}.test.invalid")); + let did = Did::new(format!("did:plc:{name}")).expect("test DID is well-formed"); + let handle = Handle::new(format!("{name}.oyster.cafe")).expect("test handle is valid"); let cid = test_cid_link(seed); h.metastore .repo_ops() @@ -414,7 +415,7 @@ mod tests { link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(), }]; - let collection = Nsid::from("app.bsky.feed.like".to_string()); + let collection = Nsid::new("app.bsky.feed.like").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id, &collection, &proposed) .unwrap(); @@ -449,7 +450,8 @@ mod tests { link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(), }]; - let collection = Nsid::from("app.bsky.feed.repost".to_string()); + let collection = + Nsid::new("app.bsky.feed.repost").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id, &collection, &proposed) .unwrap(); @@ -480,7 +482,8 @@ mod tests { link_to: "did:plc:target".to_string(), }]; - let collection = Nsid::from("app.bsky.graph.follow".to_string()); + let collection = + Nsid::new("app.bsky.graph.follow").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id, &collection, &proposed) .unwrap(); @@ -512,7 +515,7 @@ mod tests { link_to: "at://did:plc:target/app.bsky.feed.post/3k2p1".to_string(), }]; - let collection = Nsid::from("app.bsky.feed.like".to_string()); + let collection = Nsid::new("app.bsky.feed.like").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id_b, &collection, &proposed) .unwrap(); @@ -543,7 +546,7 @@ mod tests { link_to: "at://did:plc:someone/app.bsky.feed.post/3k2p1".to_string(), }]; - let collection = Nsid::from("app.bsky.feed.like".to_string()); + let collection = Nsid::new("app.bsky.feed.like").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id, &collection, &proposed) .unwrap(); @@ -557,7 +560,7 @@ mod tests { let ops = h.metastore.backlink_ops(); let (user_id, _user_hash) = create_repo(&h, "bailey", 9); - let collection = Nsid::from("app.bsky.feed.like".to_string()); + let collection = Nsid::new("app.bsky.feed.like").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id, &collection, &[]) .unwrap(); @@ -647,7 +650,7 @@ mod tests { }, ]; - let collection = Nsid::from("app.bsky.feed.like".to_string()); + let collection = Nsid::new("app.bsky.feed.like").expect("test collection is a valid NSID"); let conflicts = ops .get_backlink_conflicts(user_id, &collection, &proposed) .unwrap(); diff --git a/crates/tranquil-store/src/metastore/blob_ops.rs b/crates/tranquil-store/src/metastore/blob_ops.rs index 0703423..50c496f 100644 --- a/crates/tranquil-store/src/metastore/blob_ops.rs +++ b/crates/tranquil-store/src/metastore/blob_ops.rs @@ -363,14 +363,19 @@ impl BlobOps { })?; let start = cursor.map_or(Bound::Unbounded, Bound::Excluded); - Ok(missing + missing .range::((start, Bound::Unbounded)) .take(limit) - .map(|(cid_str, uri)| tranquil_db_traits::MissingBlobInfo { - blob_cid: CidLink::from(cid_str.clone()), - record_uri: tranquil_types::AtUri::from(uri.clone()), + .map(|(cid_str, uri)| { + Ok(tranquil_db_traits::MissingBlobInfo { + blob_cid: CidLink::new(cid_str.clone()).map_err(|_| { + MetastoreError::CorruptData("corrupt record_blobs blob cid") + })?, + record_uri: tranquil_types::AtUri::new(uri.clone()) + .map_err(|_| MetastoreError::CorruptData("corrupt record_blobs uri"))?, + }) }) - .collect()) + .collect() } fn collect_referenced_cid_bytes( @@ -445,7 +450,7 @@ fn parse_blob_cid_from_key(key: &[u8]) -> Result { ))?; reader .string() - .map(CidLink::from) + .and_then(|s| CidLink::new(s).ok()) .ok_or(MetastoreError::CorruptData("corrupt blob key: missing cid")) } diff --git a/crates/tranquil-store/src/metastore/session_ops.rs b/crates/tranquil-store/src/metastore/session_ops.rs index 07aefc3..1381a75 100644 --- a/crates/tranquil-store/src/metastore/session_ops.rs +++ b/crates/tranquil-store/src/metastore/session_ops.rs @@ -211,7 +211,8 @@ impl SessionOps { } Ok(RefreshGraceLookup::Compromised { - did: Did::from(session.did.clone()), + did: Did::new(session.did.clone()) + .map_err(|_| MetastoreError::CorruptData("corrupt session did"))?, session_id: SessionId::new(session_id), key_bytes: user.key_bytes, encryption_version: user.encryption_version, diff --git a/crates/tranquil-store/src/metastore/users.rs b/crates/tranquil-store/src/metastore/users.rs index a2d0607..39867a9 100644 --- a/crates/tranquil-store/src/metastore/users.rs +++ b/crates/tranquil-store/src/metastore/users.rs @@ -799,7 +799,7 @@ mod tests { let mut user = UserValue { id: uuid::Uuid::new_v4(), did: "did:plc:test".to_owned(), - handle: "t.invalid".to_owned(), + handle: "teq.oyster.cafe".to_owned(), email: None, email_verified: false, password_hash: None,