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}