This commit is contained in:
Alex Emmet 2026-06-25 16:46:27 +02:00
commit 8194fff3b6
4 changed files with 93 additions and 66 deletions

View file

@ -45,7 +45,7 @@ config.server_certificate_hashes = [ // optional, for certificate pinn
### TLS Certificate Handling
By default — when `server_certificate_hashes` is not set — the browser uses its
By default, when `server_certificate_hashes` is not set, the browser uses its
**built-in root certificate store** to verify the server's TLS certificate,
just like any other HTTPS/WebSocket connection. This works with publicly-trusted
certificate authorities automatically.

View file

@ -309,11 +309,7 @@ impl DataValue {
* Returns `None` if the value is not a `Container` or signing fails.
*/
#[cfg(feature = "crypto")]
pub fn sign_container(
&mut self,
algorithm: u8,
signer: &impl SignatureScheme,
) -> Option<()> {
pub fn sign_container(&mut self, algorithm: u8, signer: &impl SignatureScheme) -> Option<()> {
let entries = self.as_container()?;
let container_bytes = Self::encode_container(&entries);
@ -334,10 +330,7 @@ impl DataValue {
* blob is malformed.
*/
#[cfg(feature = "crypto")]
pub fn verify_into_container(
&mut self,
verifier: &impl SignatureScheme,
) -> Option<()> {
pub fn verify_into_container(&mut self, verifier: &impl SignatureScheme) -> Option<()> {
let blob = self.as_signed_container()?;
if blob.len() < 1 + 64 + 2 {
return None;
@ -380,7 +373,7 @@ impl DataValue {
/*
* Decrypt a `SignedEncryptedContainer` in-place, replacing it with a
* `SignedContainer`. Does NOT verify call `verify_into_container` next.
* `SignedContainer`. Does NOT verify; call `verify_into_container` next.
*/
#[cfg(feature = "crypto")]
pub fn decrypt_signed_encrypted_container(
@ -1061,9 +1054,7 @@ mod tests {
#[test]
fn test_null_in_container() {
container_roundtrip(vec![
(DataTypeId(1), DataValue::Null),
]);
container_roundtrip(vec![(DataTypeId(1), DataValue::Null)]);
}
#[test]
@ -1103,21 +1094,23 @@ mod tests {
#[test]
fn test_container_nested_roundtrip() {
container_roundtrip(vec![
(DataTypeId(1), DataValue::Container(vec![
(DataTypeId(10), DataValue::BoolTrue),
])),
(DataTypeId(2), DataValue::Array(vec![
DataValue::SignedNumber(1),
DataValue::SignedNumber(2),
])),
(
DataTypeId(1),
DataValue::Container(vec![(DataTypeId(10), DataValue::BoolTrue)]),
),
(
DataTypeId(2),
DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]),
),
]);
}
#[test]
fn test_container_base64_roundtrip() {
let dv = DataValue::Container(vec![
(DataTypeId(7), DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF])),
]);
let dv = DataValue::Container(vec![(
DataTypeId(7),
DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
)]);
let b64 = dv.to_base64();
let decoded = DataValue::from_base64(&b64).expect("base64 roundtrip failed");
assert_eq!(dv, decoded);
@ -1128,11 +1121,17 @@ mod tests {
assert_eq!(DataValue::BoolTrue.kind(), DataKind::Bool);
assert_eq!(DataValue::Bool(false).kind(), DataKind::Bool);
assert_eq!(DataValue::SignedNumber(0).kind(), DataKind::SignedNumber);
assert_eq!(DataValue::UnsignedNumber(0).kind(), DataKind::UnsignedNumber);
assert_eq!(
DataValue::UnsignedNumber(0).kind(),
DataKind::UnsignedNumber
);
assert_eq!(DataValue::Float(0, 0).kind(), DataKind::Float);
assert_eq!(DataValue::Str(String::new()).kind(), DataKind::Str);
assert_eq!(DataValue::Bytes(vec![]).kind(), DataKind::Bytes);
assert_eq!(DataValue::Array(vec![]).kind(), DataKind::Array(Box::new(DataKind::Null)));
assert_eq!(
DataValue::Array(vec![]).kind(),
DataKind::Array(Box::new(DataKind::Null))
);
assert_eq!(DataValue::Container(vec![]).kind(), DataKind::Container);
assert_eq!(DataValue::Null.kind(), DataKind::Null);
}
@ -1147,10 +1146,22 @@ mod tests {
]);
let map = dv.as_map().expect("should be a container");
assert_eq!(map.get(&DataTypeId(1)).and_then(|v| v.as_str()), Some("alice"));
assert_eq!(map.get(&DataTypeId(2)).and_then(|v| v.as_signed_number()), Some(42));
assert_eq!(map.get(&DataTypeId(3)).and_then(|v| v.as_bytes()), Some(vec![0x01, 0x02]));
assert_eq!(map.get(&DataTypeId(4)).and_then(|v| v.as_array()), Some(vec![DataValue::BoolTrue]));
assert_eq!(
map.get(&DataTypeId(1)).and_then(|v| v.as_str()),
Some("alice")
);
assert_eq!(
map.get(&DataTypeId(2)).and_then(|v| v.as_signed_number()),
Some(42)
);
assert_eq!(
map.get(&DataTypeId(3)).and_then(|v| v.as_bytes()),
Some(vec![0x01, 0x02])
);
assert_eq!(
map.get(&DataTypeId(4)).and_then(|v| v.as_array()),
Some(vec![DataValue::BoolTrue])
);
}
#[test]
@ -1191,9 +1202,7 @@ mod tests {
#[test]
fn test_truncated_container_rejected() {
let dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("hello".to_string())),
]);
let dv = DataValue::Container(vec![(DataTypeId(1), DataValue::Str("hello".to_string()))]);
let bytes = dv.to_bytes();
// Truncate to fewer than 2 bytes so neither container nor array can be read
assert!(DataValue::from_bytes(&bytes[..1]).is_none());
@ -1280,9 +1289,8 @@ mod tests {
let cipher_a = ChaCha20Poly1305::new([0xAB; 32]);
let cipher_b = ChaCha20Poly1305::new([0xCD; 32]);
let mut dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("secret".to_string())),
]);
let mut dv =
DataValue::Container(vec![(DataTypeId(1), DataValue::Str("secret".to_string()))]);
assert!(dv.encrypt_container(&cipher_a, b"aad").is_some());
assert!(dv.decrypt_into_container(&cipher_b, b"aad").is_none());
@ -1294,9 +1302,8 @@ mod tests {
use mtp_crypto::ChaCha20Poly1305;
let cipher = ChaCha20Poly1305::new([0xAB; 32]);
let mut dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("secret".to_string())),
]);
let mut dv =
DataValue::Container(vec![(DataTypeId(1), DataValue::Str("secret".to_string()))]);
assert!(dv.encrypt_container(&cipher, b"correct-aad").is_some());
assert!(dv.decrypt_into_container(&cipher, b"wrong-aad").is_none());
@ -1319,16 +1326,21 @@ mod tests {
let (signer, sk, _pk) = Ed25519Signer::generate();
let cipher = ChaCha20Poly1305::new([0xAB; 32]);
let mut dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("signed data".to_string())),
]);
let mut dv = DataValue::Container(vec![(
DataTypeId(1),
DataValue::Str("signed data".to_string()),
)]);
assert!(dv
.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"aad")
.is_some());
assert!(
dv.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"aad")
.is_some()
);
assert!(matches!(dv, DataValue::SignedEncryptedContainer(_)));
assert!(dv.decrypt_signed_encrypted_container(&cipher, b"aad").is_some());
assert!(
dv.decrypt_signed_encrypted_container(&cipher, b"aad")
.is_some()
);
assert!(matches!(dv, DataValue::SignedContainer(_)));
let verifier = Ed25519Signer::new(&sk).unwrap();
@ -1348,9 +1360,10 @@ mod tests {
let (_, sk2, _) = Ed25519Signer::generate();
let wrong_verifier = Ed25519Signer::new(&sk2).unwrap();
let mut dv = DataValue::Container(vec![
(DataTypeId(1), DataValue::Str("signed data".to_string())),
]);
let mut dv = DataValue::Container(vec![(
DataTypeId(1),
DataValue::Str("signed data".to_string()),
)]);
assert!(dv.sign_container(SigAlgorithm::ED25519, &signer).is_some());
assert!(dv.verify_into_container(&wrong_verifier).is_none());

View file

@ -6,13 +6,13 @@ import init, {
ed25519_generate,
keyring_from_ed25519,
build_demo_message,
} from 'mtp-wasm';
} from "mtp-wasm";
const STATUS = document.getElementById('status')!;
const STORAGE_KEY = 'mtp-web-client-keys';
const STATUS = document.getElementById("status")!;
const STORAGE_KEY = "mtp-web-client-keys";
function log(msg: string, cls = '') {
const line = document.createElement('div');
function log(msg: string, cls = "") {
const line = document.createElement("div");
line.textContent = msg;
if (cls) line.className = cls;
STATUS.appendChild(line);
@ -37,19 +37,23 @@ function loadKeys(): { clientId: bigint; keyringBytes: Uint8Array } | null {
}
async function initWasm() {
log('Loading WASM module...');
log("Loading WASM module...");
await init();
log(`WASM loaded. WebTransport supported: ${WasmClient.is_supported()}`);
}
function createClient(): WasmClient {
return new WasmClient(
(state: number) => log(`[state] ${ConnectionState[state] ?? state}`, 'state'),
(state: number) =>
log(`[state] ${ConnectionState[state] ?? state}`, "state"),
(data: Uint8Array) => {
const decoder = new TextDecoder();
log(`[message] ${data.length} bytes: ${decoder.decode(data)}`, 'received');
log(
`[message] ${data.length} bytes: ${decoder.decode(data)}`,
"received",
);
},
(err: any) => log(`[error] ${err}`, 'error'),
(err: any) => log(`[error] ${err}`, "error"),
);
}
@ -65,11 +69,11 @@ async function run() {
await initWasm();
if (!WasmClient.is_supported()) {
log('WebTransport is not supported in this browser.', 'error');
log("WebTransport is not supported in this browser.", "error");
return;
}
const serverUrl = 'https://127.0.0.1:8080';
const serverUrl = "https://127.0.0.1:8080";
const saved = loadKeys();
const client = createClient();
@ -81,30 +85,35 @@ async function run() {
if (saved) {
log(`Found saved client keys (ID: ${saved.clientId})`);
const hostPk = new Uint8Array(0);
clientId = await client.auth_connect(config, hostPk, saved.keyringBytes, saved.clientId);
clientId = await client.auth_connect(
config,
hostPk,
saved.keyringBytes,
saved.clientId,
);
log(`Authenticated as client ${clientId}`);
keyringBytes = saved.keyringBytes;
} else {
log('No saved keys — registering new client...');
log("No saved keys: registering new client...");
const hostPk = new Uint8Array(0);
keyringBytes = generateKeyringBytes();
clientId = await client.auth_register(config, hostPk, keyringBytes);
log(`Registered with ID: ${clientId}`);
saveKeys(clientId, keyringBytes);
log('Saved client keys to localStorage');
log("Saved client keys to localStorage");
}
config.free();
log('\nSending demo message...');
log("\nSending demo message...");
const frame = build_demo_message(clientId, keyringBytes);
await client.send(frame);
log(`Sent ${frame.length} bytes`);
log('\nClient running. Waiting for incoming messages...');
log("\nClient running. Waiting for incoming messages...");
}
run().catch((e) => {
log(`Fatal error: ${e}`, 'error');
log(`Fatal error: ${e}`, "error");
console.error(e);
});

View file

@ -7,4 +7,9 @@ export default defineConfig({
'mtp-wasm': path.resolve(__dirname, '../../wasm/pkg'),
},
},
server: {
fs: {
allow: ['.', path.resolve(__dirname, '../../wasm/pkg')],
},
},
});