mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
Use serde_jcs for RFC 8785 compliancy
This fixes serialization and reduces maintainance cost, but does more data copying on serialization.
This commit is contained in:
parent
6e7229e4ac
commit
a63d0df443
5 changed files with 60 additions and 57 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -259,11 +259,12 @@ dependencies = [
|
|||
"ed25519-dalek",
|
||||
"hex",
|
||||
"html-escape",
|
||||
"rand",
|
||||
"rand_core",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_jcs",
|
||||
"serde_json",
|
||||
"syn",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
@ -1392,6 +1393,12 @@ version = "1.0.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu-js"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f"
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.23"
|
||||
|
@ -1478,6 +1485,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_jcs"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cacecf649bc1a7c5f0e299cc813977c6a78116abda2b93b1ee01735b71ead9a8"
|
||||
dependencies = [
|
||||
"ryu-js",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.127"
|
||||
|
|
|
@ -18,11 +18,10 @@ bitflags_serde_shim = "0.2"
|
|||
ed25519-dalek = "2.1"
|
||||
hex = { version = "0.4", features = ["serde"] }
|
||||
html-escape = "0.2"
|
||||
rand = "0.8.5"
|
||||
rand_core = "0.6"
|
||||
rusqlite = { version = "0.32", optional = true }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_jcs = "0.1"
|
||||
serde_json = "1"
|
||||
uuid = { version = "1", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
syn = { version = "2.0.76", features = ["full", "visit"] }
|
||||
|
|
|
@ -5,8 +5,8 @@ use std::{fs, io};
|
|||
use anyhow::{Context, Result};
|
||||
use blah::bitflags;
|
||||
use blah::types::{
|
||||
ChatPayload, CreateRoomPayload, MemberPermission, RichText, RoomAttrs, RoomMember,
|
||||
RoomMemberList, ServerPermission, UserKey, WithSig,
|
||||
get_timestamp, ChatPayload, CreateRoomPayload, MemberPermission, RichText, RoomAttrs,
|
||||
RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig,
|
||||
};
|
||||
use blah::uuid::Uuid;
|
||||
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
||||
|
@ -220,7 +220,7 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
|||
user: UserKey(key.verifying_key().to_bytes()),
|
||||
}]),
|
||||
};
|
||||
let payload = WithSig::sign(&key, &mut OsRng, payload)?;
|
||||
let payload = WithSig::sign(&key, get_timestamp(), &mut OsRng, payload)?;
|
||||
|
||||
let ret = client
|
||||
.post(api_url.join("/room/create")?)
|
||||
|
@ -242,7 +242,7 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
|||
room,
|
||||
rich_text: RichText::from(text),
|
||||
};
|
||||
let payload = WithSig::sign(&key, &mut OsRng, payload)?;
|
||||
let payload = WithSig::sign(&key, get_timestamp(), &mut OsRng, payload)?;
|
||||
|
||||
let ret = client
|
||||
.post(api_url.join(&format!("/room/{room}/item"))?)
|
||||
|
|
|
@ -96,7 +96,12 @@ async function showChatMsg(chat) {
|
|||
let verifyRet = null;
|
||||
crypto.subtle.exportKey('raw', keypair.publicKey)
|
||||
try {
|
||||
const signeeBytes = (new TextEncoder()).encode(JSON.stringify(chat.signee));
|
||||
const sortKeys = (obj) =>
|
||||
Object.fromEntries(Object.entries(obj).sort((lhs, rhs) => lhs[0] > rhs[0]));
|
||||
const canonicalJson = chat.signee
|
||||
// Just for simplicity. Only this struct is unsorted due to serde implementation.
|
||||
canonicalJson.payload = sortKeys(canonicalJson.payload)
|
||||
const signeeBytes = (new TextEncoder()).encode(JSON.stringify(canonicalJson));
|
||||
const rawkey = hexToBuf(chat.signee.user);
|
||||
const senderKey = await crypto.subtle.importKey('raw', rawkey, { name: 'Ed25519' }, true, ['verify']);
|
||||
const success = await crypto.subtle.verify('Ed25519', senderKey, hexToBuf(chat.sig), signeeBytes);
|
||||
|
@ -244,9 +249,10 @@ async function postChat(text) {
|
|||
richText = [text];
|
||||
}
|
||||
const signedPayload = await signData({
|
||||
typ: 'chat',
|
||||
// sorted fields.
|
||||
rich_text: richText,
|
||||
room: roomUuid,
|
||||
typ: 'chat',
|
||||
});
|
||||
const resp = await fetch(`${roomUrl}/item`, {
|
||||
method: 'POST',
|
||||
|
|
74
src/types.rs
74
src/types.rs
|
@ -1,7 +1,3 @@
|
|||
//! NB. All structs here that are part of signee must be lexically sorted, as RFC8785.
|
||||
//! This is tested by `canonical_fields_sorted`.
|
||||
//! See: https://www.rfc-editor.org/rfc/rfc8785
|
||||
//! FIXME: `typ` is still always the first field because of `serde`'s implementation.
|
||||
use std::fmt;
|
||||
use std::time::SystemTime;
|
||||
|
||||
|
@ -44,7 +40,7 @@ pub struct Signee<T> {
|
|||
pub user: UserKey,
|
||||
}
|
||||
|
||||
fn get_timestamp() -> u64 {
|
||||
pub fn get_timestamp() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("after UNIX epoch")
|
||||
|
@ -55,16 +51,17 @@ impl<T: Serialize> WithSig<T> {
|
|||
/// Sign the payload with the given `key`.
|
||||
pub fn sign(
|
||||
key: &SigningKey,
|
||||
timestamp: u64,
|
||||
rng: &mut impl RngCore,
|
||||
payload: T,
|
||||
) -> Result<Self, SignatureError> {
|
||||
let signee = Signee {
|
||||
nonce: rng.next_u32(),
|
||||
payload,
|
||||
timestamp: get_timestamp(),
|
||||
timestamp,
|
||||
user: UserKey(key.verifying_key().to_bytes()),
|
||||
};
|
||||
let canonical_signee = serde_json::to_vec(&signee).expect("serialization cannot fail");
|
||||
let canonical_signee = serde_jcs::to_vec(&signee).expect("serialization cannot fail");
|
||||
let sig = key.try_sign(&canonical_signee)?.to_bytes();
|
||||
Ok(Self { sig, signee })
|
||||
}
|
||||
|
@ -73,7 +70,7 @@ impl<T: Serialize> WithSig<T> {
|
|||
///
|
||||
/// Note that this does nott check validity of timestamp and other data.
|
||||
pub fn verify(&self) -> Result<(), SignatureError> {
|
||||
let canonical_signee = serde_json::to_vec(&self.signee).expect("serialization cannot fail");
|
||||
let canonical_signee = serde_jcs::to_vec(&self.signee).expect("serialization cannot fail");
|
||||
let sig = Signature::from_bytes(&self.sig);
|
||||
VerifyingKey::from_bytes(&self.signee.user.0)?.verify_strict(&canonical_signee, &sig)?;
|
||||
Ok(())
|
||||
|
@ -431,49 +428,32 @@ mod sql_impl {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write;
|
||||
|
||||
use syn::visit;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Visitor {
|
||||
errors: String,
|
||||
}
|
||||
|
||||
const SKIP_CHECK_STRUCTS: &[&str] = &["RichTextPiece", "RichTextPieceRaw"];
|
||||
|
||||
impl<'ast> syn::visit::Visit<'ast> for Visitor {
|
||||
fn visit_fields_named(&mut self, i: &'ast syn::FieldsNamed) {
|
||||
let fields = i
|
||||
.named
|
||||
.iter()
|
||||
.flat_map(|f| f.ident.clone())
|
||||
.map(|i| i.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
if !fields.windows(2).all(|w| w[0] < w[1]) {
|
||||
writeln!(self.errors, "unsorted fields: {fields:?}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_item_struct(&mut self, i: &'ast syn::ItemStruct) {
|
||||
if !SKIP_CHECK_STRUCTS.contains(&&*i.ident.to_string()) {
|
||||
visit::visit_item_struct(self, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_fields_sorted() {
|
||||
let src = std::fs::read_to_string(file!()).unwrap();
|
||||
let file = syn::parse_file(&src).unwrap();
|
||||
fn canonical_chat() {
|
||||
let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1);
|
||||
let signing_key = SigningKey::from_bytes(&[0x42; 32]);
|
||||
let timestamp = 0xDEAD_BEEF;
|
||||
let item = WithSig::sign(
|
||||
&signing_key,
|
||||
timestamp,
|
||||
&mut fake_rng,
|
||||
ChatPayload {
|
||||
rich_text: RichText::from("hello"),
|
||||
room: Uuid::nil(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut v = Visitor::default();
|
||||
syn::visit::visit_file(&mut v, &file);
|
||||
if !v.errors.is_empty() {
|
||||
panic!("{}", v.errors);
|
||||
}
|
||||
let json = serde_jcs::to_string(&item).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
r#"{"sig":"5e52985dc9e43a77267f0b383a8223af96f36e83c180a36da627dfac6504b2bb4c6b80c9903a6c3a0bbc742718466d72af4407a8e74d41af5cb0137cf3798d08","signee":{"nonce":66,"payload":{"rich_text":["hello"],"room":"00000000-0000-0000-0000-000000000000","typ":"chat"},"timestamp":3735928559,"user":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12"}}"#
|
||||
);
|
||||
let roundtrip_item = serde_json::from_str::<WithSig<ChatPayload>>(&json).unwrap();
|
||||
// assert_eq!(roundtrip_item, item);
|
||||
roundtrip_item.verify().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Add table
Reference in a new issue