| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::resolver_service::Resolver, |
| anyhow::{anyhow, Context as _, Error}, |
| async_lock::RwLock as AsyncRwLock, |
| eager_package_config::pkg_resolver::{EagerPackageConfig, EagerPackageConfigs}, |
| fidl_fuchsia_io as fio, |
| fidl_fuchsia_pkg::{self as fpkg, CupRequest, CupRequestStream, GetInfoError, WriteError}, |
| fidl_fuchsia_pkg_ext::{cache, BlobInfo, CupData, CupMissingField}, |
| fidl_fuchsia_pkg_internal::{PersistentEagerPackage, PersistentEagerPackages}, |
| fuchsia_pkg::PackageDirectory, |
| fuchsia_syslog::fx_log_err, |
| fuchsia_url::{AbsolutePackageUrl, Hash, PinnedAbsolutePackageUrl, UnpinnedAbsolutePackageUrl}, |
| fuchsia_zircon as zx, |
| futures::prelude::*, |
| omaha_client::{ |
| cup_ecdsa::{CupVerificationError, Cupv2Verifier, PublicKeys, StandardCupv2Handler}, |
| protocol::response::Response, |
| }, |
| p256::ecdsa::{signature::Signature, DerSignature}, |
| std::{collections::BTreeMap, convert::TryInto, sync::Arc}, |
| }; |
| |
| const EAGER_PACKAGE_PERSISTENT_FIDL_NAME: &str = "eager_packages.pf"; |
| |
| #[derive(Clone, Debug)] |
| struct EagerPackage { |
| #[allow(dead_code)] |
| executable: bool, |
| package_directory: Option<PackageDirectory>, |
| cup: Option<CupData>, |
| public_keys: PublicKeys, |
| } |
| |
| #[derive(Debug)] |
| pub struct EagerPackageManager<T: Resolver> { |
| packages: BTreeMap<UnpinnedAbsolutePackageUrl, EagerPackage>, |
| package_resolver: T, |
| data_proxy: Option<fio::DirectoryProxy>, |
| } |
| |
| async fn verify_cup_signature( |
| cup_handler: &StandardCupv2Handler, |
| cup: &CupData, |
| ) -> Result<(), ParseCupResponseError> { |
| let der_signature = DerSignature::from_bytes(&cup.signature)?; |
| cup_handler |
| .verify_response_with_signature( |
| &der_signature, |
| &cup.request, |
| &cup.response, |
| cup.key_id, |
| &cup.nonce.into(), |
| ) |
| .map_err(ParseCupResponseError::VerificationError) |
| } |
| |
| impl<T: Resolver> EagerPackageManager<T> { |
| pub async fn from_namespace( |
| package_resolver: T, |
| pkg_cache: cache::Client, |
| data_proxy: Option<fio::DirectoryProxy>, |
| ) -> Result<Self, Error> { |
| let config = |
| EagerPackageConfigs::from_namespace().await.context("loading eager package config")?; |
| Ok(Self::from_config(config, package_resolver, pkg_cache, data_proxy).await) |
| } |
| |
| async fn from_config( |
| config: EagerPackageConfigs, |
| package_resolver: T, |
| pkg_cache: cache::Client, |
| data_proxy: Option<fio::DirectoryProxy>, |
| ) -> Self { |
| let mut packages = config |
| .packages |
| .into_iter() |
| .map(|EagerPackageConfig { url, executable, public_keys }| { |
| (url, EagerPackage { executable, package_directory: None, cup: None, public_keys }) |
| }) |
| .collect(); |
| |
| if let Some(ref data_proxy) = data_proxy { |
| if let Err(e) = |
| Self::load_persistent_eager_packages(&mut packages, pkg_cache, &data_proxy).await |
| { |
| fx_log_err!("failed to load persistent eager packages: {:#}", anyhow!(e)); |
| } |
| } |
| |
| Self { packages, package_resolver, data_proxy } |
| } |
| |
| async fn load_persistent_eager_packages( |
| packages: &mut BTreeMap<UnpinnedAbsolutePackageUrl, EagerPackage>, |
| pkg_cache: cache::Client, |
| data_proxy: &fio::DirectoryProxy, |
| ) -> Result<(), Error> { |
| let file_proxy = match fuchsia_fs::directory::open_file( |
| data_proxy, |
| EAGER_PACKAGE_PERSISTENT_FIDL_NAME, |
| fio::OpenFlags::RIGHT_READABLE, |
| ) |
| .await |
| { |
| Ok(proxy) => proxy, |
| Err(fuchsia_fs::node::OpenError::OpenError(s)) if s == zx::Status::NOT_FOUND => { |
| return Ok(()) |
| } |
| Err(e) => Err(e).context("while opening eager_packages.pf")?, |
| }; |
| |
| let persistent_packages = |
| fuchsia_fs::read_file_fidl::<PersistentEagerPackages>(&file_proxy) |
| .await |
| .context("while reading eager_packages.pf")?; |
| for PersistentEagerPackage { url, cup, .. } in persistent_packages |
| .packages |
| .ok_or_else(|| anyhow!("PersistentEagerPackages does not contain `packages` field"))? |
| { |
| async { |
| let url = url.ok_or_else(|| { |
| anyhow!("PersistentEagerPackage does not contain `url` field") |
| })?; |
| let cup: CupData = cup |
| .ok_or_else(|| anyhow!("PersistentEagerPackage does not contain `cup` field"))? |
| .try_into()?; |
| let response = parse_omaha_response_from_cup(&cup) |
| .with_context(|| format!("while parsing omaha response {:?}", cup.response))?; |
| let pinned_url = response |
| .apps |
| .iter() |
| .find_map(|app| { |
| app.update_check |
| .as_ref() |
| .and_then(|uc| uc.get_all_full_urls().find(|u| u.starts_with(&url.url))) |
| }) |
| .ok_or_else(|| { |
| anyhow!("could not find pinned url in CUP omaha response for {}", url.url) |
| })?; |
| let pinned_url: PinnedAbsolutePackageUrl = |
| pinned_url.parse().with_context(|| format!("while parsing {}", url.url))?; |
| let unpinned_url = pinned_url.as_unpinned(); |
| let package = packages |
| .get_mut(&unpinned_url) |
| .ok_or_else(|| anyhow!("unknown pkg url: {}", url.url))?; |
| |
| let cup_handler = StandardCupv2Handler::new(&package.public_keys); |
| |
| verify_cup_signature(&cup_handler, &cup) |
| .await |
| .map_err(|e| anyhow!("could not verify cup signature {:?}", e))?; |
| |
| package.cup = Some(cup); |
| |
| let pkg_dir = |
| Self::resolve_pinned_from_cache(&pkg_cache, pinned_url).await.with_context( |
| || format!("while resolving eager package {} from cache", url.url), |
| )?; |
| package.package_directory = Some(pkg_dir); |
| Ok(()) |
| } |
| .await |
| .unwrap_or_else(|e: Error| { |
| fx_log_err!("failed to load persistent eager package: {:#}", anyhow!(e)) |
| }) |
| } |
| Ok(()) |
| } |
| |
| // Returns eager package directory. |
| // Returns Ok(None) if that's not an eager package, or the package is pinned. |
| pub fn get_package_dir( |
| &self, |
| url: &AbsolutePackageUrl, |
| ) -> Result<Option<PackageDirectory>, Error> { |
| let url = match url { |
| AbsolutePackageUrl::Unpinned(unpinned) => unpinned, |
| AbsolutePackageUrl::Pinned(_) => return Ok(None), |
| }; |
| if let Some(eager_package) = self.packages.get(url) { |
| if eager_package.package_directory.is_some() { |
| Ok(eager_package.package_directory.clone()) |
| } else { |
| Err(anyhow!("eager package dir not found for {}", url)) |
| } |
| } else { |
| Ok(None) |
| } |
| } |
| |
| async fn resolve_pinned( |
| package_resolver: &T, |
| url: PinnedAbsolutePackageUrl, |
| ) -> Result<PackageDirectory, ResolvePinnedError> { |
| let expected_hash = url.hash(); |
| let pkg_dir = package_resolver.resolve(url.into(), None).await?; |
| let hash = pkg_dir.merkle_root().await?; |
| if hash != expected_hash { |
| return Err(ResolvePinnedError::HashMismatch(hash)); |
| } |
| Ok(pkg_dir) |
| } |
| |
| async fn resolve_pinned_from_cache( |
| pkg_cache: &cache::Client, |
| url: PinnedAbsolutePackageUrl, |
| ) -> Result<PackageDirectory, Error> { |
| let mut get = pkg_cache |
| .get(BlobInfo { blob_id: url.hash().into(), length: 0 }) |
| .context("pkg cache get")?; |
| if let Some(_needed_blob) = get.open_meta_blob().await.context("open_meta_blob")? { |
| return Err(anyhow!("meta blob missing")); |
| } |
| let missing_blobs = get.get_missing_blobs().await.context("get_missing_blobs")?; |
| if !missing_blobs.is_empty() { |
| return Err(anyhow!("at least one blob missing: {:?}", missing_blobs)); |
| } |
| Ok(get.finish().await.context("finish")?) |
| } |
| |
| async fn persist( |
| &self, |
| packages: &BTreeMap<UnpinnedAbsolutePackageUrl, EagerPackage>, |
| ) -> Result<(), PersistError> { |
| let data_proxy = self.data_proxy.as_ref().ok_or(PersistError::DataProxyNotAvailable)?; |
| |
| let packages = packages |
| .iter() |
| .map(|(url, package)| PersistentEagerPackage { |
| url: Some(fpkg::PackageUrl { url: url.to_string() }), |
| cup: package.cup.clone().map(Into::into), |
| ..PersistentEagerPackage::EMPTY |
| }) |
| .collect(); |
| let mut packages = |
| PersistentEagerPackages { packages: Some(packages), ..PersistentEagerPackages::EMPTY }; |
| |
| let temp_path = &format!("{EAGER_PACKAGE_PERSISTENT_FIDL_NAME}.new"); |
| crate::util::do_with_atomic_file( |
| &data_proxy, |
| temp_path, |
| EAGER_PACKAGE_PERSISTENT_FIDL_NAME, |
| |proxy| async move { |
| fuchsia_fs::write_file_fidl(&proxy, &mut packages) |
| .await |
| .with_context(|| format!("writing file: {}", temp_path)) |
| }, |
| ) |
| .await |
| .map_err(PersistError::AtomicWrite) |
| } |
| |
| async fn cup_write( |
| &mut self, |
| url: &fpkg::PackageUrl, |
| cup: fpkg::CupData, |
| ) -> Result<(), CupWriteError> { |
| let cup_data: CupData = cup.try_into()?; |
| let response = parse_omaha_response_from_cup(&cup_data)?; |
| // The full URL must appear in the omaha response. |
| let _app = response |
| .apps |
| .iter() |
| .find(|app| { |
| app.update_check |
| .as_ref() |
| .and_then(|uc| uc.get_all_full_urls().find(|u| u == &url.url)) |
| .is_some() |
| }) |
| .ok_or(CupWriteError::CupResponseURLNotFound)?; |
| |
| let pinned_url: PinnedAbsolutePackageUrl = url.url.parse()?; |
| let mut packages = self.packages.clone(); |
| // Make sure the url is an eager package before trying to resolve it. |
| let package = packages |
| .get_mut(pinned_url.as_unpinned()) |
| .ok_or_else(|| CupWriteError::UnknownURL(pinned_url.as_unpinned().clone()))?; |
| let pkg_dir = Self::resolve_pinned(&self.package_resolver, pinned_url).await?; |
| package.package_directory = Some(pkg_dir); |
| |
| let cup_handler = StandardCupv2Handler::new(&package.public_keys); |
| verify_cup_signature(&cup_handler, &cup_data).await?; |
| |
| package.cup = Some(cup_data); |
| |
| self.persist(&packages).await?; |
| // Only update self.packages if persist succeed, to prevent rollback after reboot. |
| self.packages = packages; |
| Ok(()) |
| } |
| |
| async fn cup_get_info( |
| &self, |
| url: &fpkg::PackageUrl, |
| ) -> Result<(String, String), CupGetInfoError> { |
| let pkg_url = UnpinnedAbsolutePackageUrl::parse(&url.url)?; |
| let package = |
| self.packages.get(&pkg_url).ok_or_else(|| CupGetInfoError::UnknownURL(pkg_url))?; |
| let cup = package.cup.as_ref().ok_or(CupGetInfoError::CupDataNotAvailable)?; |
| let response = parse_omaha_response_from_cup(&cup)?; |
| let app = response |
| .apps |
| .iter() |
| .find(|app| { |
| app.update_check |
| .as_ref() |
| .and_then(|uc| uc.get_all_full_urls().find(|u| u.starts_with(&url.url))) |
| .is_some() |
| }) |
| .ok_or(CupGetInfoError::CupResponseURLNotFound)?; |
| let version = app.get_manifest_version().ok_or(CupGetInfoError::CupDataMissingVersion)?; |
| let channel = app.cohort.name.clone().ok_or(CupGetInfoError::CupDataMissingChannel)?; |
| Ok((version, channel)) |
| } |
| } |
| |
| fn parse_omaha_response_from_cup(cup: &CupData) -> Result<Response, ParseCupResponseError> { |
| Ok(omaha_client::protocol::response::parse_json_response(&cup.response)?) |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| enum ParseCupResponseError { |
| #[error("CUP data signature is invalid")] |
| CupDataInvalidSignature(#[from] p256::ecdsa::Error), |
| #[error("while validating CUP response")] |
| VerificationError(#[from] CupVerificationError), |
| #[error("while parsing JSON")] |
| ParseJSON(#[from] serde_json::Error), |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| enum ResolvePinnedError { |
| #[error("while resolving package")] |
| Resolve(#[from] fidl_fuchsia_pkg_ext::ResolveError), |
| #[error("while reading package hash")] |
| ReadHash(#[from] fuchsia_pkg::ReadHashError), |
| #[error("resolved package hash '{0}' does not match")] |
| HashMismatch(Hash), |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| enum PersistError { |
| #[error("directory proxy to /data is not available")] |
| DataProxyNotAvailable, |
| #[error("while opening temp file")] |
| Open(#[from] fuchsia_fs::node::OpenError), |
| #[error("while writing persistent fidl")] |
| AtomicWrite(#[source] anyhow::Error), |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| enum CupWriteError { |
| #[error("CUP data does not include a field")] |
| Missing(#[from] CupMissingField), |
| #[error("while parsing package URL")] |
| ParseURL(#[from] fuchsia_url::errors::ParseError), |
| #[error("URL is not a known eager package: {0}")] |
| UnknownURL(UnpinnedAbsolutePackageUrl), |
| #[error("while resolving package")] |
| ResolvePinned(#[from] ResolvePinnedError), |
| #[error("while parsing CUP omaha response")] |
| ParseCupResponse(#[from] ParseCupResponseError), |
| #[error("URL not found in CUP omaha response")] |
| CupResponseURLNotFound, |
| #[error("while persisting CUP data")] |
| Persist(#[from] PersistError), |
| } |
| |
| impl From<&CupWriteError> for WriteError { |
| fn from(err: &CupWriteError) -> WriteError { |
| match err { |
| CupWriteError::ParseURL(_) => WriteError::UnknownUrl, |
| CupWriteError::UnknownURL(_) => WriteError::UnknownUrl, |
| CupWriteError::ResolvePinned(_) => WriteError::Download, |
| CupWriteError::Missing(_) => WriteError::Verification, |
| CupWriteError::ParseCupResponse(_) => WriteError::Verification, |
| CupWriteError::CupResponseURLNotFound => WriteError::Verification, |
| CupWriteError::Persist(_) => WriteError::Storage, |
| } |
| } |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| enum CupGetInfoError { |
| #[error("while parsing package URL")] |
| ParseURL(#[from] fuchsia_url::errors::ParseError), |
| #[error("URL is not a known eager package: {0}")] |
| UnknownURL(UnpinnedAbsolutePackageUrl), |
| #[error("CUP data is not available for this eager package")] |
| CupDataNotAvailable, |
| #[error("URL not found in CUP omaha response")] |
| CupResponseURLNotFound, |
| #[error("CUP data does not include version")] |
| CupDataMissingVersion, |
| #[error("CUP data does not include channel")] |
| CupDataMissingChannel, |
| #[error("while parsing CUP omaha response")] |
| ParseCupResponse(#[from] ParseCupResponseError), |
| } |
| |
| impl From<&CupGetInfoError> for GetInfoError { |
| fn from(err: &CupGetInfoError) -> GetInfoError { |
| match err { |
| CupGetInfoError::ParseURL(_) => GetInfoError::UnknownUrl, |
| CupGetInfoError::UnknownURL(_) => GetInfoError::UnknownUrl, |
| CupGetInfoError::CupDataNotAvailable => GetInfoError::NotAvailable, |
| CupGetInfoError::CupResponseURLNotFound => GetInfoError::Verification, |
| CupGetInfoError::CupDataMissingVersion => GetInfoError::Verification, |
| CupGetInfoError::CupDataMissingChannel => GetInfoError::Verification, |
| CupGetInfoError::ParseCupResponse(_) => GetInfoError::Verification, |
| } |
| } |
| } |
| |
| pub async fn run_cup_service<T: Resolver>( |
| manager: Arc<Option<AsyncRwLock<EagerPackageManager<T>>>>, |
| mut stream: CupRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(event) = stream.try_next().await? { |
| match event { |
| CupRequest::Write { url, cup, responder } => { |
| let mut response = match manager.as_ref() { |
| Some(manager) => { |
| manager.write().await.cup_write(&url, cup).await.map_err(|e| { |
| let write_error = (&e).into(); |
| fx_log_err!("cup_write failed for url '{}': {:#}", url.url, anyhow!(e)); |
| write_error |
| }) |
| } |
| None => Err(WriteError::Storage), |
| }; |
| responder.send(&mut response)?; |
| } |
| CupRequest::GetInfo { url, responder } => { |
| let mut response = match manager.as_ref() { |
| Some(manager) => manager.read().await.cup_get_info(&url).await.map_err(|e| { |
| let get_info_error = (&e).into(); |
| fx_log_err!("cup_get_info failed for url '{}': {:#}", url.url, anyhow!(e)); |
| get_info_error |
| }), |
| None => Err(GetInfoError::NotAvailable), |
| }; |
| responder.send(&mut response)?; |
| } |
| } |
| } |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::resolver_service::MockResolver, |
| assert_matches::assert_matches, |
| fidl_fuchsia_pkg::{ |
| BlobInfoIteratorRequest, NeededBlobsRequest, PackageCacheMarker, PackageCacheRequest, |
| PackageCacheRequestStream, |
| }, |
| fuchsia_async as fasync, fuchsia_zircon as zx, |
| omaha_client::{ |
| cup_ecdsa::{ |
| test_support::{ |
| make_default_public_key_for_test, make_default_public_key_id_for_test, |
| make_default_public_keys_for_test, make_expected_signature_for_test, |
| make_keys_for_test, make_public_keys_for_test, |
| make_standard_intermediate_for_test, RAW_PUBLIC_KEY_FOR_TEST, |
| }, |
| Cupv2RequestHandler, PublicKeyAndId, PublicKeyId, PublicKeys, |
| }, |
| protocol::request::Request, |
| }, |
| }; |
| |
| const TEST_URL: &str = "fuchsia-pkg://example.com/package"; |
| const TEST_HASH: &str = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; |
| const TEST_PINNED_URL: &str = "fuchsia-pkg://example.com/package?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; |
| |
| fn get_test_package_resolver() -> MockResolver { |
| let pkg_dir = PackageDirectory::open_from_namespace().unwrap(); |
| MockResolver::new(move |_url| { |
| let pkg_dir = pkg_dir.clone(); |
| async move { Ok(pkg_dir) } |
| }) |
| } |
| |
| fn get_mock_pkg_cache() -> (cache::Client, PackageCacheRequestStream) { |
| let (proxy, stream) = |
| fidl::endpoints::create_proxy_and_stream::<PackageCacheMarker>().unwrap(); |
| (cache::Client::from_proxy(proxy), stream) |
| } |
| |
| async fn handle_pkg_cache(mut stream: PackageCacheRequestStream) { |
| while let Some(request) = stream.try_next().await.unwrap() { |
| match request { |
| PackageCacheRequest::Get { meta_far_blob, needed_blobs, dir: _, responder } => { |
| if meta_far_blob.blob_id.merkle_root |
| != TEST_HASH.parse::<Hash>().unwrap().as_bytes() |
| { |
| responder.send(&mut Err(zx::Status::NOT_FOUND.into_raw())).unwrap(); |
| continue; |
| } |
| let mut needed_blobs = needed_blobs.into_stream().unwrap(); |
| while let Some(request) = needed_blobs.try_next().await.unwrap() { |
| match request { |
| NeededBlobsRequest::OpenMetaBlob { file: _, responder } => { |
| responder.send(&mut Ok(false)).unwrap(); |
| } |
| NeededBlobsRequest::GetMissingBlobs { iterator, control_handle: _ } => { |
| let mut stream = iterator.into_stream().unwrap(); |
| let BlobInfoIteratorRequest::Next { responder } = |
| stream.next().await.unwrap().unwrap(); |
| responder.send(&mut std::iter::empty()).unwrap(); |
| } |
| r => panic!("Unexpected request: {:?}", r), |
| } |
| } |
| responder.send(&mut Ok(())).unwrap(); |
| } |
| r => panic!("Unexpected request: {:?}", r), |
| } |
| } |
| } |
| |
| fn get_test_package_resolver_with_hash(hash: &str) -> (MockResolver, tempfile::TempDir) { |
| let dir = tempfile::tempdir().unwrap(); |
| std::fs::write(dir.path().join("meta"), hash).unwrap(); |
| let proxy = fuchsia_fs::open_directory_in_namespace( |
| dir.path().to_str().unwrap(), |
| fuchsia_fs::OpenFlags::RIGHT_READABLE, |
| ) |
| .unwrap(); |
| let pkg_dir = PackageDirectory::from_proxy(proxy); |
| let package_resolver = MockResolver::new(move |_url| { |
| let pkg_dir = pkg_dir.clone(); |
| async move { Ok(pkg_dir) } |
| }); |
| (package_resolver, dir) |
| } |
| fn get_default_cup_response() -> Vec<u8> { |
| get_cup_response_with_name(&format!("package?hash={TEST_HASH}")) |
| } |
| fn get_cup_response_with_name(package_name: &str) -> Vec<u8> { |
| let response = serde_json::json!({"response":{ |
| "server": "prod", |
| "protocol": "3.0", |
| "app": [{ |
| "appid": "appid", |
| "cohortname": "stable", |
| "status": "ok", |
| "updatecheck": { |
| "status": "ok", |
| "urls":{ |
| "url":[ |
| {"codebase": "fuchsia-pkg://example.com/"}, |
| ] |
| }, |
| "manifest": { |
| "version": "1.2.3.4", |
| "actions": { |
| "action": [], |
| }, |
| "packages": { |
| "package": [ |
| { |
| "name": package_name, |
| "required": true, |
| "fp": "", |
| } |
| ], |
| }, |
| } |
| } |
| }], |
| }}); |
| serde_json::to_vec(&response).unwrap() |
| } |
| |
| fn make_cup_data(cup_response: &[u8]) -> CupData { |
| let (priv_key, public_key) = make_keys_for_test(); |
| let public_key_id: PublicKeyId = make_default_public_key_id_for_test(); |
| let public_keys = make_public_keys_for_test(public_key_id, public_key); |
| let cup_handler = StandardCupv2Handler::new(&public_keys); |
| let request = Request::default(); |
| let mut intermediate = make_standard_intermediate_for_test(request); |
| let request_metadata = cup_handler.decorate_request(&mut intermediate).unwrap(); |
| let request_body = intermediate.serialize_body().unwrap(); |
| let expected_signature: Vec<u8> = |
| make_expected_signature_for_test(&priv_key, &request_metadata, &cup_response); |
| CupData::builder() |
| .key_id(public_key_id) |
| .nonce(request_metadata.nonce) |
| .request(request_body) |
| .response(cup_response) |
| .signature(expected_signature) |
| .build() |
| } |
| |
| async fn write_persistent_fidl( |
| data_proxy: &fio::DirectoryProxy, |
| packages: impl IntoIterator<Item = (impl std::fmt::Display, CupData)>, |
| ) { |
| let file_proxy = fuchsia_fs::directory::open_file( |
| data_proxy, |
| EAGER_PACKAGE_PERSISTENT_FIDL_NAME, |
| fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::CREATE, |
| ) |
| .await |
| .unwrap(); |
| let mut packages = PersistentEagerPackages { |
| packages: Some( |
| packages |
| .into_iter() |
| .map(|(url, cup)| PersistentEagerPackage { |
| url: Some(fpkg::PackageUrl { url: url.to_string() }), |
| cup: Some(cup.into()), |
| ..PersistentEagerPackage::EMPTY |
| }) |
| .collect(), |
| ), |
| ..PersistentEagerPackages::EMPTY |
| }; |
| fuchsia_fs::write_file_fidl(&file_proxy, &mut packages).await.unwrap(); |
| } |
| |
| #[test] |
| fn parse_eager_package_configs_json() { |
| use std::convert::TryInto; |
| |
| let public_keys = PublicKeys { |
| latest: PublicKeyAndId { |
| id: 123.try_into().unwrap(), |
| key: make_default_public_key_for_test(), |
| }, |
| historical: vec![], |
| }; |
| |
| let json = serde_json::json!( |
| { |
| "packages": |
| [ |
| { |
| "url": "fuchsia-pkg://example.com/package", |
| "executable": true, |
| "public_keys": { |
| "latest": { |
| "id": 123, |
| "key": RAW_PUBLIC_KEY_FOR_TEST, |
| }, |
| "historical": [] |
| } |
| }, |
| { |
| "url": "fuchsia-pkg://example.com/package2", |
| "executable": false, |
| "public_keys": { |
| "latest": { |
| "id": 123, |
| "key": RAW_PUBLIC_KEY_FOR_TEST, |
| }, |
| "historical": [] |
| } |
| }, |
| { |
| "url": "fuchsia-pkg://example.com/package3", |
| "public_keys": { |
| "latest": { |
| "id": 123, |
| "key": RAW_PUBLIC_KEY_FOR_TEST, |
| }, |
| "historical": [] |
| } |
| } |
| ] |
| }); |
| assert_eq!( |
| serde_json::from_value::<EagerPackageConfigs>(json).unwrap(), |
| EagerPackageConfigs { |
| packages: vec![ |
| EagerPackageConfig { |
| url: "fuchsia-pkg://example.com/package".parse().unwrap(), |
| executable: true, |
| public_keys: public_keys.clone(), |
| }, |
| EagerPackageConfig { |
| url: "fuchsia-pkg://example.com/package2".parse().unwrap(), |
| executable: false, |
| public_keys: public_keys.clone(), |
| }, |
| EagerPackageConfig { |
| url: "fuchsia-pkg://example.com/package3".parse().unwrap(), |
| executable: false, |
| public_keys, |
| }, |
| ] |
| } |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_get_eager_package_with_empty_config() { |
| let config = EagerPackageConfigs { packages: Vec::new() }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| let manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| |
| assert!(manager.packages.is_empty()); |
| |
| for url in [ |
| "fuchsia-pkg://example2.com/package", |
| "fuchsia-pkg://example.com/package3", |
| "fuchsia-pkg://example.com/package?hash=deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", |
| ] { |
| assert_matches!(manager.get_package_dir(&url.parse().unwrap()), Ok(None)); |
| } |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_resolve_pinned_hash_mismatch() { |
| let pinned_url = PinnedAbsolutePackageUrl::from_unpinned( |
| TEST_URL.parse().unwrap(), |
| TEST_HASH.parse().unwrap(), |
| ); |
| assert_matches!( |
| EagerPackageManager::resolve_pinned(&get_test_package_resolver(), pinned_url).await, |
| Err(ResolvePinnedError::HashMismatch(_)) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_load_persistent_eager_packages() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let url2 = UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.com/package2").unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![ |
| EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }, |
| EagerPackageConfig { |
| url: url2.clone(), |
| executable: false, |
| public_keys: make_default_public_keys_for_test(), |
| }, |
| ], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, pkg_cache_stream) = get_mock_pkg_cache(); |
| |
| let data_dir = tempfile::tempdir().unwrap(); |
| let data_proxy = fuchsia_fs::open_directory_in_namespace( |
| data_dir.path().to_str().unwrap(), |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| let cup1_response: Vec<u8> = get_default_cup_response(); |
| let cup1: CupData = make_cup_data(&cup1_response); |
| let cup2_response = |
| get_cup_response_with_name(&format!("package2?hash={}", "1".repeat(64))); |
| // this will fail to resolve because hash doesn't match |
| let cup2: CupData = make_cup_data(&cup2_response); |
| write_persistent_fidl( |
| &data_proxy, |
| [ |
| // packages can be out of order |
| (url2.clone(), cup2.clone()), |
| // bad packages won't break loading of other valid packages |
| ( |
| UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://unknown/package").unwrap(), |
| cup1.clone(), |
| ), |
| // If CupData is empty, we should skip and log the error but not crash or fail. |
| (url.clone(), CupData::builder().build()), |
| (url.clone(), cup1.clone()), |
| ], |
| ) |
| .await; |
| let (manager, ()) = future::join( |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, Some(data_proxy)), |
| handle_pkg_cache(pkg_cache_stream), |
| ) |
| .await; |
| assert_matches!( |
| manager |
| .get_package_dir(&"fuchsia-pkg://example.com/non-eager-package".parse().unwrap()), |
| Ok(None) |
| ); |
| assert!(manager.packages[&url].package_directory.is_some()); |
| assert!(manager.get_package_dir(&url.clone().into()).unwrap().is_some()); |
| assert_eq!(manager.packages[&url].cup, Some(cup1)); |
| assert!(manager.packages[&url2].package_directory.is_none()); |
| assert_matches!(manager.get_package_dir(&url2.clone().into()), Err(_)); |
| // cup is still loaded even if resolve fails |
| assert_eq!(manager.packages[&url2].cup, Some(cup2)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_load_persistent_eager_packages_signature_invalid() { |
| // We intentionally stage a mismatch between the PublicKeys used |
| // to configure the EagerPackageManager (latest.key_id = 777) and the |
| // PublicKeys generated in make_cup_data() (latest.key_id = 123456789), |
| // which are written to persistent fidl and which we attempt to validate |
| // at startup. |
| |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| |
| let mut public_keys = make_default_public_keys_for_test(); |
| public_keys.latest.id = 777; |
| |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { url: url.clone(), executable: true, public_keys }], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, pkg_cache_stream) = get_mock_pkg_cache(); |
| |
| let data_dir = tempfile::tempdir().unwrap(); |
| let data_proxy = fuchsia_fs::open_directory_in_namespace( |
| data_dir.path().to_str().unwrap(), |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| write_persistent_fidl(&data_proxy, [(url.clone(), cup.clone())]).await; |
| let (manager, ()) = future::join( |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, Some(data_proxy)), |
| handle_pkg_cache(pkg_cache_stream), |
| ) |
| .await; |
| // This fails to load, and we log "failed to load persistent eager |
| // package: could not verify cup signature |
| // VerificationError(SpecifiedPublicKeyIdMissing)". |
| assert!(manager.get_package_dir(&url.clone().into()).is_err()); |
| assert!(manager.packages[&url].package_directory.is_none()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_write() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let (package_resolver, _test_dir) = get_test_package_resolver_with_hash(TEST_HASH); |
| let (pkg_cache, pkg_cache_stream) = get_mock_pkg_cache(); |
| |
| let data_dir = tempfile::tempdir().unwrap(); |
| let data_proxy = fuchsia_fs::open_directory_in_namespace( |
| data_dir.path().to_str().unwrap(), |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE, |
| ) |
| .unwrap(); |
| let mut manager = EagerPackageManager::from_config( |
| config.clone(), |
| package_resolver.clone(), |
| pkg_cache.clone(), |
| Some(Clone::clone(&data_proxy)), |
| ) |
| .await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| manager |
| .cup_write(&fpkg::PackageUrl { url: TEST_PINNED_URL.into() }, cup.clone().into()) |
| .await |
| .unwrap(); |
| assert!(manager.packages[&url].package_directory.is_some()); |
| assert_eq!(manager.packages[&url].cup, Some(cup.clone())); |
| |
| // create a new manager which should load the persisted cup data. |
| let (manager2, ()) = future::join( |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, Some(data_proxy)), |
| handle_pkg_cache(pkg_cache_stream), |
| ) |
| .await; |
| assert!(manager2.packages[&url].package_directory.is_some()); |
| assert_eq!(manager2.packages[&url].cup, Some(cup)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_write_persist_fail() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let (package_resolver, _test_dir) = get_test_package_resolver_with_hash(TEST_HASH); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let mut manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| assert_matches!( |
| manager.cup_write(&fpkg::PackageUrl { url: TEST_PINNED_URL.into() }, cup.into()).await, |
| Err(CupWriteError::Persist(_)) |
| ); |
| assert!(manager.packages[&url].package_directory.is_none()); |
| assert_eq!(manager.packages[&url].cup, None); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_write_omaha_response_different_url() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let (package_resolver, _test_dir) = get_test_package_resolver_with_hash(TEST_HASH); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let mut manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| assert_matches!( |
| manager |
| .cup_write(&fpkg::PackageUrl { url: format!("{url}?hash=beefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdead") }, cup.into()) |
| .await, |
| Err(CupWriteError::CupResponseURLNotFound) |
| ); |
| assert!(manager.packages[&url].package_directory.is_none()); |
| assert!(manager.packages[&url].cup.is_none()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_write_unknown_url() { |
| let url = UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.com/package2").unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let mut manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| assert_matches!( |
| manager.cup_write(&fpkg::PackageUrl { url: TEST_PINNED_URL.into() }, cup.into()).await, |
| Err(CupWriteError::UnknownURL(_)) |
| ); |
| assert!(manager.packages[&url].package_directory.is_none()); |
| assert!(manager.packages[&url].cup.is_none()); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_get_info() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let mut manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| manager.packages.get_mut(&url).unwrap().cup = Some(cup); |
| let (version, channel) = |
| manager.cup_get_info(&fpkg::PackageUrl { url: TEST_URL.into() }).await.unwrap(); |
| assert_eq!(version, "1.2.3.4"); |
| assert_eq!(channel, "stable"); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_get_info_not_available() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| assert_matches!( |
| manager.cup_get_info(&fpkg::PackageUrl { url: TEST_URL.into() }).await, |
| Err(CupGetInfoError::CupDataNotAvailable) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_get_info_unknown_url() { |
| let url = UnpinnedAbsolutePackageUrl::parse(TEST_URL).unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let mut manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| manager.packages.get_mut(&url).unwrap().cup = Some(cup); |
| assert_matches!( |
| manager |
| .cup_get_info(&fpkg::PackageUrl { |
| url: "fuchsia-pkg://example.com/package2".into() |
| }) |
| .await, |
| Err(CupGetInfoError::UnknownURL(_)) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_cup_get_info_omaha_response_different_url() { |
| let url = UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.com/package2").unwrap(); |
| let config = EagerPackageConfigs { |
| packages: vec![EagerPackageConfig { |
| url: url.clone(), |
| executable: true, |
| public_keys: make_default_public_keys_for_test(), |
| }], |
| }; |
| let package_resolver = get_test_package_resolver(); |
| let (pkg_cache, _) = get_mock_pkg_cache(); |
| |
| let mut manager = |
| EagerPackageManager::from_config(config, package_resolver, pkg_cache, None).await; |
| let cup: CupData = make_cup_data(&get_default_cup_response()); |
| manager.packages.get_mut(&url).unwrap().cup = Some(cup); |
| assert_matches!( |
| manager |
| .cup_get_info(&fpkg::PackageUrl { |
| url: "fuchsia-pkg://example.com/package2".into() |
| }) |
| .await, |
| Err(CupGetInfoError::CupResponseURLNotFound) |
| ); |
| } |
| } |