this repo has no description
1use tranquil_pds::plc::{
2 PlcError, PlcOperation, PlcService, PlcValidationContext, cid_for_cbor, sign_operation,
3 signing_key_to_did_key, validate_plc_operation, validate_plc_operation_for_submission,
4 verify_operation_signature,
5};
6use k256::ecdsa::SigningKey;
7use serde_json::json;
8use std::collections::HashMap;
9
10fn create_valid_operation() -> serde_json::Value {
11 let key = SigningKey::random(&mut rand::thread_rng());
12 let did_key = signing_key_to_did_key(&key);
13 let op = json!({
14 "type": "plc_operation",
15 "rotationKeys": [did_key.clone()],
16 "verificationMethods": { "atproto": did_key.clone() },
17 "alsoKnownAs": ["at://test.handle"],
18 "services": {
19 "atproto_pds": {
20 "type": "AtprotoPersonalDataServer",
21 "endpoint": "https://pds.example.com"
22 }
23 },
24 "prev": null
25 });
26 sign_operation(&op, &key).unwrap()
27}
28
29#[test]
30fn test_plc_operation_basic_validation() {
31 let op = create_valid_operation();
32 assert!(validate_plc_operation(&op).is_ok());
33
34 let missing_type = json!({ "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "sig": "test" });
35 assert!(matches!(validate_plc_operation(&missing_type), Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing type")));
36
37 let invalid_type = json!({ "type": "invalid_type", "sig": "test" });
38 assert!(matches!(validate_plc_operation(&invalid_type), Err(PlcError::InvalidResponse(msg)) if msg.contains("Invalid type")));
39
40 let missing_sig = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {} });
41 assert!(matches!(validate_plc_operation(&missing_sig), Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing sig")));
42
43 let missing_rotation = json!({ "type": "plc_operation", "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "sig": "test" });
44 assert!(matches!(validate_plc_operation(&missing_rotation), Err(PlcError::InvalidResponse(msg)) if msg.contains("rotationKeys")));
45
46 let missing_verification = json!({ "type": "plc_operation", "rotationKeys": [], "alsoKnownAs": [], "services": {}, "sig": "test" });
47 assert!(matches!(validate_plc_operation(&missing_verification), Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods")));
48
49 let missing_aka = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "services": {}, "sig": "test" });
50 assert!(matches!(validate_plc_operation(&missing_aka), Err(PlcError::InvalidResponse(msg)) if msg.contains("alsoKnownAs")));
51
52 let missing_services = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "sig": "test" });
53 assert!(matches!(validate_plc_operation(&missing_services), Err(PlcError::InvalidResponse(msg)) if msg.contains("services")));
54
55 assert!(matches!(validate_plc_operation(&json!("not an object")), Err(PlcError::InvalidResponse(_))));
56}
57
58#[test]
59fn test_plc_submission_validation() {
60 let key = SigningKey::random(&mut rand::thread_rng());
61 let did_key = signing_key_to_did_key(&key);
62 let server_key = "did:key:zServer123";
63
64 let base_op = |rotation_key: &str, signing_key: &str, handle: &str, service_type: &str, endpoint: &str| json!({
65 "type": "plc_operation",
66 "rotationKeys": [rotation_key],
67 "verificationMethods": {"atproto": signing_key},
68 "alsoKnownAs": [format!("at://{}", handle)],
69 "services": { "atproto_pds": { "type": service_type, "endpoint": endpoint } },
70 "sig": "test"
71 });
72
73 let ctx = PlcValidationContext {
74 server_rotation_key: server_key.to_string(),
75 expected_signing_key: did_key.clone(),
76 expected_handle: "test.handle".to_string(),
77 expected_pds_endpoint: "https://pds.example.com".to_string(),
78 };
79
80 let op = base_op(&did_key, &did_key, "test.handle", "AtprotoPersonalDataServer", "https://pds.example.com");
81 assert!(matches!(validate_plc_operation_for_submission(&op, &ctx), Err(PlcError::InvalidResponse(msg)) if msg.contains("rotation key")));
82
83 let ctx_with_user_key = PlcValidationContext {
84 server_rotation_key: did_key.clone(),
85 expected_signing_key: did_key.clone(),
86 expected_handle: "test.handle".to_string(),
87 expected_pds_endpoint: "https://pds.example.com".to_string(),
88 };
89
90 let wrong_signing = base_op(&did_key, "did:key:zWrongKey", "test.handle", "AtprotoPersonalDataServer", "https://pds.example.com");
91 assert!(matches!(validate_plc_operation_for_submission(&wrong_signing, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("signing key")));
92
93 let wrong_handle = base_op(&did_key, &did_key, "wrong.handle", "AtprotoPersonalDataServer", "https://pds.example.com");
94 assert!(matches!(validate_plc_operation_for_submission(&wrong_handle, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("handle")));
95
96 let wrong_service_type = base_op(&did_key, &did_key, "test.handle", "WrongServiceType", "https://pds.example.com");
97 assert!(matches!(validate_plc_operation_for_submission(&wrong_service_type, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("type")));
98
99 let wrong_endpoint = base_op(&did_key, &did_key, "test.handle", "AtprotoPersonalDataServer", "https://wrong.endpoint.com");
100 assert!(matches!(validate_plc_operation_for_submission(&wrong_endpoint, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("endpoint")));
101}
102
103#[test]
104fn test_signature_verification() {
105 let key = SigningKey::random(&mut rand::thread_rng());
106 let did_key = signing_key_to_did_key(&key);
107 let op = json!({
108 "type": "plc_operation", "rotationKeys": [did_key.clone()],
109 "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "prev": null
110 });
111 let signed = sign_operation(&op, &key).unwrap();
112 let result = verify_operation_signature(&signed, &[did_key.clone()]);
113 assert!(result.is_ok() && result.unwrap());
114
115 let other_key = SigningKey::random(&mut rand::thread_rng());
116 let other_did = signing_key_to_did_key(&other_key);
117 let result = verify_operation_signature(&signed, &[other_did]);
118 assert!(result.is_ok() && !result.unwrap());
119
120 let result = verify_operation_signature(&signed, &["not-a-did-key".to_string()]);
121 assert!(result.is_ok() && !result.unwrap());
122
123 let missing_sig = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {} });
124 assert!(matches!(verify_operation_signature(&missing_sig, &[]), Err(PlcError::InvalidResponse(msg)) if msg.contains("sig")));
125
126 let invalid_base64 = json!({
127 "type": "plc_operation", "rotationKeys": [], "verificationMethods": {},
128 "alsoKnownAs": [], "services": {}, "sig": "not-valid-base64!!!"
129 });
130 assert!(matches!(verify_operation_signature(&invalid_base64, &[]), Err(PlcError::InvalidResponse(_))));
131}
132
133#[test]
134fn test_cid_and_key_utilities() {
135 let value = json!({ "alpha": 1, "beta": 2 });
136 let cid1 = cid_for_cbor(&value).unwrap();
137 let cid2 = cid_for_cbor(&value).unwrap();
138 assert_eq!(cid1, cid2, "CID should be deterministic");
139 assert!(cid1.starts_with("bafyrei"), "CID should be dag-cbor + sha256");
140
141 let value2 = json!({ "alpha": 999 });
142 let cid3 = cid_for_cbor(&value2).unwrap();
143 assert_ne!(cid1, cid3, "Different data should produce different CIDs");
144
145 let key = SigningKey::random(&mut rand::thread_rng());
146 let did = signing_key_to_did_key(&key);
147 assert!(did.starts_with("did:key:z") && did.len() > 50);
148 assert_eq!(did, signing_key_to_did_key(&key), "Same key should produce same did");
149
150 let key2 = SigningKey::random(&mut rand::thread_rng());
151 assert_ne!(did, signing_key_to_did_key(&key2), "Different keys should produce different dids");
152}
153
154#[test]
155fn test_tombstone_operations() {
156 let tombstone = json!({ "type": "plc_tombstone", "prev": "bafyreig6xxxxxyyyyyzzzzzz", "sig": "test" });
157 assert!(validate_plc_operation(&tombstone).is_ok());
158
159 let key = SigningKey::random(&mut rand::thread_rng());
160 let did_key = signing_key_to_did_key(&key);
161 let ctx = PlcValidationContext {
162 server_rotation_key: did_key.clone(),
163 expected_signing_key: did_key,
164 expected_handle: "test.handle".to_string(),
165 expected_pds_endpoint: "https://pds.example.com".to_string(),
166 };
167 assert!(validate_plc_operation_for_submission(&tombstone, &ctx).is_ok());
168}
169
170#[test]
171fn test_sign_operation_and_struct() {
172 let key = SigningKey::random(&mut rand::thread_rng());
173 let op = json!({
174 "type": "plc_operation", "rotationKeys": [], "verificationMethods": {},
175 "alsoKnownAs": [], "services": {}, "prev": null, "sig": "old_signature"
176 });
177 let signed = sign_operation(&op, &key).unwrap();
178 assert_ne!(signed.get("sig").and_then(|v| v.as_str()).unwrap(), "old_signature");
179
180 let mut services = HashMap::new();
181 services.insert("atproto_pds".to_string(), PlcService {
182 service_type: "AtprotoPersonalDataServer".to_string(),
183 endpoint: "https://pds.example.com".to_string(),
184 });
185 let mut verification_methods = HashMap::new();
186 verification_methods.insert("atproto".to_string(), "did:key:zTest123".to_string());
187 let op = PlcOperation {
188 op_type: "plc_operation".to_string(),
189 rotation_keys: vec!["did:key:zTest123".to_string()],
190 verification_methods,
191 also_known_as: vec!["at://test.handle".to_string()],
192 services,
193 prev: None,
194 sig: Some("test".to_string()),
195 };
196 let json_value = serde_json::to_value(&op).unwrap();
197 assert_eq!(json_value["type"], "plc_operation");
198 assert!(json_value["rotationKeys"].is_array());
199}