Skip to main content

ttk_server/attestation/
nitro.rs

1//! AWS Nitro Security Module (NSM) provider.
2//!
3//! Opens `/dev/nsm` with RAII ([`NsmSession`]) and produces hardware-rooted Nitro Attestation
4//! Documents, wrapped as EAT claims-sets. Document parsing helpers live in [`super::nitro_doc`].
5
6use super::nitro_doc::wrap_as_eat;
7use super::{AttestationError, AttestationProvider};
8pub use crate::AttestationParams;
9use crate::EatClaimsSet;
10use aws_nitro_enclaves_nsm_api::api::Digest;
11use aws_nitro_enclaves_nsm_api::api::{Request, Response};
12use aws_nitro_enclaves_nsm_api::driver::{nsm_exit, nsm_init, nsm_process_request};
13use std::path::Path;
14
15/// Information about the connected Nitro Security Module runtime and configuration.
16#[derive(Debug, Clone, PartialEq)]
17pub struct NsmDescription {
18    /// Major API version of the NSM.
19    pub version_major: u16,
20    /// Minor API version of the NSM.
21    pub version_minor: u16,
22    /// Patch version of the NSM.
23    pub version_patch: u16,
24    /// Module identifier for the NSM.
25    pub module_id: String,
26    /// Maximum number of Platform Configuration Registers (PCRs).
27    pub max_pcrs: u16,
28    /// The indices of PCRs that are read-only / locked.
29    pub locked_pcrs: std::collections::BTreeSet<u16>,
30    /// Digest algorithm used for PCR values.
31    pub digest: Digest,
32}
33
34/// An open session with the Nitro Security Module (`/dev/nsm`).
35///
36/// Implements RAII to ensure the device file descriptor is automatically closed
37/// via [`nsm_exit`] when the session is dropped.
38#[derive(Debug)]
39pub struct NsmSession {
40    fd: i32,
41}
42
43/// Nitro implementation of [`AttestationProvider`], backed by `/dev/nsm`.
44impl AttestationProvider for NsmSession {
45    /// Returns `"aws-nitro"`.
46    fn name(&self) -> &'static str {
47        "aws-nitro"
48    }
49
50    /// Returns `true` if the NSM device `/dev/nsm` exists.
51    fn is_available() -> bool {
52        Path::new("/dev/nsm").exists()
53    }
54
55    /// Requests a real attestation document from the NSM for `params` and wraps it as an EAT claims-set.
56    fn generate_document(
57        &self,
58        params: &AttestationParams,
59    ) -> Result<EatClaimsSet, AttestationError> {
60        wrap_as_eat(&self.create_attestation(params)?)
61    }
62}
63
64/// NSM operations: opening a session, attestation requests and PCR management.
65impl NsmSession {
66    /// Opens a new session with the Nitro Security Module.
67    ///
68    /// Calls [`nsm_init`] to open `/dev/nsm`. Returns [`AttestationError::DeviceOpenFailed`]
69    /// if the device file cannot be opened (e.g., if not running inside an AWS Nitro Enclave).
70    pub fn open() -> Result<Self, AttestationError> {
71        let fd = nsm_init();
72        if fd < 0 {
73            return Err(AttestationError::DeviceOpenFailed(
74                "Unable to open /dev/nsm. Ensure this process is running inside an AWS Nitro Enclave with the NSM device enabled."
75                    .to_string(),
76            ));
77        }
78        Ok(Self { fd })
79    }
80
81    /// Creates an [`NsmSession`] from an existing raw file descriptor.
82    pub fn from_raw_fd(fd: i32) -> Result<Self, AttestationError> {
83        if fd < 0 {
84            return Err(AttestationError::DeviceOpenFailed(
85                "Invalid file descriptor provided".to_string(),
86            ));
87        }
88        Ok(Self { fd })
89    }
90
91    /// Returns the underlying raw file descriptor.
92    pub fn raw_fd(&self) -> i32 {
93        self.fd
94    }
95
96    /// Requests an Attestation Document from the NSM.
97    ///
98    /// Returns the raw COSE_Sign1 formatted document bytes.
99    pub fn create_attestation(
100        &self,
101        params: &AttestationParams,
102    ) -> Result<Vec<u8>, AttestationError> {
103        let request = Request::Attestation {
104            user_data: params.user_data.as_ref().map(|d| d.clone().into()),
105            nonce: params.nonce.as_ref().map(|n| n.clone().into()),
106            public_key: params.public_key.as_ref().map(|pk| pk.clone().into()),
107        };
108
109        match nsm_process_request(self.fd, request) {
110            Response::Attestation { document } => Ok(document),
111            Response::Error(err) => Err(AttestationError::Driver(format!("{err:?}"))),
112            other => Err(AttestationError::UnexpectedResponse(format!("{other:?}"))),
113        }
114    }
115
116    /// Convenience method to create an attestation document binding an ephemeral TLS certificate.
117    ///
118    /// Computes the SHA-256 hash of `cert_der` and supplies it as `user_data`.
119    pub fn create_attestation_for_cert(
120        &self,
121        cert_der: &[u8],
122    ) -> Result<Vec<u8>, AttestationError> {
123        let params = AttestationParams::new().with_user_data_hash(cert_der);
124        self.create_attestation(&params)
125    }
126
127    /// Describes the connected Nitro Security Module capabilities and configuration.
128    pub fn describe_nsm(&self) -> Result<NsmDescription, AttestationError> {
129        match nsm_process_request(self.fd, Request::DescribeNSM) {
130            Response::DescribeNSM {
131                version_major,
132                version_minor,
133                version_patch,
134                module_id,
135                max_pcrs,
136                locked_pcrs,
137                digest,
138            } => Ok(NsmDescription {
139                version_major,
140                version_minor,
141                version_patch,
142                module_id,
143                max_pcrs,
144                locked_pcrs,
145                digest,
146            }),
147            Response::Error(err) => Err(AttestationError::Driver(format!("{err:?}"))),
148            other => Err(AttestationError::UnexpectedResponse(format!("{other:?}"))),
149        }
150    }
151
152    /// Requests cryptographic entropy (random bytes) from the NSM.
153    pub fn get_random(&self) -> Result<Vec<u8>, AttestationError> {
154        match nsm_process_request(self.fd, Request::GetRandom) {
155            Response::GetRandom { random } => Ok(random),
156            Response::Error(err) => Err(AttestationError::Driver(format!("{err:?}"))),
157            other => Err(AttestationError::UnexpectedResponse(format!("{other:?}"))),
158        }
159    }
160
161    /// Describes a Platform Configuration Register (PCR) at `index`.
162    /// Returns `(locked, data)`.
163    pub fn describe_pcr(&self, index: u16) -> Result<(bool, Vec<u8>), AttestationError> {
164        match nsm_process_request(self.fd, Request::DescribePCR { index }) {
165            Response::DescribePCR { lock, data } => Ok((lock, data)),
166            Response::Error(err) => Err(AttestationError::Driver(format!("{err:?}"))),
167            other => Err(AttestationError::UnexpectedResponse(format!("{other:?}"))),
168        }
169    }
170
171    /// Extends a Platform Configuration Register (PCR) at `index` with `data`.
172    pub fn extend_pcr(&self, index: u16, data: Vec<u8>) -> Result<Vec<u8>, AttestationError> {
173        match nsm_process_request(self.fd, Request::ExtendPCR { index, data }) {
174            Response::ExtendPCR { data } => Ok(data),
175            Response::Error(err) => Err(AttestationError::Driver(format!("{err:?}"))),
176            other => Err(AttestationError::UnexpectedResponse(format!("{other:?}"))),
177        }
178    }
179
180    /// Locks a Platform Configuration Register (PCR) at `index` against further modification.
181    pub fn lock_pcr(&self, index: u16) -> Result<(), AttestationError> {
182        match nsm_process_request(self.fd, Request::LockPCR { index }) {
183            Response::LockPCR => Ok(()),
184            Response::Error(err) => Err(AttestationError::Driver(format!("{err:?}"))),
185            other => Err(AttestationError::UnexpectedResponse(format!("{other:?}"))),
186        }
187    }
188}
189
190/// Closes the NSM device file descriptor when the session goes out of scope.
191impl Drop for NsmSession {
192    /// Calls `nsm_exit` on the open file descriptor, at most once.
193    fn drop(&mut self) {
194        if self.fd >= 0 {
195            nsm_exit(self.fd);
196            self.fd = -1;
197        }
198    }
199}