use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::canonical::{canonical_json, sha256_json};
use crate::error::{Error, Result};
use crate::keys::{PublicKey, Signer};
pub const SCHEMA_VERSION: u32 = 1;
pub const MAX_MESSAGE_BYTES: usize = 64 * 1024;
pub const APPROVE_TTL_SECS: i64 = 600;
pub const GENESIS_PREV: &str =
"sha256:0000000000000000000000000000000000000000000000000000000000000000";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
Chat,
Task,
#[serde(alias = "ask")]
Question,
Reply,
Done,
Claim,
Release,
Approve,
Deny,
Control,
System,
}
impl MessageType {
pub const ALL: [MessageType; 11] = [
MessageType::Chat,
MessageType::Task,
MessageType::Question,
MessageType::Reply,
MessageType::Done,
MessageType::Claim,
MessageType::Release,
MessageType::Approve,
MessageType::Deny,
MessageType::Control,
MessageType::System,
];
pub fn as_str(&self) -> &'static str {
match self {
MessageType::Chat => "chat",
MessageType::Task => "task",
MessageType::Question => "question",
MessageType::Reply => "reply",
MessageType::Done => "done",
MessageType::Claim => "claim",
MessageType::Release => "release",
MessageType::Approve => "approve",
MessageType::Deny => "deny",
MessageType::Control => "control",
MessageType::System => "system",
}
}
}
impl fmt::Display for MessageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for MessageType {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"chat" => Ok(MessageType::Chat),
"task" => Ok(MessageType::Task),
"question" | "ask" => Ok(MessageType::Question),
"reply" => Ok(MessageType::Reply),
"done" => Ok(MessageType::Done),
"claim" => Ok(MessageType::Claim),
"release" => Ok(MessageType::Release),
"approve" => Ok(MessageType::Approve),
"deny" => Ok(MessageType::Deny),
"control" => Ok(MessageType::Control),
"system" => Ok(MessageType::System),
other => Err(Error::Invalid(format!("unknown message type: {other}"))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum DataClass {
Public,
#[default]
Internal,
Confidential,
Pii,
}
impl DataClass {
pub fn as_str(&self) -> &'static str {
match self {
DataClass::Public => "public",
DataClass::Internal => "internal",
DataClass::Confidential => "confidential",
DataClass::Pii => "pii",
}
}
}
impl fmt::Display for DataClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for DataClass {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"public" => Ok(DataClass::Public),
"internal" => Ok(DataClass::Internal),
"confidential" => Ok(DataClass::Confidential),
"pii" => Ok(DataClass::Pii),
other => Err(Error::Invalid(format!("unknown data class: {other}"))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AgentInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vendor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Action {
pub verb: String,
pub target: String,
#[serde(default)]
pub params: Value,
}
impl Action {
pub fn hash(&self) -> String {
sha256_json(&serde_json::to_value(self).expect("action serializes"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Content {
#[serde(default)]
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action: Option<Action>,
#[serde(default)]
pub data: Value,
}
impl Content {
pub fn hash(&self) -> String {
sha256_json(&serde_json::to_value(self).expect("content serializes"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub v: u32,
pub id: String,
pub room: String,
#[serde(default)]
pub seq: u64,
#[serde(default)]
pub prev: String,
#[serde(default)]
pub trace: Option<String>,
pub from: String,
#[serde(default)]
pub agent: Option<AgentInfo>,
#[serde(rename = "type")]
pub kind: MessageType,
#[serde(default)]
pub text: String,
#[serde(default)]
pub action: Option<Action>,
#[serde(default)]
pub data: Value,
#[serde(default)]
pub reply_to: Option<String>,
#[serde(default)]
pub to: Option<String>,
#[serde(default)]
pub class: DataClass,
pub ts: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub action_hash: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub once: Option<bool>,
#[serde(default)]
pub sig: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub tombstone: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Draft {
pub room: String,
pub from: String,
pub kind: Option<MessageType>,
pub text: String,
pub action: Option<Action>,
pub data: Value,
pub reply_to: Option<String>,
pub to: Option<String>,
pub trace: Option<String>,
pub class: Option<DataClass>,
pub agent: Option<AgentInfo>,
pub action_hash: Option<String>,
pub expires: Option<String>,
pub once: Option<bool>,
}
pub fn new_id() -> String {
format!("m_{}", ulid::Ulid::generate())
}
pub const MAX_CLOCK_SKEW_SECS: i64 = 300;
pub fn now_ts() -> String {
chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
impl Message {
pub fn new(draft: Draft, signer: &dyn Signer) -> Result<Self> {
let mut m = Message {
v: SCHEMA_VERSION,
id: new_id(),
room: draft.room,
seq: 0,
prev: String::new(),
trace: draft.trace,
from: draft.from,
agent: draft.agent,
kind: draft.kind.unwrap_or(MessageType::Chat),
text: draft.text,
action: draft.action,
data: if draft.data.is_null() {
Value::Null
} else {
draft.data
},
reply_to: draft.reply_to,
to: draft.to,
class: draft.class.unwrap_or_default(),
ts: now_ts(),
action_hash: draft.action_hash,
expires: draft.expires,
once: draft.once,
sig: String::new(),
content_hash: None,
tombstone: false,
};
m.sig = signer.sign(&m.signing_bytes());
m.check_size()?;
Ok(m)
}
pub fn content(&self) -> Content {
Content {
text: self.text.clone(),
action: self.action.clone(),
data: self.data.clone(),
}
}
pub fn content_hash(&self) -> String {
match &self.content_hash {
Some(h) => h.clone(),
None => self.content().hash(),
}
}
pub fn with_explicit_hash(&self) -> Message {
let mut m = self.clone();
m.content_hash = Some(self.content_hash());
m
}
pub fn signing_bytes(&self) -> Vec<u8> {
let v = serde_json::json!({
"v": self.v,
"id": self.id,
"room": self.room,
"trace": self.trace,
"from": self.from,
"agent": self.agent,
"type": self.kind,
"content_hash": self.content_hash(),
"reply_to": self.reply_to,
"to": self.to,
"class": self.class,
"ts": self.ts,
"action_hash": self.action_hash,
"expires": self.expires,
"once": self.once,
});
canonical_json(&v).into_bytes()
}
pub fn verify(&self, key: &PublicKey) -> Result<()> {
key.verify(&self.signing_bytes(), &self.sig)
}
pub fn check_ts(&self, now: chrono::DateTime<chrono::Utc>) -> Result<()> {
let t = chrono::DateTime::parse_from_rfc3339(&self.ts)
.map_err(|_| Error::Invalid(format!("ts {:?} is not RFC 3339", self.ts)))?
.with_timezone(&chrono::Utc);
if t.to_rfc3339_opts(chrono::SecondsFormat::Secs, true) != self.ts {
return Err(Error::Invalid(format!(
"ts {:?} must be UTC, whole seconds, ending in Z",
self.ts
)));
}
if t > now + chrono::Duration::seconds(MAX_CLOCK_SKEW_SECS) {
return Err(Error::Invalid(format!(
"ts {} is in the future; check this computer's clock",
self.ts
)));
}
Ok(())
}
pub fn envelope(&self) -> Value {
serde_json::json!({
"v": self.v,
"id": self.id,
"room": self.room,
"seq": self.seq,
"prev": self.prev,
"trace": self.trace,
"from": self.from,
"agent": self.agent,
"type": self.kind,
"content_hash": self.content_hash(),
"reply_to": self.reply_to,
"to": self.to,
"class": self.class,
"ts": self.ts,
"action_hash": self.action_hash,
"expires": self.expires,
"once": self.once,
"sig": self.sig,
})
}
pub fn chain_hash(&self) -> String {
sha256_json(&self.envelope())
}
pub fn sequence(&mut self, seq: u64, prev: &str) {
self.seq = seq;
self.prev = prev.to_string();
}
pub fn is_sequenced(&self) -> bool {
self.seq > 0 && !self.prev.is_empty()
}
pub fn check_size(&self) -> Result<()> {
let len = serde_json::to_string(self)?.len();
if len > MAX_MESSAGE_BYTES {
return Err(Error::Invalid(format!(
"message is {len} bytes, cap is {MAX_MESSAGE_BYTES}. Send big things by reference."
)));
}
Ok(())
}
pub fn tombstone(&self) -> Message {
let mut t = self.clone();
t.content_hash = Some(self.content_hash());
t.tombstone = true;
t.text = String::new();
t.action = None;
t.data = Value::Null;
t
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::{Identity, Kind};
fn draft(room: &str, from: &str, text: &str) -> Draft {
Draft {
room: room.into(),
from: from.into(),
text: text.into(),
kind: Some(MessageType::Task),
..Default::default()
}
}
#[test]
fn new_message_is_signed_and_verifies() {
let alice = Identity::generate("alice", Kind::Agent);
let m = Message::new(draft("ops", "alice", "deploy?"), &alice).unwrap();
assert_eq!(m.v, 1);
assert!(m.id.starts_with("m_"));
m.verify(&alice.public()).unwrap();
let bob = Identity::generate("bob", Kind::Agent);
assert!(m.verify(&bob.public()).is_err());
}
#[test]
fn ts_must_be_well_formed_and_not_in_the_future() {
let alice = Identity::generate("alice", Kind::Agent);
let mut m = Message::new(draft("ops", "alice", "hi"), &alice).unwrap();
let now = chrono::Utc::now();
m.check_ts(now).unwrap();
m.ts = "2020-01-01T00:00:00Z".into();
m.check_ts(now).unwrap();
let soon = now + chrono::Duration::seconds(60);
m.ts = soon.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
m.check_ts(now).unwrap();
m.ts = "2999-01-01T00:00:00Z".into();
assert!(m.check_ts(now).is_err());
for bad in [
"2020-01-01T00:00:00+00:00",
"2020-01-01T00:00:00.5Z",
"2020-01-01 00:00:00Z",
"yesterday",
] {
m.ts = bad.into();
assert!(m.check_ts(now).is_err(), "{bad}");
}
}
#[test]
fn sequencing_does_not_break_signature() {
let alice = Identity::generate("alice", Kind::Agent);
let mut m = Message::new(draft("ops", "alice", "hi"), &alice).unwrap();
m.sequence(1, GENESIS_PREV);
m.verify(&alice.public()).unwrap();
assert!(m.is_sequenced());
}
#[test]
fn editing_content_breaks_signature() {
let alice = Identity::generate("alice", Kind::Agent);
let mut m = Message::new(draft("ops", "alice", "ship v1.2"), &alice).unwrap();
m.text = "ship v1.3".into();
assert!(m.verify(&alice.public()).is_err());
}
#[test]
fn chain_links_and_survives_tombstone() {
let alice = Identity::generate("alice", Kind::Agent);
let mut a = Message::new(draft("ops", "alice", "first"), &alice).unwrap();
a.sequence(1, GENESIS_PREV);
let mut b = Message::new(draft("ops", "alice", "second"), &alice).unwrap();
b.sequence(2, &a.chain_hash());
assert_eq!(b.prev, a.chain_hash());
let t = a.tombstone();
assert_eq!(t.text, "");
assert!(t.tombstone);
assert_eq!(t.content_hash(), a.content_hash());
assert_eq!(t.chain_hash(), b.prev);
t.verify(&alice.public()).unwrap();
let v = serde_json::to_value(&t).unwrap();
assert!(v.get("content_hash").is_some());
let v = serde_json::to_value(&a).unwrap();
assert!(v.get("content_hash").is_none());
assert!(v.get("tombstone").is_none());
}
#[test]
fn json_shape_matches_plan() {
let alice = Identity::generate("alice", Kind::Agent);
let mut d = draft("ops", "alice", "Deploy api-service v1.2 to prod?");
d.kind = Some(MessageType::Question);
d.trace = Some("ticket-4711".into());
d.action = Some(Action {
verb: "deploy".into(),
target: "api-service".into(),
params: serde_json::json!({"version": "1.2", "env": "prod"}),
});
let m = Message::new(d, &alice).unwrap();
let v = serde_json::to_value(&m).unwrap();
for key in [
"v", "id", "room", "seq", "prev", "trace", "from", "agent", "type", "text", "action",
"data", "reply_to", "to", "class", "ts", "sig",
] {
assert!(v.get(key).is_some(), "missing {key}");
}
assert_eq!(v["type"], "question");
assert_eq!(v["class"], "internal");
assert!(v.get("action_hash").is_none());
let back: Message = serde_json::from_value(v).unwrap();
assert_eq!(back, m);
let asked: MessageType = serde_json::from_str("\"ask\"").unwrap();
assert_eq!(asked, MessageType::Question);
}
#[test]
fn size_cap() {
let alice = Identity::generate("alice", Kind::Agent);
let big = "x".repeat(MAX_MESSAGE_BYTES + 1);
assert!(Message::new(draft("ops", "alice", &big), &alice).is_err());
}
}