Skip to content

Commit

Permalink
Merge pull request #12 from flatcar/dongsu/payload-file-integration
Browse files Browse the repository at this point in the history
download_sysext: integrate signature verification
  • Loading branch information
dongsupark authored Oct 25, 2023
2 parents 85b3b76 + 59dcd92 commit 30dab6d
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 139 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ tokio = { version = "1", features = ["full"] }
uuid = "1.2"
sha2 = "0.10"
url = "2"
rsa = { version = "0.9.2", features = ["sha2"] }

env_logger = "0.10"
log = "0.4"
Expand Down
43 changes: 43 additions & 0 deletions src/bin/download_sysext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use hard_xml::XmlRead;
use argh::FromArgs;
use url::Url;

use update_format_crau::delta_update;

#[derive(Debug)]
enum PackageStatus {
ToDownload,
Expand Down Expand Up @@ -110,6 +112,32 @@ impl<'a> Package<'a> {
return true;
}
}

fn verify_signature_on_disk(&mut self, from_path: &Path, pubkey_path: &str) -> Result<(), Box<dyn Error>> {
let upfile = File::open(from_path)?;

// Read update payload from file, read delta update header from the payload.
let res_data = fs::read_to_string(from_path);

let header = delta_update::read_delta_update_header(&upfile)?;

// Extract signature from header.
let sigbytes = delta_update::get_signatures_bytes(&upfile, &header)?;

// Parse signature data from the signature containing data, version, special fields.
let _sigdata = match delta_update::parse_signature_data(res_data.unwrap().as_bytes(), &sigbytes, pubkey_path) {
Some(data) => data,
_ => {
self.status = PackageStatus::BadSignature;
return Err("unable to parse signature data".into());
}
};

println!("Parsed and verified signature data from file {:?}", from_path);

self.status = PackageStatus::Verified;
Ok(())
}
}

