Repository: golemfactory/yagna (language: python)

Issue:
Provider supports whitelist of allowed domains
Why:
- Whitelisting domains will allow community users to create dApps accessing blockchain without need to distribute their certificates or signing their images by us.

What:
- Provider have configurable whitelist with domain names
- We want to distribute whitelist with yagna releases
- During negotiations Provider will accept Proposals from Requestors if, their manifest includes access only to domains on the whitelist

Progress:

- [x] Serializable domain whitelist + matching utils (regex, strict) + using whitelist in Manifest Negotiator
- [x] Config file monitoring (similar as for Globals or Presets)
- [x] Cmdline for whitelist (list, add, remove)
- [x] Yapapi modifications (optional sig+sig_alg+cert)
- [x] negotiator functional tests 
- [x] cmdline functional tests


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index b58bc5cf5b..463b000c01 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -502,6 +502,20 @@ dependencies = [
  "nom",
 ]
 
+[[package]]
+name = "assert_cmd"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e"
+dependencies = [
+ "bstr",
+ "doc-comment",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
 [[package]]
 name = "async-compression"
 version = "0.3.7"
@@ -1091,7 +1105,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2586208b33573b7f76ccfbe5adb076394c88deaf81b84d7213969805b0a952a7"
 dependencies = [
  "clicolors-control",
- "encode_unicode",
+ "encode_unicode 0.3.6",
  "lazy_static",
  "libc",
  "regex",
@@ -1107,7 +1121,7 @@ version = "0.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
 dependencies = [
- "encode_unicode",
+ "encode_unicode 0.3.6",
  "libc",
  "once_cell",
  "terminal_size",
@@ -1590,6 +1604,12 @@ dependencies = [
  "migrations_macros",
 ]
 
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
 [[package]]
 name = "digest"
 version = "0.8.1"
@@ -1681,6 +1701,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "doc-comment"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
+
 [[package]]
 name = "dotenv"
 version = "0.15.0"
@@ -1699,6 +1725,12 @@ version = "0.3.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
 
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.31"
@@ -2059,6 +2091,15 @@ dependencies = [
  "time 0.3.9",
 ]
 
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -2409,7 +2450,7 @@ dependencies = [
  "log",
  "names",
  "nix 0.22.3",
- "prettytable-rs",
+ "prettytable-rs 0.8.0",
  "promptly",
  "rustyline 6.3.0",
  "serde",
@@ -3748,6 +3789,12 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "44a1290799eababa63ea60af0cbc3f03363e328e58f32fb0294798ed3e85f444"
 
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
 [[package]]
 name = "notify"
 version = "4.0.17"
@@ -4325,6 +4372,36 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
+[[package]]
+name = "predicates"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c"
+dependencies = [
+ "difflib",
+ "float-cmp",
+ "itertools 0.10.3",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
 [[package]]
 name = "prettytable-rs"
 version = "0.8.0"
@@ -4333,9 +4410,23 @@ checksum = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e"
 dependencies = [
  "atty",
  "csv",
- "encode_unicode",
+ "encode_unicode 0.3.6",
+ "lazy_static",
+ "term 0.5.2",
+ "unicode-width",
+]
+
+[[package]]
+name = "prettytable-rs"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f375cb74c23b51d23937ffdeb48b1fbf5b6409d4b9979c1418c1de58bc8f801"
+dependencies = [
+ "atty",
+ "csv",
+ "encode_unicode 1.0.0",
  "lazy_static",
- "term",
+ "term 0.7.0",
  "unicode-width",
 ]
 
@@ -6089,6 +6180,17 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next 2.0.0",
+ "rustversion",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.3"
@@ -6117,6 +6219,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "termtree"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b"
+
 [[package]]
 name = "test-case"
 version = "2.2.1"
@@ -6806,6 +6914,15 @@ dependencies = [
  "quote",
 ]
 
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "walkdir"
 version = "2.3.2"
@@ -7473,6 +7590,17 @@ dependencies = [
  "yansi",
 ]
 
+[[package]]
+name = "ya-manifest-test-utils"
+version = "0.1.0"
+dependencies = [
+ "base64 0.13.0",
+ "openssl",
+ "tar",
+ "ya-agreement-utils",
+ "ya-manifest-utils",
+]
+
 [[package]]
 name = "ya-manifest-utils"
 version = "0.1.0"
@@ -7481,8 +7609,10 @@ dependencies = [
  "base64 0.13.0",
  "chrono",
  "hex",
+ "log",
  "md-5",
  "openssl",
+ "regex",
  "semver 1.0.13",
  "serde",
  "serde_json",
@@ -7497,6 +7627,8 @@ dependencies = [
  "thiserror",
  "url",
  "ya-agreement-utils",
+ "ya-manifest-test-utils",
+ "ya-utils-path",
 ]
 
 [[package]]
@@ -7745,7 +7877,9 @@ dependencies = [
  "actix-rt",
  "actix_derive",
  "anyhow",
+ "assert_cmd",
  "backoff",
+ "base64 0.13.0",
  "bigdecimal 0.2.2",
  "bytesize",
  "chrono",
@@ -7766,10 +7900,12 @@ dependencies = [
  "num-traits",
  "num_cpus",
  "path-clean",
+ "predicates",
  "regex",
  "semver 0.11.0",
  "serde",
  "serde_json",
+ "serial_test 0.9.0",
  "shared_child",
  "shlex 1.1.0",
  "signal-hook",
@@ -7790,6 +7926,7 @@ dependencies = [
  "ya-compile-time-utils",
  "ya-core-model",
  "ya-file-logging",
+ "ya-manifest-test-utils",
  "ya-manifest-utils",
  "ya-std-utils",
  "ya-utils-actix",
@@ -8155,7 +8292,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "lazy_static",
- "prettytable-rs",
+ "prettytable-rs 0.9.0",
  "serde",
  "serde_json",
  "serde_yaml 0.9.11",
diff --git a/Cargo.toml b/Cargo.toml
index 3154132a26..4af5623814 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -145,6 +145,7 @@ members = [
     "utils/compile-time-utils",
     "utils/futures",
     "utils/manifest-utils",
+    "utils/manifest-utils/test-utils",
     "utils/networking",
     "utils/path",
     "utils/process",
@@ -210,6 +211,9 @@ ya-diesel-utils = { path = "utils/diesel-utils"}
 ya-metrics = { path = "core/metrics" }
 ya-provider = { path = "agent/provider"}
 
+## TEST UTILS
+ya-manifest-test-utils = { path = "utils/manifest-utils/test-utils" }
+
 ethereum-tx-sign = { git = "https://github.com/mfranciszkiewicz/ethereum-tx-sign.git", rev = "1164c74187a9e2947faeaea7dde104c3cdec4195" }
 graphene-sgx = { git = " https://github.com/golemfactory/graphene-rust.git", rev = "3619f63a0c1ff44962ab604e55cd5dfb62177c56" }
 web3 = { git = "https://github.com/golemfactory/rust-web3", branch = "update_ethabi" }
diff --git a/agent/provider/Cargo.toml b/agent/provider/Cargo.toml
index 07799d5c7b..2932f0da1f 100644
--- a/agent/provider/Cargo.toml
+++ b/agent/provider/Cargo.toml
@@ -74,6 +74,12 @@ nix = "0.22.0"
 winapi = { version = "0.3.8", features = ["fileapi"] }
 
 [dev-dependencies]
+assert_cmd = "2.0"
+base64 = "0.13"
 chrono = "0.4"
-shlex = "1.1.0"
 test-case = "2.1"
+predicates = "2.1"
+serial_test = "0.9"
+shlex = "1.1"
+
+ya-manifest-test-utils = "0.1"
diff --git a/agent/provider/src/cli.rs b/agent/provider/src/cli.rs
index aa9b96d13a..bdb28cf95d 100644
--- a/agent/provider/src/cli.rs
+++ b/agent/provider/src/cli.rs
@@ -5,3 +5,13 @@ pub mod exe_unit;
 pub mod keystore;
 pub mod preset;
 pub mod profile;
+pub mod whitelist;
+
+use crate::startup_config::ProviderConfig;
+
+/// Prints line if `json` output disabled.
+fn println_conditional(config: &ProviderConfig, txt: &str) {
+    if !config.json {
+        println!("{txt}");
+    }
+}
diff --git a/agent/provider/src/cli/keystore.rs b/agent/provider/src/cli/keystore.rs
index e6f7911d90..d44f3cdcec 100644
--- a/agent/provider/src/cli/keystore.rs
+++ b/agent/provider/src/cli/keystore.rs
@@ -7,10 +7,17 @@ use ya_manifest_utils::util::{self, CertBasicData, CertBasicDataVisitor};
 use ya_manifest_utils::KeystoreLoadResult;
 use ya_utils_cli::{CommandOutput, ResponseTable};
 
+use crate::cli::println_conditional;
 use crate::startup_config::ProviderConfig;
 
 #[derive(StructOpt, Clone, Debug)]
-#[structopt(rename_all = "kebab-case")]
+#[structopt(
+    rename_all = "kebab-case",
+    help = "Keystore stores X.509 certificates. They allow to accept Demands with 
+    Computation Payload Manifests which arrive with signature and app author's public certificate. 
+    Certificate gets validated against certificates loaded into the keystore.
+    Certificates are stored in a file format in directory. Its location can be configured using '--cert-dir' param."
+)]
 pub enum KeystoreConfig {
     /// List trusted X.509 certificates
     List,
@@ -22,8 +29,12 @@ pub enum KeystoreConfig {
 
 #[derive(StructOpt, Clone, Debug)]
 pub struct Add {
-    /// Paths to X.509 certificates (PEM or DER) or certificates chains
-    #[structopt(parse(from_os_str))]
+    /// Paths to X.509 certificates or certificates chains
+    #[structopt(
+        parse(from_os_str),
+        help = "Coma separated list of X.509 certificate files (PEM or DER) 
+        or PEM certificates chains to be added to the Keystore."
+    )]
     certs: Vec<PathBuf>,
 }
 
@@ -31,6 +42,8 @@ pub struct Add {
 #[structopt(rename_all = "kebab-case")]
 pub struct Remove {
     /// Certificate ids
+    #[structopt(help = "Coma separated list of X.509 certificates' ids. 
+        To find certificate id use `keystore list` command.")]
     ids: Vec<String>,
 }
 
@@ -67,9 +80,15 @@ fn add(config: ProviderConfig, add: Add) -> anyhow::Result<()> {
             }
         }
         KeystoreLoadResult::NothingNewToLoad { skipped } => {
-            println_conditional(&config, "No new certificate to add. Skipped:");
             let certs_data = util::to_cert_data(&skipped)?;
-            print_cert_list(&config, certs_data)?;
+            if !config.json {
+                println!("No new certificate to add.");
+                println!("Dropped duplicated certificates:");
+                print_cert_list(&config, certs_data)?;
+            } else {
+                // no new certificate added, so empty list for json output
+                print_cert_list(&config, Vec::new())?;
+            }
         }
     }
     Ok(())
@@ -82,6 +101,9 @@ fn remove(config: ProviderConfig, remove: Remove) -> anyhow::Result<()> {
     match keystore_manager.remove_certs(&ids)? {
         util::KeystoreRemoveResult::NothingToRemove => {
             println_conditional(&config, "No matching certificates to remove.");
+            if config.json {
+                print_cert_list(&config, Vec::new())?;
+            }
         }
         util::KeystoreRemoveResult::Removed { removed } => {
             println!("Removed certificates:");
@@ -93,7 +115,7 @@ fn remove(config: ProviderConfig, remove: Remove) -> anyhow::Result<()> {
 }
 
 fn cert_dir_path(config: &ProviderConfig) -> anyhow::Result<PathBuf> {
-    Ok(config.cert_dir.get_or_create()?)
+    config.cert_dir.get_or_create()
 }
 
 fn print_cert_list(
@@ -104,7 +126,7 @@ fn print_cert_list(
     for data in certs_data {
         table.add(data);
     }
-    table.print(&config)?;
+    table.print(config)?;
     Ok(())
 }
 
@@ -131,19 +153,13 @@ impl CertTable {
     }
 
     pub fn add(&mut self, data: CertBasicData) {
-        self.accept(data)
+        let row = serde_json::json! {[ data.id, data.not_after, data.subject ]};
+        self.table.values.push(row)
     }
 }
 
 impl CertBasicDataVisitor for CertTable {
     fn accept(&mut self, data: CertBasicData) {
-        let row = serde_json::json! {[ data.id, data.not_after, data.subject ]};
-        self.table.values.push(row);
-    }
-}
-
-fn println_conditional(config: &ProviderConfig, txt: &str) {
-    if !config.json {
-        println!("{txt}");
+        self.add(data)
     }
 }
diff --git a/agent/provider/src/cli/whitelist.rs b/agent/provider/src/cli/whitelist.rs
new file mode 100644
index 0000000000..552241a4c4
--- /dev/null
+++ b/agent/provider/src/cli/whitelist.rs
@@ -0,0 +1,245 @@
+use std::collections::HashMap;
+
+use structopt::StructOpt;
+
+use ya_manifest_utils::{
+    matching::domain::{pattern_to_id, DomainPattern, DomainPatterns},
+    ArgMatch,
+};
+use ya_utils_cli::{CommandOutput, ResponseTable};
+
+use crate::cli::println_conditional;
+use crate::startup_config::ProviderConfig;
+
+#[derive(StructOpt, Clone, Debug)]
+#[structopt(
+    rename_all = "kebab-case",
+    help = "Domain Whitelist allows to accept Demands with 
+    Computation Payload Manifests which declare usage of Outbound Network but arrive with no signature."
+)]
+pub enum WhitelistConfig {
+    /// List domain whitelist patterns
+    List,
+    /// Add new domain whitelist patterns
+    Add(Add),
+    /// Remove domain whitelist patterns
+    Remove(Remove),
+}
+
+#[derive(StructOpt, Clone, Debug)]
+#[structopt(rename_all = "kebab-case")]
+pub struct Add {
+    /// Domain whitelist patterns
+    #[structopt(
+        long,
+        short,
+        help = "Coma separated domain Whitelist patterns. 
+        Adding URL as a pattern ('regex' or 'strict') will not work."
+    )]
+    patterns: Vec<String>,
+
+    /// Domain whitelist patterns type
+    #[structopt(
+        long = "type",
+        short = "t",
+        default_value = "strict",
+        help = "Domain Whitelist pattern type takes value 'strict' or 'regex'. 
+        Regex patterns are by default wrapped with '.*' patterns."
+    )]
+    pattern_type: ArgMatch,
+}
+
+#[derive(StructOpt, Clone, Debug)]
+#[structopt(rename_all = "kebab-case")]
+pub struct Remove {
+    /// Domain whitelist pattern ids.
+    #[structopt(
+        help = "Coma separated list of domain Whitelist patterns' ids to be removed. 
+        To find pattern's id use 'whitelist list' command."
+    )]
+    ids: Vec<String>,
+}
+
+impl WhitelistConfig {
+    pub fn run(self, config: ProviderConfig) -> anyhow::Result<()> {
+        match self {
+            WhitelistConfig::List => list(config),
+            WhitelistConfig::Add(cmd) => add(config, cmd),
+            WhitelistConfig::Remove(cmd) => remove(config, cmd),
+        }
+    }
+}
+
+fn list(config: ProviderConfig) -> anyhow::Result<()> {
+    let domain_patterns = DomainPatterns::load_or_create(&config.domain_whitelist_file)?;
+    let table = WhitelistTable::from(domain_patterns);
+    table.print(&config)
+}
+
+fn add(config: ProviderConfig, add: Add) -> anyhow::Result<()> {
+    let domain_patterns = DomainPatterns::load_or_create(&config.domain_whitelist_file)?;
+    let mut domain_patterns = DomainPatternIds::from(domain_patterns);
+    let added = domain_patterns.add(add);
+    let domain_patterns: DomainPatterns = domain_patterns.into();
+    domain_patterns.save(&config.domain_whitelist_file)?;
+    if !added.processed.is_empty() {
+        println_conditional(&config, "Added patterns:");
+        WhitelistTable::from(DomainPatterns {
+            patterns: added.processed,
+        })
+        .print(&config)?
+    } else {
+        println_conditional(&config, "No new patterns to add.");
+        if config.json {
+            // no new pattern to add, so empty list for json output
+            WhitelistTable::from(DomainPatterns {
+                patterns: Vec::new(),
+            })
+            .print(&config)?
+        }
+    }
+    if !added.skipped.is_empty() && !config.json {
+        println!("Dropped duplicated patterns:");
+        WhitelistTable::from(DomainPatterns {
+            patterns: added.skipped,
+        })
+        .print(&config)?;
+    }
+    Ok(())
+}
+
+fn remove(config: ProviderConfig, remove: Remove) -> anyhow::Result<()> {
+    let domain_patterns = DomainPatterns::load_or_create(&config.domain_whitelist_file)?;
+    let mut domain_patterns = DomainPatternIds::from(domain_patterns);
+    let removed = domain_patterns.remove(remove.ids);
+    let domain_patterns: DomainPatterns = domain_patterns.into();
+    domain_patterns.save(&config.domain_whitelist_file)?;
+    if !removed.processed.is_empty() {
+        let table = WhitelistTable::from(DomainPatterns {
+            patterns: removed.processed,
+        });
+        println_conditional(&config, "Removed patterns:");
+        table.print(&config)?;
+    } else {
+        println_conditional(&config, "No matching pattern to remove.");
+        // no new pattern added, so empty list for json output
+        if config.json {
+            WhitelistTable::from(DomainPatterns {
+                patterns: Vec::new(),
+            })
+            .print(&config)?
+        }
+    };
+    Ok(())
+}
+
+struct WhitelistTable {
+    table: ResponseTable,
+}
+
+impl WhitelistTable {
+    pub fn new() -> Self {
+        let columns = vec!["ID".to_string(), "Pattern".to_string(), "Type".to_string()];
+        let values = vec![];
+        let table = ResponseTable { columns, values };
+        Self { table }
+    }
+
+    fn add(&mut self, pattern: DomainPattern) {
+        let id = pattern_to_id(&pattern);
+        let row = serde_json::json! {[ id, pattern.domain, pattern.domain_match ]};
+        self.table.values.push(row);
+    }
+
+    pub fn print(self, config: &ProviderConfig) -> anyhow::Result<()> {
+        let output = CommandOutput::from(self.table);
+        output.print(config.json)?;
+        Ok(())
+    }
+}
+
+impl From<DomainPatterns> for WhitelistTable {
+    fn from(domain_patterns: DomainPatterns) -> Self {
+        let mut table = WhitelistTable::new();
+        for pattern in domain_patterns.patterns {
+            table.add(pattern);
+        }
+        table
+    }
+}
+
+/// Collection of `DomainPattern`s mapped to ids generated from their contents.
+/// # See also
+/// ya_manifest_utils::matching::domain::pattern_to_id
+struct DomainPatternIds {
+    pattern_ids: HashMap<String, DomainPattern>,
+}
+
+impl From<DomainPatterns> for DomainPatternIds {
+    fn from(patterns: DomainPatterns) -> Self {
+        let mut pattern_ids = HashMap::new();
+        let patterns = patterns.patterns;
+        for pattern in patterns {
+            let id = pattern_to_id(&pattern);
+            pattern_ids.insert(id, pattern);
+        }
+        Self { pattern_ids }
+    }
+}
+
+impl From<DomainPatternIds> for DomainPatterns {
+    fn from(val: DomainPatternIds) -> Self {
+        let patterns = val.pattern_ids.into_values().collect();
+        DomainPatterns { patterns }
+    }
+}
+
+impl DomainPatternIds {
+    fn remove(&mut self, ids: Vec<String>) -> DomainPatternsRemoved {
+        let mut removed = Vec::new();
+        let mut skipped = Vec::new();
+        for id in ids {
+            if let Some(pattern) = self.pattern_ids.remove(&id) {
+                removed.push(pattern);
+            } else {
+                skipped.push(id);
+            }
+        }
+        DomainPatternsRemoved {
+            processed: removed,
+            skipped,
+        }
+    }
+
+    fn add(&mut self, add: Add) -> DomainPatternsAdded {
+        let mut added = Vec::new();
+        let mut skipped = Vec::new();
+        let domain_match = add.pattern_type;
+        for domain in add.patterns.into_iter() {
+            let domain = domain.to_lowercase();
+            let pattern = DomainPattern {
+                domain,
+                domain_match,
+            };
+            let id = pattern_to_id(&pattern);
+            if let Some(duplicate) = self.pattern_ids.insert(id, pattern.clone()) {
+                skipped.push(duplicate);
+            } else {
+                added.push(pattern)
+            }
+        }
+        DomainPatternsAdded {
+            processed: added,
+            skipped,
+        }
+    }
+}
+
+struct DomainPatternsChange<SKIPPED> {
+    processed: Vec<DomainPattern>,
+    skipped: Vec<SKIPPED>,
+}
+
+type DomainPatternsAdded = DomainPatternsChange<DomainPattern>;
+/// Removed `DomainPattern`s with `id`s of skipped patterns.
+type DomainPatternsRemoved = DomainPatternsChange<String>;
diff --git a/agent/provider/src/main.rs b/agent/provider/src/main.rs
index 2c6e61ec55..1e61e35a60 100644
--- a/agent/provider/src/main.rs
+++ b/agent/provider/src/main.rs
@@ -22,6 +22,7 @@ async fn main() -> anyhow::Result<()> {
     let mut config = cli_args.config;
     let data_dir = config.data_dir.get_or_create()?;
 
+    config.domain_whitelist_file = data_dir.join(config.domain_whitelist_file);
     config.globals_file = data_dir.join(config.globals_file);
     config.presets_file = data_dir.join(config.presets_file);
     config.hardware_file = data_dir.join(config.hardware_file);
@@ -43,6 +44,7 @@ async fn main() -> anyhow::Result<()> {
         Commands::Profile(profile_cmd) => profile_cmd.run(config),
         Commands::ExeUnit(exe_unit_cmd) => exe_unit_cmd.run(config),
         Commands::Keystore(keystore_cmd) => keystore_cmd.run(config),
+        Commands::Whitelist(whitelist_cmd) => whitelist_cmd.run(config),
         Commands::Clean(clean_cmd) => clean_cmd.run(config),
     }
 }
diff --git a/agent/provider/src/market/negotiator/builtin/manifest.rs b/agent/provider/src/market/negotiator/builtin/manifest.rs
index 75e828da50..04305ace69 100644
--- a/agent/provider/src/market/negotiator/builtin/manifest.rs
+++ b/agent/provider/src/market/negotiator/builtin/manifest.rs
@@ -1,9 +1,12 @@
 use std::ops::Not;
 
+use url::Url;
 use ya_agreement_utils::{Error, OfferDefinition};
+use ya_manifest_utils::matching::domain::SharedDomainMatchers;
+use ya_manifest_utils::matching::Matcher;
 use ya_manifest_utils::policy::{Keystore, Match, Policy, PolicyConfig};
 use ya_manifest_utils::{
-    decode_manifest, Feature, CAPABILITIES_PROPERTY, DEMAND_MANIFEST_CERT_PROPERTY,
+    decode_manifest, AppManifest, Feature, CAPABILITIES_PROPERTY, DEMAND_MANIFEST_CERT_PROPERTY,
     DEMAND_MANIFEST_PROPERTY, DEMAND_MANIFEST_SIG_ALGORITHM_PROPERTY, DEMAND_MANIFEST_SIG_PROPERTY,
 };
 
@@ -13,6 +16,7 @@ use crate::market::negotiator::*;
 pub struct ManifestSignature {
     enabled: bool,
     keystore: Keystore,
+    whitelist_matcher: SharedDomainMatchers,
 }
 
 impl NegotiatorComponent for ManifestSignature {
@@ -22,26 +26,33 @@ impl NegotiatorComponent for ManifestSignature {
         offer: ProposalView,
     ) -> anyhow::Result<NegotiationResult> {
         if self.enabled.not() {
-            return Ok(NegotiationResult::Ready { offer });
+            log::trace!("Manifest signature verification disabled.");
+            return acceptance(offer);
         }
 
-        let (manifest, manifest_encoded) =
-            match demand.get_property::<String>(DEMAND_MANIFEST_PROPERTY) {
-                Err(Error::NoKey(_)) => return Ok(NegotiationResult::Ready { offer }),
-                Err(e) => return rejection(format!("invalid manifest type: {:?}", e)),
-                Ok(manifest_encoded) => match decode_manifest(&manifest_encoded) {
-                    Ok(manifest) => (manifest, manifest_encoded),
-                    Err(e) => return rejection(format!("invalid manifest: {:?}", e)),
+        let demand = match demand.get_property::<String>(DEMAND_MANIFEST_PROPERTY) {
+            Ok(manifest_encoded) => match decode_manifest(&manifest_encoded) {
+                Ok(manifest) => DemandWithManifest {
+                    demand,
+                    manifest_encoded,
+                    manifest,
                 },
-            };
-
-        if manifest.features().is_empty() {
-            return Ok(NegotiationResult::Ready { offer });
-        }
+                Err(e) => return rejection(format!("invalid manifest: {:?}", e)),
+            },
+            Err(Error::NoKey(_)) => return acceptance(offer),
+            Err(e) => return rejection(format!("invalid manifest type: {:?}", e)),
+        };
 
-        match self.verify_signature(demand, manifest_encoded) {
-            Err(err) => rejection(format!("failed to verify manifest signature: {}", err)),
-            Ok(()) => Ok(NegotiationResult::Ready { offer }),
+        if demand.has_signature() {
+            match demand.verify_signature(&self.keystore) {
+                Ok(()) => acceptance(offer),
+                Err(err) => rejection(format!("failed to verify manifest signature: {}", err)),
+            }
+        } else if demand.requires_signature(&self.whitelist_matcher) {
+            rejection("manifest requires signature but it has none".to_string())
+        } else {
+            log::trace!("No signature required. No signature provided.");
+            acceptance(offer)
         }
     }
 
@@ -83,28 +94,79 @@ impl From<PolicyConfig> for ManifestSignature {
             }
         };
 
+        let whitelist_matcher = config.domain_patterns.matchers.clone();
+        let keystore = config.trusted_keys.unwrap_or_default();
         ManifestSignature {
             enabled,
-            keystore: config.trusted_keys.unwrap_or_default(),
+            keystore,
+            whitelist_matcher,
         }
     }
 }
 
-impl ManifestSignature {
-    /// Verifies fields base64 encoding, then validates certificate, then validates signature, then verifies manifest content and returns it
-    fn verify_signature<S: AsRef<str>>(
-        &self,
-        demand: &ProposalView,
-        manifest: S,
-    ) -> anyhow::Result<()> {
-        let sig: String = demand.get_property(DEMAND_MANIFEST_SIG_PROPERTY)?;
+struct DemandWithManifest<'demand> {
+    demand: &'demand ProposalView,
+    manifest_encoded: String,
+    manifest: AppManifest,
+}
+
+impl<'demand> DemandWithManifest<'demand> {
+    fn has_signature(&self) -> bool {
+        self.demand
+            .get_property::<String>(DEMAND_MANIFEST_SIG_PROPERTY)
+            .is_ok()
+    }
+
+    fn requires_signature(&self, whitelist_matcher: &SharedDomainMatchers) -> bool {
+        let features = self.manifest.features();
+        if features.is_empty() {
+            log::debug!("No features in demand. Signature not required.");
+            return false;
+        // Inet is the only feature
+        } else if features.contains(&Feature::Inet) && features.len() == 1 {
+            if let Some(urls) = self
+                .manifest
+                .comp_manifest
+                .as_ref()
+                .and_then(|comp| comp.net.as_ref())
+                .and_then(|net| net.inet.as_ref())
+                .and_then(|inet| inet.out.as_ref())
+                .and_then(|out| out.urls.as_ref())
+            {
+                let matcher = whitelist_matcher.read().unwrap();
+                let non_whitelisted_urls: Vec<&str> = urls
+                    .iter()
+                    .flat_map(Url::host_str)
+                    .filter(|domain| matcher.matches(domain).not())
+                    .collect();
+                if non_whitelisted_urls.is_empty() {
+                    log::debug!("Demand does not require signature. Every URL on whitelist");
+                    return false;
+                }
+                log::debug!(
+                    "Demand requires signature. Non whitelisted URLs: {:?}",
+                    non_whitelisted_urls
+                );
+                return true;
+            }
+        }
+        log::debug!("Demand requires signature.");
+        true
+    }
+
+    fn verify_signature(&self, keystore: &Keystore) -> anyhow::Result<()> {
+        let sig = self
+            .demand
+            .get_property::<String>(DEMAND_MANIFEST_SIG_PROPERTY)?;
         log::trace!("sig_hex: {}", sig);
-        let sig_alg: String = demand.get_property(DEMAND_MANIFEST_SIG_ALGORITHM_PROPERTY)?;
+        let sig_alg: String = self
+            .demand
+            .get_property(DEMAND_MANIFEST_SIG_ALGORITHM_PROPERTY)?;
         log::trace!("sig_alg: {}", sig_alg);
-        let cert: String = demand.get_property(DEMAND_MANIFEST_CERT_PROPERTY)?;
+        let cert: String = self.demand.get_property(DEMAND_MANIFEST_CERT_PROPERTY)?;
         log::trace!("cert: {}", cert);
-        log::trace!("manifest: {}", manifest.as_ref());
-        self.keystore.verify_signature(cert, sig, sig_alg, manifest)
+        log::trace!("manifest: {}", &self.manifest_encoded);
+        keystore.verify_signature(cert, sig, sig_alg, &self.manifest_encoded)
     }
 }
 
@@ -115,6 +177,10 @@ fn rejection(message: String) -> anyhow::Result<NegotiationResult> {
     })
 }
 
+fn acceptance(offer: ProposalView) -> anyhow::Result<NegotiationResult> {
+    Ok(NegotiationResult::Ready { offer })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -154,7 +220,7 @@ mod tests {
             --policy-disable-component manifest_signature_validation \
             --policy-trust-property {}={}",
             CAPABILITIES_PROPERTY,
-            Feature::Inet.to_string()
+            Feature::Inet
         ));
         assert!(!policy.enabled);
 
@@ -162,7 +228,7 @@ mod tests {
             "TEST \
             --policy-trust-property {}={}",
             CAPABILITIES_PROPERTY,
-            Feature::Inet.to_string()
+            Feature::Inet
         ));
         assert!(!policy.enabled);
 
