blob: b96ae1eeaccdb386aef56e56f3df30fab42c9554 [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;
Pierre-Clément Tosia59103d2023-02-02 14:46:55 +000024use crate::rand;
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000025use core::fmt;
26use core::mem::size_of;
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000027use diced_open_dice::DiceMode;
28use diced_open_dice::Hash;
29use diced_open_dice::Hidden;
30use log::trace;
31use uuid::Uuid;
32use virtio_drivers::transport::pci::bus::PciRoot;
Alice Wangeacb7382023-06-05 12:53:54 +000033use vmbase::util::ceiling_div;
Alice Wangeade1672023-06-08 14:56:20 +000034use vmbase::virtio::pci::VirtIOBlkIterator;
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +010035use zerocopy::AsBytes;
36use zerocopy::FromBytes;
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000037
38pub enum Error {
39 /// Unexpected I/O error while accessing the underlying disk.
40 FailedIo(gpt::Error),
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000041 /// Failed to decrypt the entry.
42 FailedOpen(crypto::ErrorIterator),
Pierre-Clément Tosia59103d2023-02-02 14:46:55 +000043 /// Failed to generate a random salt to be stored.
44 FailedSaltGeneration(rand::Error),
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000045 /// Failed to encrypt the entry.
46 FailedSeal(crypto::ErrorIterator),
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000047 /// Impossible to create a new instance.img entry.
48 InstanceImageFull,
49 /// Badly formatted instance.img header block.
50 InvalidInstanceImageHeader,
51 /// No instance.img ("vm-instance") partition found.
52 MissingInstanceImage,
53 /// The instance.img doesn't contain a header.
54 MissingInstanceImageHeader,
55 /// Authority hash found in the pvmfw instance.img entry doesn't match the trusted public key.
56 RecordedAuthHashMismatch,
57 /// Code hash found in the pvmfw instance.img entry doesn't match the inputs.
58 RecordedCodeHashMismatch,
59 /// DICE mode found in the pvmfw instance.img entry doesn't match the current one.
60 RecordedDiceModeMismatch,
61 /// Size of the instance.img entry being read or written is not supported.
62 UnsupportedEntrySize(usize),
63}
64
65impl fmt::Display for Error {
66 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67 match self {
68 Self::FailedIo(e) => write!(f, "Failed I/O to disk: {e}"),
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000069 Self::FailedOpen(e_iter) => {
70 writeln!(f, "Failed to open the instance.img partition:")?;
71 for e in *e_iter {
72 writeln!(f, "\t{e}")?;
73 }
74 Ok(())
75 }
Pierre-Clément Tosia59103d2023-02-02 14:46:55 +000076 Self::FailedSaltGeneration(e) => write!(f, "Failed to generate salt: {e}"),
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +000077 Self::FailedSeal(e_iter) => {
78 writeln!(f, "Failed to seal the instance.img partition:")?;
79 for e in *e_iter {
80 writeln!(f, "\t{e}")?;
81 }
82 Ok(())
83 }
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +000084 Self::InstanceImageFull => write!(f, "Failed to obtain a free instance.img partition"),
85 Self::InvalidInstanceImageHeader => write!(f, "instance.img header is invalid"),
86 Self::MissingInstanceImage => write!(f, "Failed to find the instance.img partition"),
87 Self::MissingInstanceImageHeader => write!(f, "instance.img header is missing"),
88 Self::RecordedAuthHashMismatch => write!(f, "Recorded authority hash doesn't match"),
89 Self::RecordedCodeHashMismatch => write!(f, "Recorded code hash doesn't match"),
90 Self::RecordedDiceModeMismatch => write!(f, "Recorded DICE mode doesn't match"),
91 Self::UnsupportedEntrySize(sz) => write!(f, "Invalid entry size: {sz}"),
92 }
93 }
94}
95
96pub type Result<T> = core::result::Result<T, Error>;
97
98pub fn get_or_generate_instance_salt(
99 pci_root: &mut PciRoot,
100 dice_inputs: &PartialInputs,
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000101 secret: &[u8],
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000102) -> Result<(bool, Hidden)> {
103 let mut instance_img = find_instance_img(pci_root)?;
104
105 let entry = locate_entry(&mut instance_img)?;
106 trace!("Found pvmfw instance.img entry: {entry:?}");
107
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000108 let key = hkdf_sh512::<32>(secret, /*salt=*/ &[], b"vm-instance");
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000109 let mut blk = [0; BLK_SIZE];
110 match entry {
111 PvmfwEntry::Existing { header_index, payload_size } => {
112 if payload_size > blk.len() {
113 // We currently only support single-blk entries.
114 return Err(Error::UnsupportedEntrySize(payload_size));
115 }
116 let payload_index = header_index + 1;
117 instance_img.read_block(payload_index, &mut blk).map_err(Error::FailedIo)?;
118
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000119 let payload = &blk[..payload_size];
120 let mut entry = [0; size_of::<EntryBody>()];
121 let key = key.map_err(Error::FailedOpen)?;
122 let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedOpen)?;
123 let decrypted = aead.open(&mut entry, payload).map_err(Error::FailedOpen)?;
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000124
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100125 let body = EntryBody::read_from(decrypted).unwrap();
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000126 if body.code_hash != dice_inputs.code_hash {
127 Err(Error::RecordedCodeHashMismatch)
128 } else if body.auth_hash != dice_inputs.auth_hash {
129 Err(Error::RecordedAuthHashMismatch)
130 } else if body.mode() != dice_inputs.mode {
131 Err(Error::RecordedDiceModeMismatch)
132 } else {
133 Ok((false, body.salt))
134 }
135 }
136 PvmfwEntry::New { header_index } => {
Pierre-Clément Tosia59103d2023-02-02 14:46:55 +0000137 let salt = rand::random_array().map_err(Error::FailedSaltGeneration)?;
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100138 let body = EntryBody::new(dice_inputs, &salt);
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000139
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000140 let key = key.map_err(Error::FailedSeal)?;
141 let aead = AeadCtx::new_aes_256_gcm_randnonce(&key).map_err(Error::FailedSeal)?;
142 // We currently only support single-blk entries.
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100143 let plaintext = body.as_bytes();
144 assert!(plaintext.len() + aead.aead().unwrap().max_overhead() < blk.len());
145 let encrypted = aead.seal(&mut blk, plaintext).map_err(Error::FailedSeal)?;
Pierre-Clément Tosi90cd4f12023-02-17 11:19:56 +0000146 let payload_size = encrypted.len();
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000147 let payload_index = header_index + 1;
148 instance_img.write_block(payload_index, &blk).map_err(Error::FailedIo)?;
149
150 let header = EntryHeader::new(PvmfwEntry::UUID, payload_size);
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100151 header.write_to_prefix(blk.as_mut_slice()).unwrap();
152 blk[header.as_bytes().len()..].fill(0);
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000153 instance_img.write_block(header_index, &blk).map_err(Error::FailedIo)?;
154
155 Ok((true, salt))
156 }
157 }
158}
159
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100160#[derive(FromBytes)]
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000161#[repr(C, packed)]
162struct Header {
163 magic: [u8; Header::MAGIC.len()],
164 version: u16,
165}
166
167impl Header {
168 const MAGIC: &[u8] = b"Android-VM-instance";
169 const VERSION_1: u16 = 1;
170
171 pub fn is_valid(&self) -> bool {
172 self.magic == Self::MAGIC && self.version() == Self::VERSION_1
173 }
174
175 fn version(&self) -> u16 {
176 u16::from_le(self.version)
177 }
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000178}
179
180fn find_instance_img(pci_root: &mut PciRoot) -> Result<Partition> {
181 for device in VirtIOBlkIterator::new(pci_root) {
182 match Partition::get_by_name(device, "vm-instance") {
183 Ok(Some(p)) => return Ok(p),
184 Ok(None) => {}
185 Err(e) => log::warn!("error while reading from disk: {e}"),
186 };
187 }
188
189 Err(Error::MissingInstanceImage)
190}
191
192#[derive(Debug)]
193enum PvmfwEntry {
194 Existing { header_index: usize, payload_size: usize },
195 New { header_index: usize },
196}
197
198const BLK_SIZE: usize = Partitions::LBA_SIZE;
199
200impl PvmfwEntry {
201 const UUID: Uuid = Uuid::from_u128(0x90d2174a038a4bc6adf3824848fc5825);
202}
203
204fn locate_entry(partition: &mut Partition) -> Result<PvmfwEntry> {
205 let mut blk = [0; BLK_SIZE];
206 let mut indices = partition.indices();
207 let header_index = indices.next().ok_or(Error::MissingInstanceImageHeader)?;
208 partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
209 // The instance.img header is only used for discovery/validation.
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100210 let header = Header::read_from_prefix(blk.as_slice()).unwrap();
211 if !header.is_valid() {
212 return Err(Error::InvalidInstanceImageHeader);
213 }
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000214
215 while let Some(header_index) = indices.next() {
216 partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?;
217
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100218 let header = EntryHeader::read_from_prefix(blk.as_slice()).unwrap();
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000219 match (header.uuid(), header.payload_size()) {
220 (uuid, _) if uuid.is_nil() => return Ok(PvmfwEntry::New { header_index }),
221 (PvmfwEntry::UUID, payload_size) => {
222 return Ok(PvmfwEntry::Existing { header_index, payload_size })
223 }
224 (uuid, payload_size) => {
225 trace!("Skipping instance.img entry {uuid}: {payload_size:?} bytes");
226 let n = ceiling_div(payload_size, BLK_SIZE).unwrap();
227 if n > 0 {
228 let _ = indices.nth(n - 1); // consume
229 }
230 }
231 };
232 }
233
234 Err(Error::InstanceImageFull)
235}
236
237/// Marks the start of an instance.img entry.
238///
239/// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition".
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100240#[derive(AsBytes, FromBytes)]
241#[repr(C, packed)]
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000242struct EntryHeader {
243 uuid: u128,
244 payload_size: u64,
245}
246
247impl EntryHeader {
248 fn new(uuid: Uuid, payload_size: usize) -> Self {
Pierre-Clément Tosiafb126a2023-03-29 14:42:19 +0100249 Self { uuid: uuid.to_u128_le(), payload_size: u64::try_from(payload_size).unwrap().to_le() }
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000250 }
251
252 fn uuid(&self) -> Uuid {
Pierre-Clément Tosiafb126a2023-03-29 14:42:19 +0100253 Uuid::from_u128_le(self.uuid)
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000254 }
255
256 fn payload_size(&self) -> usize {
257 usize::try_from(u64::from_le(self.payload_size)).unwrap()
258 }
259}
260
Pierre-Clément Tosi8ad980f2023-04-25 18:23:11 +0100261#[derive(AsBytes, FromBytes)]
Pierre-Clément Tosi1cc5eb72023-02-02 11:09:18 +0000262#[repr(C)]
263struct EntryBody {
264 code_hash: Hash,
265 auth_hash: Hash,
266 salt: Hidden,
267 mode: u8,
268}
269
270impl EntryBody {
271 fn new(dice_inputs: &PartialInputs, salt: &Hidden) -> Self {
272 let mode = match dice_inputs.mode {
273 DiceMode::kDiceModeNotInitialized => 0,
274 DiceMode::kDiceModeNormal => 1,
275 DiceMode::kDiceModeDebug => 2,
276 DiceMode::kDiceModeMaintenance => 3,
277 };
278
279 Self {
280 code_hash: dice_inputs.code_hash,
281 auth_hash: dice_inputs.auth_hash,
282 salt: *salt,
283 mode,
284 }
285 }
286
287 fn mode(&self) -> DiceMode {
288 match self.mode {
289 1 => DiceMode::kDiceModeNormal,
290 2 => DiceMode::kDiceModeDebug,
291 3 => DiceMode::kDiceModeMaintenance,
292 _ => DiceMode::kDiceModeNotInitialized,
293 }
294 }
295}