Files
tranquil-pds/tests/plc_validation.rs
T
2025-12-16 21:23:11 +02:00

485 lines
15 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_validate_plc_operation_valid() {
let op = create_valid_operation();
let result = validate_plc_operation(&op);
assert!(result.is_ok());
}
#[test]
fn test_validate_plc_operation_missing_type() {
let op = json!({
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {},
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing type")));
}
#[test]
fn test_validate_plc_operation_invalid_type() {
let op = json!({
"type": "invalid_type",
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Invalid type")));
}
#[test]
fn test_validate_plc_operation_missing_sig() {
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {}
});
let result = validate_plc_operation(&op);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("Missing sig")));
}
#[test]
fn test_validate_plc_operation_missing_rotation_keys() {
let op = json!({
"type": "plc_operation",
"verificationMethods": {},
"alsoKnownAs": [],
"services": {},
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("rotationKeys")));
}
#[test]
fn test_validate_plc_operation_missing_verification_methods() {
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"alsoKnownAs": [],
"services": {},
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(
matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("verificationMethods"))
);
}
#[test]
fn test_validate_plc_operation_missing_also_known_as() {
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"services": {},
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("alsoKnownAs")));
}
#[test]
fn test_validate_plc_operation_missing_services() {
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("services")));
}
#[test]
fn test_validate_rotation_key_required() {
let key = SigningKey::random(&mut rand::thread_rng());
let did_key = signing_key_to_did_key(&key);
let server_key = "did:key:zServer123";
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"
}
},
"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 result = validate_plc_operation_for_submission(&op, &ctx);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("rotation key")));
}
#[test]
fn test_validate_signing_key_match() {
let key = SigningKey::random(&mut rand::thread_rng());
let did_key = signing_key_to_did_key(&key);
let wrong_key = "did:key:zWrongKey456";
let op = json!({
"type": "plc_operation",
"rotationKeys": [did_key.clone()],
"verificationMethods": {"atproto": wrong_key},
"alsoKnownAs": ["at://test.handle"],
"services": {
"atproto_pds": {
"type": "AtprotoPersonalDataServer",
"endpoint": "https://pds.example.com"
}
},
"sig": "test"
});
let ctx = 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 result = validate_plc_operation_for_submission(&op, &ctx);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("signing key")));
}
#[test]
fn test_validate_handle_match() {
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://wrong.handle"],
"services": {
"atproto_pds": {
"type": "AtprotoPersonalDataServer",
"endpoint": "https://pds.example.com"
}
},
"sig": "test"
});
let ctx = 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 result = validate_plc_operation_for_submission(&op, &ctx);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("handle")));
}
#[test]
fn test_validate_pds_service_type() {
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": "WrongServiceType",
"endpoint": "https://pds.example.com"
}
},
"sig": "test"
});
let ctx = 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 result = validate_plc_operation_for_submission(&op, &ctx);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("type")));
}
#[test]
fn test_validate_pds_endpoint_match() {
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://wrong.endpoint.com"
}
},
"sig": "test"
});
let ctx = 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 result = validate_plc_operation_for_submission(&op, &ctx);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("endpoint")));
}
#[test]
fn test_verify_signature_secp256k1() {
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 rotation_keys = vec![did_key];
let result = verify_operation_signature(&signed, &rotation_keys);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_verify_signature_wrong_key() {
let key = SigningKey::random(&mut rand::thread_rng());
let other_key = SigningKey::random(&mut rand::thread_rng());
let other_did_key = signing_key_to_did_key(&other_key);
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {},
"prev": null
});
let signed = sign_operation(&op, &key).unwrap();
let wrong_rotation_keys = vec![other_did_key];
let result = verify_operation_signature(&signed, &wrong_rotation_keys);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_verify_signature_invalid_did_key_format() {
let key = SigningKey::random(&mut rand::thread_rng());
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {},
"prev": null
});
let signed = sign_operation(&op, &key).unwrap();
let invalid_keys = vec!["not-a-did-key".to_string()];
let result = verify_operation_signature(&signed, &invalid_keys);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_tombstone_validation() {
let op = json!({
"type": "plc_tombstone",
"prev": "bafyreig6xxxxxyyyyyzzzzzz",
"sig": "test"
});
let result = validate_plc_operation(&op);
assert!(result.is_ok());
}
#[test]
fn test_cid_for_cbor_deterministic() {
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 generation should be deterministic");
assert!(
cid1.starts_with("bafyrei"),
"CID should start with bafyrei (dag-cbor + sha256)"
);
}
#[test]
fn test_cid_different_for_different_data() {
let value1 = json!({"data": 1});
let value2 = json!({"data": 2});
let cid1 = cid_for_cbor(&value1).unwrap();
let cid2 = cid_for_cbor(&value2).unwrap();
assert_ne!(cid1, cid2, "Different data should produce different CIDs");
}
#[test]
fn test_signing_key_to_did_key_format() {
let key = SigningKey::random(&mut rand::thread_rng());
let did_key = signing_key_to_did_key(&key);
assert!(
did_key.starts_with("did:key:z"),
"Should start with did:key:z"
);
assert!(did_key.len() > 50, "Did key should be reasonably long");
}
#[test]
fn test_signing_key_to_did_key_unique() {
let key1 = SigningKey::random(&mut rand::thread_rng());
let key2 = SigningKey::random(&mut rand::thread_rng());
let did1 = signing_key_to_did_key(&key1);
let did2 = signing_key_to_did_key(&key2);
assert_ne!(
did1, did2,
"Different keys should produce different did:keys"
);
}
#[test]
fn test_signing_key_to_did_key_consistent() {
let key = SigningKey::random(&mut rand::thread_rng());
let did1 = signing_key_to_did_key(&key);
let did2 = signing_key_to_did_key(&key);
assert_eq!(did1, did2, "Same key should produce same did:key");
}
#[test]
fn test_sign_operation_removes_existing_sig() {
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();
let new_sig = signed.get("sig").and_then(|v| v.as_str()).unwrap();
assert_ne!(new_sig, "old_signature", "Should replace old signature");
}
#[test]
fn test_validate_plc_operation_not_object() {
let result = validate_plc_operation(&json!("not an object"));
assert!(matches!(result, Err(PlcError::InvalidResponse(_))));
}
#[test]
fn test_validate_for_submission_tombstone_passes() {
let key = SigningKey::random(&mut rand::thread_rng());
let did_key = signing_key_to_did_key(&key);
let op = json!({
"type": "plc_tombstone",
"prev": "bafyreig6xxxxxyyyyyzzzzzz",
"sig": "test"
});
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(),
};
let result = validate_plc_operation_for_submission(&op, &ctx);
assert!(
result.is_ok(),
"Tombstone should pass submission validation"
);
}
#[test]
fn test_verify_signature_missing_sig() {
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {}
});
let result = verify_operation_signature(&op, &[]);
assert!(matches!(result, Err(PlcError::InvalidResponse(msg)) if msg.contains("sig")));
}
#[test]
fn test_verify_signature_invalid_base64() {
let op = json!({
"type": "plc_operation",
"rotationKeys": [],
"verificationMethods": {},
"alsoKnownAs": [],
"services": {},
"sig": "not-valid-base64!!!"
});
let result = verify_operation_signature(&op, &[]);
assert!(matches!(result, Err(PlcError::InvalidResponse(_))));
}
#[test]
fn test_plc_operation_struct() {
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());
}