diff --git a/agent/provider/src/market/negotiator/component.rs b/agent/provider/src/market/negotiator/component.rs
index 1202b3e483..462fb3556f 100644
--- a/agent/provider/src/market/negotiator/component.rs
+++ b/agent/provider/src/market/negotiator/component.rs
@@ -9,7 +9,7 @@ use crate::market::negotiator::AgreementResult;
 pub type ProposalView = AgreementView;
 
 /// Result returned by `NegotiatorComponent` during Proposals evaluation.
-#[derive(Serialize, Deserialize, Clone, Debug)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub enum NegotiationResult {
     /// `NegotiatorComponent` fully negotiated his part of Proposal,
     /// and it can be turned into valid Agreement. Provider will send
diff --git a/agent/provider/src/provider_agent.rs b/agent/provider/src/provider_agent.rs
index 95d3ba24fd..f7efdfba2a 100644
--- a/agent/provider/src/provider_agent.rs
+++ b/agent/provider/src/provider_agent.rs
@@ -2,6 +2,7 @@ use actix::prelude::*;
 use anyhow::{anyhow, Error};
 use futures::{FutureExt, StreamExt, TryFutureExt};
 use ya_client::net::NetApi;
+use ya_manifest_utils::matching::domain::{DomainPatterns, DomainWhitelistState, DomainsMatcher};
 
 use std::convert::TryFrom;
 use std::path::{Path, PathBuf};
@@ -66,6 +67,57 @@ impl GlobalsManager {
     }
 }
 
+/// Stores current whitelist state.
+/// Starts and stops whitelist config file monitor.
+struct WhitelistManager {
+    state: DomainWhitelistState,
+    monitor: Option<FileMonitor>,
+}
+
+impl WhitelistManager {
+    fn try_new(whitelist_file: &Path) -> anyhow::Result<Self> {
+        let patterns = DomainPatterns::load_or_create(whitelist_file)?;
+        let state = DomainWhitelistState::try_new(patterns)?;
+        Ok(Self {
+            state,
+            monitor: None,
+        })
+    }
+
+    fn spawn_monitor(&mut self, whitelist_file: &Path) -> anyhow::Result<()> {
+        let state = self.state.clone();
+        let handler = move |p: PathBuf| match DomainPatterns::load(&p) {
+            Ok(patterns) => {
+                match DomainsMatcher::try_from(&patterns) {
+                    Ok(matcher) => {
+                        *state.matchers.write().unwrap() = matcher;
+                        *state.patterns.lock().unwrap() = patterns;
+                    }
+                    Err(err) => log::error!("Failed to update domain whitelist: {err}"),
+                };
+            }
+            Err(e) => log::warn!(
+                "Error updating whitelist configuration from {:?}: {:?}",
+                p,
+                e
+            ),
+        };
+        let monitor = FileMonitor::spawn(whitelist_file, FileMonitor::on_modified(handler))?;
+        self.monitor = Some(monitor);
+        Ok(())
+    }
+
+    fn get_state(&self) -> DomainWhitelistState {
+        self.state.clone()
+    }
+
+    fn stop(&mut self) {
+        if let Some(monitor) = &mut self.monitor {
+            monitor.stop();
+        }
+    }
+}
+
 pub struct ProviderAgent {
     globals: GlobalsManager,
     market: Addr<ProviderMarket>,
@@ -78,6 +130,7 @@ pub struct ProviderAgent {
     networks: Vec<NetworkName>,
     keystore_monitor: FileMonitor,
     net_api: NetApi,
+    domain_whitelist: WhitelistManager,
 }
 
 impl ProviderAgent {
@@ -134,25 +187,13 @@ impl ProviderAgent {
             .unwrap_or_else(|| app_name.to_string());
 
         let cert_dir = &config.cert_dir.get_or_create()?;
-        let keystore = match Keystore::load(cert_dir) {
-            Ok(store) => {
-                log::info!("Trusted key store loaded from {}", cert_dir.display());
-                store
-            }
-            Err(err) => {
-                log::info!("Using a new keystore: {}", err);
-                Default::default()
-            }
-        };
+        let keystore = load_keystore(cert_dir)?;
 
         args.market.session_id = format!("{}-{}", name, std::process::id());
         args.runner.session_id = args.market.session_id.clone();
         args.payment.session_id = args.market.session_id.clone();
-        args.market
-            .negotiator_config
-            .composite_config
-            .policy_config
-            .trusted_keys = Some(keystore.clone());
+        let policy_config = &mut args.market.negotiator_config.composite_config.policy_config;
+        policy_config.trusted_keys = Some(keystore.clone());
 
         let networks = args.node.account.networks.clone();
         for n in networks.iter() {
@@ -166,6 +207,7 @@ impl ProviderAgent {
             };
             log::info!("Using payment network: {}", net_color.paint(&n));
         }
+
         let mut globals = GlobalsManager::try_new(&config.globals_file, args.node)?;
         globals.spawn_monitor(&config.globals_file)?;
         let mut presets = PresetManager::load_or_create(&config.presets_file)?;
@@ -173,6 +215,9 @@ impl ProviderAgent {
         let mut hardware = hardware::Manager::try_new(&config)?;
         hardware.spawn_monitor(&config.hardware_file)?;
         let keystore_monitor = spawn_keystore_monitor(cert_dir, keystore)?;
+        let mut domain_whitelist = WhitelistManager::try_new(&config.domain_whitelist_file)?;
+        domain_whitelist.spawn_monitor(&config.domain_whitelist_file)?;
+        policy_config.domain_patterns = domain_whitelist.get_state();
 
         let market = ProviderMarket::new(api.market, args.market).start();
         let payments = Payments::new(api.activity.clone(), api.payment, args.payment).start();
@@ -193,6 +238,7 @@ impl ProviderAgent {
             networks,
             keystore_monitor,
             net_api,
+            domain_whitelist,
         })
     }
 
@@ -366,6 +412,20 @@ impl ProviderAgent {
     }
 }
 
+fn load_keystore(cert_dir: &PathBuf) -> anyhow::Result<Keystore> {
+    let keystore = match Keystore::load(cert_dir) {
+        Ok(keystore) => {
+            log::info!("Trusted key store loaded from {}", cert_dir.display());
+            keystore
+        }
+        Err(err) => {
+            log::info!("Using a new keystore: {}", err);
+            Default::default()
+        }
+    };
+    Ok(keystore)
+}
+
 fn get_prices(
     pricing_model: &dyn PricingOffer,
     preset: &Preset,
@@ -538,6 +598,7 @@ impl Handler<Shutdown> for ProviderAgent {
         let runner = self.runner.clone();
         let log_handler = self.log_handler.clone();
         self.keystore_monitor.stop();
+        self.domain_whitelist.stop();
 
         async move {
             market.send(MarketShutdown).await??;
diff --git a/agent/provider/src/startup_config.rs b/agent/provider/src/startup_config.rs
index c991356f18..c00a8b08c6 100644
--- a/agent/provider/src/startup_config.rs
+++ b/agent/provider/src/startup_config.rs
@@ -21,6 +21,7 @@ use crate::cli::exe_unit::ExeUnitsConfig;
 use crate::cli::keystore::KeystoreConfig;
 pub use crate::cli::preset::PresetsConfig;
 use crate::cli::profile::ProfileConfig;
+use crate::cli::whitelist::WhitelistConfig;
 pub(crate) use crate::config::globals::GLOBALS_JSON;
 use crate::execution::{ExeUnitsRegistry, TaskRunnerConfig};
 use crate::market::config::MarketConfig;
@@ -32,6 +33,7 @@ lazy_static::lazy_static! {
     static ref DEFAULT_CERT_DIR: String = PathBuf::from(DEFAULT_DATA_DIR.as_str()).join(CERT_DIR).to_string_lossy().to_string();
     static ref DEFAULT_PLUGINS_DIR : PathBuf = default_plugins();
 }
+pub(crate) const DOMAIN_WHITELIST_JSON: &'static str = "domain_whitelist.json";
 pub(crate) const PRESETS_JSON: &'static str = "presets.json";
 pub(crate) const HARDWARE_JSON: &'static str = "hardware.json";
 pub(crate) const CERT_DIR: &'static str = "cert_dir";
@@ -72,7 +74,8 @@ pub struct ProviderConfig {
         default_value = &*DEFAULT_CERT_DIR,
     )]
     pub cert_dir: DataDir,
-
+    #[structopt(skip = DOMAIN_WHITELIST_JSON)]
+    pub domain_whitelist_file: PathBuf,
     #[structopt(skip = GLOBALS_JSON)]
     pub globals_file: PathBuf,
     #[structopt(skip = PRESETS_JSON)]
@@ -83,22 +86,22 @@ pub struct ProviderConfig {
     #[structopt(
         long,
         set = clap::ArgSettings::Global,
-        env = "YA_RT_CORES")
-    ]
+        env = "YA_RT_CORES"
+    )]
     pub rt_cores: Option<usize>,
     /// Max amount of available RAM (GiB)
     #[structopt(
         long,
         set = clap::ArgSettings::Global,
