fix(marmot): compute parent_hash chain + align required_capabilities id
Answers "can MDK and marmot-ts see and talk to a user whose group was
created on Amethyst?" — yes, after four more spec-conformance fixes:
1. parent_hash chain (RFC 9420 §7.9.2): Amethyst's commit() and
externalJoin() now compute the parent_hash for every parent node on
the committer's direct path and seal the committer's LeafNode with
the correct leaf parent_hash. Amethyst↔Amethyst used to work only
because both sides stored empty parent_hash values; against a
strict peer (ts-mls, openmls) every Amethyst-authored commit was
rejected with "Unable to verify parent hash". processCommit()
now also patches the computed parent_hashes back into its tree so
treeHash() agrees with the sender — otherwise the epoch key
schedule diverges and AEAD tags mismatch.
2. REQUIRED_CAPABILITIES extension type: 0x0002 → 0x0003 per
RFC 9420 §13.3. The old value was ratchet_tree's slot, so Amethyst's
GroupContext.extensions carried a required_capabilities blob
labelled as ratchet_tree, and openmls rejected the GroupInfo
as Malformed (ratchet_tree is not valid in GroupContext). This
completes the extension-ID set from d7114fc (ratchet_tree,
external_pub) now aligned with the IANA registry.
3. verifyParentHash on the receive side now computes the expected
chain top-down from the post-update tree rather than reading
parent_hash fields that applyUpdatePath leaves as empty
placeholders.
4. An explicit parentHash parameter on buildLeafNode so COMMIT-source
leaves include the computed value in their TBS signature.
Test harness — reverse interop (Amethyst → outside world):
- quartz/tools/{mdk,tsmls}-vector-gen/emit-joiner-kp.{rs,mjs}
generate an MDK/openmls and a marmot-ts/ts-mls KeyPackage with
marmot_group_data (0xF2EE) and self_remove (0x000A) advertised in
capabilities so Amethyst's required_capabilities is satisfiable.
- AmethystAuthoredVectorGen.kt (env-var gated JUnit test) takes the
foreign KP, adds it to a fresh Amethyst group, and emits the
Welcome plus three application PrivateMessages as JSON.
- verify-amethyst.{rs,mjs} replay the joiner's private state,
call the foreign MLS library's join + process_message, and
assert the plaintexts match.
Both verifiers now print ALL PASS end-to-end:
* openmls ← Amethyst: joinGroup + 3× process_message ✓
* ts-mls ← Amethyst: joinGroup + 3× processPrivateMessage ✓
https://claude.ai/code/session_01HfHdd5S5rvxUW2ihEpLGJr
This commit is contained in:
@@ -7,8 +7,23 @@ edition = "2021"
|
||||
openmls = { version = "0.8.1", features = ["test-utils"] }
|
||||
openmls_rust_crypto = "0.5.1"
|
||||
openmls_basic_credential = { version = "0.5", features = ["test-utils"] }
|
||||
openmls_memory_storage = { version = "0.5", features = ["persistence"] }
|
||||
openmls_traits = "0.5"
|
||||
tls_codec = "0.4"
|
||||
hex = "0.4"
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
base64 = "0.22"
|
||||
env_logger = "0.11"
|
||||
|
||||
[[bin]]
|
||||
name = "mdk-vector-gen"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "emit-joiner-kp"
|
||||
path = "src/emit_joiner_kp.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "verify-amethyst"
|
||||
path = "src/verify_amethyst.rs"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Produce an MDK/OpenMLS-authored joiner KeyPackage for the reverse-interop
|
||||
// test (Amethyst → MDK). Emits TWO artifacts:
|
||||
// - a binary storage snapshot at $1 (openmls_memory_storage save_to_file
|
||||
// format) so verify_amethyst.rs can restore Bob's provider exactly as
|
||||
// it was when the KP was generated;
|
||||
// - JSON on stdout with the public KP bytes plus the private keys
|
||||
// (hex-encoded) so Amethyst can drive its addMember and the reader
|
||||
// can sanity-check shapes:
|
||||
//
|
||||
// {
|
||||
// "cipher_suite": 1,
|
||||
// "joiner": {
|
||||
// "key_package_raw": hex,
|
||||
// "init_priv": hex(32 B),
|
||||
// "encryption_priv": hex(32 B),
|
||||
// "signature_priv": hex(32 B, raw Ed25519 seed),
|
||||
// "signature_pub": hex(32 B)
|
||||
// }
|
||||
// }
|
||||
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::process;
|
||||
|
||||
use openmls::prelude::*;
|
||||
use openmls_basic_credential::SignatureKeyPair;
|
||||
use openmls_rust_crypto::OpenMlsRustCrypto;
|
||||
use openmls_traits::OpenMlsProvider;
|
||||
use tls_codec::Serialize;
|
||||
|
||||
const CS: Ciphersuite = Ciphersuite::MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
eprintln!("usage: emit-joiner-kp <storage-snapshot-path>");
|
||||
process::exit(2);
|
||||
}
|
||||
let snapshot_path = &args[1];
|
||||
|
||||
let provider = OpenMlsRustCrypto::default();
|
||||
let cred = BasicCredential::new(b"bob".to_vec());
|
||||
let sig = SignatureKeyPair::new(CS.signature_algorithm()).unwrap();
|
||||
sig.store(provider.storage()).unwrap();
|
||||
let cwk = CredentialWithKey {
|
||||
credential: cred.into(),
|
||||
signature_key: sig.public().into(),
|
||||
};
|
||||
|
||||
// Advertise the Marmot extensions a real MDK KeyPackage would carry,
|
||||
// so Amethyst's required_capabilities (which lists marmot_group_data
|
||||
// 0xF2EE and the self_remove proposal 0x000A) is satisfied when our
|
||||
// KP is added to an Amethyst group.
|
||||
let capabilities = Capabilities::new(
|
||||
None,
|
||||
Some(&[CS]),
|
||||
Some(&[
|
||||
ExtensionType::from(0xF2EE), // marmot_group_data
|
||||
ExtensionType::ApplicationId,
|
||||
ExtensionType::LastResort,
|
||||
]),
|
||||
Some(&[openmls::prelude::ProposalType::SelfRemove]),
|
||||
None,
|
||||
);
|
||||
let bundle = KeyPackage::builder()
|
||||
.leaf_node_capabilities(capabilities)
|
||||
.mark_as_last_resort()
|
||||
.build(CS, &provider, &sig, cwk)
|
||||
.unwrap();
|
||||
let kp = bundle.key_package().clone();
|
||||
let kp_bytes = kp.tls_serialize_detached().unwrap();
|
||||
|
||||
let init_priv: Vec<u8> = (**bundle.init_private_key()).to_vec();
|
||||
let enc_priv: Vec<u8> = (**bundle.encryption_private_key()).to_vec();
|
||||
|
||||
// Persist the entire provider storage — validate_amethyst restores it
|
||||
// byte-for-byte so the KeyPackage + bundle + signature key can be used
|
||||
// to process a Welcome.
|
||||
let file = File::create(snapshot_path).expect("open snapshot file");
|
||||
provider
|
||||
.storage()
|
||||
.save_to_file(&file)
|
||||
.expect("save storage snapshot");
|
||||
|
||||
let out = serde_json::json!({
|
||||
"cipher_suite": 1,
|
||||
"joiner": {
|
||||
"key_package_raw": hex::encode(&kp_bytes),
|
||||
"init_priv": hex::encode(&init_priv),
|
||||
"encryption_priv": hex::encode(&enc_priv),
|
||||
"signature_priv": hex::encode(sig.private()),
|
||||
"signature_pub": hex::encode(sig.public()),
|
||||
}
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&out).unwrap());
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Reverse-interop verifier: Amethyst → MDK/OpenMLS.
|
||||
//
|
||||
// Takes three paths on argv:
|
||||
// arg1 — path to the binary storage snapshot written by emit_joiner_kp.rs
|
||||
// (restores Bob's openmls provider exactly as it was when the
|
||||
// KeyPackage + bundle were generated);
|
||||
// arg2 — the joiner-handoff JSON (we only read the signature_pub here,
|
||||
// to look up Bob's own leaf after joining);
|
||||
// arg3 — the Amethyst-authored fixture (Welcome + PrivateMessages).
|
||||
//
|
||||
// Prints PASS/... lines on success and exits non-zero on any failure.
|
||||
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::process;
|
||||
|
||||
use base64::Engine;
|
||||
use openmls::prelude::*;
|
||||
use openmls_rust_crypto::OpenMlsRustCrypto;
|
||||
use openmls_traits::OpenMlsProvider;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use tls_codec::Deserialize as TlsDeserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Handoff {
|
||||
cipher_suite: u16,
|
||||
joiner: HandoffJoiner,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct HandoffJoiner {
|
||||
signature_pub: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Fixture {
|
||||
cipher_suite: u16,
|
||||
welcome: String,
|
||||
app_messages_alice_to_bob: Vec<AppMessage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AppMessage {
|
||||
plaintext: String,
|
||||
private_message: String,
|
||||
}
|
||||
|
||||
fn hex_bytes(s: &str) -> Vec<u8> {
|
||||
hex::decode(s).expect("bad hex in fixture")
|
||||
}
|
||||
|
||||
fn fail(msg: &str) -> ! {
|
||||
eprintln!("FAIL: {msg}");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 4 {
|
||||
eprintln!(
|
||||
"usage: verify-amethyst <storage-snapshot.bin> \
|
||||
<joiner-handoff.json> <amethyst-fixture.json>"
|
||||
);
|
||||
process::exit(2);
|
||||
}
|
||||
|
||||
let handoff: Handoff = serde_json::from_str(&fs::read_to_string(&args[2]).unwrap()).unwrap();
|
||||
let fixture: Fixture = serde_json::from_str(&fs::read_to_string(&args[3]).unwrap()).unwrap();
|
||||
assert_eq!(handoff.cipher_suite, 1);
|
||||
assert_eq!(fixture.cipher_suite, 1);
|
||||
|
||||
// Restore Bob's MemoryStorage key/value map from the snapshot. We
|
||||
// replicate the on-disk format here (it's a simple base64-encoded
|
||||
// HashMap) instead of going through MemoryStorage::load_from_file —
|
||||
// that API wants `&mut self` on a field we can't move into the
|
||||
// OpenMlsRustCrypto provider (its fields are private and there's no
|
||||
// constructor that accepts a pre-populated storage).
|
||||
#[derive(Deserialize)]
|
||||
struct SerializableKeyStore {
|
||||
values: HashMap<String, String>,
|
||||
}
|
||||
let snap: SerializableKeyStore =
|
||||
serde_json::from_reader(File::open(&args[1]).expect("open snapshot for read"))
|
||||
.expect("parse snapshot JSON");
|
||||
let provider = OpenMlsRustCrypto::default();
|
||||
{
|
||||
let mut map = provider.storage().values.write().unwrap();
|
||||
for (k, v) in snap.values {
|
||||
map.insert(
|
||||
base64::prelude::BASE64_STANDARD.decode(k).unwrap(),
|
||||
base64::prelude::BASE64_STANDARD.decode(v).unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Amethyst's Welcome.
|
||||
let welcome_bytes = hex_bytes(&fixture.welcome);
|
||||
let msg_in = MlsMessageIn::tls_deserialize(&mut welcome_bytes.as_slice())
|
||||
.unwrap_or_else(|e| fail(&format!("welcome decode: {e:?}")));
|
||||
let welcome = match msg_in.extract() {
|
||||
MlsMessageBodyIn::Welcome(w) => w,
|
||||
other => fail(&format!("expected Welcome, got {other:?}")),
|
||||
};
|
||||
|
||||
let cfg = MlsGroupJoinConfig::builder().build();
|
||||
let staged = StagedWelcome::new_from_welcome(&provider, &cfg, welcome, None)
|
||||
.unwrap_or_else(|e| fail(&format!("StagedWelcome::new_from_welcome: {e:?}")));
|
||||
let mut group = staged
|
||||
.into_group(&provider)
|
||||
.unwrap_or_else(|e| fail(&format!("StagedWelcome::into_group: {e:?}")));
|
||||
println!(
|
||||
"PASS: joined Amethyst-authored Welcome at epoch {}",
|
||||
group.epoch().as_u64()
|
||||
);
|
||||
|
||||
// Sanity-check: the signature_pub we advertised must appear in the
|
||||
// ratchet tree as OUR leaf.
|
||||
let expected_sig_pub = hex_bytes(&handoff.joiner.signature_pub);
|
||||
let own_leaf = group.own_leaf_node().unwrap();
|
||||
if own_leaf.signature_key().as_slice() != expected_sig_pub.as_slice() {
|
||||
fail("own leaf signature key does not match handoff signature_pub");
|
||||
}
|
||||
|
||||
for (idx, app) in fixture.app_messages_alice_to_bob.iter().enumerate() {
|
||||
let bytes = hex_bytes(&app.private_message);
|
||||
let msg = MlsMessageIn::tls_deserialize(&mut bytes.as_slice())
|
||||
.unwrap_or_else(|e| fail(&format!("app msg {idx} decode: {e:?}")));
|
||||
let protocol = msg
|
||||
.try_into_protocol_message()
|
||||
.unwrap_or_else(|e| fail(&format!("app msg {idx} not a protocol message: {e:?}")));
|
||||
let processed = group
|
||||
.process_message(&provider, protocol)
|
||||
.unwrap_or_else(|e| fail(&format!("app msg {idx} process_message: {e:?}")));
|
||||
match processed.into_content() {
|
||||
ProcessedMessageContent::ApplicationMessage(app_msg) => {
|
||||
let got = app_msg.into_bytes();
|
||||
let want = hex_bytes(&app.plaintext);
|
||||
if got != want {
|
||||
fail(&format!(
|
||||
"app msg {idx} plaintext mismatch\n want: {}\n got : {}",
|
||||
hex::encode(&want),
|
||||
hex::encode(&got),
|
||||
));
|
||||
}
|
||||
println!("PASS: decrypted Amethyst app message {idx}");
|
||||
}
|
||||
other => fail(&format!("app msg {idx} unexpected content: {other:?}")),
|
||||
}
|
||||
}
|
||||
println!("ALL PASS — openmls read every Amethyst-authored artifact.");
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Produce a ts-mls-authored joiner KeyPackage for the reverse-interop test
|
||||
// (Amethyst → ts-mls). Emits on stdout:
|
||||
//
|
||||
// {
|
||||
// "cipher_suite": 1,
|
||||
// "joiner": {
|
||||
// "key_package_raw": hex, // inner KeyPackage bytes
|
||||
// "init_priv": hex(32 B),
|
||||
// "encryption_priv":hex(32 B),
|
||||
// "signature_priv": hex(32 B, raw seed), // PKCS#8 header stripped
|
||||
// "signature_pub": hex(32 B)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Amethyst reads `key_package_raw` to drive its MlsGroup.addMember flow;
|
||||
// verify-amethyst-fixture.mjs reads the private keys back to drive
|
||||
// ts-mls joinGroup + processPrivateMessage against Amethyst's Welcome.
|
||||
|
||||
import {
|
||||
defaultCapabilities,
|
||||
defaultCryptoProvider,
|
||||
defaultLifetime,
|
||||
generateKeyPackage,
|
||||
getCiphersuiteImpl,
|
||||
keyPackageEncoder,
|
||||
encode,
|
||||
} from "ts-mls";
|
||||
|
||||
const hex = (bytes) =>
|
||||
Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
|
||||
const cs = await getCiphersuiteImpl(
|
||||
"MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519",
|
||||
defaultCryptoProvider,
|
||||
);
|
||||
|
||||
// Mirror MDK: advertise marmot_group_data (0xF2EE) as a supported
|
||||
// extension and self_remove (0x000A) as a supported proposal so
|
||||
// Amethyst's required_capabilities is satisfied when our KP is added.
|
||||
const caps = defaultCapabilities();
|
||||
if (!caps.extensions.includes(0xf2ee)) caps.extensions.push(0xf2ee);
|
||||
if (!caps.proposals.includes(0x000a)) caps.proposals.push(0x000a);
|
||||
|
||||
const kp = await generateKeyPackage({
|
||||
credential: { credentialType: 1, identity: new TextEncoder().encode("bob") },
|
||||
capabilities: caps,
|
||||
lifetime: defaultLifetime(),
|
||||
cipherSuite: cs,
|
||||
});
|
||||
|
||||
const sigPriv = kp.privatePackage.signaturePrivateKey;
|
||||
const sigSeed =
|
||||
sigPriv.length === 32
|
||||
? sigPriv
|
||||
: sigPriv.length === 64
|
||||
? sigPriv.slice(0, 32)
|
||||
: sigPriv.slice(sigPriv.length - 32);
|
||||
|
||||
const out = {
|
||||
cipher_suite: 1,
|
||||
joiner: {
|
||||
key_package_raw: hex(encode(keyPackageEncoder, kp.publicPackage)),
|
||||
init_priv: hex(kp.privatePackage.initPrivateKey),
|
||||
encryption_priv: hex(kp.privatePackage.hpkePrivateKey),
|
||||
signature_priv: hex(sigSeed),
|
||||
signature_pub: hex(kp.publicPackage.leafNode.signaturePublicKey),
|
||||
},
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
||||
@@ -0,0 +1,153 @@
|
||||
// Reverse-interop verifier: Amethyst → ts-mls.
|
||||
//
|
||||
// Reads two JSON files:
|
||||
// $1 — the joiner-KP handoff produced by emit-joiner-kp.mjs
|
||||
// (contains Bob's private keys in ts-mls-compatible form).
|
||||
// $2 — the Amethyst-authored fixture (produced by the Kotlin
|
||||
// AmethystAuthoredVectorGen test) containing Alice's Welcome
|
||||
// and three PrivateMessages she sent to Bob.
|
||||
//
|
||||
// Succeeds (exit 0) if Bob can:
|
||||
// 1. decode Amethyst's KeyPackage wrapper (basic wire-format sanity),
|
||||
// 2. join Alice's group from Amethyst's Welcome,
|
||||
// 3. decrypt each of Alice's application messages and match the
|
||||
// expected plaintext.
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
decode,
|
||||
defaultCryptoProvider,
|
||||
getCiphersuiteImpl,
|
||||
joinGroup,
|
||||
keyPackageDecoder,
|
||||
mlsMessageDecoder,
|
||||
processPrivateMessage,
|
||||
unsafeTestingAuthenticationService,
|
||||
wireformats,
|
||||
} from "ts-mls";
|
||||
|
||||
function assert(cond, msg) {
|
||||
if (!cond) {
|
||||
console.error("FAIL:", msg);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const hexToBytes = (s) =>
|
||||
Uint8Array.from(s.match(/../g).map((b) => parseInt(b, 16)));
|
||||
|
||||
const [handoffPath, fixturePath] = process.argv.slice(2);
|
||||
if (!handoffPath || !fixturePath) {
|
||||
console.error(
|
||||
"usage: verify-amethyst-fixture.mjs <joiner-handoff.json> <amethyst-fixture.json>",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const handoff = JSON.parse(readFileSync(handoffPath, "utf8"));
|
||||
const fixture = JSON.parse(readFileSync(fixturePath, "utf8"));
|
||||
|
||||
const cs = await getCiphersuiteImpl(
|
||||
"MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519",
|
||||
defaultCryptoProvider,
|
||||
);
|
||||
const ctx = {
|
||||
cipherSuite: cs,
|
||||
authService: unsafeTestingAuthenticationService,
|
||||
};
|
||||
|
||||
// Rebuild Bob's public+private KP from the handoff.
|
||||
const kpBytes = hexToBytes(handoff.joiner.key_package_raw);
|
||||
const kpPub = decode(keyPackageDecoder, kpBytes);
|
||||
assert(kpPub, "failed to decode joiner key_package_raw");
|
||||
|
||||
// ts-mls signature_priv is a PKCS#8 Ed25519 DER envelope. Reconstruct
|
||||
// it from the raw 32-byte seed emitted by the handoff.
|
||||
const PKCS8_ED25519_PREFIX = hexToBytes("302e020100300506032b657004220420");
|
||||
const seed = hexToBytes(handoff.joiner.signature_priv);
|
||||
const sigPrivDer = new Uint8Array(PKCS8_ED25519_PREFIX.length + seed.length);
|
||||
sigPrivDer.set(PKCS8_ED25519_PREFIX, 0);
|
||||
sigPrivDer.set(seed, PKCS8_ED25519_PREFIX.length);
|
||||
|
||||
const privateKeys = {
|
||||
initPrivateKey: hexToBytes(handoff.joiner.init_priv),
|
||||
hpkePrivateKey: hexToBytes(handoff.joiner.encryption_priv),
|
||||
signaturePrivateKey: sigPrivDer,
|
||||
};
|
||||
|
||||
// Decode Amethyst's Welcome (wrapped in MlsMessage).
|
||||
const welcomeBytes = hexToBytes(fixture.welcome);
|
||||
const welcomeMsg = decode(mlsMessageDecoder, welcomeBytes);
|
||||
assert(welcomeMsg, "failed to decode Amethyst Welcome bytes");
|
||||
assert(
|
||||
welcomeMsg.wireformat === wireformats.mls_welcome,
|
||||
"Amethyst Welcome wire-format mismatch",
|
||||
);
|
||||
|
||||
// Join.
|
||||
let state;
|
||||
try {
|
||||
state = await joinGroup({
|
||||
context: ctx,
|
||||
welcome: welcomeMsg.welcome,
|
||||
keyPackage: kpPub,
|
||||
privateKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("FAIL: joinGroup threw:", e?.message ?? e);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("PASS: joined Amethyst-authored Welcome at epoch", state.groupContext.epoch);
|
||||
|
||||
// Dump the decoded GroupInfo for diagnostic use by other verifiers.
|
||||
// ts-mls already unwrapped it during joinGroup, so we just print a
|
||||
// compact summary + the full extensions array.
|
||||
{
|
||||
const gc = state.groupContext;
|
||||
const ext = state.publicGroupState?.groupInfoExtensions;
|
||||
// (state layout varies; these fields may or may not exist. Best-effort.)
|
||||
if (process.env.DUMP_GROUP_INFO) {
|
||||
console.log(JSON.stringify({ groupContext: gc, extensions: ext }, (_k, v) =>
|
||||
v instanceof Uint8Array ? Array.from(v).map(b => b.toString(16).padStart(2, "0")).join("") : v,
|
||||
2));
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt each application message.
|
||||
for (const [idx, msg] of fixture.app_messages_alice_to_bob.entries()) {
|
||||
const msgBytes = hexToBytes(msg.private_message);
|
||||
const framedMsg = decode(mlsMessageDecoder, msgBytes);
|
||||
assert(framedMsg, `failed to decode application message ${idx}`);
|
||||
assert(
|
||||
framedMsg.wireformat === wireformats.mls_private_message,
|
||||
`application message ${idx} wire-format mismatch`,
|
||||
);
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await processPrivateMessage({
|
||||
context: ctx,
|
||||
state,
|
||||
privateMessage: framedMsg.privateMessage,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`FAIL: processPrivateMessage(${idx}) threw:`, e?.message ?? e);
|
||||
process.exit(1);
|
||||
}
|
||||
state = res.newState;
|
||||
if (res.kind !== "applicationMessage") {
|
||||
console.error(`FAIL: processPrivateMessage(${idx}) returned kind=${res.kind}, expected applicationMessage`);
|
||||
process.exit(1);
|
||||
}
|
||||
const gotHex = Array.from(res.message ?? new Uint8Array(), (b) =>
|
||||
b.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
if (gotHex !== msg.plaintext) {
|
||||
console.error(
|
||||
`FAIL: app message ${idx} plaintext mismatch\n want: ${msg.plaintext}\n got : ${gotHex}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`PASS: decrypted Amethyst app message ${idx}`);
|
||||
}
|
||||
|
||||
console.log("ALL PASS — ts-mls read every Amethyst-authored artifact.");
|
||||
Reference in New Issue
Block a user