Lewis: May this revision serve well! lu5a@proton.me
+394
-128
Diff
Round #0
+4
-10
crates/tranquil-api/src/identity/did.rs
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+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
oyster.cafe
submitted
#0
1 commit
Expand
Collapse
plc: always keep signing key in rotationKeys
Lewis: May this revision serve well! <lu5a@proton.me>