-        env = "YA_RT_MEM")
-    ]
+        env = "YA_RT_MEM"
+    )]
     pub rt_mem: Option<f64>,
     /// Max amount of available storage (GiB)
     #[structopt(
         long,
         set = clap::ArgSettings::Global,
-        env = "YA_RT_STORAGE")
-    ]
+        env = "YA_RT_STORAGE"
+    )]
     pub rt_storage: Option<f64>,
 
     #[structopt(long, set = clap::ArgSettings::Global)]
@@ -210,6 +213,8 @@ pub enum Commands {
     ExeUnit(ExeUnitsConfig),
     /// Manage trusted keys
     Keystore(KeystoreConfig),
+    /// Manage domain whitelist
+    Whitelist(WhitelistConfig),
     /// Clean up disk space
     Clean(CleanConfig),
 }
diff --git a/utils/agreement-utils/src/agreement.rs b/utils/agreement-utils/src/agreement.rs
index c1753b9fda..744335c5af 100644
--- a/utils/agreement-utils/src/agreement.rs
+++ b/utils/agreement-utils/src/agreement.rs
@@ -14,7 +14,7 @@ const DEFAULT_FORMAT: &str = "json";
 //  - 2 fields for parsed properties (demand, offer)
 //  - other fields for agreement remain typed.
 // TODO: Move to ya-client to make it available for third party developers.
-#[derive(Serialize, Deserialize, Clone, Debug)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
 pub struct AgreementView {
     pub json: Value,
     pub agreement_id: String,
@@ -31,7 +31,6 @@ impl AgreementView {
 
     pub fn pointer_typed<'a, T: Deserialize<'a>>(&self, pointer: &str) -> Result<T, Error> {
         let value = self
-            .json
             .pointer(pointer)
             .ok_or(Error::NoKey(pointer.to_string()))?
             .clone();
@@ -64,8 +63,11 @@ impl AgreementView {
     }
 
     pub fn get_property<'a, T: Deserialize<'a>>(&self, property: &str) -> Result<T, Error> {
-        let pointer = format!("/{}", property.replace(".", "/"));
-        self.pointer_typed(pointer.as_str())
+        let pointers = property_to_pointer_paths(property);
+        match self.pointer_typed(&pointers.path_w_tag) {
+            Err(Error::NoKey(_)) => self.pointer_typed(&pointers.path),
+            result => result,
+        }
     }
 
     pub fn remove_property(&mut self, pointer: &str) -> Result<(), Error> {
@@ -80,6 +82,19 @@ impl AgreementView {
     }
 }
 
+struct PointerPaths {
+    /// Pointer path
+    path: String,
+    /// Pointer path ending with `PROPERTY_TAG`
+    path_w_tag: String,
+}
+
+fn property_to_pointer_paths(property: &str) -> PointerPaths {
+    let path = format!("/{}", property.replace(".", "/"));
+    let path_w_tag = format!("{path}/{PROPERTY_TAG}");
+    PointerPaths { path, path_w_tag }
+}
+
 pub fn parse_constraints(input: &str, reg_expr: &str, group: usize) -> Option<HashSet<String>> {
     let re = regex::Regex::new(reg_expr).unwrap();
     re.captures(input)
diff --git a/utils/cli/Cargo.toml b/utils/cli/Cargo.toml
index bfcd7d2a3b..6278eeb600 100644
--- a/utils/cli/Cargo.toml
+++ b/utils/cli/Cargo.toml
@@ -11,7 +11,7 @@ keywords = ["golem", "yagna"]
 
 [dependencies]
 anyhow = "1.0"
-prettytable-rs = "0.8"
+prettytable-rs = "0.9"
 serde = "1.0"
 serde_json = "1.0"
 serde_yaml = "0.9"
diff --git a/utils/manifest-utils/Cargo.toml b/utils/manifest-utils/Cargo.toml
index c593381b4e..467c44239d 100644
--- a/utils/manifest-utils/Cargo.toml
+++ b/utils/manifest-utils/Cargo.toml
@@ -11,12 +11,13 @@ keywords = ["golem", "yagna"]
 
 [dependencies]
 ya-agreement-utils = "0.4"
+ya-utils-path = "0.1"
 
+regex = "1.5"
 serde = "1.0"
 serde_json = "1.0"
 serde_yaml = "0.9"
 thiserror = "1.0"
-
 anyhow = { version = "1.0" }
 base64 = { version = "0.13" }
 chrono = { version = "0.4", features = ["serde"] }
@@ -28,6 +29,7 @@ strum = { version = "0.24", features = ["derive"] }
 url = {version = "2.2", features = ["serde"] }
 openssl = { version = "0.10", features = ["vendored"] }
 md-5 = "0.10"
+log = "0.4"
 
 [dev-dependencies]
 anyhow = "1.0"
@@ -35,3 +37,4 @@ serial_test = "0.9"
 shlex = "1.1"
 tar = "0.4"
 test-case = "2.2"
+ya-manifest-test-utils = "0.1"
diff --git a/utils/manifest-utils/src/lib.rs b/utils/manifest-utils/src/lib.rs
index 4eeec405ac..6c9a31558d 100644
--- a/utils/manifest-utils/src/lib.rs
+++ b/utils/manifest-utils/src/lib.rs
@@ -1,4 +1,5 @@
 pub mod manifest;
+pub mod matching;
 pub mod policy;
 pub mod util;
 
diff --git a/utils/manifest-utils/src/manifest.rs b/utils/manifest-utils/src/manifest.rs
index 7d5e06f5c1..7e9095a43b 100644
--- a/utils/manifest-utils/src/manifest.rs
+++ b/utils/manifest-utils/src/manifest.rs
@@ -7,7 +7,9 @@ use semver::Version;
 use serde::{Deserialize, Deserializer, Serialize};
 use serde_json::Value;
 use strum;
+use strum::AsRefStr;
 use strum::Display;
+use strum::EnumString;
 use url::Url;
 
 use ya_agreement_utils::AgreementView;
@@ -15,15 +17,14 @@ use ya_agreement_utils::Error as AgreementError;
 
 use crate::decode_data;
 
-pub const AGREEMENT_MANIFEST_PROPERTY: &str = "demand.properties.golem.srv.comp.payload.@tag";
-pub const AGREEMENT_MANIFEST_SIG_PROPERTY: &str = "demand.properties.golem.srv.comp.payload.sig";
-
 pub const CAPABILITIES_PROPERTY: &str = "golem.runtime.capabilities";
-pub const DEMAND_MANIFEST_PROPERTY: &str = "golem.srv.comp.payload.@tag";
-pub const DEMAND_MANIFEST_SIG_PROPERTY: &str = "golem.srv.comp.payload.sig.@tag";
+pub const DEMAND_MANIFEST_PROPERTY: &str = "golem.srv.comp.payload";
+pub const DEMAND_MANIFEST_SIG_PROPERTY: &str = "golem.srv.comp.payload.sig";
 pub const DEMAND_MANIFEST_SIG_ALGORITHM_PROPERTY: &str = "golem.srv.comp.payload.sig.algorithm";
 pub const DEMAND_MANIFEST_CERT_PROPERTY: &str = "golem.srv.comp.payload.cert";
 
+pub const AGREEMENT_MANIFEST_PROPERTY: &str = "demand.properties.golem.srv.comp.payload";
+
 #[derive(Debug, thiserror::Error)]
 pub enum Error {
     #[error("agreement error: {0}")]
@@ -226,10 +227,12 @@ impl<'de> Deserialize<'de> for Command {
     }
 }
 
-#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, EnumString, AsRefStr)]
 #[serde(rename_all = "camelCase")]
 pub enum ArgMatch {
+    #[strum(ascii_case_insensitive)]
     Strict,
+    #[strum(ascii_case_insensitive)]
     Regex,
 }
 
diff --git a/utils/manifest-utils/src/matching/domain.rs b/utils/manifest-utils/src/matching/domain.rs
new file mode 100644
index 0000000000..fb4c04acfa
--- /dev/null
+++ b/utils/manifest-utils/src/matching/domain.rs
@@ -0,0 +1,132 @@
+use std::{
+    collections::HashSet,
+    convert::TryFrom,
+    fs::OpenOptions,
+    io::BufReader,
+    path::Path,
+    sync::{Arc, Mutex, RwLock},
+};
+
+use regex::RegexSetBuilder;
+use serde::{Deserialize, Serialize};
+use ya_utils_path::SwapSave;
+
+use super::{CompositeMatcher, Matcher, RegexMatcher, StrictMatcher};
+use crate::{util::str_to_short_hash, ArgMatch};
+
+pub type DomainsMatcher = CompositeMatcher;
+pub type SharedDomainPatterns = Arc<Mutex<DomainPatterns>>;
+pub type SharedDomainMatchers = Arc<RwLock<DomainsMatcher>>;
+
+#[derive(Clone, Default, Debug)]
+pub struct DomainWhitelistState {
+    pub patterns: SharedDomainPatterns,
+    pub matchers: SharedDomainMatchers,
+}
+
+impl DomainWhitelistState {
+    /// Creates a new `DomainWhitelistState` with patterns matching generated from them matchers
+    pub fn try_new(patterns: DomainPatterns) -> Result<Self, anyhow::Error> {
+        let matcher = DomainsMatcher::try_from(&patterns)?;
+        let matcher = Arc::new(RwLock::new(matcher));
+        let patterns = Arc::new(Mutex::new(patterns));
+        Ok(Self {
+            patterns,
+            matchers: matcher,
+        })
+    }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+pub struct DomainPatterns {
+    pub patterns: Vec<DomainPattern>,
+}
+
+impl DomainPatterns {
+    pub fn load(path: &Path) -> anyhow::Result<Self> {
+        if path.exists() {
+            log::debug!("Loading domain patterns from: {}", path.display());
+            let patterns = OpenOptions::new().read(true).open(path)?;
+            let patterns = BufReader::new(patterns);
+            Ok(serde_json::from_reader(patterns)?)
+        } else {
+            Ok(Self::default())
+        }
+    }
+
+    pub fn load_or_create(path: &Path) -> anyhow::Result<Self> {
+        if path.exists() {
+            Self::load(path)
+        } else {
+            if let Some(parent) = path.parent() {
+                std::fs::create_dir_all(parent)?;
+            }
+            std::fs::File::create(&path)?;
+            let patterns = Self::default();
+            patterns.save(path)?;
+            Ok(patterns)
+        }
+    }
+
+    pub fn update_and_save(&mut self, path: &Path, patterns: DomainPatterns) -> anyhow::Result<()> {
+        self.patterns = patterns.patterns;
+        self.save(path)
+    }
+
+    pub fn save(&self, path: &Path) -> anyhow::Result<()> {
+        Ok(path.swap_save(serde_json::to_string_pretty(self)?)?)
+    }
+}
+
+#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DomainPattern {
+    pub domain: String,
+    #[serde(rename = "match", default = "DomainPattern::default_domain_match")]
+    pub domain_match: ArgMatch,
+}
+
+impl DomainPattern {
+    fn default_domain_match() -> ArgMatch {
+        ArgMatch::Regex
+    }
+}
+
+impl TryFrom<&DomainPatterns> for DomainsMatcher {
+    type Error = anyhow::Error;
+
+    fn try_from(domain_patterns: &DomainPatterns) -> Result<Self, Self::Error> {
+        let mut strict_patterns = HashSet::new();
+        let mut regex_patterns = HashSet::new();
+        for domain_pattern in &domain_patterns.patterns {
+            match domain_pattern.domain_match {
+                ArgMatch::Strict => strict_patterns.insert(domain_pattern.domain.to_lowercase()),
+                ArgMatch::Regex => regex_patterns.insert(domain_pattern.domain.to_lowercase()),
+            };
+        }
+        let mut matchers: Vec<Box<dyn Matcher>> = Vec::new();
+        if !strict_patterns.is_empty() {
+            let matcher = StrictMatcher {
+                values: strict_patterns,
+            };
+            matchers.push(Box::new(matcher));
+        }
+        if !regex_patterns.is_empty() {
+            let regex_patterns = regex_patterns.into_iter().collect::<Vec<String>>();
+            let regex_patterns = RegexSetBuilder::new(&regex_patterns)
+                .case_insensitive(true)
+                .ignore_whitespace(true)
+                .build()?;
+            let matcher = RegexMatcher {
+                patterns: regex_patterns,
+            };
+            matchers.push(Box::new(matcher));
+        }
+        Ok(CompositeMatcher { matchers })
+    }
+}
+
+pub fn pattern_to_id(pattern: &DomainPattern) -> String {
+    let pattern = &pattern.domain;
+    str_to_short_hash(pattern)
+}
diff --git a/utils/manifest-utils/src/matching/mod.rs b/utils/manifest-utils/src/matching/mod.rs
new file mode 100644
index 0000000000..bb161d5269
--- /dev/null
+++ b/utils/manifest-utils/src/matching/mod.rs
@@ -0,0 +1,42 @@
+pub mod domain;
+
+use std::{collections::HashSet, fmt::Debug};
+
+use regex::RegexSet;
+
+pub trait Matcher: Debug + Send + Sync {
+    fn matches(&self, txt: &str) -> bool;
+}
+
+#[derive(Debug)]
+struct RegexMatcher {
+    patterns: RegexSet,
+}
+
+impl Matcher for RegexMatcher {
+    fn matches(&self, txt: &str) -> bool {
+        self.patterns.matches(txt).matched_any()
+    }
+}
+
+#[derive(Debug)]
+struct StrictMatcher {
+    values: HashSet<String>,
+}
+
+impl Matcher for StrictMatcher {
+    fn matches(&self, txt: &str) -> bool {
+        self.values.contains(&txt.to_lowercase())
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct CompositeMatcher {
+    matchers: Vec<Box<dyn Matcher>>,
+}
+
+impl Matcher for CompositeMatcher {
+    fn matches(&self, txt: &str) -> bool {
+        self.matchers.iter().any(|matcher| matcher.matches(txt))
+    }
+}
diff --git a/utils/manifest-utils/src/policy.rs b/utils/manifest-utils/src/policy.rs
index 717b2a84a5..31524c7ba4 100644
--- a/utils/manifest-utils/src/policy.rs
+++ b/utils/manifest-utils/src/policy.rs
@@ -13,6 +13,7 @@ use openssl::x509::{X509ObjectRef, X509StoreContext, X509};
 use structopt::StructOpt;
 use strum::{Display, EnumIter, EnumString, EnumVariantNames, IntoEnumIterator, VariantNames};
 
+use crate::matching::domain::DomainWhitelistState;
 use crate::util::{CertBasicDataVisitor, X509Visitor};
 
 /// Policy configuration
@@ -39,6 +40,8 @@ pub struct PolicyConfig {
     pub policy_trust_property: Vec<(String, Match)>,
     #[structopt(skip)]
     pub trusted_keys: Option<Keystore>,
+    #[structopt(skip)]
+    pub domain_patterns: DomainWhitelistState,
 }
 
 impl PolicyConfig {
@@ -144,10 +147,8 @@ impl Keystore {
 
         let pkey = self.verify_cert(cert)?;
 
-        let msg_digest = MessageDigest::from_name(sig_alg.as_ref()).ok_or(anyhow::anyhow!(
-            "Unknown signature algorithm: {}",
-            sig_alg.as_ref()
-        ))?;
+        let msg_digest = MessageDigest::from_name(sig_alg.as_ref())
+            .ok_or_else(|| anyhow::anyhow!("Unknown signature algorithm: {}", sig_alg.as_ref()))?;
         let mut verifier = Verifier::new(msg_digest, pkey.as_ref())?;
         if !(verifier.verify_oneshot(&sig, data.as_ref().as_bytes())?) {
             return Err(anyhow::anyhow!("Invalid signature"));
diff --git a/utils/manifest-utils/src/util.rs b/utils/manifest-utils/src/util.rs
index 102bace60b..280619322b 100644
--- a/utils/manifest-utils/src/util.rs
+++ b/utils/manifest-utils/src/util.rs
@@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
 use std::convert::TryFrom;
 use std::ffi::OsStr;
 use std::fs::{self, DirEntry};
+use std::path::Path;
 use std::{fs::File, path::PathBuf};
 
 use md5::{Digest, Md5};
@@ -45,15 +46,15 @@ pub fn parse_cert_file(cert: &PathBuf) -> anyhow::Result<Vec<X509>> {
     }
 }
 
-fn get_file_extension(path: &PathBuf) -> Option<String> {
+fn get_file_extension(path: &Path) -> Option<String> {
     path.extension().map(os_str_to_string)
 }
 
-fn get_file_name(path: &PathBuf) -> Option<String> {
+fn get_file_name(path: &Path) -> Option<String> {
     path.file_name().map(os_str_to_string)
 }
 
-fn get_file_stem(path: &PathBuf) -> Option<String> {
+fn get_file_stem(path: &Path) -> Option<String> {
     path.file_stem().map(os_str_to_string)
 }
 
@@ -103,8 +104,7 @@ fn cert_subject_entries(cert: &X509Ref, nid: Nid) -> Option<String> {
 
 pub fn cert_to_id(cert: &X509Ref) -> anyhow::Result<String> {
     let txt = cert.to_text()?;
-    let digest = Md5::digest(&txt);
-    Ok(format!("{digest:x}"))
+    Ok(str_to_short_hash(&txt))
 }
 
 pub fn visit_certificates<T: CertBasicDataVisitor>(
@@ -269,7 +269,7 @@ impl KeystoreManager {
             let file_stem = get_file_stem(&new_cert_path).expect("Has to have stem");
             let dot_extension = get_file_extension(&new_cert_path)
                 .map(|ex| format!(".{ex}"))
-                .unwrap_or(String::from(""));
+                .unwrap_or_else(|| String::from(""));
             for i in 0..u32::MAX {
                 let numbered_filename = format!("{file_stem}.{i}{dot_extension}");
                 new_cert_path = self.cert_dir.clone();
@@ -287,16 +287,12 @@ impl KeystoreManager {
     }
 
     /// Loads certificates as individual files to `cert_dir`
-    fn load_as_certificate_files(
-        &self,
-        cert_path: &PathBuf,
-        certs: Vec<X509>,
-    ) -> anyhow::Result<()> {
+    fn load_as_certificate_files(&self, cert_path: &Path, certs: Vec<X509>) -> anyhow::Result<()> {
         let file_stem = get_file_stem(cert_path)
             .ok_or_else(|| anyhow::anyhow!("Cannot get file name stem."))?;
         let dot_extension = get_file_extension(cert_path)
             .map(|ex| format!(".{ex}"))
-            .unwrap_or(String::from(""));
+            .unwrap_or_else(|| String::from(""));
         for cert in certs.into_iter() {
             let id = cert_to_id(&cert)?;
             let mut new_cert_path = self.cert_dir.clone();
@@ -323,6 +319,14 @@ pub enum KeystoreRemoveResult {
     Removed { removed: Vec<X509> },
 }
 
+/// Calculates Md5 of `txt` and returns first 8 characters.
+pub fn str_to_short_hash(txt: impl AsRef<[u8]>) -> String {
+    let digest = Md5::digest(txt);
+    let digest = format!("{digest:x}");
+    let short_hash = &digest[..8]; // Md5 is 32 characters
+    short_hash.to_string()
+}
+
 #[cfg(test)]
 pub mod tests {
     use super::*;


Test patch:
diff --git a/agent/provider/tests/manifest_negotiator.rs b/agent/provider/tests/manifest_negotiator.rs
new file mode 100644
index 0000000000..1e5e80e900
--- /dev/null
+++ b/agent/provider/tests/manifest_negotiator.rs
@@ -0,0 +1,348 @@
+#[macro_use]
+extern crate serial_test;
+
+use std::collections::HashMap;
+use std::fs;
+use std::path::PathBuf;
+
+use serde_json::{json, Value};
+use test_case::test_case;
+use ya_agreement_utils::AgreementView;
+use ya_manifest_test_utils::{load_certificates_from_dir, TestResources};
+use ya_manifest_utils::matching::domain::{DomainPatterns, DomainWhitelistState};
+use ya_manifest_utils::{Keystore, Policy, PolicyConfig};
+use ya_provider::market::negotiator::builtin::ManifestSignature;
+use ya_provider::market::negotiator::*;
+
+static MANIFEST_TEST_RESOURCES: TestResources = TestResources {
+    temp_dir: env!("CARGO_TARGET_TMPDIR"),
+};
+
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    None, // private key file
+    None, // sig alg
+    None, // cert
+    None; // error msg
+    "Manifest without singature accepted because domain whitelisted"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "do.*ain.com", "type": "regex" }, { "domain": "another.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    None, // private key file
+    None, // sig alg
+    None, // cert
+    None; // error msg
+    "Manifest without singature accepted because domain whitelisted (regex pattern)"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "different_domain.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    None, // private key file
+    None, // sig alg
+    None, // cert
+    Some("manifest requires signature but it has none"); // error msg
+    "Manifest without singature rejected because domain NOT whitelisted"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "regex" }, { "domain": "another.whitelisted.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com", "https://not.whitelisted.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    None, // private key file
+    None, // sig alg
+    None, // cert
+    Some("manifest requires signature but it has none"); // error msg
+    "Manifest without singature rejected because ONE of domains NOT whitelisted"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "regex" }] }"#, // data_dir/domain_whitelist.json
+    r#"[]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    None, // private key file
+    None, // sig alg
+    None, // cert
+    None; // error msg
+    "Manifest accepted because its urls list is empty"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "regex" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    Some("foo_req.key.pem"), // private key file
+    Some("sha256"), // sig alg
+    Some("foo_req.cert.pem"), // cert
+    None; // error msg
+    "Manifest accepted with url NOT whitelisted because signature valid"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    Some("foo_req.key.pem"), // private key file
+    Some("sha256"), // sig alg
+    Some("dummy_inter.cert.pem"), // untrusted cert
+    Some("failed to verify manifest signature: Invalid certificate"); // error msg
+    "Manifest rejected because of invalid certificate even when urls domains are whitelisted"
+)]
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    Some("foo_inter.key.pem"), // private key file not matching certificate
+    Some("sha256"), // sig alg
+    Some("foo_req.cert.pem"), // certificate not matching private key
+    Some("failed to verify manifest signature: Invalid signature"); // error msg
+    "Manifest rejected because of invalid signature (signed using incorrect private key) even when urls domains are whitelisted"
+)]
+#[serial]
+fn manifest_negotiator_test(
+    whitelist: &str,
+    urls: &str,
+    offer: &str,
+    signing_key: Option<&str>,
+    signature_alg: Option<&str>,
+    cert: Option<&str>,
+    error_msg: Option<&str>,
+) {
+    let comp_manifest_b64 = create_comp_manifest_b64(urls);
+
+    let signature_b64 = match signing_key {
+        Some(signing_key) => {
+            Some(MANIFEST_TEST_RESOURCES.sign_data(comp_manifest_b64.as_bytes(), signing_key))
+        }
+        None => None,
+    };
+
+    let cert_b64 = match cert {
+        Some(cert) => Some(cert_file_to_cert_b64(cert)),
+        None => None,
+    };
+
+    manifest_negotiator_test_encoded_manifest_sign_and_cert(
+        whitelist,
+        comp_manifest_b64,
+        offer,
+        signature_b64,
+        signature_alg,
+        cert_b64,
+        error_msg,
+    )
+}
+
+#[test_case(
+    r#"{ "patterns": [{ "domain": "domain.com", "type": "strict" }] }"#, // data_dir/domain_whitelist.json
+    r#"["https://domain.com"]"#, // compManifest.net.inet.out.urls
+    r#"{ "any": "thing" }"#, // offer
+    Some("broken_signature"), // signature (broken)
+    Some("sha256"), // sig alg
+    Some("foo_req.cert.pem"), // cert
+    Some("failed to verify manifest signature: Invalid signature"); // error msg
+    "Manifest rejected because of invalid signature"
+)]
+#[serial]
+fn manifest_negotiator_test_encoded_sign_and_cert(
+    whitelist: &str,
+    urls: &str,
+    offer: &str,
+    signature_b64: Option<&str>,
+    signature_alg: Option<&str>,
+    cert: Option<&str>,
+    error_msg: Option<&str>,
+) {
+    let comp_manifest_b64 = create_comp_manifest_b64(urls);
+    let signature_b64 = match signature_b64 {
+        Some(signature) => Some(signature.to_string()),
+        None => None,
+    };
+
+    let cert_b64 = match cert {
+        Some(cert) => Some(cert_file_to_cert_b64(cert)),
+        None => None,
+    };
+    manifest_negotiator_test_encoded_manifest_sign_and_cert(
+        whitelist,
+        comp_manifest_b64,
+        offer,
+        signature_b64,
+        signature_alg,
+        cert_b64,
+        error_msg,
+    )
+}
+
+fn manifest_negotiator_test_encoded_manifest_sign_and_cert(
+    whitelist: &str,
+    comp_manifest_b64: String,
+    offer: &str,
+    signature_b64: Option<String>,
+    signature_alg: Option<&str>,
+    cert_b64: Option<String>,
+    error_msg: Option<&str>,
+) {
+    // Having
+    let whitelist_state = create_whitelist(whitelist);
+    let (resource_cert_dir, test_cert_dir) = MANIFEST_TEST_RESOURCES.init_cert_dirs();
+
+    if signature_b64.is_some() {
+        load_certificates_from_dir(
+            &resource_cert_dir,
+            &test_cert_dir,
+            &["foo_ca-chain.cert.pem"],
+        );
+    }
+    let keystore = Keystore::load(&test_cert_dir).expect("Can load test certificates");
+
+    let mut config = create_manifest_signature_validating_policy_config();
+    config.domain_patterns = whitelist_state;
+    config.trusted_keys = Some(keystore);
+    let mut manifest_negotiator = ManifestSignature::from(config);
+
+    let demand = create_demand_json(&comp_manifest_b64, signature_b64, signature_alg, cert_b64);
+    let demand: Value = serde_json::from_str(&demand).unwrap();
+    let demand = AgreementView {
+        json: demand,
+        agreement_id: "id".to_string(),
+    };
+    let offer: Value = serde_json::from_str(offer).unwrap();
+    let offer = AgreementView {
+        json: offer,
+        agreement_id: "id".to_string(),
+    };
+
+    // When
+    let negotiation_result = manifest_negotiator.negotiate_step(&demand, offer.clone());
+
+    // Then
+    let negotiation_result = negotiation_result.expect("Negotiator had not failed");
+    if let Some(message) = error_msg {
+        assert_eq!(
+            negotiation_result,
+            NegotiationResult::Reject {
+                message: message.to_string(),
+                is_final: true
+            }
+        );
+    } else {
+        assert_eq!(negotiation_result, NegotiationResult::Ready { offer });
+    }
+}
+
+fn cert_file_to_cert_b64(cert_file: &str) -> String {
+    let (resource_cert_dir, _) = MANIFEST_TEST_RESOURCES.init_cert_dirs();
+    let mut cert_path = resource_cert_dir;
+    cert_path.push(cert_file);
+    println!("{cert_path:?}");
+    let cert = fs::read(cert_path).expect("Can read cert from resources");
+    base64::encode(cert)
+}
+
+fn create_comp_manifest_b64(urls: &str) -> String {
+    let manifest_template = r#"{
+        "version": "0.1.0",
+        "createdAt": "2022-09-07T02:57:00.000000Z",
+        "expiresAt": "2100-01-01T00:01:00.000000Z",
+        "metadata": { "name": "App", "version": "0.1.0" },
+        "payload": [],
+        "compManifest": {
+            "version": "0.1.0",
+            "script": { "commands": [], "match": "regex" },
+            "net": {
+                "inet": {
+                    "out": {
+                        "protocols": ["https"],
+                        "urls": __URLS__
+                    }
+                }
+            }
+        }
+    }"#;
+    let manifest = manifest_template.replace("__URLS__", urls);
+    base64::encode(manifest)
+}
+
+fn create_demand_json(
+    comp_manifest_b64: &str,
+    signature_b64: Option<String>,
+    signature_alg_b64: Option<&str>,
+    cert_b64: Option<String>,
+) -> String {
+    let mut payload = HashMap::new();
+    payload.insert("@tag", json!(comp_manifest_b64));
+    if signature_b64.is_some() && signature_alg_b64.is_some() {
+        payload.insert(
+            "sig",
+            json!({
+                "@tag": signature_b64.unwrap().to_string(),
+                "algorithm": signature_alg_b64.unwrap().to_string()
+            }),
+        );
+    } else if signature_b64.is_some() {
+        payload.insert("sig", json!(signature_b64.unwrap().to_string()));
+    } else if signature_alg_b64.is_some() {
+        payload.insert(
+            "sig",
+            json!({ "algorithm": signature_alg_b64.unwrap().to_string() }),
+        );
+    }
+    if let Some(cert_b64) = cert_b64 {
+        payload.insert("cert", json!(cert_b64.to_string()));
+    }
+    // let mut payload = manifest.to_string();
+    let manifest = json!({
+        "golem": {
+            "srv": {
+                "comp": {
+                    "payload": payload
+                }
+            }
+        },
+    });
+    let demand = serde_json::to_string_pretty(&manifest).unwrap();
+    println!("Tested demand:\n{demand}");
+    demand
+}
+
+fn create_manifest_signature_validating_policy_config() -> PolicyConfig {
+    let mut config = PolicyConfig::default();
+    config
+        .policy_disable_component
+        .push(Policy::ManifestCompliance);
+    config
+        .policy_disable_component
+        .push(Policy::ManifestInetUrlCompliance);
+    config
+        .policy_disable_component
+        .push(Policy::ManifestScriptCompliance);
+    config
+}
+
+fn create_whitelist(whitelist_json: &str) -> DomainWhitelistState {
+    let whitelist = create_whitelist_file(whitelist_json);
+    let whitelist_patterns =
+        DomainPatterns::load(&whitelist).expect("Can deserialize whitelist patterns");
+    DomainWhitelistState::try_new(whitelist_patterns)
+        .expect("Can create whitelist state from patterns")
+}
+
+fn create_whitelist_file(whitelist_json: &str) -> PathBuf {
+    let whitelist_file = whitelist_file();
+    if whitelist_file.exists() {
+        fs::remove_file(&whitelist_file).expect("Can delete whitelist file");
+    }
+    fs::write(whitelist_file.as_path(), whitelist_json).expect("Can write whitelist file");
+    whitelist_file
+}
+
+fn whitelist_file() -> PathBuf {
+    tmp_resource("whitelist.json")
+}
+
+fn tmp_resource(name: &str) -> PathBuf {
+    let mut resource = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
+    resource.push(name);
+    resource
+}
diff --git a/agent/provider/tests/whitelist_cli.rs b/agent/provider/tests/whitelist_cli.rs
new file mode 100644
index 0000000000..7e7c67d6de
--- /dev/null
+++ b/agent/provider/tests/whitelist_cli.rs
@@ -0,0 +1,176 @@
+#[macro_use]
+extern crate serial_test;
+
+use std::fs;
+use std::path::PathBuf;
+
+use assert_cmd::{assert::Assert, Command};
+use serde_json::Value;
+use test_case::test_case;
+
+#[serial]
+#[test]
+fn empty_whitelist_json() {
+    clean_data_dir();
+    Command::cargo_bin("ya-provider")
+        .unwrap()
+        .arg("whitelist")
+        .arg("list")
+        .arg("--json")
+        .args(data_dir_args())
+        .assert()
+        .stdout("[]\n")
+        .success();
+}
+
+#[test_case(
+    &["domain.com"],  
+    "strict",
+    r#"[{ "ID": "5ce448a6", "Pattern": "domain.com", "Type": "strict" }]"#;
+    "Adding one pattern"
+)]
+#[test_case(
+    &["dom.*\\.com", "myapp.com", "other\\.*"],  
+    "regex",
+    r#"[
+        { "ID": "979b6b99", "Pattern": "dom.*\\.com", "Type": "regex" },
+        { "ID": "89dcf5f6", "Pattern": "myapp.com", "Type": "regex" },
+        { "ID": "c31deaea", "Pattern": "other\\.*", "Type": "regex" }
+    ]"#;
+    "Adding multiple patterns"
+)]
+#[test_case(
+    &["domain.com", "domain.com"],  
+    "strict",
+    r#"[
+        { "ID": "5ce448a6", "Pattern": "domain.com", "Type": "strict" }
+    ]"#;
+    "Adding duplicated patterns results in one result"
+)]
+#[serial]
+fn whitelist_add_test(add: &[&str], pattern_type: &str, expected_add_json_out: &str) {
+    clean_data_dir();
+    let expected_add_json_out = json_to_printed_output(expected_add_json_out);
+    whitelist_add(add, pattern_type)
+        .stdout(expected_add_json_out.to_string())
+        .success();
+}
+
+#[test_case(
+    &["domain.com"],  
+    "strict",
+    &["5ce448a6"],
+    r#"[{ "ID": "5ce448a6", "Pattern": "domain.com", "Type": "strict" }]"#,
+    "[]";
+    "Adding one pattern, removing it, empty list."
+)]
+#[test_case(
+    &["domain.com"],  
+    "strict",
+    &["5ce448a6", "5ce448a6"],
+    r#"[{ "ID": "5ce448a6", "Pattern": "domain.com", "Type": "strict" }]"#,
+    "[]";
+    "Adding one pattern, removing with duplicated ids, empty list."
+)]
+#[test_case(
+    &["domain.com", "another.com"],  
+    "strict",
+    &["5ce448a6"],
+    r#"[{ "ID": "5ce448a6", "Pattern": "domain.com", "Type": "strict" }]"#,
+    r#"[ { "ID": "ee0cc088",  "Pattern": "another.com",  "Type": "strict" }]"#;
+    "Adding two patterns, removing one, one on the list."
+)]
+#[test_case(
+    &["domain.com"],  
+    "strict",
+    &["no_such_id"],
+    "[]",
+    r#"[{ "ID": "5ce448a6", "Pattern": "domain.com", "Type": "strict" }]"#;
+    "Adding one pattern, removing not existing, one on the list."
+)]
+#[test_case(
+    &[],
+    "",
+    &["no_such_id", "nope"],
+    "[]",
+    "[]";
+    "Removing on empty list. List on empty produces empty list."
+)]
+#[serial]
+fn whitelist_remove_test(
+    add: &[&str],
+    pattern_type: &str,
+    remove_ids: &[&str],
+    remove_out: &str,
+    list_out: &str,
+) {
+    clean_data_dir();
+    if !add.is_empty() {
+        whitelist_add(add, pattern_type).success();
+    }
+    let remove_out = json_to_printed_output(remove_out);
+    whitelist_remove(remove_ids).stdout(remove_out).success();
+    let list_out = json_to_printed_output(list_out);
+    whitelist_list().stdout(list_out).success();
+}
+
+// Whitelist test utils
+
+fn whitelist_add(add: &[&str], pattern_type: &str) -> Assert {
+    let mut cmd = Command::cargo_bin("ya-provider").unwrap();
+    cmd.arg("whitelist")
+        .arg("add")
+        .arg("-p")
+        .args(add)
+        .arg("-t")
+        .arg(pattern_type.to_string())
+        .arg("--json")
+        .args(data_dir_args())
+        .assert()
+}
+
+fn whitelist_remove(ids: &[&str]) -> Assert {
+    let mut cmd = Command::cargo_bin("ya-provider").unwrap();
+    cmd.arg("whitelist")
+        .arg("remove")
+        .args(ids)
+        .arg("--json")
+        .args(data_dir_args())
+        .assert()
+}
+
+fn whitelist_list() -> Assert {
+    let mut cmd = Command::cargo_bin("ya-provider").unwrap();
+    cmd.arg("whitelist")
+        .arg("list")
+        .arg("--json")
+        .args(data_dir_args())
+        .assert()
+}
+
+fn json_to_printed_output(out: &str) -> String {
+    let out: Value = serde_json::from_str(&out).unwrap();
+    let out = serde_json::to_string_pretty(&out).unwrap();
+    format!("{out}\n")
+}
+
+fn clean_data_dir() {
+    let data_dir = data_dir_path();
+    if data_dir.exists() {
+        fs::remove_dir_all(&data_dir).expect("Can delete data dir");
+    }
+    // Without creating dir eprintln breaks tests https://github.com/golemfactory/yagna/blob/master/utils/path/src/data_dir.rs#L23
+    fs::create_dir(data_dir).unwrap();
+}
+
+fn data_dir_args() -> [String; 2] {
+    let data_dir = data_dir_path();
+    let data_dir = data_dir.to_str().unwrap().to_string();
+    ["--data-dir".to_string(), data_dir]
+}
+
+fn data_dir_path() -> PathBuf {
+    let mut data_dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
+    data_dir.push("data_dir");
+    data_dir
+}
diff --git a/utils/manifest-utils/test-utils/Cargo.toml b/utils/manifest-utils/test-utils/Cargo.toml
new file mode 100644
index 0000000000..320748fd80
--- /dev/null
+++ b/utils/manifest-utils/test-utils/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "ya-manifest-test-utils"
+version = "0.1.0"
+authors = ["Golem Factory <contact@golem.network>"]
+edition = "2018"
+homepage = "https://github.com/golemfactory/yagna"
+repository = "https://github.com/golemfactory/yagna"
+license = "LGPL-3.0"
+description = "Yagna manifest INTERNAL TEST utils"
+
+[dependencies]
+base64 = "0.13"
+tar = "0.4"
+openssl = "0.10"
+
+ya-manifest-utils = "0.1"
+ya-agreement-utils = "0.4"
\ No newline at end of file
diff --git a/utils/manifest-utils/resources/test/certificates.tar b/utils/manifest-utils/test-utils/resources/test/certificates.tar
similarity index 64%
rename from utils/manifest-utils/resources/test/certificates.tar
rename to utils/manifest-utils/test-utils/resources/test/certificates.tar
index 19cb33efc3..3e74f34421 100644
Binary files a/utils/manifest-utils/resources/test/certificates.tar and b/utils/manifest-utils/test-utils/resources/test/certificates.tar differ
diff --git a/utils/manifest-utils/resources/test/data.json.base64 b/utils/manifest-utils/test-utils/resources/test/data.json.base64
similarity index 100%
rename from utils/manifest-utils/resources/test/data.json.base64
rename to utils/manifest-utils/test-utils/resources/test/data.json.base64
diff --git a/utils/manifest-utils/resources/test/data.json.base64.foo_req_sign.sha256.base64 b/utils/manifest-utils/test-utils/resources/test/data.json.base64.foo_req_sign.sha256.base64
similarity index 100%
rename from utils/manifest-utils/resources/test/data.json.base64.foo_req_sign.sha256.base64
rename to utils/manifest-utils/test-utils/resources/test/data.json.base64.foo_req_sign.sha256.base64
diff --git a/utils/manifest-utils/resources/test/data.json.base64.unknown_req_sign.sha256.base64 b/utils/manifest-utils/test-utils/resources/test/data.json.base64.unknown_req_sign.sha256.base64
similarity index 100%
rename from utils/manifest-utils/resources/test/data.json.base64.unknown_req_sign.sha256.base64
rename to utils/manifest-utils/test-utils/resources/test/data.json.base64.unknown_req_sign.sha256.base64
diff --git a/utils/manifest-utils/test-utils/src/lib.rs b/utils/manifest-utils/test-utils/src/lib.rs
new file mode 100644
index 0000000000..e549a39646
--- /dev/null
+++ b/utils/manifest-utils/test-utils/src/lib.rs
@@ -0,0 +1,164 @@
+use std::fs::{self, File};
+use std::str;
+use std::sync::Once;
+use std::{collections::HashSet, path::PathBuf};
+
+use openssl::hash::MessageDigest;
+use openssl::pkey::PKey;
+use openssl::rsa::Rsa;
+use openssl::sign::Signer;
+use tar::Archive;
+
+use ya_manifest_utils::{
+    util::{self, CertBasicData, CertBasicDataVisitor},
+    KeystoreLoadResult, KeystoreRemoveResult,
+};
+
+static INIT: Once = Once::new();
+
+#[allow(clippy::ptr_arg)]
+pub fn load_certificates_from_dir(
+    resource_cert_dir: &PathBuf,
+    test_cert_dir: &PathBuf,
+    certfile_names: &[&str],
+) -> KeystoreLoadResult {
+    let cert_paths: Vec<PathBuf> = certfile_names
+        .iter()
+        .map(|certfile_name| {
+            let mut cert_path = resource_cert_dir.clone();
+            cert_path.push(certfile_name);
+            cert_path
+        })
+        .collect();
+    let keystore_manager =
+        util::KeystoreManager::try_new(test_cert_dir).expect("Can create keystore manager");
+    keystore_manager
+        .load_certs(&cert_paths)
+        .expect("Can load certificates")
+}
+
+pub fn remove_certificates(test_cert_dir: &PathBuf, cert_ids: &[&str]) -> KeystoreRemoveResult {
+    let keystore_manager =
+        util::KeystoreManager::try_new(test_cert_dir).expect("Can create keystore manager");
+    keystore_manager
+        .remove_certs(&slice_to_set(cert_ids))
+        .expect("Can load certificates")
+}
+
+#[derive(Default)]
+pub struct TestCertDataVisitor {
+    expected: HashSet<String>,
+    actual: HashSet<String>,
+}
+
+impl TestCertDataVisitor {
+    #[allow(dead_code)]
+    pub fn new(expected: &[&str]) -> Self {
+        Self {
+            expected: expected.iter().map(|s| s.to_string()).collect(),
+            ..Default::default()
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn test(&self) {
+        assert_eq!(self.expected, self.actual)
+    }
+}
+
+impl CertBasicDataVisitor for TestCertDataVisitor {
+    fn accept(&mut self, cert_data: CertBasicData) {
+        self.actual.insert(cert_data.id);
+    }
+}
+
+pub struct TestResources {
+    pub temp_dir: &'static str,
+}
+
+impl TestResources {
+    /// Creates new empty cert directory and resources directory with unpacked certificates.
+    pub fn init_cert_dirs(&self) -> (PathBuf, PathBuf) {
+        let resource_cert_dir = self.resource_cert_dir_path();
+        INIT.call_once(|| {
+            if resource_cert_dir.exists() {
+                fs::remove_dir_all(&resource_cert_dir).expect("Can delete test cert resources dir");
+            }
+            fs::create_dir_all(&resource_cert_dir).expect("Can create temp dir");
+            self.unpack_cert_resources(&resource_cert_dir);
+        });
+        let store_cert_dir = self.store_cert_dir_path();
+        if store_cert_dir.exists() {
+            // we want to clean store cert dir before every test
+            fs::remove_dir_all(&store_cert_dir).expect("Can delete test temp dir");
+        }
+        fs::create_dir_all(&store_cert_dir).expect("Can create temp dir");
+        (resource_cert_dir, store_cert_dir)
+    }
+
+    pub fn loaded_cert_files(&self) -> HashSet<String> {
+        let store_cert_dir = self.store_cert_dir_path();
+        let cert_dir = fs::read_dir(store_cert_dir).expect("Can read cert dir");
+        cert_dir
+            .into_iter()
+            .map(|file| file.expect("Can list cert files"))
+            .map(|x| x.file_name().to_string_lossy().to_string())
+            .collect()
+    }
+
+    // Signs given `data_b64` using `signing_key` (filename) and returns base64 encoded signature.
+    pub fn sign_data(&self, data_b64: &[u8], signing_key: &str) -> String {
+        let mut signing_key_path = self.resource_cert_dir_path();
+        signing_key_path.push(signing_key);
+        let signing_key = fs::read(signing_key_path).expect("Can read signing key");
+        let mut password = self.resource_cert_dir_path();
+        password.push("pass.txt");
+        let password = fs::read(password).expect("Can read password file");
+        let password = str::from_utf8(&password).unwrap().trim(); // just in case it got newline at the end
+        let keypair = Rsa::private_key_from_pem_passphrase(&signing_key, password.as_bytes())
+            .expect("Can parse signing key");
+        let keypair = PKey::from_rsa(keypair).unwrap();
+        let mut signer = Signer::new(MessageDigest::sha256(), &keypair).unwrap();
+        signer.update(data_b64).unwrap();
+        let signature = signer.sign_to_vec().expect("Can sign manifest");
+        base64::encode(signature)
+    }
+
+    fn unpack_cert_resources(&self, cert_resources_dir: &PathBuf) {
+        let mut cert_archive = self.test_resources_dir_path();
+        cert_archive.push("certificates.tar");
+        let cert_archive = File::open(cert_archive).expect("Can open cert archive file");
+        let mut cert_archive = Archive::new(cert_archive);
+        cert_archive
+            .unpack(cert_resources_dir)
+            .expect("Can unack cert archive");
+    }
+
+    pub fn test_resources_dir_path(&self) -> PathBuf {
+        let mut test_resources = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+        test_resources.push("resources/test");
+        test_resources
+    }
+
+    fn resource_cert_dir_path(&self) -> PathBuf {
+        let mut cert_resources = self.temp_dir_path();
+        cert_resources.push("cert_resources");
+        cert_resources
+    }
+
+    fn store_cert_dir_path(&self) -> PathBuf {
+        let mut cert_dir = self.temp_dir_path();
+        cert_dir.push("cert_dir");
+        cert_dir
+    }
+
+    fn temp_dir_path(&self) -> PathBuf {
+        PathBuf::from(self.temp_dir)
+    }
+}
+
+pub fn slice_to_set<T: AsRef<str>>(v: &[T]) -> HashSet<String> {
+    v.iter()
+        .map(|s| s.as_ref().to_string())
+        .collect::<HashSet<String>>()
+}
diff --git a/utils/manifest-utils/tests/certificate_store.rs b/utils/manifest-utils/tests/certificate_store.rs
index 9e4ca7cbbe..58e716036a 100644
--- a/utils/manifest-utils/tests/certificate_store.rs
+++ b/utils/manifest-utils/tests/certificate_store.rs
@@ -1,16 +1,17 @@
-pub mod common;
-
 #[macro_use]
 extern crate serial_test;
 
 use std::fs;
 
-use common::*;
-
 use test_case::test_case;
 
+use ya_manifest_test_utils::*;
 use ya_manifest_utils::util::visit_certificates;
 
+static TEST_RESOURCES: TestResources = TestResources {
+    temp_dir: env!("CARGO_TARGET_TMPDIR"),
+};
+
 /// Test utilities
 #[test_case(
     &[],
@@ -22,13 +23,13 @@ use ya_manifest_utils::util::visit_certificates;
 #[test_case(
     &["foo_ca.cert.pem"],
     &[],
-    &["c128af8c6d0ba34d940582c01443911d"],
+    &["c128af8c"],
     &["foo_ca.cert.pem"]; 
     "Can load one certificate"
 )]
 #[test_case(
     &["foo_ca.cert.pem", "foo_inter.cert.pem"],
-    &["c128af8c6d0ba34d940582c01443911d", "4e0df976b534cb73794a7613b31af51c"],
+    &["c128af8c", "4e0df976"],
     &[],
     &[];
     "Can remove all certificates"
@@ -36,28 +37,28 @@ use ya_manifest_utils::util::visit_certificates;
 #[test_case(
     &["foo_ca-chain.cert.pem"],
     &[],
-    &["4e0df976b534cb73794a7613b31af51c", "c128af8c6d0ba34d940582c01443911d"],
+    &["4e0df976", "c128af8c"],
     &["foo_ca-chain.cert.pem"]; 
     "Load keychain loads two certificates and stores them in received form (single keychain file)"
 )]
 #[test_case(
     &["foo_ca-chain.cert.pem"],
-    &["c128af8c6d0ba34d940582c01443911d"],
-    &["4e0df976b534cb73794a7613b31af51c"],
-    &["foo_ca-chain.cert.4e0df976b534cb73794a7613b31af51c.pem"]; 
+    &["c128af8c"],
+    &["4e0df976"],
+    &["foo_ca-chain.cert.4e0df976.pem"]; 
     "Load keychain and remove root CA results with intermediate cert and cert file with id in the name"
 )]
 #[test_case(
     &["foo_ca-chain.cert.pem"], 
