oasis_core_runtime/
identity.rs

1//! Runtime attestation key handling.
2use std::{
3    collections::VecDeque,
4    sync::{Arc, RwLock},
5};
6
7use anyhow::Result;
8use base64::prelude::*;
9use rand::{rngs::OsRng, Rng};
10use sgx_isa::Targetinfo;
11use thiserror::Error;
12use tiny_keccak::{Hasher, TupleHash};
13
14use crate::{
15    common::{
16        crypto::{
17            hash::Hash,
18            mrae::deoxysii::{self, Opener},
19            signature::{self, Signature, Signer},
20            x25519,
21        },
22        sgx::{self, EnclaveIdentity, Quote, QuotePolicy, VerifiedQuote},
23        time::insecure_posix_time,
24    },
25    consensus::registry::EndorsedCapabilityTEE,
26    TeeType, BUILD_INFO,
27};
28
29/// Context used for computing the RAK digest.
30const RAK_HASH_CONTEXT: &[u8] = b"oasis-core/node: TEE RAK binding";
31/// Context used for deriving the nonce used in quotes.
32const QUOTE_NONCE_CONTEXT: &[u8] = b"oasis-core/node: TEE quote nonce";
33
34/// A dummy RAK seed for use in non-SGX tests where integrity is not needed.
35const INSECURE_RAK_SEED: &str = "ekiden test key manager RAK seed";
36/// A dummy REK seed for use in non-SGX tests where confidentiality is not needed.
37const INSECURE_REK_SEED: &str = "ekiden test key manager REK seed";
38
39/// Identity-related error.
40#[derive(Error, Debug)]
41enum IdentityError {
42    #[error("RAK binding mismatch")]
43    BindingMismatch,
44    #[error("malformed report data")]
45    MalformedReportData,
46}
47
48/// Quote-related errors.
49#[derive(Error, Debug)]
50enum QuoteError {
51    #[error("target info not set")]
52    TargetInfoNotSet,
53    #[error("malformed target_info")]
54    MalformedTargetInfo,
55    #[error("MRENCLAVE mismatch")]
56    MrEnclaveMismatch,
57    #[error("MRSIGNER mismatch")]
58    MrSignerMismatch,
59    #[error("quote nonce mismatch")]
60    NonceMismatch,
61    #[error("quote policy not set")]
62    QuotePolicyNotSet,
63    #[error("node identity not set")]
64    NodeIdentityNotSet,
65    #[error("endorsed quote mismatch")]
66    EndorsedQuoteMismatch,
67}
68
69struct Inner {
70    rak: signature::PrivateKey,
71    rek: x25519::PrivateKey,
72    quote: Option<Arc<Quote>>,
73    quote_timestamp: Option<i64>,
74    quote_policy: Option<Arc<QuotePolicy>>,
75    known_quotes: VecDeque<Arc<Quote>>,
76    enclave_identity: Option<EnclaveIdentity>,
77    node_identity: Option<signature::PublicKey>,
78    endorsed_capability_tee: Option<EndorsedCapabilityTEE>,
79    target_info: Option<Targetinfo>,
80    nonce: Option<[u8; 32]>,
81}
82
83/// Runtime identity.
84///
85/// The identity can be used to sign remote attestations with runtime
86/// attestation key (RAK) or to decrypt ciphertexts sent to the enclave
87/// with runtime encryption key (REK). RAK avoids round trips to IAS/PCS
88/// for each verification as the verifier can instead verify the RAK signature
89/// and the signature on the provided quote which binds RAK to the enclave.
90/// REK allows enclaves to publish encrypted data on-chain to an enclave
91/// instance.
92pub struct Identity {
93    inner: RwLock<Inner>,
94}
95
96impl Default for Identity {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102impl Identity {
103    /// Create an uninitialized runtime identity.
104    pub fn new() -> Self {
105        let (rak, rek) = match BUILD_INFO.tee_type {
106            TeeType::None => {
107                // Use insecure mock keys for insecure non-TEE builds.
108                assert!(!BUILD_INFO.is_secure);
109
110                (
111                    signature::PrivateKey::from_test_seed(INSECURE_RAK_SEED.to_string()),
112                    x25519::PrivateKey::from_test_seed(INSECURE_REK_SEED.to_string()),
113                )
114            }
115            _ => {
116                // Generate ephemeral RAK and REK.
117                (
118                    signature::PrivateKey::generate(),
119                    x25519::PrivateKey::generate(),
120                )
121            }
122        };
123
124        Self {
125            inner: RwLock::new(Inner {
126                rak,
127                rek,
128                quote: None,
129                quote_timestamp: None,
130                quote_policy: None,
131                known_quotes: Default::default(),
132                enclave_identity: EnclaveIdentity::current(),
133                node_identity: None,
134                endorsed_capability_tee: None,
135                target_info: None,
136                nonce: None,
137            }),
138        }
139    }
140
141    /// Generate report body = H(RAK_HASH_CONTEXT || RAK_pub).
142    fn report_body_for_rak(rak: &signature::PublicKey) -> Hash {
143        let mut message = [0; 64];
144        message[0..32].copy_from_slice(RAK_HASH_CONTEXT);
145        message[32..64].copy_from_slice(rak.as_ref());
146        Hash::digest_bytes(&message)
147    }
148
149    /// Generate a random 256-bit nonce, for anti-replay.
150    fn generate_nonce() -> [u8; 32] {
151        let mut nonce_bytes = [0u8; 32];
152        OsRng.fill(&mut nonce_bytes);
153
154        let mut h = TupleHash::v256(QUOTE_NONCE_CONTEXT);
155        h.update(&nonce_bytes);
156        h.finalize(&mut nonce_bytes);
157
158        nonce_bytes
159    }
160
161    /// Get the SGX target info.
162    fn get_sgx_target_info(&self) -> Option<Targetinfo> {
163        let inner = self.inner.read().unwrap();
164        inner.target_info.clone()
165    }
166
167    /// Initialize the SGX target info.
168    pub(crate) fn init_target_info(&self, target_info: Vec<u8>) -> Result<()> {
169        match BUILD_INFO.tee_type {
170            TeeType::Sgx => {
171                let mut inner = self.inner.write().unwrap();
172
173                // Set the Quoting Enclave target_info first, as unlike key generation
174                // it can fail.
175                let target_info = match Targetinfo::try_copy_from(&target_info) {
176                    Some(target_info) => target_info,
177                    None => return Err(QuoteError::MalformedTargetInfo.into()),
178                };
179                inner.target_info = Some(target_info);
180
181                Ok(())
182            }
183            TeeType::Tdx => {
184                // Target info configuration is not needed on TDX and MUST be empty.
185                if !target_info.is_empty() {
186                    return Err(QuoteError::MalformedTargetInfo.into());
187                }
188
189                Ok(())
190            }
191            TeeType::None => Ok(()),
192        }
193    }
194
195    /// Initialize the attestation report.
196    pub(crate) fn init_report(
197        &self,
198    ) -> Result<(signature::PublicKey, x25519::PublicKey, Vec<u8>, String)> {
199        let rak_pub = self.public_rak();
200        let rek_pub = self.public_rek();
201
202        // Generate a new anti-replay nonce.
203        let nonce = Self::generate_nonce();
204        // Generate report body.
205        let report_body = Self::report_body_for_rak(&rak_pub);
206        let mut report_data = [0; 64];
207        report_data[0..32].copy_from_slice(report_body.as_ref());
208        report_data[32..64].copy_from_slice(nonce.as_ref());
209
210        let result = match BUILD_INFO.tee_type {
211            TeeType::Sgx => {
212                let target_info = self
213                    .get_sgx_target_info()
214                    .ok_or(QuoteError::TargetInfoNotSet)?;
215
216                // The derived nonce is only used in case IAS-based attestation is used
217                // as it is included in the outer AVR envelope. But given that the body
218                // also includes the nonce in our specific case, this is not relevant.
219                let quote_nonce = BASE64_STANDARD.encode(&nonce[..24]);
220
221                let report = sgx::report_for(&target_info, &report_data);
222                let report: &[u8] = report.as_ref();
223                let report = report.to_vec();
224
225                // This used to reset the quote, but that is now done in the external
226                // accessor combined with a freshness check.
227
228                (rak_pub, rek_pub, report, quote_nonce)
229            }
230            #[cfg(feature = "tdx")]
231            TeeType::Tdx => {
232                // In TDX we can immediately generate a quote. Do it and return it as a "report".
233                let quote = crate::common::tdx::report::get_quote(&report_data)?;
234
235                (rak_pub, rek_pub, quote, String::new())
236            }
237            _ => panic!("init_report called outside TEE environment"),
238        };
239
240        // Cache the nonce, the report was generated.
241        let mut inner = self.inner.write().unwrap();
242        inner.nonce = Some(nonce);
243
244        Ok(result)
245    }
246
247    /// Configure the remote attestation quote for RAK.
248    pub(crate) fn set_quote(
249        &self,
250        node_id: signature::PublicKey,
251        quote: Quote,
252    ) -> Result<VerifiedQuote> {
253        let rak_pub = self.public_rak();
254
255        let mut inner = self.inner.write().unwrap();
256
257        // If there is no anti-replay nonce set, we aren't in the process of attesting.
258        let expected_nonce = match &inner.nonce {
259            Some(nonce) => *nonce,
260            None => return Err(QuoteError::NonceMismatch.into()),
261        };
262
263        // Verify that the quote's nonce matches one that we generated,
264        // and remove it.  If the validation fails for any reason, we
265        // should not accept a new quote with the same nonce as a quote
266        // that failed.
267        inner.nonce = None;
268
269        let policy = inner
270            .quote_policy
271            .as_ref()
272            .ok_or(QuoteError::QuotePolicyNotSet)?;
273        let verified_quote = quote.verify(policy)?;
274        let nonce = &verified_quote.report_data[32..];
275        if expected_nonce.as_ref() != nonce {
276            return Err(QuoteError::NonceMismatch.into());
277        }
278
279        // Verify that the quote's enclave identity matches our own.
280        let enclave_identity = inner
281            .enclave_identity
282            .as_ref()
283            .expect("Enclave identity must be configured");
284        if verified_quote.identity.mr_enclave != enclave_identity.mr_enclave {
285            return Err(QuoteError::MrEnclaveMismatch.into());
286        }
287        if verified_quote.identity.mr_signer != enclave_identity.mr_signer {
288            return Err(QuoteError::MrSignerMismatch.into());
289        }
290
291        // Verify that the quote has H(RAK) in report body.
292        Self::verify_binding(&verified_quote, &rak_pub)?;
293
294        // If there is an existing quote that is dated more recently than
295        // the one being set, silently ignore the update.
296        if inner.quote.is_some() {
297            let existing_timestamp = inner.quote_timestamp.unwrap();
298            if existing_timestamp > verified_quote.timestamp {
299                return Ok(verified_quote);
300            }
301        }
302
303        // Ensure host identity cannot change.
304        match inner.node_identity {
305            Some(existing_node_id) if node_id != existing_node_id => {
306                panic!("host node identity may never change");
307            }
308            Some(_) => {} // Host identity already set and is the same.
309            None => inner.node_identity = Some(node_id),
310        }
311
312        let quote = Arc::new(quote);
313        inner.quote = Some(quote.clone());
314        inner.quote_timestamp = Some(verified_quote.timestamp);
315
316        // Keep around last two valid quotes to allow for transition as node registration does not
317        // happen immediately after a quote has been verified by the runtime.
318        inner.known_quotes.push_back(quote);
319        if inner.known_quotes.len() > 2 {
320            inner.known_quotes.pop_front();
321        }
322
323        Ok(verified_quote)
324    }
325
326    /// Configure the runtime quote policy.
327    pub(crate) fn set_quote_policy(&self, policy: QuotePolicy) -> Result<()> {
328        let mut inner = self.inner.write().unwrap();
329        inner.quote_policy = Some(Arc::new(policy));
330
331        Ok(())
332    }
333
334    /// Configure the endorsed TEE capability.
335    pub(crate) fn set_endorsed_capability_tee(&self, ect: EndorsedCapabilityTEE) -> Result<()> {
336        // Make sure the endorsed quote is actually ours.
337        if !ect.capability_tee.matches(self) {
338            return Err(QuoteError::EndorsedQuoteMismatch.into());
339        }
340
341        let mut inner = self.inner.write().unwrap();
342        let policy = inner
343            .quote_policy
344            .as_ref()
345            .ok_or(QuoteError::QuotePolicyNotSet)?;
346        let node_id = inner.node_identity.ok_or(QuoteError::NodeIdentityNotSet)?;
347
348        // Verify the endorsed capability TEE to make sure it matches our state.
349        if ect.node_endorsement.public_key != node_id {
350            return Err(QuoteError::EndorsedQuoteMismatch.into());
351        }
352        ect.verify(policy)?;
353
354        inner.endorsed_capability_tee = Some(ect);
355
356        Ok(())
357    }
358
359    /// Endorsed TEE capability.
360    pub fn endorsed_capability_tee(&self) -> Option<EndorsedCapabilityTEE> {
361        let inner = self.inner.read().unwrap();
362        inner.endorsed_capability_tee.clone()
363    }
364
365    /// Host node identity public key.
366    pub fn node_identity(&self) -> Option<signature::PublicKey> {
367        let inner = self.inner.read().unwrap();
368        inner.node_identity
369    }
370
371    /// Public part of RAK.
372    ///
373    /// This method will return an insecure test key in the case where
374    /// the enclave is not running on SGX hardware.
375    pub fn public_rak(&self) -> signature::PublicKey {
376        let inner = self.inner.read().unwrap();
377        inner.rak.public_key()
378    }
379
380    /// Public part of REK.
381    ///
382    /// This method will return an insecure test key in the case where
383    /// the enclave is not running on SGX hardware.
384    pub fn public_rek(&self) -> x25519::PublicKey {
385        let inner = self.inner.read().unwrap();
386        inner.rek.public_key()
387    }
388
389    /// Quote for RAK.
390    ///
391    /// This method may return `None` in case quote has not yet been set from
392    /// the outside, or if the quote has expired.
393    pub fn quote(&self) -> Option<Arc<Quote>> {
394        let now = insecure_posix_time();
395
396        // Enforce quote expiration.
397        let mut inner = self.inner.write().unwrap();
398        if inner.quote.is_some() {
399            let quote = inner.quote.as_ref().unwrap();
400            let timestamp = inner.quote_timestamp.unwrap();
401            let quote_policy = inner.quote_policy.as_ref().unwrap();
402
403            if !quote.is_fresh(now, timestamp, quote_policy) {
404                // Reset the quote.
405                inner.quote = None;
406                inner.quote_timestamp = None;
407                inner.quote_policy = None;
408
409                return None;
410            }
411        }
412
413        inner.quote.clone()
414    }
415
416    /// Runtime quote policy.
417    ///
418    /// This method may return `None` in the case where the enclave is not
419    /// running on SGX hardware or if the quote policy has not yet been
420    /// fetched from the consensus layer.
421    pub fn quote_policy(&self) -> Option<Arc<QuotePolicy>> {
422        let inner = self.inner.read().unwrap();
423        inner.quote_policy.clone()
424    }
425
426    /// Verify a provided RAK binding.
427    pub fn verify_binding(quote: &VerifiedQuote, rak: &signature::PublicKey) -> Result<()> {
428        if quote.report_data.len() < 32 {
429            return Err(IdentityError::MalformedReportData.into());
430        }
431        if Self::report_body_for_rak(rak).as_ref() != &quote.report_data[..32] {
432            return Err(IdentityError::BindingMismatch.into());
433        }
434
435        Ok(())
436    }
437
438    /// Checks whether the RAK matches another specified (RAK_pub, quote) pair.
439    pub fn rak_matches(&self, rak: &signature::PublicKey, quote: &Quote) -> bool {
440        // Check if public key matches.
441        if &self.public_rak() != rak {
442            return false;
443        }
444
445        let inner = self.inner.read().unwrap();
446        inner.known_quotes.iter().any(|q| &**q == quote)
447    }
448}
449
450impl Signer for Identity {
451    fn public(&self) -> signature::PublicKey {
452        let inner = self.inner.read().unwrap();
453        inner.rak.public_key()
454    }
455
456    fn sign(&self, context: &[u8], message: &[u8]) -> Result<Signature> {
457        let inner = self.inner.read().unwrap();
458        inner.rak.sign(context, message)
459    }
460}
461
462impl Opener for Identity {
463    fn box_open(
464        &self,
465        nonce: &[u8; deoxysii::NONCE_SIZE],
466        ciphertext: Vec<u8>,
467        additional_data: Vec<u8>,
468        peers_public_key: &x25519_dalek::PublicKey,
469    ) -> Result<Vec<u8>> {
470        let inner = self.inner.read().unwrap();
471        let private_key = &inner.rek.0;
472
473        deoxysii::box_open(
474            nonce,
475            ciphertext,
476            additional_data,
477            peers_public_key,
478            private_key,
479        )
480    }
481}