Skip to main content

lexe_enclave/
types.rs

1use std::{borrow::Cow, error::Error as StdError, fmt, io, mem};
2
3use lexe_byte_array::ByteArray;
4use lexe_hex::hex;
5use lexe_serde::impl_serde_hexstr_or_bytes;
6use lexe_sha256::sha256;
7#[cfg(any(test, feature = "test-utils"))]
8use proptest_derive::Arbitrary;
9use ref_cast::RefCast;
10
11/// An SGX enclave measurement (MRENCLAVE): a SHA-256 hash of the enclave
12/// binary, used to verify node integrity. Serialized as a 64-character hex
13/// string.
14//
15// Get the current enclave measurement with [`measurement`].
16// Get the current signer measurement with [`signer`].
17#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
18#[derive(Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, RefCast)]
19#[repr(transparent)]
20pub struct Measurement([u8; 32]);
21
22impl_serde_hexstr_or_bytes!(Measurement);
23
24/// A [`Measurement`] shortened to its first four bytes (8 hex chars).
25#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
26#[derive(Copy, Clone, Hash, Eq, PartialEq, RefCast)]
27#[repr(transparent)]
28pub struct MrShort([u8; 4]);
29
30impl_serde_hexstr_or_bytes!(MrShort);
31
32pub enum Error {
33    SgxError(sgx_isa::ErrorCode),
34    SealInputTooLarge,
35    UnsealInputTooSmall,
36    InvalidKeyRequestLength,
37    UnsealDecryptionError,
38    DeserializationError,
39}
40
41/// A unique identifier for a particular hardware enclave.
42///
43/// The intention with this identifier is to compactly communicate whether a
44/// piece of hardware with this id (in its current state) has the _capability_
45/// to `unseal` some [`Sealed`] data that was `seal`ed on hardware
46/// possessing the same id.
47///
48/// An easy way to show unsealing capability is to actually derive a key from
49/// the SGX platform. Rather than encrypting some data with this key, we'll
50/// instead use it as a public identifier. For different enclaves to still
51/// derive the same machine id, we'll use the enclave signer (Lexe) in our key
52/// derivation instead of the per-enclave measurement.
53///
54/// As an added bonus, if the machine operator ever bumps the [`OWNER_EPOCH`] in
55/// the BIOS, the machine id will automatically change to a different value,
56/// correctly reflecting this machine's inability to unseal the old data.
57///
58/// NOTE: on SGX this capability is modulo the [CPUSVN] (i.e., doesn't commit to
59///       the CPUSVN) since it allows us to easily upgrade the SGX TCB platform
60///       without needing to also online-migrate sealed enclave state.
61///
62/// [CPUSVN]: https://phlip9.com/notes/confidential%20computing/intel%20SGX/SGX%20lingo/#security-version-number-svn
63/// [`OWNER_EPOCH`]: https://phlip9.com/notes/confidential%20computing/intel%20SGX/SGX%20lingo/#owner-epoch
64#[cfg_attr(any(test, feature = "test-utils"), derive(Arbitrary))]
65#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, RefCast)]
66#[repr(transparent)]
67pub struct MachineId([u8; 16]);
68
69impl_serde_hexstr_or_bytes!(MachineId);
70
71/// Sealed and encrypted data
72// TODO(phlip9): use a real serialization format like CBOR or something
73// TODO(phlip9): additional authenticated data?
74#[derive(Eq, PartialEq)]
75pub struct Sealed<'a> {
76    /// A truncated [`sgx_isa::Keyrequest`].
77    ///
78    /// This field contains all the data needed to correctly recover the
79    /// underlying seal key material inside an enclave. Currently this field is
80    /// 76 bytes in SGX.
81    pub keyrequest: Cow<'a, [u8]>,
82    /// Encrypted ciphertext
83    pub ciphertext: Cow<'a, [u8]>,
84}
85
86/// Our Min CPUSVN determines our current minimum SGX platform version that our
87/// enclaves will run on.
88///
89/// Each Intel platform that supports running SGX enclaves has a CPU Security
90/// Version Number, which compactly encodes the the security levels of all the
91/// platform components (quoting enclave versions, firmware and microcode
92/// patches, etc).
93///
94/// Intel doesn't appear to publicly commit to the CPUSVN structure, so we
95/// don't have a stable way to compare them. Instead we indirectly rely on
96/// `enclave::machine_id()` and `enclave::seal()` to enforce that we're running
97/// a trustworthy platform version.
98#[derive(Copy, Clone)]
99#[repr(transparent)]
100pub(crate) struct MinCpusvn([u8; 16]);
101
102// --- impl Error --- //
103
104impl StdError for Error {}
105
106impl fmt::Display for Error {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        let s = match self {
109            Self::SgxError(_) => "",
110            Self::SealInputTooLarge => "sealing: input data is too large",
111            Self::UnsealInputTooSmall => "unsealing: ciphertext is too small",
112            Self::InvalidKeyRequestLength => "keyrequest is not a valid length",
113            Self::UnsealDecryptionError =>
114                "unseal error: ciphertext or metadata may be corrupted",
115            Self::DeserializationError => "deserialize: input is malformed",
116        };
117        match self {
118            Self::SgxError(err) => write!(f, "SGX error: {err:?}"),
119            _ => f.write_str(s),
120        }
121    }
122}
123
124impl fmt::Debug for Error {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        write!(f, "enclave::Error({self})")
127    }
128}
129
130impl From<sgx_isa::ErrorCode> for Error {
131    fn from(err: sgx_isa::ErrorCode) -> Self {
132        Self::SgxError(err)
133    }
134}
135
136// --- impl Measurement --- //
137
138impl Measurement {
139    pub const MOCK_ENCLAVE: Self =
140        Self::new(*b"~~~~~~~ LEXE MOCK ENCLAVE ~~~~~~");
141    pub const MOCK_SIGNER: Self =
142        Self::new(*b"======= LEXE MOCK SIGNER =======");
143
144    /// The enclave signer measurement our debug enclaves are signed with.
145    /// This is also the measurement of the fortanix/rust-sgx dummy key:
146    /// <https://github.com/fortanix/rust-sgx/blob/master/intel-sgx/enclave-runner/src/dummy.key>
147    ///
148    /// Running an enclave with `run-sgx .. --debug` will automatically sign
149    /// with this key just before running.
150    pub const DEV_SIGNER: Self = Self::new(hex::decode_const(
151        b"9affcfae47b848ec2caf1c49b4b283531e1cc425f93582b36806e52a43d78d1a",
152    ));
153
154    /// The enclave signer measurement our production enclaves should be signed
155    /// with. Inside an enclave, get the signer with `signer`.
156    pub const PROD_SIGNER: Self = Self::new(hex::decode_const(
157        b"02d07f56b7f4a71d32211d6821beaeb316fbf577d02bab0dfe1f18a73de08a8e",
158    ));
159
160    /// Return the expected signer measurement by `DeployEnv` and whether
161    /// we're in mock or sgx mode.
162    pub const fn expected_signer(use_sgx: bool, is_dev: bool) -> Self {
163        if use_sgx {
164            if is_dev {
165                Self::DEV_SIGNER
166            } else {
167                Self::PROD_SIGNER
168            }
169        } else {
170            Self::MOCK_SIGNER
171        }
172    }
173
174    /// Compute an enclave measurement from an `.sgxs` file stream
175    /// [`std::io::Read`].
176    ///
177    /// * Enclave binaries are first converted to `.sgxs` files, which exactly
178    ///   mirror the memory layout of the loaded enclave binaries right before
179    ///   running.
180    /// * Conveniently, the SHA-256 hash of an enclave `.sgxs` binary is exactly
181    ///   the same as the actual enclave measurement hash, since the memory
182    ///   layout is identical (caveat: unless we use some more sophisticated
183    ///   extendable enclave features).
184    pub fn compute_from_sgxs(
185        mut sgxs_reader: impl io::Read,
186    ) -> io::Result<Self> {
187        let mut buf = [0u8; 4096];
188        let mut digest = sha256::Context::new();
189
190        loop {
191            let n = sgxs_reader.read(&mut buf)?;
192            if n == 0 {
193                let hash = digest.finish();
194                return Ok(Self::new(hash.to_array()));
195            } else {
196                digest.update(&buf[0..n]);
197            }
198        }
199    }
200
201    pub const fn new(bytes: [u8; 32]) -> Self {
202        Self(bytes)
203    }
204
205    pub fn short(&self) -> MrShort {
206        MrShort::from(self)
207    }
208}
209
210lexe_byte_array::impl_byte_array!(Measurement, 32);
211lexe_byte_array::impl_fromstr_fromhex!(Measurement, 32);
212lexe_byte_array::impl_debug_display_as_hex!(Measurement);
213
214// --- impl MrShort --- //
215
216impl MrShort {
217    pub const fn new(bytes: [u8; 4]) -> Self {
218        Self(bytes)
219    }
220
221    /// Whether this [`MrShort`] is a prefix of the given [`Measurement`].
222    pub fn is_prefix_of(&self, long: &Measurement) -> bool {
223        self.0 == long.0[..4]
224    }
225}
226
227lexe_byte_array::impl_byte_array!(MrShort, 4);
228lexe_byte_array::impl_fromstr_fromhex!(MrShort, 4);
229lexe_byte_array::impl_debug_display_as_hex!(MrShort);
230
231impl From<&Measurement> for MrShort {
232    fn from(long: &Measurement) -> Self {
233        (long.0)[..4].try_into().map(Self).unwrap()
234    }
235}
236
237// --- impl MachineId --- //
238
239impl MachineId {
240    // TODO(phlip9): use the machine id of my dev machine until we build a
241    // proper get-machine-id bin util.
242    pub const MOCK: Self =
243        MachineId::new(hex::decode_const(b"52bc575eb9618084083ca7b3a45a2a76"));
244
245    pub const fn new(bytes: [u8; 16]) -> Self {
246        Self(bytes)
247    }
248}
249
250lexe_byte_array::impl_byte_array!(MachineId, 16);
251lexe_byte_array::impl_fromstr_fromhex!(MachineId, 16);
252lexe_byte_array::impl_debug_display_as_hex!(MachineId);
253
254// --- impl Sealed --- //
255
256impl<'a> Sealed<'a> {
257    /// AES-256-GCM tag length
258    pub const TAG_LEN: usize = 16;
259
260    pub fn serialize(&self) -> Vec<u8> {
261        let out_len = mem::size_of::<u32>()
262            + self.keyrequest.len()
263            + mem::size_of::<u32>()
264            + self.ciphertext.len();
265        let mut out = Vec::with_capacity(out_len);
266
267        out.extend_from_slice(&(self.keyrequest.len() as u32).to_le_bytes());
268        out.extend_from_slice(self.keyrequest.as_ref());
269        out.extend_from_slice(&(self.ciphertext.len() as u32).to_le_bytes());
270        out.extend_from_slice(self.ciphertext.as_ref());
271
272        out
273    }
274
275    pub fn deserialize(bytes: &'a [u8]) -> Result<Self, Error> {
276        let (keyrequest, bytes) = Self::read_bytes(bytes)?;
277        let (ciphertext, bytes) = Self::read_bytes(bytes)?;
278
279        if bytes.is_empty() {
280            Ok(Self {
281                keyrequest: Cow::Borrowed(keyrequest),
282                ciphertext: Cow::Borrowed(ciphertext),
283            })
284        } else {
285            Err(Error::DeserializationError)
286        }
287    }
288
289    // Helper to split a byte slice into a 4 byte little-endian slice and the
290    // remainder. Errors if the input slice is smaller than 4 bytes.
291    fn read_bytes(bytes: &[u8]) -> Result<(&[u8], &[u8]), Error> {
292        let (len, bytes) = Self::read_u32_le(bytes)?;
293        let len = len as usize;
294        if bytes.len() >= len {
295            Ok(bytes.split_at(len))
296        } else {
297            Err(Error::DeserializationError)
298        }
299    }
300
301    // Reads a little-endian u32 from the start of a slice. Returns the u32 and
302    // the remainder, or errors if there aren't enough bytes.
303    fn read_u32_le(bytes: &[u8]) -> Result<(u32, &[u8]), Error> {
304        match bytes.split_first_chunk::<4>() {
305            Some((val, rest)) => Ok((u32::from_le_bytes(*val), rest)),
306            None => Err(Error::DeserializationError),
307        }
308    }
309}
310
311impl fmt::Debug for Sealed<'_> {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        f.debug_struct("Sealed")
314            .field("keyrequest", &hex::display(&self.keyrequest))
315            .field("ciphertext", &hex::display(&self.ciphertext))
316            .finish()
317    }
318}
319
320// --- impl MinCpusvn --- //
321
322impl MinCpusvn {
323    /// This is the current CPUSVN we commit to when sealing data in SGX.
324    ///
325    /// Updated: 2024/12/04 - Linux SGX platform v2.25
326    pub const CURRENT: Self =
327        Self::new(hex::decode_const(b"0e0e100fffff01000000000000000000"));
328
329    pub const fn new(bytes: [u8; 16]) -> Self {
330        Self(bytes)
331    }
332
333    pub const fn to_array(self) -> [u8; 16] {
334        self.0
335    }
336}
337
338// --- SGX feature flag consts --- //
339
340// SGX platform feature flags vs masks
341//
342// Each feature set has two components: a MASK, which determines _which_
343// features we want to bind, and then the FLAGS, which determines the _value_
344// (on/off) of each (bound) feature. All bits in FLAGS that aren't also set in
345// MASK are ignored.
346//
347// As a simplified example, let's look at a system with only two features:
348// DEBUG = 0b01 and FAST = 0b10. Suppose DEBUG enables the enclave DEBUG mode,
349// which isn't safe for production, while FAST enables some high-performance CPU
350// hardware feature.
351//
352// If we're building an enclave for production, we care about the values of both
353// features, so we set MASK = 0b11. It would be a problem if our enclave ran
354// with DEBUG enabled and FAST turned off, so we set FLAGS = 0b10.
355//
356// In the event we don't care about the FAST feature (maybe we need to run on
357// both old and new CPUs), we would instead only bind the DEBUG feature, with
358// MASK = 0b01. In this case, only the value of the DEBUG bit matters and the
359// FAST bit is completely ignored, so FLAGS = 0b01 and 0b11 are both equivalent.
360//
361// By not including a feature in our MASK, we are effectively allowing
362// the platform to determine the value at runtime.
363
364/// SGX platform feature flags
365///
366/// See: [`AttributesFlags`](sgx_isa::AttributesFlags)
367pub mod attributes {
368    use sgx_isa::AttributesFlags;
369
370    /// In production, ensure the SGX debug flag is turned off. We're also not
371    /// currently using any other features, like KSS (Key sharing and
372    /// separation) or AEX Notify (Async Enclave eXit Notify).
373    pub const LEXE_FLAGS_PROD: AttributesFlags = AttributesFlags::MODE64BIT;
374
375    pub const LEXE_FLAGS_DEBUG: AttributesFlags =
376        LEXE_FLAGS_PROD.union(AttributesFlags::DEBUG);
377
378    /// The flags we want to bind (whether on or off).
379    pub const LEXE_MASK: AttributesFlags = AttributesFlags::INIT
380        .union(AttributesFlags::DEBUG)
381        .union(AttributesFlags::MODE64BIT);
382}
383
384/// XFRM: CPU feature flags
385///
386/// See: <https://github.com/intel/linux-sgx> `common/inc/sgx_attributes.h`
387pub mod xfrm {
388    /// Legacy XFRM which includes the basic feature bits required by SGX
389    /// x87 state(0x01) and SSE state(0x02).
390    pub const LEGACY: u64 = 0x0000000000000003;
391    /// AVX XFRM which includes AVX state(0x04) and SSE state(0x02) required by
392    /// AVX.
393    pub const AVX: u64 = 0x0000000000000006;
394    /// AVX-512 XFRM.
395    pub const AVX512: u64 = 0x00000000000000e6;
396    /// MPX XFRM - not supported.
397    pub const MPX: u64 = 0x0000000000000018;
398    /// PKRU state.
399    pub const PKRU: u64 = 0x0000000000000200;
400    /// AMX XFRM, including XTILEDATA(0x40000) and XTILECFG(0x20000).
401    pub const AMX: u64 = 0x0000000000060000;
402
403    /// Features required by LEXE enclaves.
404    ///
405    /// Absolutely mandatory requirements:
406    ///   + SSE3+ (basic vectorization)
407    ///   + AESNI (AES crypto accelerators).
408    ///
409    /// Full AVX512 is not required but at least ensures we're running on more
410    /// recent hardware.
411    pub const LEXE_FLAGS: u64 = AVX512 | LEGACY;
412
413    /// Require LEXE features but allow new ones, determined at runtime.
414    pub const LEXE_MASK: u64 = LEXE_FLAGS;
415}
416
417/// SGX platform MISCSELECT feature flags
418pub mod miscselect {
419    use sgx_isa::Miscselect;
420
421    /// Don't need any features.
422    pub const LEXE_FLAGS: Miscselect = Miscselect::empty();
423
424    /// Bind nothing.
425    pub const LEXE_MASK: Miscselect = Miscselect::empty();
426}
427
428#[cfg(test)]
429mod test {
430    use std::str::FromStr;
431
432    use proptest::{arbitrary::any, proptest, strategy::Strategy};
433    use serde_core::{de::DeserializeOwned, ser::Serialize};
434
435    use super::*;
436
437    #[track_caller]
438    fn json_string_roundtrip<
439        T: DeserializeOwned + Serialize + PartialEq + fmt::Debug,
440    >(
441        s1: &str,
442    ) {
443        let x1: T = serde_json::from_str(s1).unwrap();
444        let s2 = serde_json::to_string(&x1).unwrap();
445        let x2: T = serde_json::from_str(&s2).unwrap();
446        assert_eq!(x1, x2);
447        assert_eq!(s1, s2);
448    }
449
450    #[track_caller]
451    fn fromstr_display_roundtrip<
452        T: FromStr + fmt::Display + PartialEq + fmt::Debug,
453    >(
454        s1: &str,
455    ) {
456        let x1 = T::from_str(s1).map_err(|_| ()).unwrap();
457        let s2 = x1.to_string();
458        let x2 = T::from_str(&s2).map_err(|_| ()).unwrap();
459        assert_eq!(x1, x2);
460        assert_eq!(s1, s2);
461    }
462
463    #[test]
464    fn serde_roundtrips() {
465        json_string_roundtrip::<Measurement>(
466            "\"c4f249b8d3121b0e61170a93a526beda574058f782c0b3f339e74651c379f888\"",
467        );
468        json_string_roundtrip::<MachineId>(
469            "\"df3d290e1371112bd3da4a6cdda1f245\"",
470        );
471    }
472
473    #[test]
474    fn fromstr_display_roundtrips() {
475        fromstr_display_roundtrip::<Measurement>(
476            "c4f249b8d3121b0e61170a93a526beda574058f782c0b3f339e74651c379f888",
477        );
478        fromstr_display_roundtrip::<MachineId>(
479            "df3d290e1371112bd3da4a6cdda1f245",
480        );
481    }
482
483    #[test]
484    fn test_mr_short() {
485        proptest!(|(
486            long1 in any::<Measurement>(),
487            long2 in any::<Measurement>(),
488        )| {
489            let short1 = long1.short();
490            let short2 = long2.short();
491            assert!(short1.is_prefix_of(&long1));
492            assert!(short2.is_prefix_of(&long2));
493
494            if short1 != short2 {
495                assert_ne!(long1, long2);
496                assert!(!short1.is_prefix_of(&long2));
497                assert!(!short2.is_prefix_of(&long1));
498            }
499        });
500    }
501
502    #[test]
503    fn test_sealed_serialization() {
504        let arb_keyrequest = any::<Vec<u8>>();
505        let arb_ciphertext = any::<Vec<u8>>();
506        let arb_sealed = (arb_keyrequest, arb_ciphertext).prop_map(
507            |(keyrequest, ciphertext)| Sealed {
508                keyrequest: keyrequest.into(),
509                ciphertext: ciphertext.into(),
510            },
511        );
512
513        proptest!(|(sealed in arb_sealed)| {
514            let bytes = sealed.serialize();
515            let sealed2 = Sealed::deserialize(&bytes).unwrap();
516            assert_eq!(sealed, sealed2);
517        });
518    }
519}