Phase B: identifier+key on the wire, JCS envelope signing (FRX/0.5), registry binding

This commit is contained in:
George Coles
2026-09-15 05:54:52 -04:00
parent 559c260639
commit d30e612be8
12 changed files with 159 additions and 89 deletions
+31 -23
View File
@@ -7,7 +7,7 @@ use std::time::{Duration, Instant};
use common::{
ask, challenge, client, collection, config_for, messages_of_type, poll, poll_messages, publish,
query_envelope, register, spawn_relay, spawn_relay_with_capacity, unicast,
query_envelope, register, spawn_relay, spawn_relay_with_capacity, test_envelope, unicast,
};
use frxd::crypto::Keypair;
use frxd::index::LocalIndex;
@@ -87,7 +87,7 @@ async fn raw_query(
timeout_ms: u64,
) -> Vec<Value> {
register(http, relay_url, asker).await;
let envelope = query_envelope(asker, text, 5);
let envelope = query_envelope(asker, &asker.public_hex(), text, 5);
assert!(
publish(http, relay_url, &envelope)
.await
@@ -100,13 +100,14 @@ async fn raw_query(
#[test]
fn envelope_field_set_is_fixed() {
let key = Keypair::generate();
let envelope = Envelope::new(&key, TYPE_QUERY, json!({"qid": "q", "text": "t"}));
let envelope = test_envelope(&key, TYPE_QUERY, json!({"qid": "q", "text": "t"}));
let value = serde_json::to_value(&envelope).unwrap();
assert_eq!(
keys(&value),
BTreeSet::from([
"body".to_string(),
"from".to_string(),
"key".to_string(),
"nonce".to_string(),
"sig".to_string(),
"ts".to_string(),
@@ -337,7 +338,7 @@ async fn entities_are_optional_hints() {
"entities": ["Q999999", "not-a-real-entity"],
"budget": {"max_results": 5}
});
let envelope = Envelope::new(&alice, TYPE_QUERY, body);
let envelope = test_envelope(&alice, TYPE_QUERY, body);
assert!(
publish(&http, &relay_url, &envelope)
.await
@@ -384,7 +385,7 @@ async fn member_directory_filters_senders() {
assert_eq!(trusted.len(), 1, "trusted member got no response");
register(&http, &relay_url, &untrusted).await;
let envelope = query_envelope(&untrusted, "rust", 5);
let envelope = query_envelope(&untrusted, &untrusted.public_hex(), "rust", 5);
assert!(
publish(&http, &relay_url, &envelope)
.await
@@ -424,7 +425,7 @@ async fn rotated_keys_are_accepted_through_previous_listing() {
let http = client();
register(&http, &relay_url, &old_key).await;
let envelope = query_envelope(&old_key, "rust", 5);
let envelope = query_envelope(&old_key, &old_key.public_hex(), "rust", 5);
assert!(
publish(&http, &relay_url, &envelope)
.await
@@ -436,7 +437,7 @@ async fn rotated_keys_are_accepted_through_previous_listing() {
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);
let envelope = query_envelope(&old_key, &old_key.public_hex(), "rust", 5);
assert!(
publish(&http, &relay_url, &envelope)
.await
@@ -453,7 +454,7 @@ async fn stale_envelopes_are_rejected() {
let http = client();
let key = Keypair::generate();
let mut envelope = query_envelope(&key, "stale", 5);
let mut envelope = query_envelope(&key, &key.public_hex(), "stale", 5);
envelope.ts = frxd::crypto::now_ts().saturating_sub(3600);
envelope.sig = key.sign(&frxd::crypto::signing_bytes(&envelope));
assert_eq!(
@@ -461,7 +462,7 @@ async fn stale_envelopes_are_rejected() {
reqwest::StatusCode::BAD_REQUEST
);
let mut response = Envelope::new(
let mut response = test_envelope(
&key,
TYPE_RESPONSE,
json!({"qid": "q1", "results": [], "truncated": false, "more_available": 0, "cursor": null}),
@@ -484,12 +485,12 @@ async fn backpressure_is_visible_and_recoverable() {
let publisher = Keypair::generate();
register(&http, &relay_url, &member).await;
let first = query_envelope(&publisher, "one", 5);
let first = query_envelope(&publisher, &publisher.public_hex(), "one", 5);
assert_eq!(
publish(&http, &relay_url, &first).await.status(),
reqwest::StatusCode::ACCEPTED
);
let second = query_envelope(&publisher, "two", 5);
let second = query_envelope(&publisher, &publisher.public_hex(), "two", 5);
let response = publish(&http, &relay_url, &second).await;
assert_eq!(response.status(), reqwest::StatusCode::TOO_MANY_REQUESTS);
assert_eq!(
@@ -511,7 +512,7 @@ async fn backpressure_is_visible_and_recoverable() {
1
);
let third = query_envelope(&publisher, "three", 5);
let third = query_envelope(&publisher, &publisher.public_hex(), "three", 5);
assert_eq!(
publish(&http, &relay_url, &third).await.status(),
reqwest::StatusCode::ACCEPTED
@@ -527,7 +528,7 @@ async fn unicast_is_need_to_know() {
register(&http, &relay_url, &alice).await;
register(&http, &relay_url, &bob).await;
let query = query_envelope(&alice, "rust", 5);
let query = query_envelope(&alice, &alice.public_hex(), "rust", 5);
assert!(
publish(&http, &relay_url, &query)
.await
@@ -535,7 +536,7 @@ async fn unicast_is_need_to_know() {
.is_success()
);
let response = Envelope::new(
let response = test_envelope(
&bob,
TYPE_RESPONSE,
json!({"qid": "q1", "results": [], "truncated": false, "more_available": 0, "cursor": null}),
@@ -561,7 +562,7 @@ async fn unicast_to_unknown_member_is_visible() {
let relay_url = spawn_relay().await;
let http = client();
let sender = Keypair::generate();
let response = Envelope::new(
let response = test_envelope(
&sender,
TYPE_RESPONSE,
json!({"qid": "q1", "results": [], "truncated": false, "more_available": 0, "cursor": null}),
@@ -580,12 +581,12 @@ async fn tampered_envelopes_are_rejected() {
let relay_url = spawn_relay().await;
let http = client();
let key = Keypair::generate();
let mut envelope = query_envelope(&key, "rust", 5);
let mut envelope = query_envelope(&key, &key.public_hex(), "rust", 5);
envelope.body = json!({"qid": "q1", "text": "forged", "budget": {"max_results": 5}});
let publish_response = publish(&http, &relay_url, &envelope).await;
assert_eq!(publish_response.status(), reqwest::StatusCode::BAD_REQUEST);
let mut response = Envelope::new(
let mut response = test_envelope(
&key,
TYPE_RESPONSE,
json!({"qid": "q1", "results": [], "truncated": false, "more_available": 0, "cursor": null}),
@@ -854,7 +855,7 @@ async fn aggregates_are_unicast_only_and_ignored_by_nodes() {
let sender = Keypair::generate();
register(&http, &relay_url, &bob.node.key).await;
let broadcast = Envelope::new(
let broadcast = test_envelope(
&sender,
TYPE_AGGREGATE,
json!({"period": "2026-03", "sent": 1, "passed": 0}),
@@ -865,7 +866,7 @@ async fn aggregates_are_unicast_only_and_ignored_by_nodes() {
"aggregates must not travel the broadcast channel"
);
let unicast_envelope = Envelope::new(
let unicast_envelope = test_envelope(
&sender,
TYPE_AGGREGATE,
json!({"period": "2026-03", "sent": 1, "passed": 0}),
@@ -927,7 +928,7 @@ async fn duplicate_delivery_is_answered_once() {
let alice = Keypair::generate();
let http = client();
register(&http, &relay_url, &alice).await;
let envelope = query_envelope(&alice, "rust", 5);
let envelope = query_envelope(&alice, &alice.public_hex(), "rust", 5);
assert!(
publish(&http, &relay_url, &envelope)
.await
@@ -1000,7 +1001,8 @@ async fn multiple_responders_merge_with_provenance() {
fn canonical_signing_bytes_are_stable() {
let mut envelope = Envelope {
msg_type: TYPE_QUERY.to_string(),
from: "aa".repeat(32),
from: "alice.frx.example".to_string(),
key: "aa".repeat(32),
ts: 1_700_000_000,
nonce: "00112233445566778899aabbccddeeff".to_string(),
body: json!({"text": "rust", "qid": "q1", "budget": {"max_results": 5}, "entities": []}),
@@ -1011,9 +1013,15 @@ fn canonical_signing_bytes_are_stable() {
json!({"entities": [], "budget": {"max_results": 5}, "qid": "q1", "text": "rust"});
let second = frxd::crypto::signing_bytes(&envelope);
assert_eq!(first, second);
let key = frxd::crypto::Keypair::from_hex(&"00".repeat(32)).unwrap();
let signature = key.sign(&first);
assert_eq!(
String::from_utf8(first).unwrap(),
"FRX/0.4\nquery\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n1700000000\n00112233445566778899aabbccddeeff\n{\"budget\":{\"max_results\":5},\"entities\":[],\"qid\":\"q1\",\"text\":\"rust\"}"
"FRX/0.5\n{\"body\":{\"budget\":{\"max_results\":5},\"entities\":[],\"qid\":\"q1\",\"text\":\"rust\"},\"from\":\"alice.frx.example\",\"key\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"nonce\":\"00112233445566778899aabbccddeeff\",\"ts\":1700000000,\"type\":\"query\"}"
);
assert_eq!(
signature,
"2d5df0f85b5ea6b97d5dbc53d316b410c848d52e407583f9740d10889ba5a40fbcf03a930fa7d351689aa657884e3a32e0c6941ff11653a71d75dea58b3e9b09"
);
}
@@ -1033,7 +1041,7 @@ async fn query_body_without_entities_parses() {
let alice = Keypair::generate();
let http = client();
register(&http, &relay_url, &alice).await;
let envelope = Envelope::new(
let envelope = test_envelope(
&alice,
TYPE_QUERY,
json!({"qid": "min", "text": "rust", "budget": {"max_results": 5}}),