commit dfa0de42c16026869b5d51e803595e6b81b31321
parent 123e4de2829ab15fdb2965e5e141e008fff52577
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Fri, 23 Feb 2024 17:38:52 -0500
SDK configuration settings support (infrastructure) (#408)
* Settings support (infrastructure)
* Add parameter validation support
* test hidden settings
* build fixes
* format fix
* Build fix
* Build fixes
* build fix
* return value sugguestion
Diffstat:
6 files changed, 608 insertions(+), 21 deletions(-)
diff --git a/export_schema/Cargo.toml b/export_schema/Cargo.toml
@@ -10,4 +10,4 @@ rust-version = "1.73.0"
anyhow = "1.0.40"
c2pa = { path = "../sdk", features = ["file_io", "json_schema"] }
schemars = "0.8.13"
-serde_json = "1.0.81"
+serde_json = "1.0.114"
diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml
@@ -23,5 +23,5 @@ image = { version = "0.24.7", default-features = false, features = [
memchr = "2.7.1"
nom = "7.1.3"
regex = "1.5.6"
-serde = "1.0.137"
-serde_json = "1.0.81"
+serde = "1.0.197"
+serde_json = "1.0.114"
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -60,7 +60,7 @@ crate-type = ["lib"]
[dependencies]
asn1-rs = "0.5.2"
async-generic = "0.1.2"
-async-trait = { version = "0.1.48" }
+async-trait = { version = "0.1.77" }
atree = "0.5.2"
base64 = "0.21.2"
bcder = "0.7.1"
@@ -72,6 +72,7 @@ chrono = { version = "0.4.27", default-features = false, features = [
"wasmbind",
] }
ciborium = "0.2.0"
+config = "0.14.0"
conv = "0.3.3"
coset = "0.3.1"
extfmt = "0.1.1"
@@ -97,11 +98,11 @@ rasn-pkix = "0.12.4"
rasn = "0.12.4"
riff = "1.0.1"
schemars = { version = "0.8.13", optional = true }
-serde = { version = "1.0.137", features = ["derive"] }
+serde = { version = "1.0.197", features = ["derive"] }
serde_bytes = "0.11.5"
serde_cbor = "0.11.1"
-serde_derive = "1.0.137"
-serde_json = "1.0.81"
+serde_derive = "1.0.197"
+serde_json = "1.0.114"
serde-transcode = "1.1.1"
sha2 = "0.10.2"
tempfile = "3.1.0"
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -140,6 +140,7 @@ pub(crate) mod hashed_uri;
#[allow(dead_code)]
pub(crate) mod jumbf;
pub(crate) mod salt;
+pub(crate) mod settings;
pub(crate) mod status_tracker;
pub(crate) mod store;
pub(crate) mod time_stamp;
diff --git a/sdk/src/openssl/rsa_signer.rs b/sdk/src/openssl/rsa_signer.rs
@@ -51,20 +51,21 @@ impl RsaSigner {
let ocsp_data = self.ocsp_rsp.take();
let next_update = ocsp_data.next_update;
self.ocsp_rsp.set(ocsp_data);
- if now < next_update {
- return;
- }
- #[cfg(feature = "psxxx_ocsp_stapling_experimental")]
- {
- if let Ok(certs) = self.certs() {
- if let Some(ocsp_rsp) = crate::ocsp_utils::fetch_ocsp_response(&certs) {
- self.ocsp_size.set(ocsp_rsp.len());
- let mut validation_log =
- crate::status_tracker::DetailedStatusTracker::default();
- if let Ok(ocsp_data) =
- crate::ocsp_utils::check_ocsp_response(&ocsp_rsp, None, &mut validation_log)
- {
- self.ocsp_rsp.set(ocsp_data);
+ if now > next_update {
+ #[cfg(feature = "psxxx_ocsp_stapling_experimental")]
+ {
+ if let Ok(certs) = self.certs() {
+ if let Some(ocsp_rsp) = crate::ocsp_utils::fetch_ocsp_response(&certs) {
+ self.ocsp_size.set(ocsp_rsp.len());
+ let mut validation_log =
+ crate::status_tracker::DetailedStatusTracker::default();
+ if let Ok(ocsp_data) = crate::ocsp_utils::check_ocsp_response(
+ &ocsp_rsp,
+ None,
+ &mut validation_log,
+ ) {
+ self.ocsp_rsp.set(ocsp_data);
+ }
}
}
}
diff --git a/sdk/src/settings.rs b/sdk/src/settings.rs
@@ -0,0 +1,584 @@
+// Copyright 2024 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.
+
+use std::{path::Path, sync::RwLock};
+
+use config::{Config, FileFormat};
+use lazy_static::lazy_static;
+use serde_derive::{Deserialize, Serialize};
+
+use crate::{Error, Result};
+
+lazy_static! {
+ static ref SETTINGS: RwLock<Config> =
+ RwLock::new(Config::try_from(&Settings::default()).unwrap_or_default());
+}
+
+// trait used to validate user input to make sure user supplied configurations are valid
+pub trait SettingsValidate {
+ // returns error if settings are invalid
+ fn validate(&self) -> Result<()> {
+ Ok(())
+ }
+}
+
+// Settings for trust list feature
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[allow(unused)]
+pub struct Trust {
+ private_anchors: Option<String>,
+ trust_anchors: Option<String>,
+ trust_config: Option<String>,
+ allowed_list: Option<String>,
+}
+
+impl SettingsValidate for Trust {}
+
+// Settings for core C2PA-RS functionality
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[allow(unused)]
+pub struct Core {
+ debug: bool,
+ hash_alg: String,
+ salt_jumbf_boxes: bool,
+ prefer_box_hash: bool,
+ prefer_bmff_merkle_tree: bool,
+ compress_manifests: bool,
+ max_memory_usage: Option<u64>,
+}
+
+impl Default for Core {
+ fn default() -> Self {
+ Self {
+ debug: false,
+ hash_alg: "sha256".into(),
+ salt_jumbf_boxes: true,
+ prefer_box_hash: false,
+ prefer_bmff_merkle_tree: false,
+ compress_manifests: true,
+ max_memory_usage: None,
+ }
+ }
+}
+
+impl SettingsValidate for Core {
+ fn validate(&self) -> Result<()> {
+ match self.hash_alg.as_str() {
+ "sha256" | "sha384" | "sha512" => Ok(()),
+ _ => Err(Error::UnsupportedType),
+ }
+ }
+}
+
+// Settings for verification options
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[allow(unused)]
+pub struct Verify {
+ verify_after_sign: bool,
+ verify_trust: bool,
+ ocsp_fetch: bool,
+ remote_manifest_fetch: bool,
+}
+
+impl Default for Verify {
+ fn default() -> Self {
+ Self {
+ verify_after_sign: false,
+ verify_trust: false,
+ ocsp_fetch: false,
+ remote_manifest_fetch: true,
+ }
+ }
+}
+
+impl SettingsValidate for Verify {}
+
+// Settings for manifest API options
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+#[allow(unused)]
+pub struct Manifest {
+ auto_thumbnail: bool,
+}
+
+impl Default for Manifest {
+ fn default() -> Self {
+ Self {
+ auto_thumbnail: true,
+ }
+ }
+}
+
+impl SettingsValidate for Manifest {}
+
+// Settings configuration for C2PA-RS. Default configuration values
+// are lazy loaded on first use. Values can also be loaded from a configuration
+// file or by setting specific value via code. There is a single configuration
+// setting for the entire C2PA-RS instance.
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
+#[allow(unused)]
+pub struct Settings {
+ trust: Trust,
+ core: Core,
+ verify: Verify,
+ manifest: Manifest,
+}
+
+impl Settings {
+ #[allow(unused)]
+ pub fn from_file<P: AsRef<Path>>(setting_path: P) -> Result<Self> {
+ let ext = setting_path
+ .as_ref()
+ .extension()
+ .ok_or(Error::UnsupportedType)?
+ .to_string_lossy();
+
+ let setting_buf = std::fs::read(&setting_path).map_err(Error::IoError)?;
+ Settings::from_string(&String::from_utf8_lossy(&setting_buf), &ext)
+ }
+
+ #[allow(unused)]
+ pub fn from_string(settings_str: &str, format: &str) -> Result<Self> {
+ let f = match format.to_lowercase().as_str() {
+ "json" => FileFormat::Json,
+ "json5" => FileFormat::Json5,
+ "ini" => FileFormat::Ini,
+ "toml" => FileFormat::Toml,
+ "yaml" => FileFormat::Yaml,
+ "ron" => FileFormat::Ron,
+ _ => 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()))?;
+
+ // blocking write
+ match SETTINGS.write() {
+ Ok(mut c) => {
+ let source = c.clone();
+ let update_config = Config::builder()
+ .add_source(source)
+ .add_source(new_config) // merge overrides, allows for partial changes
+ .build()
+ .map_err(|_e| Error::OtherError("could not update configuration".into()))?;
+
+ // sanity check the values before committing
+ let settings = update_config
+ .clone()
+ .try_deserialize::<Settings>()
+ .map_err(|_e| {
+ Error::BadParam("configuration file contains unrecognized param".into())
+ })?;
+ settings.validate()?;
+
+ // update if valid
+ *c = update_config;
+
+ Ok(settings)
+ }
+ Err(_) => Err(Error::OtherError("could not save settings".into())),
+ }
+ }
+}
+
+impl SettingsValidate for Settings {
+ fn validate(&self) -> Result<()> {
+ self.trust.validate()?;
+ self.core.validate()?;
+ self.trust.validate()?;
+ self.manifest.validate()
+ }
+}
+
+// Get snapshot of the Settings objects, returns None if there is an error
+#[allow(unused)]
+pub(crate) fn get_settings() -> Option<Settings> {
+ match SETTINGS.try_read() {
+ // concurrent read
+ Ok(c) => {
+ let source = c.clone(); // clone required since deserialize consumes object
+ let cloned_config = Config::builder().add_source(source).build();
+
+ if let Ok(cloned_config) = cloned_config {
+ match cloned_config.try_deserialize::<Settings>() {
+ Ok(s) => Some(s),
+ Err(_) => None,
+ }
+ } else {
+ None
+ }
+ }
+ Err(_) => None,
+ }
+}
+
+// Load settings from configuration file
+#[allow(unused)]
+pub fn load_settings<P: AsRef<Path>>(settings_path: P) -> Result<()> {
+ 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)?;
+
+ load_settings_from_str(&String::from_utf8_lossy(&setting_buf), &ext)
+}
+
+// Load settings form string representation of the configuration. Format of configuration must be supplied.
+#[allow(unused)]
+pub fn load_settings_from_str(settings_str: &str, format: &str) -> Result<()> {
+ Settings::from_string(settings_str, format).map(|_| ())
+}
+
+// Save the current configuration to a json file.
+#[allow(unused)]
+pub fn save_settings_as_json<P: AsRef<Path>>(settings_path: P) -> Result<()> {
+ let settings =
+ get_settings().ok_or(Error::OtherError("could not get current settings".into()))?;
+
+ 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)
+}
+
+// Set a Settings value by path reference. The path is nested names of of the Settings objects
+// separated by "." notation. For example "core.hash_alg" would set settings.core.hash_alg value.
+// The nesting can be arbitrarily deep based on the Settings definition.
+#[allow(unused)]
+pub(crate) fn set_settings_value<T: Into<config::Value>>(value_path: &str, value: T) -> Result<()> {
+ match SETTINGS.write() {
+ Ok(mut c) => {
+ let source = c.clone();
+ let update_config = Config::builder()
+ .add_source(source)
+ .set_override(value_path, value);
+
+ if let Ok(updated) = update_config {
+ *c = updated
+ .build()
+ .map_err(|_e| Error::OtherError("could not update configuration".into()))?;
+ Ok(())
+ } else {
+ Err(Error::OtherError("could not save settings".into()))
+ }
+ }
+ Err(_) => Err(Error::OtherError("could not save settings".into())),
+ }
+}
+
+// Get a Settings value by path reference. The path is nested names of of the Settings objects
+// separated by "." notation. For example "core.hash_alg" would get the settings.core.hash_alg value.
+// The nesting can be arbitrarily deep based on the Settings definition.
+#[allow(unused)]
+pub(crate) fn get_settings_value<'de, T: serde::de::Deserialize<'de>>(
+ value_path: &str,
+) -> Result<T> {
+ match SETTINGS.read() {
+ Ok(settings) => settings.get::<T>(value_path).map_err(|_| Error::NotFound),
+ Err(_) => Err(Error::OtherError("could not read setting object".into())),
+ }
+}
+
+// Set settings back to the default values. Current use case is for testing.
+#[allow(unused)]
+pub(crate) fn reset_default_settings() -> Result<()> {
+ if let Ok(default_settings) = Config::try_from(&Settings::default()) {
+ match SETTINGS.write() {
+ Ok(mut current_settings) => {
+ *current_settings = default_settings;
+ Ok(())
+ }
+ Err(_) => Err(Error::OtherError("could not save settings".into())),
+ }
+ } else {
+ Err(Error::OtherError("could not save settings".into()))
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ #![allow(clippy::panic)]
+ #![allow(clippy::unwrap_used)]
+
+ use std::sync::Mutex;
+
+ use super::*;
+
+ // prevent tests from polluting the results of each other because of Rust unit test concurrency
+ static PROTECT: Mutex<u32> = Mutex::new(1); // prevent tests from polluting the results of each other
+
+ #[test]
+ fn test_get_defaults() {
+ let _protect = PROTECT.lock().unwrap();
+
+ let settings = get_settings().unwrap();
+
+ assert_eq!(settings.core, Core::default());
+ assert_eq!(settings.trust, Trust::default());
+ assert_eq!(settings.verify, Verify::default());
+ assert_eq!(settings.manifest, Manifest::default());
+
+ reset_default_settings().unwrap();
+ }
+
+ #[test]
+ fn test_get_val_by_direct_path() {
+ let _protect = PROTECT.lock().unwrap();
+
+ // you can do this for all values but if these sanity checks pass they all should if the path is correct
+ assert_eq!(
+ get_settings_value::<String>("core.hash_alg").unwrap(),
+ Core::default().hash_alg
+ );
+ assert_eq!(
+ get_settings_value::<bool>("manifest.auto_thumbnail").unwrap(),
+ Manifest::default().auto_thumbnail
+ );
+ assert_eq!(
+ get_settings_value::<Option<String>>("trust.private_anchors").unwrap(),
+ Trust::default().private_anchors
+ );
+
+ // test getting full objects
+ assert_eq!(get_settings_value::<Core>("core").unwrap(), Core::default());
+ assert_eq!(
+ get_settings_value::<Verify>("verify").unwrap(),
+ Verify::default()
+ );
+ assert_eq!(
+ get_settings_value::<Manifest>("manifest").unwrap(),
+ Manifest::default()
+ );
+ assert_eq!(
+ get_settings_value::<Trust>("trust").unwrap(),
+ Trust::default()
+ );
+
+ // test implicit deserialization
+ let hash_alg: String = get_settings_value("core.hash_alg").unwrap();
+ let remote_manifest_fetch: bool =
+ get_settings_value("verify.remote_manifest_fetch").unwrap();
+ let auto_thumbnail: bool = get_settings_value("manifest.auto_thumbnail").unwrap();
+ let private_anchors: Option<String> = get_settings_value("trust.private_anchors").unwrap();
+
+ assert_eq!(hash_alg, Core::default().hash_alg);
+ assert_eq!(
+ remote_manifest_fetch,
+ Verify::default().remote_manifest_fetch
+ );
+ assert_eq!(auto_thumbnail, Manifest::default().auto_thumbnail);
+ assert_eq!(private_anchors, Trust::default().private_anchors);
+
+ // test implicit deserialization on objects
+ let core: Core = get_settings_value("core").unwrap();
+ let verify: Verify = get_settings_value("verify").unwrap();
+ let manifest: Manifest = get_settings_value("manifest").unwrap();
+ let trust: Trust = get_settings_value("trust").unwrap();
+
+ assert_eq!(core, Core::default());
+ assert_eq!(verify, Verify::default());
+ assert_eq!(manifest, Manifest::default());
+ assert_eq!(trust, Trust::default());
+
+ reset_default_settings().unwrap();
+ }
+
+ #[test]
+ fn test_set_val_by_direct_path() {
+ let _protect = PROTECT.lock().unwrap();
+
+ // test updating values
+ set_settings_value("core.hash_alg", "sha512").unwrap();
+ set_settings_value("verify.remote_manifest_fetch", false).unwrap();
+ set_settings_value("manifest.auto_thumbnail", false).unwrap();
+ set_settings_value(
+ "trust.private_anchors",
+ Some("path/to/my/content".to_string()),
+ )
+ .unwrap();
+
+ assert_eq!(
+ get_settings_value::<String>("core.hash_alg").unwrap(),
+ "sha512"
+ );
+ assert!(!get_settings_value::<bool>("verify.remote_manifest_fetch").unwrap());
+ assert!(!get_settings_value::<bool>("manifest.auto_thumbnail").unwrap());
+ assert_eq!(
+ get_settings_value::<Option<String>>("trust.private_anchors").unwrap(),
+ Some("path/to/my/content".to_string())
+ );
+
+ // the current config should be different from the defaults
+ assert_ne!(get_settings_value::<Core>("core").unwrap(), Core::default());
+ assert_ne!(
+ get_settings_value::<Verify>("verify").unwrap(),
+ Verify::default()
+ );
+ assert_ne!(
+ get_settings_value::<Manifest>("manifest").unwrap(),
+ Manifest::default()
+ );
+ assert_ne!(
+ get_settings_value::<Trust>("trust").unwrap(),
+ Trust::default()
+ );
+
+ reset_default_settings().unwrap();
+ }
+
+ #[cfg(feature = "file_io")]
+ #[test]
+ fn test_save_load() {
+ let _protect = PROTECT.lock().unwrap();
+
+ let temp_dir = tempfile::tempdir().unwrap();
+ let op = crate::utils::test::temp_dir_path(&temp_dir, "sdk_config.json");
+
+ save_settings_as_json(&op).unwrap();
+
+ load_settings(&op).unwrap();
+ let settings = get_settings().unwrap();
+
+ assert_eq!(settings, Settings::default());
+
+ reset_default_settings().unwrap();
+ }
+
+ #[cfg(feature = "file_io")]
+ #[test]
+ fn test_save_load_from_string() {
+ let _protect = PROTECT.lock().unwrap();
+
+ let temp_dir = tempfile::tempdir().unwrap();
+ let op = crate::utils::test::temp_dir_path(&temp_dir, "sdk_config.json");
+
+ save_settings_as_json(&op).unwrap();
+
+ let setting_buf = std::fs::read(&op).unwrap();
+
+ load_settings_from_str(&String::from_utf8_lossy(&setting_buf), "json").unwrap();
+ let settings = get_settings().unwrap();
+
+ assert_eq!(settings, Settings::default());
+
+ reset_default_settings().unwrap();
+ }
+
+ #[test]
+ fn test_partial_loading() {
+ let _protect = PROTECT.lock().unwrap();
+
+ // we support just changing the fields you are interested in changing
+ // here are two examples of incomplete structures only overriding specific
+ // fields
+
+ let modified_trust = r#"{
+ "trust": {
+ "private_anchors": "this is a test",
+ "allowed_list": "another test"
+ }
+ }"#;
+
+ let modified_core = r#"{
+ "core": {
+ "debug": true,
+ "hash_alg": "sha512",
+ "max_memory_usage": 123456
+ }
+ }"#;
+
+ load_settings_from_str(modified_trust, "json").unwrap();
+ load_settings_from_str(modified_core, "json").unwrap();
+
+ // see if updated values match
+ assert_eq!(
+ get_settings_value::<Option<String>>("trust.private_anchors").unwrap(),
+ Some("this is a test".to_string())
+ );
+ assert_eq!(
+ get_settings_value::<Option<String>>("trust.allowed_list").unwrap(),
+ Some("another test".to_string())
+ );
+ assert!(get_settings_value::<bool>("core.debug").unwrap());
+ assert_eq!(
+ get_settings_value::<String>("core.hash_alg").unwrap(),
+ "sha512".to_string()
+ );
+ assert_eq!(
+ get_settings_value::<u32>("core.max_memory_usage").unwrap(),
+ 123456u32
+ );
+
+ // check a few defaults to make sure they are still there
+ assert_eq!(
+ get_settings_value::<bool>("manifest.auto_thumbnail").unwrap(),
+ Manifest::default().auto_thumbnail
+ );
+ assert_eq!(
+ get_settings_value::<Option<String>>("trust.trust_anchors").unwrap(),
+ Trust::default().trust_anchors
+ );
+ assert_eq!(
+ get_settings_value::<bool>("core.salt_jumbf_boxes").unwrap(),
+ Core::default().salt_jumbf_boxes
+ );
+
+ reset_default_settings().unwrap();
+ }
+
+ #[test]
+ fn test_bad_setting() {
+ let _protect = PROTECT.lock().unwrap();
+
+ let modified_core = r#"{
+ "core": {
+ "debug": true,
+ "hash_alg": "sha1000000",
+ "max_memory_usage": 123456
+ }
+ }"#;
+
+ assert!(load_settings_from_str(modified_core, "json").is_err());
+
+ reset_default_settings().unwrap();
+ }
+ #[test]
+ fn test_hidden_setting() {
+ let _protect = PROTECT.lock().unwrap();
+
+ let secret = r#"{
+ "hidden": {
+ "test1": true,
+ "test2": "hello world",
+ "test3": 123456
+ }
+ }"#;
+
+ load_settings_from_str(secret, "json").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();
+ }
+}