forked from brl/citadel-tools
540 lines
16 KiB
Rust
540 lines
16 KiB
Rust
use std::fs::{File,OpenOptions};
|
|
use std::io::{Read, Write};
|
|
use std::path::Path;
|
|
|
|
use toml;
|
|
|
|
use crate::blockdev::AlignedBuffer;
|
|
use crate::{Result, BlockDev, public_key_for_channel, PublicKey};
|
|
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
|
use std::sync::atomic::{Ordering,AtomicIsize};
|
|
use std::os::unix::fs::MetadataExt;
|
|
use std::io;
|
|
|
|
/// Expected magic value in header
|
|
const MAGIC: &[u8] = b"SGOS";
|
|
|
|
/// Offset into header of the start of the metainfo document
|
|
const METAINFO_OFFSET: usize = 8;
|
|
|
|
/// Signature is 64 bytes long
|
|
const SIGNATURE_LENGTH: usize = 64;
|
|
|
|
/// Maximum amount of space in block for metainfo document
|
|
const MAX_METAINFO_LEN: usize = ImageHeader::HEADER_SIZE - (METAINFO_OFFSET + SIGNATURE_LENGTH);
|
|
|
|
fn is_valid_status_code(code: u8) -> bool {
|
|
code <= ImageHeader::STATUS_BAD_META
|
|
}
|
|
|
|
///
|
|
/// The Image Header structure is stored in a 4096 byte block at the start of
|
|
/// every resource image file. When an image is installed to a partition it
|
|
/// is stored at the last 4096 byte block of the block device for the partition.
|
|
///
|
|
/// The layout of this structure is the following:
|
|
///
|
|
/// field size (bytes) offset
|
|
/// ----- ------------ ------
|
|
///
|
|
/// magic 4 0
|
|
/// status 1 4
|
|
/// flags 1 5
|
|
/// length 2 6
|
|
///
|
|
/// metainfo <length> 8
|
|
///
|
|
/// signature 64 8 + length
|
|
///
|
|
/// magic : Must match ascii bytes 'SGOS' for the header to be considered valid
|
|
///
|
|
/// status : One of the `STATUS` constants defined below
|
|
///
|
|
/// flags : May contain 'FLAG' values defined below.
|
|
///
|
|
/// length : The size of the metainfo field in bytes as a 16-bit Big Endian value
|
|
///
|
|
/// metainfo : A utf-8 encoded TOML document with various fields describing the image
|
|
///
|
|
/// signature : ed25519 signature over the bytes of the metainfo field
|
|
///
|
|
|
|
pub struct ImageHeader {
|
|
buffer: RwLock<HeaderBytes>,
|
|
metainfo: Mutex<Option<Arc<MetaInfo>>>,
|
|
timestamp: AtomicIsize,
|
|
}
|
|
|
|
struct HeaderBytes([u8; ImageHeader::HEADER_SIZE]);
|
|
|
|
impl HeaderBytes {
|
|
|
|
fn create_empty() -> RwLock<Self> {
|
|
let mut buffer = HeaderBytes::new();
|
|
buffer.clear();
|
|
RwLock::new(buffer)
|
|
}
|
|
|
|
fn create_from_slice(slice: &[u8]) -> RwLock<Self> {
|
|
assert_eq!(slice.len(), ImageHeader::HEADER_SIZE);
|
|
let mut buffer = HeaderBytes::new();
|
|
buffer.0.copy_from_slice(slice);
|
|
RwLock::new(buffer)
|
|
}
|
|
|
|
fn new() -> Self {
|
|
HeaderBytes([0u8; ImageHeader::HEADER_SIZE])
|
|
}
|
|
|
|
fn clear(&mut self) {
|
|
for b in &mut self.0[..] {
|
|
*b = 0;
|
|
}
|
|
self.write_bytes(0, MAGIC);
|
|
}
|
|
|
|
fn read_u8(&self, idx: usize) -> u8 {
|
|
self.0[idx]
|
|
}
|
|
|
|
fn write_u8(&mut self, idx: usize, val: u8) {
|
|
self.0[idx] = val;
|
|
}
|
|
|
|
fn read_u16(&self, idx: usize) -> u16 {
|
|
let hi = u16::from(self.read_u8(idx));
|
|
let lo = u16::from(self.read_u8(idx + 1));
|
|
(hi << 8) | lo
|
|
}
|
|
|
|
fn write_u16(&mut self, idx: usize, val: u16) {
|
|
let hi = (val >> 8) as u8;
|
|
let lo = val as u8;
|
|
self.write_u8(idx, hi);
|
|
self.write_u8(idx + 1, lo);
|
|
}
|
|
|
|
fn set_metainfo_len(&mut self, len: usize) {
|
|
self.write_u16(6, len as u16);
|
|
}
|
|
|
|
fn write_bytes(&mut self, offset: usize, data: &[u8]) {
|
|
self.0[offset..offset + data.len()].copy_from_slice(data)
|
|
}
|
|
|
|
fn read_bytes(&self, offset: usize, len: usize) -> Vec<u8> {
|
|
Vec::from(&self.0[offset..offset + len])
|
|
}
|
|
|
|
}
|
|
const CODE_TO_LABEL: [&str; 7] = [
|
|
"Invalid",
|
|
"New",
|
|
"Try Boot",
|
|
"Good",
|
|
"Failed Boot",
|
|
"Bad Signature",
|
|
"Bad Metainfo",
|
|
];
|
|
|
|
impl ImageHeader {
|
|
pub const FLAG_PREFER_BOOT: u8 = 0x01; // Set to override usual strategy for choosing a partition to boot and force this one.
|
|
pub const FLAG_HASH_TREE: u8 = 0x02; // dm-verity hash tree data is appended to the image
|
|
pub const FLAG_DATA_COMPRESSED: u8 = 0x04; // The image data is compressed and needs to be uncompressed before use.
|
|
|
|
pub const STATUS_INVALID: u8 = 0; // Set on partition before writing a new rootfs disk image
|
|
pub const STATUS_NEW: u8 = 1; // Set on partition after write of new rootfs disk image completes successfully
|
|
pub const STATUS_TRY_BOOT: u8 = 2; // Set on boot selected partition if in `STATUS_NEW` state.
|
|
pub const STATUS_GOOD: u8 = 3; // Set on boot when a `STATUS_TRY_BOOT` partition successfully launches desktop
|
|
pub const STATUS_FAILED: u8 = 4; // Set on boot for any partition in state `STATUS_TRY_BOOT`
|
|
pub const STATUS_BAD_SIG: u8 = 5; // Set on boot selected partition when signature fails to verify
|
|
pub const STATUS_BAD_META: u8 = 6; // Set on partition when metainfo cannot be parsed
|
|
|
|
/// Size of header block
|
|
pub const HEADER_SIZE: usize = 4096;
|
|
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
/// Reload header if file has changed on disk
|
|
pub fn reload_if_stale<P: AsRef<Path>>(&self, path: P) -> Result<bool> {
|
|
let path = path.as_ref();
|
|
if !path.exists() {
|
|
bail!("cannot reload header because image file {:?} is missing", path);
|
|
}
|
|
let reload = self.is_stale(path)?;
|
|
if reload {
|
|
self.reload_file(path)?;
|
|
}
|
|
Ok(reload)
|
|
}
|
|
|
|
fn is_stale(&self, path: &Path) -> Result<bool> {
|
|
let (_,ts) = Self::file_metadata(path)?;
|
|
let stale = self.timestamp.swap(ts, Ordering::SeqCst) != ts;
|
|
Ok(stale)
|
|
}
|
|
|
|
fn reload_file(&self, path: &Path) -> Result<()> {
|
|
let header = Self::from_file(path)?;
|
|
let header_lock = header.metainfo.lock().unwrap();
|
|
let mut lock = self.metainfo.lock().unwrap();
|
|
self.bytes_mut().0.copy_from_slice(&header.bytes().0);
|
|
*lock = (*header_lock).clone();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
let path = path.as_ref();
|
|
let (size,ts) = Self::file_metadata(path)?;
|
|
if size < Self::HEADER_SIZE {
|
|
bail!("cannot load image header from {:?} because file is too short ({})", path, size);
|
|
}
|
|
let mut f = File::open(path)
|
|
.map_err(context!("failed to load image header from {:?}", path))?;
|
|
|
|
let mut header = Self::from_reader(&mut f)?;
|
|
*header.timestamp.get_mut() = ts;
|
|
Ok(header)
|
|
}
|
|
|
|
// returns tuple of (size,mtime)
|
|
fn file_metadata(path: &Path) -> Result<(usize, isize)> {
|
|
let metadata = path.metadata()
|
|
.map_err(context!("failed to load image header from {:?}", path))?;
|
|
Ok((metadata.len() as usize, metadata.mtime() as isize))
|
|
}
|
|
|
|
pub fn from_reader<R: Read>(r: &mut R) -> Result<Self> {
|
|
let mut v = vec![0u8; Self::HEADER_SIZE];
|
|
r.read_exact(&mut v)
|
|
.map_err(context!("error reading header bytes"))?;
|
|
Self::from_slice(&v)
|
|
}
|
|
|
|
fn from_slice(slice: &[u8]) -> Result<Self> {
|
|
assert_eq!(slice.len(), Self::HEADER_SIZE);
|
|
let buffer = HeaderBytes::create_from_slice(slice);
|
|
let metainfo = Mutex::new(None);
|
|
let timestamp = AtomicIsize::new(0);
|
|
let header = ImageHeader { buffer, metainfo, timestamp };
|
|
header.load_metainfo_if_magic_valid()?;
|
|
Ok(header)
|
|
}
|
|
|
|
pub fn from_partition<P: AsRef<Path>>(path: P) -> Result<Self> {
|
|
let mut dev = BlockDev::open_ro(path.as_ref())?;
|
|
let nsectors = dev.nsectors()?;
|
|
if nsectors < 8 {
|
|
bail!("cannot load/store header from block device {:?} because it's too short ({} sectors)", path.as_ref(), nsectors);
|
|
}
|
|
let mut buffer = AlignedBuffer::new(Self::HEADER_SIZE);
|
|
dev.read_sectors(nsectors - 8, buffer.as_mut())?;
|
|
Self::from_slice(buffer.as_ref())
|
|
}
|
|
|
|
pub fn write_partition<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
|
let mut dev = BlockDev::open_rw(path.as_ref())?;
|
|
let nsectors = dev.nsectors()?;
|
|
if nsectors < 8 {
|
|
bail!("cannot load/store header from block device {:?} because it's too short ({} sectors)", path.as_ref(), nsectors);
|
|
}
|
|
let lock = self.bytes();
|
|
let buffer = AlignedBuffer::from_slice(&lock.0);
|
|
dev.write_sectors(nsectors - 8, buffer.as_ref())?;
|
|
Ok(())
|
|
}
|
|
|
|
|
|
fn bytes(&self) -> RwLockReadGuard<HeaderBytes> {
|
|
self.buffer.read().unwrap()
|
|
}
|
|
|
|
fn bytes_mut(&self) -> RwLockWriteGuard<HeaderBytes> {
|
|
self.buffer.write().unwrap()
|
|
}
|
|
|
|
fn with_bytes<F,R>(&self, f: F) -> R
|
|
where F: FnOnce(&HeaderBytes) -> R
|
|
{
|
|
f(&self.bytes())
|
|
}
|
|
|
|
fn with_bytes_mut<F,R>(&self, f: F) -> R
|
|
where F: FnOnce(&mut HeaderBytes) -> R
|
|
{
|
|
f(&mut self.bytes_mut())
|
|
}
|
|
|
|
fn load_metainfo_if_magic_valid(&self) -> Result<()> {
|
|
if !self.is_magic_valid() {
|
|
return Ok(())
|
|
}
|
|
|
|
let mut lock = self.metainfo.lock().unwrap();
|
|
let mb = self.metainfo_bytes();
|
|
let metainfo = MetaInfo::parse_bytes(&mb)
|
|
.ok_or(format_err!("image header has invalid metainfo"))?;
|
|
*lock = Some(Arc::new(metainfo));
|
|
Ok(())
|
|
}
|
|
|
|
pub fn metainfo(&self) -> Arc<MetaInfo> {
|
|
let lock = self.metainfo.lock().unwrap();
|
|
lock.as_ref().expect("Header has no metainfo set").clone()
|
|
}
|
|
|
|
pub fn is_magic_valid(&self) -> bool {
|
|
self.with_bytes(|bs| bs.read_bytes(0,4) == MAGIC)
|
|
}
|
|
|
|
pub fn status(&self) -> u8 {
|
|
self.read_u8(4)
|
|
}
|
|
|
|
pub fn set_status(&self, status: u8) {
|
|
self.write_u8(4, status);
|
|
}
|
|
|
|
pub fn status_code_label(&self) -> String {
|
|
let code = self.status();
|
|
|
|
if is_valid_status_code(code) {
|
|
CODE_TO_LABEL[code as usize].to_string()
|
|
} else {
|
|
format!("Invalid status code: {}", code)
|
|
}
|
|
}
|
|
|
|
pub fn flags(&self) -> u8 {
|
|
self.read_u8(5)
|
|
}
|
|
|
|
pub fn has_flag(&self, flag: u8) -> bool {
|
|
(self.flags() & flag) == flag
|
|
}
|
|
|
|
/// Return `true` if flag value changed
|
|
pub fn set_flag(&self, flag: u8) -> bool {
|
|
self.change_flag(flag, true)
|
|
}
|
|
|
|
pub fn clear_flag(&self, flag: u8) -> bool {
|
|
self.change_flag(flag, false)
|
|
}
|
|
|
|
fn change_flag(&self, flag: u8, set: bool) -> bool {
|
|
let old = self.flags();
|
|
let new = if set { old | flag } else { old & !flag };
|
|
self.write_u8(5, new);
|
|
old == new
|
|
}
|
|
|
|
pub fn metainfo_len(&self) -> usize {
|
|
self.read_u16(6) as usize
|
|
}
|
|
|
|
pub fn update_metainfo<P: AsRef<Path>>(&self, metainfo_bytes: &[u8], signature: &[u8], path: P) -> Result<()> {
|
|
self.set_metainfo_bytes(metainfo_bytes)?;
|
|
self.set_signature(signature);
|
|
self.write_header_to(path)
|
|
}
|
|
|
|
pub fn set_metainfo_bytes(&self, bytes: &[u8]) -> Result<()> {
|
|
let metainfo = MetaInfo::parse_bytes(bytes)
|
|
.ok_or(format_err!("cannot parse header metainfo bytes as a valid metainfo document"))?;
|
|
|
|
let mut lock = self.metainfo.lock().unwrap();
|
|
self.with_bytes_mut(|bs| {
|
|
bs.0.iter_mut().skip(8).for_each(|b| *b = 0);
|
|
bs.set_metainfo_len(bytes.len());
|
|
bs.write_bytes(8,bytes);
|
|
});
|
|
*lock = Some(Arc::new(metainfo));
|
|
Ok(())
|
|
}
|
|
|
|
pub fn metainfo_bytes(&self) -> Vec<u8> {
|
|
let mlen = self.metainfo_len();
|
|
assert!(mlen > 0 && mlen < MAX_METAINFO_LEN);
|
|
self.read_bytes(METAINFO_OFFSET, mlen)
|
|
}
|
|
|
|
pub fn has_signature(&self) -> bool {
|
|
self.signature().iter().any(|b| *b != 0)
|
|
}
|
|
|
|
pub fn signature(&self) -> Vec<u8> {
|
|
let mlen = self.metainfo_len();
|
|
assert!(mlen > 0 && mlen < MAX_METAINFO_LEN);
|
|
self.read_bytes(METAINFO_OFFSET + mlen, SIGNATURE_LENGTH)
|
|
}
|
|
|
|
pub fn set_signature(&self, signature: &[u8]) {
|
|
assert_eq!(signature.len(), SIGNATURE_LENGTH, "Signature has invalid length");
|
|
let mlen = self.metainfo_len();
|
|
self.write_bytes(8 + mlen, signature);
|
|
}
|
|
|
|
pub fn clear_signature(&self) {
|
|
let zeros = vec![0u8; SIGNATURE_LENGTH];
|
|
self.set_signature(&zeros);
|
|
}
|
|
|
|
pub fn public_key(&self) -> Result<Option<PublicKey>> {
|
|
public_key_for_channel(self.metainfo().channel())
|
|
}
|
|
|
|
pub fn verify_signature(&self, pubkey: PublicKey) -> bool {
|
|
pubkey.verify(&self.metainfo_bytes(), &self.signature())
|
|
}
|
|
|
|
pub fn write_header<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
|
self.with_bytes(|bs| writer.write_all(&bs.0))
|
|
}
|
|
|
|
pub fn write_header_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
|
let path = path.as_ref();
|
|
let w = OpenOptions::new().write(true).open(path)
|
|
.map_err(context!("failed to open image file {:?} to write header", path))?;
|
|
self.write_header(w)
|
|
.map_err(context!("error writing header to image file {:?}", path))
|
|
}
|
|
|
|
fn read_u8(&self, idx: usize) -> u8 {
|
|
self.with_bytes(|bs| bs.read_u8(idx))
|
|
}
|
|
|
|
fn write_u8(&self, idx: usize, val: u8) {
|
|
self.with_bytes_mut(|bs| bs.write_u8(idx, val))
|
|
}
|
|
|
|
fn read_u16(&self, idx: usize) -> u16 {
|
|
self.with_bytes(|bs| bs.read_u16(idx))
|
|
}
|
|
|
|
fn write_bytes(&self, offset: usize, data: &[u8]) {
|
|
self.with_bytes_mut(|bs| bs.write_bytes(offset, data))
|
|
}
|
|
|
|
fn read_bytes(&self, offset: usize, len: usize) -> Vec<u8> {
|
|
self.with_bytes(|bs| bs.read_bytes(offset, len))
|
|
}
|
|
}
|
|
|
|
impl Default for ImageHeader {
|
|
fn default() -> Self {
|
|
let metainfo = Mutex::new(None);
|
|
let buffer = HeaderBytes::create_empty();
|
|
let timestamp = AtomicIsize::new(0);
|
|
ImageHeader { buffer, metainfo, timestamp }
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Serialize, Clone, Default)]
|
|
pub struct MetaInfo {
|
|
#[serde(rename = "image-type")]
|
|
image_type: String,
|
|
|
|
#[serde(default)]
|
|
channel: String,
|
|
|
|
#[serde(rename = "kernel-version")]
|
|
kernel_version: Option<String>,
|
|
|
|
#[serde(rename = "kernel-id")]
|
|
kernel_id: Option<String>,
|
|
|
|
#[serde(rename = "realmfs-name")]
|
|
realmfs_name: Option<String>,
|
|
|
|
#[serde(rename = "realmfs-owner")]
|
|
realmfs_owner: Option<String>,
|
|
|
|
#[serde(default)]
|
|
version: u32,
|
|
|
|
#[serde(default)]
|
|
timestamp: String,
|
|
|
|
#[serde(default)]
|
|
nblocks: u32,
|
|
|
|
#[serde(default)]
|
|
shasum: String,
|
|
|
|
#[serde(default, rename = "verity-salt")]
|
|
verity_salt: String,
|
|
|
|
#[serde(default, rename = "verity-root")]
|
|
verity_root: String,
|
|
}
|
|
|
|
impl MetaInfo {
|
|
|
|
fn parse_bytes(bytes: &[u8]) -> Option<MetaInfo> {
|
|
toml::from_slice::<MetaInfo>(bytes).ok()
|
|
}
|
|
|
|
pub fn image_type(&self) -> &str {
|
|
self.image_type.as_str()
|
|
}
|
|
|
|
pub fn channel(&self) -> &str {
|
|
self.channel.as_str()
|
|
}
|
|
|
|
fn str_ref(arg: &Option<String>) -> Option<&str> {
|
|
match arg {
|
|
Some(ref s) => Some(s.as_str()),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
pub fn kernel_version(&self) -> Option<&str> {
|
|
Self::str_ref(&self.kernel_version)
|
|
}
|
|
|
|
pub fn kernel_id(&self) -> Option<&str> {
|
|
Self::str_ref(&self.kernel_id)
|
|
}
|
|
|
|
pub fn realmfs_name(&self) -> Option<&str> {
|
|
Self::str_ref(&self.realmfs_name)
|
|
}
|
|
|
|
pub fn realmfs_owner(&self) -> Option<&str> {
|
|
Self::str_ref(&self.realmfs_owner)
|
|
}
|
|
|
|
pub fn version(&self) -> u32 {
|
|
self.version
|
|
}
|
|
|
|
pub fn timestamp(&self) -> &str {
|
|
&self.timestamp
|
|
}
|
|
|
|
pub fn nblocks(&self) -> usize {
|
|
self.nblocks as usize
|
|
}
|
|
|
|
pub fn shasum(&self) -> &str {
|
|
&self.shasum
|
|
}
|
|
|
|
pub fn verity_root(&self) -> &str {
|
|
&self.verity_root
|
|
}
|
|
|
|
pub fn verity_salt(&self) -> &str {
|
|
&self.verity_salt
|
|
}
|
|
|
|
pub fn verity_tag(&self) -> &str {
|
|
&self.verity_root()[..8]
|
|
}
|
|
}
|
|
|