feat: implement RNS transport (Smol Mail 1.2) over microReticulum
This commit is contained in:
parent
af1d31be83
commit
7203556186
19 changed files with 1939 additions and 1141 deletions
195
src/session.rs
195
src/session.rs
|
|
@ -1,23 +1,24 @@
|
|||
//! Per-connection dispatch and the six wire operations.
|
||||
|
||||
use crate::bind::TransportBindValues;
|
||||
use crate::crypto::{b32, ct_eq, hmac_sha256, message_id, verify};
|
||||
use crate::proto::{
|
||||
valid_username, ProtocolError, Reader, AUTH_FAILED, AUTH_REQUIRED, BAD_VERSION, CERT_LEN,
|
||||
ENVELOPE_MAGIC, ENVELOPE_MIN, ENVELOPE_VERSION, FETCH_BUDGET, ID_LEN, KEY_LEN, LABEL_AUTH,
|
||||
LABEL_MAC, LABEL_REGISTER, LABEL_ROTATE, MAC_LEN, MALFORMED, MAX_CHAIN, NOT_PERMITTED, OK,
|
||||
OP_AUTH, OP_DELETE, OP_FETCH, OP_REGISTER, OP_RESOLVE, OP_SEND, QUOTA_EXCEEDED, RATE_LIMITED,
|
||||
TOKEN_LEN, TOO_LARGE, UNKNOWN_USER,
|
||||
ENVELOPE_MAGIC, ENVELOPE_MIN, ENVELOPE_VERSION, ID_LEN, KEY_LEN, LABEL_AUTH, LABEL_MAC,
|
||||
LABEL_REGISTER, LABEL_ROTATE, MAC_LEN, MALFORMED, MAX_CHAIN, NOT_PERMITTED, OK, OP_AUTH,
|
||||
OP_DELETE, OP_FETCH, OP_REGISTER, OP_RESOLVE, OP_SEND, QUOTA_EXCEEDED, RATE_LIMITED, TOKEN_LEN,
|
||||
TOO_LARGE, UNKNOWN_USER,
|
||||
};
|
||||
use crate::ratelimit::RateLimiter;
|
||||
use crate::store::Store;
|
||||
|
||||
pub struct ServerConfig {
|
||||
pub max_envelope: usize,
|
||||
pub fetch_budget: usize,
|
||||
pub main_quota: i64,
|
||||
pub requests_quota: i64,
|
||||
pub max_tokens: u16,
|
||||
pub invite_token: Option<Vec<u8>>,
|
||||
pub server_static: [u8; KEY_LEN],
|
||||
pub conn_limiter: RateLimiter,
|
||||
pub send_limiter: RateLimiter,
|
||||
pub token_limiter: RateLimiter,
|
||||
|
|
@ -47,17 +48,22 @@ pub struct Session<'a> {
|
|||
config: &'a ServerConfig,
|
||||
store: Store,
|
||||
peer_ip: String,
|
||||
handshake_hash: Vec<u8>,
|
||||
bind: TransportBindValues,
|
||||
username: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> Session<'a> {
|
||||
pub fn new(config: &'a ServerConfig, store: Store, peer_ip: String, handshake_hash: Vec<u8>) -> Self {
|
||||
pub fn new(
|
||||
config: &'a ServerConfig,
|
||||
store: Store,
|
||||
peer_ip: String,
|
||||
bind: TransportBindValues,
|
||||
) -> Self {
|
||||
Session {
|
||||
config,
|
||||
store,
|
||||
peer_ip,
|
||||
handshake_hash,
|
||||
bind,
|
||||
username: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +131,7 @@ impl<'a> Session<'a> {
|
|||
return Ok((AUTH_FAILED, Vec::new()));
|
||||
}
|
||||
let mut msg = LABEL_AUTH.to_vec();
|
||||
msg.extend_from_slice(&self.handshake_hash);
|
||||
msg.extend_from_slice(&self.bind.h);
|
||||
if !verify(&identity, &signature, &msg) {
|
||||
return Ok((AUTH_FAILED, Vec::new()));
|
||||
}
|
||||
|
|
@ -231,9 +237,12 @@ impl<'a> Session<'a> {
|
|||
r.done()?;
|
||||
let username = self.username.as_ref().expect("AUTH_REQUIRED gate above");
|
||||
let keys = self.store.keys_of(username)?;
|
||||
let records = self
|
||||
.store
|
||||
.pending(&keys, after_received_at, &after_id, FETCH_BUDGET)?;
|
||||
let records = self.store.pending(
|
||||
&keys,
|
||||
after_received_at,
|
||||
&after_id,
|
||||
self.config.fetch_budget,
|
||||
)?;
|
||||
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(&(records.len() as u16).to_be_bytes());
|
||||
|
|
@ -286,7 +295,7 @@ impl<'a> Session<'a> {
|
|||
// (SPEC.md sec 6.1). Binding server_static stops the attestation from
|
||||
// being replayed against another server.
|
||||
let mut pop_msg = LABEL_REGISTER.to_vec();
|
||||
pop_msg.extend_from_slice(&self.config.server_static);
|
||||
pop_msg.extend_from_slice(&self.bind.server_static);
|
||||
pop_msg.extend_from_slice(username.as_bytes());
|
||||
pop_msg.extend_from_slice(&identity);
|
||||
if !verify(&identity, &signature, &pop_msg) {
|
||||
|
|
@ -343,3 +352,163 @@ impl<'a> Session<'a> {
|
|||
Ok((OK, Vec::new()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::sha256;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
fn test_config() -> ServerConfig {
|
||||
ServerConfig {
|
||||
max_envelope: 1024,
|
||||
fetch_budget: 512,
|
||||
main_quota: 1 << 20,
|
||||
requests_quota: 1 << 20,
|
||||
max_tokens: 16,
|
||||
invite_token: None,
|
||||
conn_limiter: RateLimiter::new(0),
|
||||
send_limiter: RateLimiter::new(0),
|
||||
token_limiter: RateLimiter::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_store(name: &str) -> Store {
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("bunshin-session-{name}-{}.db", std::process::id()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
Store::open(path.to_str().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn str_field(s: &str) -> Vec<u8> {
|
||||
let mut out = vec![s.len() as u8];
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn register_body(username: &str, identity: &[u8], signature: &[u8]) -> Vec<u8> {
|
||||
let mut body = str_field(username);
|
||||
body.extend_from_slice(identity);
|
||||
body.extend_from_slice(signature);
|
||||
body.push(0); // invite token, none configured
|
||||
body.push(0); // cert, plain registration
|
||||
body
|
||||
}
|
||||
|
||||
fn auth_body(username: &str, identity: &[u8], signature: &[u8]) -> Vec<u8> {
|
||||
let mut body = str_field(username);
|
||||
body.extend_from_slice(identity);
|
||||
body.extend_from_slice(signature);
|
||||
body.push(0); // sync = 0, stored tokens untouched
|
||||
body.extend_from_slice(&0u16.to_be_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rns_bind_signatures_accepted_and_tcp_bind_signatures_rejected() {
|
||||
let config = test_config();
|
||||
let key = SigningKey::from_bytes(&[3u8; 32]);
|
||||
let identity = key.verifying_key().as_bytes().to_vec();
|
||||
|
||||
let bind = TransportBindValues::rns(&[0x11; 16], &[0x22; 16]);
|
||||
|
||||
let mut pop = LABEL_REGISTER.to_vec();
|
||||
pop.extend_from_slice(&bind.server_static);
|
||||
pop.extend_from_slice(b"alice");
|
||||
pop.extend_from_slice(&identity);
|
||||
let good_register = register_body("alice", &identity, &key.sign(&pop).to_bytes());
|
||||
|
||||
// A TCP client would sign over the real X25519 static key instead.
|
||||
let tcp_static = crate::crypto::derive_public(&[9u8; 32]);
|
||||
let mut tcp_pop = LABEL_REGISTER.to_vec();
|
||||
tcp_pop.extend_from_slice(&tcp_static);
|
||||
tcp_pop.extend_from_slice(b"alice");
|
||||
tcp_pop.extend_from_slice(&identity);
|
||||
let tcp_register = register_body("alice", &identity, &key.sign(&tcp_pop).to_bytes());
|
||||
|
||||
let mut auth_msg = LABEL_AUTH.to_vec();
|
||||
auth_msg.extend_from_slice(&bind.h);
|
||||
let good_auth = auth_body("alice", &identity, &key.sign(&auth_msg).to_bytes());
|
||||
|
||||
// A Noise client would sign over the handshake hash instead.
|
||||
let handshake_hash = sha256(&[b"handshake"]);
|
||||
let mut tcp_auth = LABEL_AUTH.to_vec();
|
||||
tcp_auth.extend_from_slice(&handshake_hash);
|
||||
let tcp_auth = auth_body("alice", &identity, &key.sign(&tcp_auth).to_bytes());
|
||||
|
||||
let mut session = Session::new(&config, temp_store("rns-bind"), "rns".into(), bind);
|
||||
assert_eq!(session.dispatch(OP_REGISTER, &good_register).0, OK);
|
||||
assert_eq!(session.dispatch(OP_AUTH, &good_auth).0, OK);
|
||||
|
||||
let mut session = Session::new(
|
||||
&config,
|
||||
temp_store("rns-bind-reject"),
|
||||
"rns".into(),
|
||||
TransportBindValues::rns(&[0x11; 16], &[0x22; 16]),
|
||||
);
|
||||
assert_eq!(session.dispatch(OP_REGISTER, &good_register).0, OK);
|
||||
assert_eq!(session.dispatch(OP_AUTH, &tcp_auth).0, AUTH_FAILED);
|
||||
|
||||
let mut session = Session::new(
|
||||
&config,
|
||||
temp_store("rns-bind-tcp-register"),
|
||||
"rns".into(),
|
||||
TransportBindValues::rns(&[0x11; 16], &[0x22; 16]),
|
||||
);
|
||||
assert_eq!(session.dispatch(OP_REGISTER, &tcp_register).0, AUTH_FAILED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tcp_bind_signatures_accepted_and_rns_bind_signatures_rejected() {
|
||||
let config = test_config();
|
||||
let key = SigningKey::from_bytes(&[4u8; 32]);
|
||||
let identity = key.verifying_key().as_bytes().to_vec();
|
||||
|
||||
let server_static = crate::crypto::derive_public(&[9u8; 32]);
|
||||
let handshake_hash = sha256(&[b"handshake"]);
|
||||
let bind = TransportBindValues::tcp(&handshake_hash, &server_static).unwrap();
|
||||
|
||||
let mut pop = LABEL_REGISTER.to_vec();
|
||||
pop.extend_from_slice(&server_static);
|
||||
pop.extend_from_slice(b"alice");
|
||||
pop.extend_from_slice(&identity);
|
||||
let good_register = register_body("alice", &identity, &key.sign(&pop).to_bytes());
|
||||
|
||||
let mut auth_msg = LABEL_AUTH.to_vec();
|
||||
auth_msg.extend_from_slice(&handshake_hash);
|
||||
let good_auth = auth_body("alice", &identity, &key.sign(&auth_msg).to_bytes());
|
||||
|
||||
// An RNS client would sign over the derived bind values instead.
|
||||
let rns_bind = TransportBindValues::rns(&[0x11; 16], &[0x22; 16]);
|
||||
let mut rns_pop = LABEL_REGISTER.to_vec();
|
||||
rns_pop.extend_from_slice(&rns_bind.server_static);
|
||||
rns_pop.extend_from_slice(b"alice");
|
||||
rns_pop.extend_from_slice(&identity);
|
||||
let rns_register = register_body("alice", &identity, &key.sign(&rns_pop).to_bytes());
|
||||
|
||||
let mut rns_auth = LABEL_AUTH.to_vec();
|
||||
rns_auth.extend_from_slice(&rns_bind.h);
|
||||
let rns_auth = auth_body("alice", &identity, &key.sign(&rns_auth).to_bytes());
|
||||
|
||||
let mut session = Session::new(&config, temp_store("tcp-bind"), "tcp".into(), bind);
|
||||
assert_eq!(session.dispatch(OP_REGISTER, &good_register).0, OK);
|
||||
assert_eq!(session.dispatch(OP_AUTH, &good_auth).0, OK);
|
||||
|
||||
let mut session = Session::new(
|
||||
&config,
|
||||
temp_store("tcp-bind-reject"),
|
||||
"tcp".into(),
|
||||
TransportBindValues::tcp(&handshake_hash, &server_static).unwrap(),
|
||||
);
|
||||
assert_eq!(session.dispatch(OP_REGISTER, &good_register).0, OK);
|
||||
assert_eq!(session.dispatch(OP_AUTH, &rns_auth).0, AUTH_FAILED);
|
||||
|
||||
let mut session = Session::new(
|
||||
&config,
|
||||
temp_store("tcp-bind-rns-register"),
|
||||
"tcp".into(),
|
||||
TransportBindValues::tcp(&handshake_hash, &server_static).unwrap(),
|
||||
);
|
||||
assert_eq!(session.dispatch(OP_REGISTER, &rns_register).0, AUTH_FAILED);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue