diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index bca66941d..8ef34de7d 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -96,6 +96,17 @@ exclude_re = [ # `true` there refuses the same values with the same error. The guard stays # so the model says what it holds without leaning on the writer behind it. 'stack-encrypt/src/dynamic/value_frame\.rs:\d+:\d+: replace match guard logical\.admits\(&value\) with true in model$', + # A serde visitor's `expecting` writes only the "expected ..." half of a + # serde error's message, and no caller of these three reads that message: + # a context, statistics and a plan's root are each refused with one fixed + # `Reason` (`ContextKind`, `NotStats`, `NotAnObject`), and the plan's + # field-level reasons are read from the error's path, so writing nothing + # refuses the same input with the same error. + 'stack-encrypt/src/dynamic/(context|query|record)\.rs:\d+:\d+: replace for ContextVisitor|serde::Deserialize<\x27de> for Confidence>::deserialize:: for V|Deserialize<\x27de> for PlanWire>::deserialize:: for V)>::expecting -> fmt::Result with Ok\(Default::default\(\)\)$', + # The same for the visitor that reads a refused plan's keys back + # (`entry_key`): its error is discarded (`.ok()`), so its message is never + # read at all. + 'stack-encrypt/src/dynamic/record\.rs:\d+:\d+: replace entry_key:: for Keys>::deserialize:: for V>::expecting -> fmt::Result with Ok\(Default::default\(\)\)$', ] # Headroom over the measured baseline before a slow-but-correct mutant is diff --git a/Cargo.lock b/Cargo.lock index bac0275fe..b8e275d66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3236,8 +3236,8 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "vitaminc 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc 0.6.1", + "vitaminc-protected 0.6.1", "web-time", "zeroize", "zerokms-protocol", @@ -3259,7 +3259,7 @@ dependencies = [ "tempfile", "tokio", "url", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] @@ -3298,12 +3298,13 @@ dependencies = [ "tokio", "trybuild", "uuid", - "vitaminc-aead 0.6.0", - "vitaminc-encrypt 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-aead-frame", + "vitaminc-encrypt 0.6.1", "vitaminc-hmac", "vitaminc-normalize", "vitaminc-prf", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", "zerokms-protocol", ] @@ -3328,9 +3329,9 @@ dependencies = [ "proptest", "serde_json", "thiserror 1.0.69", - "vitaminc-aead 0.6.0", + "vitaminc-aead 0.6.1", "vitaminc-aead-frame", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] @@ -3362,8 +3363,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc 0.6.1", + "vitaminc-protected 0.6.1", "zeroize", "zerokms-protocol", ] @@ -4128,16 +4129,16 @@ dependencies = [ [[package]] name = "vitaminc" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" +checksum = "ac5c15b6c25efcb9a6111f7108ab8101947a7bf7e9af3ff6d6a73358a1ace0da" dependencies = [ - "vitaminc-aead 0.6.0", - "vitaminc-context 0.6.0", - "vitaminc-encrypt 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", - "vitaminc-traits 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-context 0.6.1", + "vitaminc-encrypt 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", + "vitaminc-traits 0.6.1", ] [[package]] @@ -4157,18 +4158,18 @@ dependencies = [ [[package]] name = "vitaminc-aead" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" +checksum = "71b8c5c4ced92545dce5fd7cf04581d08210def174cc81475cf227fa2506c5b7" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.18", - "vitaminc-aead-derive 0.6.0", - "vitaminc-context 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-aead-derive 0.6.1", + "vitaminc-context 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] @@ -4185,9 +4186,9 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" +checksum = "4c546497a972d0f0498f2f8d5f537c514bad82df8662951c63d0c80023808900" dependencies = [ "proc-macro2", "quote", @@ -4196,11 +4197,13 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c253055230469c4af4791e902c41ce0ce55069c512249b33cf041dd5c58c517" +checksum = "e35992a269bcaa5b201f1938e54bfbf167bfa6fe975d83cbdbdb54c2a363c5ef" dependencies = [ - "vitaminc-aead 0.6.0", + "serde", + "vitaminc-aead 0.6.1", + "zeroize", ] [[package]] @@ -4215,12 +4218,12 @@ dependencies = [ [[package]] name = "vitaminc-context" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" +checksum = "fa4c4bc2bd9811253eb427657565a45bd4826e2379e93ee1c4d00c808a769882" dependencies = [ "mutants", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", ] [[package]] @@ -4239,56 +4242,56 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +checksum = "f0923694602855b33a296d9af473e282e8096c1cfe1b30b8695d3b6651717fc5" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] [[package]] name = "vitaminc-hmac" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "393354b2348eabce0420626fdc7cf70ac779870f82abb566fb491ea19eeea066" +checksum = "bd361f626e95cbe770b9d012e4a86c43cfefe4aefb3d5b3ed9339a83085bdc53" dependencies = [ "hmac", "sha2 0.11.0", "vitaminc-prf", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] [[package]] name = "vitaminc-normalize" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda6ec7f852ba8cd7e0ba6e8e7eaa3045e6fb866007c6d7ad6128833d8980f0c" +checksum = "214b2a9d085924c3bc3c91f3ba74238e8848bf54990e5f48f170c71fbacea07a" dependencies = [ "thiserror 2.0.18", "unicode-general-category", "unicode-normalization", - "vitaminc-aead 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-protected 0.6.1", ] [[package]] name = "vitaminc-prf" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11de28e524ab1deecedbb023bd328916b6c023e2f52156521ac1df75a689d911" +checksum = "c2715cca25006b4656223bc932ab4fd7f70d387aa4ec63a4e2ef33aa8cd2b3a5" dependencies = [ "mutants", "thiserror 2.0.18", - "vitaminc-aead 0.6.0", - "vitaminc-context 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-context 0.6.1", "vitaminc-normalize", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", ] [[package]] @@ -4310,9 +4313,9 @@ dependencies = [ [[package]] name = "vitaminc-protected" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" +checksum = "b1bb76dd473f5aa89b844f92018ebe3041deb8e5390e7061cca4283d959ff855" dependencies = [ "bitvec", "digest 0.11.3", @@ -4321,7 +4324,7 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.18", - "vitaminc-protected-derive 0.6.0", + "vitaminc-protected-derive 0.6.1", "zeroize", ] @@ -4338,9 +4341,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" +checksum = "e7478ed160c019059746e75fc613563dcdcd5266d155a34601d29377a1c4afc3" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4365,16 +4368,16 @@ dependencies = [ [[package]] name = "vitaminc-random" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" +checksum = "2b477af7c01c3d9ef2647e46952ad3df7f246b3f806b1d4009ee83beecec613a" dependencies = [ "chacha20", "getrandom 0.4.2", "rand 0.10.1", "thiserror 2.0.18", - "vitaminc-protected 0.6.0", - "vitaminc-random-derives 0.6.0", + "vitaminc-protected 0.6.1", + "vitaminc-random-derives 0.6.1", "zeroize", ] @@ -4391,9 +4394,9 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" +checksum = "ab5a0de8da428c6789dc067a578a0462e411099721d616736cf29e1622230c35" dependencies = [ "proc-macro2", "quote", @@ -4418,17 +4421,17 @@ dependencies = [ [[package]] name = "vitaminc-traits" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" +checksum = "a900f0edcacd49c8557ab54722175869fec66926054a6fbdc53a05069db3c922" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.18", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 22e8a3d21..0629e45a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,16 +120,15 @@ web-time = "1.1" zeroize = { version = "1.8.1", features = ["derive"] } # This is needed because lock_api 0.4.6 was breaking things and the branch forces the use of 0.4.12 temp-env = { git = "https://github.com/cipherstash/temp-env", branch = "main" } -vitaminc = { version = "0.6.0", features = ["random", "protected", "encrypt"] } -vitaminc-aead = "0.6.0" +vitaminc = { version = "0.6.1", features = ["random", "protected", "encrypt"] } +vitaminc-aead = "0.6.1" # The transport frame and ciphertext codec every language binding carries # across its FFI boundary. Optional in stack-encrypt (the `dynamic` feature). -vitaminc-aead-frame = "0.6.0" -vitaminc-encrypt = "0.6.0" -vitaminc-hmac = "0.6.0" -# Not on crates.io yet: the `[patch.crates-io]` block below supplies it. +vitaminc-aead-frame = "0.6.1" +vitaminc-encrypt = "0.6.1" +vitaminc-hmac = "0.6.1" # Normalized equality terms (Unicode 16 NFC text, one integer encoding for # every width), which `vitaminc-prf`'s `normalize` feature derives over. -vitaminc-normalize = "0.6.0" -vitaminc-prf = { version = "0.6.0", features = ["normalize"] } -vitaminc-protected = "0.6.0" +vitaminc-normalize = "0.6.1" +vitaminc-prf = { version = "0.6.1", features = ["normalize"] } +vitaminc-protected = "0.6.1" diff --git a/languages/golang/auth/guest/Cargo.lock b/languages/golang/auth/guest/Cargo.lock index 58ae17c2a..9073d2b3f 100644 --- a/languages/golang/auth/guest/Cargo.lock +++ b/languages/golang/auth/guest/Cargo.lock @@ -1847,8 +1847,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc 0.6.1", + "vitaminc-protected 0.6.1", "web-time", "zeroize", "zerokms-protocol", @@ -1867,7 +1867,7 @@ dependencies = [ "stack-profile", "tempfile", "url", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] @@ -1878,9 +1878,9 @@ dependencies = [ "miette", "serde_json", "thiserror 1.0.69", - "vitaminc-aead 0.6.0", + "vitaminc-aead 0.6.1", "vitaminc-aead-frame", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] @@ -2299,16 +2299,16 @@ dependencies = [ [[package]] name = "vitaminc" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" +checksum = "ac5c15b6c25efcb9a6111f7108ab8101947a7bf7e9af3ff6d6a73358a1ace0da" dependencies = [ - "vitaminc-aead 0.6.0", - "vitaminc-context 0.6.0", - "vitaminc-encrypt 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", - "vitaminc-traits 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-context 0.6.1", + "vitaminc-encrypt 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", + "vitaminc-traits 0.6.1", ] [[package]] @@ -2328,18 +2328,18 @@ dependencies = [ [[package]] name = "vitaminc-aead" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" +checksum = "71b8c5c4ced92545dce5fd7cf04581d08210def174cc81475cf227fa2506c5b7" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.20", - "vitaminc-aead-derive 0.6.0", - "vitaminc-context 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-aead-derive 0.6.1", + "vitaminc-context 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] @@ -2356,9 +2356,9 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" +checksum = "4c546497a972d0f0498f2f8d5f537c514bad82df8662951c63d0c80023808900" dependencies = [ "proc-macro2", "quote", @@ -2367,11 +2367,11 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c253055230469c4af4791e902c41ce0ce55069c512249b33cf041dd5c58c517" +checksum = "e35992a269bcaa5b201f1938e54bfbf167bfa6fe975d83cbdbdb54c2a363c5ef" dependencies = [ - "vitaminc-aead 0.6.0", + "vitaminc-aead 0.6.1", ] [[package]] @@ -2386,12 +2386,12 @@ dependencies = [ [[package]] name = "vitaminc-context" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" +checksum = "fa4c4bc2bd9811253eb427657565a45bd4826e2379e93ee1c4d00c808a769882" dependencies = [ "mutants", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", ] [[package]] @@ -2410,15 +2410,15 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +checksum = "f0923694602855b33a296d9af473e282e8096c1cfe1b30b8695d3b6651717fc5" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] @@ -2441,9 +2441,9 @@ dependencies = [ [[package]] name = "vitaminc-protected" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" +checksum = "b1bb76dd473f5aa89b844f92018ebe3041deb8e5390e7061cca4283d959ff855" dependencies = [ "bitvec", "digest 0.11.3", @@ -2452,7 +2452,7 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.20", - "vitaminc-protected-derive 0.6.0", + "vitaminc-protected-derive 0.6.1", "zeroize", ] @@ -2469,9 +2469,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" +checksum = "e7478ed160c019059746e75fc613563dcdcd5266d155a34601d29377a1c4afc3" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2496,16 +2496,16 @@ dependencies = [ [[package]] name = "vitaminc-random" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" +checksum = "2b477af7c01c3d9ef2647e46952ad3df7f246b3f806b1d4009ee83beecec613a" dependencies = [ "chacha20", "getrandom 0.4.3", "rand 0.10.3", "thiserror 2.0.20", - "vitaminc-protected 0.6.0", - "vitaminc-random-derives 0.6.0", + "vitaminc-protected 0.6.1", + "vitaminc-random-derives 0.6.1", "zeroize", ] @@ -2522,9 +2522,9 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" +checksum = "ab5a0de8da428c6789dc067a578a0462e411099721d616736cf29e1622230c35" dependencies = [ "proc-macro2", "quote", @@ -2549,17 +2549,17 @@ dependencies = [ [[package]] name = "vitaminc-traits" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" +checksum = "a900f0edcacd49c8557ab54722175869fec66926054a6fbdc53a05069db3c922" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.20", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 189086c89..c9d6985fa 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -2062,8 +2062,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc 0.6.1", + "vitaminc-protected 0.6.1", "web-time", "zeroize", "zerokms-protocol", @@ -2082,12 +2082,13 @@ dependencies = [ "stack-kms", "thiserror 1.0.69", "uuid", - "vitaminc-aead 0.6.0", - "vitaminc-encrypt 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-aead-frame", + "vitaminc-encrypt 0.6.1", "vitaminc-hmac", "vitaminc-normalize", "vitaminc-prf", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] @@ -2117,7 +2118,7 @@ dependencies = [ "stack-kms", "uuid", "vitaminc-aead-frame", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", "zerokms-protocol", ] @@ -2129,9 +2130,9 @@ dependencies = [ "miette", "serde_json", "thiserror 1.0.69", - "vitaminc-aead 0.6.0", + "vitaminc-aead 0.6.1", "vitaminc-aead-frame", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] @@ -2157,8 +2158,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc 0.6.1", + "vitaminc-protected 0.6.1", "zeroize", "zerokms-protocol", ] @@ -2627,16 +2628,16 @@ dependencies = [ [[package]] name = "vitaminc" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" +checksum = "ac5c15b6c25efcb9a6111f7108ab8101947a7bf7e9af3ff6d6a73358a1ace0da" dependencies = [ - "vitaminc-aead 0.6.0", - "vitaminc-context 0.6.0", - "vitaminc-encrypt 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", - "vitaminc-traits 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-context 0.6.1", + "vitaminc-encrypt 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", + "vitaminc-traits 0.6.1", ] [[package]] @@ -2656,18 +2657,18 @@ dependencies = [ [[package]] name = "vitaminc-aead" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" +checksum = "71b8c5c4ced92545dce5fd7cf04581d08210def174cc81475cf227fa2506c5b7" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.20", - "vitaminc-aead-derive 0.6.0", - "vitaminc-context 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-aead-derive 0.6.1", + "vitaminc-context 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] @@ -2684,9 +2685,9 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" +checksum = "4c546497a972d0f0498f2f8d5f537c514bad82df8662951c63d0c80023808900" dependencies = [ "proc-macro2", "quote", @@ -2695,11 +2696,13 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c253055230469c4af4791e902c41ce0ce55069c512249b33cf041dd5c58c517" +checksum = "e35992a269bcaa5b201f1938e54bfbf167bfa6fe975d83cbdbdb54c2a363c5ef" dependencies = [ - "vitaminc-aead 0.6.0", + "serde", + "vitaminc-aead 0.6.1", + "zeroize", ] [[package]] @@ -2714,12 +2717,12 @@ dependencies = [ [[package]] name = "vitaminc-context" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" +checksum = "fa4c4bc2bd9811253eb427657565a45bd4826e2379e93ee1c4d00c808a769882" dependencies = [ "mutants", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", ] [[package]] @@ -2738,56 +2741,56 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +checksum = "f0923694602855b33a296d9af473e282e8096c1cfe1b30b8695d3b6651717fc5" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead 0.6.0", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] [[package]] name = "vitaminc-hmac" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "393354b2348eabce0420626fdc7cf70ac779870f82abb566fb491ea19eeea066" +checksum = "bd361f626e95cbe770b9d012e4a86c43cfefe4aefb3d5b3ed9339a83085bdc53" dependencies = [ "hmac", "sha2 0.11.0", "vitaminc-prf", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", "zeroize", ] [[package]] name = "vitaminc-normalize" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda6ec7f852ba8cd7e0ba6e8e7eaa3045e6fb866007c6d7ad6128833d8980f0c" +checksum = "214b2a9d085924c3bc3c91f3ba74238e8848bf54990e5f48f170c71fbacea07a" dependencies = [ "thiserror 2.0.20", "unicode-general-category", "unicode-normalization", - "vitaminc-aead 0.6.0", - "vitaminc-protected 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-protected 0.6.1", ] [[package]] name = "vitaminc-prf" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11de28e524ab1deecedbb023bd328916b6c023e2f52156521ac1df75a689d911" +checksum = "c2715cca25006b4656223bc932ab4fd7f70d387aa4ec63a4e2ef33aa8cd2b3a5" dependencies = [ "mutants", "thiserror 2.0.20", - "vitaminc-aead 0.6.0", - "vitaminc-context 0.6.0", + "vitaminc-aead 0.6.1", + "vitaminc-context 0.6.1", "vitaminc-normalize", - "vitaminc-protected 0.6.0", + "vitaminc-protected 0.6.1", ] [[package]] @@ -2809,9 +2812,9 @@ dependencies = [ [[package]] name = "vitaminc-protected" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" +checksum = "b1bb76dd473f5aa89b844f92018ebe3041deb8e5390e7061cca4283d959ff855" dependencies = [ "bitvec", "digest 0.11.3", @@ -2820,7 +2823,7 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.20", - "vitaminc-protected-derive 0.6.0", + "vitaminc-protected-derive 0.6.1", "zeroize", ] @@ -2837,9 +2840,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" +checksum = "e7478ed160c019059746e75fc613563dcdcd5266d155a34601d29377a1c4afc3" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2864,16 +2867,16 @@ dependencies = [ [[package]] name = "vitaminc-random" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" +checksum = "2b477af7c01c3d9ef2647e46952ad3df7f246b3f806b1d4009ee83beecec613a" dependencies = [ "chacha20", "getrandom 0.4.3", "rand 0.10.2", "thiserror 2.0.20", - "vitaminc-protected 0.6.0", - "vitaminc-random-derives 0.6.0", + "vitaminc-protected 0.6.1", + "vitaminc-random-derives 0.6.1", "zeroize", ] @@ -2890,9 +2893,9 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" +checksum = "ab5a0de8da428c6789dc067a578a0462e411099721d616736cf29e1622230c35" dependencies = [ "proc-macro2", "quote", @@ -2917,17 +2920,17 @@ dependencies = [ [[package]] name = "vitaminc-traits" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" +checksum = "a900f0edcacd49c8557ab54722175869fec66926054a6fbdc53a05069db3c922" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.20", - "vitaminc-protected 0.6.0", - "vitaminc-random 0.6.0", + "vitaminc-protected 0.6.1", + "vitaminc-random 0.6.1", "zeroize", ] diff --git a/languages/golang/encrypt/guest/Cargo.toml b/languages/golang/encrypt/guest/Cargo.toml index 08acd7086..64626710b 100644 --- a/languages/golang/encrypt/guest/Cargo.toml +++ b/languages/golang/encrypt/guest/Cargo.toml @@ -47,7 +47,7 @@ eql = ["dep:eql-bindings"] # HTTP comes from the host (see `wasm:wasi-check` in the suite root). stack-auth = { path = "../../../../packages/stack-auth", default-features = false } # `dynamic`: the runtime value model this guest speaks — contexts, index -# terms and record plans over `FfiValue`, shared with every other binding. +# terms and record plans read from frames, shared with every other binding. stack-encrypt = { path = "../../../../packages/stack-encrypt", default-features = false, features = ["dynamic"] } # The guest ABI every guest under bindings/go shares: the allocator and # buffer registry (and with them the `se_alloc`/`se_dealloc` exports), the @@ -66,8 +66,8 @@ zerokms-protocol = "=0.12.31" # against (see the comment in packages/stack-encrypt/Cargo.toml). A # different version here fails at the `FfiValue: Decrypt` bound, since the # aead crate would be duplicated. -vitaminc-aead-frame = "0.6.0" -vitaminc-protected = "0.6.0" +vitaminc-aead-frame = { version = "0.6.1", features = ["serde"] } +vitaminc-protected = "0.6.1" futures = { version = "0.3", default-features = false, features = ["executor"] } serde = "1" diff --git a/languages/golang/encrypt/guest/src/abi.rs b/languages/golang/encrypt/guest/src/abi.rs index 76be06f9b..1cd4ce19a 100644 --- a/languages/golang/encrypt/guest/src/abi.rs +++ b/languages/golang/encrypt/guest/src/abi.rs @@ -75,7 +75,6 @@ use std::cell::{Cell, RefCell}; use std::panic::{catch_unwind, AssertUnwindSafe}; use futures::executor::block_on; -use stack_encrypt::dynamic::FfiValue; use stack_encrypt::{KeysetCipher, StackCipher}; use stack_guest_abi::abi::{export, input, take_plaintext, wipe_input}; use stack_guest_abi::buffers; @@ -111,12 +110,6 @@ thread_local! { static SHUT_DOWN: Cell = const { Cell::new(false) }; } -/// Decode one codec-encoded input. -fn decode(bytes: &[u8]) -> Result { - FfiValue::from_frame(bytes) - .map_err(|_| malformed("an input is not a value in the transport frame")) -} - /// Run `f` with the instance's cipher, or report `STATUS_STATE` when there /// is none (never initialised, or shut down). fn with_cipher(f: impl FnOnce(&GuestCipher) -> Result) -> Result { @@ -139,7 +132,7 @@ fn with_keyset( opts: &[u8], f: impl FnOnce(&KeysetCipher<'_, GuestKms>) -> Result, ) -> Result { - let options = parse_options(decode(opts)?, Side::Mint)?; + let options = parse_options(opts, Side::Mint)?; with_cipher(|cipher| { let keyset = block_on(options.keyset.resolve(cipher))?; f(&keyset) @@ -152,14 +145,14 @@ fn with_scope( opts: &[u8], f: impl FnOnce(Scope<'_, GuestKms>) -> Result, ) -> Result { - let options = parse_options(decode(opts)?, Side::Open)?; + let options = parse_options(opts, Side::Open)?; with_cipher(|cipher| { let scope = block_on(scope_for(cipher, &options.keyset))?; f(scope) }) } -/// Initialise the instance's cipher from an FFI-codec-encoded config object +/// Initialise the instance's cipher from a config frame /// (see [`crate::config`]). Performs one `load-keyset` round trip through /// the host transport for the default keyset, and returns that keyset's id /// (16 raw UUID bytes) as the output buffer. The raw config buffer — which @@ -182,27 +175,36 @@ pub unsafe extern "C" fn se_cipher_init(cfg_ptr: *mut u8, cfg_len: u32) -> u64 { export(|| { // The pointer/length pair is validated first and on its own: a pair // that fails here returns before anything touches the range, which - // is `wipe_input`'s precondition. Only a validated buffer is decoded - // and, whatever the decode outcome, wiped. + // is `wipe_input`'s precondition. Only a validated buffer is parsed + // and, whatever the outcome, wiped. // SAFETY: host-owned ranges the export was handed; the borrows end // before it returns and before any wipe of an overlapping range. let bytes = unsafe { input(cfg_ptr, cfg_len)? }; - let decoded = decode(bytes); - // The borrow of the raw buffer ends with `decoded` owned; wipe the - // buffer now — it holds the client-key hex — before parsing (and - // before the init round trip), whatever the decode outcome. + // The parser borrows the buffer's strings and owns only what it + // builds from them, so the borrow ends here; wipe the buffer now — + // it holds the client key — before the init round trip, whatever + // the parse outcome. + let config = parse_init(bytes); unsafe { wipe_input(cfg_ptr, cfg_len) }; - cipher_init(decoded?) + cipher_init(config?) }) } #[cfg(not(feature = "deterministic-kms"))] -fn cipher_init(decoded: FfiValue) -> Result, u32> { +use crate::config::{parse_config as parse_init, CipherConfig as InitConfig}; +#[cfg(feature = "deterministic-kms")] +use crate::deterministic::Seed as InitConfig; +#[cfg(feature = "deterministic-kms")] +fn parse_init(bytes: &[u8]) -> Result { + InitConfig::parse(bytes) +} + +#[cfg(not(feature = "deterministic-kms"))] +fn cipher_init(config: InitConfig) -> Result, u32> { use crate::host::{HostTokenStrategy, WasiHostConnection}; use crate::status::STATUS_KMS_TRANSPORT; use stack_kms::{ClientOpts, StackKms}; - let config = crate::config::parse_config(decoded).map_err(|e| malformed(e.describe()))?; if SHUT_DOWN.with(Cell::get) || CIPHER.with(|c| c.borrow().is_some()) { return Err(init_out_of_order()); } @@ -246,22 +248,11 @@ fn cipher_init(decoded: FfiValue) -> Result, u32> { /// endpoint, no token, no network. A test artefact, never shipped; see /// [`crate::deterministic`]. #[cfg(feature = "deterministic-kms")] -fn cipher_init(decoded: FfiValue) -> Result, u32> { - use vitaminc_protected::Controlled; - - let not_a_seed = || malformed("the deterministic build's config is a 32-byte seed"); - let FfiValue::Bytes(seed) = decoded else { - return Err(not_a_seed()); - }; - let seed: [u8; 32] = seed - .risky_ref() - .as_slice() - .try_into() - .map_err(|_| not_a_seed())?; +fn cipher_init(seed: InitConfig) -> Result, u32> { if SHUT_DOWN.with(Cell::get) || CIPHER.with(|c| c.borrow().is_some()) { return Err(init_out_of_order()); } - let builder = StackCipher::builder().kms(crate::deterministic::DeterministicSource::new(seed)); + let builder = StackCipher::builder().kms(seed.source()); install(block_on(builder.init()).map_err(|e| crate::status::fail_error(&e))?) } @@ -319,7 +310,7 @@ pub unsafe extern "C" fn se_keyset(sel_ptr: *const u8, sel_len: u32) -> u64 { export(|| { // SAFETY: host-owned ranges the export was handed; the borrows end // before it returns and before any wipe of an overlapping range. - let selector = parse_selector(decode(unsafe { input(sel_ptr, sel_len)? })?)?; + let selector = parse_selector(unsafe { input(sel_ptr, sel_len)? })?; if selector == KeysetSelector::Any { return Err(malformed(r#"{"any"} is not a keyset"#)); } @@ -440,7 +431,7 @@ pub unsafe extern "C" fn se_decrypt_record( let record = unsafe { input(rec_ptr, rec_len)? }; let plan = unsafe { input(plan_ptr, plan_len)? }; let opts = unsafe { input(opt_ptr, opt_len)? }; - let expected = parse_options(decode(opts)?, Side::Open)?.context; + let expected = parse_options(opts, Side::Open)?.context; ops::validate::record_tree(record, plan, expected.as_ref())?; with_scope(opts, |scope| { block_on(ops::decrypt_record(scope, record, plan, expected)) diff --git a/languages/golang/encrypt/guest/src/config.rs b/languages/golang/encrypt/guest/src/config.rs index e31f1e7d7..a08f576ce 100644 --- a/languages/golang/encrypt/guest/src/config.rs +++ b/languages/golang/encrypt/guest/src/config.rs @@ -1,8 +1,8 @@ //! Parsing of the `se_cipher_init` configuration. //! -//! The config crosses the boundary as one FFI-codec-encoded -//! [`FfiValue::Object`] — the same codec every other entry point uses, so -//! there is no second config format. Recognised keys (all string values): +//! The config crosses the boundary as one frame object, read by serde +//! (`vitaminc_aead_frame::serde`), the same format as every other control +//! message. Recognised keys (all string values): //! //! | key | required | meaning | //! |---------------|----------|---------| @@ -17,60 +17,26 @@ //! Every other keyset is selected per call (see [`crate::options`]). Unknown //! keys are rejected — a typo'd optional key must not silently fall back to a //! default — so a host still sending `keyset` is told so rather than quietly -//! encrypting somewhere else. +//! encrypting somewhere else. A key given twice is refused by the frame. //! -//! The parsed [`FfiValue`] holds the client-key hex inside -//! `Protected`, which wipes on drop; the raw config *buffer* is wiped by the -//! ABI layer immediately after decoding (see [`crate::abi`]). +//! Every string is borrowed from the config buffer, so the only copy of the +//! client key this parser makes is the [`ClientKey`] it builds. The ABI +//! layer wipes the buffer as soon as [`parse_config`] returns (see +//! [`crate::abi`]). +//! +//! A refusal is [`STATUS_ENCODING`](crate::status::STATUS_ENCODING), with a +//! message that names the key at fault and never a value, because the +//! config holds the client key. An unrecognised key is not named either: +//! if a value is pasted where a key should be, the unrecognised key *is* +//! that value. use std::num::NonZeroUsize; -use stack_encrypt::dynamic::FfiValue; +use serde::{Deserialize, Deserializer}; +use stack_guest_abi::last_error::malformed; use stack_kms::{ClientKey, ZeroKmsEndpoint}; use uuid::Uuid; -use zeroize::Zeroizing; - -/// A parse failure, carrying which key was at fault. Maps to -/// `STATUS_ENCODING` at the ABI, and [`describe`](Self::describe) is the -/// message the host reads through `se_last_error`: the name of the key at -/// fault crosses the boundary, its value never does. -#[derive(Debug, PartialEq, Eq)] -pub enum ConfigError { - /// The config was not an object of string values. - NotAnObject, - /// A required key was absent. - Missing(&'static str), - /// A key held something other than a string. - NotAString(&'static str), - /// A key's value failed its own validation (bad UUID, bad hex, bad URL). - Invalid(&'static str), - /// A key appeared twice. The codec rejects duplicate object keys before - /// this parser runs, but `parse_config` is `pub` and takes any - /// [`FfiValue`] — last-write-wins on, say, `client_key` must never be - /// silent. - Duplicate(&'static str), - /// A key this version does not recognise. - UnknownKey(String), -} - -impl ConfigError { - /// Says what was wrong with the config, for the host to read. - /// - /// The message names the key at fault but never its value, because the - /// config holds the client key. An unrecognised key is not named - /// either: if a value is pasted where a key should be, the unrecognised - /// key *is* that value. - pub fn describe(&self) -> String { - match self { - Self::NotAnObject => "the config is not an object of string values".to_owned(), - Self::Missing(key) => format!("the config has no {key}"), - Self::NotAString(key) => format!("the config's {key} is not a string"), - Self::Invalid(key) => format!("the config's {key} is not valid"), - Self::Duplicate(key) => format!("the config gives {key} twice"), - Self::UnknownKey(_) => "the config has a key this version does not know".to_owned(), - } - } -} +use vitaminc_aead_frame::serde::from_slice; /// Everything `se_cipher_init` needs to build the cipher. pub struct CipherConfig { @@ -79,93 +45,93 @@ pub struct CipherConfig { pub keyset_cache_size: Option, } -/// Parse a decoded config value. Consumes it so the client-key material has -/// one owner; the `Protected` payloads are wiped when the strings drop here. -pub fn parse_config(value: FfiValue) -> Result { - let FfiValue::Object(entries) = value else { - return Err(ConfigError::NotAnObject); - }; +/// Parse the config frame a host sent. See the [module docs](self). +pub fn parse_config(bytes: &[u8]) -> Result { + read(bytes).map_err(malformed) +} - // Every slot is a `Zeroizing`, not just the key one: the - // client-key slot *must* wipe on every exit path (any of the `?`s below - // can fire while it holds a full encoding of the client root key), and - // making one slot special invites the next edit to add an early return - // above the wipe. Uniform is cheaper than remembering. - let mut client_id: Option> = None; - let mut client_key_encoded: Option> = None; - let mut url: Option> = None; - let mut cache_size: Option> = None; +/// [`parse_config`], with the refusal's message. +fn read(bytes: &[u8]) -> Result { + let wire: Wire<'_> = from_slice(bytes).map_err(|e| format!("the config: {e}"))?; + // `from_encoded_v1` is lenient by design: hex in either case *or* the + // base64 `secretkey.json` holds, matching every native loader. + let client_key = ClientKey::from_encoded_v1(wire.client_id.0, wire.client_key) + .map_err(|_| "the config: client_key: a client key that is not valid".to_owned())?; + Ok(CipherConfig { + client_key, + endpoint: wire.zerokms_url.map(|url| url.0), + keyset_cache_size: wire.keyset_cache_size.map(|size| size.0), + }) +} - for (key, value) in entries { - let slot = match key.as_str() { - "client_id" => &mut client_id, - "client_key" => &mut client_key_encoded, - "zerokms_url" => &mut url, - "keyset_cache_size" => &mut cache_size, - _ => return Err(ConfigError::UnknownKey(key)), - }; - // The codec already rejects duplicate object keys, so on the ABI - // path the slot is always vacant — but this function accepts any - // `FfiValue`, so enforce it rather than assume it. - if slot.is_some() { - return Err(ConfigError::Duplicate(name_of(&key))); - } - let FfiValue::String(s) = value else { - return Err(ConfigError::NotAString(name_of(&key))); - }; - let text = Zeroizing::new( - std::str::from_utf8(s.risky_ref()) - .map_err(|_| ConfigError::NotAString(name_of(&key)))? - .to_string(), - ); - *slot = Some(text); - } +/// The config as a host writes it. Each field validates as it reads, +/// through `TryFrom`. The frame's serde never repeats a `TryFrom` error's +/// text (it could carry the value), so a refusal names the key and says +/// only that its value was not accepted; each error here is still a fixed +/// string, for the reader. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct Wire<'a> { + client_id: ClientId, + client_key: &'a str, + #[serde(default, deserialize_with = "present")] + zerokms_url: Option, + #[serde(default, deserialize_with = "present")] + keyset_cache_size: Option, +} - let client_id = client_id.ok_or(ConfigError::Missing("client_id"))?; - let client_id = Uuid::parse_str(&client_id).map_err(|_| ConfigError::Invalid("client_id"))?; +/// Absent is `None`. Present, a value must be a string: a null is refused. +fn present<'de, D: Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result, D::Error> { + T::deserialize(d).map(Some) +} - // Lenient by design: `from_encoded_v1` takes hex in either case *or* the - // base64 `secretkey.json` holds, matching every native loader. The - // `Zeroizing` slot wipes the encoded copy however this returns — the - // decoded `FfiValue`'s own copy was consumed above, and the raw input - // buffer is the ABI layer's to wipe. - let client_key_encoded = client_key_encoded.ok_or(ConfigError::Missing("client_key"))?; - let client_key = ClientKey::from_encoded_v1(client_id, &client_key_encoded) - .map_err(|_| ConfigError::Invalid("client_key"))?; +/// A client id: a UUID, as text. +#[derive(Deserialize)] +#[serde(try_from = "&str")] +struct ClientId(Uuid); - let endpoint = url - .map(|u| u.parse().map_err(|_| ConfigError::Invalid("zerokms_url"))) - .transpose()?; +impl TryFrom<&str> for ClientId { + type Error = &'static str; - let keyset_cache_size = cache_size - .map(|n| { - n.parse::() - .map_err(|_| ConfigError::Invalid("keyset_cache_size")) - }) - .transpose()?; + fn try_from(text: &str) -> Result { + Uuid::parse_str(text) + .map(ClientId) + .map_err(|_| "a client id that is not a UUID") + } +} - Ok(CipherConfig { - client_key, - endpoint, - keyset_cache_size, - }) +/// The ZeroKMS endpoint to pin. +#[derive(Deserialize)] +#[serde(try_from = "&str")] +struct Url(ZeroKmsEndpoint); + +impl TryFrom<&str> for Url { + type Error = &'static str; + + fn try_from(text: &str) -> Result { + text.parse().map(Url).map_err(|_| "a URL that is not valid") + } } -/// Intern the key name for error reporting (`&'static str` keeps -/// [`ConfigError`] cheap; unknown keys carry the owned string instead). -fn name_of(key: &str) -> &'static str { - match key { - "client_id" => "client_id", - "client_key" => "client_key", - "zerokms_url" => "zerokms_url", - "keyset_cache_size" => "keyset_cache_size", - _ => "unknown", +/// A positive integer, as a decimal string. +#[derive(Deserialize)] +#[serde(try_from = "&str")] +struct CacheSize(NonZeroUsize); + +impl TryFrom<&str> for CacheSize { + type Error = &'static str; + + fn try_from(text: &str) -> Result { + text.parse() + .map(CacheSize) + .map_err(|_| "a size that is not a positive decimal integer") } } #[cfg(test)] mod tests { use super::*; + use crate::test_frame::{frame, obj, s, V}; /// A valid v1 client key for fixtures, minted through the real key type /// so the hex exercises the actual `from_hex_v1` decoder. @@ -178,22 +144,27 @@ mod tests { (id, key.to_hex_v1().expect("encode fixture key")) } - fn obj(entries: Vec<(&str, &str)>) -> FfiValue { - FfiValue::Object( - entries - .into_iter() - .map(|(k, v)| (k.to_string(), FfiValue::String(v.into()))) - .collect(), - ) + fn config(entries: Vec<(&'static str, V)>) -> Result { + read(&frame(&obj(entries))) + } + + /// The config's refusal, checked to name neither the client key nor any + /// value or unknown key the test sent. + fn refused(v: &V, secrets: &[&str]) -> String { + let message = read(&frame(v)).err().expect("refused"); + for secret in secrets { + assert!(!message.contains(secret), "{message:?} echoes {secret:?}"); + } + message } #[test] fn parses_a_minimal_config() { let (id, hex) = client_key_hex(); - let cfg = parse_config(obj(vec![ - ("client_id", &id.to_string()), - ("client_key", &hex), - ])) + let cfg = config(vec![ + ("client_id", s(&id.to_string())), + ("client_key", s(&hex)), + ]) .expect("minimal config parses"); assert_eq!(cfg.client_key.key_id, id); assert!(cfg.endpoint.is_none()); @@ -203,28 +174,27 @@ mod tests { #[test] fn parses_the_keyset_cache_size() { let (id, hex) = client_key_hex(); - let id_s = id.to_string(); - let cfg = parse_config(obj(vec![ - ("client_id", &id_s), - ("client_key", &hex), - ("keyset_cache_size", "16"), - ])) - .expect("config parses"); + let with = |size: V| { + obj(vec![ + ("client_id", s(&id.to_string())), + ("client_key", s(&hex)), + ("keyset_cache_size", size), + ]) + }; + let cfg = read(&frame(&with(s("16")))).expect("config parses"); assert_eq!(cfg.keyset_cache_size, NonZeroUsize::new(16)); for bad in ["0", "-1", "sixteen", ""] { - assert!( - matches!( - parse_config(obj(vec![ - ("client_id", &id_s), - ("client_key", &hex), - ("keyset_cache_size", bad), - ])), - Err(ConfigError::Invalid("keyset_cache_size")) - ), + assert_eq!( + refused(&with(s(bad)), &[&hex]), + "the config: keyset_cache_size: a value its type does not accept", "{bad:?} must be refused" ); } + // A decimal string, not an integer, and not null. + for bad in [V::U32(16), V::Null] { + assert!(refused(&with(bad), &[&hex]).starts_with("the config: keyset_cache_size: ")); + } } /// A keyset is not a config key: the default is the server's and a @@ -235,25 +205,27 @@ mod tests { fn a_keyset_key_is_rejected_like_any_other_unknown_key() { let (id, hex) = client_key_hex(); for key in ["keyset", "keyset_id"] { - assert!(matches!( - parse_config(obj(vec![ - ("client_id", &id.to_string()), - ("client_key", &hex), - (key, "users"), - ])), - Err(ConfigError::UnknownKey(_)) - )); + let message = refused( + &obj(vec![ + ("client_id", s(&id.to_string())), + ("client_key", s(&hex)), + (key, s("users")), + ]), + // "keyset" alone is inside the expected `keyset_cache_size`. + &[&hex, "users", "keyset_id"], + ); + assert!(message.contains("an unknown key"), "{message}"); } } #[test] fn parses_the_zerokms_url() { let (id, hex) = client_key_hex(); - let cfg = parse_config(obj(vec![ - ("client_id", &id.to_string()), - ("client_key", &hex), - ("zerokms_url", "https://zerokms.example.com"), - ])) + let cfg = config(vec![ + ("client_id", s(&id.to_string())), + ("client_key", s(&hex)), + ("zerokms_url", s("https://zerokms.example.com")), + ]) .expect("config parses"); assert!(cfg.endpoint.is_some()); } @@ -266,7 +238,6 @@ mod tests { use base64ct::Encoding; let (id, hex) = client_key_hex(); - let id_s = id.to_string(); let bytes = base16ct::lower::decode_vec(&hex).expect("fixture hex"); let base64 = base64ct::Base64::encode_string(&bytes); @@ -275,8 +246,11 @@ mod tests { ("uppercase hex", hex.to_uppercase()), ("base64", base64), ] { - let cfg = parse_config(obj(vec![("client_id", &id_s), ("client_key", &encoded)])) - .unwrap_or_else(|e| panic!("{label} must parse, got {e:?}")); + let cfg = config(vec![ + ("client_id", s(&id.to_string())), + ("client_key", s(&encoded)), + ]) + .unwrap_or_else(|e| panic!("{label} must parse, got {e}")); assert_eq!(cfg.client_key.key_id, id, "{label}"); assert_eq!( cfg.client_key.to_hex_v1().expect("re-encode"), @@ -286,57 +260,96 @@ mod tests { } } + /// Every refusal names the key at fault and never a value: not the + /// client key, not a bad id or URL, not an unknown key. #[test] - fn rejects_bad_configs() { + fn rejects_bad_configs_without_echoing_a_value() { let (id, hex) = client_key_hex(); let id_s = id.to_string(); + let secrets = [ + hex.as_str(), + "nope", + "zerokms_urk", + "typo.example", + "deadbeef", + ]; + let cases = [ + (V::Null, "the config: expected "), + ( + V::Arr(vec![s(&id_s), s(&hex)]), + "the config: expected struct Wire, found an array", + ), + ( + obj(vec![ + ("client_id", s(&id_s)), + ("client_key", V::Bytes(hex.clone().into_bytes())), + ]), + "the config: client_key: expected a borrowed string, found bytes", + ), + ( + obj(vec![("client_id", s(&id_s))]), + "the config: missing field `client_key`", + ), + ( + obj(vec![("client_id", s("nope")), ("client_key", s(&hex))]), + "the config: client_id: a value its type does not accept", + ), + ( + obj(vec![("client_id", s(&id_s)), ("client_key", V::U64(1))]), + "the config: client_key: ", + ), + ( + // Valid hex, not a keyset. + obj(vec![("client_id", s(&id_s)), ("client_key", s("deadbeef"))]), + "the config: client_key: a client key that is not valid", + ), + ( + obj(vec![ + ("client_id", s(&id_s)), + ("client_key", s(&hex)), + ("zerokms_urk", s("https://typo.example.com")), + ]), + "the config: an unknown key, ", + ), + ( + obj(vec![ + ("client_id", s(&id_s)), + ("client_key", s(&hex)), + ("zerokms_url", s("not a url")), + ]), + "the config: zerokms_url: a value its type does not accept", + ), + ( + obj(vec![ + ("client_id", s(&id_s)), + ("client_key", s(&hex)), + ("zerokms_url", V::Null), + ]), + "the config: zerokms_url: ", + ), + ]; + for (config, expected) in &cases { + let message = refused(config, &secrets); + assert!( + message.starts_with(expected), + "{message:?} is not {expected:?}" + ); + } + } - assert!(matches!( - parse_config(FfiValue::Null), - Err(ConfigError::NotAnObject) - )); - assert!(matches!( - parse_config(obj(vec![("client_id", &id_s)])), - Err(ConfigError::Missing("client_key")) - )); - assert!(matches!( - parse_config(obj(vec![("client_id", "nope"), ("client_key", &hex)])), - Err(ConfigError::Invalid("client_id")) - )); - assert!(matches!( - parse_config(obj(vec![ - ("client_id", &id_s), - ("client_key", "deadbeef"), // valid hex, not a keyset - ])), - Err(ConfigError::Invalid("client_key")) - )); - assert!(matches!( - parse_config(obj(vec![ - ("client_id", &id_s), - ("client_key", &hex), - ("zerokms_urk", "https://typo.example.com"), - ])), - Err(ConfigError::UnknownKey(_)) - )); - // The codec refuses duplicate keys on the ABI path, but this - // function is `pub` over any `FfiValue`: a repeated `client_key` - // must be an error, never a silent last-write-wins on root key - // material. - assert!(matches!( - parse_config(obj(vec![ - ("client_id", &id_s), - ("client_key", &hex), - ("client_key", &hex), - ])), - Err(ConfigError::Duplicate("client_key")) - )); - assert!(matches!( - parse_config(obj(vec![ - ("client_id", &id_s), - ("client_key", &hex), - ("zerokms_url", "not a url"), - ])), - Err(ConfigError::Invalid("zerokms_url")) - )); + /// A repeated `client_key` is never a silent last-write-wins on root key + /// material: the frame refuses it. + #[test] + fn a_repeated_key_is_refused() { + let (id, hex) = client_key_hex(); + let message = refused( + &obj(vec![ + ("client_id", s(&id.to_string())), + ("client_key", s(&hex)), + ("client_key", s(&hex)), + ]), + &[&hex], + ); + assert!(message.starts_with("the config: "), "{message}"); } } diff --git a/languages/golang/encrypt/guest/src/deterministic.rs b/languages/golang/encrypt/guest/src/deterministic.rs index e58a26e74..88a789d51 100644 --- a/languages/golang/encrypt/guest/src/deterministic.rs +++ b/languages/golang/encrypt/guest/src/deterministic.rs @@ -11,3 +11,59 @@ //! binary carries it; the tests read it from disk. pub use stack_kms::DeterministicSource; + +use serde::Deserialize; +use stack_guest_abi::last_error::malformed; +use vitaminc_aead_frame::serde::from_slice; +use zeroize::Zeroize; + +/// The `deterministic-kms` build's whole config: the source's 32-byte seed, +/// a frame holding bytes and nothing else. Wiped when it drops. +#[derive(Deserialize)] +#[serde(try_from = "&[u8]")] +pub struct Seed([u8; 32]); + +impl TryFrom<&[u8]> for Seed { + type Error = &'static str; + + fn try_from(bytes: &[u8]) -> Result { + bytes + .try_into() + .map(Seed) + .map_err(|_| "a seed that is not 32 bytes") + } +} + +impl Drop for Seed { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +impl Seed { + /// Read the seed frame the host sent in place of a config. + pub fn parse(bytes: &[u8]) -> Result { + from_slice(bytes) + .map_err(|_| malformed("the deterministic build's config is a 32-byte seed")) + } + + /// The key source this seed drives. + pub fn source(&self) -> DeterministicSource { + DeterministicSource::new(self.0) + } +} + +#[cfg(test)] +mod tests { + use super::Seed; + use crate::test_frame::{frame, V}; + use vitaminc_aead_frame::serde::from_slice; + + /// A seed is 32 bytes, and only bytes: 32 characters of text are refused. + #[test] + fn a_seed_is_32_bytes_never_text() { + assert!(from_slice::(&frame(&V::Bytes(vec![7; 32]))).is_ok()); + assert!(from_slice::(&frame(&V::Bytes(vec![7; 31]))).is_err()); + assert!(from_slice::(&frame(&V::Str("x".repeat(32)))).is_err()); + } +} diff --git a/languages/golang/encrypt/guest/src/lib.rs b/languages/golang/encrypt/guest/src/lib.rs index fd25df371..e6ff1b343 100644 --- a/languages/golang/encrypt/guest/src/lib.rs +++ b/languages/golang/encrypt/guest/src/lib.rs @@ -96,6 +96,8 @@ pub mod options; pub mod response; pub mod status; pub mod targets; +#[cfg(test)] +mod test_frame; // The ABI's packed u64 results embed 32-bit pointers, its bounds checks // read the wasm linear-memory size, and `host` calls imported functions — diff --git a/languages/golang/encrypt/guest/src/ops.rs b/languages/golang/encrypt/guest/src/ops.rs index eba53ba34..d0aefcb77 100644 --- a/languages/golang/encrypt/guest/src/ops.rs +++ b/languages/golang/encrypt/guest/src/ops.rs @@ -42,17 +42,16 @@ //! values to verify are read as [`ValueFrame`]s: the frame is checked against //! the engine's value model, and the engine walks it, splitting a source into //! one frame per field as it goes. A record opens into one frame, which is -//! the output. Control data (a plan, an options object, query statistics) -//! carries no secret and is decoded into an [`FfiValue`]. +//! the output. Control data (a plan, a context, an options object, query +//! statistics) carries no secret and is read from its frame with serde. -use stack_encrypt::dynamic::{self, FfiValue, Scope, ValueFrame}; +use stack_encrypt::dynamic::{self, Scope, ValueFrame}; use stack_encrypt::sem::MatchOptions; use stack_encrypt::target::IndexSpec; use stack_encrypt::ErrorPayload; -use stack_encrypt::{CipherText, Encrypt, KeysetCipher, Label, Node, SealedValue, StackCipherText}; +use stack_encrypt::{CipherText, KeysetCipher, Label, Node, SealedValue, StackCipherText}; use stack_kms::DataKeySource; -use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext, Frame, FrameWriter}; -use vitaminc_protected::Controlled; +use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext, Frame}; use zeroize::Zeroizing; use stack_guest_abi::last_error; @@ -87,8 +86,8 @@ type BytesTree = Node>; /// /// The context is one part — a string, bytes, or an `i32`/`i64`/`u32`/`u64` /// — or an array of parts, nested as deep as the transport codec allows -/// ([`MAX_DEPTH`](vitaminc_aead_frame::MAX_DEPTH) levels from the root of the encoded value; deeper -/// is [`STATUS_ENCODING`] before the context is parsed), exactly as a plan +/// ([`MAX_DEPTH`](vitaminc_aead_frame::MAX_DEPTH) levels from the root of the frame; deeper +/// is [`STATUS_ENCODING`] before the context is read), exactly as a plan /// field's; [`dynamic::context`] is the one home of that grammar. Shape is identity: /// `[x]` is a PAE-framed list and `x` is not, so a probe takes the context /// in the shape the field was sealed under — a plan field's context @@ -106,7 +105,7 @@ where { // The same proof every stack-encrypt leaf demands: an empty context is // `STATUS_ENCODING` here, before any derivation. - let context = dynamic::context(decode_value(context)?).map_err(|e| fail_dynamic(&e))?; + let context = parse_context(context)?; let value = parse_frame(value)?; let kind = parse_term(value, kind)?; dynamic::term(cipher, value, &kind, context) @@ -114,6 +113,16 @@ where .map_err(|e| fail_dynamic(&e)) } +/// A term's context: one frame, read by [`dynamic::context`]. Bytes that +/// are not a frame are refused as every other input's are, before the +/// context's own grammar. +fn parse_context( + context: &[u8], +) -> Result>, u32> { + let _ = parse_frame(context)?; + dynamic::context(context).map_err(|e| fail_dynamic(&e)) +} + /// The static half of a term: the kind is one of the ABI's table, and the /// value is one scalar the index takes ([`IndexSpec::defines`]). Shared by /// [`term`] and [`validate::term`] so the ABI refuses exactly what the @@ -222,7 +231,8 @@ where /// Derive the query term for one plan field, an operator and its input /// (ADR-0009; ADR-0007, amended 2026-10-16): a codec-encoded input, the /// codec-encoded plan, the field's name and the operator's name (both UTF-8) -/// in, the term's codec-encoded object out ([`dynamic::QueryTerm::to_value`]): +/// in, the term's codec-encoded object out (the `Serialize` form of +/// [`dynamic::QueryTerm`]): /// /// ```text /// { "operator": "matches", "fidelity": "probabilistic", "index": "match", @@ -259,7 +269,7 @@ where .map_err(|e| fail_dynamic(&e))? .await .map_err(|e| fail_error(&e))?; - encode_value(term.to_value()) + encode_answer(&term) } /// Verify a page of decrypted values against a probabilistic query: the @@ -293,18 +303,17 @@ pub fn verify( &resolver(), ) .map_err(|e| fail_dynamic(&e))?; - encode_value(FfiValue::Array( - verified.into_iter().map(FfiValue::Bool).collect(), - )) + encode_answer(&verified) } /// How many rows a probabilistic query should fetch for `want` of them to /// verify: the term's bytes as [`query_term`] returned them (not a frame), /// the codec-encoded statistics -/// ([`Stats::from_value`](stack_encrypt::op::Stats::from_value)), the plan, +/// ([`Stats::from_frame`](stack_encrypt::op::Stats::from_frame)), the plan, /// the field's name and the operator's name in; the codec-encoded limit out, /// an unsigned 64-bit integer or null for no finite fetch -/// ([`dynamic::limit_to_value`]). Needs no cipher and no key. +/// ([`Limit::rows`](stack_encrypt::op::Limit::rows)). Needs no cipher and +/// no key. pub fn target_limit( term: &[u8], stats: &[u8], @@ -316,11 +325,11 @@ pub fn target_limit( let (plan, field, operator) = parse_query(plan, field, operator)?; // Statistics are read without a field to hand; the error names the // query's, as every other refusal of a query does. - let stats = stack_encrypt::op::Stats::from_value(&decode_value(stats)?) + let stats = stack_encrypt::op::Stats::from_frame(stats) .map_err(|e| fail_dynamic(&e.in_field(field)))?; let limit = dynamic::target_limit(&plan, field, operator, term, want, &stats, &resolver()) .map_err(|e| fail_dynamic(&e))?; - encode_value(dynamic::limit_to_value(limit)) + encode_answer(&limit.rows()) } /// The plan, field name and operator a query names: the plan parsed against @@ -347,11 +356,13 @@ fn parse_query<'f>( Ok((plan, field, operator)) } -/// A codec-encoded plan, parsed against this build's resolver: a target -/// name this build cannot run is refused here, the same way at every -/// export that takes a plan. +/// A plan's frame, parsed against this build's resolver: a target name this +/// build cannot run is refused here, the same way at every export that +/// takes a plan. Bytes that are not a frame are refused as every other +/// input's are, before the plan's own grammar. fn parse_plan(plan: &[u8]) -> Result { - dynamic::record::plan_with(decode_value(plan)?, &resolver()).map_err(|e| fail_dynamic(&e)) + let _ = parse_frame(plan)?; + dynamic::record::plan_with(plan, &resolver()).map_err(|e| fail_dynamic(&e)) } // ============================================================================= @@ -371,8 +382,8 @@ pub fn plan_check(plan: &[u8]) -> Result<(), u32> { /// The EQL types this build of the engine knows, as a codec-encoded /// `{"targets": [...]}`: one entry per type in -/// [`TargetDescriptor::to_value`](stack_encrypt::dynamic::TargetDescriptor::to_value)'s -/// wire form, producible or not, with the reason when not. The build +/// [`TargetDescriptor`](stack_encrypt::dynamic::TargetDescriptor)'s +/// `Serialize` form, producible or not, with the reason when not. The build /// without EQL types lists none, which a generator reads as "no /// `encrypt_into` type is available" and refuses the tag; the `eql` build /// lists the catalog, and a generator writes `encrypt_into` only for an @@ -401,22 +412,19 @@ pub fn plan_check(plan: &[u8]) -> Result<(), u32> { /// type answers and how far each answer can be trusted. pub fn targets() -> Result, u32> { use stack_encrypt::dynamic::TargetResolver as _; - encode_value(FfiValue::Object(vec![( - "targets".to_string(), - FfiValue::Array( - resolver() - .targets() - .iter() - .map(|target| target.to_value()) - .collect(), - ), - )])) + #[derive(serde::Serialize)] + struct Targets { + targets: Vec, + } + encode_answer(&Targets { + targets: resolver().targets(), + }) } /// The operators one field of a codec-encoded plan answers, as a /// codec-encoded list: one object per operator, in the order the engine -/// lists them, each written by -/// [`dynamic::operator_to_value`]: +/// lists them, each the `Serialize` form of a +/// [`dynamic::OperatorEntry`]: /// /// ```text /// [ { "operator": "eq", "fidelity": "exact", "arity": "one", "index": "eq" }, ... ] @@ -435,12 +443,12 @@ pub fn operators(plan: &[u8], field: &[u8]) -> Result, u32> { .map_err(|_| last_error::malformed("the field name is not UTF-8"))?; let operators = dynamic::field_operators(&plan, field, &resolver()).map_err(|e| fail_dynamic(&e))?; - encode_value(FfiValue::Array( - operators + encode_answer( + &operators .iter() - .map(|(operator, index)| dynamic::operator_to_value(*operator, index)) - .collect(), - )) + .map(|(operator, index)| dynamic::OperatorEntry::new(*operator, index)) + .collect::>(), + ) } // ============================================================================= @@ -461,9 +469,7 @@ pub mod validate { /// non-empty, the kind is one of [`TERM_EQUALITY`] .. [`TERM_OPE`], and /// the value is one scalar the index takes ([`IndexSpec::defines`]). pub fn term(value: &[u8], context: &[u8], kind: u32) -> Result<(), u32> { - dynamic::context(decode_value(context)?) - .map(drop) - .map_err(|e| fail_dynamic(&e))?; + let _ = parse_context(context)?; parse_term(parse_frame(value)?, kind).map(drop) } @@ -510,11 +516,6 @@ pub mod validate { // Codec glue // ============================================================================= -/// Control data from a frame: a plan, an options object, statistics. -fn decode_value(bytes: &[u8]) -> Result { - FfiValue::from_frame(bytes).map_err(|_| not_a_frame()) -} - /// Plaintext from a frame: checked against the engine's value model and /// held as the frame, in a buffer wiped when it drops. The same frames are /// refused as control data's, so a malformed input fails the same way on @@ -533,7 +534,7 @@ fn checked_plaintext(bytes: &[u8]) -> Result, u32> { } /// An opened value as the output buffer: its frame, copied into a buffer -/// whose capacity is exactly its length (see [`encode_value`]); the frame +/// whose capacity is exactly its length (see [`encode_tree`]); the frame /// is wiped when it drops. fn encode_frame(frame: ValueFrame) -> Result, u32> { let mut out = exact_buffer(Some(frame.risky_ref().len()))?; @@ -551,23 +552,17 @@ fn not_a_frame() -> u32 { last_error::malformed("an input is not a value in the transport frame") } -/// Encode a value tree into a buffer whose capacity is exactly its length. -/// -/// This buffer is plaintext on the decrypt path, and the registry wipes only -/// the buffer it holds: `register`'s `into_boxed_slice` reallocates a `Vec` -/// whose capacity exceeds its length, leaving the old allocation unwiped. -/// The frame writer reserves an upper bound per leaf (a six-byte string -/// reserves 14 for 11), so writing straight into the exactly sized buffer -/// can still grow it. It writes into a `Zeroizing` staging buffer instead -/// (the writer wipes any allocation it replaces), which is copied into an -/// exactly sized output and then wiped. -fn encode_value(value: FfiValue) -> Result, u32> { - let mut staging = Zeroizing::new(exact_buffer(value_encoded_len(&value))?); - (&value) - .visit(FrameWriter::new(&mut staging)) - .map_err(|_| STATUS_ENCODING)?; - let mut out = exact_buffer(Some(staging.len()))?; - out.extend_from_slice(&staging); +/// One of the engine's answers (a query term, a page of verdicts, a limit, a +/// catalog), written to its frame by serde, in a buffer whose capacity is +/// exactly its length. An answer is not plaintext, but the registry's +/// `into_boxed_slice` would reallocate a buffer with spare capacity, so it +/// is sized as every other output is. A query term's search tokens are +/// derived from plaintext, so the staging buffer is wiped once copied. +fn encode_answer(answer: &T) -> Result, u32> { + let frame = + Zeroizing::new(vitaminc_aead_frame::serde::to_vec(answer).map_err(|_| STATUS_ENCODING)?); + let mut out = exact_buffer(Some(frame.len()))?; + out.extend_from_slice(&frame); Ok(out) } @@ -584,8 +579,9 @@ fn decode_tree(bytes: &[u8]) -> Result { } /// The encode twin of [`decode_tree`]. Passthrough nodes can carry caller -/// plaintext, so this is sized up front for the same reason -/// [`encode_value`] is. +/// plaintext, so this is sized up front: the registry wipes only the buffer +/// it holds, and `register`'s `into_boxed_slice` reallocates a `Vec` whose +/// capacity exceeds its length, leaving the old allocation unwiped. fn encode_tree(tree: StackCipherText) -> Result, u32> { let tree = map_leaves(tree, &mut |l: SealedValue| Ok::<_, u32>(l.to_bytes()))?; let mut out = exact_buffer(tree_encoded_len(&tree))?; @@ -635,29 +631,6 @@ fn exact_buffer(len: Option) -> Result, u32> { Ok(out) } -/// Exact byte length of the frame of `value`. `None` on overflow or on a -/// length the frame's `u32` lengths cannot express — the encode would fail -/// on those anyway, so the caller reports an encoding error. -fn value_encoded_len(value: &FfiValue) -> Option { - // tag byte + fixed payload, or tag + u32 length prefix + payload. - let framed = |len: usize| u32::try_from(len).ok().and_then(|_| len.checked_add(5)); - match value { - FfiValue::Null | FfiValue::Undefined | FfiValue::Bool(_) => Some(1), - FfiValue::Int32(_) | FfiValue::UInt32(_) | FfiValue::Float32(_) => Some(5), - FfiValue::Int64(_) | FfiValue::UInt64(_) | FfiValue::Float64(_) => Some(9), - FfiValue::String(s) => framed(s.risky_ref().len()), - FfiValue::Bytes(b) => framed(b.risky_ref().len()), - FfiValue::Array(items) => items.iter().try_fold(5usize, |acc, item| { - acc.checked_add(value_encoded_len(item)?) - }), - FfiValue::Object(entries) => entries.iter().try_fold(5usize, |acc, (key, value)| { - acc.checked_add(framed(key.len())?.checked_sub(1)?)? - .checked_add(value_encoded_len(value)?) - }), - FfiValue::Passthrough(inner) => value_encoded_len(inner)?.checked_add(1), - } -} - /// Exact byte length of the codec's encoding of a ciphertext tree. A /// passthrough is its node byte and its frame, written as it is. fn tree_encoded_len(tree: &BytesTree) -> Option { @@ -680,65 +653,32 @@ fn tree_encoded_len(tree: &BytesTree) -> Option { #[cfg(test)] mod tests { use super::*; - use vitaminc_protected::Protected; - - // `value_encoded_len` / `tree_encoded_len` re-derive the frame's and the - // codec's arithmetic; neither exports an `encoded_len` of its own, so these - // pins are the only thing that fails if the two drift. Drift is not a - // cosmetic bug: an undersized reservation makes `encode_value` - // reallocate mid-encode, leaving unwiped partial plaintext in the - // abandoned allocation — silently. - - fn every_value_shape() -> Vec { - vec![ - FfiValue::Null, - FfiValue::Undefined, - FfiValue::Bool(true), - FfiValue::Int32(-5), - FfiValue::UInt32(5), - FfiValue::Float32(1.5), - FfiValue::Int64(-9), - FfiValue::UInt64(9), - FfiValue::Float64(2.5), - FfiValue::String("".into()), - FfiValue::String("héllo".into()), - FfiValue::Bytes(Protected::new(Vec::new())), - FfiValue::Bytes(Protected::new(vec![0u8; 300])), - FfiValue::Array(Vec::new()), - FfiValue::Array(vec![FfiValue::Bool(false), FfiValue::String("x".into())]), - FfiValue::Object(Vec::new()), - FfiValue::Object(vec![ - ("a".to_string(), FfiValue::Int32(1)), - ( - "nested".to_string(), - FfiValue::Object(vec![("b".to_string(), FfiValue::Null)]), - ), - ]), - FfiValue::Passthrough(Box::new(FfiValue::Int64(7))), - FfiValue::Passthrough(Box::new(FfiValue::Array(vec![FfiValue::String( - "deep".into(), - )]))), - ] - } + use stack_encrypt::dynamic::FfiValue; + use vitaminc_protected::{Controlled, Protected}; - /// The registry wipes the buffer it is handed, and `into_boxed_slice` - /// reallocates one whose capacity exceeds its length, leaving the old - /// allocation unwiped. So every encoded output's capacity is its length, - /// not only its length the frame's. - #[test] - fn an_encoded_value_has_no_spare_capacity() { - for (i, value) in every_value_shape().into_iter().enumerate() { - let out = encode_value(value).expect("encodes"); - assert_eq!(out.capacity(), out.len(), "shape {i}"); - } - } + // `tree_encoded_len` re-derives the codec's arithmetic; the codec exports + // no `encoded_len` of its own, so this pin is the only thing that fails if + // the two drift. Drift is not a cosmetic bug: an undersized reservation + // makes `encode_tree` reallocate mid-encode, leaving unwiped partial + // plaintext in the abandoned allocation — silently. + /// The registry wipes the buffer it is handed, and `into_boxed_slice` + /// reallocates one whose capacity exceeds its length. So every answer's + /// capacity is its length. #[test] - fn value_encoded_len_matches_the_codec_exactly() { - for (i, value) in every_value_shape().into_iter().enumerate() { - let expected = value_encoded_len(&value).expect("encodable shape"); - let out = value.to_frame().expect("frame encode"); - assert_eq!(out.risky_ref().len(), expected, "shape {i}"); + fn an_encoded_answer_has_no_spare_capacity() { + let entries = [ + dynamic::OperatorEntry::new(dynamic::OpSpec::Eq, &IndexSpec::Equality), + dynamic::OperatorEntry::new(dynamic::OpSpec::Between, &IndexSpec::Ope), + ]; + for out in [ + encode_answer(&vec![true, false, true]), + encode_answer(&Some(11u64)), + encode_answer(&None::), + encode_answer(&entries), + ] { + let out = out.expect("encodes"); + assert_eq!(out.capacity(), out.len()); } } @@ -824,7 +764,7 @@ mod tests { #[test] fn targets_lists_what_this_build_holds_in_the_wire_shape() { let out = targets().expect("encodes"); - let FfiValue::Object(entries) = decode_value(&out).expect("decodes") else { + let FfiValue::Object(entries) = FfiValue::from_frame(&out).expect("decodes") else { panic!("an object"); }; assert_eq!(entries.len(), 1); @@ -947,7 +887,7 @@ mod tests { validate::record_tree(&sealed, &plan, None).expect("the tree fits"); let opened = block_on(decrypt_record(Scope::Client(&cipher), &sealed, &plan, None)).expect("opens"); - let FfiValue::Object(values) = decode_value(&opened).expect("a value") else { + let FfiValue::Object(values) = FfiValue::from_frame(&opened).expect("a value") else { panic!("an object") }; assert!( @@ -960,7 +900,7 @@ mod tests { let probe = encoded(text("alice@example.com")); validate::query_term(&probe, &plan, b"email", b"eq").expect("a target field answers eq"); let term = block_on(query_term(&keyset, &probe, &plan, b"email", b"eq")).expect("derives"); - let FfiValue::Object(term) = decode_value(&term).expect("a value") else { + let FfiValue::Object(term) = FfiValue::from_frame(&term).expect("a value") else { panic!("a query term is an object") }; assert!( diff --git a/languages/golang/encrypt/guest/src/options.rs b/languages/golang/encrypt/guest/src/options.rs index 569b4d05c..d3bb9f5fb 100644 --- a/languages/golang/encrypt/guest/src/options.rs +++ b/languages/golang/encrypt/guest/src/options.rs @@ -1,7 +1,7 @@ //! The per-call options object, and the keyset selector it carries. //! //! Every export that touches a keyset takes one more codec-encoded -//! argument: an [`FfiValue::Object`] with one required key, `keyset`, whose +//! argument: a frame object with one required key, `keyset`, whose //! value is a tagged object naming the keyset the call binds to, and on //! `se_decrypt_record` one optional key, `context` (below): //! @@ -41,15 +41,14 @@ //! it is objects, strings, bytes and nothing else, and this module is its //! one home. The Go bindings plan points here. -use stack_encrypt::dynamic::FfiValue; use stack_encrypt::dynamic::Scope; use stack_encrypt::{KeysetCipher, Label, StackCipher}; use stack_kms::{IdentifiedBy, IndexKeySource}; use uuid::Uuid; -use vitaminc_protected::Controlled; use zerokms_protocol::Name; use stack_guest_abi::last_error::malformed; +use vitaminc_aead_frame::serde::from_slice; use crate::status::fail_error; #[cfg(any(doc, test))] @@ -88,84 +87,133 @@ pub struct Options { pub context: Option