xmtp_cryptography/
basic_credential.rs

1use ed25519_dalek::SigningKey;
2use openmls::prelude::SignaturePublicKey;
3use openmls_basic_credential::SignatureKeyPair;
4use openmls_traits::signatures::Signer;
5use openmls_traits::{signatures, types::SignatureScheme};
6use serde::de::Error;
7use std::io::BufReader;
8use tls_codec::SecretTlsVecU8;
9use zeroize::Zeroizing;
10
11/// Wrapper for [`signatures::SignerError`] that implements [`std::fmt::Display`]
12#[derive(thiserror::Error, Debug)]
13pub struct SignerError {
14    inner: signatures::SignerError,
15}
16
17impl From<signatures::SignerError> for SignerError {
18    fn from(err: signatures::SignerError) -> SignerError {
19        SignerError { inner: err }
20    }
21}
22
23impl std::fmt::Display for SignerError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        use signatures::SignerError::*;
26        match self.inner {
27            SigningError => write!(f, "signing error"),
28            InvalidSignature => write!(f, "invalid signature"),
29            CryptoError(c) => write!(f, "{c}"),
30        }
31    }
32}
33
34mod private {
35
36    /// A rudimentary form of specialization
37    /// this allows implementing CredentialSigning
38    /// on `XmtpInstallationCredential` in foreign crates.
39    /// A `private::NotSpecialized` trait may only be defined in `xmtp_cryptography`.
40    /// Since it is not defined, implementations in their own crates are preferred.
41    pub struct NotSpecialized;
42}
43
44/// Sign with some public/private keypair credential
45pub trait CredentialSign<SP = private::NotSpecialized> {
46    /// the hashed context this credential signature takes place in
47    type Error;
48
49    fn credential_sign<T: SigningContextProvider>(
50        &self,
51        text: impl AsRef<str>,
52    ) -> Result<Vec<u8>, Self::Error>;
53}
54
55pub trait SigningContextProvider {
56    fn context() -> &'static [u8];
57}
58
59/// Verify a credential signature with its public key
60pub trait CredentialVerify<SP = private::NotSpecialized> {
61    type Error;
62
63    fn credential_verify<T: SigningContextProvider>(
64        &self,
65        signature_text: impl AsRef<str>,
66        signature_bytes: &[u8; 64],
67    ) -> Result<(), Self::Error>;
68}
69
70/// The credential for an XMTP Installation
71/// an XMTP Installation often refers to one specific device,
72/// and is an ed25519 key
73// Boxing the inner value avoids creating large enums if an enum stores multiple installation
74// credentials
75#[derive(Debug, Clone)]
76pub struct XmtpInstallationCredential(Box<SigningKey>);
77
78impl Default for XmtpInstallationCredential {
79    fn default() -> Self {
80        Self(Box::new(SigningKey::generate(&mut crate::rand::rng())))
81    }
82}
83
84impl XmtpInstallationCredential {
85    /// Create a new [`XmtpInstallationCredential`] with [`rand_chacha::ChaCha20Rng`]
86    pub fn new() -> Self {
87        Self(Box::new(SigningKey::generate(&mut crate::rand::rng())))
88    }
89
90    /// Get a reference to the public [`ed25519_dalek::VerifyingKey`]
91    /// Can be used to verify signatures
92    pub fn verifying_key(&self) -> ed25519_dalek::VerifyingKey {
93        self.0.verifying_key()
94    }
95
96    /// View the public [`ed25519_dalek::VerifyingKey`] as constant-sized bytes
97    pub fn public_bytes(&self) -> &[u8; 32] {
98        self.0.as_ref().as_ref().as_bytes()
99    }
100
101    /// View the public [`ed25519_dalek::VerifyingKey`] as a slice
102    pub fn public_slice(&self) -> &[u8] {
103        self.0.as_ref().as_ref().as_ref()
104    }
105
106    /// get the scheme, prefer the public [`Signer::signature_scheme`]
107    fn scheme(&self) -> SignatureScheme {
108        SignatureScheme::ED25519
109    }
110
111    pub fn with_context<'k, 'v>(
112        &'k self,
113        context: &'v [u8],
114    ) -> Result<ed25519_dalek::Context<'k, 'v, SigningKey>, ed25519_dalek::SignatureError> {
115        self.0.with_context(context)
116    }
117
118    /// Internal helper function to safely create a credential from its raw parts
119    /// private and public must be exactly 32 bytes large.
120    fn from_raw(private: &[u8], public: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
121        let keypair = Zeroizing::new({
122            let mut keypair = [0u8; 64];
123            keypair[0..32].copy_from_slice(private);
124            keypair[32..].copy_from_slice(public);
125            keypair
126        });
127
128        let signing_key = SigningKey::from_keypair_bytes(&keypair)?;
129        Ok(Self(Box::new(signing_key)))
130    }
131
132    /// Alias for [`ed25519_dalek::SigningKey::from_bytes`]
133    pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, ed25519_dalek::SignatureError> {
134        let key = SigningKey::from_bytes(bytes);
135        Ok(Self(Box::new(key)))
136    }
137
138    /// private key for this credential
139    #[cfg(feature = "exposed-keys")]
140    pub fn private_bytes(&self) -> [u8; 32] {
141        self.0.to_bytes()
142    }
143}
144
145impl From<XmtpInstallationCredential> for SignaturePublicKey {
146    fn from(value: XmtpInstallationCredential) -> Self {
147        SignaturePublicKey::from(value.verifying_key().as_ref())
148    }
149}
150
151/// The signer here must maintain compatibility with `SignatureKeyPair`
152impl Signer for XmtpInstallationCredential {
153    fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, signatures::SignerError> {
154        SignatureKeyPair::from(self).sign(payload)
155    }
156
157    fn signature_scheme(&self) -> SignatureScheme {
158        self.scheme()
159    }
160}
161
162// The signer here must maintain compatibility with `SignatureKeyPair`
163impl Signer for &XmtpInstallationCredential {
164    fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, signatures::SignerError> {
165        SignatureKeyPair::from(*self).sign(payload)
166    }
167
168    fn signature_scheme(&self) -> SignatureScheme {
169        self.scheme()
170    }
171}
172
173impl tls_codec::Deserialize for XmtpInstallationCredential {
174    fn tls_deserialize<R: std::io::Read>(bytes: &mut R) -> Result<Self, tls_codec::Error>
175    where
176        Self: Sized,
177    {
178        // a bufreader consumes its input, unlike just a `Read` instance.
179        let mut buf = BufReader::new(bytes);
180        let private = SecretTlsVecU8::tls_deserialize(&mut buf)?;
181        let public = SecretTlsVecU8::tls_deserialize(&mut buf)?;
182        let scheme = SignatureScheme::tls_deserialize(&mut buf)?;
183        if scheme != SignatureScheme::ED25519 {
184            return Err(tls_codec::Error::DecodingError(
185                "XMTP InstallationCredential must be Ed25519".into(),
186            ));
187        }
188
189        Self::from_raw(private.as_slice(), public.as_slice())
190            .map_err(|e| tls_codec::Error::DecodingError(e.to_string()))
191    }
192}
193
194impl From<XmtpInstallationCredential> for SignatureKeyPair {
195    fn from(key: XmtpInstallationCredential) -> SignatureKeyPair {
196        SignatureKeyPair::from_raw(
197            key.signature_scheme(),
198            key.0.to_bytes().into(),
199            key.0.verifying_key().to_bytes().into(),
200        )
201    }
202}
203
204impl<'a> From<&'a XmtpInstallationCredential> for SignatureKeyPair {
205    fn from(key: &'a XmtpInstallationCredential) -> SignatureKeyPair {
206        SignatureKeyPair::from_raw(
207            key.signature_scheme(),
208            key.0.to_bytes().into(),
209            key.0.verifying_key().to_bytes().into(),
210        )
211    }
212}
213
214impl From<SigningKey> for XmtpInstallationCredential {
215    fn from(signing_key: SigningKey) -> Self {
216        Self(Box::new(signing_key))
217    }
218}
219
220impl<'a> From<&'a SigningKey> for XmtpInstallationCredential {
221    fn from(signing_key: &'a SigningKey) -> Self {
222        Self(Box::new(signing_key.clone()))
223    }
224}
225
226impl tls_codec::Serialize for XmtpInstallationCredential {
227    fn tls_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<usize, tls_codec::Error> {
228        SignatureKeyPair::from(self).tls_serialize(writer)
229    }
230}
231
232impl tls_codec::Size for XmtpInstallationCredential {
233    fn tls_serialized_len(&self) -> usize {
234        SignatureKeyPair::from(self).tls_serialized_len()
235    }
236}
237
238impl serde::Serialize for XmtpInstallationCredential {
239    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
240    where
241        S: serde::Serializer,
242    {
243        SignatureKeyPair::from(self).serialize(serializer)
244    }
245}
246
247impl<'de> serde::Deserialize<'de> for XmtpInstallationCredential {
248    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
249    where
250        D: serde::Deserializer<'de>,
251    {
252        #[derive(serde::Deserialize, zeroize::ZeroizeOnDrop)]
253        struct SignatureKeyPairRemote {
254            private: Vec<u8>,
255            public: Vec<u8>,
256            #[allow(dead_code)]
257            #[zeroize(skip)]
258            _signature_scheme: SignatureScheme,
259        }
260
261        let SignatureKeyPairRemote {
262            ref private,
263            ref public,
264            ..
265        } = SignatureKeyPairRemote::deserialize(deserializer)?;
266
267        Self::from_raw(private.as_slice(), public.as_slice())
268            .map_err(|e| <D as serde::Deserializer<'_>>::Error::custom(format!("{e}")))
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use tls_codec::{Deserialize as _, Serialize as _};
276
277    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
278    #[cfg_attr(not(target_arch = "wasm32"), test)]
279    fn test_is_binary_compatible_with_mls_deser() {
280        // XmtpInstallationCredential needs to be binary-compatible/tls-codec/serde compatible with
281        // `SignatureKeyPair` from xmtp_basic_credential
282        let keypair = SignatureKeyPair::new(SignatureScheme::ED25519).unwrap();
283        let mut serialized: Vec<u8> = Vec::new();
284
285        keypair.tls_serialize(&mut serialized).unwrap();
286        let x_kp = XmtpInstallationCredential::tls_deserialize(&mut serialized.as_slice()).unwrap();
287        assert_eq!(keypair.private(), &x_kp.0.to_bytes());
288        assert_eq!(keypair.public(), &x_kp.0.verifying_key().to_bytes());
289    }
290
291    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
292    #[cfg_attr(not(target_arch = "wasm32"), test)]
293    fn test_is_binary_compatible_with_mls_ser() {
294        let keypair = XmtpInstallationCredential::new();
295        let mut serialized: Vec<u8> = Vec::new();
296
297        keypair.tls_serialize(&mut serialized).unwrap();
298        let mls_kp = SignatureKeyPair::tls_deserialize(&mut serialized.as_slice()).unwrap();
299        assert_eq!(mls_kp.private(), &keypair.0.to_bytes());
300        assert_eq!(mls_kp.public(), &keypair.0.verifying_key().to_bytes());
301        assert_eq!(mls_kp.signature_scheme(), keypair.scheme());
302    }
303
304    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
305    #[cfg_attr(not(target_arch = "wasm32"), test)]
306    fn test_is_binary_compatible_with_mls_deser_serde() {
307        // XmtpInstallationCredential needs to be serde compatible with
308        // `SignatureKeyPair` from xmtp_basic_credential
309        let keypair = SignatureKeyPair::new(SignatureScheme::ED25519).unwrap();
310        let serialized: Vec<u8> = bincode::serialize(&keypair).unwrap();
311
312        let x_kp: XmtpInstallationCredential = bincode::deserialize(serialized.as_slice()).unwrap();
313        assert_eq!(keypair.private(), &x_kp.0.to_bytes());
314        assert_eq!(keypair.public(), &x_kp.0.verifying_key().to_bytes());
315    }
316
317    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
318    #[cfg_attr(not(target_arch = "wasm32"), test)]
319    fn test_is_binary_compatible_with_mls_ser_serde() {
320        let keypair = XmtpInstallationCredential::new();
321        let serialized: Vec<u8> = bincode::serialize(&keypair).unwrap();
322
323        let mls_kp: SignatureKeyPair = bincode::deserialize(serialized.as_slice()).unwrap();
324        assert_eq!(mls_kp.private(), &keypair.0.to_bytes());
325        assert_eq!(mls_kp.public(), &keypair.0.verifying_key().to_bytes());
326        assert_eq!(mls_kp.signature_scheme(), keypair.scheme());
327    }
328
329    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
330    #[cfg_attr(not(target_arch = "wasm32"), test)]
331    fn secret_key_can_not_be_exposed() {
332        let keypair = XmtpInstallationCredential::new();
333        let secret = keypair.0.as_ref();
334
335        assert_ne!(keypair.public_bytes(), secret.as_bytes());
336        assert_ne!(keypair.public_slice(), secret.as_bytes());
337        assert_ne!(keypair.verifying_key().as_bytes(), &secret.to_bytes());
338    }
339}