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