mod check_payload;
mod neighbour;
mod publication;
mod reply;
mod request;
mod session;
mod stream;
pub use check_payload::{
check_frame, check_payload, FRAME_RESERVED_ELEMENTS, MAX_PAYLOAD_ELEMENTS, MAX_PAYLOAD_NESTING,
};
pub use neighbour::{
advertise_frame, goodbye_frame, neighbour_signed, sign_neighbour, subscribe_frame,
unadvertise_frame, unsubscribe_frame, verify_neighbour, NeighbourLink, NeighbourPeer,
};
pub use publication::{sign_publish, verify_publication, PublicationSpec, VerifiedPublication};
pub use reply::{
claimed_reply_ids, sign_provider_error, sign_relay_error, sign_result, verify_relay_error,
verify_reply, RelayErrorSpec, RelayErrorType, ReplyType, VerifiedRelayError, VerifiedReply,
};
pub use request::{
request_fields_accepted, sign_call, sign_stream_open, verify_request, RequestSpec, RequestType,
VerifiedRequest, MAX_PROOFS, MAX_PROOFS_BYTES,
};
pub use session::{
liveness_nonce, liveness_ping_frame, liveness_pong_frame, verify_session_frame, Liveness,
LIVENESS_NONCE_SIZE,
};
pub use stream::{
open_stream, sign_caller_stream, sign_provider_stream, verify_caller_stream,
verify_provider_stream, StreamEncoding, StreamFields, StreamMode, StreamRole, StreamState,
VerifiedStreamFrame,
};
use std::fmt;
use crate::cbor::{self, Value};
use crate::node_key::{KeyError, NodeKey, Purpose};
use crate::signed_object::ObjectError;
pub const PROTOCOL_VERSION: i64 = 2;
pub const MAX_FRAME_BYTES: usize = 0x00FF_FFFF;
const REQUEST_LABEL: &str = "MACULA-PQ-REQUEST-V1";
const REPLY_LABEL: &str = "MACULA-PQ-REPLY-V1";
const RELAY_ERROR_LABEL: &str = "MACULA-PQ-RELAY-ERROR-V1";
const STREAM_LABEL: &str = "MACULA-PQ-STREAM-V1";
const CALLER_STREAM_LABEL: &str = "MACULA-PQ-CALLER-STREAM-V1";
const PUBLICATION_LABEL: &str = "MACULA-PQ-PUBLICATION-V1";
const MAX_PROTOCOL_INT: u64 = 1 << 53;
const MAX_PROCEDURE_BYTES: usize = 512;
const MAX_ERROR_CODE_BYTES: usize = 64;
const MAX_ERROR_TEXT_BYTES: usize = 256;
const MAX_TOPIC_BYTES: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameError {
TooLarge(usize),
Malformed,
Payload(String),
BreaksDecodingRule(String),
ProofsOutOfBound,
KeyIdMismatch,
RequestMismatch,
NotTheTarget,
NotTheConnection,
Unsignable,
TextTooLong(String),
InvalidText(String),
RelayCodeOutsideItsSet,
OutOfRange(String),
NotAllowed(String),
SeqMismatch,
StreamEnded,
NeighbourSigned,
NotYetValid(i64),
Expired(i64),
SignatureInvalid,
Key(KeyError),
}
impl fmt::Display for FrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FrameError::TooLarge(n) => write!(
f,
"a frame of {n} bytes, over the {MAX_FRAME_BYTES}-byte cap"
),
FrameError::Malformed => f.write_str("malformed frame"),
FrameError::Payload(why) | FrameError::BreaksDecodingRule(why) => f.write_str(why),
FrameError::ProofsOutOfBound => {
f.write_str("the request's proofs are outside the bound")
}
FrameError::KeyIdMismatch => {
f.write_str("the signer the frame names is not the key it verified with")
}
FrameError::RequestMismatch => f.write_str("the frame names another request"),
FrameError::NotTheTarget => f.write_str("the frame is not from the request's target"),
FrameError::NotTheConnection => {
f.write_str("the relay error is not from the connection's station")
}
FrameError::Unsignable => f.write_str("the key cannot sign this frame"),
FrameError::TextTooLong(what) => write!(f, "text longer than its bound: {what}"),
FrameError::InvalidText(what) => write!(f, "text that is not valid UTF-8: {what}"),
FrameError::RelayCodeOutsideItsSet => {
f.write_str("a relay error code outside its closed set")
}
FrameError::OutOfRange(what) => write!(f, "a field outside its range: {what}"),
FrameError::NotAllowed(what) => {
write!(f, "a stream frame its side does not send: {what}")
}
FrameError::SeqMismatch => f.write_str("a stream frame out of its side's order"),
FrameError::StreamEnded => f.write_str("a stream frame after its side's STREAM_END"),
FrameError::NeighbourSigned => {
f.write_str("the frame already carries a neighbour signature")
}
FrameError::NotYetValid(ms) => write!(f, "a publication not yet valid, by {ms} ms"),
FrameError::Expired(ms) => write!(f, "a publication past its expiry, by {ms} ms"),
FrameError::SignatureInvalid => {
f.write_str("the signed object's signature does not verify")
}
FrameError::Key(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for FrameError {}
fn object_refusal(e: ObjectError) -> FrameError {
match e {
ObjectError::SignatureInvalid => FrameError::SignatureInvalid,
ObjectError::Key(k) => FrameError::Key(k),
_ => FrameError::Malformed,
}
}
pub fn encode(frame: &Value) -> Result<Vec<u8>, FrameError> {
let payload = cbor::encode(frame).map_err(|e| FrameError::Payload(e.to_string()))?;
if payload.len() > MAX_FRAME_BYTES {
return Err(FrameError::TooLarge(payload.len()));
}
let mut out = Vec::with_capacity(4 + payload.len());
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
out.extend_from_slice(&payload);
Ok(out)
}
#[derive(Debug, Clone, PartialEq)]
pub enum Decoded {
Complete { frame: Value, consumed: usize },
NeedMore(usize),
}
pub fn decode(buf: &[u8]) -> Result<Decoded, FrameError> {
let Some((header, rest)) = buf.split_first_chunk::<4>() else {
return Ok(Decoded::NeedMore(4 - buf.len()));
};
let length = u32::from_be_bytes(*header) as usize;
if length > MAX_FRAME_BYTES {
return Err(FrameError::TooLarge(length));
}
if rest.len() < length {
return Ok(Decoded::NeedMore(length - rest.len()));
}
let frame = cbor::decode(&rest[..length]).map_err(|_| FrameError::Malformed)?;
Ok(Decoded::Complete {
frame,
consumed: 4 + length,
})
}
#[derive(Debug, Clone, Copy)]
enum Rule {
Any,
AnyBytes,
BytesOf(usize),
TextWithin(usize),
TextIn(&'static [&'static str]),
ProtocolUint,
ProtocolVersion,
CarriedObject,
HeldObject,
StreamObject,
Proofs,
}
impl Rule {
fn accepts(self, v: &Value) -> bool {
match self {
Rule::Any => true,
Rule::AnyBytes => matches!(v, Value::Bytes(_)),
Rule::BytesOf(n) => matches!(v, Value::Bytes(b) if b.len() == n),
Rule::TextWithin(n) => matches!(v, Value::Text(t) if t.len() <= n),
Rule::TextIn(names) => matches!(v, Value::Text(t) if names.contains(&t.as_str())),
Rule::ProtocolUint => protocol_uint(v).is_some(),
Rule::ProtocolVersion => {
matches!(v, Value::Int(n) if *n == i128::from(PROTOCOL_VERSION))
}
Rule::CarriedObject => crate::signed_object::Object::from_value(v).is_ok(),
Rule::HeldObject => crate::signed_object::HeldObject::from_value(v).is_ok(),
Rule::StreamObject => Rule::CarriedObject.accepts(v) || Rule::HeldObject.accepts(v),
Rule::Proofs => request::proofs_within_bound(v),
}
}
}
type Fields = std::collections::HashMap<String, Value>;
fn read_fields(v: &Value, table: &[(&str, Rule)]) -> Option<Fields> {
let Value::Map(pairs) = v else {
return None;
};
let mut fields = Fields::with_capacity(pairs.len());
for (key, value) in pairs {
let Value::Text(name) = key else {
return None;
};
let rule = table.iter().find(|(n, _)| *n == name)?.1;
if fields.contains_key(name) || !rule.accepts(value) {
return None;
}
fields.insert(name.clone(), value.clone());
}
Some(fields)
}
fn has_fields(fields: &Fields, names: &[&str]) -> bool {
names.iter().all(|n| fields.contains_key(*n))
}
fn received_frame(
v: &Value,
object_name: &str,
object_rule: Rule,
routes: &[(&str, Rule)],
types: &'static [&'static str],
) -> Option<(String, Value)> {
let mut table = vec![
("version", Rule::ProtocolVersion),
("frame_type", Rule::TextIn(types)),
(object_name, object_rule),
];
table.extend_from_slice(routes);
let fields = read_fields(v, &table)?;
if !has_fields(&fields, &["version", "frame_type", object_name]) {
return None;
}
Some((text_of(&fields["frame_type"]), fields[object_name].clone()))
}
fn bounded_text(field: &str, text: &[u8], max: usize) -> Result<(), FrameError> {
if text.len() > max {
return Err(FrameError::TextTooLong(format!(
"a {field} of {} bytes, over {max}",
text.len()
)));
}
if std::str::from_utf8(text).is_err() {
return Err(FrameError::InvalidText(format!("the {field}")));
}
Ok(())
}
fn identity_signer(key: &NodeKey) -> Result<(), FrameError> {
if key.purpose() != Purpose::Identity {
return Err(FrameError::Unsignable);
}
Ok(())
}
fn protocol_uint(v: &Value) -> Option<u64> {
match v {
Value::Int(n) if *n >= 0 && *n < i128::from(MAX_PROTOCOL_INT) => Some(*n as u64),
_ => None,
}
}
fn text_of(v: &Value) -> String {
match v {
Value::Text(t) => t.clone(),
_ => String::new(),
}
}
fn bytes_of(v: &Value) -> Vec<u8> {
match v {
Value::Bytes(b) => b.clone(),
_ => Vec::new(),
}
}
fn fixed<const N: usize>(v: &Value) -> [u8; N] {
let mut out = [0u8; N];
if let Value::Bytes(b) = v {
if b.len() == N {
out.copy_from_slice(b);
}
}
out
}
fn entry(name: &str, value: Value) -> (Value, Value) {
(Value::text(name), value)
}
fn uint(n: u64) -> Value {
Value::Int(i128::from(n))
}
fn names_request(fields: &Fields, request: &VerifiedRequest) -> bool {
fields.get("request_id") == Some(&Value::Bytes(request.request_id.to_vec()))
&& fields.get("request_hash") == Some(&Value::Bytes(request.request_hash.to_vec()))
}
fn base(frame_type: &str) -> Vec<(Value, Value)> {
vec![
entry("version", Value::Int(i128::from(PROTOCOL_VERSION))),
entry("frame_type", Value::text(frame_type)),
entry("frame_id", Value::Bytes(crate::uuid_v7::new().to_vec())),
entry("sent_at_ms", uint(crate::uuid_v7::now_ms())),
entry("capabilities", uint(0)),
entry("realm", Value::Null),
entry("call_id", Value::Null),
entry("source_route", Value::Null),
]
}
fn with_field(mut fields: Vec<(Value, Value)>, key: &str, value: Value) -> Vec<(Value, Value)> {
match fields.iter_mut().find(|(k, _)| *k == Value::text(key)) {
Some(slot) => slot.1 = value,
None => fields.push(entry(key, value)),
}
fields
}