From 7f8f89eff741ae8f8a908cc029eaa698facc222c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 10 Oct 2024 11:25:49 +0200 Subject: [PATCH] identifiers: Differentiate signing keys from device keys Use OwnedCrossSigningKeyId, OwnedDeviceSigningKeyId and OwnedCrossSigningOrDeviceSigningKeyId instead of OwnedDeviceKeyId to identify signing keys. --- crates/ruma-client-api/CHANGELOG.md | 3 + crates/ruma-client-api/src/backup.rs | 4 +- crates/ruma-common/CHANGELOG.md | 11 +++ crates/ruma-common/src/encryption.rs | 26 ++++-- crates/ruma-common/src/identifiers.rs | 31 +++++-- .../src/identifiers/base64_public_key.rs | 84 +++++++++++++++++++ .../base64_public_key_or_device_id.rs | 65 ++++++++++++++ crates/ruma-common/src/identifiers/key_id.rs | 34 +++++++- .../ruma-identifiers-validation/CHANGELOG.md | 1 + .../src/base64_public_key.rs | 10 +++ crates/ruma-identifiers-validation/src/lib.rs | 1 + crates/ruma-macros/src/lib.rs | 17 +++- 12 files changed, 265 insertions(+), 22 deletions(-) create mode 100644 crates/ruma-common/src/identifiers/base64_public_key.rs create mode 100644 crates/ruma-common/src/identifiers/base64_public_key_or_device_id.rs create mode 100644 crates/ruma-identifiers-validation/src/base64_public_key.rs diff --git a/crates/ruma-client-api/CHANGELOG.md b/crates/ruma-client-api/CHANGELOG.md index d4278060..b98d3b4b 100644 --- a/crates/ruma-client-api/CHANGELOG.md +++ b/crates/ruma-client-api/CHANGELOG.md @@ -21,6 +21,9 @@ Breaking changes: - Use `OwnedOneTimeKeyId` and `OneTimeKeyAlgorithm` instead of `OwnedDeviceKeyId` and `DeviceKeyAlgorithm` respectively to identify one-time and fallback keys and their algorithm. +- Use `OwnedCrossSigningOrDeviceSigningKeyId` instead of `OwnedDeviceKeyId` to + identify signing keys in `BackupAlgorithm::MegolmBackupV1Curve25519AesSha2`'s + `signatures`. Improvements: diff --git a/crates/ruma-client-api/src/backup.rs b/crates/ruma-client-api/src/backup.rs index 6131fd9d..caaad95b 100644 --- a/crates/ruma-client-api/src/backup.rs +++ b/crates/ruma-client-api/src/backup.rs @@ -20,7 +20,7 @@ use std::collections::BTreeMap; use js_int::UInt; use ruma_common::{ serde::{Base64, Raw}, - OwnedDeviceKeyId, OwnedUserId, + OwnedCrossSigningOrDeviceSigningKeyId, OwnedUserId, }; use serde::{Deserialize, Serialize}; @@ -51,7 +51,7 @@ pub enum BackupAlgorithm { public_key: Base64, /// Signatures of the auth_data as Signed JSON. - signatures: BTreeMap>, + signatures: BTreeMap>, }, } diff --git a/crates/ruma-common/CHANGELOG.md b/crates/ruma-common/CHANGELOG.md index 2b47f6d2..e237dfee 100644 --- a/crates/ruma-common/CHANGELOG.md +++ b/crates/ruma-common/CHANGELOG.md @@ -29,6 +29,13 @@ Breaking changes: `Signatures::insert` is now dereferenced to `BTreeMap::insert`. - Move the `DeviceKeyAlgorithm::SignedCurve25519` into the new `OneTimeKeyAlgorithm` type. +- Add `(Owned)CrossSigningKeyId` and use it instead of `OwnedDeviceKeyId` to + identify `CrossSigningKey`'s `keys`. +- Add `(Owned)CrossSigningOrDeviceSigningKeyId` and use it instead of + `OwnedDeviceKeyId` to identify signing keys in `DeviceKeys`'s and + `CrossSigningKey`'s `signatures`. +- Use `OwnedDeviceSigningKeyId` instead of `OwnedDeviceKeyId` to identify + signing keys in `SignedKey`'s `signatures`. Improvements: @@ -49,6 +56,10 @@ Improvements: `(entity, key_identifier, value)` tuples. - Add `(Owned)OneTimeKeyId` and `(Owned)OneTimeKeyName` to identify one-time and fallback keys instead of using `(Owned)DeviceKeyId`. +- Add `(Owned)Base64PublicKey` and `(Owned)Base64PublicKeyOrDeviceId` to + identify cross-signing keys. + - Add `(owned_)base_64_public_key` to construct a compile-time validated + `(Owned)Base64PublicKey`. # 0.13.0 diff --git a/crates/ruma-common/src/encryption.rs b/crates/ruma-common/src/encryption.rs index 56a1c873..7f210a45 100644 --- a/crates/ruma-common/src/encryption.rs +++ b/crates/ruma-common/src/encryption.rs @@ -8,7 +8,8 @@ use serde::{Deserialize, Serialize}; use crate::{ serde::{Base64, StringEnum}, - EventEncryptionAlgorithm, OwnedDeviceId, OwnedDeviceKeyId, OwnedUserId, PrivOwnedStr, + EventEncryptionAlgorithm, OwnedCrossSigningKeyId, OwnedCrossSigningOrDeviceSigningKeyId, + OwnedDeviceId, OwnedDeviceKeyId, OwnedDeviceSigningKeyId, OwnedUserId, PrivOwnedStr, }; /// Identity keys for a device. @@ -32,7 +33,7 @@ pub struct DeviceKeys { pub keys: BTreeMap, /// Signatures for the device key object. - pub signatures: BTreeMap>, + pub signatures: BTreeMap>, /// Additional data added to the device key information by intermediate servers, and /// not covered by the signatures. @@ -48,7 +49,7 @@ impl DeviceKeys { device_id: OwnedDeviceId, algorithms: Vec, keys: BTreeMap, - signatures: BTreeMap>, + signatures: BTreeMap>, ) -> Self { Self { user_id, device_id, algorithms, keys, signatures, unsigned: Default::default() } } @@ -76,7 +77,7 @@ impl UnsignedDeviceInfo { } /// Signatures for a `SignedKey` object. -pub type SignedKeySignatures = BTreeMap>; +pub type SignedKeySignatures = BTreeMap>; /// A key for the SignedCurve25519 algorithm #[derive(Debug, Clone, Serialize, Deserialize)] @@ -118,9 +119,12 @@ pub enum OneTimeKey { } /// Signatures for a `CrossSigningKey` object. -pub type CrossSigningKeySignatures = BTreeMap>; +pub type CrossSigningKeySignatures = + BTreeMap>; -/// A cross signing key. +/// A [cross-signing] key. +/// +/// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct CrossSigningKey { @@ -133,11 +137,15 @@ pub struct CrossSigningKey { /// The public key. /// /// The object must have exactly one property. - pub keys: BTreeMap, + pub keys: BTreeMap, /// Signatures of the key. /// - /// Only optional for master key. + /// The master key should be signed by the device key and can be signed by other users' + /// user-signing key. The user-signing and self-signing keys must be signed by the master + /// key. + /// + /// Only optional for the master key. #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub signatures: CrossSigningKeySignatures, } @@ -147,7 +155,7 @@ impl CrossSigningKey { pub fn new( user_id: OwnedUserId, usage: Vec, - keys: BTreeMap, + keys: BTreeMap, signatures: CrossSigningKeySignatures, ) -> Self { Self { user_id, usage, keys, signatures } diff --git a/crates/ruma-common/src/identifiers.rs b/crates/ruma-common/src/identifiers.rs index a7d80d55..20883f0e 100644 --- a/crates/ruma-common/src/identifiers.rs +++ b/crates/ruma-common/src/identifiers.rs @@ -16,6 +16,8 @@ use serde::de::{self, Deserializer, Unexpected}; #[doc(inline)] pub use self::{ + base64_public_key::{Base64PublicKey, OwnedBase64PublicKey}, + base64_public_key_or_device_id::{Base64PublicKeyOrDeviceId, OwnedBase64PublicKeyOrDeviceId}, client_secret::{ClientSecret, OwnedClientSecret}, crypto_algorithms::{ DeviceKeyAlgorithm, EventEncryptionAlgorithm, KeyDerivationAlgorithm, OneTimeKeyAlgorithm, @@ -25,9 +27,10 @@ pub use self::{ device_key_id::{DeviceKeyId, OwnedDeviceKeyId}, event_id::{EventId, OwnedEventId}, key_id::{ - DeviceSigningKeyId, KeyAlgorithm, KeyId, OneTimeKeyId, OwnedDeviceSigningKeyId, OwnedKeyId, - OwnedOneTimeKeyId, OwnedServerSigningKeyId, OwnedSigningKeyId, ServerSigningKeyId, - SigningKeyId, + CrossSigningKeyId, CrossSigningOrDeviceSigningKeyId, DeviceSigningKeyId, KeyAlgorithm, + KeyId, OneTimeKeyId, OwnedCrossSigningKeyId, OwnedCrossSigningOrDeviceSigningKeyId, + OwnedDeviceSigningKeyId, OwnedKeyId, OwnedOneTimeKeyId, OwnedServerSigningKeyId, + OwnedSigningKeyId, ServerSigningKeyId, SigningKeyId, }, matrix_uri::{MatrixToUri, MatrixUri}, mxc_uri::{Mxc, MxcUri, OwnedMxcUri}, @@ -49,6 +52,8 @@ pub use self::{ pub mod matrix_uri; pub mod user_id; +mod base64_public_key; +mod base64_public_key_or_device_id; mod client_secret; mod crypto_algorithms; mod device_id; @@ -113,8 +118,8 @@ macro_rules! owned_device_id { #[doc(hidden)] pub mod __private_macros { pub use ruma_macros::{ - device_key_id, event_id, mxc_uri, room_alias_id, room_id, room_version_id, server_name, - server_signing_key_version, user_id, + base64_public_key, device_key_id, event_id, mxc_uri, room_alias_id, room_id, + room_version_id, server_name, server_signing_key_version, user_id, }; } @@ -274,3 +279,19 @@ macro_rules! owned_user_id { $crate::user_id!($s).to_owned() }; } + +/// Compile-time checked [`Base64PublicKey`] construction. +#[macro_export] +macro_rules! base64_public_key { + ($s:literal) => { + $crate::__private_macros::base64_public_key!($crate, $s) + }; +} + +/// Compile-time checked [`OwnedBase64PublicKey`] construction. +#[macro_export] +macro_rules! owned_base64_public_key { + ($s:literal) => { + $crate::base64_public_key!($s).to_owned() + }; +} diff --git a/crates/ruma-common/src/identifiers/base64_public_key.rs b/crates/ruma-common/src/identifiers/base64_public_key.rs new file mode 100644 index 00000000..4b87c02c --- /dev/null +++ b/crates/ruma-common/src/identifiers/base64_public_key.rs @@ -0,0 +1,84 @@ +use ruma_macros::IdZst; + +use super::{IdParseError, KeyName}; +use crate::serde::{base64::Standard, Base64, Base64DecodeError}; + +/// A public key encoded using unpadded base64, used as an identifier for [cross-signing] keys. +/// +/// This string is validated using the set `[a-zA-Z0-9+/=]`, but it is not validated to be decodable +/// as base64. This type is provided simply for its semantic value. +/// +/// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing +#[repr(transparent)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] +#[ruma_id(validate = ruma_identifiers_validation::base64_public_key::validate)] +pub struct Base64PublicKey(str); + +impl OwnedBase64PublicKey { + /// Construct a new `OwnedBase64PublicKey` by encoding the given bytes using unpadded base64. + pub fn with_bytes>(bytes: B) -> OwnedBase64PublicKey { + Base64::::new(bytes).into() + } +} + +impl KeyName for Base64PublicKey { + fn validate(s: &str) -> Result<(), IdParseError> { + ruma_identifiers_validation::base64_public_key::validate(s) + } +} + +impl KeyName for OwnedBase64PublicKey { + fn validate(s: &str) -> Result<(), IdParseError> { + ruma_identifiers_validation::base64_public_key::validate(s) + } +} + +impl> From> for OwnedBase64PublicKey { + fn from(value: Base64) -> Self { + value.to_string().try_into().unwrap_or_else(|_| unreachable!()) + } +} + +impl TryFrom<&Base64PublicKey> for Base64> { + type Error = Base64DecodeError; + + fn try_from(value: &Base64PublicKey) -> Result { + Base64::parse(value) + } +} + +impl TryFrom<&OwnedBase64PublicKey> for Base64> { + type Error = Base64DecodeError; + + fn try_from(value: &OwnedBase64PublicKey) -> Result { + Base64::parse(value) + } +} + +impl TryFrom for Base64> { + type Error = Base64DecodeError; + + fn try_from(value: OwnedBase64PublicKey) -> Result { + Base64::parse(value) + } +} + +#[cfg(test)] +mod tests { + use super::{Base64PublicKey, OwnedBase64PublicKey}; + + #[test] + fn valid_string() { + <&Base64PublicKey>::try_from("base64+master+public+key").unwrap(); + } + + #[test] + fn invalid_string() { + <&Base64PublicKey>::try_from("not@base@64").unwrap_err(); + } + + #[test] + fn constructor() { + _ = OwnedBase64PublicKey::with_bytes(b"self-signing master public key"); + } +} diff --git a/crates/ruma-common/src/identifiers/base64_public_key_or_device_id.rs b/crates/ruma-common/src/identifiers/base64_public_key_or_device_id.rs new file mode 100644 index 00000000..681bf21a --- /dev/null +++ b/crates/ruma-common/src/identifiers/base64_public_key_or_device_id.rs @@ -0,0 +1,65 @@ +use ruma_macros::IdZst; + +use super::{ + Base64PublicKey, DeviceId, IdParseError, KeyName, OwnedBase64PublicKey, OwnedDeviceId, +}; + +/// A Matrix ID that can be either a [`DeviceId`] or a [`Base64PublicKey`]. +/// +/// Device identifiers in Matrix are completely opaque character sequences and cross-signing keys +/// are identified by their base64-encoded public key. This type is provided simply for its semantic +/// value. +/// +/// It is not recommended to construct this type directly, it should instead be converted from a +/// [`DeviceId`] or a [`Base64PublicKey`]. +/// +/// # Example +/// +/// ``` +/// use ruma_common::{Base64PublicKeyOrDeviceId, OwnedBase64PublicKeyOrDeviceId}; +/// +/// let ref_id: &Base64PublicKeyOrDeviceId = "abcdefghi".into(); +/// assert_eq!(ref_id.as_str(), "abcdefghi"); +/// +/// let owned_id: OwnedBase64PublicKeyOrDeviceId = "ijklmnop".into(); +/// assert_eq!(owned_id.as_str(), "ijklmnop"); +/// ``` +#[repr(transparent)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, IdZst)] +pub struct Base64PublicKeyOrDeviceId(str); + +impl KeyName for Base64PublicKeyOrDeviceId { + fn validate(_s: &str) -> Result<(), IdParseError> { + Ok(()) + } +} + +impl KeyName for OwnedBase64PublicKeyOrDeviceId { + fn validate(_s: &str) -> Result<(), IdParseError> { + Ok(()) + } +} + +impl<'a> From<&'a DeviceId> for &'a Base64PublicKeyOrDeviceId { + fn from(value: &'a DeviceId) -> Self { + Self::from(value.as_str()) + } +} + +impl From for OwnedBase64PublicKeyOrDeviceId { + fn from(value: OwnedDeviceId) -> Self { + Self::from(value.as_str()) + } +} + +impl<'a> From<&'a Base64PublicKey> for &'a Base64PublicKeyOrDeviceId { + fn from(value: &'a Base64PublicKey) -> Self { + Self::from(value.as_str()) + } +} + +impl From for OwnedBase64PublicKeyOrDeviceId { + fn from(value: OwnedBase64PublicKey) -> Self { + Self::from(value.as_str()) + } +} diff --git a/crates/ruma-common/src/identifiers/key_id.rs b/crates/ruma-common/src/identifiers/key_id.rs index 215d00a7..ea22a7cc 100644 --- a/crates/ruma-common/src/identifiers/key_id.rs +++ b/crates/ruma-common/src/identifiers/key_id.rs @@ -7,8 +7,8 @@ use std::{ use ruma_macros::IdZst; use super::{ - crypto_algorithms::SigningKeyAlgorithm, DeviceId, KeyName, OneTimeKeyAlgorithm, OneTimeKeyName, - ServerSigningKeyVersion, + crypto_algorithms::SigningKeyAlgorithm, Base64PublicKey, Base64PublicKeyOrDeviceId, DeviceId, + KeyName, OneTimeKeyAlgorithm, OneTimeKeyName, ServerSigningKeyVersion, }; /// A key algorithm and key name delimited by a colon. @@ -63,12 +63,38 @@ pub type ServerSigningKeyId = SigningKeyId; /// Algorithm + key name for homeserver signing keys. pub type OwnedServerSigningKeyId = OwnedSigningKeyId; -/// Algorithm + key name for device keys. +/// Algorithm + key name for [device signing keys]. +/// +/// [device signing keys]: https://spec.matrix.org/latest/client-server-api/#device-keys pub type DeviceSigningKeyId = SigningKeyId; -/// Algorithm + key name for device keys. +/// Algorithm + key name for [device signing] keys. +/// +/// [device signing keys]: https://spec.matrix.org/latest/client-server-api/#device-keys pub type OwnedDeviceSigningKeyId = OwnedSigningKeyId; +/// Algorithm + key name for [cross-signing] keys. +/// +/// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing +pub type CrossSigningKeyId = SigningKeyId; + +/// Algorithm + key name for [cross-signing] keys. +/// +/// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing +pub type OwnedCrossSigningKeyId = OwnedSigningKeyId; + +/// Algorithm + key name for [cross-signing] or [device signing] keys. +/// +/// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing +/// [device signing]: https://spec.matrix.org/latest/client-server-api/#device-keys +pub type CrossSigningOrDeviceSigningKeyId = SigningKeyId; + +/// Algorithm + key name for [cross-signing] or [device signing] keys. +/// +/// [cross-signing]: https://spec.matrix.org/latest/client-server-api/#cross-signing +/// [device signing]: https://spec.matrix.org/latest/client-server-api/#device-keys +pub type OwnedCrossSigningOrDeviceSigningKeyId = OwnedSigningKeyId; + /// Algorithm + key name for [one-time and fallback keys]. /// /// [one-time and fallback keys]: https://spec.matrix.org/latest/client-server-api/#one-time-and-fallback-keys diff --git a/crates/ruma-identifiers-validation/CHANGELOG.md b/crates/ruma-identifiers-validation/CHANGELOG.md index 079535d4..e3f0b469 100644 --- a/crates/ruma-identifiers-validation/CHANGELOG.md +++ b/crates/ruma-identifiers-validation/CHANGELOG.md @@ -11,6 +11,7 @@ Breaking changes: Improvements: - Add `server_signing_key_version::validate`. +- Add `base64_public_key::validate`. # 0.9.5 diff --git a/crates/ruma-identifiers-validation/src/base64_public_key.rs b/crates/ruma-identifiers-validation/src/base64_public_key.rs new file mode 100644 index 00000000..643e88a4 --- /dev/null +++ b/crates/ruma-identifiers-validation/src/base64_public_key.rs @@ -0,0 +1,10 @@ +use crate::Error; + +pub fn validate(s: &str) -> Result<(), Error> { + if s.is_empty() { + return Err(Error::Empty); + } else if !s.chars().all(|c| c.is_alphanumeric() || matches!(c, '+' | '/' | '=')) { + return Err(Error::InvalidCharacters); + } + Ok(()) +} diff --git a/crates/ruma-identifiers-validation/src/lib.rs b/crates/ruma-identifiers-validation/src/lib.rs index 1d845c56..83bb78cf 100644 --- a/crates/ruma-identifiers-validation/src/lib.rs +++ b/crates/ruma-identifiers-validation/src/lib.rs @@ -1,6 +1,7 @@ #![doc(html_favicon_url = "https://ruma.dev/favicon.ico")] #![doc(html_logo_url = "https://ruma.dev/images/logo.png")] +pub mod base64_public_key; pub mod client_secret; pub mod device_key_id; pub mod error; diff --git a/crates/ruma-macros/src/lib.rs b/crates/ruma-macros/src/lib.rs index 355488a5..a3a6b764 100644 --- a/crates/ruma-macros/src/lib.rs +++ b/crates/ruma-macros/src/lib.rs @@ -15,8 +15,8 @@ use proc_macro::TokenStream; use proc_macro2 as pm2; use quote::quote; use ruma_identifiers_validation::{ - device_key_id, event_id, mxc_uri, room_alias_id, room_id, room_version_id, server_name, - server_signing_key_version, user_id, + base64_public_key, device_key_id, event_id, mxc_uri, room_alias_id, room_id, room_version_id, + server_name, server_signing_key_version, user_id, }; use syn::{parse_macro_input, DeriveInput, ItemEnum, ItemStruct}; @@ -266,6 +266,19 @@ pub fn user_id(input: TokenStream) -> TokenStream { output.into() } +/// Compile-time checked `Base64PublicKey` construction. +#[proc_macro] +pub fn base64_public_key(input: TokenStream) -> TokenStream { + let IdentifierInput { dollar_crate, id } = parse_macro_input!(input as IdentifierInput); + assert!(base64_public_key::validate(&id.value()).is_ok(), "Invalid base64 public key"); + + let output = quote! { + <&#dollar_crate::DeviceKeyId as ::std::convert::TryFrom<&str>>::try_from(#id).unwrap() + }; + + output.into() +} + /// Derive the `AsRef` trait for an enum. #[proc_macro_derive(AsRefStr, attributes(ruma_enum))] pub fn derive_enum_as_ref_str(input: TokenStream) -> TokenStream {