main.rs raw
1 // mls-interop: Interop test binary for Go↔Rust MLS (NIP-EE) validation.
2 //
3 // Reads JSON commands from stdin (one per line), processes with MDK,
4 // writes JSON results to stdout. State persists across commands.
5 //
6 // Commands:
7 // generate_key_package → outputs kind 443 event JSON
8 // process_key_package → validates a Go-generated kind 443 event
9 // create_group → creates group from member key packages, outputs welcome + message
10 // process_welcome → joins group from kind 444 welcome rumor
11 // create_message → encrypts a message, outputs kind 445 event
12 // process_message → decrypts a kind 445 event
13
14 use base64::engine::general_purpose::STANDARD as BASE64;
15 use base64::Engine;
16 use mdk_core::prelude::*;
17 use mdk_core::messages::MessageProcessingResult;
18 use mdk_core::MDK;
19 use mdk_memory_storage::MdkMemoryStorage;
20 use nostr::event::builder::EventBuilder;
21 use nostr::{Event, EventId, JsonUtil, Keys, Kind, RelayUrl, UnsignedEvent};
22 use openmls::prelude::tls_codec::Deserialize as TlsDeserialize;
23 use openmls::key_packages::KeyPackageIn;
24 use serde::{Deserialize, Serialize};
25 use std::io::{self, BufRead, Write};
26
27 #[derive(Deserialize)]
28 #[serde(tag = "cmd")]
29 enum Command {
30 #[serde(rename = "generate_key_package")]
31 GenerateKeyPackage {
32 relay: String,
33 },
34 #[serde(rename = "process_key_package")]
35 ProcessKeyPackage {
36 event_json: String,
37 },
38 #[serde(rename = "create_group")]
39 CreateGroup {
40 member_kp_event_json: String,
41 name: String,
42 relay: String,
43 },
44 #[serde(rename = "process_welcome")]
45 ProcessWelcome {
46 rumor_json: String,
47 },
48 #[serde(rename = "create_message")]
49 CreateMessage {
50 mls_group_id_hex: String,
51 content: String,
52 },
53 #[serde(rename = "process_message")]
54 ProcessMessage {
55 event_json: String,
56 },
57 #[serde(rename = "get_group_info")]
58 GetGroupInfo {},
59 #[serde(rename = "parse_kp_raw")]
60 ParseKpRaw {
61 kp_base64: String,
62 },
63 #[serde(rename = "dump_kp_bytes")]
64 DumpKpBytes {
65 relay: String,
66 },
67 }
68
69 #[derive(Serialize)]
70 struct Response {
71 ok: bool,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 error: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 event_json: Option<String>,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 rumor_json: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pubkey: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 content: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 kind: Option<u16>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 mls_group_id_hex: Option<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 nostr_group_id_hex: Option<String>,
88 }
89
90 impl Response {
91 fn ok() -> Self {
92 Response {
93 ok: true,
94 error: None,
95 event_json: None,
96 rumor_json: None,
97 pubkey: None,
98 content: None,
99 kind: None,
100 mls_group_id_hex: None,
101 nostr_group_id_hex: None,
102 }
103 }
104 fn err(msg: String) -> Self {
105 Response {
106 ok: false,
107 error: Some(msg),
108 event_json: None,
109 rumor_json: None,
110 pubkey: None,
111 content: None,
112 kind: None,
113 mls_group_id_hex: None,
114 nostr_group_id_hex: None,
115 }
116 }
117 }
118
119 struct State {
120 keys: Keys,
121 mdk: MDK<MdkMemoryStorage>,
122 }
123
124 fn main() {
125 let keys = Keys::generate();
126 let mdk = MDK::new(MdkMemoryStorage::default());
127 let mut state = State { keys, mdk };
128
129 // Print our pubkey on startup so Go knows it
130 let init = serde_json::json!({
131 "ready": true,
132 "pubkey": state.keys.public_key().to_hex(),
133 });
134 println!("{}", init);
135 io::stdout().flush().unwrap();
136
137 let stdin = io::stdin();
138 for line in stdin.lock().lines() {
139 let line = match line {
140 Ok(l) => l,
141 Err(_) => break,
142 };
143 if line.trim().is_empty() {
144 continue;
145 }
146
147 let cmd: Command = match serde_json::from_str(&line) {
148 Ok(c) => c,
149 Err(e) => {
150 let r = Response::err(format!("parse command: {}", e));
151 println!("{}", serde_json::to_string(&r).unwrap());
152 io::stdout().flush().unwrap();
153 continue;
154 }
155 };
156
157 let resp = handle_command(&mut state, cmd);
158 println!("{}", serde_json::to_string(&resp).unwrap());
159 io::stdout().flush().unwrap();
160 }
161 }
162
163 fn handle_command(state: &mut State, cmd: Command) -> Response {
164 match cmd {
165 Command::GenerateKeyPackage { relay } => generate_key_package(state, &relay),
166 Command::ProcessKeyPackage { event_json } => process_key_package(state, &event_json),
167 Command::CreateGroup {
168 member_kp_event_json,
169 name,
170 relay,
171 } => create_group(state, &member_kp_event_json, &name, &relay),
172 Command::ProcessWelcome { rumor_json } => process_welcome(state, &rumor_json),
173 Command::CreateMessage {
174 mls_group_id_hex,
175 content,
176 } => create_message(state, &mls_group_id_hex, &content),
177 Command::ProcessMessage { event_json } => process_message(state, &event_json),
178 Command::GetGroupInfo {} => get_group_info(state),
179 Command::ParseKpRaw { kp_base64 } => parse_kp_raw(&kp_base64),
180 Command::DumpKpBytes { relay } => dump_kp_bytes(state, &relay),
181 }
182 }
183
184 fn generate_key_package(state: &mut State, relay: &str) -> Response {
185 let relay_url = match RelayUrl::parse(relay) {
186 Ok(u) => u,
187 Err(e) => return Response::err(format!("parse relay URL: {}", e)),
188 };
189
190 let (content, tags, _hash_ref) = match state
191 .mdk
192 .create_key_package_for_event(&state.keys.public_key(), [relay_url])
193 {
194 Ok(r) => r,
195 Err(e) => return Response::err(format!("create key package: {}", e)),
196 };
197
198 // Build the kind 443 event
199 let event = match EventBuilder::new(Kind::MlsKeyPackage, &content)
200 .tags(tags)
201 .sign_with_keys(&state.keys)
202 {
203 Ok(ev) => ev,
204 Err(e) => return Response::err(format!("sign key package event: {}", e)),
205 };
206
207 let mut resp = Response::ok();
208 resp.event_json = Some(event.as_json());
209 resp.pubkey = Some(state.keys.public_key().to_hex());
210 resp
211 }
212
213 fn process_key_package(_state: &mut State, event_json: &str) -> Response {
214 // Just validate we can parse it as a nostr event with the right structure
215 let event: Event = match Event::from_json(event_json) {
216 Ok(ev) => ev,
217 Err(e) => return Response::err(format!("parse event JSON: {}", e)),
218 };
219
220 if event.kind != Kind::MlsKeyPackage {
221 return Response::err(format!(
222 "wrong kind: expected 443, got {}",
223 event.kind.as_u16()
224 ));
225 }
226
227 // Validate content is valid base64
228 let content_str = event.content.to_string();
229 match BASE64.decode(&content_str) {
230 Ok(bytes) => {
231 if bytes.len() < 10 {
232 return Response::err(format!("key package too short: {} bytes", bytes.len()));
233 }
234 }
235 Err(e) => return Response::err(format!("invalid base64 content: {}", e)),
236 }
237
238 // Check required tags
239 let has_encoding = event
240 .tags
241 .iter()
242 .any(|t| t.kind().to_string() == "encoding");
243 if !has_encoding {
244 return Response::err("missing 'encoding' tag".to_string());
245 }
246
247 let mut resp = Response::ok();
248 resp.pubkey = Some(event.pubkey.to_hex());
249 resp
250 }
251
252 fn create_group(
253 state: &mut State,
254 member_kp_event_json: &str,
255 name: &str,
256 relay: &str,
257 ) -> Response {
258 let member_event: Event = match Event::from_json(member_kp_event_json) {
259 Ok(ev) => ev,
260 Err(e) => return Response::err(format!("parse member KP event: {}", e)),
261 };
262
263 eprintln!("[interop] parse_key_package...");
264 match state.mdk.parse_key_package(&member_event) {
265 Ok(kp) => eprintln!("[interop] parse_key_package OK, ciphersuite={:?}", kp.ciphersuite()),
266 Err(e) => eprintln!("[interop] parse_key_package failed: {}", e),
267 }
268
269 let relay_url = match RelayUrl::parse(relay) {
270 Ok(u) => u,
271 Err(e) => return Response::err(format!("parse relay URL: {}", e)),
272 };
273
274 let config = NostrGroupConfigData::new(
275 name.to_string(),
276 String::new(),
277 None,
278 None,
279 None,
280 vec![relay_url],
281 vec![state.keys.public_key(), member_event.pubkey],
282 );
283
284 eprintln!("[interop] calling create_group...");
285 let result = match state.mdk.create_group(
286 &state.keys.public_key(),
287 vec![member_event],
288 config,
289 ) {
290 Ok(r) => r,
291 Err(e) => return Response::err(format!("create group: {}", e)),
292 };
293
294 // NOTE: MDK's create_group already calls merge_pending_commit internally
295 eprintln!("[interop] create_group succeeded");
296
297 let welcome_rumor = match result.welcome_rumors.first() {
298 Some(r) => r,
299 None => return Response::err("no welcome rumor generated".to_string()),
300 };
301
302 let mut resp = Response::ok();
303 resp.rumor_json = Some(welcome_rumor.as_json());
304 resp.mls_group_id_hex = Some(hex::encode(result.group.mls_group_id.as_slice()));
305 resp.nostr_group_id_hex = Some(hex::encode(&result.group.nostr_group_id));
306 resp
307 }
308
309 fn process_welcome(state: &mut State, rumor_json: &str) -> Response {
310 let rumor: UnsignedEvent = match UnsignedEvent::from_json(rumor_json) {
311 Ok(r) => r,
312 Err(e) => return Response::err(format!("parse rumor JSON: {}", e)),
313 };
314
315 let welcome = match state
316 .mdk
317 .process_welcome(&EventId::all_zeros(), &rumor)
318 {
319 Ok(w) => w,
320 Err(e) => return Response::err(format!("process welcome: {}", e)),
321 };
322
323 if let Err(e) = state.mdk.accept_welcome(&welcome) {
324 return Response::err(format!("accept welcome: {}", e));
325 }
326
327 let groups = match state.mdk.get_groups() {
328 Ok(g) => g,
329 Err(e) => return Response::err(format!("get groups: {}", e)),
330 };
331
332 let group = match groups.first() {
333 Some(g) => g,
334 None => return Response::err("no groups after accepting welcome".to_string()),
335 };
336
337 let mut resp = Response::ok();
338 resp.mls_group_id_hex = Some(hex::encode(group.mls_group_id.as_slice()));
339 resp.nostr_group_id_hex = Some(hex::encode(&group.nostr_group_id));
340 resp
341 }
342
343 fn create_message(state: &mut State, mls_group_id_hex: &str, content: &str) -> Response {
344 let mls_group_id_bytes = match hex::decode(mls_group_id_hex) {
345 Ok(b) => b,
346 Err(e) => return Response::err(format!("decode mls_group_id: {}", e)),
347 };
348 let group_id = GroupId::from_slice(&mls_group_id_bytes);
349
350 let rumor = EventBuilder::new(Kind::Custom(9), content).build(state.keys.public_key());
351
352 let event = match state.mdk.create_message(&group_id, rumor) {
353 Ok(ev) => ev,
354 Err(e) => return Response::err(format!("create message: {}", e)),
355 };
356
357 let mut resp = Response::ok();
358 resp.event_json = Some(event.as_json());
359 resp
360 }
361
362 fn process_message(state: &mut State, event_json: &str) -> Response {
363 let event: Event = match Event::from_json(event_json) {
364 Ok(ev) => ev,
365 Err(e) => return Response::err(format!("parse event JSON: {}", e)),
366 };
367
368 match state.mdk.process_message(&event) {
369 Ok(MessageProcessingResult::ApplicationMessage(msg)) => {
370 let mut resp = Response::ok();
371 resp.content = Some(msg.content.clone());
372 resp.kind = Some(msg.kind.as_u16());
373 resp.pubkey = Some(msg.pubkey.to_hex());
374 resp
375 }
376 Ok(other) => Response::err(format!("unexpected result: {:?}", other)),
377 Err(e) => Response::err(format!("process message: {}", e)),
378 }
379 }
380
381 fn get_group_info(state: &mut State) -> Response {
382 let groups = match state.mdk.get_groups() {
383 Ok(g) => g,
384 Err(e) => return Response::err(format!("get groups: {}", e)),
385 };
386
387 if groups.is_empty() {
388 return Response::err("no groups".to_string());
389 }
390
391 let group = &groups[0];
392 let mut resp = Response::ok();
393 resp.mls_group_id_hex = Some(hex::encode(group.mls_group_id.as_slice()));
394 resp.nostr_group_id_hex = Some(hex::encode(&group.nostr_group_id));
395 resp
396 }
397
398 fn dump_kp_bytes(state: &mut State, relay: &str) -> Response {
399 let relay_url = match RelayUrl::parse(relay) {
400 Ok(u) => u,
401 Err(e) => return Response::err(format!("parse relay URL: {}", e)),
402 };
403
404 let (content_b64, _tags, _hash_ref) = match state
405 .mdk
406 .create_key_package_for_event(&state.keys.public_key(), [relay_url])
407 {
408 Ok(r) => r,
409 Err(e) => return Response::err(format!("create key package: {}", e)),
410 };
411
412 // content_b64 is base64(tls_serialize_detached(KeyPackage))
413 let raw_bytes = match BASE64.decode(&content_b64) {
414 Ok(b) => b,
415 Err(e) => return Response::err(format!("decode own b64: {}", e)),
416 };
417
418 let hex_str = hex::encode(&raw_bytes);
419 eprintln!("[interop] Rust raw KP bytes ({} bytes): {}", raw_bytes.len(), &hex_str);
420
421 let mut resp = Response::ok();
422 resp.content = Some(hex_str);
423 resp
424 }
425
426 fn parse_kp_raw(kp_base64: &str) -> Response {
427 let kp_bytes = match BASE64.decode(kp_base64) {
428 Ok(b) => b,
429 Err(e) => return Response::err(format!("base64 decode: {}", e)),
430 };
431
432 eprintln!("[interop] raw KP bytes ({} bytes): {}", kp_bytes.len(), hex::encode(&kp_bytes));
433
434 match KeyPackageIn::tls_deserialize(&mut kp_bytes.as_slice()) {
435 Ok(_kp_in) => {
436 eprintln!("[interop] OpenMLS parsed KeyPackageIn OK");
437 let mut resp = Response::ok();
438 resp.content = Some(format!("parsed OK"));
439 resp
440 }
441 Err(e) => {
442 eprintln!("[interop] OpenMLS deserialize failed: {:?}", e);
443 Response::err(format!("tls_deserialize: {:?}", e))
444 }
445 }
446 }
447