Skip to main content

lexe_enclave/
dev.rs

1//! Post-build dev version/measurement patching for SGX enclaves in testing.
2//!
3//! With this, `sgx-builder` only needs to compile and link each enclave once.
4//! It can then copy the binary and patch the `Identity::PLACEHOLDER` with the
5//! dev version and measurement. This produces distinct enclave measurements
6//! without rebuilding and relinking the enclave for every test version.
7//!
8//! Everything except the `Identity` encoding (needed by `sgx-builder`) is gated
9//! on the `test-utils` feature: production enclaves contain no patch slot and
10//! always report a `None` dev version and dev measurement.
11
12#[cfg(feature = "test-utils")]
13use std::sync::LazyLock;
14
15use lexe_byte_array::ByteArray;
16
17use crate::types::Measurement;
18
19/// Returns the dev version patched into this enclave binary.
20pub fn version() -> Option<&'static str> {
21    cfg_if::cfg_if! {
22        if #[cfg(feature = "test-utils")] {
23            DEV_IDENTITY.version()
24        } else {
25            None
26        }
27    }
28}
29
30/// Returns the dev measurement patched into the enclave binary.
31///
32/// Only effective outside SGX in test/simulation binaries.
33#[cfg(not(target_env = "sgx"))]
34pub(crate) fn measurement() -> Option<Measurement> {
35    cfg_if::cfg_if! {
36        if #[cfg(feature = "test-utils")] {
37            DEV_IDENTITY.measurement()
38        } else {
39            None
40        }
41    }
42}
43
44/// The dev enclave identity read once from this binary's patch slot.
45#[cfg(feature = "test-utils")]
46static DEV_IDENTITY: LazyLock<Identity> =
47    LazyLock::new(Identity::read_patch_slot);
48
49/// An encoded dev enclave identity (dev version + dev measurement), as
50/// patched into a compiled binary. Possibly still the unpatched placeholder.
51pub struct Identity([u8; Self::LEN]);
52
53impl Identity {
54    /// Length of the patch slot in bytes.
55    pub const LEN: usize = 64;
56
57    /// Bytes which `sgx-builder` replaces after compiling a dev enclave.
58    pub const PLACEHOLDER: Identity = Identity(
59        *b"\xffLEXE_DEV_ENCLAVE_IDENTITY_PATCH_SLOT_REPLACE_ME__0123456789ABCD",
60    );
61
62    /// ELF section which lets `sgx-builder` locate the patch slot.
63    //
64    // Keep in sync with the `read_patch_slot()` attribute literals.
65    pub const SECTION_ELF: &'static str = ".lexe_dev_id";
66
67    /// Mach-O equivalent of [`Identity::SECTION_ELF`], for non-SGX dev builds
68    /// on macOS. Lives in the `__DATA` segment.
69    //
70    // Keep in sync with the `read_patch_slot()` attribute literals.
71    pub const SECTION_MACHO: &'static str = "__lexe_dev_id";
72
73    const MEASUREMENT_OFFSET: usize = 32;
74    const VERSION_MAX_LEN: usize = Self::MEASUREMENT_OFFSET - 1;
75    const VERSION_OFFSET: usize = 1;
76
77    /// Encodes a dev enclave identity which `sgx-builder` writes over
78    /// [`Identity::PLACEHOLDER`] in a copied binary.
79    ///
80    /// Panics if `version` is empty or too long to fit in the patch slot.
81    pub fn encode(version: &str, measurement: Measurement) -> Self {
82        assert!(!version.is_empty(), "Dev version is empty");
83        let max_len = Self::VERSION_MAX_LEN;
84        assert!(
85            version.len() <= max_len,
86            "Dev version exceeds {max_len} bytes: {version}"
87        );
88
89        let mut encoded = [0; Self::LEN];
90        encoded[0] = version.len() as u8;
91        encoded[Self::VERSION_OFFSET..][..version.len()]
92            .copy_from_slice(version.as_bytes());
93        encoded[Self::MEASUREMENT_OFFSET..]
94            .copy_from_slice(measurement.as_slice());
95        Self(encoded)
96    }
97
98    pub fn as_bytes(&self) -> &[u8; Self::LEN] {
99        &self.0
100    }
101}
102
103/// Parsing and reading the patched identity. Not available in production
104/// enclaves, which contain no patch slot.
105#[cfg(any(test, feature = "test-utils"))]
106impl Identity {
107    const UNPATCHED_VERSION_LEN: u8 = 0xff;
108
109    /// Returns the dev version, or `None` if unpatched.
110    fn version(&self) -> Option<&str> {
111        self.is_patched().then(|| {
112            let version_len = usize::from(self.0[0]);
113            assert!(
114                (1..=Self::VERSION_MAX_LEN).contains(&version_len),
115                "Invalid patched dev version length: {version_len}"
116            );
117            std::str::from_utf8(&self.0[Self::VERSION_OFFSET..][..version_len])
118                .expect("Patched dev version is not UTF-8")
119        })
120    }
121
122    /// Returns the dev measurement, or `None` if unpatched.
123    #[cfg(any(test, not(target_env = "sgx")))]
124    fn measurement(&self) -> Option<Measurement> {
125        self.is_patched().then(|| {
126            let measurement: [u8; 32] =
127                self.0[Self::MEASUREMENT_OFFSET..].try_into().unwrap();
128            Measurement::new(measurement)
129        })
130    }
131
132    fn is_patched(&self) -> bool {
133        self.0[0] != Self::UNPATCHED_VERSION_LEN
134    }
135
136    /// Reads the bytes which `sgx-builder` patches after linking.
137    #[cfg(feature = "test-utils")] // Not used by lexe-enclave unit tests
138    #[inline(never)]
139    fn read_patch_slot() -> Self {
140        #[used]
141        // Keep in sync with `Identity::SECTION_ELF` / `SECTION_MACHO`.
142        // Assume ELF normally.
143        #[cfg_attr(
144            not(target_vendor = "apple"),
145            unsafe(link_section = ".lexe_dev_id")
146        )]
147        // For Apple Mach-O binaries, we also need to specify the segment first.
148        #[cfg_attr(
149            target_vendor = "apple",
150            unsafe(link_section = "__DATA,__lexe_dev_id")
151        )]
152        static mut PATCH_SLOT: [u8; Identity::LEN] = Identity::PLACEHOLDER.0;
153
154        // SAFETY: The patch slot is never mutated while the process runs, and
155        // the byte array is `Copy`. `static mut` prevents this non-inlined read
156        // from being replaced with the placeholder.
157        Self(unsafe { std::ptr::read(&raw mut PATCH_SLOT) })
158    }
159}
160
161#[cfg(test)]
162mod test {
163    use super::*;
164
165    #[test]
166    fn encode_roundtrip() {
167        let version = "0.0.0-dev.1";
168        let measurement = Measurement::new([42; 32]);
169
170        let id = Identity::encode(version, measurement);
171
172        assert_eq!(id.version(), Some(version));
173        assert_eq!(id.measurement(), Some(measurement));
174    }
175
176    #[test]
177    fn unpatched_placeholder() {
178        assert!(Identity::PLACEHOLDER.version().is_none());
179        assert!(Identity::PLACEHOLDER.measurement().is_none());
180    }
181
182    /// The patch slot in this test binary should be unpatched.
183    #[cfg(feature = "test-utils")]
184    #[test]
185    fn unpatched_slot() {
186        assert!(version().is_none());
187    }
188}