1
0
forked from brl/citadel-tools
citadel-tools/libcitadel/src/header.rs

382 lines
11 KiB
Rust

use std::cell::RefCell;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use failure::ResultExt;
use toml;
use blockdev::AlignedBuffer;
use {BlockDev, Channel, Config, Result};
/// 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
///
#[derive(Clone)]
pub struct ImageHeader(RefCell<Vec<u8>>);
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() -> ImageHeader {
let v = vec![0u8; ImageHeader::HEADER_SIZE];
let header = ImageHeader(RefCell::new(v));
header.write_bytes(0, MAGIC);
header
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<ImageHeader> {
// XXX check file size is at least HEADER_SIZE
let mut f = File::open(path.as_ref())?;
ImageHeader::from_reader(&mut f)
}
pub fn from_reader<R: Read>(r: &mut R) -> Result<ImageHeader> {
let mut v = vec![0u8; ImageHeader::HEADER_SIZE];
r.read_exact(&mut v)?;
Ok(ImageHeader(RefCell::new(v)))
}
pub fn from_partition<P: AsRef<Path>>(path: P) -> Result<ImageHeader> {
let mut dev = BlockDev::open_ro(path.as_ref())?;
let nsectors = dev.nsectors()?;
ensure!(
nsectors >= 8,
"{} is a block device bit it's too short ({} sectors)",
path.as_ref().display(),
nsectors
);
let mut buffer = AlignedBuffer::new(ImageHeader::HEADER_SIZE);
dev.read_sectors(nsectors - 8, buffer.as_mut())?;
let header = ImageHeader(RefCell::new(buffer.as_ref().into()));
Ok(header)
}
pub fn write_partition<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut dev = BlockDev::open_rw(path.as_ref())?;
let nsectors = dev.nsectors()?;
ensure!(
nsectors >= 8,
"{} is a block device bit it's too short ({} sectors)",
path.as_ref().display(),
nsectors
);
let buffer = AlignedBuffer::from_slice(&self.0.borrow());
dev.write_sectors(nsectors - 8, buffer.as_ref())?;
Ok(())
}
pub fn metainfo(&self) -> Result<MetaInfo> {
let mlen = self.metainfo_len();
if mlen == 0 || mlen > MAX_METAINFO_LEN {
bail!("Invalid metainfo-len field: {}", mlen);
}
let mbytes = self.metainfo_bytes();
let mut metainfo = MetaInfo::new(mbytes);
metainfo.parse_toml()?;
Ok(metainfo)
}
pub fn is_magic_valid(&self) -> bool {
self.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 set_metainfo_len(&self, len: usize) {
self.write_u16(6, len as u16);
}
pub fn set_metainfo_bytes(&self, bytes: &[u8]) {
self.set_metainfo_len(bytes.len());
self.write_bytes(8, bytes);
}
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 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 sign_metainfo(&self, channel: &Channel) -> Result<()> {
let mlen = self.metainfo_len();
// XXX assert mlen is good
let sig = channel.sign(&self.0.borrow()[8..8 + mlen])?;
self.write_bytes(8 + mlen, sig.to_bytes());
Ok(())
}
pub fn verify_signature(&self, config: &Config) -> Result<()> {
let metainfo = self.metainfo()?;
let channel = match config.channel(metainfo.channel()) {
Some(channel) => channel,
None => bail!("Cannot verify signature for channel '{}' because it does not exist in configuration file", metainfo.channel()),
};
channel
.verify(metainfo.bytes(), &self.signature())
.context("failed to verify header signature")?;
Ok(())
}
pub fn write_header<W: Write>(&self, mut writer: W) -> Result<()> {
writer.write_all(&self.0.borrow())?;
Ok(())
}
pub fn clear(&self) {
for b in &mut self.0.borrow_mut()[..] {
*b = 0;
}
self.write_bytes(0, MAGIC);
}
fn read_u8(&self, idx: usize) -> u8 {
self.0.borrow()[idx]
}
fn read_u16(&self, idx: usize) -> u16 {
let hi = self.read_u8(idx) as u16;
let lo = self.read_u8(idx + 1) as u16;
(hi << 8) | lo
}
fn write_u8(&self, idx: usize, val: u8) {
self.0.borrow_mut()[idx] = val;
}
fn write_u16(&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 write_bytes(&self, offset: usize, data: &[u8]) {
self.0.borrow_mut()[offset..offset + data.len()].copy_from_slice(data)
}
fn read_bytes(&self, offset: usize, len: usize) -> Vec<u8> {
Vec::from(&self.0.borrow()[offset..offset + len])
}
}
#[derive(Clone)]
pub struct MetaInfo {
bytes: Vec<u8>,
is_parsed: bool,
toml: Option<MetaInfoToml>,
}
#[derive(Deserialize, Serialize, Clone)]
struct MetaInfoToml {
#[serde(rename = "image-type")]
image_type: String,
channel: String,
version: u32,
#[serde(rename = "base-version")]
base_version: Option<u32>,
date: Option<String>,
gitrev: Option<String>,
nblocks: u32,
shasum: String,
#[serde(rename = "verity-salt")]
verity_salt: String,
#[serde(rename = "verity-root")]
verity_root: String,
}
impl MetaInfo {
fn new(bytes: Vec<u8>) -> MetaInfo {
MetaInfo {
bytes,
is_parsed: false,
toml: None,
}
}
fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn parse_toml(&mut self) -> Result<()> {
if !self.is_parsed {
self.is_parsed = true;
let toml =
toml::from_slice::<MetaInfoToml>(&self.bytes).context("parsing header metainfo")?;
self.toml = Some(toml);
}
Ok(())
}
pub fn verify(&self, config: &Config, signature: &[u8]) -> Result<()> {
let channel = match config.channel(self.channel()) {
Some(channel) => channel,
None => bail!("Channel '{}' not found in config file", self.channel()),
};
channel
.verify(&self.bytes, signature)
.context("Bad metainfo signature in header")?;
Ok(())
}
fn toml(&self) -> &MetaInfoToml {
self.toml.as_ref().unwrap()
}
pub fn image_type(&self) -> &str {
self.toml().image_type.as_str()
}
pub fn channel(&self) -> &str {
self.toml().channel.as_str()
}
pub fn version(&self) -> u32 {
self.toml().version
}
pub fn date(&self) -> Option<&str> {
self.toml().date.as_ref().map(|s| s.as_str())
}
pub fn gitrev(&self) -> Option<&str> {
self.toml().gitrev.as_ref().map(|s| s.as_str())
}
pub fn nblocks(&self) -> usize {
self.toml().nblocks as usize
}
pub fn shasum(&self) -> &str {
&self.toml().shasum
}
pub fn verity_root(&self) -> &str {
&self.toml().verity_root
}
pub fn verity_salt(&self) -> &str {
&self.toml().verity_salt
}
}