blob: 7a9f0e0962f4edc5e073c6254a0a34f7f8f2f8be [file] [log] [blame]
Jiyong Park21ce2c52021-08-28 02:32:17 +09001// 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
Alan Stokes1125e012023-10-13 12:31:10 +010036use crate::dice_driver::DiceDriver;
Jiyong Parka999a322022-02-07 15:10:32 +090037use crate::ioutil;
38
Jiyong Park21ce2c52021-08-28 02:32:17 +090039use anyhow::{anyhow, bail, Context, Result};
40use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
Andrew Sculla0d1b1a2022-05-24 19:32:47 +000041use openssl::symm::{decrypt_aead, encrypt_aead, Cipher};
Jiyong Parkf7dea252021-09-08 01:42:54 +090042use serde::{Deserialize, Serialize};
Jiyong Park21ce2c52021-08-28 02:32:17 +090043use std::fs::{File, OpenOptions};
44use std::io::{Read, Seek, SeekFrom, Write};
45use uuid::Uuid;
46
47/// Path to the instance disk inside the VM
48const INSTANCE_IMAGE_PATH: &str = "/dev/block/by-name/vm-instance";
49
Andrew Sculld64ae7d2022-10-05 17:41:43 +000050/// Identifier for the key used to seal the instance data.
51const INSTANCE_KEY_IDENTIFIER: &[u8] = b"microdroid_manager_key";
52
Jiyong Park21ce2c52021-08-28 02:32:17 +090053/// Magic string in the instance disk header
54const DISK_HEADER_MAGIC: &str = "Android-VM-instance";
55
56/// Version of the instance disk format
57const DISK_HEADER_VERSION: u16 = 1;
58
59/// Size of the headers in the instance disk
60const DISK_HEADER_SIZE: u64 = 512;
61const PARTITION_HEADER_SIZE: u64 = 512;
62
63/// UUID of the partition that microdroid manager uses
64const MICRODROID_PARTITION_UUID: &str = "cf9afe9a-0662-11ec-a329-c32663a09d75";
65
Andrew Sculla0d1b1a2022-05-24 19:32:47 +000066/// Size of the AES256-GCM tag
67const AES_256_GCM_TAG_LENGTH: usize = 16;
68
69/// Size of the AES256-GCM nonce
70const AES_256_GCM_NONCE_LENGTH: usize = 12;
Jiyong Park21ce2c52021-08-28 02:32:17 +090071
72/// Handle to the instance disk
73pub struct InstanceDisk {
74 file: File,
75}
76
77/// Information from a partition header
78struct PartitionHeader {
79 uuid: Uuid,
80 payload_size: u64, // in bytes
81}
82
83/// Offset of a partition in the instance disk
84type PartitionOffset = u64;
85
86impl InstanceDisk {
87 /// Creates handle to instance disk
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.
Andrew Sculld64ae7d2022-10-05 17:41:43 +0000116 pub fn read_microdroid_data(&mut self, dice: &DiceDriver) -> Result<Option<MicrodroidData>> {
Jiyong Park21ce2c52021-08-28 02:32:17 +0900117 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
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000125 // Read the nonce (unencrypted)
126 let mut nonce = [0; AES_256_GCM_NONCE_LENGTH];
Jiyong Park21ce2c52021-08-28 02:32:17 +0900127 self.file.read_exact(&mut nonce)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900128
129 // Read the encrypted payload
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000130 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];
Jiyong Park21ce2c52021-08-28 02:32:17 +0900133 self.file.read_exact(&mut data)?;
134
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000135 // Read the tag
136 let mut tag = [0; AES_256_GCM_TAG_LENGTH];
137 self.file.read_exact(&mut tag)?;
138
Jiyong Park21ce2c52021-08-28 02:32:17 +0900139 // 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
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000144 // Decrypt and authenticate the data (along with the header).
Alice Wang7e6c9352023-02-15 15:44:13 +0000145 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)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900148
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000149 let microdroid_data = serde_cbor::from_slice(plaintext.as_slice())?;
Jiyong Parkf7dea252021-09-08 01:42:54 +0900150 Ok(Some(microdroid_data))
Jiyong Park21ce2c52021-08-28 02:32:17 +0900151 }
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.
Andrew Sculld64ae7d2022-10-05 17:41:43 +0000155 pub fn write_microdroid_data(
156 &mut self,
157 microdroid_data: &MicrodroidData,
158 dice: &DiceDriver,
159 ) -> Result<()> {
Jiyong Park21ce2c52021-08-28 02:32:17 +0900160 let (header, offset) = self.locate_microdroid_header()?;
161
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000162 let data = serde_cbor::to_vec(microdroid_data)?;
Jiyong Parkf7dea252021-09-08 01:42:54 +0900163
Jiyong Park21ce2c52021-08-28 02:32:17 +0900164 // By encrypting and signing the data, tag will be appended. The tag also becomes part of
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000165 // 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;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900168
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.
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000186 let nonce = rand::random::<[u8; AES_256_GCM_NONCE_LENGTH]>();
Jiyong Park21ce2c52021-08-28 02:32:17 +0900187 self.file.seek(SeekFrom::Start(offset + PARTITION_HEADER_SIZE))?;
188 self.file.write_all(nonce.as_ref())?;
189
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000190 // Then encrypt and sign the data.
Alice Wang7e6c9352023-02-15 15:44:13 +0000191 let cipher = Cipher::aes_256_gcm();
192 let key = dice.get_sealing_key(INSTANCE_KEY_IDENTIFIER, cipher.key_len())?;
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000193 let mut tag = [0; AES_256_GCM_TAG_LENGTH];
Alice Wang7e6c9352023-02-15 15:44:13 +0000194 let ciphertext = encrypt_aead(cipher, &key, Some(&nonce), &header, &data, &mut tag)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900195
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000196 // Persist the encrypted payload data and the tag.
197 self.file.write_all(&ciphertext)?;
198 self.file.write_all(&tag)?;
Jiyong Parka999a322022-02-07 15:10:32 +0900199 ioutil::blkflsbuf(&mut self.file)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900200
201 Ok(())
202 }
203
204 /// Read header at `header_offset` and parse it into a `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`
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.
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`
265fn 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
Chris Wailes6f5a9b52022-08-11 15:01:54 -0700274#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
Jiyong Parkf7dea252021-09-08 01:42:54 +0900275pub struct MicrodroidData {
Andrew Scull34916a72022-01-30 21:34:24 +0000276 pub salt: Vec<u8>, // Should be [u8; 64] but that isn't serializable.
Jiyong Parkf7dea252021-09-08 01:42:54 +0900277 pub apk_data: ApkData,
Inseob Kim197748b2021-12-01 19:49:00 +0900278 pub extra_apks_data: Vec<ApkData>,
Jooyung Han7a343f92021-09-08 22:53:11 +0900279 pub apex_data: Vec<ApexData>,
Jiyong Parkf7dea252021-09-08 01:42:54 +0900280}
281
Alice Wang061478b2023-04-11 13:26:17 +0000282impl MicrodroidData {
283 pub fn extra_apk_root_hash_eq(&self, i: usize, root_hash: &[u8]) -> bool {
284 self.extra_apks_data.get(i).map_or(false, |apk| apk.root_hash_eq(root_hash))
285 }
286}
287
Chris Wailes6f5a9b52022-08-11 15:01:54 -0700288#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
Jiyong Parkf7dea252021-09-08 01:42:54 +0900289pub struct ApkData {
Alan Stokes1508df22023-12-04 11:31:21 +0000290 pub root_hash: Vec<u8>,
291 pub cert_hash: Vec<u8>,
Alan Stokes9b8b8ec2023-10-13 15:58:11 +0100292 pub package_name: String,
293 pub version_code: u64,
Jiyong Parkf7dea252021-09-08 01:42:54 +0900294}
295
Alice Wang061478b2023-04-11 13:26:17 +0000296impl ApkData {
297 pub fn root_hash_eq(&self, root_hash: &[u8]) -> bool {
Alan Stokes1508df22023-12-04 11:31:21 +0000298 self.root_hash == root_hash
Alice Wang061478b2023-04-11 13:26:17 +0000299 }
300}
301
Chris Wailes6f5a9b52022-08-11 15:01:54 -0700302#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
Jooyung Han7a343f92021-09-08 22:53:11 +0900303pub struct ApexData {
304 pub name: String,
Alan Stokes185e34c2023-11-24 12:28:39 +0000305 pub manifest_name: Option<String>,
306 pub manifest_version: Option<i64>,
Jooyung Hanc8deb472021-09-13 13:48:25 +0900307 pub public_key: Vec<u8>,
308 pub root_digest: Vec<u8>,
Andrew Walbran40be9d52022-01-19 14:32:53 +0000309 pub last_update_seconds: u64,
Jiyong Parkd6502352022-01-27 01:07:30 +0900310 pub is_factory: bool,
Jooyung Han7a343f92021-09-08 22:53:11 +0900311}