diff --git a/src/spaces/car.rs b/src/spaces/car.rs index 9d7404e..e4156ba 100644 --- a/src/spaces/car.rs +++ b/src/spaces/car.rs @@ -62,11 +62,12 @@ pub fn serialize_repo(commit: &SignedCommit, records: &[SpaceRecord]) -> Result< let mut indexed: Vec<(&SpaceRecord, Vec, Cid)> = records .iter() .map(|r| { - let block = serde_json::to_vec(&r.record).unwrap_or_default(); + let block = serde_json::to_vec(&r.record) + .map_err(|e| AppError::Internal(format!("failed to serialize record: {e}")))?; let cid = make_cid(RAW, &block); - (r, block, cid) + Ok((r, block, cid)) }) - .collect(); + .collect::, AppError>>()?; indexed.sort_by(|a, b| { let ka = format!("{}/{}", a.0.collection, a.0.rkey); let kb = format!("{}/{}", b.0.collection, b.0.rkey); diff --git a/src/spaces/commit.rs b/src/spaces/commit.rs index 92cce61..bfc6810 100644 --- a/src/spaces/commit.rs +++ b/src/spaces/commit.rs @@ -84,6 +84,13 @@ pub fn verify_commit( author_did: &str, verifying_key: &VerifyingKey, ) -> Result<(), AppError> { + if commit.ver != 1 { + return Err(AppError::BadRequest(format!( + "unsupported commit version: {}", + commit.ver + ))); + } + let ctx = build_context(space_uri, author_did, &commit.rev, &commit.ikm); let sig = Signature::from_bytes(commit.sig.as_slice().into()) @@ -283,6 +290,18 @@ mod tests { assert!(verify_commit(&c2, space, "did:plc:testuser", &vk).is_ok()); } + #[test] + fn verify_rejects_unknown_version() { + let sk = test_signing_key(); + let vk = *sk.verifying_key(); + let hash = [0xCC; 32]; + let space = "at://did:plc:abc/space/com.example.forum/main"; + + let mut commit = sign_commit(&hash, space, "did:plc:testuser", "rev1", &sk).unwrap(); + commit.ver = 2; + assert!(verify_commit(&commit, space, "did:plc:testuser", &vk).is_err()); + } + #[test] fn verify_rejects_tampered_mac() { let sk = test_signing_key(); diff --git a/src/spaces/members.rs b/src/spaces/members.rs index c4deef5..2c3eceb 100644 --- a/src/spaces/members.rs +++ b/src/spaces/members.rs @@ -93,7 +93,7 @@ async fn resolve_delegation_target( backend: DatabaseBackend, member: &SpaceMember, ) -> Result, AppError> { - if member.did.starts_with("at://") { + if member.did.starts_with("at://") || member.did.starts_with("ats://") { let uri = SpaceUri::parse(&member.did)?; let space = db::get_space_by_address(pool, backend, &uri.did, &uri.type_nsid, &uri.skey).await?; diff --git a/src/spaces/mod.rs b/src/spaces/mod.rs index 4b96a6e..47ca8e5 100644 --- a/src/spaces/mod.rs +++ b/src/spaces/mod.rs @@ -79,7 +79,12 @@ impl SpaceUri { let type_nsid = parts[2].to_string(); let skey = parts[3].to_string(); - let (user_did, collection, rkey) = if parts.len() >= 7 { + let (user_did, collection, rkey) = if parts.len() == 7 { + if parts[4].is_empty() || parts[5].is_empty() || parts[6].is_empty() { + return Err(AppError::BadRequest( + "SpaceUri record components must not be empty".into(), + )); + } ( Some(parts[4].to_string()), Some(parts[5].to_string()), diff --git a/src/spaces/routes.rs b/src/spaces/routes.rs index 3e34a28..3ca762c 100644 --- a/src/spaces/routes.rs +++ b/src/spaces/routes.rs @@ -1167,18 +1167,31 @@ async fn get_latest_commit( let repo_state = db::get_or_create_repo_state(&state.db, state.db_backend, &space.id, ¶ms.did).await?; + let commit = if let Some(h) = repo_state.hash.as_ref() { + let ikm = repo_state.ikm.as_deref().ok_or_else(|| { + AppError::Internal("corrupt repo state: hash present but ikm missing".into()) + })?; + let sig = repo_state.sig.as_deref().ok_or_else(|| { + AppError::Internal("corrupt repo state: hash present but sig missing".into()) + })?; + let mac = repo_state.mac.as_deref().ok_or_else(|| { + AppError::Internal("corrupt repo state: hash present but mac missing".into()) + })?; + Some(serde_json::json!({ + "ver": 1, + "hash": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(h), + "ikm": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(ikm), + "sig": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(sig), + "mac": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(mac), + "rev": repo_state.rev, + })) + } else { + None + }; + Ok(Json(serde_json::json!({ "rev": repo_state.rev, - "commit": repo_state.hash.as_ref().map(|h| { - serde_json::json!({ - "ver": 1, - "hash": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(h), - "ikm": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(repo_state.ikm.as_deref().unwrap_or_default()), - "sig": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(repo_state.sig.as_deref().unwrap_or_default()), - "mac": base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(repo_state.mac.as_deref().unwrap_or_default()), - "rev": repo_state.rev, - }) - }), + "commit": commit, }))) } @@ -1207,25 +1220,38 @@ async fn get_repo( let records = db::list_all_space_records(&state.db, state.db_backend, &space.id, ¶ms.did).await?; + let hash = repo_state + .hash + .as_deref() + .ok_or_else(|| AppError::NotFound("no commit exists for this repo".into()))?; + let hash: [u8; 32] = hash + .try_into() + .map_err(|_| AppError::Internal("corrupt repo state: hash is not 32 bytes".into()))?; + let ikm: [u8; 32] = repo_state + .ikm + .as_deref() + .ok_or_else(|| AppError::Internal("corrupt repo state: missing ikm".into()))? + .try_into() + .map_err(|_| AppError::Internal("corrupt repo state: ikm is not 32 bytes".into()))?; + let mac: [u8; 32] = repo_state + .mac + .as_deref() + .ok_or_else(|| AppError::Internal("corrupt repo state: missing mac".into()))? + .try_into() + .map_err(|_| AppError::Internal("corrupt repo state: mac is not 32 bytes".into()))?; + let sig = repo_state + .sig + .ok_or_else(|| AppError::Internal("corrupt repo state: missing sig".into()))?; + let rev = repo_state + .rev + .ok_or_else(|| AppError::Internal("corrupt repo state: missing rev".into()))?; let commit = crate::spaces::commit::SignedCommit { ver: 1, - hash: repo_state - .hash - .as_deref() - .and_then(|h| <[u8; 32]>::try_from(h).ok()) - .unwrap_or([0u8; 32]), - ikm: repo_state - .ikm - .as_deref() - .and_then(|i| <[u8; 32]>::try_from(i).ok()) - .unwrap_or([0u8; 32]), - sig: repo_state.sig.unwrap_or_default(), - mac: repo_state - .mac - .as_deref() - .and_then(|m| <[u8; 32]>::try_from(m).ok()) - .unwrap_or([0u8; 32]), - rev: repo_state.rev.unwrap_or_default(), + hash, + ikm, + sig, + mac, + rev, }; let car_bytes = crate::spaces::car::serialize_repo(&commit, &records)?;