mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 08:41: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",
|
"ed25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
|
"rand",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_jcs",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"syn",
|
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1392,6 +1393,12 @@ version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu-js"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -1478,6 +1485,17 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.127"
|
version = "1.0.127"
|
||||||
|
|
|
@ -18,11 +18,10 @@ bitflags_serde_shim = "0.2"
|
||||||
ed25519-dalek = "2.1"
|
ed25519-dalek = "2.1"
|
||||||
hex = { version = "0.4", features = ["serde"] }
|
hex = { version = "0.4", features = ["serde"] }
|
||||||
html-escape = "0.2"
|
html-escape = "0.2"
|
||||||
|
rand = "0.8.5"
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
rusqlite = { version = "0.32", optional = true }
|
rusqlite = { version = "0.32", optional = true }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_jcs = "0.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
uuid = { version = "1", features = ["serde"] }
|
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 anyhow::{Context, Result};
|
||||||
use blah::bitflags;
|
use blah::bitflags;
|
||||||
use blah::types::{
|
use blah::types::{
|
||||||
ChatPayload, CreateRoomPayload, MemberPermission, RichText, RoomAttrs, RoomMember,
|
get_timestamp, ChatPayload, CreateRoomPayload, MemberPermission, RichText, RoomAttrs,
|
||||||
RoomMemberList, ServerPermission, UserKey, WithSig,
|
RoomMember, RoomMemberList, ServerPermission, UserKey, WithSig,
|
||||||
};
|
};
|
||||||
use blah::uuid::Uuid;
|
use blah::uuid::Uuid;
|
||||||
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
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()),
|
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
|
let ret = client
|
||||||
.post(api_url.join("/room/create")?)
|
.post(api_url.join("/room/create")?)
|
||||||
|
@ -242,7 +242,7 @@ async fn main_api(api_url: Url, command: ApiCommand) -> Result<()> {
|
||||||
room,
|
room,
|
||||||
rich_text: RichText::from(text),
|
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
|
let ret = client
|
||||||
.post(api_url.join(&format!("/room/{room}/item"))?)
|
.post(api_url.join(&format!("/room/{room}/item"))?)
|
||||||
|
|
|
@ -96,7 +96,12 @@ async function showChatMsg(chat) {
|
||||||
let verifyRet = null;
|
let verifyRet = null;
|
||||||
crypto.subtle.exportKey('raw', keypair.publicKey)
|
crypto.subtle.exportKey('raw', keypair.publicKey)
|
||||||
try {
|
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 rawkey = hexToBuf(chat.signee.user);
|
||||||
const senderKey = await crypto.subtle.importKey('raw', rawkey, { name: 'Ed25519' }, true, ['verify']);
|
const senderKey = await crypto.subtle.importKey('raw', rawkey, { name: 'Ed25519' }, true, ['verify']);
|
||||||
const success = await crypto.subtle.verify('Ed25519', senderKey, hexToBuf(chat.sig), signeeBytes);
|
const success = await crypto.subtle.verify('Ed25519', senderKey, hexToBuf(chat.sig), signeeBytes);
|
||||||
|
@ -244,9 +249,10 @@ async function postChat(text) {
|
||||||
richText = [text];
|
richText = [text];
|
||||||
}
|
}
|
||||||
const signedPayload = await signData({
|
const signedPayload = await signData({
|
||||||
typ: 'chat',
|
// sorted fields.
|
||||||
rich_text: richText,
|
rich_text: richText,
|
||||||
room: roomUuid,
|
room: roomUuid,
|
||||||
|
typ: 'chat',
|
||||||
});
|
});
|
||||||
const resp = await fetch(`${roomUrl}/item`, {
|
const resp = await fetch(`${roomUrl}/item`, {
|
||||||
method: 'POST',
|
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::fmt;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
@ -44,7 +40,7 @@ pub struct Signee<T> {
|
||||||
pub user: UserKey,
|
pub user: UserKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_timestamp() -> u64 {
|
pub fn get_timestamp() -> u64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.expect("after UNIX epoch")
|
.expect("after UNIX epoch")
|
||||||
|
@ -55,16 +51,17 @@ impl<T: Serialize> WithSig<T> {
|
||||||
/// Sign the payload with the given `key`.
|
/// Sign the payload with the given `key`.
|
||||||
pub fn sign(
|
pub fn sign(
|
||||||
key: &SigningKey,
|
key: &SigningKey,
|
||||||
|
timestamp: u64,
|
||||||
rng: &mut impl RngCore,
|
rng: &mut impl RngCore,
|
||||||
payload: T,
|
payload: T,
|
||||||
) -> Result<Self, SignatureError> {
|
) -> Result<Self, SignatureError> {
|
||||||
let signee = Signee {
|
let signee = Signee {
|
||||||
nonce: rng.next_u32(),
|
nonce: rng.next_u32(),
|
||||||
payload,
|
payload,
|
||||||
timestamp: get_timestamp(),
|
timestamp,
|
||||||
user: UserKey(key.verifying_key().to_bytes()),
|
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();
|
let sig = key.try_sign(&canonical_signee)?.to_bytes();
|
||||||
Ok(Self { sig, signee })
|
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.
|
/// Note that this does nott check validity of timestamp and other data.
|
||||||
pub fn verify(&self) -> Result<(), SignatureError> {
|
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);
|
let sig = Signature::from_bytes(&self.sig);
|
||||||
VerifyingKey::from_bytes(&self.signee.user.0)?.verify_strict(&canonical_signee, &sig)?;
|
VerifyingKey::from_bytes(&self.signee.user.0)?.verify_strict(&canonical_signee, &sig)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -431,49 +428,32 @@ mod sql_impl {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use syn::visit;
|
|
||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn canonical_fields_sorted() {
|
fn canonical_chat() {
|
||||||
let src = std::fs::read_to_string(file!()).unwrap();
|
let mut fake_rng = rand::rngs::mock::StepRng::new(0x42, 1);
|
||||||
let file = syn::parse_file(&src).unwrap();
|
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();
|
let json = serde_jcs::to_string(&item).unwrap();
|
||||||
syn::visit::visit_file(&mut v, &file);
|
assert_eq!(
|
||||||
if !v.errors.is_empty() {
|
json,
|
||||||
panic!("{}", v.errors);
|
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]
|
#[test]
|
||||||
|
|
Loading…
Add table
Reference in a new issue