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