Draft 0.5: identity/registry spec; Phase A signed MA registry, mailbox challenge auth, key rotation, skew rejection
This commit is contained in:
+154
-26
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use common::{
|
||||
ask, client, collection, config_for, messages_of_type, poll, poll_messages, publish,
|
||||
ask, challenge, client, collection, config_for, messages_of_type, poll, poll_messages, publish,
|
||||
query_envelope, register, spawn_relay, spawn_relay_with_capacity, unicast,
|
||||
};
|
||||
use frxd::crypto::Keypair;
|
||||
@@ -63,14 +63,14 @@ async fn start_node_with_corpus(
|
||||
async fn collect_responses(
|
||||
http: &reqwest::Client,
|
||||
relay_url: &str,
|
||||
member: &str,
|
||||
key: &Keypair,
|
||||
timeout_ms: u64,
|
||||
) -> Vec<Value> {
|
||||
let start = Instant::now();
|
||||
let deadline = Duration::from_millis(timeout_ms);
|
||||
let mut responses = Vec::new();
|
||||
while start.elapsed() < deadline {
|
||||
let messages = poll_messages(http, relay_url, member, 200).await;
|
||||
let messages = poll_messages(http, relay_url, key, 200).await;
|
||||
responses.extend(messages_of_type(&messages, TYPE_RESPONSE));
|
||||
if !responses.is_empty() {
|
||||
break;
|
||||
@@ -86,7 +86,7 @@ async fn raw_query(
|
||||
text: &str,
|
||||
timeout_ms: u64,
|
||||
) -> Vec<Value> {
|
||||
register(http, relay_url, &asker.public_hex()).await;
|
||||
register(http, relay_url, asker).await;
|
||||
let envelope = query_envelope(asker, text, 5);
|
||||
assert!(
|
||||
publish(http, relay_url, &envelope)
|
||||
@@ -94,7 +94,7 @@ async fn raw_query(
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
collect_responses(http, relay_url, &asker.public_hex(), timeout_ms).await
|
||||
collect_responses(http, relay_url, asker, timeout_ms).await
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -330,7 +330,7 @@ async fn entities_are_optional_hints() {
|
||||
.await;
|
||||
let alice = Keypair::generate();
|
||||
let http = client();
|
||||
register(&http, &relay_url, &alice.public_hex()).await;
|
||||
register(&http, &relay_url, &alice).await;
|
||||
let body = json!({
|
||||
"qid": "entities-test",
|
||||
"text": "rust",
|
||||
@@ -344,7 +344,7 @@ async fn entities_are_optional_hints() {
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
let responses = collect_responses(&http, &relay_url, &alice.public_hex(), 700).await;
|
||||
let responses = collect_responses(&http, &relay_url, &alice, 700).await;
|
||||
assert_eq!(responses.len(), 1);
|
||||
assert_eq!(
|
||||
responses[0].pointer("/body/qid").and_then(Value::as_str),
|
||||
@@ -367,6 +367,7 @@ async fn member_directory_filters_senders() {
|
||||
name: "alice".to_string(),
|
||||
pubkey: alice.public_hex(),
|
||||
class: frxd::config::CLASS_SOURCE.to_string(),
|
||||
previous: Vec::new(),
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
@@ -382,7 +383,7 @@ async fn member_directory_filters_senders() {
|
||||
let trusted = raw_query(&http, &relay_url, &alice, "rust", 700).await;
|
||||
assert_eq!(trusted.len(), 1, "trusted member got no response");
|
||||
|
||||
register(&http, &relay_url, &untrusted.public_hex()).await;
|
||||
register(&http, &relay_url, &untrusted).await;
|
||||
let envelope = query_envelope(&untrusted, "rust", 5);
|
||||
assert!(
|
||||
publish(&http, &relay_url, &envelope)
|
||||
@@ -390,17 +391,98 @@ async fn member_directory_filters_senders() {
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
let denied = collect_responses(&http, &relay_url, &untrusted.public_hex(), 400).await;
|
||||
let denied = collect_responses(&http, &relay_url, &untrusted, 400).await;
|
||||
assert!(denied.is_empty(), "untrusted member received a response");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn rotated_keys_are_accepted_through_previous_listing() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let relay_url = spawn_relay().await;
|
||||
let old_key = Keypair::generate();
|
||||
let new_key = Keypair::generate();
|
||||
|
||||
let docs = corpus_dir(root.path(), "bob", &[("doc.txt", "rotation rust document")]);
|
||||
let config = config_for(&root.path().join("bob"), "bob", &relay_url);
|
||||
let listing = |previous: Vec<String>| {
|
||||
vec![frxd::config::Member {
|
||||
name: "carol".to_string(),
|
||||
pubkey: new_key.public_hex(),
|
||||
class: frxd::config::CLASS_SOURCE.to_string(),
|
||||
previous,
|
||||
}]
|
||||
};
|
||||
frxd::config::save_members(&config.members_path(), &listing(vec![old_key.public_hex()]))
|
||||
.unwrap();
|
||||
{
|
||||
let index = LocalIndex::open(&config.index_dir()).unwrap();
|
||||
index
|
||||
.add_collection(&collection("docs", &docs, true, EXPOSURE_FULL))
|
||||
.unwrap();
|
||||
}
|
||||
let _bob = Node::start(config.clone()).await.unwrap();
|
||||
|
||||
let http = client();
|
||||
register(&http, &relay_url, &old_key).await;
|
||||
let envelope = query_envelope(&old_key, "rust", 5);
|
||||
assert!(
|
||||
publish(&http, &relay_url, &envelope)
|
||||
.await
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
let accepted = collect_responses(&http, &relay_url, &old_key, 700).await;
|
||||
assert_eq!(accepted.len(), 1, "listed previous key was rejected");
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
frxd::config::save_members(&config.members_path(), &listing(Vec::new())).unwrap();
|
||||
let envelope = query_envelope(&old_key, "rust", 5);
|
||||
assert!(
|
||||
publish(&http, &relay_url, &envelope)
|
||||
.await
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
let revoked = collect_responses(&http, &relay_url, &old_key, 500).await;
|
||||
assert!(revoked.is_empty(), "revoked key was still accepted");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn stale_envelopes_are_rejected() {
|
||||
let relay_url = spawn_relay().await;
|
||||
let http = client();
|
||||
let key = Keypair::generate();
|
||||
|
||||
let mut envelope = query_envelope(&key, "stale", 5);
|
||||
envelope.ts = frxd::crypto::now_ts().saturating_sub(3600);
|
||||
envelope.sig = key.sign(&frxd::crypto::signing_bytes(&envelope));
|
||||
assert_eq!(
|
||||
publish(&http, &relay_url, &envelope).await.status(),
|
||||
reqwest::StatusCode::BAD_REQUEST
|
||||
);
|
||||
|
||||
let mut response = Envelope::new(
|
||||
&key,
|
||||
TYPE_RESPONSE,
|
||||
json!({"qid": "q1", "results": [], "truncated": false, "more_available": 0, "cursor": null}),
|
||||
);
|
||||
response.ts = frxd::crypto::now_ts().saturating_sub(3600);
|
||||
response.sig = key.sign(&frxd::crypto::signing_bytes(&response));
|
||||
assert_eq!(
|
||||
unicast(&http, &relay_url, &key.public_hex(), &response)
|
||||
.await
|
||||
.status(),
|
||||
reqwest::StatusCode::BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn backpressure_is_visible_and_recoverable() {
|
||||
let relay_url = spawn_relay_with_capacity(1).await;
|
||||
let http = client();
|
||||
let member = Keypair::generate();
|
||||
let publisher = Keypair::generate();
|
||||
register(&http, &relay_url, &member.public_hex()).await;
|
||||
register(&http, &relay_url, &member).await;
|
||||
|
||||
let first = query_envelope(&publisher, "one", 5);
|
||||
assert_eq!(
|
||||
@@ -419,13 +501,11 @@ async fn backpressure_is_visible_and_recoverable() {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
poll_messages(&http, &relay_url, &member.public_hex(), 300)
|
||||
.await
|
||||
.len(),
|
||||
poll_messages(&http, &relay_url, &member, 300).await.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
poll_messages(&http, &relay_url, &publisher.public_hex(), 300)
|
||||
poll_messages(&http, &relay_url, &publisher, 300)
|
||||
.await
|
||||
.len(),
|
||||
1
|
||||
@@ -444,8 +524,8 @@ async fn unicast_is_need_to_know() {
|
||||
let http = client();
|
||||
let alice = Keypair::generate();
|
||||
let bob = Keypair::generate();
|
||||
register(&http, &relay_url, &alice.public_hex()).await;
|
||||
register(&http, &relay_url, &bob.public_hex()).await;
|
||||
register(&http, &relay_url, &alice).await;
|
||||
register(&http, &relay_url, &bob).await;
|
||||
|
||||
let query = query_envelope(&alice, "rust", 5);
|
||||
assert!(
|
||||
@@ -467,9 +547,9 @@ async fn unicast_is_need_to_know() {
|
||||
reqwest::StatusCode::OK
|
||||
);
|
||||
|
||||
let alice_messages = poll_messages(&http, &relay_url, &alice.public_hex(), 300).await;
|
||||
let alice_messages = poll_messages(&http, &relay_url, &alice, 300).await;
|
||||
assert_eq!(messages_of_type(&alice_messages, TYPE_RESPONSE).len(), 1);
|
||||
let bob_messages = poll_messages(&http, &relay_url, &bob.public_hex(), 300).await;
|
||||
let bob_messages = poll_messages(&http, &relay_url, &bob, 300).await;
|
||||
assert!(
|
||||
messages_of_type(&bob_messages, TYPE_RESPONSE).is_empty(),
|
||||
"unicast response leaked to another member"
|
||||
@@ -516,10 +596,58 @@ async fn tampered_envelopes_are_rejected() {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn invalid_poll_member_is_rejected() {
|
||||
async fn poll_requires_proof_of_key() {
|
||||
let relay_url = spawn_relay().await;
|
||||
let response = poll(&client(), &relay_url, "not-hex-at-all", 30).await;
|
||||
assert_eq!(response.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||
let http = client();
|
||||
let member = Keypair::generate();
|
||||
let imposter = Keypair::generate();
|
||||
|
||||
assert!(
|
||||
challenge(&http, &relay_url, "not-hex-at-all")
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
|
||||
let nonce = challenge(&http, &relay_url, &member.public_hex())
|
||||
.await
|
||||
.unwrap();
|
||||
let forged = imposter.sign(&frxd::crypto::poll_signing_bytes(
|
||||
&member.public_hex(),
|
||||
&nonce,
|
||||
));
|
||||
let response = http
|
||||
.get(format!(
|
||||
"{relay_url}/v1/poll?member={}&nonce={nonce}&sig={forged}&timeout_ms=30",
|
||||
member.public_hex()
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), reqwest::StatusCode::UNAUTHORIZED);
|
||||
|
||||
let response = poll(&http, &relay_url, &member, 30).await;
|
||||
assert_eq!(response.status(), reqwest::StatusCode::NO_CONTENT);
|
||||
|
||||
let nonce = challenge(&http, &relay_url, &member.public_hex())
|
||||
.await
|
||||
.unwrap();
|
||||
let sig = member.sign(&frxd::crypto::poll_signing_bytes(
|
||||
&member.public_hex(),
|
||||
&nonce,
|
||||
));
|
||||
let url = format!(
|
||||
"{relay_url}/v1/poll?member={}&nonce={nonce}&sig={sig}&timeout_ms=30",
|
||||
member.public_hex()
|
||||
);
|
||||
assert_eq!(
|
||||
http.get(&url).send().await.unwrap().status(),
|
||||
reqwest::StatusCode::NO_CONTENT
|
||||
);
|
||||
assert_eq!(
|
||||
http.get(&url).send().await.unwrap().status(),
|
||||
reqwest::StatusCode::UNAUTHORIZED,
|
||||
"challenge nonce must be single-use"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
@@ -724,7 +852,7 @@ async fn aggregates_are_unicast_only_and_ignored_by_nodes() {
|
||||
.await;
|
||||
let http = client();
|
||||
let sender = Keypair::generate();
|
||||
register(&http, &relay_url, &bob.pubkey).await;
|
||||
register(&http, &relay_url, &bob.node.key).await;
|
||||
|
||||
let broadcast = Envelope::new(
|
||||
&sender,
|
||||
@@ -798,7 +926,7 @@ async fn duplicate_delivery_is_answered_once() {
|
||||
.await;
|
||||
let alice = Keypair::generate();
|
||||
let http = client();
|
||||
register(&http, &relay_url, &alice.public_hex()).await;
|
||||
register(&http, &relay_url, &alice).await;
|
||||
let envelope = query_envelope(&alice, "rust", 5);
|
||||
assert!(
|
||||
publish(&http, &relay_url, &envelope)
|
||||
@@ -812,7 +940,7 @@ async fn duplicate_delivery_is_answered_once() {
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
let responses = collect_responses(&http, &relay_url, &alice.public_hex(), 700).await;
|
||||
let responses = collect_responses(&http, &relay_url, &alice, 700).await;
|
||||
assert_eq!(
|
||||
responses.len(),
|
||||
1,
|
||||
@@ -904,7 +1032,7 @@ async fn query_body_without_entities_parses() {
|
||||
.await;
|
||||
let alice = Keypair::generate();
|
||||
let http = client();
|
||||
register(&http, &relay_url, &alice.public_hex()).await;
|
||||
register(&http, &relay_url, &alice).await;
|
||||
let envelope = Envelope::new(
|
||||
&alice,
|
||||
TYPE_QUERY,
|
||||
@@ -916,7 +1044,7 @@ async fn query_body_without_entities_parses() {
|
||||
.status()
|
||||
.is_success()
|
||||
);
|
||||
let responses = collect_responses(&http, &relay_url, &alice.public_hex(), 700).await;
|
||||
let responses = collect_responses(&http, &relay_url, &alice, 700).await;
|
||||
assert_eq!(responses.len(), 1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user