pub mod builder;
pub mod signer;
#[cfg(feature = "file_io")]
use std::path::Path;
use std::{
cell::RefCell,
io::{BufRead, BufReader, Cursor},
};
use config::{Config, FileFormat};
use serde_derive::{Deserialize, Serialize};
use signer::SignerSettings;
use crate::{
crypto::base64, http::restricted::HostPattern, settings::builder::BuilderSettings, Error,
Result,
};
const VERSION: u32 = 1;
pub(crate) const MAX_ASSERTIONS: usize = 100_000;
thread_local!(
static SETTINGS: RefCell<Config> =
RefCell::new(Config::try_from(&Settings::default()).unwrap_or_default());
);
pub(crate) trait SettingsValidate {
fn validate(&self) -> Result<()> {
Ok(())
}
}
#[cfg_attr(
feature = "json_schema",
derive(schemars::JsonSchema),
schemars(default)
)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Trust {
pub(crate) verify_trust_list: bool,
pub user_anchors: Option<String>,
pub trust_anchors: Option<String>,
pub trust_config: Option<String>,
pub allowed_list: Option<String>,
}
impl Trust {
fn load_trust_from_data(&self, trust_data: &[u8]) -> Result<Vec<Vec<u8>>> {
let mut certs = Vec::new();
let trust_data = String::from_utf8_lossy(trust_data)
.replace("\\n", "\n")
.into_bytes();
for pem_result in x509_parser::pem::Pem::iter_from_buffer(&trust_data) {
let pem = pem_result.map_err(|_e| Error::CoseInvalidCert)?;
certs.push(pem.contents);
}
Ok(certs)
}
fn test_load_trust(&self, allowed_list: &[u8]) -> Result<()> {
if let Ok(cert_list) = self.load_trust_from_data(allowed_list) {
if !cert_list.is_empty() {
return Ok(());
}
}
let reader = Cursor::new(allowed_list);
let buf_reader = BufReader::new(reader);
let mut found_der_hash = false;
let mut inside_cert_block = false;
for l in buf_reader.lines().map_while(|v| v.ok()) {
if l.contains("-----BEGIN") {
inside_cert_block = true;
}
if l.contains("-----END") {
inside_cert_block = false;
}
if !inside_cert_block && base64::decode(&l).is_ok() && !l.is_empty() {
found_der_hash = true;
}
}
if found_der_hash {
Ok(())
} else {
Err(Error::CoseInvalidCert)
}
}
}
#[allow(clippy::derivable_impls)]
impl Default for Trust {
fn default() -> Self {
#[cfg(test)]
{
let mut trust = Self {
verify_trust_list: true,
user_anchors: None,
trust_anchors: None,
trust_config: None,
allowed_list: None,
};
trust.trust_config = Some(
String::from_utf8_lossy(include_bytes!(
"../../tests/fixtures/certs/trust/store.cfg"
))
.into_owned(),
);
trust.user_anchors = Some(
String::from_utf8_lossy(include_bytes!(
"../../tests/fixtures/certs/trust/test_cert_root_bundle.pem"
))
.into_owned(),
);
trust
}
#[cfg(not(test))]
{
Self {
verify_trust_list: true,
user_anchors: None,
trust_anchors: None,
trust_config: None,
allowed_list: None,
}
}
}
}
impl SettingsValidate for Trust {
fn validate(&self) -> Result<()> {
if let Some(ta) = &self.trust_anchors {
self.test_load_trust(ta.as_bytes())?;
}
if let Some(pa) = &self.user_anchors {
self.test_load_trust(pa.as_bytes())?;
}
if let Some(al) = &self.allowed_list {
self.test_load_trust(al.as_bytes())?;
}
Ok(())
}
}
#[cfg_attr(
feature = "json_schema",
derive(schemars::JsonSchema),
schemars(default)
)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Core {
pub merkle_tree_chunk_size_in_kb: Option<usize>,
pub merkle_tree_max_proofs: usize,
pub backing_store_memory_threshold_in_mb: usize,
pub decode_identity_assertions: bool,
pub allowed_network_hosts: Option<Vec<HostPattern>>,
}
impl Default for Core {
fn default() -> Self {
Self {
merkle_tree_chunk_size_in_kb: None,
merkle_tree_max_proofs: 5,
backing_store_memory_threshold_in_mb: 512,
decode_identity_assertions: true,
allowed_network_hosts: None,
}
}
}
impl SettingsValidate for Core {
fn validate(&self) -> Result<()> {
Ok(())
}
}
#[cfg_attr(
feature = "json_schema",
derive(schemars::JsonSchema),
schemars(default)
)]
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Verify {
pub verify_after_reading: bool,
pub verify_after_sign: bool,
pub(crate) verify_trust: bool,
pub(crate) verify_timestamp_trust: bool,
pub ocsp_fetch: bool,
pub remote_manifest_fetch: bool,
pub(crate) skip_ingredient_conflict_resolution: bool,
pub strict_v1_validation: bool,
}
impl Default for Verify {
fn default() -> Self {
Self {
verify_after_reading: true,
verify_after_sign: false, verify_trust: true,
verify_timestamp_trust: !cfg!(test), ocsp_fetch: false,
remote_manifest_fetch: true,
skip_ingredient_conflict_resolution: false,
strict_v1_validation: false,
}
}
}
impl SettingsValidate for Verify {}
#[cfg_attr(
feature = "json_schema",
derive(schemars::JsonSchema),
schemars(default)
)]
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct Settings {
pub version: u32,
pub trust: Trust,
pub cawg_trust: Trust,
pub core: Core,
pub verify: Verify,
pub builder: BuilderSettings,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer: Option<SignerSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cawg_x509_signer: Option<SignerSettings>,
}
impl Settings {
#[cfg(feature = "file_io")]
#[doc(hidden)]
#[deprecated(
note = "Use `Settings::new().with_file(path)` instead, which does not modify thread-local state."
)]
#[allow(deprecated)]
pub fn from_file<P: AsRef<Path>>(settings_path: P) -> Result<Self> {
let ext = settings_path
.as_ref()
.extension()
.ok_or(Error::UnsupportedType)?
.to_string_lossy();
let setting_buf = std::fs::read(&settings_path).map_err(Error::IoError)?;
Settings::from_string(&String::from_utf8_lossy(&setting_buf), &ext)
}
#[doc(hidden)]
#[deprecated(
note = "Use `Settings::new().with_json(str)` or `Settings::new().with_toml(str)` instead, which do not modify thread-local state."
)]
pub fn from_string(settings_str: &str, format: &str) -> Result<Self> {
let f = match format.to_lowercase().as_str() {
"json" => FileFormat::Json,
"toml" => FileFormat::Toml,
_ => return Err(Error::UnsupportedType),
};
let new_config = Config::builder()
.add_source(config::File::from_str(settings_str, f))
.build()
.map_err(|_e| Error::BadParam("could not parse configuration file".into()))?;
let update_config = SETTINGS.with_borrow(|current_settings| {
Config::builder()
.add_source(current_settings.clone())
.add_source(new_config)
.build() });
match update_config {
Ok(update_config) => {
let settings = update_config
.clone()
.try_deserialize::<Settings>()
.map_err(|e| Error::BadParam(e.to_string()))?;
settings.validate()?;
SETTINGS.set(update_config.clone());
Ok(settings)
}
Err(_) => Err(Error::OtherError("could not update configuration".into())),
}
}
#[deprecated(
note = "Use `Settings::new().with_toml(toml)` instead, which does not modify thread-local state."
)]
#[allow(deprecated)]
pub fn from_toml(toml: &str) -> Result<()> {
Settings::from_string(toml, "toml").map(|_| ())
}
pub fn update_from_str(&mut self, settings_str: &str, format: &str) -> Result<()> {
let file_format = match format.to_lowercase().as_str() {
"json" => FileFormat::Json,
"toml" => FileFormat::Toml,
_ => return Err(Error::UnsupportedType),
};
let current_config = Config::try_from(&*self)
.map_err(|e| Error::BadParam(format!("could not convert settings: {e}")))?;
let merged_config = Config::builder()
.add_source(current_config)
.add_source(config::File::from_str(settings_str, file_format))
.build()
.map_err(|e| Error::BadParam(format!("could not merge configuration: {e}")))?;
let updated_settings = merged_config
.try_deserialize::<Settings>()
.map_err(|e| Error::BadParam(e.to_string()))?;
updated_settings.validate()?;
*self = updated_settings;
Ok(())
}
#[allow(unused)]
pub(crate) fn set_thread_local_value<T: Into<config::Value>>(
value_path: &str,
value: T,
) -> Result<()> {
let c = SETTINGS.take();
let update_config = Config::builder()
.add_source(c.clone())
.set_override(value_path, value);
if let Ok(updated) = update_config {
let update_config = updated
.build()
.map_err(|_e| Error::OtherError("could not update configuration".into()))?;
let settings = update_config
.clone()
.try_deserialize::<Settings>()
.map_err(|e| Error::BadParam(e.to_string()))?;
settings.validate()?;
SETTINGS.set(update_config);
Ok(())
} else {
SETTINGS.set(c);
Err(Error::OtherError("could not save settings".into()))
}
}
#[allow(unused)]
fn get_thread_local_value<'de, T: serde::de::Deserialize<'de>>(value_path: &str) -> Result<T> {
SETTINGS.with_borrow(|current_settings| {
let update_config = Config::builder()
.add_source(current_settings.clone())
.build()
.map_err(|_e| Error::OtherError("could not update configuration".into()))?;
update_config
.get::<T>(value_path)
.map_err(|_| Error::BadParam("could not get settings value".into()))
})
}
#[allow(unused)]
pub(crate) fn reset() -> Result<()> {
if let Ok(default_settings) = Config::try_from(&Settings::default()) {
SETTINGS.set(default_settings);
Ok(())
} else {
Err(Error::OtherError("could not reset settings".into()))
}
}
pub fn new() -> Self {
Self::default()
}
pub fn with_json(self, json: &str) -> Result<Self> {
self.with_string(json, "json")
}
pub fn with_toml(self, toml: &str) -> Result<Self> {
self.with_string(toml, "toml")
}
#[cfg(feature = "file_io")]
pub fn with_file<P: AsRef<Path>>(self, path: P) -> Result<Self> {
let path = path.as_ref();
let ext = path
.extension()
.ok_or(Error::BadParam(
"settings file must have json or toml extension".into(),
))?
.to_str()
.ok_or(Error::BadParam("invalid settings file name".into()))?;
let setting_buf = std::fs::read(path).map_err(Error::IoError)?;
self.with_string(&String::from_utf8_lossy(&setting_buf), ext)
}
fn with_string(self, settings_str: &str, format: &str) -> Result<Self> {
let f = match format.to_lowercase().as_str() {
"json" => FileFormat::Json,
"toml" => FileFormat::Toml,
_ => return Err(Error::UnsupportedType),
};
let current_config = Config::try_from(&self).map_err(|e| Error::OtherError(Box::new(e)))?;
let updated_config = Config::builder()
.add_source(current_config)
.add_source(config::File::from_str(settings_str, f))
.build()
.map_err(|_e| Error::BadParam("could not parse configuration".into()))?;
let settings = updated_config
.try_deserialize::<Settings>()
.map_err(|e| Error::BadParam(e.to_string()))?;
settings.validate()?;
Ok(settings)
}
#[doc(hidden)]
#[deprecated(
note = "Use `toml::to_string(&settings)` on a `Settings` instance instead of reading from thread-local state."
)]
pub fn to_toml() -> Result<String> {
let settings = get_thread_local_settings();
Ok(toml::to_string(&settings)?)
}
#[doc(hidden)]
#[deprecated(
note = "Use `toml::to_string_pretty(&settings)` on a `Settings` instance instead of reading from thread-local state."
)]
pub fn to_pretty_toml() -> Result<String> {
let settings = get_thread_local_settings();
Ok(toml::to_string_pretty(&settings)?)
}
#[inline]
#[deprecated(
note = "Configure the signer via `Context` and pass it to `Builder::from_context` instead of using thread-local signer settings."
)]
pub fn signer() -> Result<crate::BoxedSigner> {
SignerSettings::signer()
}
pub fn with_value<T: Into<config::Value>>(self, path: &str, value: T) -> Result<Self> {
let config = Config::try_from(&self).map_err(|e| Error::OtherError(Box::new(e)))?;
let updated_config = Config::builder()
.add_source(config)
.set_override(path, value)
.map_err(|e| Error::BadParam(format!("Invalid path '{path}': {e}")))?
.build()
.map_err(|e| Error::OtherError(Box::new(e)))?;
let updated_settings = updated_config
.try_deserialize::<Settings>()
.map_err(|e| Error::BadParam(format!("Invalid value for '{path}': {e}")))?;
updated_settings.validate()?;
Ok(updated_settings)
}
pub fn set_value<T: Into<config::Value>>(&mut self, path: &str, value: T) -> Result<()> {
*self = std::mem::take(self).with_value(path, value)?;
Ok(())
}
pub fn get_value<'de, T: serde::de::Deserialize<'de>>(&self, path: &str) -> Result<T> {
let config = Config::try_from(self).map_err(|e| Error::OtherError(Box::new(e)))?;
config
.get::<T>(path)
.map_err(|e| Error::BadParam(format!("Failed to get value at '{path}': {e}")))
}
}
impl Default for Settings {
fn default() -> Self {
Settings {
version: VERSION,
trust: Default::default(),
cawg_trust: Default::default(),
core: Default::default(),
verify: Default::default(),
builder: Default::default(),
signer: None,
cawg_x509_signer: None,
}
}
}
impl SettingsValidate for Settings {
fn validate(&self) -> Result<()> {
if self.version > VERSION {
return Err(Error::VersionCompatibility(
"settings version too new".into(),
));
}
if let Some(signer) = &self.signer {
signer.validate()?;
}
if let Some(cawg_x509_signer) = &self.cawg_x509_signer {
cawg_x509_signer.validate()?;
}
self.trust.validate()?;
self.cawg_trust.validate()?;
self.core.validate()?;
self.builder.validate()
}
}
#[allow(unused)]
pub(crate) fn get_thread_local_settings() -> Settings {
SETTINGS.with_borrow(|config| {
config
.clone()
.try_deserialize::<Settings>()
.unwrap_or_default()
})
}
#[cfg(test)]
pub(crate) fn set_settings_value<T: Into<config::Value>>(value_path: &str, value: T) -> Result<()> {
Settings::set_thread_local_value(value_path, value)
}
#[cfg(test)]
fn get_settings_value<'de, T: serde::de::Deserialize<'de>>(value_path: &str) -> Result<T> {
Settings::get_thread_local_value(value_path)
}
#[cfg(test)]
pub fn reset_default_settings() -> Result<()> {
Settings::reset()
}
#[cfg(test)]
pub mod tests {
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use super::*;
#[cfg(feature = "file_io")]
use crate::utils::io_utils::tempdirectory;
use crate::{utils::test::test_settings, SigningAlg};
#[cfg(feature = "file_io")]
fn save_settings_as_json<P: AsRef<Path>>(settings_path: P) -> Result<()> {
let settings = get_thread_local_settings();
let settings_json = serde_json::to_string_pretty(&settings).map_err(Error::JsonError)?;
std::fs::write(settings_path, settings_json.as_bytes()).map_err(Error::IoError)
}
#[test]
fn test_thread_local_settings() {
let settings = get_thread_local_settings();
assert_eq!(settings.core, Core::default());
assert_eq!(settings.trust, Trust::default());
assert_eq!(settings.verify, Verify::default());
assert_eq!(settings.builder, BuilderSettings::default());
assert_eq!(
get_settings_value::<bool>("builder.thumbnail.enabled").unwrap(),
BuilderSettings::default().thumbnail.enabled
);
assert_eq!(
get_settings_value::<bool>("verify.remote_manifest_fetch").unwrap(),
Verify::default().remote_manifest_fetch
);
Settings::set_thread_local_value("core.merkle_tree_chunk_size_in_kb", 10).unwrap();
Settings::set_thread_local_value("verify.remote_manifest_fetch", false).unwrap();
Settings::set_thread_local_value("builder.thumbnail.enabled", false).unwrap();
assert_eq!(
get_settings_value::<usize>("core.merkle_tree_chunk_size_in_kb").unwrap(),
10
);
assert!(!get_settings_value::<bool>("verify.remote_manifest_fetch").unwrap());
assert!(!get_settings_value::<bool>("builder.thumbnail.enabled").unwrap());
reset_default_settings().unwrap();
}
#[cfg(feature = "file_io")]
#[test]
fn test_save_load() {
let temp_dir = tempdirectory().unwrap();
let op = crate::utils::test::temp_dir_path(&temp_dir, "sdk_config.json");
save_settings_as_json(&op).unwrap();
let settings = Settings::new().with_file(&op).unwrap();
assert_eq!(settings, Settings::default());
}
#[test]
fn test_settings_from_json_str() {
let json = serde_json::to_string(&Settings::default()).unwrap();
let settings = Settings::new().with_json(&json).unwrap();
assert_eq!(settings, Settings::default());
}
#[test]
fn test_bad_setting() {
let modified_core = toml::toml! {
[core]
merkle_tree_chunk_size_in_kb = true
merkle_tree_max_proofs = "sha1000000"
backing_store_memory_threshold_in_mb = -123456
}
.to_string();
assert!(Settings::new().with_toml(&modified_core).is_err());
}
#[test]
#[allow(deprecated)]
fn test_thread_local_hidden_setting() {
let secret = toml::toml! {
[hidden]
test1 = true
test2 = "hello world"
test3 = 123456
}
.to_string();
Settings::from_toml(&secret).unwrap();
assert!(get_settings_value::<bool>("hidden.test1").unwrap());
assert_eq!(
get_settings_value::<String>("hidden.test2").unwrap(),
"hello world".to_string()
);
assert_eq!(
get_settings_value::<u32>("hidden.test3").unwrap(),
123456u32
);
reset_default_settings().unwrap();
}
#[test]
fn test_load_settings_from_sample_toml() {
let toml = include_bytes!("../../examples/c2pa.toml");
Settings::new()
.with_toml(std::str::from_utf8(toml).unwrap())
.unwrap();
}
#[test]
fn test_update_from_str_toml() {
let mut settings = Settings::default();
assert!(settings.verify.verify_after_reading);
assert!(settings.verify.verify_trust);
settings
.update_from_str(
r#"
[verify]
verify_after_reading = false
verify_trust = false
"#,
"toml",
)
.unwrap();
assert!(!settings.verify.verify_after_reading);
assert!(!settings.verify.verify_trust);
settings
.update_from_str(
r#"
[verify]
verify_after_reading = true
"#,
"toml",
)
.unwrap();
assert!(settings.verify.verify_after_reading);
assert!(!settings.verify.verify_trust);
}
#[test]
fn test_update_from_str_json() {
let mut settings = Settings::default();
assert!(settings.verify.verify_after_reading);
assert!(settings.verify.verify_trust);
assert!(settings.builder.created_assertion_labels.is_none());
settings
.update_from_str(
r#"
{
"verify": {
"verify_after_reading": false,
"verify_trust": false
},
"builder": {
"created_assertion_labels": ["c2pa.metadata"]
}
}
"#,
"json",
)
.unwrap();
assert!(!settings.verify.verify_after_reading);
assert!(!settings.verify.verify_trust);
assert_eq!(
settings.builder.created_assertion_labels,
Some(vec!["c2pa.metadata".to_string()])
);
settings
.update_from_str(
r#"
{
"verify": {
"verify_after_reading": true
}
}
"#,
"json",
)
.unwrap();
assert!(settings.verify.verify_after_reading);
assert!(!settings.verify.verify_trust);
assert_eq!(
settings.builder.created_assertion_labels,
Some(vec!["c2pa.metadata".to_string()])
);
settings
.update_from_str(
r#"
{
"builder": {
"created_assertion_labels": null
}
}
"#,
"json",
)
.unwrap();
assert!(settings.verify.verify_after_reading);
assert!(!settings.verify.verify_trust);
assert!(settings.builder.created_assertion_labels.is_none());
}
#[test]
fn test_update_from_str_invalid() {
assert!(Settings::default()
.update_from_str("invalid toml { ]", "toml")
.is_err());
assert!(Settings::default()
.update_from_str("{ invalid json }", "json")
.is_err());
assert!(Settings::default().update_from_str("data", "yaml").is_err());
}
#[test]
fn test_instance_with_value() {
let settings = Settings::default()
.with_value("verify.verify_trust", false)
.unwrap()
.with_value("core.merkle_tree_chunk_size_in_kb", 1024i64)
.unwrap()
.with_value("builder.thumbnail.enabled", false)
.unwrap();
assert!(!settings.verify.verify_trust);
assert_eq!(settings.core.merkle_tree_chunk_size_in_kb, Some(1024));
assert!(!settings.builder.thumbnail.enabled);
}
#[test]
fn test_instance_set_value() {
let mut settings = Settings::default();
settings.set_value("verify.verify_trust", true).unwrap();
settings
.set_value("core.merkle_tree_chunk_size_in_kb", 2048i64)
.unwrap();
settings
.set_value("builder.thumbnail.enabled", false)
.unwrap();
assert!(settings.verify.verify_trust);
assert_eq!(settings.core.merkle_tree_chunk_size_in_kb, Some(2048));
assert!(!settings.builder.thumbnail.enabled);
}
#[test]
fn test_instance_get_value() {
let mut settings = Settings::default();
settings.verify.verify_trust = false;
settings.core.merkle_tree_chunk_size_in_kb = Some(512);
let verify_trust: bool = settings.get_value("verify.verify_trust").unwrap();
assert!(!verify_trust);
let chunk_size = settings
.get_value::<Option<usize>>("core.merkle_tree_chunk_size_in_kb")
.unwrap();
assert_eq!(chunk_size, Some(512));
}
#[test]
fn test_instance_methods_with_context() {
use crate::Context;
let settings = Settings::default()
.with_value("verify.verify_after_sign", true)
.unwrap()
.with_value("verify.verify_trust", true)
.unwrap();
let _context = Context::new().with_settings(settings).unwrap();
}
#[test]
fn test_instance_value_error_handling() {
let mut settings = Settings::default();
let result = settings.set_value("verify.verify_trust", "not a bool");
assert!(result.is_err());
let settings = Settings::default();
let result = settings.get_value::<bool>("does.not.exist");
assert!(result.is_err());
let result = Settings::default().with_value("verify.verify_trust", "not a bool");
assert!(result.is_err());
}
#[test]
fn test_test_settings() {
let settings = test_settings();
assert!(
settings.trust.trust_anchors.is_some(),
"test_settings should include trust anchors"
);
assert!(
!settings.trust.trust_anchors.as_ref().unwrap().is_empty(),
"test_settings trust_anchors should not be empty"
);
assert!(
settings.signer.is_some(),
"test_settings should include a signer"
);
if let Some(SignerSettings::Local { alg, .. }) = &settings.signer {
assert!(
matches!(
alg,
SigningAlg::Ps256
| SigningAlg::Es256
| SigningAlg::Es384
| SigningAlg::Es512
| SigningAlg::Ed25519
),
"test_settings should have a valid signing algorithm"
);
} else {
panic!("test_settings should have a Local signer configured");
}
}
#[test]
fn test_builder_pattern() {
let settings = Settings::new()
.with_json(r#"{"verify": {"verify_trust": false}}"#)
.unwrap();
assert!(!settings.verify.verify_trust);
let settings = Settings::new()
.with_json(r#"{"verify": {"verify_after_reading": false}}"#)
.unwrap()
.with_value("verify.verify_trust", true)
.unwrap();
assert!(!settings.verify.verify_after_reading);
assert!(settings.verify.verify_trust);
let settings = Settings::new()
.with_toml(
r#"
[verify]
verify_trust = false
verify_after_sign = false
"#,
)
.unwrap();
assert!(!settings.verify.verify_trust);
assert!(!settings.verify.verify_after_sign);
let original = get_thread_local_settings();
let _settings = Settings::new()
.with_json(r#"{"verify": {"verify_trust": false}}"#)
.unwrap();
let after = get_thread_local_settings();
assert_eq!(
original.verify.verify_trust, after.verify.verify_trust,
"Builder pattern should not modify thread_local settings"
);
}
}