Skip to main content

nitro_cli/enclave_proc/
connection.rs

1// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3#![deny(missing_docs)]
4#![deny(warnings)]
5
6use log::{debug, warn};
7use nix::sys::epoll::EpollFlags;
8use nix::sys::socket::sockopt::PeerCredentials;
9use nix::sys::socket::UnixCredentials;
10use serde::de::DeserializeOwned;
11use std::collections::HashMap;
12use std::io::Write;
13use std::os::unix::io::AsRawFd;
14use std::os::unix::net::UnixStream;
15use std::sync::{Arc, Mutex};
16
17use crate::common::{receive_from_stream, write_u64_le};
18use crate::common::{
19    EnclaveProcessCommandType, EnclaveProcessReply, ExitGracefully, NitroCliErrorEnum,
20    NitroCliFailure, NitroCliResult,
21};
22use crate::new_nitro_cli_failure;
23
24/// The types of requesters which may send commands to the enclave process.
25#[derive(PartialEq, Eq, Hash)]
26enum CommandRequesterType {
27    /// The requester is the user with the given UID.
28    User(libc::uid_t),
29    /// The requester is the group with the given GID.
30    Group(libc::gid_t),
31    /// The requester is any other user.
32    Others,
33}
34
35/// The policy used to filter received commands based on the requester's type.
36struct CommandRequesterPolicy {
37    /// A mapping between a requester's type and all of its allowed commands.
38    policy: HashMap<CommandRequesterType, Vec<EnclaveProcessCommandType>>,
39}
40
41/// Data held by a connection.
42struct ConnectionData {
43    /// Flags received from `epoll` if this was an event-triggered connection.
44    epoll_flags: EpollFlags,
45    /// A communication stream with the peer, if this was a socket-triggered connection.
46    input_stream: Option<UnixStream>,
47}
48
49/// An enclave process connection to a CLI instance, an enclave or itself.
50#[derive(Clone)]
51pub struct Connection {
52    /// The thread-safe data used internally by the connection.
53    data: Arc<Mutex<ConnectionData>>,
54}
55
56impl Drop for ConnectionData {
57    fn drop(&mut self) {
58        if let Some(input_stream) = &self.input_stream {
59            // Close the stream.
60            input_stream
61                .shutdown(std::net::Shutdown::Both)
62                .map_err(|e| {
63                    new_nitro_cli_failure!(
64                        &format!("Stream shutdown error: {e:?}"),
65                        NitroCliErrorEnum::SocketCloseError
66                    )
67                })
68                .ok_or_exit_with_errno(Some("Failed to shut down"));
69        }
70    }
71}
72
73impl CommandRequesterPolicy {
74    /// Create a new `CommandRequesterPolicy` with the default rules. These rules allow
75    /// the user which spawned the enclave, together with `root`, to make any request,
76    /// whereas all other users are only allowed to make read-only requests (namely,
77    /// to describe an enclave or to read its CID).
78    fn new_with_defaults() -> Self {
79        let cmds_read_write = vec![
80            EnclaveProcessCommandType::Run,
81            EnclaveProcessCommandType::Terminate,
82            EnclaveProcessCommandType::TerminateComplete,
83            EnclaveProcessCommandType::Describe,
84            EnclaveProcessCommandType::GetEnclaveCID,
85            EnclaveProcessCommandType::GetEnclaveFlags,
86            EnclaveProcessCommandType::GetEnclaveName,
87            EnclaveProcessCommandType::GetIDbyName,
88            EnclaveProcessCommandType::ConnectionListenerStop,
89        ];
90        let cmds_read_only = vec![
91            EnclaveProcessCommandType::Describe,
92            EnclaveProcessCommandType::GetEnclaveCID,
93            EnclaveProcessCommandType::GetEnclaveFlags,
94            EnclaveProcessCommandType::GetEnclaveName,
95            EnclaveProcessCommandType::GetIDbyName,
96        ];
97        let mut policy = HashMap::new();
98
99        // The user which owns this enclave process may issue any command.
100        policy.insert(
101            CommandRequesterType::User(unsafe { libc::getuid() }),
102            cmds_read_write.clone(),
103        );
104
105        // The root user may issue any command.
106        policy.insert(CommandRequesterType::User(0_u32), cmds_read_write);
107
108        // All other users may only issue read-only commands.
109        policy.insert(CommandRequesterType::Others, cmds_read_only);
110
111        CommandRequesterPolicy { policy }
112    }
113
114    /// Find the policy rule which applies to the given requester and command.
115    fn find_policy_rule(
116        &self,
117        cmd: EnclaveProcessCommandType,
118        requester: &CommandRequesterType,
119    ) -> bool {
120        match self.policy.get(requester) {
121            None => false,
122            Some(allowed_cmds) => allowed_cmds.contains(&cmd),
123        }
124    }
125
126    /// Check if the user with the specified credentials has permission to run the specified command.
127    fn can_execute_command(&self, cmd: EnclaveProcessCommandType, creds: &UnixCredentials) -> bool {
128        // Search for a policy rule on the provided user ID.
129        if self.find_policy_rule(cmd, &CommandRequesterType::User(creds.uid())) {
130            return true;
131        }
132
133        // Search for a policy rule on the provided group ID.
134        if self.find_policy_rule(cmd, &CommandRequesterType::Group(creds.gid())) {
135            return true;
136        }
137
138        // Search for a policy rule on all other users.
139        if self.find_policy_rule(cmd, &CommandRequesterType::Others) {
140            return true;
141        }
142
143        // If we haven't found any applicable policy rule we can't allow the command to be executed.
144        false
145    }
146}
147
148impl Connection {
149    /// Create a new connection instance.
150    pub fn new(epoll_flags: EpollFlags, input_stream: Option<UnixStream>) -> Self {
151        let conn_data = ConnectionData {
152            epoll_flags,
153            input_stream,
154        };
155
156        Connection {
157            data: Arc::new(Mutex::new(conn_data)),
158        }
159    }
160
161    /// Read a command and its corresponding credentials.
162    pub fn read_command(&self) -> NitroCliResult<EnclaveProcessCommandType> {
163        let mut lock = self.data.lock().map_err(|e| {
164            new_nitro_cli_failure!(
165                &format!("Failed to acquire lock: {e:?}"),
166                NitroCliErrorEnum::LockAcquireFailure
167            )
168        })?;
169        if lock.input_stream.is_none() {
170            return Err(new_nitro_cli_failure!(
171                "Cannot read a command from this connection",
172                NitroCliErrorEnum::UnusableConnectionError
173            ));
174        }
175
176        // First, read the incoming command.
177        let mut cmd =
178            receive_from_stream::<EnclaveProcessCommandType>(lock.input_stream.as_mut().unwrap())?;
179
180        // Next, read the credentials of the command requester.
181        let conn_fd = lock.input_stream.as_ref().unwrap().as_raw_fd();
182        let socket_creds = nix::sys::socket::getsockopt(conn_fd, PeerCredentials);
183
184        // If the credentials cannot be read, the command will be skipped.
185        let user_creds = match socket_creds {
186            Ok(creds) => creds,
187            Err(e) => {
188                warn!("Failed to get user credentials: {}", e);
189                return Ok(EnclaveProcessCommandType::NotPermitted);
190            }
191        };
192
193        // Apply the default command access policy based on the user's credentials.
194        let policy = CommandRequesterPolicy::new_with_defaults();
195        if !policy.can_execute_command(cmd, &user_creds) {
196            // Log the failed execution attempt.
197            warn!(
198                "The requester with credentials ({:?}) is not allowed to perform '{:?}'.",
199                user_creds, cmd
200            );
201
202            // Force the command to be skipped by the main event loop.
203            cmd = EnclaveProcessCommandType::NotPermitted;
204        } else {
205            // Log the successful execution attempt.
206            debug!(
207                "The requester with credentials ({:?}) is allowed to perform '{:?}'.",
208                user_creds, cmd
209            );
210        }
211
212        Ok(cmd)
213    }
214
215    /// Read an object of the specified type from this connection.
216    pub fn read<T>(&self) -> NitroCliResult<T>
217    where
218        T: DeserializeOwned,
219    {
220        let mut lock = self.data.lock().map_err(|e| {
221            new_nitro_cli_failure!(
222                &format!("Failed to acquire lock: {e:?}"),
223                NitroCliErrorEnum::LockAcquireFailure
224            )
225        })?;
226        if lock.input_stream.is_none() {
227            return Err(new_nitro_cli_failure!(
228                "Cannot read from this connection",
229                NitroCliErrorEnum::SocketError
230            ));
231        }
232
233        receive_from_stream::<T>(lock.input_stream.as_mut().unwrap())
234    }
235
236    /// Write a 64-bit unsigned value on this connection.
237    pub fn write_u64(&self, value: u64) -> NitroCliResult<()> {
238        let mut lock = self.data.lock().map_err(|e| {
239            new_nitro_cli_failure!(
240                &format!("Failed to acquire lock: {e:?}"),
241                NitroCliErrorEnum::LockAcquireFailure
242            )
243        })?;
244        if lock.input_stream.is_none() {
245            return Err(new_nitro_cli_failure!(
246                "Cannot write a 64-bit value to this connection",
247                NitroCliErrorEnum::SocketError
248            ));
249        }
250
251        write_u64_le(lock.input_stream.as_mut().unwrap(), value)
252    }
253
254    /// Write a message to the standard output of the connection's other end.
255    pub fn println(&self, msg: &str) -> NitroCliResult<()> {
256        let mut msg_str = msg.to_string();
257
258        // Append a new-line at the end of the string.
259        msg_str.push('\n');
260
261        let reply = EnclaveProcessReply::StdOutMessage(msg_str);
262        self.write_reply(&reply)
263    }
264
265    /// Write a message to the standard error of the connection's other end.
266    pub fn eprintln(&self, msg: &str) -> NitroCliResult<()> {
267        let mut msg_str = msg.to_string();
268
269        // Append a new-line at the end of the string.
270        msg_str.push('\n');
271
272        let reply = EnclaveProcessReply::StdErrMessage(msg_str);
273        self.write_reply(&reply)
274    }
275
276    /// Write an operation's status to the connection's other end.
277    pub fn write_status(&self, status: i32) -> NitroCliResult<()> {
278        let reply = EnclaveProcessReply::Status(status);
279        self.write_reply(&reply)
280    }
281
282    /// Get the enclave event flags.
283    pub fn get_enclave_event_flags(&self) -> NitroCliResult<Option<EpollFlags>> {
284        let lock = self.data.lock().map_err(|e| {
285            new_nitro_cli_failure!(
286                &format!("Failed to acquire connection lock: {e:?}"),
287                NitroCliErrorEnum::LockAcquireFailure
288            )
289        })?;
290        match lock.input_stream {
291            None => Ok(Some(lock.epoll_flags)),
292            _ => Ok(None),
293        }
294    }
295
296    /// Write a string and its corresponding destination to a socket.
297    fn write_reply(&self, reply: &EnclaveProcessReply) -> NitroCliResult<()> {
298        let mut lock = self.data.lock().map_err(|e| {
299            new_nitro_cli_failure!(
300                &format!("Failed to acquire lock: {e:?}"),
301                NitroCliErrorEnum::LockAcquireFailure
302            )
303        })?;
304        if lock.input_stream.is_none() {
305            return Err(new_nitro_cli_failure!(
306                "Cannot write message to connection",
307                NitroCliErrorEnum::SocketError
308            ));
309        }
310
311        let mut stream = lock.input_stream.as_mut().unwrap();
312        let mut reply_bytes = Vec::new();
313        ciborium::into_writer(reply, &mut reply_bytes).map_err(|e| {
314            new_nitro_cli_failure!(
315                &format!("Failed to serialize reply: {e:?}"),
316                NitroCliErrorEnum::SerdeError
317            )
318        })?;
319
320        write_u64_le(&mut stream, reply_bytes.len() as u64)
321            .map_err(|e| e.add_subaction("Write reply".to_string()))?;
322        stream.write_all(&reply_bytes).map_err(|e| {
323            new_nitro_cli_failure!(
324                &format!("Failed to write to stream: {e:?}"),
325                NitroCliErrorEnum::SocketError
326            )
327        })
328    }
329}
330
331/// Print a message to a connection's standard output, if the connection is available.
332pub fn safe_conn_println(conn: Option<&Connection>, msg: &str) -> NitroCliResult<()> {
333    if conn.is_none() {
334        return Ok(());
335    }
336
337    conn.unwrap().println(msg)
338}
339
340/// Print a message to a connection's standard error, if the connection is available.
341pub fn safe_conn_eprintln(conn: Option<&Connection>, msg: &str) -> NitroCliResult<()> {
342    if conn.is_none() {
343        return Ok(());
344    }
345
346    conn.unwrap().eprintln(msg)
347}