-    &["4e0df976b534cb73794a7613b31af51c"], 
-    &["c128af8c6d0ba34d940582c01443911d"], 
-    &["foo_ca-chain.cert.c128af8c6d0ba34d940582c01443911d.pem"]; 
+    &["4e0df976"], 
+    &["c128af8c"], 
+    &["foo_ca-chain.cert.c128af8c.pem"]; 
     "Load keychain and remove intermediate cert results with root CA and cert file with id in the name"
 )]
 #[test_case(
     &["foo_ca.cert.pem", "foo_ca.cert.pem", "foo_ca.cert.pem"], 
     &[],
-    &["c128af8c6d0ba34d940582c01443911d"],
+    &["c128af8c"],
     &["foo_ca.cert.pem"]; 
     "Adding duplicates results in a single certificate in the store"
 )]
@@ -69,16 +70,16 @@ fn certificate_store_test(
     expected_files: &[&str],
 ) {
     // Having
-    let (resource_cert_dir, test_cert_dir) = init_cert_dirs();
-    load_certificates(&resource_cert_dir, &test_cert_dir, certs_to_add);
+    let (resource_cert_dir, test_cert_dir) = TEST_RESOURCES.init_cert_dirs();
+    load_certificates_from_dir(&resource_cert_dir, &test_cert_dir, certs_to_add);
     remove_certificates(&test_cert_dir, ids_to_remove);
     let mut visitor = TestCertDataVisitor::new(expected_ids);
     // When
     visitor = visit_certificates(&test_cert_dir, visitor).expect("Can visit loaded certificates");
     // Then
     visitor.test();
-    let certs = loaded_cert_files();
-    assert_eq!(certs, to_set(expected_files));
+    let certs = TEST_RESOURCES.loaded_cert_files();
+    assert_eq!(certs, slice_to_set(expected_files));
 }
 
 /// Name collision should be resolved
