mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-30 05:55:33 +00:00
200 lines
9.7 KiB
Rust
200 lines
9.7 KiB
Rust
use bspds::plc::{
|
|
PlcError, PlcOperation, PlcService, PlcValidationContext, cid_for_cbor, sign_operation,
|
|
signing_key_to_did_key, validate_plc_operation, validate_plc_operation_for_submission,
|
|
verify_operation_signature,
|
|
};
|
|
use k256::ecdsa::SigningKey;
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
|
|
fn create_valid_operation() -> serde_json::Value {
|
|
let key = SigningKey::random(&mut rand::thread_rng());
|
|
let did_key = signing_key_to_did_key(&key);
|
|
let op = json!({
|
|
"type": "plc_operation",
|
|
"rotationKeys": [did_key.clone()],
|
|
"verificationMethods": { "atproto": did_key.clone() },
|
|
"alsoKnownAs": ["at://test.handle"],
|
|
"services": {
|
|
"atproto_pds": {
|
|
"type": "AtprotoPersonalDataServer",
|
|
"endpoint": "https://pds.example.com"
|
|
}
|
|
},
|
|
"prev": null
|
|
});
|
|
sign_operation(&op, &key).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_plc_operation_basic_validation() {
|
|
let op = create_valid_operation();
|
|
assert!(validate_plc_operation(&op).is_ok());
|
|
|
|
let missing_type = json!({ "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "sig": "test" });
|
|
assert!(matches!(validate_plc_operation(&missing_type), Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing type")));
|
|
|
|
let invalid_type = json!({ "type": "invalid_type", "sig": "test" });
|
|
assert!(matches!(validate_plc_operation(&invalid_type), Err(PlcError::InvalidResponse(msg)) if msg.contains("Invalid type")));
|
|
|
|
let missing_sig = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {} });
|
|
assert!(matches!(validate_plc_operation(&missing_sig), Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing sig")));
|
|
|
|
let missing_rotation = json!({ "type": "plc_operation", "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "sig": "test" });
|
|
assert!(matches!(validate_plc_operation(&missing_rotation), Err(PlcError::InvalidResponse(msg)) if msg.contains("rotationKeys")));
|
|
|
|
let missing_verification = json!({ "type": "plc_operation", "rotationKeys": [], "alsoKnownAs": [], "services": {}, "sig": "test" });
|
|
assert!(matches!(validate_plc_operation(&missing_verification), Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods")));
|
|
|
|
let missing_aka = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "services": {}, "sig": "test" });
|
|
assert!(matches!(validate_plc_operation(&missing_aka), Err(PlcError::InvalidResponse(msg)) if msg.contains("alsoKnownAs")));
|
|
|
|
let missing_services = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "sig": "test" });
|
|
assert!(matches!(validate_plc_operation(&missing_services), Err(PlcError::InvalidResponse(msg)) if msg.contains("services")));
|
|
|
|
assert!(matches!(validate_plc_operation(&json!("not an object")), Err(PlcError::InvalidResponse(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_plc_submission_validation() {
|
|
let key = SigningKey::random(&mut rand::thread_rng());
|
|
let did_key = signing_key_to_did_key(&key);
|
|
let server_key = "did:key:zServer123";
|
|
|
|
let base_op = |rotation_key: &str, signing_key: &str, handle: &str, service_type: &str, endpoint: &str| json!({
|
|
"type": "plc_operation",
|
|
"rotationKeys": [rotation_key],
|
|
"verificationMethods": {"atproto": signing_key},
|
|
"alsoKnownAs": [format!("at://{}", handle)],
|
|
"services": { "atproto_pds": { "type": service_type, "endpoint": endpoint } },
|
|
"sig": "test"
|
|
});
|
|
|
|
let ctx = PlcValidationContext {
|
|
server_rotation_key: server_key.to_string(),
|
|
expected_signing_key: did_key.clone(),
|
|
expected_handle: "test.handle".to_string(),
|
|
expected_pds_endpoint: "https://pds.example.com".to_string(),
|
|
};
|
|
|
|
let op = base_op(&did_key, &did_key, "test.handle", "AtprotoPersonalDataServer", "https://pds.example.com");
|
|
assert!(matches!(validate_plc_operation_for_submission(&op, &ctx), Err(PlcError::InvalidResponse(msg)) if msg.contains("rotation key")));
|
|
|
|
let ctx_with_user_key = PlcValidationContext {
|
|
server_rotation_key: did_key.clone(),
|
|
expected_signing_key: did_key.clone(),
|
|
expected_handle: "test.handle".to_string(),
|
|
expected_pds_endpoint: "https://pds.example.com".to_string(),
|
|
};
|
|
|
|
let wrong_signing = base_op(&did_key, "did:key:zWrongKey", "test.handle", "AtprotoPersonalDataServer", "https://pds.example.com");
|
|
assert!(matches!(validate_plc_operation_for_submission(&wrong_signing, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("signing key")));
|
|
|
|
let wrong_handle = base_op(&did_key, &did_key, "wrong.handle", "AtprotoPersonalDataServer", "https://pds.example.com");
|
|
assert!(matches!(validate_plc_operation_for_submission(&wrong_handle, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("handle")));
|
|
|
|
let wrong_service_type = base_op(&did_key, &did_key, "test.handle", "WrongServiceType", "https://pds.example.com");
|
|
assert!(matches!(validate_plc_operation_for_submission(&wrong_service_type, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("type")));
|
|
|
|
let wrong_endpoint = base_op(&did_key, &did_key, "test.handle", "AtprotoPersonalDataServer", "https://wrong.endpoint.com");
|
|
assert!(matches!(validate_plc_operation_for_submission(&wrong_endpoint, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("endpoint")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_signature_verification() {
|
|
let key = SigningKey::random(&mut rand::thread_rng());
|
|
let did_key = signing_key_to_did_key(&key);
|
|
let op = json!({
|
|
"type": "plc_operation", "rotationKeys": [did_key.clone()],
|
|
"verificationMethods": {}, "alsoKnownAs": [], "services": {}, "prev": null
|
|
});
|
|
let signed = sign_operation(&op, &key).unwrap();
|
|
let result = verify_operation_signature(&signed, &[did_key.clone()]);
|
|
assert!(result.is_ok() && result.unwrap());
|
|
|
|
let other_key = SigningKey::random(&mut rand::thread_rng());
|
|
let other_did = signing_key_to_did_key(&other_key);
|
|
let result = verify_operation_signature(&signed, &[other_did]);
|
|
assert!(result.is_ok() && !result.unwrap());
|
|
|
|
let result = verify_operation_signature(&signed, &["not-a-did-key".to_string()]);
|
|
assert!(result.is_ok() && !result.unwrap());
|
|
|
|
let missing_sig = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {} });
|
|
assert!(matches!(verify_operation_signature(&missing_sig, &[]), Err(PlcError::InvalidResponse(msg)) if msg.contains("sig")));
|
|
|
|
let invalid_base64 = json!({
|
|
"type": "plc_operation", "rotationKeys": [], "verificationMethods": {},
|
|
"alsoKnownAs": [], "services": {}, "sig": "not-valid-base64!!!"
|
|
});
|
|
assert!(matches!(verify_operation_signature(&invalid_base64, &[]), Err(PlcError::InvalidResponse(_))));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cid_and_key_utilities() {
|
|
let value = json!({ "alpha": 1, "beta": 2 });
|
|
let cid1 = cid_for_cbor(&value).unwrap();
|
|
let cid2 = cid_for_cbor(&value).unwrap();
|
|
assert_eq!(cid1, cid2, "CID should be deterministic");
|
|
assert!(cid1.starts_with("bafyrei"), "CID should be dag-cbor + sha256");
|
|
|
|
let value2 = json!({ "alpha": 999 });
|
|
let cid3 = cid_for_cbor(&value2).unwrap();
|
|
assert_ne!(cid1, cid3, "Different data should produce different CIDs");
|
|
|
|
let key = SigningKey::random(&mut rand::thread_rng());
|
|
let did = signing_key_to_did_key(&key);
|
|
assert!(did.starts_with("did:key:z") && did.len() > 50);
|
|
assert_eq!(did, signing_key_to_did_key(&key), "Same key should produce same did");
|
|
|
|
let key2 = SigningKey::random(&mut rand::thread_rng());
|
|
assert_ne!(did, signing_key_to_did_key(&key2), "Different keys should produce different dids");
|
|
}
|
|
|
|
#[test]
|
|
fn test_tombstone_operations() {
|
|
let tombstone = json!({ "type": "plc_tombstone", "prev": "bafyreig6xxxxxyyyyyzzzzzz", "sig": "test" });
|
|
assert!(validate_plc_operation(&tombstone).is_ok());
|
|
|
|
let key = SigningKey::random(&mut rand::thread_rng());
|
|
let did_key = signing_key_to_did_key(&key);
|
|
let ctx = PlcValidationContext {
|
|
server_rotation_key: did_key.clone(),
|
|
expected_signing_key: did_key,
|
|
expected_handle: "test.handle".to_string(),
|
|
expected_pds_endpoint: "https://pds.example.com".to_string(),
|
|
};
|
|
assert!(validate_plc_operation_for_submission(&tombstone, &ctx).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_sign_operation_and_struct() {
|
|
let key = SigningKey::random(&mut rand::thread_rng());
|
|
let op = json!({
|
|
"type": "plc_operation", "rotationKeys": [], "verificationMethods": {},
|
|
"alsoKnownAs": [], "services": {}, "prev": null, "sig": "old_signature"
|
|
});
|
|
let signed = sign_operation(&op, &key).unwrap();
|
|
assert_ne!(signed.get("sig").and_then(|v| v.as_str()).unwrap(), "old_signature");
|
|
|
|
let mut services = HashMap::new();
|
|
services.insert("atproto_pds".to_string(), PlcService {
|
|
service_type: "AtprotoPersonalDataServer".to_string(),
|
|
endpoint: "https://pds.example.com".to_string(),
|
|
});
|
|
let mut verification_methods = HashMap::new();
|
|
verification_methods.insert("atproto".to_string(), "did:key:zTest123".to_string());
|
|
let op = PlcOperation {
|
|
op_type: "plc_operation".to_string(),
|
|
rotation_keys: vec!["did:key:zTest123".to_string()],
|
|
verification_methods,
|
|
also_known_as: vec!["at://test.handle".to_string()],
|
|
services,
|
|
prev: None,
|
|
sig: Some("test".to_string()),
|
|
};
|
|
let json_value = serde_json::to_value(&op).unwrap();
|
|
assert_eq!(json_value["type"], "plc_operation");
|
|
assert!(json_value["rotationKeys"].is_array());
|
|
}
|