diff --git a/Cargo.lock b/Cargo.lock index ff580e5..b1eef2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1414,7 +1414,6 @@ dependencies = [ "omaha", "protobuf", "reqwest", - "rsa", "sha2", "tokio", "update-format-crau", @@ -1449,6 +1448,7 @@ version = "0.1.0" dependencies = [ "log", "protobuf", + "rsa", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 9ba09ae..1749366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/bin/download_sysext.rs b/src/bin/download_sysext.rs index 432ab51..aebfeb0 100644 --- a/src/bin/download_sysext.rs +++ b/src/bin/download_sysext.rs @@ -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, @@ -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> { + 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] @@ -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')] @@ -231,6 +263,17 @@ async fn main() -> Result<(), Box> { 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(()) diff --git a/src/lib.rs b/src/lib.rs index 19cfd39..72dbc06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,4 +2,3 @@ mod download; pub use download::download_and_hash; pub mod request; -pub mod verify_sig; diff --git a/src/testdata/omaha-request-example.xml b/src/testdata/omaha-request-example.xml new file mode 100644 index 0000000..2190653 --- /dev/null +++ b/src/testdata/omaha-request-example.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/testdata/omaha-response-example.xml b/src/testdata/omaha-response-example.xml new file mode 100644 index 0000000..cb22020 --- /dev/null +++ b/src/testdata/omaha-response-example.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/crau_verify.rs b/test/crau_verify.rs index 92c21c5..28d8a3c 100644 --- a/test/crau_verify.rs +++ b/test/crau_verify.rs @@ -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> { - 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> { - 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> { - // 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> { - // 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> { - // 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()), }; diff --git a/update-format-crau/Cargo.toml b/update-format-crau/Cargo.toml index ed29734..b27198f 100644 --- a/update-format-crau/Cargo.toml +++ b/update-format-crau/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] log = "0.4.19" protobuf = "3" +rsa = { version = "0.9.2", features = ["sha2"] } diff --git a/update-format-crau/src/delta_update.rs b/update-format-crau/src/delta_update.rs new file mode 100644 index 0000000..18704e1 --- /dev/null +++ b/update-format-crau/src/delta_update.rs @@ -0,0 +1,132 @@ +use std::io::{Read, Seek, SeekFrom}; +use std::error::Error; +use std::fs::File; +use log::debug; + +use protobuf::Message; + +use crate::proto::signatures::Signature; +use crate::proto; +use crate::verify_sig; +use crate::verify_sig::get_public_key_pkcs_pem; +use crate::verify_sig::KeyType::KeyTypePkcs8; + +const DELTA_UPDATE_HEADER_SIZE: u64 = 4 + 8 + 8; +const DELTA_UPDATE_FILE_MAGIC: &[u8] = b"CrAU"; + +#[derive(Debug)] +pub struct DeltaUpdateFileHeader { + magic: [u8; 4], + file_format_version: u64, + manifest_size: u64, +} + +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. +pub fn read_delta_update_header(mut f: &File) -> Result> { + 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. +pub fn get_signatures_bytes<'a>(mut f: &'a File, header: &'a DeltaUpdateFileHeader) -> Result, Box> { + 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. +pub fn parse_signature_data(testdata: &[u8], sigbytes: &[u8], pubkeyfile: &str) -> Option> { + // 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 +pub fn verify_sig_pubkey(testdata: &[u8], sig: &Signature, pubkeyfile: &str) -> Option> { + // 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() +} diff --git a/update-format-crau/src/lib.rs b/update-format-crau/src/lib.rs index a9099b4..2ebe5f9 100644 --- a/update-format-crau/src/lib.rs +++ b/update-format-crau/src/lib.rs @@ -1,4 +1,6 @@ +pub mod delta_update; mod generated; +pub mod verify_sig; pub mod proto { pub use super::generated::update_metadata::*; diff --git a/src/verify_sig.rs b/update-format-crau/src/verify_sig.rs similarity index 92% rename from src/verify_sig.rs rename to update-format-crau/src/verify_sig.rs index 917f460..c64e134 100644 --- a/src/verify_sig.rs +++ b/update-format-crau/src/verify_sig.rs @@ -80,10 +80,10 @@ mod tests { use crate::verify_sig::KeyType::{KeyTypePkcs1, KeyTypePkcs8}; const TESTDATA: &str = "test data for verifying signature"; - const PRIVKEY_PKCS1_PATH: &str = "./src/testdata/private_key_test_pkcs1.pem"; - const PUBKEY_PKCS1_PATH: &str = "./src/testdata/public_key_test_pkcs1.pem"; - const PRIVKEY_PKCS8_PATH: &str = "./src/testdata/private_key_test_pkcs8.pem"; - const PUBKEY_PKCS8_PATH: &str = "./src/testdata/public_key_test_pkcs8.pem"; + const PRIVKEY_PKCS1_PATH: &str = "../src/testdata/private_key_test_pkcs1.pem"; + const PUBKEY_PKCS1_PATH: &str = "../src/testdata/public_key_test_pkcs1.pem"; + const PRIVKEY_PKCS8_PATH: &str = "../src/testdata/private_key_test_pkcs8.pem"; + const PUBKEY_PKCS8_PATH: &str = "../src/testdata/public_key_test_pkcs8.pem"; #[test] fn test_verify_sig() {