Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

download_sysext: integrate signature verification #12

Merged
merged 3 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need a call to verify_sig_pubkey() first?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Loading