1 // Copyright 2021, 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 //! Provides routines to read/write on the instance disk.
16 //!
17 //! Instance disk is a disk where the identity of a VM instance is recorded. The identity usually
18 //! includes certificates of the VM payload that is trusted, but not limited to it. Instance disk
19 //! is empty when a VM is first booted. The identity data is filled in during the first boot, and
20 //! then encrypted and signed. Subsequent boots decrypts and authenticates the data and uses the
21 //! identity data to further verify the payload (e.g. against the certificate).
22 //!
23 //! Instance disk consists of a disk header and one or more partitions each of which consists of a
24 //! header and payload. Each header (both the disk header and a partition header) is 512 bytes
25 //! long. Payload is just next to the header and its size can be arbitrary. Headers are located at
26 //! 512 bytes boundaries. So, when the size of a payload is not multiple of 512, there exists a gap
27 //! between the end of the payload and the start of the next partition (if there is any).
28 //!
29 //! Each partition is identified by a UUID. A partition is created for a program loader that
30 //! participates in the boot chain of the VM. Each program loader is expected to locate the
31 //! partition that corresponds to the loader using the UUID that is assigned to the loader.
32 //!
33 //! The payload of a partition is encrypted/signed by a key that is unique to the loader and to the
34 //! VM as well. Failing to decrypt/authenticate a partition by a loader stops the boot process.
35
36 use crate::ioutil;
37
38 use anyhow::{anyhow, bail, Context, Result};
39 use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
40 use dice_driver::DiceDriver;
41 use openssl::symm::{decrypt_aead, encrypt_aead, Cipher};
42 use serde::{Deserialize, Serialize};
43 use std::fs::{File, OpenOptions};
44 use std::io::{Read, Seek, SeekFrom, Write};
45 use uuid::Uuid;
46
47 /// Path to the instance disk inside the VM
48 const INSTANCE_IMAGE_PATH: &str = "/dev/block/by-name/vm-instance";
49
50 /// Identifier for the key used to seal the instance data.
51 const INSTANCE_KEY_IDENTIFIER: &[u8] = b"microdroid_manager_key";
52
53 /// Magic string in the instance disk header
54 const DISK_HEADER_MAGIC: &str = "Android-VM-instance";
55
56 /// Version of the instance disk format
57 const DISK_HEADER_VERSION: u16 = 1;
58
59 /// Size of the headers in the instance disk
60 const DISK_HEADER_SIZE: u64 = 512;
61 const PARTITION_HEADER_SIZE: u64 = 512;
62
63 /// UUID of the partition that microdroid manager uses
64 const MICRODROID_PARTITION_UUID: &str = "cf9afe9a-0662-11ec-a329-c32663a09d75";
65
66 /// Size of the AES256-GCM tag
67 const AES_256_GCM_TAG_LENGTH: usize = 16;
68
69 /// Size of the AES256-GCM nonce
70 const AES_256_GCM_NONCE_LENGTH: usize = 12;
71
72 /// Handle to the instance disk
73 pub struct InstanceDisk {
74 file: File,
75 }
76
77 /// Information from a partition header
78 struct PartitionHeader {
79 uuid: Uuid,
80 payload_size: u64, // in bytes
81 }
82
83 /// Offset of a partition in the instance disk
84 type PartitionOffset = u64;
85
86 impl InstanceDisk {
87 /// Creates handle to instance disk
new() -> Result<Self>88 pub fn new() -> Result<Self> {
89 let mut file = OpenOptions::new()
90 .read(true)
91 .write(true)
92 .open(INSTANCE_IMAGE_PATH)
93 .with_context(|| format!("Failed to open {}", INSTANCE_IMAGE_PATH))?;
94
95 // Check if this file is a valid instance disk by examining the header (the first block)
96 let mut magic = [0; DISK_HEADER_MAGIC.len()];
97 file.read_exact(&mut magic)?;
98 if magic != DISK_HEADER_MAGIC.as_bytes() {
99 bail!("invalid magic: {:?}", magic);
100 }
101
102 let version = file.read_u16::<LittleEndian>()?;
103 if version == 0 {
104 bail!("invalid version: {}", version);
105 }
106 if version > DISK_HEADER_VERSION {
107 bail!("unsupported version: {}", version);
108 }
109
110 Ok(Self { file })
111 }
112
113 /// Reads the identity data that was written by microdroid manager. The returned data is
114 /// plaintext, although it is stored encrypted. In case when the partition for microdroid
115 /// manager doesn't exist, which can happen if it's the first boot, `Ok(None)` is returned.
read_microdroid_data(&mut self, dice: &DiceDriver) -> Result<Option<MicrodroidData>>116 pub fn read_microdroid_data(&mut self, dice: &DiceDriver) -> Result<Option<MicrodroidData>> {
117 let (header, offset) = self.locate_microdroid_header()?;
118 if header.is_none() {
119 return Ok(None);
120 }
121 let header = header.unwrap();
122 let payload_offset = offset + PARTITION_HEADER_SIZE;
123 self.file.seek(SeekFrom::Start(payload_offset))?;
124
125 // Read the nonce (unencrypted)
126 let mut nonce = [0; AES_256_GCM_NONCE_LENGTH];
127 self.file.read_exact(&mut nonce)?;
128
129 // Read the encrypted payload
130 let payload_size =
131 header.payload_size as usize - AES_256_GCM_NONCE_LENGTH - AES_256_GCM_TAG_LENGTH;
132 let mut data = vec![0; payload_size];
133 self.file.read_exact(&mut data)?;
134
135 // Read the tag
136 let mut tag = [0; AES_256_GCM_TAG_LENGTH];
137 self.file.read_exact(&mut tag)?;
138
139 // Read the header as well because it's part of the signed data (though not encrypted).
140 let mut header = [0; PARTITION_HEADER_SIZE as usize];
141 self.file.seek(SeekFrom::Start(offset))?;
142 self.file.read_exact(&mut header)?;
143
144 // Decrypt and authenticate the data (along with the header).
145 let cipher = Cipher::aes_256_gcm();
146 let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
147 let plaintext = decrypt_aead(cipher, &key, Some(&nonce), &header, &data, &tag)?;
148
149 let microdroid_data = serde_cbor::from_slice(plaintext.as_slice())?;
150 Ok(Some(microdroid_data))
151 }
152
153 /// Writes identity data to the partition for microdroid manager. The partition is appended
154 /// if it doesn't exist. The data is stored encrypted.
write_microdroid_data( &mut self, microdroid_data: &MicrodroidData, dice: &DiceDriver, ) -> Result<()>155 pub fn write_microdroid_data(
156 &mut self,
157 microdroid_data: &MicrodroidData,
158 dice: &DiceDriver,
159 ) -> Result<()> {
160 let (header, offset) = self.locate_microdroid_header()?;
161
162 let data = serde_cbor::to_vec(microdroid_data)?;
163
164 // By encrypting and signing the data, tag will be appended. The tag also becomes part of
165 // the encrypted payload which will be written. In addition, a nonce will be prepended
166 // (non-encrypted).
167 let payload_size = (AES_256_GCM_NONCE_LENGTH + data.len() + AES_256_GCM_TAG_LENGTH) as u64;
168
169 // If the partition exists, make sure we don't change the partition size. If not (i.e.
170 // partition is not found), write the header at the empty place.
171 if let Some(header) = header {
172 if header.payload_size != payload_size {
173 bail!("Can't change payload size from {} to {}", header.payload_size, payload_size);
174 }
175 } else {
176 let uuid = Uuid::parse_str(MICRODROID_PARTITION_UUID)?;
177 self.write_header_at(offset, &uuid, payload_size)?;
178 }
179
180 // Read the header as it is used as additionally authenticated data (AAD).
181 let mut header = [0; PARTITION_HEADER_SIZE as usize];
182 self.file.seek(SeekFrom::Start(offset))?;
183 self.file.read_exact(&mut header)?;
184
185 // Generate a nonce randomly and recorde it on the disk first.
186 let nonce = rand::random::<[u8; AES_256_GCM_NONCE_LENGTH]>();
187 self.file.seek(SeekFrom::Start(offset + PARTITION_HEADER_SIZE))?;
188 self.file.write_all(nonce.as_ref())?;
189
190 // Then encrypt and sign the data.
191 let cipher = Cipher::aes_256_gcm();
192 let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
193 let mut tag = [0; AES_256_GCM_TAG_LENGTH];
194 let ciphertext = encrypt_aead(cipher, &key, Some(&nonce), &header, &data, &mut tag)?;
195
196 // Persist the encrypted payload data and the tag.
197 self.file.write_all(&ciphertext)?;
198 self.file.write_all(&tag)?;
199 ioutil::blkflsbuf(&mut self.file)?;
200
201 Ok(())
202 }
203
204 /// Read header at `header_offset` and parse it into a `PartitionHeader`.
read_header_at(&mut self, header_offset: u64) -> Result<PartitionHeader>205 fn read_header_at(&mut self, header_offset: u64) -> Result<PartitionHeader> {
206 assert!(
207 header_offset % PARTITION_HEADER_SIZE == 0,
208 "header offset {} is not aligned to 512 bytes",
209 header_offset
210 );
211
212 let mut uuid = [0; 16];
213 self.file.seek(SeekFrom::Start(header_offset))?;
214 self.file.read_exact(&mut uuid)?;
215 let uuid = Uuid::from_bytes(uuid);
216 let payload_size = self.file.read_u64::<LittleEndian>()?;
217
218 Ok(PartitionHeader { uuid, payload_size })
219 }
220
221 /// Write header at `header_offset`
write_header_at( &mut self, header_offset: u64, uuid: &Uuid, payload_size: u64, ) -> Result<()>222 fn write_header_at(
223 &mut self,
224 header_offset: u64,
225 uuid: &Uuid,
226 payload_size: u64,
227 ) -> Result<()> {
228 self.file.seek(SeekFrom::Start(header_offset))?;
229 self.file.write_all(uuid.as_bytes())?;
230 self.file.write_u64::<LittleEndian>(payload_size)?;
231 Ok(())
232 }
233
234 /// Locate the header of the partition for microdroid manager. A pair of `PartitionHeader` and
235 /// the offset of the partition in the disk is returned. If the partition is not found,
236 /// `PartitionHeader` is `None` and the offset points to the empty partition that can be used
237 /// for the partition.
locate_microdroid_header(&mut self) -> Result<(Option<PartitionHeader>, PartitionOffset)>238 fn locate_microdroid_header(&mut self) -> Result<(Option<PartitionHeader>, PartitionOffset)> {
239 let microdroid_uuid = Uuid::parse_str(MICRODROID_PARTITION_UUID)?;
240
241 // the first partition header is located just after the disk header
242 let mut header_offset = DISK_HEADER_SIZE;
243 loop {
244 let header = self.read_header_at(header_offset)?;
245 if header.uuid == microdroid_uuid {
246 // found a matching header
247 return Ok((Some(header), header_offset));
248 } else if header.uuid == Uuid::nil() {
249 // found an empty space
250 return Ok((None, header_offset));
251 }
252 // Move to the next partition. Be careful about overflow.
253 let payload_size = round_to_multiple(header.payload_size, PARTITION_HEADER_SIZE)?;
254 let part_size = payload_size
255 .checked_add(PARTITION_HEADER_SIZE)
256 .ok_or_else(|| anyhow!("partition too large"))?;
257 header_offset = header_offset
258 .checked_add(part_size)
259 .ok_or_else(|| anyhow!("next partition at invalid offset"))?;
260 }
261 }
262 }
263
264 /// Round `n` up to the nearest multiple of `unit`
round_to_multiple(n: u64, unit: u64) -> Result<u64>265 fn round_to_multiple(n: u64, unit: u64) -> Result<u64> {
266 assert!((unit & (unit - 1)) == 0, "{} is not power of two", unit);
267 let ret = (n + unit - 1) & !(unit - 1);
268 if ret < n {
269 bail!("overflow")
270 }
271 Ok(ret)
272 }
273
274 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
275 pub struct MicrodroidData {
276 // `salt` is obsolete, it was used as a differentiator for non-protected VM instances running
277 // same payload. Instance-id (present in DT) is used for that now.
278 pub salt: Vec<u8>, // Should be [u8; 64] but that isn't serializable.
279 pub apk_data: ApkData,
280 pub extra_apks_data: Vec<ApkData>,
281 pub apex_data: Vec<ApexData>,
282 }
283
284 impl MicrodroidData {
extra_apk_root_hash_eq(&self, i: usize, root_hash: &[u8]) -> bool285 pub fn extra_apk_root_hash_eq(&self, i: usize, root_hash: &[u8]) -> bool {
286 self.extra_apks_data.get(i).map_or(false, |apk| apk.root_hash_eq(root_hash))
287 }
288 }
289
290 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
291 pub struct ApkData {
292 pub root_hash: Vec<u8>,
293 pub cert_hash: Vec<u8>,
294 pub package_name: String,
295 pub version_code: u64,
296 }
297
298 impl ApkData {
root_hash_eq(&self, root_hash: &[u8]) -> bool299 pub fn root_hash_eq(&self, root_hash: &[u8]) -> bool {
300 self.root_hash == root_hash
301 }
302 }
303
304 #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
305 pub struct ApexData {
306 pub name: String,
307 pub manifest_name: Option<String>,
308 pub manifest_version: Option<i64>,
309 pub public_key: Vec<u8>,
310 pub root_digest: Vec<u8>,
311 pub last_update_seconds: u64,
312 pub is_factory: bool,
313 }
314