Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 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 | |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 36 | use android_security_dice::aidl::android::security::dice::IDiceNode::IDiceNode; |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 37 | use anyhow::{anyhow, bail, Context, Result}; |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 38 | use binder::wait_for_interface; |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 39 | use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; |
| 40 | use ring::aead::{Aad, Algorithm, LessSafeKey, Nonce, UnboundKey, AES_256_GCM}; |
| 41 | use ring::hkdf::{Salt, HKDF_SHA256}; |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 42 | use serde::{Deserialize, Serialize}; |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 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 | /// Magic string in the instance disk header |
| 51 | const DISK_HEADER_MAGIC: &str = "Android-VM-instance"; |
| 52 | |
| 53 | /// Version of the instance disk format |
| 54 | const DISK_HEADER_VERSION: u16 = 1; |
| 55 | |
| 56 | /// Size of the headers in the instance disk |
| 57 | const DISK_HEADER_SIZE: u64 = 512; |
| 58 | const PARTITION_HEADER_SIZE: u64 = 512; |
| 59 | |
| 60 | /// UUID of the partition that microdroid manager uses |
| 61 | const MICRODROID_PARTITION_UUID: &str = "cf9afe9a-0662-11ec-a329-c32663a09d75"; |
| 62 | |
| 63 | /// Encryption algorithm used to cipher payload |
| 64 | static ENCRYPT_ALG: &Algorithm = &AES_256_GCM; |
| 65 | |
| 66 | /// Handle to the instance disk |
| 67 | pub struct InstanceDisk { |
| 68 | file: File, |
| 69 | } |
| 70 | |
| 71 | /// Information from a partition header |
| 72 | struct PartitionHeader { |
| 73 | uuid: Uuid, |
| 74 | payload_size: u64, // in bytes |
| 75 | } |
| 76 | |
| 77 | /// Offset of a partition in the instance disk |
| 78 | type PartitionOffset = u64; |
| 79 | |
| 80 | impl InstanceDisk { |
| 81 | /// Creates handle to instance disk |
| 82 | pub fn new() -> Result<Self> { |
| 83 | let mut file = OpenOptions::new() |
| 84 | .read(true) |
| 85 | .write(true) |
| 86 | .open(INSTANCE_IMAGE_PATH) |
| 87 | .with_context(|| format!("Failed to open {}", INSTANCE_IMAGE_PATH))?; |
| 88 | |
| 89 | // Check if this file is a valid instance disk by examining the header (the first block) |
| 90 | let mut magic = [0; DISK_HEADER_MAGIC.len()]; |
| 91 | file.read_exact(&mut magic)?; |
| 92 | if magic != DISK_HEADER_MAGIC.as_bytes() { |
| 93 | bail!("invalid magic: {:?}", magic); |
| 94 | } |
| 95 | |
| 96 | let version = file.read_u16::<LittleEndian>()?; |
| 97 | if version == 0 { |
| 98 | bail!("invalid version: {}", version); |
| 99 | } |
| 100 | if version > DISK_HEADER_VERSION { |
| 101 | bail!("unsupported version: {}", version); |
| 102 | } |
| 103 | |
| 104 | Ok(Self { file }) |
| 105 | } |
| 106 | |
| 107 | /// Reads the identity data that was written by microdroid manager. The returned data is |
| 108 | /// plaintext, although it is stored encrypted. In case when the partition for microdroid |
| 109 | /// manager doesn't exist, which can happen if it's the first boot, `Ok(None)` is returned. |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 110 | pub fn read_microdroid_data(&mut self) -> Result<Option<MicrodroidData>> { |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 111 | let (header, offset) = self.locate_microdroid_header()?; |
| 112 | if header.is_none() { |
| 113 | return Ok(None); |
| 114 | } |
| 115 | let header = header.unwrap(); |
| 116 | let payload_offset = offset + PARTITION_HEADER_SIZE; |
| 117 | self.file.seek(SeekFrom::Start(payload_offset))?; |
| 118 | |
| 119 | // Read the 12-bytes nonce (unencrypted) |
| 120 | let mut nonce = [0; 12]; |
| 121 | self.file.read_exact(&mut nonce)?; |
| 122 | let nonce = Nonce::assume_unique_for_key(nonce); |
| 123 | |
| 124 | // Read the encrypted payload |
| 125 | let payload_size = header.payload_size - 12; // we already have read the nonce |
| 126 | let mut data = vec![0; payload_size as usize]; |
| 127 | self.file.read_exact(&mut data)?; |
| 128 | |
| 129 | // Read the header as well because it's part of the signed data (though not encrypted). |
| 130 | let mut header = [0; PARTITION_HEADER_SIZE as usize]; |
| 131 | self.file.seek(SeekFrom::Start(offset))?; |
| 132 | self.file.read_exact(&mut header)?; |
| 133 | |
| 134 | // Decrypt and authenticate the data (along with the header). The data is decrypted in |
| 135 | // place. `open_in_place` returns slice to the decrypted part in the buffer. |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 136 | let plaintext_len = get_key()?.open_in_place(nonce, Aad::from(&header), &mut data)?.len(); |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 137 | // Truncate to remove the tag |
| 138 | data.truncate(plaintext_len); |
| 139 | |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 140 | let microdroid_data = serde_cbor::from_slice(data.as_slice())?; |
| 141 | Ok(Some(microdroid_data)) |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 142 | } |
| 143 | |
| 144 | /// Writes identity data to the partition for microdroid manager. The partition is appended |
| 145 | /// if it doesn't exist. The data is stored encrypted. |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 146 | pub fn write_microdroid_data(&mut self, microdroid_data: &MicrodroidData) -> Result<()> { |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 147 | let (header, offset) = self.locate_microdroid_header()?; |
| 148 | |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 149 | let mut data = serde_cbor::to_vec(microdroid_data)?; |
| 150 | |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 151 | // By encrypting and signing the data, tag will be appended. The tag also becomes part of |
| 152 | // the encrypted payload which will be written. In addition, a 12-bytes nonce will be |
| 153 | // prepended (non-encrypted). |
| 154 | let payload_size = (data.len() + ENCRYPT_ALG.tag_len() + 12) as u64; |
| 155 | |
| 156 | // If the partition exists, make sure we don't change the partition size. If not (i.e. |
| 157 | // partition is not found), write the header at the empty place. |
| 158 | if let Some(header) = header { |
| 159 | if header.payload_size != payload_size { |
| 160 | bail!("Can't change payload size from {} to {}", header.payload_size, payload_size); |
| 161 | } |
| 162 | } else { |
| 163 | let uuid = Uuid::parse_str(MICRODROID_PARTITION_UUID)?; |
| 164 | self.write_header_at(offset, &uuid, payload_size)?; |
| 165 | } |
| 166 | |
| 167 | // Read the header as it is used as additionally authenticated data (AAD). |
| 168 | let mut header = [0; PARTITION_HEADER_SIZE as usize]; |
| 169 | self.file.seek(SeekFrom::Start(offset))?; |
| 170 | self.file.read_exact(&mut header)?; |
| 171 | |
| 172 | // Generate a nonce randomly and recorde it on the disk first. |
| 173 | let nonce = Nonce::assume_unique_for_key(rand::random::<[u8; 12]>()); |
| 174 | self.file.seek(SeekFrom::Start(offset + PARTITION_HEADER_SIZE))?; |
| 175 | self.file.write_all(nonce.as_ref())?; |
| 176 | |
| 177 | // Then encrypt and sign the data. The non-encrypted input data is copied to a vector |
| 178 | // because it is encrypted in place, and also the tag is appended. |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 179 | get_key()?.seal_in_place_append_tag(nonce, Aad::from(&header), &mut data)?; |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 180 | |
| 181 | // Persist the encrypted payload data |
| 182 | self.file.write_all(&data)?; |
| 183 | self.file.flush()?; |
| 184 | |
| 185 | Ok(()) |
| 186 | } |
| 187 | |
| 188 | /// Read header at `header_offset` and parse it into a `PartitionHeader`. |
| 189 | fn read_header_at(&mut self, header_offset: u64) -> Result<PartitionHeader> { |
| 190 | assert!( |
| 191 | header_offset % PARTITION_HEADER_SIZE == 0, |
| 192 | "header offset {} is not aligned to 512 bytes", |
| 193 | header_offset |
| 194 | ); |
| 195 | |
| 196 | let mut uuid = [0; 16]; |
| 197 | self.file.seek(SeekFrom::Start(header_offset))?; |
| 198 | self.file.read_exact(&mut uuid)?; |
| 199 | let uuid = Uuid::from_bytes(uuid); |
| 200 | let payload_size = self.file.read_u64::<LittleEndian>()?; |
| 201 | |
| 202 | Ok(PartitionHeader { uuid, payload_size }) |
| 203 | } |
| 204 | |
| 205 | /// Write header at `header_offset` |
| 206 | fn write_header_at( |
| 207 | &mut self, |
| 208 | header_offset: u64, |
| 209 | uuid: &Uuid, |
| 210 | payload_size: u64, |
| 211 | ) -> Result<()> { |
| 212 | self.file.seek(SeekFrom::Start(header_offset))?; |
| 213 | self.file.write_all(uuid.as_bytes())?; |
| 214 | self.file.write_u64::<LittleEndian>(payload_size)?; |
| 215 | Ok(()) |
| 216 | } |
| 217 | |
| 218 | /// Locate the header of the partition for microdroid manager. A pair of `PartitionHeader` and |
| 219 | /// the offset of the partition in the disk is returned. If the partition is not found, |
| 220 | /// `PartitionHeader` is `None` and the offset points to the empty partition that can be used |
| 221 | /// for the partition. |
| 222 | fn locate_microdroid_header(&mut self) -> Result<(Option<PartitionHeader>, PartitionOffset)> { |
| 223 | let microdroid_uuid = Uuid::parse_str(MICRODROID_PARTITION_UUID)?; |
| 224 | |
| 225 | // the first partition header is located just after the disk header |
| 226 | let mut header_offset = DISK_HEADER_SIZE; |
| 227 | loop { |
| 228 | let header = self.read_header_at(header_offset)?; |
| 229 | if header.uuid == microdroid_uuid { |
| 230 | // found a matching header |
| 231 | return Ok((Some(header), header_offset)); |
| 232 | } else if header.uuid == Uuid::nil() { |
| 233 | // found an empty space |
| 234 | return Ok((None, header_offset)); |
| 235 | } |
| 236 | // Move to the next partition. Be careful about overflow. |
| 237 | let payload_size = round_to_multiple(header.payload_size, PARTITION_HEADER_SIZE)?; |
| 238 | let part_size = payload_size |
| 239 | .checked_add(PARTITION_HEADER_SIZE) |
| 240 | .ok_or_else(|| anyhow!("partition too large"))?; |
| 241 | header_offset = header_offset |
| 242 | .checked_add(part_size) |
| 243 | .ok_or_else(|| anyhow!("next partition at invalid offset"))?; |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | |
| 248 | /// Round `n` up to the nearest multiple of `unit` |
| 249 | fn round_to_multiple(n: u64, unit: u64) -> Result<u64> { |
| 250 | assert!((unit & (unit - 1)) == 0, "{} is not power of two", unit); |
| 251 | let ret = (n + unit - 1) & !(unit - 1); |
| 252 | if ret < n { |
| 253 | bail!("overflow") |
| 254 | } |
| 255 | Ok(ret) |
| 256 | } |
| 257 | |
| 258 | struct ZeroOnDropKey(LessSafeKey); |
| 259 | |
| 260 | impl Drop for ZeroOnDropKey { |
| 261 | fn drop(&mut self) { |
| 262 | // Zeroize the key by overwriting it with a key constructed from zeros of same length |
| 263 | // This works because the raw key bytes are allocated inside the struct, not on the heap |
| 264 | let zero = [0; 32]; |
| 265 | let zero_key = LessSafeKey::new(UnboundKey::new(ENCRYPT_ALG, &zero).unwrap()); |
| 266 | unsafe { |
| 267 | ::std::ptr::write_volatile::<LessSafeKey>(&mut self.0, zero_key); |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | impl std::ops::Deref for ZeroOnDropKey { |
| 273 | type Target = LessSafeKey; |
| 274 | fn deref(&self) -> &LessSafeKey { |
| 275 | &self.0 |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | /// Returns the key that is used to encrypt the microdroid manager partition. It is derived from |
| 280 | /// the sealing CDI of the previous stage, which is Android Boot Loader (ABL). |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 281 | fn get_key() -> Result<ZeroOnDropKey> { |
| 282 | // Sealing CDI from the previous stage. |
| 283 | let diced = wait_for_interface::<dyn IDiceNode>("android.security.dice.IDiceNode") |
| 284 | .context("IDiceNode service not found")?; |
| 285 | let bcc_handover = diced.derive(&[]).context("Failed to get BccHandover")?; |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 286 | |
| 287 | // Derive a key from the Sealing CDI |
| 288 | // Step 1 is extraction: https://datatracker.ietf.org/doc/html/rfc5869#section-2.2 where a |
| 289 | // pseduo random key (PRK) is extracted from (Input Keying Material - IKM, which is secret) and |
| 290 | // optional salt. |
| 291 | let salt = Salt::new(HKDF_SHA256, &[]); // use 0 as salt |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 292 | let prk = salt.extract(&bcc_handover.cdiSeal); // Sealing CDI as IKM |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 293 | |
| 294 | // Step 2 is expansion: https://datatracker.ietf.org/doc/html/rfc5869#section-2.3 where the PRK |
| 295 | // (optionally with the `info` which gives contextual information) is expanded into the output |
| 296 | // keying material (OKM). Note that the process fails only when the size of OKM is longer than |
| 297 | // 255 * SHA256_HASH_SIZE (32), which isn't the case here. |
| 298 | let info = [b"microdroid_manager_key".as_ref()]; |
| 299 | let okm = prk.expand(&info, HKDF_SHA256).unwrap(); // doesn't fail as explained above |
| 300 | let mut key = [0; 32]; |
| 301 | okm.fill(&mut key).unwrap(); // doesn't fail as explained above |
| 302 | |
| 303 | // The term LessSafe might be misleading here. LessSafe here just means that the API can |
| 304 | // possibly accept same nonces for different messages. However, since we encrypt/decrypt only a |
| 305 | // single message (the microdroid_manager partition payload) with a randomly generated nonce, |
| 306 | // this is safe enough. |
| 307 | let ret = ZeroOnDropKey(LessSafeKey::new(UnboundKey::new(ENCRYPT_ALG, &key).unwrap())); |
| 308 | |
| 309 | // Don't forget to zeroize the raw key array as well |
| 310 | unsafe { |
| 311 | ::std::ptr::write_volatile::<[u8; 32]>(&mut key, [0; 32]); |
| 312 | } |
| 313 | |
Andrew Scull | c96b72e | 2022-01-21 14:36:55 +0000 | [diff] [blame] | 314 | Ok(ret) |
Jiyong Park | 21ce2c5 | 2021-08-28 02:32:17 +0900 | [diff] [blame] | 315 | } |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 316 | |
Jooyung Han | 7a343f9 | 2021-09-08 22:53:11 +0900 | [diff] [blame] | 317 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 318 | pub struct MicrodroidData { |
| 319 | pub apk_data: ApkData, |
Inseob Kim | 197748b | 2021-12-01 19:49:00 +0900 | [diff] [blame] | 320 | pub extra_apks_data: Vec<ApkData>, |
Jooyung Han | 7a343f9 | 2021-09-08 22:53:11 +0900 | [diff] [blame] | 321 | pub apex_data: Vec<ApexData>, |
Jiyong Park | 9f72ea6 | 2021-12-06 21:18:38 +0900 | [diff] [blame] | 322 | pub bootconfig: Box<[u8]>, |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 323 | } |
| 324 | |
Jooyung Han | 7a343f9 | 2021-09-08 22:53:11 +0900 | [diff] [blame] | 325 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 326 | pub struct ApkData { |
| 327 | pub root_hash: Box<RootHash>, |
Jiyong Park | a41535b | 2021-09-10 19:31:48 +0900 | [diff] [blame] | 328 | pub pubkey: Box<[u8]>, |
Jiyong Park | f7dea25 | 2021-09-08 01:42:54 +0900 | [diff] [blame] | 329 | } |
| 330 | |
| 331 | pub type RootHash = [u8]; |
Jooyung Han | 7a343f9 | 2021-09-08 22:53:11 +0900 | [diff] [blame] | 332 | |
| 333 | #[derive(Debug, Serialize, Deserialize, PartialEq)] |
| 334 | pub struct ApexData { |
| 335 | pub name: String, |
Jooyung Han | c8deb47 | 2021-09-13 13:48:25 +0900 | [diff] [blame] | 336 | pub public_key: Vec<u8>, |
| 337 | pub root_digest: Vec<u8>, |
Andrew Walbran | 40be9d5 | 2022-01-19 14:32:53 +0000 | [diff] [blame] | 338 | pub last_update_seconds: u64, |
Jiyong Park | d650235 | 2022-01-27 01:07:30 +0900 | [diff] [blame^] | 339 | pub is_factory: bool, |
Jooyung Han | 7a343f9 | 2021-09-08 22:53:11 +0900 | [diff] [blame] | 340 | } |