Our Personal Data Server from scratch! tranquil.farm
6

Configure Feed

Select the types of activity you want to include in your feed.

plc: always keep signing key in rotationKeys #1

Open opened by oyster.cafe targeting main from plc/keep-sign-key-in-rot
Labels

None yet.

Participants 1
AT URI
at://did:plc:3fwecdnvtcscjnrx2p4n7alz/sh.tangled.repo.pull/3mp7cczos7b22
+394 -128
Diff #0
+4 -10
crates/tranquil-api/src/identity/did.rs
··· 491 491 let rotation_keys = if auth.did.starts_with("did:web:") { 492 492 vec![] 493 493 } else { 494 - let server_rotation_key = match &tranquil_config::get().secrets.plc_rotation_key { 495 - Some(key) => key.clone(), 496 - None => { 497 - warn!( 498 - "PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation" 499 - ); 500 - did_key.clone() 501 - } 502 - }; 503 - vec![server_rotation_key] 494 + tranquil_pds::plc::rotation_keys_for( 495 + tranquil_config::get().secrets.plc_rotation_key.as_deref(), 496 + &signing_key, 497 + ) 504 498 }; 505 499 Ok(Json(GetRecommendedDidCredentialsOutput { 506 500 rotation_keys,
+16 -1
crates/tranquil-api/src/identity/plc/sign.rs
··· 9 9 use tranquil_pds::api::error::DbResultExt; 10 10 use tranquil_pds::auth::{Auth, Permissive}; 11 11 use tranquil_pds::circuit_breaker::with_circuit_breaker; 12 - use tranquil_pds::plc::{PlcError, PlcService, create_update_op, sign_operation}; 12 + use tranquil_pds::plc::{ 13 + PlcError, PlcService, create_update_op, missing_required_rotation_key, sign_operation, 14 + signing_key_to_did_key, 15 + }; 13 16 use tranquil_pds::state::AppState; 14 17 15 18 #[derive(Debug, Deserialize)] ··· 115 118 } 116 119 })?; 117 120 121 + let signing_did_key = signing_key_to_did_key(&signing_key); 122 + if let Some(rotation_keys) = unsigned_op.get("rotationKeys").and_then(Value::as_array) { 123 + let rotation_key_strs: Vec<&str> = rotation_keys.iter().filter_map(Value::as_str).collect(); 124 + if let Some(missing) = missing_required_rotation_key( 125 + &rotation_key_strs, 126 + &signing_did_key, 127 + tranquil_config::get().secrets.plc_rotation_key.as_deref(), 128 + ) { 129 + return Err(ApiError::InvalidRequest(missing.message().into())); 130 + } 131 + } 132 + 118 133 let signed_op = sign_operation(&unsigned_op, &signing_key).map_err(|e| { 119 134 error!("Failed to sign PLC operation: {:?}", e); 120 135 ApiError::InternalError(None)
+7 -12
crates/tranquil-api/src/identity/plc/submit.rs
··· 67 67 })?; 68 68 69 69 let user_did_key = signing_key_to_did_key(&signing_key); 70 - let server_rotation_key = tranquil_config::get() 71 - .secrets 72 - .plc_rotation_key 73 - .clone() 74 - .unwrap_or_else(|| user_did_key.clone()); 75 70 if let Some(rotation_keys) = op.get("rotationKeys").and_then(Value::as_array) { 76 - let has_server_key = rotation_keys 77 - .iter() 78 - .any(|k| k.as_str() == Some(&server_rotation_key)); 79 - if !has_server_key { 80 - return Err(ApiError::InvalidRequest( 81 - "Rotation keys do not include server's rotation key".into(), 82 - )); 71 + let rotation_key_strs: Vec<&str> = rotation_keys.iter().filter_map(Value::as_str).collect(); 72 + if let Some(missing) = tranquil_pds::plc::missing_required_rotation_key( 73 + &rotation_key_strs, 74 + &user_did_key, 75 + tranquil_config::get().secrets.plc_rotation_key.as_deref(), 76 + ) { 77 + return Err(ApiError::InvalidRequest(missing.message().into())); 83 78 } 84 79 } 85 80 if let Some(services) = op.get("services").and_then(Value::as_object)
+1 -7
crates/tranquil-api/src/identity/provision.rs
··· 45 45 let hostname = &tranquil_config::get().server.hostname; 46 46 let pds_endpoint = format!("https://{}", hostname); 47 47 48 - let rotation_key = tranquil_config::get() 49 - .secrets 50 - .plc_rotation_key 51 - .clone() 52 - .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(signing_key)); 53 - 54 48 let genesis_result = tranquil_pds::plc::create_genesis_operation( 55 49 signing_key, 56 - &rotation_key, 50 + tranquil_config::get().secrets.plc_rotation_key.as_deref(), 57 51 handle, 58 52 &pds_endpoint, 59 53 )
+43 -32
crates/tranquil-api/src/repo/import.rs
··· 14 14 use tranquil_pds::types::Did; 15 15 use tranquil_types::{AtUri, CidLink}; 16 16 17 + fn map_car_verify_error(e: tranquil_pds::sync::verify::VerifyError) -> ApiError { 18 + use tranquil_pds::sync::verify::VerifyError; 19 + match e { 20 + VerifyError::DidMismatch { 21 + commit_did, 22 + expected_did, 23 + } => ApiError::InvalidRepo(format!( 24 + "CAR file is for DID {} but you are authenticated as {}", 25 + commit_did, expected_did 26 + )), 27 + VerifyError::InvalidSignature => ApiError::InvalidRequest( 28 + "Repo commit signature does not match the DID document signing key".into(), 29 + ), 30 + VerifyError::NoSigningKey => { 31 + ApiError::InvalidRequest("DID document has no atproto signing key".into()) 32 + } 33 + VerifyError::DidResolutionFailed(msg) => { 34 + ApiError::InvalidRequest(format!("Could not resolve DID document: {}", msg)) 35 + } 36 + VerifyError::MstValidationFailed(msg) => { 37 + ApiError::InvalidRequest(format!("MST validation failed: {}", msg)) 38 + } 39 + other => { 40 + error!("CAR verification failed: {:?}", other); 41 + ApiError::InvalidRequest(format!("CAR verification failed: {}", other)) 42 + } 43 + } 44 + } 45 + 17 46 pub async fn import_repo( 18 47 State(state): State<AppState>, 19 48 auth: Auth<NotTakendown>, ··· 88 117 let is_migration = user.inbound_migration && user.deactivated_at.is_some(); 89 118 if skip_verification { 90 119 warn!("Skipping all CAR verification for repo import (SKIP_IMPORT_VERIFICATION=true)"); 120 + } else if is_migration { 121 + let verified = CarVerifier::new() 122 + .verify_car_structure_only(&root, &blocks) 123 + .map_err(map_car_verify_error)?; 124 + debug!( 125 + "CAR structure verified for migration import: rev={}, data_cid={}", 126 + verified.rev, verified.data_cid 127 + ); 91 128 } else { 129 + let verified = CarVerifier::new() 130 + .verify_car(did, &root, &blocks) 131 + .await 132 + .map_err(map_car_verify_error)?; 92 133 debug!( 93 - "Verifying CAR file structure for repo import (skipping signature and DID verification)" 134 + "CAR signature and structure verified: rev={}, data_cid={}", 135 + verified.rev, verified.data_cid 94 136 ); 95 - let verifier = CarVerifier::new(); 96 - match verifier.verify_car_structure_only(&root, &blocks) { 97 - Ok(verified) => { 98 - debug!( 99 - "CAR structure verification successful: rev={}, data_cid={}", 100 - verified.rev, verified.data_cid 101 - ); 102 - } 103 - Err(tranquil_pds::sync::verify::VerifyError::DidMismatch { 104 - commit_did, 105 - expected_did, 106 - }) => { 107 - return Err(ApiError::InvalidRepo(format!( 108 - "CAR file is for DID {} but you are authenticated as {}", 109 - commit_did, expected_did 110 - ))); 111 - } 112 - Err(tranquil_pds::sync::verify::VerifyError::MstValidationFailed(msg)) => { 113 - return Err(ApiError::InvalidRequest(format!( 114 - "MST validation failed: {}", 115 - msg 116 - ))); 117 - } 118 - Err(e) => { 119 - error!("CAR structure verification error: {:?}", e); 120 - return Err(ApiError::InvalidRequest(format!( 121 - "CAR verification failed: {}", 122 - e 123 - ))); 124 - } 125 - } 126 137 } 127 138 let max_blocks = tranquil_config::get().import.max_blocks as usize; 128 139 let _write_lock = state.repo_write_locks.lock(user_id).await;
+22 -11
crates/tranquil-api/src/server/account_status.rs
··· 197 197 .await 198 198 .map_err(ApiError::InvalidRequest)?; 199 199 200 + let doc_rotation_keys = doc_data 201 + .get("rotationKeys") 202 + .and_then(Value::as_array) 203 + .map(|arr| arr.iter().filter_map(Value::as_str).collect::<Vec<_>>()) 204 + .unwrap_or_default(); 205 + 200 206 let server_rotation_key = tranquil_config::get().secrets.plc_rotation_key.clone(); 201 - if let Some(ref expected_rotation_key) = server_rotation_key { 202 - let rotation_keys = doc_data 203 - .get("rotationKeys") 204 - .and_then(Value::as_array) 205 - .map(|arr| arr.iter().filter_map(Value::as_str).collect::<Vec<_>>()) 206 - .unwrap_or_default(); 207 - if !rotation_keys.contains(&expected_rotation_key.as_str()) { 208 - return Err(ApiError::InvalidRequest( 209 - "Server rotation key not included in PLC DID data".into(), 210 - )); 211 - } 207 + if let Some(ref expected_rotation_key) = server_rotation_key 208 + && !doc_rotation_keys.contains(&expected_rotation_key.as_str()) 209 + { 210 + return Err(ApiError::InvalidRequest( 211 + "Server rotation key not included in PLC DID data".into(), 212 + )); 212 213 } 213 214 214 215 let doc_signing_key = doc_data ··· 243 244 "DID document verification method does not match expected signing key".into(), 244 245 )); 245 246 } 247 + 248 + if !doc_rotation_keys.contains(&expected_did_key.as_str()) { 249 + warn!( 250 + "DID {} rotation keys {:?} omit the PDS-managed signing key {}", 251 + did, doc_rotation_keys, expected_did_key 252 + ); 253 + return Err(ApiError::InvalidRequest( 254 + "PLC rotation keys omit the PDS-managed signing key required to sign operations for this identity".into(), 255 + )); 256 + } 246 257 } 247 258 } else if let Some(host_and_path) = did.as_str().strip_prefix("did:web:") { 248 259 let client = tranquil_pds::api::proxy_client::did_resolution_client();
+1 -7
crates/tranquil-api/src/server/passkey_account.rs
··· 224 224 )); 225 225 } 226 226 } else { 227 - let rotation_key = tranquil_config::get() 228 - .secrets 229 - .plc_rotation_key 230 - .clone() 231 - .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&secret_key)); 232 - 233 227 let genesis_result = match tranquil_pds::plc::create_genesis_operation( 234 228 &secret_key, 235 - &rotation_key, 229 + tranquil_config::get().secrets.plc_rotation_key.as_deref(), 236 230 &handle, 237 231 &pds_endpoint, 238 232 ) {
+3 -1
crates/tranquil-config/src/lib.rs
··· 632 632 #[config(env = "MASTER_KEY")] 633 633 pub master_key: Option<String>, 634 634 635 - /// PLC rotation key (DID key). If not set, user-level keys are used. 635 + /// Optional operator-held PLC recovery key, as a public `did:key`. The PDS 636 + /// continues to sign PLC operations with the per-account signing key, which 637 + /// always remains in `rotationKeys`. 636 638 #[config(env = "PLC_ROTATION_KEY")] 637 639 pub plc_rotation_key: Option<String>, 638 640
+1 -7
crates/tranquil-oauth-server/src/sso_endpoints.rs
··· 1043 1043 d.to_string() 1044 1044 } 1045 1045 _ => { 1046 - let rotation_key = tranquil_config::get() 1047 - .secrets 1048 - .plc_rotation_key 1049 - .clone() 1050 - .unwrap_or_else(|| tranquil_pds::plc::signing_key_to_did_key(&signing_key)); 1051 - 1052 1046 let genesis_result = match tranquil_pds::plc::create_genesis_operation( 1053 1047 &signing_key, 1054 - &rotation_key, 1048 + tranquil_config::get().secrets.plc_rotation_key.as_deref(), 1055 1049 &handle, 1056 1050 &pds_endpoint, 1057 1051 ) {
+53 -10
crates/tranquil-pds/src/api/invite.rs
··· 18 18 } 19 19 } 20 20 21 + fn bootstrap_registration( 22 + expected_code: &str, 23 + user_count: i64, 24 + invite_code: Option<&str>, 25 + ) -> Option<Result<InviteRegistration, ApiError>> { 26 + if user_count != 0 { 27 + return None; 28 + } 29 + Some(match invite_code { 30 + Some(code) if code == expected_code => Ok(InviteRegistration::Bootstrap), 31 + _ => Err(ApiError::InvalidInviteCode), 32 + }) 33 + } 34 + 21 35 pub async fn check_registration_invite( 22 36 state: &AppState, 23 37 invite_code: Option<&str>, 24 38 ) -> Result<InviteRegistration, ApiError> { 25 - let is_bootstrap = state.bootstrap_invite_code.is_some() 26 - && state.repos.user.count_users().await.unwrap_or(1) == 0; 27 - 28 - if is_bootstrap { 29 - return match invite_code { 30 - Some(code) if Some(code) == state.bootstrap_invite_code.as_deref() => { 31 - Ok(InviteRegistration::Bootstrap) 32 - } 33 - _ => Err(ApiError::InvalidInviteCode), 34 - }; 39 + if let Some(expected) = state.bootstrap_invite_code.as_deref() { 40 + let user_count = state.repos.user.count_users().await.unwrap_or(1); 41 + if let Some(decision) = bootstrap_registration(expected, user_count, invite_code) { 42 + return decision; 43 + } 35 44 } 36 45 37 46 match invite_code.map(str::trim).filter(|code| !code.is_empty()) { ··· 49 58 }, 50 59 } 51 60 } 61 + 62 + #[cfg(test)] 63 + mod tests { 64 + use super::*; 65 + 66 + #[test] 67 + fn bootstrap_taken_for_matching_code_on_empty_instance() { 68 + assert!(matches!( 69 + bootstrap_registration("squid-bootstrap", 0, Some("squid-bootstrap")), 70 + Some(Ok(InviteRegistration::Bootstrap)) 71 + )); 72 + } 73 + 74 + #[test] 75 + fn bootstrap_rejects_wrong_code_on_empty_instance() { 76 + assert!(matches!( 77 + bootstrap_registration("squid-bootstrap", 0, Some("whelk")), 78 + Some(Err(ApiError::InvalidInviteCode)) 79 + )); 80 + } 81 + 82 + #[test] 83 + fn bootstrap_rejects_missing_code_on_empty_instance() { 84 + assert!(matches!( 85 + bootstrap_registration("squid-bootstrap", 0, None), 86 + Some(Err(ApiError::InvalidInviteCode)) 87 + )); 88 + } 89 + 90 + #[test] 91 + fn bootstrap_falls_through_once_users_exist() { 92 + assert!(bootstrap_registration("squid-bootstrap", 1, Some("squid-bootstrap")).is_none()); 93 + } 94 + }
+225 -3
crates/tranquil-pds/src/plc/mod.rs
··· 114 114 } 115 115 116 116 pub const SECP256K1_MULTICODEC_PREFIX: [u8; 2] = [0xe7, 0x01]; 117 + pub const P256_MULTICODEC_PREFIX: [u8; 2] = [0x80, 0x24]; 117 118 118 119 #[derive(Debug, Clone, Serialize, Deserialize)] 119 120 pub struct PlcOperation { ··· 415 416 format!("did:key:{}", encoded) 416 417 } 417 418 419 + pub fn rotation_keys_for( 420 + configured_rotation_key: Option<&str>, 421 + signing_key: &SigningKey, 422 + ) -> Vec<String> { 423 + let signing_did_key = signing_key_to_did_key(signing_key); 424 + match configured_rotation_key { 425 + Some(key) if key != signing_did_key.as_str() => vec![key.to_string(), signing_did_key], 426 + _ => vec![signing_did_key], 427 + } 428 + } 429 + 430 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 431 + pub enum RequiredRotationKey { 432 + Signing, 433 + Operator, 434 + } 435 + 436 + impl RequiredRotationKey { 437 + pub fn message(self) -> &'static str { 438 + match self { 439 + Self::Signing => { 440 + "Rotation keys must include the PDS-managed signing key so the server can sign future operations" 441 + } 442 + Self::Operator => { 443 + "Rotation keys must include the operator-held PLC recovery key configured for this server" 444 + } 445 + } 446 + } 447 + } 448 + 449 + pub fn missing_required_rotation_key( 450 + rotation_keys: &[&str], 451 + signing_did_key: &str, 452 + configured_rotation_key: Option<&str>, 453 + ) -> Option<RequiredRotationKey> { 454 + if !rotation_keys.contains(&signing_did_key) { 455 + return Some(RequiredRotationKey::Signing); 456 + } 457 + match configured_rotation_key { 458 + Some(operator_key) if !rotation_keys.contains(&operator_key) => { 459 + Some(RequiredRotationKey::Operator) 460 + } 461 + _ => None, 462 + } 463 + } 464 + 465 + fn validate_compressed_did_key<V, E>( 466 + key_bytes: &[u8], 467 + label: &str, 468 + did_key: &str, 469 + parse: impl Fn(&[u8]) -> Result<V, E>, 470 + ) -> Result<(), String> 471 + where 472 + E: std::fmt::Display, 473 + { 474 + if key_bytes.len() != 33 { 475 + return Err(format!( 476 + "`{did_key}` must be a compressed {label} public key" 477 + )); 478 + } 479 + parse(key_bytes) 480 + .map(|_| ()) 481 + .map_err(|e| format!("`{did_key}` is not a valid {label} public key: {e}")) 482 + } 483 + 484 + pub fn validate_rotation_did_key(did_key: &str) -> Result<(), String> { 485 + let multibase_part = did_key 486 + .strip_prefix("did:key:") 487 + .ok_or_else(|| format!("must be a did:key, got `{did_key}`"))?; 488 + let (base, decoded) = multibase::decode(multibase_part) 489 + .map_err(|e| format!("`{did_key}` is not valid multibase: {e}"))?; 490 + if base != multibase::Base::Base58Btc { 491 + return Err(format!("`{did_key}` must use base58btc multibase encoding")); 492 + } 493 + let (prefix, key_bytes) = decoded.split_at(decoded.len().min(2)); 494 + if prefix == SECP256K1_MULTICODEC_PREFIX { 495 + validate_compressed_did_key( 496 + key_bytes, 497 + "secp256k1", 498 + did_key, 499 + k256::ecdsa::VerifyingKey::from_sec1_bytes, 500 + ) 501 + } else if prefix == P256_MULTICODEC_PREFIX { 502 + validate_compressed_did_key( 503 + key_bytes, 504 + "p256", 505 + did_key, 506 + p256::ecdsa::VerifyingKey::from_sec1_bytes, 507 + ) 508 + } else { 509 + Err(format!("`{did_key}` is not a secp256k1 or p256 did:key")) 510 + } 511 + } 512 + 418 513 pub struct GenesisResult { 419 514 pub did: String, 420 515 pub signed_operation: Value, ··· 422 517 423 518 pub fn create_genesis_operation( 424 519 signing_key: &SigningKey, 425 - rotation_key: &str, 520 + configured_rotation_key: Option<&str>, 426 521 handle: &str, 427 522 pds_endpoint: &str, 428 523 ) -> Result<GenesisResult, PlcError> { 429 524 let signing_did_key = signing_key_to_did_key(signing_key); 525 + let rotation_keys = rotation_keys_for(configured_rotation_key, signing_key); 430 526 let mut verification_methods = HashMap::new(); 431 - verification_methods.insert("atproto".to_string(), signing_did_key.clone()); 527 + verification_methods.insert("atproto".to_string(), signing_did_key); 432 528 let mut services = HashMap::new(); 433 529 services.insert( 434 530 "atproto_pds".to_string(), ··· 439 535 ); 440 536 let genesis_op = PlcOperation { 441 537 op_type: PlcOpType::Operation, 442 - rotation_keys: vec![rotation_key.to_string()], 538 + rotation_keys, 443 539 verification_methods, 444 540 also_known_as: vec![format!("at://{}", handle)], 445 541 services, ··· 651 747 assert!(did_key.starts_with("did:key:z")); 652 748 } 653 749 750 + #[test] 751 + fn test_rotation_keys_default_is_signing_key() { 752 + let key = SigningKey::random(&mut rand::thread_rng()); 753 + let signing_did_key = signing_key_to_did_key(&key); 754 + assert_eq!(rotation_keys_for(None, &key), vec![signing_did_key]); 755 + } 756 + 757 + #[test] 758 + fn test_rotation_keys_prepends_operator_key() { 759 + let key = SigningKey::random(&mut rand::thread_rng()); 760 + let signing_did_key = signing_key_to_did_key(&key); 761 + let operator_key = "did:key:zQ3shScallopRecoveryKey"; 762 + assert_eq!( 763 + rotation_keys_for(Some(operator_key), &key), 764 + vec![operator_key.to_string(), signing_did_key.clone()] 765 + ); 766 + } 767 + 768 + #[test] 769 + fn test_rotation_keys_dedupes_when_operator_equals_signing() { 770 + let key = SigningKey::random(&mut rand::thread_rng()); 771 + let signing_did_key = signing_key_to_did_key(&key); 772 + assert_eq!( 773 + rotation_keys_for(Some(&signing_did_key), &key), 774 + vec![signing_did_key] 775 + ); 776 + } 777 + 778 + #[test] 779 + fn test_genesis_includes_signing_key_with_operator_rotation_key() { 780 + let key = SigningKey::random(&mut rand::thread_rng()); 781 + let signing_did_key = signing_key_to_did_key(&key); 782 + let operator_key = "did:key:zQ3shWhelkOperatorKey"; 783 + let result = 784 + create_genesis_operation(&key, Some(operator_key), "whelk.nel.pet", "https://nel.pet") 785 + .unwrap(); 786 + let rotation_keys = result.signed_operation["rotationKeys"] 787 + .as_array() 788 + .unwrap() 789 + .iter() 790 + .filter_map(|v| v.as_str()) 791 + .collect::<Vec<_>>(); 792 + assert_eq!(rotation_keys, vec![operator_key, signing_did_key.as_str()]); 793 + assert!( 794 + verify_operation_signature( 795 + &result.signed_operation, 796 + &[operator_key.to_string(), signing_did_key] 797 + ) 798 + .unwrap() 799 + ); 800 + } 801 + 802 + fn p256_did_key(key: &p256::ecdsa::SigningKey) -> String { 803 + let point = key.verifying_key().to_encoded_point(true); 804 + let mut prefixed = Vec::from(P256_MULTICODEC_PREFIX); 805 + prefixed.extend_from_slice(point.as_bytes()); 806 + format!( 807 + "did:key:{}", 808 + multibase::encode(multibase::Base::Base58Btc, &prefixed) 809 + ) 810 + } 811 + 812 + #[test] 813 + fn test_validate_rotation_did_key_accepts_secp256k1() { 814 + let key = SigningKey::random(&mut rand::thread_rng()); 815 + assert!(validate_rotation_did_key(&signing_key_to_did_key(&key)).is_ok()); 816 + } 817 + 818 + #[test] 819 + fn test_validate_rotation_did_key_accepts_p256() { 820 + let key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng()); 821 + assert!(validate_rotation_did_key(&p256_did_key(&key)).is_ok()); 822 + } 823 + 824 + #[test] 825 + fn test_validate_rotation_did_key_rejects_non_did_key() { 826 + assert!(validate_rotation_did_key("did:plc:squid").is_err()); 827 + assert!(validate_rotation_did_key("zSomeMultibaseButNoPrefix").is_err()); 828 + } 829 + 830 + #[test] 831 + fn test_validate_rotation_did_key_rejects_unknown_multicodec() { 832 + let mut ed25519 = vec![0xed, 0x01]; 833 + ed25519.extend_from_slice(&[0u8; 32]); 834 + let did_key = format!( 835 + "did:key:{}", 836 + multibase::encode(multibase::Base::Base58Btc, &ed25519) 837 + ); 838 + assert!(validate_rotation_did_key(&did_key).is_err()); 839 + } 840 + 841 + #[test] 842 + fn test_validate_rotation_did_key_rejects_non_base58btc() { 843 + let key = SigningKey::random(&mut rand::thread_rng()); 844 + let point = key.verifying_key().to_encoded_point(true); 845 + let mut prefixed = Vec::from(SECP256K1_MULTICODEC_PREFIX); 846 + prefixed.extend_from_slice(point.as_bytes()); 847 + let hex_did_key = format!( 848 + "did:key:{}", 849 + multibase::encode(multibase::Base::Base16Lower, &prefixed) 850 + ); 851 + assert!(validate_rotation_did_key(&hex_did_key).is_err()); 852 + } 853 + 854 + #[test] 855 + fn test_missing_required_rotation_key() { 856 + let signing = "did:key:zSigning"; 857 + let operator = "did:key:zOperator"; 858 + assert_eq!( 859 + missing_required_rotation_key(&[operator], signing, None), 860 + Some(RequiredRotationKey::Signing) 861 + ); 862 + assert_eq!( 863 + missing_required_rotation_key(&[signing], signing, Some(operator)), 864 + Some(RequiredRotationKey::Operator) 865 + ); 866 + assert_eq!( 867 + missing_required_rotation_key(&[operator, signing], signing, Some(operator)), 868 + None 869 + ); 870 + assert_eq!( 871 + missing_required_rotation_key(&[signing], signing, None), 872 + None 873 + ); 874 + } 875 + 654 876 #[test] 655 877 fn test_cid_for_cbor() { 656 878 let value = json!({
-24
crates/tranquil-pds/tests/invite_registration.rs
··· 25 25 async fn check_registration_invite_validates_without_consuming() { 26 26 let state = get_test_app_state().await; 27 27 28 - assert_eq!( 29 - state.repos.user.count_users().await.unwrap(), 30 - 0, 31 - "bootstrap branch needs a zero-user instance" 32 - ); 33 - 34 - let mut bootstrap = state.clone(); 35 - bootstrap.bootstrap_invite_code = Some("squid-bootstrap".to_string()); 36 - 37 - assert_eq!( 38 - check_registration_invite(&bootstrap, Some("squid-bootstrap")) 39 - .await 40 - .unwrap(), 41 - InviteRegistration::Bootstrap 42 - ); 43 - assert!(matches!( 44 - check_registration_invite(&bootstrap, Some("whelk")).await, 45 - Err(ApiError::InvalidInviteCode) 46 - )); 47 - assert!(matches!( 48 - check_registration_invite(&bootstrap, None).await, 49 - Err(ApiError::InvalidInviteCode) 50 - )); 51 - 52 28 let client = client(); 53 29 let (admin_jwt, _did) = create_admin_account_and_login(&client).await; 54 30 let code = create_invite_code(&client, &admin_jwt, 1).await;
+13
crates/tranquil-server/src/main.rs
··· 62 62 eprint!("{e}"); 63 63 return ExitCode::FAILURE; 64 64 } 65 + if let Some(key) = config.secrets.plc_rotation_key.as_deref() 66 + && let Err(reason) = tranquil_pds::plc::validate_rotation_did_key(key) 67 + { 68 + eprintln!("secrets.plc_rotation_key (PLC_ROTATION_KEY) {reason}"); 69 + return ExitCode::FAILURE; 70 + } 65 71 if !*ignore_secrets 66 72 && let Some((cert, key)) = config.server.tls.material() 67 73 && let Err(e) = tls::load_certified_key(cert, key) ··· 90 96 return ExitCode::FAILURE; 91 97 } 92 98 99 + if let Some(key) = config.secrets.plc_rotation_key.as_deref() 100 + && let Err(reason) = tranquil_pds::plc::validate_rotation_did_key(key) 101 + { 102 + error!("secrets.plc_rotation_key (PLC_ROTATION_KEY) {reason}"); 103 + return ExitCode::FAILURE; 104 + } 105 + 93 106 tranquil_config::init(config); 94 107 95 108 tranquil_pds::metrics::init_metrics();
+5 -3
example.toml
··· 4 4 # Can also be specified via environment variable `PDS_HOSTNAME`. 5 5 # 6 6 # Required! This value must be specified. 7 - #hostname = "pds.example.com" 7 + #hostname = 8 8 9 9 # Address to bind the HTTP server to. 10 10 # ··· 24 24 # Defaults to the PDS hostname when not set. 25 25 # 26 26 # Can also be specified via environment variable `PDS_USER_HANDLE_DOMAINS`. 27 - #user_handle_domains = ["example.com"] 27 + #user_handle_domains = 28 28 29 29 # Enable PDS-hosted did:web identities. Hosting did:web requires a 30 30 # long-term commitment to serve DID documents; opt-in only. ··· 200 200 # Can also be specified via environment variable `MASTER_KEY`. 201 201 #master_key = 202 202 203 - # PLC rotation key (DID key). If not set, user-level keys are used. 203 + # Optional operator-held PLC recovery key, as a public `did:key`. The PDS 204 + # continues to sign PLC operations with the per-account signing key, which 205 + # always remains in `rotationKeys`. 204 206 # 205 207 # Can also be specified via environment variable `PLC_ROTATION_KEY`. 206 208 #plc_rotation_key =

History

1 round 0 comments
Sign up or Login to add to the discussion
oyster.cafe submitted #0
1 commit
Expand
plc: always keep signing key in rotationKeys
Checking mergeability…
Expand 0 comments