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