blob: 5a77198128e7562ef5b28f8d026950891bc2a9a0 [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
Andrew Scullc96b72e2022-01-21 14:36:55 +000036use android_security_dice::aidl::android::security::dice::IDiceNode::IDiceNode;
Jiyong Park21ce2c52021-08-28 02:32:17 +090037use anyhow::{anyhow, bail, Context, Result};
Andrew Scullc96b72e2022-01-21 14:36:55 +000038use binder::wait_for_interface;
Jiyong Park21ce2c52021-08-28 02:32:17 +090039use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
40use ring::aead::{Aad, Algorithm, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
41use ring::hkdf::{Salt, HKDF_SHA256};
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
50/// Magic string in the instance disk header
51const DISK_HEADER_MAGIC: &str = "Android-VM-instance";
52
53/// Version of the instance disk format
54const DISK_HEADER_VERSION: u16 = 1;
55
56/// Size of the headers in the instance disk
57const DISK_HEADER_SIZE: u64 = 512;
58const PARTITION_HEADER_SIZE: u64 = 512;
59
60/// UUID of the partition that microdroid manager uses
61const MICRODROID_PARTITION_UUID: &str = "cf9afe9a-0662-11ec-a329-c32663a09d75";
62
63/// Encryption algorithm used to cipher payload
64static ENCRYPT_ALG: &Algorithm = &AES_256_GCM;
65
66/// Handle to the instance disk
67pub struct InstanceDisk {
68 file: File,
69}
70
71/// Information from a partition header
72struct PartitionHeader {
73 uuid: Uuid,
74 payload_size: u64, // in bytes
75}
76
77/// Offset of a partition in the instance disk
78type PartitionOffset = u64;
79
80impl 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 Parkf7dea252021-09-08 01:42:54 +0900110 pub fn read_microdroid_data(&mut self) -> Result<Option<MicrodroidData>> {
Jiyong Park21ce2c52021-08-28 02:32:17 +0900111 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 Scullc96b72e2022-01-21 14:36:55 +0000136 let plaintext_len = get_key()?.open_in_place(nonce, Aad::from(&header), &mut data)?.len();
Jiyong Park21ce2c52021-08-28 02:32:17 +0900137 // Truncate to remove the tag
138 data.truncate(plaintext_len);
139
Jiyong Parkf7dea252021-09-08 01:42:54 +0900140 let microdroid_data = serde_cbor::from_slice(data.as_slice())?;
141 Ok(Some(microdroid_data))
Jiyong Park21ce2c52021-08-28 02:32:17 +0900142 }
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 Parkf7dea252021-09-08 01:42:54 +0900146 pub fn write_microdroid_data(&mut self, microdroid_data: &MicrodroidData) -> Result<()> {
Jiyong Park21ce2c52021-08-28 02:32:17 +0900147 let (header, offset) = self.locate_microdroid_header()?;
148
Jiyong Parkf7dea252021-09-08 01:42:54 +0900149 let mut data = serde_cbor::to_vec(microdroid_data)?;
150
Jiyong Park21ce2c52021-08-28 02:32:17 +0900151 // 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 Scullc96b72e2022-01-21 14:36:55 +0000179 get_key()?.seal_in_place_append_tag(nonce, Aad::from(&header), &mut data)?;
Jiyong Park21ce2c52021-08-28 02:32:17 +0900180
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`
249fn 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
258struct ZeroOnDropKey(LessSafeKey);
259
260impl 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
272impl 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 Scullc96b72e2022-01-21 14:36:55 +0000281fn 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 Park21ce2c52021-08-28 02:32:17 +0900286
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 Scullc96b72e2022-01-21 14:36:55 +0000292 let prk = salt.extract(&bcc_handover.cdiSeal); // Sealing CDI as IKM
Jiyong Park21ce2c52021-08-28 02:32:17 +0900293
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 Scullc96b72e2022-01-21 14:36:55 +0000314 Ok(ret)
Jiyong Park21ce2c52021-08-28 02:32:17 +0900315}
Jiyong Parkf7dea252021-09-08 01:42:54 +0900316
Jooyung Han7a343f92021-09-08 22:53:11 +0900317#[derive(Debug, Serialize, Deserialize, PartialEq)]
Jiyong Parkf7dea252021-09-08 01:42:54 +0900318pub struct MicrodroidData {
319 pub apk_data: ApkData,
Inseob Kim197748b2021-12-01 19:49:00 +0900320 pub extra_apks_data: Vec<ApkData>,
Jooyung Han7a343f92021-09-08 22:53:11 +0900321 pub apex_data: Vec<ApexData>,
Jiyong Park9f72ea62021-12-06 21:18:38 +0900322 pub bootconfig: Box<[u8]>,
Jiyong Parkf7dea252021-09-08 01:42:54 +0900323}
324
Jooyung Han7a343f92021-09-08 22:53:11 +0900325#[derive(Debug, Serialize, Deserialize, PartialEq)]
Jiyong Parkf7dea252021-09-08 01:42:54 +0900326pub struct ApkData {
327 pub root_hash: Box<RootHash>,
Jiyong Parka41535b2021-09-10 19:31:48 +0900328 pub pubkey: Box<[u8]>,
Jiyong Parkf7dea252021-09-08 01:42:54 +0900329}
330
331pub type RootHash = [u8];
Jooyung Han7a343f92021-09-08 22:53:11 +0900332
333#[derive(Debug, Serialize, Deserialize, PartialEq)]
334pub struct ApexData {
335 pub name: String,
Jooyung Hanc8deb472021-09-13 13:48:25 +0900336 pub public_key: Vec<u8>,
337 pub root_digest: Vec<u8>,
Andrew Walbran40be9d52022-01-19 14:32:53 +0000338 pub last_update_seconds: u64,
Jiyong Parkd6502352022-01-27 01:07:30 +0900339 pub is_factory: bool,
Jooyung Han7a343f92021-09-08 22:53:11 +0900340}