Skip to main content

offline_protocol_leaf/
identity.rs

1//! The device's long-term identity, and the MLS client built on it.
2//!
3//! A leaf node holds one Ed25519 keypair, generated once at provisioning. It
4//! signs control frames, it derives the device's address, and it is the
5//! signature key inside the device's MLS credential, which is the same three
6//! jobs one key does on a phone.
7//!
8//! # One device, one key
9//!
10//! A fleet provisioned with a shared identity key is one identity on many
11//! devices. Extracting it from a single unit in a laboratory then yields every
12//! unit's identity, and the address that names one lock names all of them.
13//! This crate cannot prevent a manufacturer from doing that, so it is written
14//! down here and in the
15//! [threat model](https://github.com/Offline-Protocol/offline-protocol-sdk/blob/main/docs/security/threat-model.md)
16//! as R12.
17//!
18//! # Where the randomness comes from
19//!
20//! [`LeafDevice::provision`](crate::LeafDevice::provision) draws from the
21//! `getrandom` backend the firmware registers, and the key is exactly as
22//! strong as what that returns. This crate deliberately registers no backend
23//! of its own: doing so would let a device link and run with entropy this
24//! crate invented, which is the one failure that leaves no trace anywhere.
25
26use alloc::{format, string::ToString, sync::Arc, vec, vec::Vec};
27use mls_rs::client_builder::MlsConfig;
28use mls_rs::identity::basic::{BasicCredential, BasicIdentityProvider};
29use mls_rs::identity::SigningIdentity;
30use mls_rs::{CipherSuite, CipherSuiteProvider, Client, CryptoProvider};
31use mls_rs_core::crypto::{SignaturePublicKey, SignatureSecretKey};
32use mls_rs_crypto_rustcrypto::RustCryptoProvider;
33use offline_protocol_core::Address;
34use offline_protocol_sealed::{derive_address, LEAF_KEY_PACKAGE_LIFETIME};
35
36use crate::adapters::{GroupStateAdapter, KeyPackageAdapter};
37use crate::error::{LeafError, Result};
38use crate::store::{LeafStore, KEY_TYPE_IDENTITY};
39
40/// The one ciphersuite this protocol uses, and never negotiates.
41///
42/// `MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`, ciphersuite 3. The phone
43/// pins the same suite in one place under its own MLS implementation's
44/// spelling, and the two names have to mean the same number.
45pub const CIPHERSUITE: CipherSuite = CipherSuite::CURVE25519_AES128;
46
47const KEY_ID_SECRET: &str = "signature_secret";
48const KEY_ID_PUBLIC: &str = "signature_public";
49
50/// A provisioned device's signing identity.
51#[derive(Clone)]
52pub(crate) struct Identity {
53    pub(crate) secret: SignatureSecretKey,
54    pub(crate) public: SignaturePublicKey,
55    pub(crate) address: Address,
56}
57
58impl Identity {
59    /// Generates a fresh identity and writes it before returning it.
60    ///
61    /// The write comes first for the same reason every other write in this
62    /// crate does: a device that hands out an address it did not persist comes
63    /// back after a power cut as a different device, and the peer that paired
64    /// with the first one has no way to learn that.
65    ///
66    /// # Why the secret is written last
67    ///
68    /// An identity is two entries and the store is atomic per entry, not
69    /// across a pair, so a cut lands between them. The secret is therefore
70    /// both the last write and the marker this function refuses on, which
71    /// makes a half-written identity a state the next boot **overwrites**
72    /// rather than one it is stuck in.
73    ///
74    /// Written the other way round the two checks disagree: `resume` refuses
75    /// for the missing public key, `provision` refuses for the present secret,
76    /// and `open` has no third door. A device that lost power once during its
77    /// very first boot would then answer every call with an error, in the
78    /// field, with nothing short of an out-of-band wipe to recover it.
79    pub(crate) fn provision(store: &Arc<dyn LeafStore>) -> Result<Self> {
80        if store
81            .load(KEY_TYPE_IDENTITY, KEY_ID_SECRET)
82            .map_err(|e| LeafError::Storage(e.to_string()))?
83            .is_some()
84        {
85            return Err(LeafError::AlreadyProvisioned);
86        }
87
88        let suite = suite_provider()?;
89        let (secret, public) = suite
90            .signature_key_generate()
91            .map_err(|e| LeafError::Crypto(format!("cannot generate a signature key: {e:?}")))?;
92
93        store
94            .store(KEY_TYPE_IDENTITY, KEY_ID_PUBLIC, public.as_bytes())
95            .map_err(|e| LeafError::Storage(e.to_string()))?;
96        store
97            .store(KEY_TYPE_IDENTITY, KEY_ID_SECRET, secret.as_bytes())
98            .map_err(|e| LeafError::Storage(e.to_string()))?;
99
100        let address = derive_address(public.as_bytes())?;
101        Ok(Self {
102            secret,
103            public,
104            address,
105        })
106    }
107
108    /// Loads a previously provisioned identity.
109    ///
110    /// Both entries are required, which is safe only because
111    /// [`Identity::provision`] writes the public key first: the pair is either
112    /// complete or missing the secret, and the second of those is what `open`
113    /// recovers from. Swapping those two writes makes this function the half
114    /// of a deadlock.
115    ///
116    /// # Why the pair is checked rather than trusted
117    ///
118    /// The two entries are stored separately, and the whole reason this crate
119    /// has a durability contract is that a part can hand back something other
120    /// than what was written. A device that resumed on a public key its secret
121    /// does not derive to would take an address no peer knows it by and sign
122    /// frames that verify nowhere: every gate in this protocol refuses it, and
123    /// every one of them names a different failure than the one that happened,
124    /// which is a bench chasing a pairing problem that is really a flash
125    /// problem. One scalar multiplication per boot buys the right error.
126    pub(crate) fn resume(store: &Arc<dyn LeafStore>) -> Result<Self> {
127        let secret = store
128            .load(KEY_TYPE_IDENTITY, KEY_ID_SECRET)
129            .map_err(|e| LeafError::Storage(e.to_string()))?
130            .ok_or(LeafError::NotProvisioned)?;
131        let public = store
132            .load(KEY_TYPE_IDENTITY, KEY_ID_PUBLIC)
133            .map_err(|e| LeafError::Storage(e.to_string()))?
134            .ok_or(LeafError::NotProvisioned)?;
135
136        let secret = SignatureSecretKey::from(secret);
137        let public = SignaturePublicKey::from(public);
138
139        let derived = suite_provider()?
140            .signature_key_derive_public(&secret)
141            .map_err(|e| {
142                LeafError::Crypto(format!("cannot derive a public key from the secret: {e:?}"))
143            })?;
144        if derived.as_bytes() != public.as_bytes() {
145            return Err(LeafError::Storage(alloc::string::String::from(
146                "the stored public key is not the one this device's secret derives to",
147            )));
148        }
149
150        let address = derive_address(public.as_bytes())?;
151        Ok(Self {
152            secret,
153            public,
154            address,
155        })
156    }
157
158    /// The credential this device presents inside MLS.
159    ///
160    /// A basic credential whose whole content is the device's address in
161    /// canonical text. RFC 9420 calls a basic credential "a bare assertion of
162    /// an identity", which is exactly what it is: the assertion means
163    /// something only because the address is the hash of the signature key
164    /// beside it, so a verifier re-derives rather than trusts.
165    pub(crate) fn signing_identity(&self) -> SigningIdentity {
166        let credential = BasicCredential::new(self.address.to_string().into_bytes());
167        SigningIdentity::new(credential.into_credential(), self.public.clone())
168    }
169}
170
171/// The crypto provider, restricted to the one suite.
172///
173/// `with_enabled_cipher_suites` is a runtime filter rather than a compile-time
174/// one: the provider keeps all four curves in one enum, so P-384 and P-256
175/// arithmetic link into the image whatever this says. Restricting it is still
176/// right, because it makes a request for another suite fail here rather than
177/// succeed with a suite the peer does not speak.
178pub(crate) fn crypto_provider() -> RustCryptoProvider {
179    RustCryptoProvider::with_enabled_cipher_suites(vec![CIPHERSUITE])
180}
181
182/// The suite provider, for the raw sign and verify a control frame needs.
183pub(crate) fn suite_provider() -> Result<<RustCryptoProvider as CryptoProvider>::CipherSuiteProvider>
184{
185    crypto_provider()
186        .cipher_suite_provider(CIPHERSUITE)
187        .ok_or_else(|| {
188            LeafError::Crypto(alloc::string::String::from(
189                "the crypto provider does not support the protocol's ciphersuite",
190            ))
191        })
192}
193
194/// Builds the MLS client for this device.
195///
196/// Storage is the device's own, through the adapters, so the client reads and
197/// writes the same blobs across a power cycle. Nothing about the group lives
198/// in this value: it is rebuilt per operation and the group is loaded from
199/// storage, which is what makes storage the source of truth rather than a
200/// cache of something held in RAM.
201pub(crate) fn build_client(
202    identity: &Identity,
203    store: &Arc<dyn LeafStore>,
204) -> Result<Client<impl MlsConfig>> {
205    Ok(Client::builder()
206        .identity_provider(BasicIdentityProvider)
207        .crypto_provider(crypto_provider())
208        .group_state_storage(GroupStateAdapter::new(Arc::clone(store)))
209        .key_package_repo(KeyPackageAdapter::new(Arc::clone(store)))
210        .signing_identity(
211            identity.signing_identity(),
212            identity.secret.clone(),
213            CIPHERSUITE,
214        )
215        .key_package_lifetime(LEAF_KEY_PACKAGE_LIFETIME)
216        .build())
217}
218
219/// Signs `payload` with the device's identity key.
220pub(crate) fn sign(identity: &Identity, payload: &[u8]) -> Result<Vec<u8>> {
221    suite_provider()?
222        .sign(&identity.secret, payload)
223        .map_err(|e| LeafError::Crypto(format!("cannot sign: {e:?}")))
224}
225
226/// Verifies `signature` over `payload` under `public_key`.
227pub(crate) fn verify(public_key: &[u8], signature: &[u8], payload: &[u8]) -> Result<()> {
228    let key = SignaturePublicKey::from(public_key.to_vec());
229    suite_provider()?
230        .verify(&key, signature, payload)
231        .map_err(|e| LeafError::ControlFrameRefused(format!("signature does not verify: {e:?}")))
232}