blob: 358d88bb9e40fadbd9051fe8b51cc3fd8bbe4038 [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
Jiyong Parka999a322022-02-07 15:10:32 +090036use crate::ioutil;
37
Andrew Scullc96b72e2022-01-21 14:36:55 +000038use android_security_dice::aidl::android::security::dice::IDiceNode::IDiceNode;
Jiyong Park21ce2c52021-08-28 02:32:17 +090039use anyhow::{anyhow, bail, Context, Result};
Andrew Scullc96b72e2022-01-21 14:36:55 +000040use binder::wait_for_interface;
Jiyong Park21ce2c52021-08-28 02:32:17 +090041use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
Andrew Sculla0d1b1a2022-05-24 19:32:47 +000042use keystore2_crypto::ZVec;
43use openssl::hkdf::hkdf;
44use openssl::md::Md;
45use openssl::symm::{decrypt_aead, encrypt_aead, Cipher};
Jiyong Parkf7dea252021-09-08 01:42:54 +090046use serde::{Deserialize, Serialize};
Jiyong Park21ce2c52021-08-28 02:32:17 +090047use std::fs::{File, OpenOptions};
48use std::io::{Read, Seek, SeekFrom, Write};
49use uuid::Uuid;
50
51/// Path to the instance disk inside the VM
52const INSTANCE_IMAGE_PATH: &str = "/dev/block/by-name/vm-instance";
53
54/// Magic string in the instance disk header
55const DISK_HEADER_MAGIC: &str = "Android-VM-instance";
56
57/// Version of the instance disk format
58const DISK_HEADER_VERSION: u16 = 1;
59
60/// Size of the headers in the instance disk
61const DISK_HEADER_SIZE: u64 = 512;
62const PARTITION_HEADER_SIZE: u64 = 512;
63
64/// UUID of the partition that microdroid manager uses
65const MICRODROID_PARTITION_UUID: &str = "cf9afe9a-0662-11ec-a329-c32663a09d75";
66
Andrew Sculla0d1b1a2022-05-24 19:32:47 +000067/// Size of the AES256-GCM tag
68const AES_256_GCM_TAG_LENGTH: usize = 16;
69
70/// Size of the AES256-GCM nonce
71const AES_256_GCM_NONCE_LENGTH: usize = 12;
Jiyong Park21ce2c52021-08-28 02:32:17 +090072
73/// Handle to the instance disk
74pub struct InstanceDisk {
75 file: File,
76}
77
78/// Information from a partition header
79struct PartitionHeader {
80 uuid: Uuid,
81 payload_size: u64, // in bytes
82}
83
84/// Offset of a partition in the instance disk
85type PartitionOffset = u64;
86
87impl InstanceDisk {
88 /// Creates handle to instance disk
89 pub fn new() -> Result<Self> {
90 let mut file = OpenOptions::new()
91 .read(true)
92 .write(true)
93 .open(INSTANCE_IMAGE_PATH)
94 .with_context(|| format!("Failed to open {}", INSTANCE_IMAGE_PATH))?;
95
96 // Check if this file is a valid instance disk by examining the header (the first block)
97 let mut magic = [0; DISK_HEADER_MAGIC.len()];
98 file.read_exact(&mut magic)?;
99 if magic != DISK_HEADER_MAGIC.as_bytes() {
100 bail!("invalid magic: {:?}", magic);
101 }
102
103 let version = file.read_u16::<LittleEndian>()?;
104 if version == 0 {
105 bail!("invalid version: {}", version);
106 }
107 if version > DISK_HEADER_VERSION {
108 bail!("unsupported version: {}", version);
109 }
110
111 Ok(Self { file })
112 }
113
114 /// Reads the identity data that was written by microdroid manager. The returned data is
115 /// plaintext, although it is stored encrypted. In case when the partition for microdroid
116 /// manager doesn't exist, which can happen if it's the first boot, `Ok(None)` is returned.
Jiyong Parkf7dea252021-09-08 01:42:54 +0900117 pub fn read_microdroid_data(&mut self) -> Result<Option<MicrodroidData>> {
Jiyong Park21ce2c52021-08-28 02:32:17 +0900118 let (header, offset) = self.locate_microdroid_header()?;
119 if header.is_none() {
120 return Ok(None);
121 }
122 let header = header.unwrap();
123 let payload_offset = offset + PARTITION_HEADER_SIZE;
124 self.file.seek(SeekFrom::Start(payload_offset))?;
125
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000126 // Read the nonce (unencrypted)
127 let mut nonce = [0; AES_256_GCM_NONCE_LENGTH];
Jiyong Park21ce2c52021-08-28 02:32:17 +0900128 self.file.read_exact(&mut nonce)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900129
130 // Read the encrypted payload
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000131 let payload_size =
132 header.payload_size as usize - AES_256_GCM_NONCE_LENGTH - AES_256_GCM_TAG_LENGTH;
133 let mut data = vec![0; payload_size];
Jiyong Park21ce2c52021-08-28 02:32:17 +0900134 self.file.read_exact(&mut data)?;
135
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000136 // Read the tag
137 let mut tag = [0; AES_256_GCM_TAG_LENGTH];
138 self.file.read_exact(&mut tag)?;
139
Jiyong Park21ce2c52021-08-28 02:32:17 +0900140 // Read the header as well because it's part of the signed data (though not encrypted).
141 let mut header = [0; PARTITION_HEADER_SIZE as usize];
142 self.file.seek(SeekFrom::Start(offset))?;
143 self.file.read_exact(&mut header)?;
144
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000145 // Decrypt and authenticate the data (along with the header).
146 let key = get_key()?;
147 let plaintext =
148 decrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &tag)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900149
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000150 let microdroid_data = serde_cbor::from_slice(plaintext.as_slice())?;
Jiyong Parkf7dea252021-09-08 01:42:54 +0900151 Ok(Some(microdroid_data))
Jiyong Park21ce2c52021-08-28 02:32:17 +0900152 }
153
154 /// Writes identity data to the partition for microdroid manager. The partition is appended
155 /// if it doesn't exist. The data is stored encrypted.
Jiyong Parkf7dea252021-09-08 01:42:54 +0900156 pub fn write_microdroid_data(&mut self, microdroid_data: &MicrodroidData) -> Result<()> {
Jiyong Park21ce2c52021-08-28 02:32:17 +0900157 let (header, offset) = self.locate_microdroid_header()?;
158
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000159 let data = serde_cbor::to_vec(microdroid_data)?;
Jiyong Parkf7dea252021-09-08 01:42:54 +0900160
Jiyong Park21ce2c52021-08-28 02:32:17 +0900161 // By encrypting and signing the data, tag will be appended. The tag also becomes part of
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000162 // the encrypted payload which will be written. In addition, a nonce will be prepended
163 // (non-encrypted).
164 let payload_size = (AES_256_GCM_NONCE_LENGTH + data.len() + AES_256_GCM_TAG_LENGTH) as u64;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900165
166 // If the partition exists, make sure we don't change the partition size. If not (i.e.
167 // partition is not found), write the header at the empty place.
168 if let Some(header) = header {
169 if header.payload_size != payload_size {
170 bail!("Can't change payload size from {} to {}", header.payload_size, payload_size);
171 }
172 } else {
173 let uuid = Uuid::parse_str(MICRODROID_PARTITION_UUID)?;
174 self.write_header_at(offset, &uuid, payload_size)?;
175 }
176
177 // Read the header as it is used as additionally authenticated data (AAD).
178 let mut header = [0; PARTITION_HEADER_SIZE as usize];
179 self.file.seek(SeekFrom::Start(offset))?;
180 self.file.read_exact(&mut header)?;
181
182 // Generate a nonce randomly and recorde it on the disk first.
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000183 let nonce = rand::random::<[u8; AES_256_GCM_NONCE_LENGTH]>();
Jiyong Park21ce2c52021-08-28 02:32:17 +0900184 self.file.seek(SeekFrom::Start(offset + PARTITION_HEADER_SIZE))?;
185 self.file.write_all(nonce.as_ref())?;
186
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000187 // Then encrypt and sign the data.
188 let key = get_key()?;
189 let mut tag = [0; AES_256_GCM_TAG_LENGTH];
190 let ciphertext =
191 encrypt_aead(Cipher::aes_256_gcm(), &key, Some(&nonce), &header, &data, &mut tag)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900192
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000193 // Persist the encrypted payload data and the tag.
194 self.file.write_all(&ciphertext)?;
195 self.file.write_all(&tag)?;
Jiyong Parka999a322022-02-07 15:10:32 +0900196 ioutil::blkflsbuf(&mut self.file)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900197
198 Ok(())
199 }
200
201 /// Read header at `header_offset` and parse it into a `PartitionHeader`.
202 fn read_header_at(&mut self, header_offset: u64) -> Result<PartitionHeader> {
203 assert!(
204 header_offset % PARTITION_HEADER_SIZE == 0,
205 "header offset {} is not aligned to 512 bytes",
206 header_offset
207 );
208
209 let mut uuid = [0; 16];
210 self.file.seek(SeekFrom::Start(header_offset))?;
211 self.file.read_exact(&mut uuid)?;
212 let uuid = Uuid::from_bytes(uuid);
213 let payload_size = self.file.read_u64::<LittleEndian>()?;
214
215 Ok(PartitionHeader { uuid, payload_size })
216 }
217
218 /// Write header at `header_offset`
219 fn write_header_at(
220 &mut self,
221 header_offset: u64,
222 uuid: &Uuid,
223 payload_size: u64,
224 ) -> Result<()> {
225 self.file.seek(SeekFrom::Start(header_offset))?;
226 self.file.write_all(uuid.as_bytes())?;
227 self.file.write_u64::<LittleEndian>(payload_size)?;
228 Ok(())
229 }
230
231 /// Locate the header of the partition for microdroid manager. A pair of `PartitionHeader` and
232 /// the offset of the partition in the disk is returned. If the partition is not found,
233 /// `PartitionHeader` is `None` and the offset points to the empty partition that can be used
234 /// for the partition.
235 fn locate_microdroid_header(&mut self) -> Result<(Option<PartitionHeader>, PartitionOffset)> {
236 let microdroid_uuid = Uuid::parse_str(MICRODROID_PARTITION_UUID)?;
237
238 // the first partition header is located just after the disk header
239 let mut header_offset = DISK_HEADER_SIZE;
240 loop {
241 let header = self.read_header_at(header_offset)?;
242 if header.uuid == microdroid_uuid {
243 // found a matching header
244 return Ok((Some(header), header_offset));
245 } else if header.uuid == Uuid::nil() {
246 // found an empty space
247 return Ok((None, header_offset));
248 }
249 // Move to the next partition. Be careful about overflow.
250 let payload_size = round_to_multiple(header.payload_size, PARTITION_HEADER_SIZE)?;
251 let part_size = payload_size
252 .checked_add(PARTITION_HEADER_SIZE)
253 .ok_or_else(|| anyhow!("partition too large"))?;
254 header_offset = header_offset
255 .checked_add(part_size)
256 .ok_or_else(|| anyhow!("next partition at invalid offset"))?;
257 }
258 }
259}
260
261/// Round `n` up to the nearest multiple of `unit`
262fn round_to_multiple(n: u64, unit: u64) -> Result<u64> {
263 assert!((unit & (unit - 1)) == 0, "{} is not power of two", unit);
264 let ret = (n + unit - 1) & !(unit - 1);
265 if ret < n {
266 bail!("overflow")
267 }
268 Ok(ret)
269}
270
Jiyong Park21ce2c52021-08-28 02:32:17 +0900271/// Returns the key that is used to encrypt the microdroid manager partition. It is derived from
272/// the sealing CDI of the previous stage, which is Android Boot Loader (ABL).
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000273fn get_key() -> Result<ZVec> {
Andrew Scullc96b72e2022-01-21 14:36:55 +0000274 // Sealing CDI from the previous stage.
275 let diced = wait_for_interface::<dyn IDiceNode>("android.security.dice.IDiceNode")
276 .context("IDiceNode service not found")?;
277 let bcc_handover = diced.derive(&[]).context("Failed to get BccHandover")?;
Andrew Sculla0d1b1a2022-05-24 19:32:47 +0000278 // Deterministically derive another key to use for encrypting the data, rather than using the
279 // CDI directly, so we have the chance to rotate the key if needed. A salt isn't needed as the
280 // input key material is already cryptographically strong.
281 let salt = &[];
282 let info = b"microdroid_manager_key".as_ref();
283 let mut key = ZVec::new(32)?;
284 hkdf(&mut key, Md::sha256(), &bcc_handover.cdiSeal, salt, info)?;
285 Ok(key)
Jiyong Park21ce2c52021-08-28 02:32:17 +0900286}
Jiyong Parkf7dea252021-09-08 01:42:54 +0900287
Chris Wailes6f5a9b52022-08-11 15:01:54 -0700288#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
Jiyong Parkf7dea252021-09-08 01:42:54 +0900289pub struct MicrodroidData {
Andrew Scull34916a72022-01-30 21:34:24 +0000290 pub salt: Vec<u8>, // Should be [u8; 64] but that isn't serializable.
Jiyong Parkf7dea252021-09-08 01:42:54 +0900291 pub apk_data: ApkData,
Inseob Kim197748b2021-12-01 19:49:00 +0900292 pub extra_apks_data: Vec<ApkData>,
Jooyung Han7a343f92021-09-08 22:53:11 +0900293 pub apex_data: Vec<ApexData>,
Jiyong Parkf7dea252021-09-08 01:42:54 +0900294}
295
Chris Wailes6f5a9b52022-08-11 15:01:54 -0700296#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
Jiyong Parkf7dea252021-09-08 01:42:54 +0900297pub struct ApkData {
298 pub root_hash: Box<RootHash>,
Jiyong Parka41535b2021-09-10 19:31:48 +0900299 pub pubkey: Box<[u8]>,
Jiyong Parkf7dea252021-09-08 01:42:54 +0900300}
301
302pub type RootHash = [u8];
Jooyung Han7a343f92021-09-08 22:53:11 +0900303
Chris Wailes6f5a9b52022-08-11 15:01:54 -0700304#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
Jooyung Han7a343f92021-09-08 22:53:11 +0900305pub struct ApexData {
306 pub name: String,
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}