feat(extensions): 切换前验证已签名配置模式
This commit is contained in:
@@ -217,3 +217,12 @@ Core 的独立数据目录目前不等于已授权 Vault。Python 旧笔记写
|
||||
- 这仍是安装事务的存储层:配置只要求对象,完整类型 schema/秘密剥离、依赖计划和用户确认复核、在线撤回、停旧实例、沙箱内迁移、真实健康探测及 UI 尚待编排。不能把注入错误重开测试当成 D-03 断电/磁盘满完整矩阵,也不能开启 extensions capability。
|
||||
|
||||
- 本轮 Rust desktop 全量回归 78 项通过,2 个既有特殊入口 ignored;实际 Sync 子进程中断恢复由父测试执行通过。全量日志 `.build/extension-transaction-rust-full.log`。
|
||||
|
||||
|
||||
## 增量:切换前的签名配置 schema 校验
|
||||
|
||||
- switch_prepared 从重新验证的签名清单读取 configuration_schema,配置不能自带替代 schema。未声明时只接受空对象;声明使用 Draft 2020-12、离线 jsonschema 0.55.0(关闭默认网络/文件解析功能),启用格式检查和线性正则引擎。
|
||||
- schema 与配置各限 64 KiB、4096 节点、24 层;当前拒绝所有引用,包括本地引用,避免 Host 执行递归 schema。高级正则、其他 schema draft 不接受;此限制应在安装 UI 中明确展示,尚无完整配置编辑 UI。
|
||||
- 配置递归拒绝常用秘密字段;writeOnly / x-opennexus-secret 声明禁止持久化对应值,包括条件和组合分支,不能通过 anyOf 的另一个分支绕过。此规则对条件分支采取保守拒绝;秘密应由后续凭据 broker 提供,任意普通字符串不能被自动识别为秘密。
|
||||
- 测试覆盖嵌套类型、范围、未知属性、错误 schema、引用、深度/体积、秘密声明和组合绕过;安装入口拒绝秘密配置后事务表保持为空。26 项扩展回归通过,日志 `.build/extension-config-tests.log`。
|
||||
- 这不替代各扩展类型的完整运行配置契约、用户确认、在线信任与沙箱,也没有完成配置迁移/导入秘密的完整流程。整体生产化继续未完成。
|
||||
|
||||
Generated
+263
@@ -56,7 +56,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
@@ -497,6 +499,12 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.4"
|
||||
@@ -533,6 +541,12 @@ version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.2"
|
||||
@@ -1068,6 +1082,12 @@ version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe"
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
|
||||
|
||||
[[package]]
|
||||
name = "dbus"
|
||||
version = "0.9.12"
|
||||
@@ -1422,6 +1442,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email_address"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.11"
|
||||
@@ -1552,6 +1581,17 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52e0387578e845beb7a1acff126228499f26cb18edf12919cc513bb863266464"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
@@ -1637,6 +1677,17 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
|
||||
dependencies = [
|
||||
"borrow-or-share",
|
||||
"ref-cast",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1685,6 +1736,16 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fraction"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e246562084dde8ebbcc943b261c406ce4f68e5032ec28029a251a47d6a295500"
|
||||
dependencies = [
|
||||
"num",
|
||||
"num-bigint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-set-times"
|
||||
version = "0.20.3"
|
||||
@@ -1920,9 +1981,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2149,6 +2212,11 @@ name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -2740,6 +2808,59 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49e89c7d35509ab5248f4160addc47098d0f2b09221851bf6234a948f28a70c4"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytecount",
|
||||
"data-encoding",
|
||||
"email_address",
|
||||
"fancy-regex",
|
||||
"fraction",
|
||||
"getrandom 0.3.4",
|
||||
"itoa",
|
||||
"jsonschema-regex",
|
||||
"jsonschema-value",
|
||||
"num-cmp",
|
||||
"num-traits",
|
||||
"percent-encoding",
|
||||
"referencing",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"unicode-general-category",
|
||||
"uuid-simd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-regex"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1fb77178bdde7326e2e414526342ad4e26b602ba5372e53440129dc061bc50b"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema-value"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dadb285b774085f3f3e67cb48ecce59e5d92028f19a01fe014083a4fbaf7fea"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bytecount",
|
||||
"fraction",
|
||||
"getrandom 0.3.4",
|
||||
"num-cmp",
|
||||
"num-traits",
|
||||
"serde_json",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "k256"
|
||||
version = "0.13.4"
|
||||
@@ -2958,6 +3079,12 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "micromap"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
@@ -3127,6 +3254,7 @@ dependencies = [
|
||||
"fs2",
|
||||
"hmac",
|
||||
"iota_stronghold",
|
||||
"jsonschema",
|
||||
"rand 0.8.8",
|
||||
"reqwest 0.12.28",
|
||||
"rfd",
|
||||
@@ -3148,12 +3276,81 @@ dependencies = [
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-cmp"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -3408,6 +3605,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -4007,6 +4210,23 @@ dependencies = [
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cc7249156d90d60480d54850c861c37c92f20c6dbb9ae85d98ecca19d81c0b2"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"fluent-uri",
|
||||
"getrandom 0.3.4",
|
||||
"hashbrown 0.17.1",
|
||||
"itoa",
|
||||
"micromap",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
@@ -4822,6 +5042,27 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -5667,6 +5908,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7f66b1c8f8caa2ab31dc6d3f35386f16efdab89668f93411e565ac368908e8f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
@@ -5790,6 +6037,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-simd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@@ -5808,6 +6065,12 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vsimd"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64"
|
||||
|
||||
[[package]]
|
||||
name = "vswhom"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -45,6 +45,7 @@ serde-saphyr = { version = "1.2", default-features = false, features = ["deseria
|
||||
semver = { version = "1", features = ["serde"] }
|
||||
cap-std = "4.0.2"
|
||||
cap-fs-ext = "4.0.2"
|
||||
jsonschema = { version = "0.55", default-features = false }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Storage_FileSystem", "Win32_System_RemoteDesktop", "Win32_UI_WindowsAndMessaging", "Win32_Graphics_Gdi", "Win32_System_LibraryLoader"] }
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
//! Signed, offline configuration schemas. Credentials belong to the vault broker.
|
||||
use crate::workspace::{HostError, Result};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn walk(value: &Value, depth: usize, nodes: &mut usize, schema: bool) -> Result<()> {
|
||||
if depth > 24 || *nodes >= 4096 {
|
||||
return Err(HostError::new("EXTENSION_CONFIG_LIMIT"));
|
||||
}
|
||||
*nodes += 1;
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
for (key, value) in map {
|
||||
if schema && matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef") {
|
||||
// Recursive/unbounded schema execution is not allowed in the Host.
|
||||
return Err(HostError::new("EXTENSION_CONFIG_REFERENCE"));
|
||||
}
|
||||
if !schema
|
||||
&& matches!(
|
||||
key.to_ascii_lowercase().as_str(),
|
||||
"api_key"
|
||||
| "apikey"
|
||||
| "password"
|
||||
| "token"
|
||||
| "secret"
|
||||
| "access_token"
|
||||
| "refresh_token"
|
||||
| "authorization"
|
||||
| "client_secret"
|
||||
| "private_key"
|
||||
)
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_CONFIG_SECRET"));
|
||||
}
|
||||
walk(value, depth + 1, nodes, schema)?;
|
||||
}
|
||||
}
|
||||
Value::Array(items) => {
|
||||
for item in items {
|
||||
walk(item, depth + 1, nodes, schema)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Examine every applicable schema branch; an alternative branch must not
|
||||
// turn a secret declaration back into persistable plaintext.
|
||||
fn reject_secrets(schema: &Value, instance: &Value) -> Result<()> {
|
||||
let Some(map) = schema.as_object() else {
|
||||
return Ok(());
|
||||
};
|
||||
if map.get("writeOnly") == Some(&Value::Bool(true))
|
||||
|| map.get("x-opennexus-secret") == Some(&Value::Bool(true))
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_CONFIG_SECRET"));
|
||||
}
|
||||
for key in ["allOf", "anyOf", "oneOf"] {
|
||||
if let Some(items) = map.get(key).and_then(Value::as_array) {
|
||||
for item in items {
|
||||
reject_secrets(item, instance)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
for key in ["if", "then", "else", "not"] {
|
||||
if let Some(child) = map.get(key) {
|
||||
reject_secrets(child, instance)?;
|
||||
}
|
||||
}
|
||||
if let Some(properties) = instance.as_object() {
|
||||
for (name, value) in properties {
|
||||
if let Some(child) = map.get("unevaluatedProperties") {
|
||||
reject_secrets(child, value)?;
|
||||
}
|
||||
let declared = map.get("properties").and_then(|p| p.get(name));
|
||||
if let Some(child) = declared {
|
||||
reject_secrets(child, value)?;
|
||||
}
|
||||
let mut matched = false;
|
||||
if let Some(patterns) = map.get("patternProperties").and_then(Value::as_object) {
|
||||
for (pattern, child) in patterns {
|
||||
let matcher = jsonschema::options()
|
||||
.offline()
|
||||
.with_pattern_options(jsonschema::PatternOptions::regex())
|
||||
.build(&json!({"pattern":pattern}))
|
||||
.map_err(|_| HostError::new("EXTENSION_CONFIG_SCHEMA"))?;
|
||||
if matcher.is_valid(&Value::String(name.clone())) {
|
||||
matched = true;
|
||||
reject_secrets(child, value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if declared.is_none() && !matched {
|
||||
if let Some(child) = map.get("additionalProperties") {
|
||||
reject_secrets(child, value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(dependent) = map.get("dependentSchemas").and_then(Value::as_object) {
|
||||
for (name, child) in dependent {
|
||||
if properties.contains_key(name) {
|
||||
reject_secrets(child, instance)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(items) = instance.as_array() {
|
||||
let prefix = map.get("prefixItems").and_then(Value::as_array);
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
if let Some(child) = map.get("unevaluatedItems") {
|
||||
reject_secrets(child, item)?;
|
||||
}
|
||||
if let Some(child) = prefix
|
||||
.and_then(|p| p.get(index))
|
||||
.or_else(|| map.get("items"))
|
||||
{
|
||||
reject_secrets(child, item)?;
|
||||
}
|
||||
if let Some(child) = map.get("contains") {
|
||||
reject_secrets(child, item)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Schema is read from the verified manifest, never from the proposed configuration.
|
||||
pub fn validate(manifest: &Value, configuration: &Value) -> Result<()> {
|
||||
if !configuration.is_object() {
|
||||
return Err(HostError::new("EXTENSION_CONFIG_INVALID"));
|
||||
}
|
||||
walk(configuration, 0, &mut 0, false)?;
|
||||
let schema = manifest
|
||||
.get("configuration_schema")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({"type":"object","additionalProperties":false}));
|
||||
walk(&schema, 0, &mut 0, true)?;
|
||||
if schema
|
||||
.get("$schema")
|
||||
.is_some_and(|v| v.as_str() != Some("https://json-schema.org/draft/2020-12/schema"))
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_CONFIG_SCHEMA"));
|
||||
}
|
||||
for value in [&schema, configuration] {
|
||||
if serde_json::to_vec(value)
|
||||
.map_err(|_| HostError::new("EXTENSION_CONFIG_INVALID"))?
|
||||
.len()
|
||||
> 64 * 1024
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_CONFIG_LIMIT"));
|
||||
}
|
||||
}
|
||||
let validator = jsonschema::options()
|
||||
.offline()
|
||||
.with_draft(jsonschema::Draft::Draft202012)
|
||||
.with_pattern_options(jsonschema::PatternOptions::regex())
|
||||
.should_validate_formats(true)
|
||||
.should_ignore_unknown_formats(false)
|
||||
.build(&schema)
|
||||
.map_err(|_| HostError::new("EXTENSION_CONFIG_SCHEMA"))?;
|
||||
if !validator.is_valid(configuration) {
|
||||
return Err(HostError::new("EXTENSION_CONFIG_INVALID"));
|
||||
}
|
||||
reject_secrets(&schema, configuration)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn signed_schema_checks_nested_types_bounds_and_unknown_properties() {
|
||||
let manifest = json!({"configuration_schema":{"type":"object","required":["count"],"additionalProperties":false,
|
||||
"properties":{"count":{"type":"integer","minimum":1,"maximum":5},"labels":{"type":"array","maxItems":2,"items":{"type":"string","pattern":"^[a-z]+$"}}}}});
|
||||
validate(&manifest, &json!({"count":2,"labels":["valid"]})).unwrap();
|
||||
for config in [
|
||||
json!({}),
|
||||
json!({"count":0}),
|
||||
json!({"count":"2"}),
|
||||
json!({"count":2,"extra":true}),
|
||||
json!({"count":2,"labels":["INVALID"]}),
|
||||
] {
|
||||
assert!(validate(&manifest, &config).is_err());
|
||||
}
|
||||
validate(&json!({}), &json!({})).unwrap();
|
||||
assert!(validate(&json!({}), &json!({"undeclared":true})).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn secrets_references_and_excessive_work_are_refused_without_values_in_errors() {
|
||||
let secret = "do-not-print-this-value";
|
||||
for field in ["password", "TOKEN", "api_key", "client_secret"] {
|
||||
let err = validate(
|
||||
&json!({"configuration_schema":true}),
|
||||
&json!({"nested":[{field:secret}]}),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(err.code, "EXTENSION_CONFIG_SECRET");
|
||||
assert!(!format!("{err:?}").contains(secret));
|
||||
}
|
||||
for mark in ["writeOnly", "x-opennexus-secret"] {
|
||||
let schema = json!({"configuration_schema":{"properties":{"custom":{"type":"string",mark:true}}}});
|
||||
assert!(validate(&schema, &json!({"custom":secret})).is_err());
|
||||
validate(&schema, &json!({})).unwrap();
|
||||
}
|
||||
assert!(validate(&json!({"configuration_schema":{"anyOf":[{"properties":{"custom":{"writeOnly":true}}},true]}}),&json!({"custom":secret})).is_err());
|
||||
for reference in ["file:///etc/passwd", "https://example.com/schema", "#"] {
|
||||
assert!(validate(
|
||||
&json!({"configuration_schema":{"$ref":reference}}),
|
||||
&json!({})
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
assert!(validate(&json!({"configuration_schema":{"type":42}}), &json!({})).is_err());
|
||||
let mut deep = json!(true);
|
||||
for _ in 0..25 {
|
||||
deep = json!({"nested":deep});
|
||||
}
|
||||
assert!(validate(&json!({"configuration_schema":true}), &deep).is_err());
|
||||
assert!(validate(
|
||||
&json!({"configuration_schema":true}),
|
||||
&json!({"value":"a".repeat(65536)})
|
||||
)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
@@ -290,7 +290,7 @@ impl ExtensionStore {
|
||||
let public: [u8; 32] = key
|
||||
.try_into()
|
||||
.map_err(|_| HostError::new("EXTENSION_STORE_CORRUPT"))?;
|
||||
let (inventory, _) = release.verify_package(
|
||||
let (inventory, manifest) = release.verify_package(
|
||||
&public,
|
||||
&release.key_id,
|
||||
&release.namespace,
|
||||
@@ -298,6 +298,7 @@ impl ExtensionStore {
|
||||
false,
|
||||
&self.archive(&change.target.package_key)?,
|
||||
)?;
|
||||
crate::extension_config::validate(&manifest, &change.target.configuration)?;
|
||||
let slot = hash(
|
||||
&serde_json::to_vec(&(&vault, &source, &release.namespace, &release.package_id))
|
||||
.unwrap(),
|
||||
@@ -663,7 +664,7 @@ mod tests {
|
||||
package_key: staged.package_key,
|
||||
directory: prepared.directory.clone(),
|
||||
tree_sha256: prepared.tree_sha256,
|
||||
configuration: serde_json::json!({"review":true}),
|
||||
configuration: serde_json::json!({}),
|
||||
},
|
||||
expected_revision: None,
|
||||
}];
|
||||
@@ -674,6 +675,19 @@ mod tests {
|
||||
&changes
|
||||
)
|
||||
.is_err());
|
||||
let mut invalid = changes.clone();
|
||||
invalid[0].target.configuration = serde_json::json!({"password":"never-persist-this"});
|
||||
assert!(store
|
||||
.switch_prepared(&Uuid::new_v4().to_string(), &vault, &invalid)
|
||||
.is_err());
|
||||
assert_eq!(
|
||||
store
|
||||
.db
|
||||
.query_row("SELECT COUNT(*) FROM extension_transactions", [], |r| r
|
||||
.get::<_, i64>(0))
|
||||
.unwrap(),
|
||||
0
|
||||
);
|
||||
let operation = Uuid::new_v4().to_string();
|
||||
store.switch_prepared(&operation, &vault, &changes).unwrap();
|
||||
assert!(store
|
||||
|
||||
@@ -45,3 +45,6 @@ pub mod extension_permit;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mod extension_transaction;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mod extension_config;
|
||||
|
||||
Reference in New Issue
Block a user