#[rustfmt::skip]
Expand Down Expand Up @@ -169,6 +197,10 @@ struct Args {
#[argh(option, short = 'i')]
input_xml: String,

/// path to the public key file
#[argh(option, short = 'p')]
pubkey_file: String,

/// glob pattern to match update URLs.
/// may be specified multiple times.
#[argh(option, short = 'm')]
Expand Down Expand Up @@ -231,6 +263,17 @@ async fn main() -> Result<(), Box<dyn Error>> {
pkg.check_download(&unverified_dir)?;

pkg.download(&unverified_dir, &client).await?;

let pkg_unverified = unverified_dir.join(&*pkg.name);
let pkg_verified = output_dir.join(&*pkg.name);

match pkg.verify_signature_on_disk(&pkg_unverified, &args.pubkey_file) {
Ok(_) => {
// move the verified file back from unverified_dir to output_dir
fs::rename(&pkg_unverified, &pkg_verified)?;
}
_ => return Err(format!("unable to verify signature \"{}\"", pkg.name).into()),
};
}

Ok(())
Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ mod download;
pub use download::download_and_hash;

pub mod request;
pub mod verify_sig;
9 changes: 9 additions & 0 deletions src/testdata/omaha-request-example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<request protocol="3.0" version="update_engine-0.4.10" updaterversion="update_engine-0.4.10" installsource="scheduler" ismachine="1">
<os version="Chateau" platform="CoreOS" sp="2512.2.0_x86_64"></os>
<app appid="e96281a6-d1af-4bde-9a0a-97b76e56dc57" version="1.2.3" track="alpha" bootid="{965fb4c5-ad3e-4eb7-a4c2-ca0c0e31ec84}" oem="ami" oemversion="0.1.1-r1" alephversion="1688.5.3" machineid="abce671d61774703ac7be60715220bfe" lang="en-US" board="amd64-usr" hardware_class="" delta_okay="false" >
<ping active="1"></ping>
<updatecheck></updatecheck>
<event eventtype="3" eventresult="1"></event>
</app>
</request>
24 changes: 24 additions & 0 deletions src/testdata/omaha-response-example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<response protocol="3.0" server="nebraska">
<daystart elapsed_seconds="0"></daystart>
<app appid="e96281a6-d1af-4bde-9a0a-97b76e56dc57" status="ok">
<ping status="ok"></ping>
<updatecheck status="ok">
<urls>
<url codebase="https://update.release.flatcar-linux.net/amd64-usr/3732.0.0/"></url>
</urls>
<manifest version="3732.0.0">
<packages>
<package name="flatcar_production_update.gz" hash="I6DGNAOrJRUxPbCuLw+AITfiVMo=" size="382137917" required="true">
</package>
<package name="oem-azure.gz" hash="wepxwEV9L2SS1l/ycEZSqWM3dDc=" hash_sha256="3aed3129de50b959a97e4913ba485bd60e72d2bb6aa377d5ed404103f0680043" size="40897503" required="false"></package>
<package name="oem-qemu.gz" hash="+06iWWI6gaSlcIHV7pjIEJrE9CA=" hash_sha256="8ab630ee4079ecd5f8f512c05b44fec5e4f8db844db916c67c8d54a575cfe506" size="2282" required="false"></package>
</packages>
<actions>
<action event="postinstall" sha256="stLG3U/o4Ar8TMHFwT/RWB0iNkaWOO6QtLrq6+AHBbA=" DisablePayloadBackoff="true"></action>
</actions>
</manifest>
</updatecheck>
<event status="ok"></event>
</app>
</response>
151 changes: 19 additions & 132 deletions test/crau_verify.rs
Original file line number Diff line number Diff line change
@@ -1,155 +1,42 @@
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::Write;
use std::error::Error;
use std::fs;
use std::fs::File;
use log::debug;

use protobuf::Message;
use proto::signatures::Signature;
use update_format_crau::proto;
use update_format_crau::delta_update;

use ue_rs::verify_sig;
use ue_rs::verify_sig::get_public_key_pkcs_pem;
use ue_rs::verify_sig::KeyType::KeyTypePkcs8;

const DELTA_UPDATE_HEADER_SIZE: u64 = 4 + 8 + 8;
const DELTA_UPDATE_FILE_MAGIC: &[u8] = b"CrAU";
use argh::FromArgs;

const PUBKEY_FILE: &str = "../src/testdata/public_key_test_pkcs8.pem";

#[derive(Debug)]
struct DeltaUpdateFileHeader {
magic: [u8; 4],
file_format_version: u64,
manifest_size: u64,
}
#[derive(FromArgs, Debug)]
/// A test program for verifying CRAU header of update payloads.
struct Args {
/// source payload path
#[argh(option, short = 's')]
src_path: String,

impl DeltaUpdateFileHeader {
#[inline]
fn translate_offset(&self, offset: u64) -> u64 {
DELTA_UPDATE_HEADER_SIZE + self.manifest_size + offset
}
}

// Read delta update header from the given file, return DeltaUpdateFileHeader.
fn read_delta_update_header(mut f: &File) -> Result<DeltaUpdateFileHeader, Box<dyn Error>> {
let mut header = DeltaUpdateFileHeader {
magic: [0; 4],
file_format_version: 0,
manifest_size: 0,
};

f.read_exact(&mut header.magic)?;
if header.magic != DELTA_UPDATE_FILE_MAGIC {
return Err("bad file magic".into());
}

let mut buf = [0u8; 8];
f.read_exact(&mut buf)?;
header.file_format_version = u64::from_be_bytes(buf);
if header.file_format_version != 1 {
return Err("unsupported file format version".into());
}

f.read_exact(&mut buf)?;
header.manifest_size = u64::from_be_bytes(buf);

Ok(header)
}

// Take a file stream and DeltaUpdateFileHeader,
// return a bytes slice of the actual signature data as well as its length.
fn get_signatures_bytes<'a>(mut f: &'a File, header: &'a DeltaUpdateFileHeader) -> Result<Box<[u8]>, Box<dyn Error>> {
let manifest_bytes = {
let mut buf = vec![0u8; header.manifest_size as usize];
f.read_exact(&mut buf)?;
buf.into_boxed_slice()
};

let manifest = proto::DeltaArchiveManifest::parse_from_bytes(&manifest_bytes)?;

// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// !!! signature offsets are from the END of the manifest !!!
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// this may also be the case for the InstallOperations
// use header.translate_offset()

let signatures_bytes = match (manifest.signatures_offset, manifest.signatures_size) {
(Some(sig_offset), Some(sig_size)) => {
f.seek(SeekFrom::Start(header.translate_offset(sig_offset)))?;

let mut buf = vec![0u8; sig_size as usize];
f.read_exact(&mut buf)?;
Some(buf.into_boxed_slice())
}
_ => None,
};

Ok(signatures_bytes.unwrap())
}

#[rustfmt::skip]
// parse_signature_data takes a bytes slice for signature and public key file path.
// Return only actual data, without version and special fields.
fn parse_signature_data(testdata: &[u8], sigbytes: &[u8], pubkeyfile: &str) -> Option<Box<[u8]>> {
// Signatures has a container of the fields, i.e. version, data, and
// special fields.
let sigmessage = match proto::Signatures::parse_from_bytes(sigbytes) {
Ok(data) => data,
_ => return None,
};

// sigmessages.signatures[] has a single element in case of dev update payloads,
// while it could have multiple elements in case of production update payloads.
// For now we assume only dev update payloads are supported.
// Return the first valid signature, iterate into the next slot if invalid.
sigmessage.signatures.iter()
.find_map(|sig|
verify_sig_pubkey(testdata, sig, pubkeyfile)
.map(Vec::into_boxed_slice))
}

// Verify signature with public key
fn verify_sig_pubkey(testdata: &[u8], sig: &Signature, pubkeyfile: &str) -> Option<Vec<u8>> {
// The signature version is actually a numeration of the present signatures,
// with the index starting at 2 if only one signature is present.
// The Flatcar dev payload has only one signature but
// the production payload has two from which only one is valid.
// So, we see only "version 2" for dev payloads , and "version 1" and "version 2"
// in case of production update payloads. However, we do not explicitly check
// for a signature version, as the number could differ in some cases.
debug!("supported signature version: {:?}", sig.version());
let sigvec = match &sig.data {
Some(sigdata) => Some(sigdata),
_ => None,
};

debug!("data: {:?}", sig.data());
debug!("special_fields: {:?}", sig.special_fields());

// verify signature with pubkey
_ = verify_sig::verify_rsa_pkcs(testdata, sig.data(), get_public_key_pkcs_pem(pubkeyfile, KeyTypePkcs8));
_ = pubkeyfile;

sigvec.cloned()
/// destination signature path
#[argh(option, short = 'd')]
sig_path: String,
}

fn main() -> Result<(), Box<dyn Error>> {
// TODO: parse args using a decent command-line parameter framework
let srcpath = std::env::args().nth(1).expect("missing source payload path (second argument)");
let sigpath = std::env::args().nth(2).expect("missing destination signature path (third argument)");
let args: Args = argh::from_env();

let srcpath = &args.src_path;
let sigpath = &args.sig_path;

// Read update payload from srcpath, read delta update header from the payload.
let upfile = fs::File::open(srcpath.clone())?;
let header = read_delta_update_header(&upfile)?;
let header = delta_update::read_delta_update_header(&upfile)?;

// Extract signature from header.
let sigbytes = get_signatures_bytes(&upfile, &header)?;
let sigbytes = delta_update::get_signatures_bytes(&upfile, &header)?;

const TESTDATA: &str = "test data for verifying signature";

// Parse signature data from the signature containing data, version, special fields.
let sigdata = match parse_signature_data(TESTDATA.as_bytes(), &sigbytes, PUBKEY_FILE) {
let sigdata = match delta_update::parse_signature_data(TESTDATA.as_bytes(), &sigbytes, PUBKEY_FILE) {
Some(data) => Box::leak(data),
_ => return Err("unable to parse signature data".into()),
};
Expand Down
1 change: 1 addition & 0 deletions update-format-crau/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ edition = "2021"
[dependencies]
log = "0.4.19"
protobuf = "3"
rsa = { version = "0.9.2", features = ["sha2"] }
Loading

0 comments on commit 30dab6d

Please sign in to comment.