Skip to main content

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::new()
81    }
82}
83
84impl XmtpInstallationCredential {
85    /// Create a new [`XmtpInstallationCredential`] with [`rand_chacha::ChaCha20Rng`]
86    pub fn new() -> Self {
87        let seed = Zeroizing::new(crate::rand::rand_array());
88        Self(Box::new(SigningKey::from_bytes(&seed)))
89    }
90
91    /// Get a reference to the public [`ed25519_dalek::VerifyingKey`]
92    /// Can be used to verify signatures
93    pub fn verifying_key(&self) -> ed25519_dalek::VerifyingKey {
94        self.0.verifying_key()
95    }
96
97    /// View the public [`ed25519_dalek::VerifyingKey`] as constant-sized bytes
98    pub fn public_bytes(&self) -> &[u8; 32] {
99        self.0.as_ref().as_ref().as_bytes()
100    }
101
102    /// View the public [`ed25519_dalek::VerifyingKey`] as a slice
103    pub fn public_slice(&self) -> &[u8] {
104        self.0.as_ref().as_ref().as_ref()
105    }
106
107    /// get the scheme, prefer the public [`Signer::signature_scheme`]
108    fn scheme(&self) -> SignatureScheme {
109        SignatureScheme::ED25519
110    }
111
112    pub fn with_context<'k, 'v>(
113        &'k self,
114        context: &'v [u8],
115    ) -> Result<ed25519_dalek::Context<'k, 'v, SigningKey>, ed25519_dalek::SignatureError> {
116        self.0.with_context(context)
117    }
118
119    /// Internal helper function to safely create a credential from its raw parts.
120    /// Keys of any length other than 32 bytes are an error.
121    fn from_raw(private: &[u8], public: &[u8]) -> Result<Self, ed25519_dalek::SignatureError> {
122        const KEY_LENGTH: usize = 32;
123        if private.len() != KEY_LENGTH || public.len() != KEY_LENGTH {
124            return Err(ed25519_dalek::SignatureError::new());
125        }
126        let keypair = Zeroizing::new({
127            let mut keypair = [0u8; 64];
128            keypair[0..32].copy_from_slice(private);
129            keypair[32..].copy_from_slice(public);
130            keypair
131        });
132
133        let signing_key = SigningKey::from_keypair_bytes(&keypair)?;
134        Ok(Self(Box::new(signing_key)))
135    }
136
137    /// Alias for [`ed25519_dalek::SigningKey::from_bytes`]
138    pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, ed25519_dalek::SignatureError> {
139        let key = SigningKey::from_bytes(bytes);
140        Ok(Self(Box::new(key)))
141    }
142
143    /// private key for this credential, wiped when the returned value drops
144    #[cfg(feature = "exposed-keys")]
145    pub fn private_bytes(&self) -> Zeroizing<[u8; 32]> {
146        Zeroizing::new(*self.0.as_bytes())
147    }
148}
149
150impl From<XmtpInstallationCredential> for SignaturePublicKey {
151    fn from(value: XmtpInstallationCredential) -> Self {
152        SignaturePublicKey::from(value.verifying_key().as_ref())
153    }
154}
155
156/// Signs with the held key and returns the same bytes as `SignatureKeyPair::sign`:
157/// OpenMLS's Ed25519 signer makes the same deterministic `ed25519_dalek` call.
158impl Signer for XmtpInstallationCredential {
159    fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, signatures::SignerError> {
160        Ok(ed25519_dalek::Signer::sign(&*self.0, payload)
161            .to_bytes()
162            .to_vec())
163    }
164
165    fn signature_scheme(&self) -> SignatureScheme {
166        self.scheme()
167    }
168}
169
170// The signer here must maintain compatibility with `SignatureKeyPair`
171impl Signer for &XmtpInstallationCredential {
172    fn sign(&self, payload: &[u8]) -> Result<Vec<u8>, signatures::SignerError> {
173        <XmtpInstallationCredential as Signer>::sign(self, payload)
174    }
175
176    fn signature_scheme(&self) -> SignatureScheme {
177        self.scheme()
178    }
179}
180
181impl tls_codec::Deserialize for XmtpInstallationCredential {
182    fn tls_deserialize<R: std::io::Read>(bytes: &mut R) -> Result<Self, tls_codec::Error>
183    where
184        Self: Sized,
185    {
186        // a bufreader consumes its input, unlike just a `Read` instance.
187        let mut buf = BufReader::new(bytes);
188        let private = SecretTlsVecU8::tls_deserialize(&mut buf)?;
189        let public = SecretTlsVecU8::tls_deserialize(&mut buf)?;
190        let scheme = SignatureScheme::tls_deserialize(&mut buf)?;
191        if scheme != SignatureScheme::ED25519 {
192            return Err(tls_codec::Error::DecodingError(
193                "XMTP InstallationCredential must be Ed25519".into(),
194            ));
195        }
196
197        Self::from_raw(private.as_slice(), public.as_slice())
198            .map_err(|e| tls_codec::Error::DecodingError(e.to_string()))
199    }
200}
201
202impl From<XmtpInstallationCredential> for SignatureKeyPair {
203    fn from(key: XmtpInstallationCredential) -> SignatureKeyPair {
204        SignatureKeyPair::from_raw(
205            key.signature_scheme(),
206            key.0.as_bytes().to_vec(),
207            key.0.verifying_key().to_bytes().into(),
208        )
209    }
210}
211
212impl<'a> From<&'a XmtpInstallationCredential> for SignatureKeyPair {
213    fn from(key: &'a XmtpInstallationCredential) -> SignatureKeyPair {
214        SignatureKeyPair::from_raw(
215            key.signature_scheme(),
216            key.0.as_bytes().to_vec(),
217            key.0.verifying_key().to_bytes().into(),
218        )
219    }
220}
221
222impl From<SigningKey> for XmtpInstallationCredential {
223    fn from(signing_key: SigningKey) -> Self {
224        Self(Box::new(signing_key))
225    }
226}
227
228impl<'a> From<&'a SigningKey> for XmtpInstallationCredential {
229    fn from(signing_key: &'a SigningKey) -> Self {
230        Self(Box::new(signing_key.clone()))
231    }
232}
233
234impl tls_codec::Serialize for XmtpInstallationCredential {
235    fn tls_serialize<W: std::io::Write>(&self, writer: &mut W) -> Result<usize, tls_codec::Error> {
236        SignatureKeyPair::from(self).tls_serialize(writer)
237    }
238}
239
240impl tls_codec::Size for XmtpInstallationCredential {
241    fn tls_serialized_len(&self) -> usize {
242        SignatureKeyPair::from(self).tls_serialized_len()
243    }
244}
245
246impl serde::Serialize for XmtpInstallationCredential {
247    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248    where
249        S: serde::Serializer,
250    {
251        SignatureKeyPair::from(self).serialize(serializer)
252    }
253}
254
255impl<'de> serde::Deserialize<'de> for XmtpInstallationCredential {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: serde::Deserializer<'de>,
259    {
260        // `private` is a `Secret`, so it is wiped on drop also when a later
261        // field fails to decode and the struct is never built. The row keeps
262        // the serde shape of OpenMLS's `SignatureKeyPair`, which this type
263        // serializes through, so the key is a sequence of `u8`.
264        #[derive(serde::Deserialize, zeroize::ZeroizeOnDrop)]
265        struct SignatureKeyPairRemote {
266            #[serde(deserialize_with = "crate::secret::deserialize_seq")]
267            private: crate::Secret,
268            public: Vec<u8>,
269            #[allow(dead_code)]
270            #[zeroize(skip)]
271            // `SignatureKeyPair` names this field `signature_scheme`. Codecs
272            // that store field names need the same name.
273            #[serde(rename = "signature_scheme")]
274            _signature_scheme: SignatureScheme,
275        }
276
277        let SignatureKeyPairRemote {
278            ref private,
279            ref public,
280            ..
281        } = SignatureKeyPairRemote::deserialize(deserializer)?;
282
283        Self::from_raw(private.as_slice(), public.as_slice())
284            .map_err(|e| <D as serde::Deserializer<'_>>::Error::custom(format!("{e}")))
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use tls_codec::{Deserialize as _, Serialize as _};
292
293    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
294    #[cfg_attr(not(target_arch = "wasm32"), test)]
295    fn test_is_binary_compatible_with_mls_deser() {
296        // XmtpInstallationCredential needs to be binary-compatible/tls-codec/serde compatible with
297        // `SignatureKeyPair` from xmtp_basic_credential
298        let keypair = SignatureKeyPair::new(SignatureScheme::ED25519).unwrap();
299        let mut serialized: Vec<u8> = Vec::new();
300
301        keypair.tls_serialize(&mut serialized).unwrap();
302        let x_kp = XmtpInstallationCredential::tls_deserialize(&mut serialized.as_slice()).unwrap();
303        assert_eq!(keypair.private(), x_kp.0.as_bytes());
304        assert_eq!(keypair.public(), &x_kp.0.verifying_key().to_bytes());
305    }
306
307    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
308    #[cfg_attr(not(target_arch = "wasm32"), test)]
309    fn test_is_binary_compatible_with_mls_ser() {
310        let keypair = XmtpInstallationCredential::new();
311        let mut serialized: Vec<u8> = Vec::new();
312
313        keypair.tls_serialize(&mut serialized).unwrap();
314        let mls_kp = SignatureKeyPair::tls_deserialize(&mut serialized.as_slice()).unwrap();
315        assert_eq!(mls_kp.private(), keypair.0.as_bytes());
316        assert_eq!(mls_kp.public(), &keypair.0.verifying_key().to_bytes());
317        assert_eq!(mls_kp.signature_scheme(), keypair.scheme());
318    }
319
320    /// The client database stores installation keys as CBOR. CBOR stores
321    /// field names, so this also pins the `signature_scheme` rename.
322    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
323    #[cfg_attr(not(target_arch = "wasm32"), test)]
324    fn test_is_binary_compatible_with_mls_deser_serde() {
325        // XmtpInstallationCredential needs to be serde compatible with
326        // `SignatureKeyPair` from xmtp_basic_credential
327        let keypair = SignatureKeyPair::new(SignatureScheme::ED25519).unwrap();
328        let serialized: Vec<u8> = cbor4ii::serde::to_vec(Vec::new(), &keypair).unwrap();
329
330        let x_kp: XmtpInstallationCredential =
331            cbor4ii::serde::from_slice(serialized.as_slice()).unwrap();
332        assert_eq!(keypair.private(), x_kp.0.as_bytes());
333        assert_eq!(keypair.public(), &x_kp.0.verifying_key().to_bytes());
334    }
335
336    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
337    #[cfg_attr(not(target_arch = "wasm32"), test)]
338    fn test_is_binary_compatible_with_mls_ser_serde() {
339        let keypair = XmtpInstallationCredential::new();
340        let serialized: Vec<u8> = cbor4ii::serde::to_vec(Vec::new(), &keypair).unwrap();
341
342        let mls_kp: SignatureKeyPair = cbor4ii::serde::from_slice(serialized.as_slice()).unwrap();
343        assert_eq!(mls_kp.private(), keypair.0.as_bytes());
344        assert_eq!(mls_kp.public(), &keypair.0.verifying_key().to_bytes());
345        assert_eq!(mls_kp.signature_scheme(), keypair.scheme());
346    }
347
348    /// A stored key pair whose private or public key is not 32 bytes is a
349    /// decode error, not a panic.
350    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
351    #[cfg_attr(not(target_arch = "wasm32"), test)]
352    fn wrong_key_lengths_are_decode_errors() {
353        for (private, public) in [
354            (vec![1; 31], vec![2; 32]),
355            (vec![1; 33], vec![2; 32]),
356            (vec![1; 32], vec![2; 31]),
357            (vec![1; 32], vec![2; 33]),
358        ] {
359            let keypair = SignatureKeyPair::from_raw(SignatureScheme::ED25519, private, public);
360            let bytes = cbor4ii::serde::to_vec(Vec::new(), &keypair).unwrap();
361            assert!(cbor4ii::serde::from_slice::<XmtpInstallationCredential>(&bytes).is_err());
362        }
363    }
364
365    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
366    #[cfg_attr(not(target_arch = "wasm32"), test)]
367    fn signs_like_signature_key_pair() {
368        let payloads: [&[u8]; 3] = [b"", b"an mls payload", &[0xa5; 1024]];
369        for seed in [[0x01u8; 32], [0x7f; 32], [0xc3; 32]] {
370            let credential = XmtpInstallationCredential::from_bytes(&seed).unwrap();
371            let key_pair = SignatureKeyPair::from(&credential);
372            let owned = SignatureKeyPair::from(credential.clone());
373            for pair in [&key_pair, &owned] {
374                assert_eq!(pair.private(), seed);
375                assert_eq!(pair.public(), credential.public_slice());
376            }
377            for payload in payloads {
378                let expected = key_pair.sign(payload).unwrap();
379                assert_eq!(Signer::sign(&credential, payload).unwrap(), expected);
380                assert_eq!(Signer::sign(&&credential, payload).unwrap(), expected);
381            }
382        }
383    }
384
385    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
386    #[cfg_attr(not(target_arch = "wasm32"), test)]
387    fn secret_key_can_not_be_exposed() {
388        let keypair = XmtpInstallationCredential::new();
389        let secret = keypair.0.as_ref();
390
391        assert_ne!(keypair.public_bytes(), secret.as_bytes());
392        assert_ne!(keypair.public_slice(), secret.as_bytes());
393        assert_ne!(keypair.verifying_key().as_bytes(), secret.as_bytes());
394    }
395}