@@ -86,7 +87,7 @@ fn certificate_store_test(
 #[serial]
 fn certificate_name_collision_test() {
     // Having
-    let (resource_cert_dir, test_cert_dir) = init_cert_dirs();
+    let (resource_cert_dir, test_cert_dir) = TEST_RESOURCES.init_cert_dirs();
     let colliding_name = "foo_inter.cert.pem";
 
     let mut colliding_file_1 = resource_cert_dir.clone();
@@ -100,22 +101,19 @@ fn certificate_name_collision_test() {
     other.push("foo_req.cert.pem");
     fs::copy(other, colliding_file_2).expect("Can copy file");
 
-    load_certificates(
+    load_certificates_from_dir(
         &resource_cert_dir,
         &test_cert_dir,
         &[colliding_name, &format!("copy/{colliding_name}")],
     );
-    let mut visitor = TestCertDataVisitor::new(&[
-        "4e0df976b534cb73794a7613b31af51c",
-        "0e136cb330db24e5630e64ad46489557",
-    ]);
+    let mut visitor = TestCertDataVisitor::new(&["4e0df976", "0e136cb3"]);
     // When
     visitor = visit_certificates(&test_cert_dir, visitor).expect("Can visit loaded certificates");
     // Then
     visitor.test();
-    let certs = loaded_cert_files();
+    let certs = TEST_RESOURCES.loaded_cert_files();
     assert_eq!(
         certs,
-        to_set(&["foo_inter.cert.pem", "foo_inter.cert.0.pem"])
+        slice_to_set(&["foo_inter.cert.pem", "foo_inter.cert.0.pem"])
     );
 }
diff --git a/utils/manifest-utils/tests/certificate_validation.rs b/utils/manifest-utils/tests/certificate_validation.rs
index e285f59de9..62dde40c04 100644
--- a/utils/manifest-utils/tests/certificate_validation.rs
+++ b/utils/manifest-utils/tests/certificate_validation.rs
@@ -1,19 +1,21 @@
-pub mod common;
-
 #[macro_use]
 extern crate serial_test;
 
 use std::{fs, path::PathBuf};
 
-use common::*;
+use ya_manifest_test_utils::*;
 use ya_manifest_utils::Keystore;
 
+static TEST_RESOURCES: TestResources = TestResources {
+    temp_dir: env!("CARGO_TARGET_TMPDIR"),
+};
+
 #[test]
 #[serial]
 fn valid_certificate_test() {
     // Having
-    let (resource_cert_dir, test_cert_dir) = init_cert_dirs();
-    load_certificates(
+    let (resource_cert_dir, test_cert_dir) = TEST_RESOURCES.init_cert_dirs();
+    load_certificates_from_dir(
         &resource_cert_dir,
         &test_cert_dir,
         &["foo_ca-chain.cert.pem"],
@@ -32,8 +34,8 @@ fn valid_certificate_test() {
 #[serial]
 fn invalid_certificate_test() {
     // Having
-    let (resource_cert_dir, test_cert_dir) = init_cert_dirs();
-    load_certificates(&resource_cert_dir, &test_cert_dir, &[]);
+    let (resource_cert_dir, test_cert_dir) = TEST_RESOURCES.init_cert_dirs();
+    load_certificates_from_dir(&resource_cert_dir, &test_cert_dir, &[]);
 
     let request = prepare_request(resource_cert_dir);
 
@@ -45,7 +47,7 @@ fn invalid_certificate_test() {
         result.is_err(),
         "Keystore has no intermediate cert - verification should fail"
     );
-    let err = result.err().expect("Error result");
+    let err = result.expect_err("Error result");
     let msg = format!("{err:?}");
     assert_eq!(msg, "Invalid certificate");
 }
@@ -58,9 +60,9 @@ struct SignedRequest {
 }
 
 fn prepare_request(resource_cert_dir: PathBuf) -> SignedRequest {
-    let resource_dir = common::test_resources_dir_path();
+    let resource_dir = TEST_RESOURCES.test_resources_dir_path();
 
-    let mut cert = resource_cert_dir.clone();
+    let mut cert = resource_cert_dir;
     cert.push("foo_req.cert.pem");
     let mut cert = fs::read_to_string(cert).expect("Can read certificate file");
     cert = base64::encode(cert);
@@ -69,7 +71,7 @@ fn prepare_request(resource_cert_dir: PathBuf) -> SignedRequest {
     data.push("data.json.base64");
     let data = fs::read_to_string(data).expect("Can read resource file");
 
-    let mut sig = resource_dir.clone();
+    let mut sig = resource_dir;
     sig.push("data.json.base64.foo_req_sign.sha256.base64");
     let sig = fs::read_to_string(sig).expect("Can read resource file");
 
diff --git a/utils/manifest-utils/tests/common.rs b/utils/manifest-utils/tests/common.rs
deleted file mode 100644
index 2036c8c9ca..0000000000
--- a/utils/manifest-utils/tests/common.rs
+++ /dev/null
@@ -1,128 +0,0 @@
-use std::fs::{self, File};
-use std::sync::Once;
-use std::{collections::HashSet, path::PathBuf};
-
-use tar::Archive;
-
-use ya_manifest_utils::{
-    util::{self, CertBasicData, CertBasicDataVisitor},
-    KeystoreLoadResult, KeystoreRemoveResult,
-};
-
-static INIT: Once = Once::new();
-
-pub fn load_certificates(
-    resource_cert_dir: &PathBuf,
-    test_cert_dir: &PathBuf,
-    certfile_names: &[&str],
-) -> KeystoreLoadResult {
-    let cert_paths: Vec<PathBuf> = certfile_names
-        .iter()
-        .map(|certfile_name| {
-            let mut cert_path = resource_cert_dir.clone();
-            cert_path.push(certfile_name);
-            cert_path
-        })
-        .collect();
-    let keystore_manager =
-        util::KeystoreManager::try_new(test_cert_dir).expect("Can create keystore manager");
-    keystore_manager
-        .load_certs(&cert_paths)
-        .expect("Can load certificates")
-}
-
-pub fn remove_certificates(test_cert_dir: &PathBuf, cert_ids: &[&str]) -> KeystoreRemoveResult {
-    let keystore_manager =
-        util::KeystoreManager::try_new(test_cert_dir).expect("Can create keystore manager");
-    keystore_manager
-        .remove_certs(&to_set(cert_ids))
-        .expect("Can load certificates")
-}
-
-#[derive(Default)]
-pub struct TestCertDataVisitor {
-    expected: HashSet<String>,
-    actual: HashSet<String>,
-}
-
-impl TestCertDataVisitor {
-    #[allow(dead_code)]
-    pub fn new(expected: &[&str]) -> Self {
-        let mut visitor = Self::default();
-        visitor.expected = expected.iter().map(|s| s.to_string()).collect();
-        visitor
-    }
-
-    #[allow(dead_code)]
-    pub fn test(&self) {
-        assert_eq!(self.expected, self.actual)
-    }
-}
-
-impl CertBasicDataVisitor for TestCertDataVisitor {
-    fn accept(&mut self, cert_data: CertBasicData) {
-        self.actual.insert(cert_data.id);
-    }
-}
-
-pub fn init_cert_dirs() -> (PathBuf, PathBuf) {
-    let resource_cert_dir = resource_cert_dir_path();
-    INIT.call_once(|| {
-        if resource_cert_dir.exists() {
-            fs::remove_dir_all(&resource_cert_dir).expect("Can delete test cert resources dir");
-        }
-        fs::create_dir_all(&resource_cert_dir).expect("Can create temp dir");
-        unpack_cert_resources(&resource_cert_dir);
-    });
-    let store_cert_dir = store_cert_dir_path();
-    if store_cert_dir.exists() {
-        // we want to clean store cert dir before every test
-        fs::remove_dir_all(&store_cert_dir).expect("Can delete test temp dir");
-    }
-    fs::create_dir_all(&store_cert_dir).expect("Can create temp dir");
-    (resource_cert_dir, store_cert_dir)
-}
-
-pub fn loaded_cert_files() -> HashSet<String> {
-    let store_cert_dir = store_cert_dir_path();
-    let cert_dir = fs::read_dir(store_cert_dir).expect("Can read cert dir");
-    cert_dir
-        .into_iter()
-        .map(|file| file.expect("Can list cert files"))
-        .map(|x| x.file_name().to_string_lossy().to_string())
-        .collect()
-}
-
-pub fn to_set<T: AsRef<str>>(v: &[T]) -> HashSet<String> {
-    v.iter()
-        .map(|s| s.as_ref().to_string())
-        .collect::<HashSet<String>>()
-}
-
-fn unpack_cert_resources(cert_resources_dir: &PathBuf) {
-    let mut cert_archive = test_resources_dir_path();
-    cert_archive.push("certificates.tar");
-    let cert_archive = File::open(cert_archive).expect("Can open cert archive file");
-    let mut cert_archive = Archive::new(cert_archive);
-    cert_archive
-        .unpack(cert_resources_dir)
-        .expect("Can unack cert archive");
-}
-
-pub fn test_resources_dir_path() -> PathBuf {
-    let mut test_resources = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
-    test_resources.push("resources/test");
-    test_resources
-}
-
-fn resource_cert_dir_path() -> PathBuf {
-    let mut cert_dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
-    cert_dir.push("cert_resources");
-    cert_dir
-}
-
-fn store_cert_dir_path() -> PathBuf {
-    let mut cert_dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"));
-    cert_dir.push("cert_dir");
-    cert_dir
-}