[WIP]
This commit is contained in:
parent
3f65aa92bd
commit
8194fff3b6
4 changed files with 93 additions and 66 deletions
|
|
@ -45,7 +45,7 @@ config.server_certificate_hashes = [ // optional, for certificate pinn
|
||||||
|
|
||||||
### TLS Certificate Handling
|
### 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,
|
**built-in root certificate store** to verify the server's TLS certificate,
|
||||||
just like any other HTTPS/WebSocket connection. This works with publicly-trusted
|
just like any other HTTPS/WebSocket connection. This works with publicly-trusted
|
||||||
certificate authorities automatically.
|
certificate authorities automatically.
|
||||||
|
|
|
||||||
|
|
@ -309,11 +309,7 @@ impl DataValue {
|
||||||
* Returns `None` if the value is not a `Container` or signing fails.
|
* Returns `None` if the value is not a `Container` or signing fails.
|
||||||
*/
|
*/
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub fn sign_container(
|
pub fn sign_container(&mut self, algorithm: u8, signer: &impl SignatureScheme) -> Option<()> {
|
||||||
&mut self,
|
|
||||||
algorithm: u8,
|
|
||||||
signer: &impl SignatureScheme,
|
|
||||||
) -> Option<()> {
|
|
||||||
let entries = self.as_container()?;
|
let entries = self.as_container()?;
|
||||||
let container_bytes = Self::encode_container(&entries);
|
let container_bytes = Self::encode_container(&entries);
|
||||||
|
|
||||||
|
|
@ -334,10 +330,7 @@ impl DataValue {
|
||||||
* blob is malformed.
|
* blob is malformed.
|
||||||
*/
|
*/
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub fn verify_into_container(
|
pub fn verify_into_container(&mut self, verifier: &impl SignatureScheme) -> Option<()> {
|
||||||
&mut self,
|
|
||||||
verifier: &impl SignatureScheme,
|
|
||||||
) -> Option<()> {
|
|
||||||
let blob = self.as_signed_container()?;
|
let blob = self.as_signed_container()?;
|
||||||
if blob.len() < 1 + 64 + 2 {
|
if blob.len() < 1 + 64 + 2 {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -380,7 +373,7 @@ impl DataValue {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Decrypt a `SignedEncryptedContainer` in-place, replacing it with a
|
* 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")]
|
#[cfg(feature = "crypto")]
|
||||||
pub fn decrypt_signed_encrypted_container(
|
pub fn decrypt_signed_encrypted_container(
|
||||||
|
|
@ -1061,9 +1054,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_null_in_container() {
|
fn test_null_in_container() {
|
||||||
container_roundtrip(vec![
|
container_roundtrip(vec![(DataTypeId(1), DataValue::Null)]);
|
||||||
(DataTypeId(1), DataValue::Null),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1103,21 +1094,23 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_container_nested_roundtrip() {
|
fn test_container_nested_roundtrip() {
|
||||||
container_roundtrip(vec![
|
container_roundtrip(vec![
|
||||||
(DataTypeId(1), DataValue::Container(vec![
|
(
|
||||||
(DataTypeId(10), DataValue::BoolTrue),
|
DataTypeId(1),
|
||||||
])),
|
DataValue::Container(vec![(DataTypeId(10), DataValue::BoolTrue)]),
|
||||||
(DataTypeId(2), DataValue::Array(vec![
|
),
|
||||||
DataValue::SignedNumber(1),
|
(
|
||||||
DataValue::SignedNumber(2),
|
DataTypeId(2),
|
||||||
])),
|
DataValue::Array(vec![DataValue::SignedNumber(1), DataValue::SignedNumber(2)]),
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_container_base64_roundtrip() {
|
fn test_container_base64_roundtrip() {
|
||||||
let dv = DataValue::Container(vec![
|
let dv = DataValue::Container(vec![(
|
||||||
(DataTypeId(7), DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF])),
|
DataTypeId(7),
|
||||||
]);
|
DataValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
|
||||||
|
)]);
|
||||||
let b64 = dv.to_base64();
|
let b64 = dv.to_base64();
|
||||||
let decoded = DataValue::from_base64(&b64).expect("base64 roundtrip failed");
|
let decoded = DataValue::from_base64(&b64).expect("base64 roundtrip failed");
|
||||||
assert_eq!(dv, decoded);
|
assert_eq!(dv, decoded);
|
||||||
|
|
@ -1128,11 +1121,17 @@ mod tests {
|
||||||
assert_eq!(DataValue::BoolTrue.kind(), DataKind::Bool);
|
assert_eq!(DataValue::BoolTrue.kind(), DataKind::Bool);
|
||||||
assert_eq!(DataValue::Bool(false).kind(), DataKind::Bool);
|
assert_eq!(DataValue::Bool(false).kind(), DataKind::Bool);
|
||||||
assert_eq!(DataValue::SignedNumber(0).kind(), DataKind::SignedNumber);
|
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::Float(0, 0).kind(), DataKind::Float);
|
||||||
assert_eq!(DataValue::Str(String::new()).kind(), DataKind::Str);
|
assert_eq!(DataValue::Str(String::new()).kind(), DataKind::Str);
|
||||||
assert_eq!(DataValue::Bytes(vec![]).kind(), DataKind::Bytes);
|
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::Container(vec![]).kind(), DataKind::Container);
|
||||||
assert_eq!(DataValue::Null.kind(), DataKind::Null);
|
assert_eq!(DataValue::Null.kind(), DataKind::Null);
|
||||||
}
|
}
|
||||||
|
|
@ -1147,10 +1146,22 @@ mod tests {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let map = dv.as_map().expect("should be a container");
|
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!(
|
||||||
assert_eq!(map.get(&DataTypeId(2)).and_then(|v| v.as_signed_number()), Some(42));
|
map.get(&DataTypeId(1)).and_then(|v| v.as_str()),
|
||||||
assert_eq!(map.get(&DataTypeId(3)).and_then(|v| v.as_bytes()), Some(vec![0x01, 0x02]));
|
Some("alice")
|
||||||
assert_eq!(map.get(&DataTypeId(4)).and_then(|v| v.as_array()), Some(vec![DataValue::BoolTrue]));
|
);
|
||||||
|
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]
|
#[test]
|
||||||
|
|
@ -1191,9 +1202,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_truncated_container_rejected() {
|
fn test_truncated_container_rejected() {
|
||||||
let dv = DataValue::Container(vec![
|
let dv = DataValue::Container(vec![(DataTypeId(1), DataValue::Str("hello".to_string()))]);
|
||||||
(DataTypeId(1), DataValue::Str("hello".to_string())),
|
|
||||||
]);
|
|
||||||
let bytes = dv.to_bytes();
|
let bytes = dv.to_bytes();
|
||||||
// Truncate to fewer than 2 bytes so neither container nor array can be read
|
// Truncate to fewer than 2 bytes so neither container nor array can be read
|
||||||
assert!(DataValue::from_bytes(&bytes[..1]).is_none());
|
assert!(DataValue::from_bytes(&bytes[..1]).is_none());
|
||||||
|
|
@ -1280,9 +1289,8 @@ mod tests {
|
||||||
let cipher_a = ChaCha20Poly1305::new([0xAB; 32]);
|
let cipher_a = ChaCha20Poly1305::new([0xAB; 32]);
|
||||||
let cipher_b = ChaCha20Poly1305::new([0xCD; 32]);
|
let cipher_b = ChaCha20Poly1305::new([0xCD; 32]);
|
||||||
|
|
||||||
let mut dv = DataValue::Container(vec![
|
let mut dv =
|
||||||
(DataTypeId(1), DataValue::Str("secret".to_string())),
|
DataValue::Container(vec![(DataTypeId(1), DataValue::Str("secret".to_string()))]);
|
||||||
]);
|
|
||||||
|
|
||||||
assert!(dv.encrypt_container(&cipher_a, b"aad").is_some());
|
assert!(dv.encrypt_container(&cipher_a, b"aad").is_some());
|
||||||
assert!(dv.decrypt_into_container(&cipher_b, b"aad").is_none());
|
assert!(dv.decrypt_into_container(&cipher_b, b"aad").is_none());
|
||||||
|
|
@ -1294,9 +1302,8 @@ mod tests {
|
||||||
use mtp_crypto::ChaCha20Poly1305;
|
use mtp_crypto::ChaCha20Poly1305;
|
||||||
let cipher = ChaCha20Poly1305::new([0xAB; 32]);
|
let cipher = ChaCha20Poly1305::new([0xAB; 32]);
|
||||||
|
|
||||||
let mut dv = DataValue::Container(vec![
|
let mut dv =
|
||||||
(DataTypeId(1), DataValue::Str("secret".to_string())),
|
DataValue::Container(vec![(DataTypeId(1), DataValue::Str("secret".to_string()))]);
|
||||||
]);
|
|
||||||
|
|
||||||
assert!(dv.encrypt_container(&cipher, b"correct-aad").is_some());
|
assert!(dv.encrypt_container(&cipher, b"correct-aad").is_some());
|
||||||
assert!(dv.decrypt_into_container(&cipher, b"wrong-aad").is_none());
|
assert!(dv.decrypt_into_container(&cipher, b"wrong-aad").is_none());
|
||||||
|
|
@ -1319,16 +1326,21 @@ mod tests {
|
||||||
let (signer, sk, _pk) = Ed25519Signer::generate();
|
let (signer, sk, _pk) = Ed25519Signer::generate();
|
||||||
let cipher = ChaCha20Poly1305::new([0xAB; 32]);
|
let cipher = ChaCha20Poly1305::new([0xAB; 32]);
|
||||||
|
|
||||||
let mut dv = DataValue::Container(vec![
|
let mut dv = DataValue::Container(vec![(
|
||||||
(DataTypeId(1), DataValue::Str("signed data".to_string())),
|
DataTypeId(1),
|
||||||
]);
|
DataValue::Str("signed data".to_string()),
|
||||||
|
)]);
|
||||||
|
|
||||||
assert!(dv
|
assert!(
|
||||||
.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"aad")
|
dv.sign_and_encrypt_container(SigAlgorithm::ED25519, &signer, &cipher, b"aad")
|
||||||
.is_some());
|
.is_some()
|
||||||
|
);
|
||||||
assert!(matches!(dv, DataValue::SignedEncryptedContainer(_)));
|
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(_)));
|
assert!(matches!(dv, DataValue::SignedContainer(_)));
|
||||||
|
|
||||||
let verifier = Ed25519Signer::new(&sk).unwrap();
|
let verifier = Ed25519Signer::new(&sk).unwrap();
|
||||||
|
|
@ -1348,9 +1360,10 @@ mod tests {
|
||||||
let (_, sk2, _) = Ed25519Signer::generate();
|
let (_, sk2, _) = Ed25519Signer::generate();
|
||||||
let wrong_verifier = Ed25519Signer::new(&sk2).unwrap();
|
let wrong_verifier = Ed25519Signer::new(&sk2).unwrap();
|
||||||
|
|
||||||
let mut dv = DataValue::Container(vec![
|
let mut dv = DataValue::Container(vec![(
|
||||||
(DataTypeId(1), DataValue::Str("signed data".to_string())),
|
DataTypeId(1),
|
||||||
]);
|
DataValue::Str("signed data".to_string()),
|
||||||
|
)]);
|
||||||
|
|
||||||
assert!(dv.sign_container(SigAlgorithm::ED25519, &signer).is_some());
|
assert!(dv.sign_container(SigAlgorithm::ED25519, &signer).is_some());
|
||||||
assert!(dv.verify_into_container(&wrong_verifier).is_none());
|
assert!(dv.verify_into_container(&wrong_verifier).is_none());
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import init, {
|
||||||
ed25519_generate,
|
ed25519_generate,
|
||||||
keyring_from_ed25519,
|
keyring_from_ed25519,
|
||||||
build_demo_message,
|
build_demo_message,
|
||||||
} from 'mtp-wasm';
|
} from "mtp-wasm";
|
||||||
|
|
||||||
const STATUS = document.getElementById('status')!;
|
const STATUS = document.getElementById("status")!;
|
||||||
const STORAGE_KEY = 'mtp-web-client-keys';
|
const STORAGE_KEY = "mtp-web-client-keys";
|
||||||
|
|
||||||
function log(msg: string, cls = '') {
|
function log(msg: string, cls = "") {
|
||||||
const line = document.createElement('div');
|
const line = document.createElement("div");
|
||||||
line.textContent = msg;
|
line.textContent = msg;
|
||||||
if (cls) line.className = cls;
|
if (cls) line.className = cls;
|
||||||
STATUS.appendChild(line);
|
STATUS.appendChild(line);
|
||||||
|
|
@ -37,19 +37,23 @@ function loadKeys(): { clientId: bigint; keyringBytes: Uint8Array } | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initWasm() {
|
async function initWasm() {
|
||||||
log('Loading WASM module...');
|
log("Loading WASM module...");
|
||||||
await init();
|
await init();
|
||||||
log(`WASM loaded. WebTransport supported: ${WasmClient.is_supported()}`);
|
log(`WASM loaded. WebTransport supported: ${WasmClient.is_supported()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createClient(): WasmClient {
|
function createClient(): WasmClient {
|
||||||
return new WasmClient(
|
return new WasmClient(
|
||||||
(state: number) => log(`[state] ${ConnectionState[state] ?? state}`, 'state'),
|
(state: number) =>
|
||||||
|
log(`[state] ${ConnectionState[state] ?? state}`, "state"),
|
||||||
(data: Uint8Array) => {
|
(data: Uint8Array) => {
|
||||||
const decoder = new TextDecoder();
|
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();
|
await initWasm();
|
||||||
|
|
||||||
if (!WasmClient.is_supported()) {
|
if (!WasmClient.is_supported()) {
|
||||||
log('WebTransport is not supported in this browser.', 'error');
|
log("WebTransport is not supported in this browser.", "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverUrl = 'https://127.0.0.1:8080';
|
const serverUrl = "https://127.0.0.1:8080";
|
||||||
const saved = loadKeys();
|
const saved = loadKeys();
|
||||||
|
|
||||||
const client = createClient();
|
const client = createClient();
|
||||||
|
|
@ -81,30 +85,35 @@ async function run() {
|
||||||
if (saved) {
|
if (saved) {
|
||||||
log(`Found saved client keys (ID: ${saved.clientId})`);
|
log(`Found saved client keys (ID: ${saved.clientId})`);
|
||||||
const hostPk = new Uint8Array(0);
|
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}`);
|
log(`Authenticated as client ${clientId}`);
|
||||||
keyringBytes = saved.keyringBytes;
|
keyringBytes = saved.keyringBytes;
|
||||||
} else {
|
} else {
|
||||||
log('No saved keys — registering new client...');
|
log("No saved keys: registering new client...");
|
||||||
const hostPk = new Uint8Array(0);
|
const hostPk = new Uint8Array(0);
|
||||||
keyringBytes = generateKeyringBytes();
|
keyringBytes = generateKeyringBytes();
|
||||||
clientId = await client.auth_register(config, hostPk, keyringBytes);
|
clientId = await client.auth_register(config, hostPk, keyringBytes);
|
||||||
log(`Registered with ID: ${clientId}`);
|
log(`Registered with ID: ${clientId}`);
|
||||||
saveKeys(clientId, keyringBytes);
|
saveKeys(clientId, keyringBytes);
|
||||||
log('Saved client keys to localStorage');
|
log("Saved client keys to localStorage");
|
||||||
}
|
}
|
||||||
|
|
||||||
config.free();
|
config.free();
|
||||||
|
|
||||||
log('\nSending demo message...');
|
log("\nSending demo message...");
|
||||||
const frame = build_demo_message(clientId, keyringBytes);
|
const frame = build_demo_message(clientId, keyringBytes);
|
||||||
await client.send(frame);
|
await client.send(frame);
|
||||||
log(`Sent ${frame.length} bytes`);
|
log(`Sent ${frame.length} bytes`);
|
||||||
|
|
||||||
log('\nClient running. Waiting for incoming messages...');
|
log("\nClient running. Waiting for incoming messages...");
|
||||||
}
|
}
|
||||||
|
|
||||||
run().catch((e) => {
|
run().catch((e) => {
|
||||||
log(`Fatal error: ${e}`, 'error');
|
log(`Fatal error: ${e}`, "error");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,9 @@ export default defineConfig({
|
||||||
'mtp-wasm': path.resolve(__dirname, '../../wasm/pkg'),
|
'mtp-wasm': path.resolve(__dirname, '../../wasm/pkg'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
allow: ['.', path.resolve(__dirname, '../../wasm/pkg')],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue