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