diff --git a/rust/agama-software/src/callbacks/security.rs b/rust/agama-software/src/callbacks/security.rs index 07076ae636..b3c32314f9 100644 --- a/rust/agama-software/src/callbacks/security.rs +++ b/rust/agama-software/src/callbacks/security.rs @@ -3,16 +3,30 @@ use agama_utils::{actor::Handler, api::question::QuestionSpec, question}; use i18n_format::i18n_format; use zypp_agama::callbacks::security; -use crate::callbacks::ask_software_question; +use crate::{callbacks::ask_software_question, state::RepoKey}; #[derive(Clone)] pub struct Security { questions: Handler, + trusted_gpg_keys: Vec, + unsigned_repos: Vec, } impl Security { pub fn new(questions: Handler) -> Self { - Self { questions } + Self { + questions, + trusted_gpg_keys: vec![], + unsigned_repos: vec![], + } + } + + pub fn set_trusted_gpg_keys(&mut self, trusted_gpg_keys: Vec) { + self.trusted_gpg_keys = trusted_gpg_keys; + } + + pub fn set_unsigned_repos(&mut self, unsigned_repos: Vec) { + self.unsigned_repos = unsigned_repos; } } @@ -23,7 +37,11 @@ impl security::Callback for Security { file, repository_alias ); - // TODO: support for extra_repositories with allow_unsigned config + + if self.unsigned_repos.contains(&repository_alias) { + return true; + } + // TODO: localization for text when parameters in gextext will be solved let text = if repository_alias.is_empty() { format!( @@ -53,7 +71,7 @@ impl security::Callback for Security { key_id: String, key_name: String, key_fingerprint: String, - _repository_alias: String, + repository_alias: String, ) -> security::GpgKeyTrust { tracing::info!( "accept_key callback: key_id='{}', key_name='{}', key_fingerprint='{}'", @@ -61,14 +79,31 @@ impl security::Callback for Security { key_name, key_fingerprint, ); - // TODO: support for extra_repositories with specified gpg key checksum + + let predefined = self + .trusted_gpg_keys + .iter() + .any(|key| key.alias == repository_alias && key.fingerprint == key_fingerprint); + if predefined { + tracing::info!("GPG key trusted as specified in profile"); + return security::GpgKeyTrust::Import; + } + + let human_fingerprint = key_fingerprint + .chars() + .collect::>() + .chunks(4) + .map(|chunk| chunk.iter().collect::()) + .collect::>() + .join(" "); + let text = i18n_format!( // TRANSLATORS: substituting: key ID, (key name), fingerprint "The key {0} ({1}) with fingerprint {2} is unknown. \ Do you want to trust this key?", &key_id, &key_name, - &key_fingerprint + &human_fingerprint ); let question = QuestionSpec::new(&text, "software.import_gpg") .with_action_ids(&[gettext_noop("Trust"), gettext_noop("Skip")]) diff --git a/rust/agama-software/src/model/state.rs b/rust/agama-software/src/model/state.rs index 325b10b4a0..153227c94b 100644 --- a/rust/agama-software/src/model/state.rs +++ b/rust/agama-software/src/model/state.rs @@ -35,6 +35,12 @@ use url::Url; use crate::{model::software_selection::SoftwareSelection, Resolvable, ResolvableType}; +#[derive(Clone, Debug)] +pub struct RepoKey { + pub alias: String, + pub fingerprint: String, +} + /// Represents the wanted software configuration. /// /// It includes the list of repositories, selected resolvables, configuration @@ -50,6 +56,8 @@ pub struct SoftwareState { pub options: SoftwareOptions, pub registration: Option, pub allow_registration: bool, + pub trusted_gpg_keys: Vec, + pub unsigned_repos: Vec, } impl SoftwareState { @@ -62,6 +70,8 @@ impl SoftwareState { options: Default::default(), registration: None, allow_registration: false, + trusted_gpg_keys: vec![], + unsigned_repos: vec![], } } } @@ -218,6 +228,23 @@ impl<'a> SoftwareStateBuilder<'a> { if let Some(repositories) = &config.extra_repositories { let extra = repositories.iter().map(Repository::from); state.repositories.extend(extra); + + // create map for gpg signatures + for repo in repositories { + if let Some(gpg_fingerprints) = &repo.gpg_fingerprints { + for fingerprint in gpg_fingerprints { + state.trusted_gpg_keys.push(RepoKey { + alias: repo.alias.clone(), + // remove all whitespaces to sanitize input + fingerprint: fingerprint.replace(" ", ""), + }); + } + } + + if repo.allow_unsigned == Some(true) { + state.unsigned_repos.push(repo.alias.clone()); + } + } } if let Some(patterns) = &config.patterns { @@ -371,6 +398,8 @@ impl<'a> SoftwareStateBuilder<'a> { registration: None, options: Default::default(), allow_registration: self.product.registration, + trusted_gpg_keys: vec![], + unsigned_repos: vec![], } } } diff --git a/rust/agama-software/src/zypp_server.rs b/rust/agama-software/src/zypp_server.rs index 32a9ed5b47..363f8479aa 100644 --- a/rust/agama-software/src/zypp_server.rs +++ b/rust/agama-software/src/zypp_server.rs @@ -45,7 +45,7 @@ use crate::{ registration::RegistrationError, state::{self, SoftwareState}, }, - state::{Addon, RegistrationState, ResolvableSelection}, + state::{Addon, RegistrationState, RepoKey, ResolvableSelection}, Registration, ResolvableType, }; @@ -127,6 +127,8 @@ pub struct ZyppServer { registration: RegistrationStatus, root_dir: Utf8PathBuf, install_dir: Utf8PathBuf, + trusted_keys: Vec, + unsigned_repos: Vec, } impl ZyppServer { @@ -144,6 +146,8 @@ impl ZyppServer { root_dir: root_dir.as_ref().to_path_buf(), install_dir: install_dir.as_ref().to_path_buf(), registration: Default::default(), + trusted_keys: vec![], + unsigned_repos: vec![], }; // drop the returned JoinHandle: the thread will be detached @@ -238,6 +242,8 @@ impl ZyppServer { callbacks::CommitDownload::new(progress.clone(), question.clone()); let mut install_callback = callbacks::Install::new(progress.clone(), question.clone()); let mut security_callback = callbacks::Security::new(question); + security_callback.set_trusted_gpg_keys(self.trusted_keys.clone()); + security_callback.set_unsigned_repos(self.unsigned_repos.clone()); let packages_count = zypp.packages_count(); // use packages count *2 as we need to download package and also install it @@ -313,6 +319,11 @@ impl ZyppServer { self.update_registration(registration_config, &zypp, &security_srv, &mut issues); } + self.trusted_keys = state.trusted_gpg_keys; + security.set_trusted_gpg_keys(self.trusted_keys.clone()); + self.unsigned_repos = state.unsigned_repos; + security.set_unsigned_repos(self.unsigned_repos.clone()); + progress.cast(progress::message::Next::new(Scope::Software))?; let old_aliases: Vec<_> = old_state .repositories diff --git a/rust/package/agama.changes b/rust/package/agama.changes index e33a13a730..2cf2515f24 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Wed Jan 28 09:59:43 UTC 2026 - Josef Reidinger + +- Support "gpgFingerprints" and "allowUnsigned" keys in + "software/extraRepositories" section of profile + (gh#agama-project/agama#3087) + ------------------------------------------------------------------- Tue Jan 27 18:35:59 UTC 2026 - Ladislav Slezák