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}