blob: 6a5462367d540de19f824c8c762ff86a1baea875 [file] [log] [blame]
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +00001// Copyright 2023, The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Support for reading and writing to the instance.img.
16
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000017use crate::crypto;
18use crate::crypto::hkdf_sh512;
19use crate::crypto::AeadCtx;
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000020use crate::dice::PartialInputs;
21use crate::gpt;
22use crate::gpt::Partition;
23use crate::gpt::Partitions;
24use crate::helpers::ceiling_div;
25use crate::virtio::pci::VirtIOBlkIterator;
26use core::fmt;
27use core::mem::size_of;
28use core::slice;
29use diced_open_dice::DiceMode;
30use diced_open_dice::Hash;
31use diced_open_dice::Hidden;
32use log::trace;
33use uuid::Uuid;
34use virtio_drivers::transport::pci::bus::PciRoot;
35
36pub enum Error {
37 /// Unexpected I/O error while accessing the underlying disk.
38 FailedIo(gpt::Error),
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000039 /// Failed to decrypt the entry.
40 FailedOpen(crypto::ErrorIterator),
41 /// Failed to encrypt the entry.
42 FailedSeal(crypto::ErrorIterator),
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000043 /// Impossible to create a new instance.img entry.
44 InstanceImageFull,
45 /// Badly formatted instance.img header block.
46 InvalidInstanceImageHeader,
47 /// No instance.img ("vm-instance") partition found.
48 MissingInstanceImage,
49 /// The instance.img doesn't contain a header.
50 MissingInstanceImageHeader,
51 /// Authority hash found in the pvmfw instance.img entry doesn't match the trusted public key.
52 RecordedAuthHashMismatch,
53 /// Code hash found in the pvmfw instance.img entry doesn't match the inputs.
54 RecordedCodeHashMismatch,
55 /// DICE mode found in the pvmfw instance.img entry doesn't match the current one.
56 RecordedDiceModeMismatch,
57 /// Size of the instance.img entry being read or written is not supported.
58 UnsupportedEntrySize(usize),
59}
60
61impl fmt::Display for Error {
62 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
63 match self {
64 Self::FailedIo(e) => write!(f, "Failed I/O to disk: {e}"),
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000065 Self::FailedOpen(e_iter) => {
66 writeln!(f, "Failed to open the instance.img partition:")?;
67 for e in *e_iter {
68 writeln!(f, "\t{e}")?;
69 }
70 Ok(())
71 }
72 Self::FailedSeal(e_iter) => {
73 writeln!(f, "Failed to seal the instance.img partition:")?;
74 for e in *e_iter {
75 writeln!(f, "\t{e}")?;
76 }
77 Ok(())
78 }
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000079 Self::InstanceImageFull => write!(f, "Failed to obtain a free instance.img partition"),
80 Self::InvalidInstanceImageHeader => write!(f, "instance.img header is invalid"),
81 Self::MissingInstanceImage => write!(f, "Failed to find the instance.img partition"),
82 Self::MissingInstanceImageHeader => write!(f, "instance.img header is missing"),
83 Self::RecordedAuthHashMismatch => write!(f, "Recorded authority hash doesn't match"),
84 Self::RecordedCodeHashMismatch => write!(f, "Recorded code hash doesn't match"),
85 Self::RecordedDiceModeMismatch => write!(f, "Recorded DICE mode doesn't match"),
86 Self::UnsupportedEntrySize(sz) => write!(f, "Invalid entry size: {sz}"),
87 }
88 }
89}
90
91pub type Result<T> = core::result::Result<T, Error>;
92
93pub fn get_or_generate_instance_salt(
94 pci_root: &mut PciRoot,
95 dice_inputs: &PartialInputs,
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000096 secret: &[u8],
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000097) -> Result<(bool, Hidden)> {
98 let mut instance_img = find_instance_img(pci_root)?;
99
100 let entry = locate_entry(&mut instance_img)?;
101 trace!("Found pvmfw instance.img entry: {entry:?}");
102
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000103 let key = hkdf_sh512::<32>(secret, /*salt=*/ &[], b"vm-instance");
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000104 let mut blk = [0; BLK_SIZE];
105 match entry {
106 PvmfwEntry::Existing { header_index, payload_size } => {
107 if payload_size > blk.len() {
108 // We currently only support single-blk entries.
109 return Err(Error::UnsupportedEntrySize(payload_size));
110 }
111 let payload_index = header_index + 1;
112 instance_img.read_block(payload_index, &mut blk).map_err(Error::FailedIo)?;
113
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000114 let payload = &blk[..payload_size];
115 let mut entry = [0; size_of::<EntryBody>()];
116 let key = key.map_err(Error::FailedOpen)?;
117 let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedOpen)?;
118 let decrypted = aead.open(&mut entry, payload).map_err(Error::FailedOpen)?;
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000119
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000120 let body: &EntryBody = decrypted.as_ref();
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000121 if body.code_hash != dice_inputs.code_hash {
122 Err(Error::RecordedCodeHashMismatch)
123 } else if body.auth_hash != dice_inputs.auth_hash {
124 Err(Error::RecordedAuthHashMismatch)
125 } else if body.mode() != dice_inputs.mode {
126 Err(Error::RecordedDiceModeMismatch)
127 } else {
128 Ok((false, body.salt))
129 }
130 }
131 PvmfwEntry::New { header_index } => {
132 let salt = [0; size_of::<Hidden>()]; // TODO(b/262393451): Generate using TRNG.
133 let entry_body = EntryBody::new(dice_inputs, &salt);
134 let body = entry_body.as_ref();
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000135
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000136 let key = key.map_err(Error::FailedSeal)?;
137 let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedSeal)?;
138 // We currently only support single-blk entries.
139 assert!(body.len() + aead.aead().unwrap().max_overhead() < blk.len());
140 let encrypted = aead.seal(&mut blk, body).map_err(Error::FailedSeal)?;
141 let payload_size = encrypted.len();
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000142 let payload_index = header_index + 1;
143 instance_img.write_block(payload_index, &blk).map_err(Error::FailedIo)?;
144
145 let header = EntryHeader::new(PvmfwEntry::UUID, payload_size);
146 let (blk_header, blk_rest) = blk.split_at_mut(size_of::<EntryHeader>());
147 blk_header.copy_from_slice(header.as_ref());
148 blk_rest.fill(0);
149 instance_img.write_block(header_index, &blk).map_err(Error::FailedIo)?;
150
151 Ok((true, salt))
152 }
153 }
154}
155
156#[repr(C, packed)]
157struct Header {
158 magic: [u8; Header::MAGIC.len()],
159 version: u16,
160}
161
162impl Header {
163 const MAGIC: &[u8] = b"Android-VM-instance";
164 const VERSION_1: u16 = 1;
165
166 pub fn is_valid(&self) -> bool {
167 self.magic == Self::MAGIC && self.version() == Self::VERSION_1
168 }
169
170 fn version(&self) -> u16 {
171 u16::from_le(self.version)
172 }
173
174 fn from_bytes(bytes: &[u8]) -> Option<&Self> {
175 let header: &Self = bytes.as_ref();
176
177 if header.is_valid() {
178 Some(header)
179 } else {
180 None
181 }
182 }
183}
184
185impl AsRef<Header> for [u8] {
186 fn as_ref(&self) -> &Header {
187 // SAFETY - Assume that the alignement and size match Header.
188 unsafe { &*self.as_ptr().cast::<Header>() }
189 }
190}
191
192fn find_instance_img(pci_root: &mut PciRoot) -> Result<Partition> {
193 for device in VirtIOBlkIterator::new(pci_root) {
194 match Partition::get_by_name(device, "vm-instance") {
195 Ok(Some(p)) => return Ok(p),
196 Ok(None) => {}
197 Err(e) => log::warn!("error while reading from disk: {e}"),
198 };
199 }
200
201 Err(Error::MissingInstanceImage)
202}
203
204#[derive(Debug)]
205enum PvmfwEntry {
206 Existing { header_index: usize, payload_size: usize },
207 New { header_index: usize },
208}
209
210const BLK_SIZE: usize = Partitions::LBA_SIZE;
211
212impl PvmfwEntry {
213 const UUID: Uuid = Uuid::from_u128(0x90d2174a038a4bc6adf3824848fc5825);
214}
215
216fn locate_entry(partition: &mut Partition) -> Result<PvmfwEntry> {
217 let mut blk = [0; BLK_SIZE];
218 let mut indices = partition.indices();
219 let header_index = indices.next().ok_or(Error::MissingInstanceImageHeader)?;
220 partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
221 // The instance.img header is only used for discovery/validation.
222 let _ = Header::from_bytes(&blk).ok_or(Error::InvalidInstanceImageHeader)?;
223
224 while let Some(header_index) = indices.next() {
225 partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
226
227 let header: &EntryHeader = blk[..size_of::<EntryHeader>()].as_ref();
228 match (header.uuid(), header.payload_size()) {
229 (uuid, _) if uuid.is_nil() => return Ok(PvmfwEntry::New { header_index }),
230 (PvmfwEntry::UUID, payload_size) => {
231 return Ok(PvmfwEntry::Existing { header_index, payload_size })
232 }
233 (uuid, payload_size) => {
234 trace!("Skipping instance.img entry {uuid}: {payload_size:?} bytes");
235 let n = ceiling_div(payload_size, BLK_SIZE).unwrap();
236 if n > 0 {
237 let _ = indices.nth(n - 1); // consume
238 }
239 }
240 };
241 }
242
243 Err(Error::InstanceImageFull)
244}
245
246/// Marks the start of an instance.img entry.
247///
248/// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition".
249#[repr(C)]
250struct EntryHeader {
251 uuid: u128,
252 payload_size: u64,
253}
254
255impl EntryHeader {
256 fn new(uuid: Uuid, payload_size: usize) -> Self {
257 Self { uuid: uuid.as_u128(), payload_size: u64::try_from(payload_size).unwrap().to_le() }
258 }
259
260 fn uuid(&self) -> Uuid {
261 Uuid::from_u128(self.uuid)
262 }
263
264 fn payload_size(&self) -> usize {
265 usize::try_from(u64::from_le(self.payload_size)).unwrap()
266 }
267}
268
269impl AsRef<EntryHeader> for [u8] {
270 fn as_ref(&self) -> &EntryHeader {
271 assert_eq!(self.len(), size_of::<EntryHeader>());
272 // SAFETY - The size of the slice was checked and any value may be considered valid.
273 unsafe { &*self.as_ptr().cast::<EntryHeader>() }
274 }
275}
276
277impl AsRef<[u8]> for EntryHeader {
278 fn as_ref(&self) -> &[u8] {
279 let s = self as *const Self;
280 // SAFETY - Transmute the (valid) bytes into a slice.
281 unsafe { slice::from_raw_parts(s.cast::<u8>(), size_of::<Self>()) }
282 }
283}
284
285#[repr(C)]
286struct EntryBody {
287 code_hash: Hash,
288 auth_hash: Hash,
289 salt: Hidden,
290 mode: u8,
291}
292
293impl EntryBody {
294 fn new(dice_inputs: &PartialInputs, salt: &Hidden) -> Self {
295 let mode = match dice_inputs.mode {
296 DiceMode::kDiceModeNotInitialized => 0,
297 DiceMode::kDiceModeNormal => 1,
298 DiceMode::kDiceModeDebug => 2,
299 DiceMode::kDiceModeMaintenance => 3,
300 };
301
302 Self {
303 code_hash: dice_inputs.code_hash,
304 auth_hash: dice_inputs.auth_hash,
305 salt: *salt,
306 mode,
307 }
308 }
309
310 fn mode(&self) -> DiceMode {
311 match self.mode {
312 1 => DiceMode::kDiceModeNormal,
313 2 => DiceMode::kDiceModeDebug,
314 3 => DiceMode::kDiceModeMaintenance,
315 _ => DiceMode::kDiceModeNotInitialized,
316 }
317 }
318}
319
320impl AsRef<EntryBody> for [u8] {
321 fn as_ref(&self) -> &EntryBody {
322 assert_eq!(self.len(), size_of::<EntryBody>());
323 // SAFETY - The size of the slice was checked and members are validated by accessors.
324 unsafe { &*self.as_ptr().cast::<EntryBody>() }
325 }
326}
327
328impl AsRef<[u8]> for EntryBody {
329 fn as_ref(&self) -> &[u8] {
330 let s = self as *const Self;
331 // SAFETY - Transmute the (valid) bytes into a slice.
332 unsafe { slice::from_raw_parts(s.cast::<u8>(), size_of::<Self>()) }
333 }
334}