this repo has no description
1use k256::ecdsa::SigningKey;
2use serde_json::json;
3use std::collections::HashMap;
4use tranquil_pds::plc::{
5 PlcError, PlcOperation, PlcService, PlcValidationContext, cid_for_cbor, sign_operation,
6 signing_key_to_did_key, validate_plc_operation, validate_plc_operation_for_submission,
7 verify_operation_signature,
8};
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!(
36 matches!(validate_plc_operation(&missing_type), Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing type"))
37 );
38
39 let invalid_type = json!({ "type": "invalid_type", "sig": "test" });
40 assert!(
41 matches!(validate_plc_operation(&invalid_type), Err(PlcError::InvalidResponse(msg)) if msg.contains("Invalid type"))
42 );
43
44 let missing_sig = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {} });
45 assert!(
46 matches!(validate_plc_operation(&missing_sig), Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing sig"))
47 );
48
49 let missing_rotation = json!({ "type": "plc_operation", "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "sig": "test" });
50 assert!(
51 matches!(validate_plc_operation(&missing_rotation), Err(PlcError::InvalidResponse(msg)) if msg.contains("rotationKeys"))
52 );
53
54 let missing_verification = json!({ "type": "plc_operation", "rotationKeys": [], "alsoKnownAs": [], "services": {}, "sig": "test" });
55 assert!(
56 matches!(validate_plc_operation(&missing_verification), Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods"))
57 );
58
59 let missing_aka = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "services": {}, "sig": "test" });
60 assert!(
61 matches!(validate_plc_operation(&missing_aka), Err(PlcError::InvalidResponse(msg)) if msg.contains("alsoKnownAs"))
62 );
63
64 let missing_services = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "sig": "test" });
65 assert!(
66 matches!(validate_plc_operation(&missing_services), Err(PlcError::InvalidResponse(msg)) if msg.contains("services"))
67 );
68
69 assert!(matches!(
70 validate_plc_operation(&json!("not an object")),
71 Err(PlcError::InvalidResponse(_))
72 ));
73}
74
75#[test]
76fn test_plc_submission_validation() {
77 let key = SigningKey::random(&mut rand::thread_rng());
78 let did_key = signing_key_to_did_key(&key);
79 let server_key = "did:key:zServer123";
80
81 let base_op = |rotation_key: &str,
82 signing_key: &str,
83 handle: &str,
84 service_type: &str,
85 endpoint: &str| {
86 json!({
87 "type": "plc_operation",
88 "rotationKeys": [rotation_key],
89 "verificationMethods": {"atproto": signing_key},
90 "alsoKnownAs": [format!("at://{}", handle)],
91 "services": { "atproto_pds": { "type": service_type, "endpoint": endpoint } },
92 "sig": "test"
93 })
94 };
95
96 let ctx = PlcValidationContext {
97 server_rotation_key: server_key.to_string(),
98 expected_signing_key: did_key.clone(),
99 expected_handle: "test.handle".to_string(),
100 expected_pds_endpoint: "https://pds.example.com".to_string(),
101 };
102
103 let op = base_op(
104 &did_key,
105 &did_key,
106 "test.handle",
107 "AtprotoPersonalDataServer",
108 "https://pds.example.com",
109 );
110 assert!(
111 matches!(validate_plc_operation_for_submission(&op, &ctx), Err(PlcError::InvalidResponse(msg)) if msg.contains("rotation key"))
112 );
113
114 let ctx_with_user_key = PlcValidationContext {
115 server_rotation_key: did_key.clone(),
116 expected_signing_key: did_key.clone(),
117 expected_handle: "test.handle".to_string(),
118 expected_pds_endpoint: "https://pds.example.com".to_string(),
119 };
120
121 let wrong_signing = base_op(
122 &did_key,
123 "did:key:zWrongKey",
124 "test.handle",
125 "AtprotoPersonalDataServer",
126 "https://pds.example.com",
127 );
128 assert!(
129 matches!(validate_plc_operation_for_submission(&wrong_signing, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("signing key"))
130 );
131
132 let wrong_handle = base_op(
133 &did_key,
134 &did_key,
135 "wrong.handle",
136 "AtprotoPersonalDataServer",
137 "https://pds.example.com",
138 );
139 assert!(
140 matches!(validate_plc_operation_for_submission(&wrong_handle, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("handle"))
141 );
142
143 let wrong_service_type = base_op(
144 &did_key,
145 &did_key,
146 "test.handle",
147 "WrongServiceType",
148 "https://pds.example.com",
149 );
150 assert!(
151 matches!(validate_plc_operation_for_submission(&wrong_service_type, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("type"))
152 );
153
154 let wrong_endpoint = base_op(
155 &did_key,
156 &did_key,
157 "test.handle",
158 "AtprotoPersonalDataServer",
159 "https://wrong.endpoint.com",
160 );
161 assert!(
162 matches!(validate_plc_operation_for_submission(&wrong_endpoint, &ctx_with_user_key), Err(PlcError::InvalidResponse(msg)) if msg.contains("endpoint"))
163 );
164}
165
166#[test]
167fn test_signature_verification() {
168 let key = SigningKey::random(&mut rand::thread_rng());
169 let did_key = signing_key_to_did_key(&key);
170 let op = json!({
171 "type": "plc_operation", "rotationKeys": [did_key.clone()],
172 "verificationMethods": {}, "alsoKnownAs": [], "services": {}, "prev": null
173 });
174 let signed = sign_operation(&op, &key).unwrap();
175 let result = verify_operation_signature(&signed, std::slice::from_ref(&did_key));
176 assert!(result.is_ok() && result.unwrap());
177
178 let other_key = SigningKey::random(&mut rand::thread_rng());
179 let other_did = signing_key_to_did_key(&other_key);
180 let result = verify_operation_signature(&signed, &[other_did]);
181 assert!(result.is_ok() && !result.unwrap());
182
183 let result = verify_operation_signature(&signed, &["not-a-did-key".to_string()]);
184 assert!(result.is_ok() && !result.unwrap());
185
186 let missing_sig = json!({ "type": "plc_operation", "rotationKeys": [], "verificationMethods": {}, "alsoKnownAs": [], "services": {} });
187 assert!(
188 matches!(verify_operation_signature(&missing_sig, &[]), Err(PlcError::InvalidResponse(msg)) if msg.contains("sig"))
189 );
190
191 let invalid_base64 = json!({
192 "type": "plc_operation", "rotationKeys": [], "verificationMethods": {},
193 "alsoKnownAs": [], "services": {}, "sig": "not-valid-base64!!!"
194 });
195 assert!(matches!(
196 verify_operation_signature(&invalid_base64, &[]),
197 Err(PlcError::InvalidResponse(_))
198 ));
199}
200
201#[test]
202fn test_cid_and_key_utilities() {
203 let value = json!({ "alpha": 1, "beta": 2 });
204 let cid1 = cid_for_cbor(&value).unwrap();
205 let cid2 = cid_for_cbor(&value).unwrap();
206 assert_eq!(cid1, cid2, "CID should be deterministic");
207 assert!(
208 cid1.starts_with("bafyrei"),
209 "CID should be dag-cbor + sha256"
210 );
211
212 let value2 = json!({ "alpha": 999 });
213 let cid3 = cid_for_cbor(&value2).unwrap();
214 assert_ne!(cid1, cid3, "Different data should produce different CIDs");
215
216 let key = SigningKey::random(&mut rand::thread_rng());
217 let did = signing_key_to_did_key(&key);
218 assert!(did.starts_with("did:key:z") && did.len() > 50);
219 assert_eq!(
220 did,
221 signing_key_to_did_key(&key),
222 "Same key should produce same did"
223 );
224
225 let key2 = SigningKey::random(&mut rand::thread_rng());
226 assert_ne!(
227 did,
228 signing_key_to_did_key(&key2),
229 "Different keys should produce different dids"
230 );
231}
232
233#[test]
234fn test_tombstone_operations() {
235 let tombstone =
236 json!({ "type": "plc_tombstone", "prev": "bafyreig6xxxxxyyyyyzzzzzz", "sig": "test" });
237 assert!(validate_plc_operation(&tombstone).is_ok());
238
239 let key = SigningKey::random(&mut rand::thread_rng());
240 let did_key = signing_key_to_did_key(&key);
241 let ctx = PlcValidationContext {
242 server_rotation_key: did_key.clone(),
243 expected_signing_key: did_key,
244 expected_handle: "test.handle".to_string(),
245 expected_pds_endpoint: "https://pds.example.com".to_string(),
246 };
247 assert!(validate_plc_operation_for_submission(&tombstone, &ctx).is_ok());
248}
249
250#[test]
251fn test_sign_operation_and_struct() {
252 let key = SigningKey::random(&mut rand::thread_rng());
253 let op = json!({
254 "type": "plc_operation", "rotationKeys": [], "verificationMethods": {},
255 "alsoKnownAs": [], "services": {}, "prev": null, "sig": "old_signature"
256 });
257 let signed = sign_operation(&op, &key).unwrap();
258 assert_ne!(
259 signed.get("sig").and_then(|v| v.as_str()).unwrap(),
260 "old_signature"
261 );
262
263 let mut services = HashMap::new();
264 services.insert(
265 "atproto_pds".to_string(),
266 PlcService {
267 service_type: "AtprotoPersonalDataServer".to_string(),
268 endpoint: "https://pds.example.com".to_string(),
269 },
270 );
271 let mut verification_methods = HashMap::new();
272 verification_methods.insert("atproto".to_string(), "did:key:zTest123".to_string());
273 let op = PlcOperation {
274 op_type: "plc_operation".to_string(),
275 rotation_keys: vec!["did:key:zTest123".to_string()],
276 verification_methods,
277 also_known_as: vec!["at://test.handle".to_string()],
278 services,
279 prev: None,
280 sig: Some("test".to_string()),
281 };
282 let json_value = serde_json::to_value(&op).unwrap();
283 assert_eq!(json_value["type"], "plc_operation");
284 assert!(json_value["rotationKeys"].is_array());
285}