diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index c2b0b1718..c20e5e607 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -37,7 +37,7 @@ use crate::{ }, claim::Claim, error::{Error, Result}, - http::{AsyncGenericResolver, SyncGenericResolver}, + http::{restricted::RestrictedResolver, AsyncGenericResolver, SyncGenericResolver}, jumbf_io, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, settings::{self, Settings}, @@ -617,6 +617,13 @@ impl Builder { R: Read + Seek + Send, { let settings = crate::settings::get_settings().unwrap_or_default(); + let http_resolver = if _sync { + SyncGenericResolver::new() + } else { + AsyncGenericResolver::new() + }; + let mut http_resolver = RestrictedResolver::new(http_resolver); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); let ingredient: Ingredient = Ingredient::from_json(&ingredient_json.into())?; @@ -632,10 +639,10 @@ impl Builder { } let ingredient = if _sync { - ingredient.with_stream(format, stream, &SyncGenericResolver::new(), &settings)? + ingredient.with_stream(format, stream, &http_resolver, &settings)? } else { ingredient - .with_stream_async(format, stream, &AsyncGenericResolver::new(), &settings) + .with_stream_async(format, stream, &http_resolver, &settings) .await? }; @@ -862,6 +869,8 @@ impl Builder { // so we will read the store directly here //crate::Reader::from_stream("application/c2pa", stream).and_then(|r| r.into_builder()) let settings = crate::settings::get_settings().unwrap_or_default(); + let mut http_resolver = RestrictedResolver::new(SyncGenericResolver::new()); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); let mut validation_log = crate::status_tracker::StatusTracker::default(); stream.rewind()?; // Ensure stream is at the start @@ -871,7 +880,7 @@ impl Builder { &mut stream, false, &mut validation_log, - &SyncGenericResolver::new(), + &http_resolver, &settings, )?; let reader = Reader::from_store(store, &mut validation_log, &settings)?; @@ -1581,6 +1590,8 @@ impl Builder { } else { AsyncGenericResolver::new() }; + let mut http_resolver = RestrictedResolver::new(http_resolver); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); let format = format_to_mime(format); self.definition.format.clone_from(&format); diff --git a/sdk/src/http/mod.rs b/sdk/src/http/mod.rs index 22fedf69a..df28f2b8b 100644 --- a/sdk/src/http/mod.rs +++ b/sdk/src/http/mod.rs @@ -11,6 +11,32 @@ // specific language governing permissions and limitations under // each license. +//! HTTP abstraction layer. +//! +//! This module defines generic traits and helpers for performing HTTP requests +//! without hard-wiring a specific HTTP client. It allows host applications to +//! plug in their own HTTP implementation, restrict where the SDK may connect, +//! or disable networking entirely. +//! +//! # When do outbound network requests occur? +//! +//! The SDK may issue outbound HTTP/S requests in the following scenarios: +//! - [`Reader`]: +//! - Fetching remote manifests +//! - Validating CAWG identity assertions +//! - Fetching OCSP revocation status +//! - [`Builder`]: +//! - Fetching ingredient remote manifests +//! - Fetching timestamps +//! - Fetching [`TimeStamp`] assertions +//! - Fetching OCSP staples +//! - Fetching [`CertificateStatus`] assertions +//! +//! [`Reader`]: crate::Reader +//! [`Builder`]: crate::Builder +//! [`TimeStamp`]: crate::assertions::TimeStamp +//! [`CertificateStatus`]: crate::assertions::CertificateStatus + use std::io::{self, Read}; use async_trait::async_trait; @@ -22,6 +48,9 @@ mod reqwest; mod ureq; mod wasi; +/// Structs for restricting outbound network requests. +pub mod restricted; + // Since we use `http::Request` and `http::Response` we also expose the `http` crate. pub use http; @@ -150,6 +179,14 @@ pub enum HttpResolverError { #[error("the async http resolver is not implemented")] AsyncHttpResolverNotImplemented, + /// The remote URI is blocked by the allowed list. + /// + /// The allowed list is normally set in a [`RestrictedResolver`]. + /// + /// [`RestrictedResolver`]: restricted::RestrictedResolver + #[error("remote URI \"{uri}\" is not permitted by the allowed list")] + UriDisallowed { uri: String }, + /// An error occured from the underlying HTTP resolver. #[error("an error occurred from the underlying http resolver")] Other(Box), diff --git a/sdk/src/http/restricted.rs b/sdk/src/http/restricted.rs new file mode 100644 index 000000000..c82a61474 --- /dev/null +++ b/sdk/src/http/restricted.rs @@ -0,0 +1,404 @@ +// Copyright 2025 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +//! HTTP request restriction layer. +//! +//! # Why restrict network requests? +//! In some environments, you may not want the SDK to talk to arbitrary hosts. Restricting +//! network requests help to: +//! - Reduce SSRF-style risks (e.g. requests to internal services). +//! - Constrain requests to a small, trusted set of domains. +//! +//! # OCSP and other dynamic endpoints +//! Some protocols used by the SDK (like OCSP or CRLs) discover endpoints from certificate +//! metadata at runtime. In a restricted environment, there is no way for the resolver to +//! know that these endpoints are "special" unless you anticipate them in advance and add +//! their hosts to the allow-list. +//! +//! # Disabling networking completely +//! This restriction layer is a runtime control. To turn networking off entirely at compile +//! time, do not enable any of the HTTP features (`http_*`), see ["Features"]. +//! +//! ["Features"]: crate#features + +use std::io::Read; + +use async_trait::async_trait; +use http::{Request, Response, Uri}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + http::{AsyncHttpResolver, HttpResolverError, SyncHttpResolver}, + Result, +}; + +/// HTTP resolver wrapper that enforces an allowed list of outbound hosts. +/// +/// If the allowed list is empty, no filtering is applied and all outbound requests are allowed. +/// +/// When a URI is not permitted, the resolver returns [`HttpResolverError::UriDisallowed`]. +#[derive(Debug)] +pub struct RestrictedResolver { + inner: T, + allowed_hosts: Option>, +} + +impl RestrictedResolver { + /// Creates a new `RestrictedResolver` with an empty allowed list. + pub fn new(inner: T) -> Self { + Self { + inner, + allowed_hosts: None, + } + } + + /// Creates a new `RestrictedResolver` with the specified allowed list. + #[allow(dead_code)] // TODO: temp until http module is public + pub fn with_allowed_hosts(inner: T, allowed_hosts: Vec) -> Self { + Self { + inner, + allowed_hosts: Some(allowed_hosts), + } + } + + /// Replaces the current allowed list with the given allowed list if specified. + pub fn set_allowed_hosts(&mut self, allowed_hosts: Option>) { + self.allowed_hosts = allowed_hosts; + } + + /// Returns a reference to the allowed list. + pub fn allowed_hosts(&self) -> Option<&[HostPattern]> { + self.allowed_hosts.as_deref() + } +} + +impl SyncHttpResolver for RestrictedResolver { + fn http_resolve( + &self, + request: Request>, + ) -> Result>, HttpResolverError> { + if self + .allowed_hosts() + .is_none_or(|hosts| is_uri_allowed(hosts, request.uri())) + { + self.inner.http_resolve(request) + } else { + Err(HttpResolverError::UriDisallowed { + uri: request.uri().to_string(), + }) + } + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +impl AsyncHttpResolver for RestrictedResolver { + async fn http_resolve_async( + &self, + request: Request>, + ) -> Result>, HttpResolverError> { + if self + .allowed_hosts() + .is_none_or(|hosts| is_uri_allowed(hosts, request.uri())) + { + self.inner.http_resolve_async(request).await + } else { + Err(HttpResolverError::UriDisallowed { + uri: request.uri().to_string(), + }) + } + } +} + +/// A host/scheme pattern used to restrict outbound network requests. +/// +/// Each pattern may include: +/// - A scheme (e.g. `https://` or `http://`) +/// - A hostname, which may have a single leading wildcard (e.g. `*.contentauthenticity.org`) +/// +/// Matching is case-insensitive. A wildcard pattern such as `*.contentauthenticity.org` matches +/// `sub.contentauthenticity.org`, but does not match `contentauthenticity.org` or `fakecontentauthenticity.org`. +/// If a scheme is present in the pattern, only URIs using the same scheme are considered a match. If the scheme +/// is omitted, any scheme is allowed as long as the host matches. +#[cfg_attr( + feature = "json_schema", + derive(schemars::JsonSchema), + schemars(with = "String") +)] +#[derive(Debug, Clone, PartialEq)] +pub struct HostPattern { + pattern: String, + scheme: Option, + host: Option, +} + +impl HostPattern { + /// Creates a new `HostPattern` with the given pattern. + pub fn new(pattern: &str) -> Self { + let pattern = pattern.to_ascii_lowercase(); + let (scheme, host): (Option, Option) = + if let Some(host) = pattern.strip_prefix("https://") { + (Some("https".to_owned()), Some(host.to_owned())) + } else if let Some(host) = pattern.strip_prefix("http://") { + (Some("http".to_owned()), Some(host.to_owned())) + } else { + (None, Some(pattern.clone())) + }; + + Self { + pattern, + scheme, + host, + } + } + + /// Returns if the given URI matches the `HostPattern`. + pub fn matches(&self, uri: &Uri) -> bool { + if let Some(allowed_host_pattern) = &self.host { + if let Some(host) = uri.host() { + // If there's a wildcard, do an suffix match, otherwise do an exact match. + let host_allowed = if let Some(suffix) = allowed_host_pattern.strip_prefix("*.") { + let host = host.to_ascii_lowercase(); + + if host.len() <= suffix.len() || !host.ends_with(&suffix) { + false + } else { + // Make sure there is a component in place of the wildcard. + host.as_bytes()[host.len() - suffix.len() - 1] == b'.' + } + } else { + allowed_host_pattern.eq_ignore_ascii_case(host) + }; + + if host_allowed { + if let Some(allowed_scheme) = &self.scheme { + if let Some(scheme) = uri.scheme() { + return scheme.as_str() == allowed_scheme; + } + } else { + return true; + } + } + } + } else if let Some(allowed_scheme) = &self.scheme { + if let Some(scheme) = uri.scheme() { + return scheme.as_str() == allowed_scheme; + } + } + + false + } +} + +impl From<&str> for HostPattern { + fn from(pattern: &str) -> Self { + Self::new(pattern) + } +} + +impl Serialize for HostPattern { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.pattern.to_string()) + } +} + +impl<'de> Deserialize<'de> for HostPattern { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(HostPattern::new(&String::deserialize(deserializer)?)) + } +} + +/// Returns if the given URI matches at least one of the [`HostPattern`]s. +fn is_uri_allowed(patterns: &[HostPattern], uri: &Uri) -> bool { + for pattern in patterns { + if pattern.matches(uri) { + return true; + } + } + + false +} + +#[cfg(test)] +mod test { + #![allow(clippy::panic, clippy::unwrap_used)] + + use super::*; + + struct NoopHttpResolver; + + impl SyncHttpResolver for NoopHttpResolver { + fn http_resolve( + &self, + _request: Request>, + ) -> Result>, HttpResolverError> { + Ok(Response::new(Box::new(std::io::empty()) as Box)) + } + } + + fn assert_allowed_uri(resolver: &impl SyncHttpResolver, uri: &'static str) { + let result = resolver.http_resolve( + Request::get(Uri::from_static(uri)) + .body(Vec::new()) + .unwrap(), + ); + assert!(matches!(result, Ok(..))); + } + + fn assert_disallowed_uri(resolver: &impl SyncHttpResolver, uri: &'static str) { + let result = resolver.http_resolve( + Request::get(Uri::from_static(uri)) + .body(Vec::new()) + .unwrap(), + ); + assert!(matches!( + result, + Err(HttpResolverError::UriDisallowed { .. }) + )); + } + + #[test] + fn allowed_http_request() { + let allowed_list = vec![ + "*.prefix.contentauthenticity.org".into(), + "test.contentauthenticity.org".into(), + "fakecontentauthenticity.org".into(), + "https://*.contentauthenticity.org".into(), + "https://test.contentauthenticity.org".into(), + ]; + let restricted_resolver = + RestrictedResolver::with_allowed_hosts(NoopHttpResolver, allowed_list); + + assert_allowed_uri(&restricted_resolver, "fakecontentauthenticity.org"); + assert_allowed_uri(&restricted_resolver, "test.prefix.contentauthenticity.org"); + assert_allowed_uri(&restricted_resolver, "https://test.contentauthenticity.org"); + assert_allowed_uri( + &restricted_resolver, + "https://test2.contentauthenticity.org", + ); + + assert_disallowed_uri(&restricted_resolver, "test.test.contentauthenticity.org"); + assert_disallowed_uri( + &restricted_resolver, + "https://test.prefix.fakecontentauthenticity.org", + ); + assert_disallowed_uri( + &restricted_resolver, + "https://test.fakecontentauthenticity.org", + ); + assert_disallowed_uri(&restricted_resolver, "https://contentauthenticity.org"); + } + + #[test] + fn allowed_none_http_request() { + let allowed_list = vec![]; + let restricted_resolver = + RestrictedResolver::with_allowed_hosts(NoopHttpResolver, allowed_list); + + assert_disallowed_uri( + &restricted_resolver, + "test.test.fakecontentauthenticity.org", + ); + assert_disallowed_uri( + &restricted_resolver, + "https://test.prefix.fakecontentauthenticity.org", + ); + assert_disallowed_uri( + &restricted_resolver, + "https://test.fakecontentauthenticity.org", + ); + assert_disallowed_uri(&restricted_resolver, "https://contentauthenticity.org"); + } + + #[test] + fn wildcard_pattern() { + let pattern = HostPattern::new("*.contentauthenticity.org"); + + let uri = Uri::from_static("test.contentauthenticity.org"); + assert!(pattern.matches(&uri)); + + let uri = Uri::from_static("contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("fakecontentauthenticity.org"); + assert!(!pattern.matches(&uri)); + } + + #[test] + fn wildcard_pattern_with_scheme() { + let pattern = HostPattern::new("https://*.contentauthenticity.org"); + + let uri = Uri::from_static("test.contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("fakecontentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("https://test.contentauthenticity.org"); + assert!(pattern.matches(&uri)); + + let uri = Uri::from_static("https://contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("https://fakecontentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("http://test.contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + } + + #[test] + fn case_insensitive_pattern() { + let pattern = HostPattern::new("*.contentAuthenticity.org"); + + let uri = Uri::from_static("tEst.conTentauthenticity.orG"); + assert!(pattern.matches(&uri)); + } + + #[test] + fn exact_pattern() { + let pattern = HostPattern::new("contentauthenticity.org"); + + let uri = Uri::from_static("contentauthenticity.org"); + assert!(pattern.matches(&uri)); + + let uri = Uri::from_static("https://contentauthenticity.org"); + assert!(pattern.matches(&uri)); + + let uri = Uri::from_static("http://contentauthenticity.org"); + assert!(pattern.matches(&uri)); + } + + #[test] + fn exact_pattern_with_schema() { + let pattern = HostPattern::new("https://contentauthenticity.org"); + + let uri = Uri::from_static("https://contentauthenticity.org"); + assert!(pattern.matches(&uri)); + + let uri = Uri::from_static("http://contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + + let uri = Uri::from_static("contentauthenticity.org"); + assert!(!pattern.matches(&uri)); + } +} diff --git a/sdk/src/ingredient.rs b/sdk/src/ingredient.rs index 6f1aa71aa..8ec46d444 100644 --- a/sdk/src/ingredient.rs +++ b/sdk/src/ingredient.rs @@ -35,7 +35,10 @@ use crate::{ crypto::base64, error::{Error, Result}, hashed_uri::HashedUri, - http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, + http::{ + restricted::RestrictedResolver, AsyncGenericResolver, AsyncHttpResolver, + SyncGenericResolver, SyncHttpResolver, + }, jumbf::{ self, labels::{assertion_label_from_uri, manifest_label_from_uri}, @@ -733,7 +736,9 @@ impl Ingredient { options: &dyn IngredientOptions, ) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = SyncGenericResolver::new(); + let mut http_resolver = RestrictedResolver::new(SyncGenericResolver::new()); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); + Self::from_file_impl(path.as_ref(), options, &http_resolver, &settings) } @@ -842,9 +847,12 @@ impl Ingredient { /// Thumbnail will be set only if one can be retrieved from a previous valid manifest. pub fn from_stream(format: &str, stream: &mut dyn CAIRead) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); + let mut http_resolver = RestrictedResolver::new(SyncGenericResolver::new()); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); + let ingredient = Self::from_stream_info(stream, format, "untitled"); stream.rewind()?; - ingredient.add_stream_internal(format, stream, &SyncGenericResolver::new(), &settings) + ingredient.add_stream_internal(format, stream, &http_resolver, &settings) } /// Create an Ingredient from JSON. @@ -1023,7 +1031,9 @@ impl Ingredient { /// Thumbnail will be set only if one can be retrieved from a previous valid manifest. pub async fn from_stream_async(format: &str, stream: &mut dyn CAIRead) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = AsyncGenericResolver::new(); + let mut http_resolver = RestrictedResolver::new(AsyncGenericResolver::new()); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); + Self::from_stream_async_with_settings(format, stream, &http_resolver, &settings).await } @@ -1042,7 +1052,7 @@ impl Ingredient { let (result, manifest_bytes) = match Store::load_jumbf_from_stream_async( format, stream, - &AsyncGenericResolver::new(), + http_resolver, settings, ) .await @@ -1489,7 +1499,9 @@ impl Ingredient { stream: &mut dyn CAIRead, ) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = AsyncGenericResolver::new(); + let mut http_resolver = RestrictedResolver::new(AsyncGenericResolver::new()); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); + let mut ingredient = Self::from_stream_info(stream, format, "untitled"); let mut validation_log = StatusTracker::default(); diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 0eff6e704..0c7d5e293 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -35,7 +35,7 @@ use crate::{ claim::Claim, dynamic_assertion::PartialClaim, error::{Error, Result}, - http::{AsyncGenericResolver, SyncGenericResolver}, + http::{restricted::RestrictedResolver, AsyncGenericResolver, SyncGenericResolver}, jumbf::labels::{manifest_label_from_uri, to_absolute_uri, to_relative_uri}, jumbf_io, log_item, manifest::StoreOptions, @@ -140,11 +140,15 @@ impl Reader { #[cfg(not(target_arch = "wasm32"))] pub fn from_stream(format: &str, mut stream: impl Read + Seek + Send) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); + let http_resolver = if _sync { SyncGenericResolver::new() } else { AsyncGenericResolver::new() }; + let mut http_resolver = RestrictedResolver::new(http_resolver); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); + // TODO: passing verify is redundant with settings let verify = settings.verify.verify_after_reading; @@ -187,6 +191,9 @@ impl Reader { } else { AsyncGenericResolver::new() }; + let mut http_resolver = RestrictedResolver::new(http_resolver); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); + // TODO: passing verify is redundant with settings let verify = settings.verify.verify_after_reading; @@ -314,6 +321,8 @@ impl Reader { } else { AsyncGenericResolver::new() }; + let mut http_resolver = RestrictedResolver::new(http_resolver); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); let mut validation_log = StatusTracker::default(); @@ -368,6 +377,8 @@ impl Reader { } else { AsyncGenericResolver::new() }; + let mut http_resolver = RestrictedResolver::new(http_resolver); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); let mut validation_log = StatusTracker::default(); @@ -404,7 +415,8 @@ impl Reader { fragments: &Vec, ) -> Result { let settings = crate::settings::get_settings().unwrap_or_default(); - let http_resolver = SyncGenericResolver::new(); + let mut http_resolver = RestrictedResolver::new(SyncGenericResolver::new()); + http_resolver.set_allowed_hosts(settings.core.allowed_network_hosts.clone()); let verify = settings.verify.verify_after_reading; let mut validation_log = StatusTracker::default(); diff --git a/sdk/src/settings/mod.rs b/sdk/src/settings/mod.rs index f271a062c..0109a3767 100644 --- a/sdk/src/settings/mod.rs +++ b/sdk/src/settings/mod.rs @@ -27,7 +27,10 @@ use config::{Config, FileFormat}; use serde_derive::{Deserialize, Serialize}; use signer::SignerSettings; -use crate::{crypto::base64, settings::builder::BuilderSettings, Error, Result, Signer}; +use crate::{ + crypto::base64, http::restricted::HostPattern, settings::builder::BuilderSettings, Error, + Result, Signer, +}; const VERSION: u32 = 1; @@ -219,6 +222,30 @@ pub struct Core { /// [`IdentityAssertion`]: crate::identity::IdentityAssertion /// [`Reader`]: crate::Reader pub decode_identity_assertions: bool, + /// List of host patterns that are allowed for outbound network requests. + /// + /// Each pattern may include: + /// - A scheme (e.g. `https://` or `http://`) + /// - A hostname, which may have a single leading wildcard (e.g. `*.contentauthenticity.org`) + /// + /// Matching is case-insensitive. A wildcard pattern such as `*.contentauthenticity.org` matches + /// `sub.contentauthenticity.org`, but does not match `contentauthenticity.org` or `fakecontentauthenticity.org`. + /// If a scheme is present in the pattern, only URIs using the same scheme are considered a match. If the scheme + /// is omitted, any scheme is allowed as long as the host matches. + /// + /// The behavior is as follows: + /// - `None` (default) no filtering enabled. + /// - `Some(vec)` where `vec` is empty, all outbound traffic is blocked. + /// - `Some(vec)` with at least one pattern, filtering enabled for only those patterns. + /// + /// These settings are consumed by [`RestrictedResolver`]. + /// + /// For information on when the SDK might perform an outbound network request, see ["When do outbound network requests occur?"] + /// + /// ["When do outbound network requests occur?"]: crate::http#when-do-outbound-network-requests-occur + /// [`HostPattern`]: crate::http::restricted::HostPattern + /// [`RestrictedResolver`]: crate::http::restricted::RestrictedResolver + pub allowed_network_hosts: Option>, } impl Default for Core { @@ -228,6 +255,7 @@ impl Default for Core { merkle_tree_max_proofs: 5, backing_store_memory_threshold_in_mb: 512, decode_identity_assertions: true, + allowed_network_hosts: None, } } }