diff --git a/Cargo.lock b/Cargo.lock index 7297307..7d50be6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -219,8 +219,24 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" dependencies = [ - "asn1-rs-derive", - "asn1-rs-impl", + "asn1-rs-derive 0.4.0", + "asn1-rs-impl 0.1.0", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl 0.2.0", "displaydoc", "nom", "num-traits", @@ -241,6 +257,18 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + [[package]] name = "asn1-rs-impl" version = "0.1.0" @@ -252,6 +280,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "ast_node" version = "5.0.0" @@ -275,6 +314,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "async-trait" version = "0.1.92" @@ -346,6 +407,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -368,6 +435,28 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "base64urlsafedata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08e33815c87d8cadcddb1e74ac307368a3751fbe40c961538afa21a1899f21c" +dependencies = [ + "base64 0.21.7", + "pastey", + "serde", +] + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + [[package]] name = "better_scoped_tls" version = "1.0.1" @@ -383,7 +472,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -403,7 +492,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cexpr", "clang-sys", "itertools 0.13.0", @@ -451,6 +540,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.2" @@ -505,6 +600,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "borsh" version = "1.8.1" @@ -590,6 +695,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" @@ -729,6 +840,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.2" @@ -740,6 +862,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.45" @@ -758,6 +893,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout", + "zeroize", ] [[package]] @@ -1268,6 +1404,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -1313,7 +1450,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f7c1384d87fc0a6439a065312fbef8f6ac6128689dbc2831b28b3a1d4f3a4e6" dependencies = [ - "base64", + "base64 0.22.1", "capacity_builder", "deno_error", "deno_media_type", @@ -1356,7 +1493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ad7d5294f640fd99db08c455e2453c9860b1fa728b76fbdacf0eed3e308d48a" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "bit-set 0.8.0", "bit-vec 0.8.0", "boxed_error", @@ -1401,7 +1538,7 @@ dependencies = [ "aes-kw", "argon2", "aws-lc-rs", - "base64", + "base64 0.22.1", "cbc", "const-oid 0.9.6", "ctr", @@ -1488,7 +1625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "752387783c99f75ea6962bb88af3735baba9f4a9e66716eacd39586db0e28d99" dependencies = [ "async-compression", - "base64", + "base64 0.22.1", "bytes", "data-url", "deno_core", @@ -1632,7 +1769,7 @@ dependencies = [ "tokio-vsock", "url", "web-transport-proto", - "x509-parser", + "x509-parser 0.15.1", ] [[package]] @@ -1882,7 +2019,21 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", "displaydoc", "nom", "num-bigint", @@ -2076,6 +2227,30 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "ed448" version = "0.5.0" @@ -2331,6 +2506,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2483,7 +2673,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ - "bitflags", + "bitflags 2.13.2", "debugid", "rustc-hash 2.1.3", "serde", @@ -2619,6 +2809,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash2curve" version = "0.14.0" @@ -2680,6 +2881,12 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hickory-proto" version = "0.25.2" @@ -3437,7 +3644,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -3450,7 +3657,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", @@ -3500,6 +3707,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "num-integer" version = "0.1.47" @@ -3578,7 +3796,16 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", ] [[package]] @@ -3603,12 +3830,59 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de8111e94a56adca7ebfe6c365b6d42f99deb69b74b6becacd4204e2f193aa2" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.6.1+3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c413d6ec9ebaf5e1c829d355cb3e9f783778d700f135be0921d52fce3c60ae" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.32.0" @@ -3657,7 +3931,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ - "base64", + "base64 0.22.1", "const-hex", "opentelemetry", "opentelemetry_sdk", @@ -3790,12 +4064,27 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -3979,6 +4268,17 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -4080,8 +4380,8 @@ checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" dependencies = [ "bit-set 0.11.1", "bit-vec 0.10.1", - "bitflags", - "chacha20", + "bitflags 2.13.2", + "chacha20 0.10.2", "core_detect", "num-traits", "rand 0.10.3", @@ -4266,7 +4566,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "chacha20", + "chacha20 0.10.2", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -4359,7 +4659,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.2", ] [[package]] @@ -4471,6 +4771,7 @@ dependencies = [ "pkcs1", "pkcs8 0.10.2", "rand_core 0.6.4", + "sha2", "signature 2.2.0", "spki 0.7.3", "subtle", @@ -4493,7 +4794,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags", + "bitflags 2.13.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4544,7 +4845,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4557,7 +4858,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys 0.12.1", @@ -4690,8 +4991,11 @@ dependencies = [ name = "sausage" version = "0.3.0" dependencies = [ + "aes-gcm", "anyhow", "async-trait", + "base64 0.22.1", + "chacha20poly1305", "clap", "deno_ast", "deno_core", @@ -4703,19 +5007,35 @@ dependencies = [ "deno_telemetry", "deno_web", "deno_webidl", + "ed25519-dalek", + "hex", + "hkdf", + "hmac", + "http", + "openssl", + "p256", + "rsa", "rusqlite", "rustls", "sausage-vfs", "serde", "serde_json", "serde_yaml_ng", + "sha2", + "ssh-key", "sys_traits", "tempfile", + "thiserror 2.0.21", "tokio", + "url", + "uuid", "wasmtime", "wasmtime-wasi", "wasmtime-wasi-http", "wat", + "webauthn-authenticator-rs", + "webauthn-rs", + "zeroize", ] [[package]] @@ -4793,7 +5113,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -4846,6 +5166,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_cbor_2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -5158,6 +5488,57 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20 0.9.1", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "ssh-key" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3" +dependencies = [ + "bcrypt-pbkdf", + "ed25519-dalek", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1 0.7.3", + "sha2", + "signature 2.2.0", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5324,7 +5705,7 @@ version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a573a0c72850dec8d4d8085f152d5778af35a2520c3093b242d2d1d50776da7c" dependencies = [ - "bitflags", + "bitflags 2.13.2", "is-macro", "num-bigint", "once_cell", @@ -5379,7 +5760,7 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e82f7747e052c6ff6e111fa4adeb14e33b46ee6e94fe5ef717601f651db48fc" dependencies = [ - "bitflags", + "bitflags 2.13.2", "either", "num-bigint", "rustc-hash 2.1.3", @@ -5416,7 +5797,7 @@ version = "27.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1a51af1a92cd4904c073b293e491bbc0918400a45d58227b34c961dd6f52d7" dependencies = [ - "bitflags", + "bitflags 2.13.2", "either", "num-bigint", "phf 0.11.3", @@ -5502,7 +5883,7 @@ version = "33.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03de12e38e47ac1c96ac576f793ad37a9d7b16fbf4f2203881f89152f2498682" dependencies = [ - "base64", + "base64 0.22.1", "bytes-str", "indexmap", "once_cell", @@ -5685,6 +6066,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "synstructure" version = "0.14.0" @@ -5967,6 +6359,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -6234,9 +6627,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.28.0" +version = "1.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -6251,7 +6644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a978ff11f15b24e5c05a7123cf2b68f41e763546699781a924ef4e2cf43a49" dependencies = [ "bindgen 0.72.1", - "bitflags", + "bitflags 2.13.2", "fslock", "gzip-header", "home", @@ -6268,7 +6661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3701ffb3692a24808503d17f758b0a3f688d2c451779247982566b5941033e63" dependencies = [ "bindgen 0.70.1", - "bitflags", + "bitflags 2.13.2", "cc", "paste", "temporal_capi", @@ -6447,7 +6840,7 @@ version = "0.258.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" dependencies = [ - "bitflags", + "bitflags 2.13.2", "hashbrown 0.17.1", "indexmap", "semver", @@ -6460,7 +6853,7 @@ version = "0.262.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" dependencies = [ - "bitflags", + "bitflags 2.13.2", "indexmap", "semver", ] @@ -6484,7 +6877,7 @@ checksum = "4fbdd39f5c66f63ba6b5b31391b324b7ade7237d29c866cdb561330118ef436e" dependencies = [ "addr2line", "async-trait", - "bitflags", + "bitflags 2.13.2", "bumpalo", "bytes", "cc", @@ -6566,7 +6959,7 @@ version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "952086551d2041430a159fe4bda3471cfea3d52846b93c825829cea9dd01954d" dependencies = [ - "base64", + "base64 0.22.1", "directories-next", "log", "postcard", @@ -6706,7 +7099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cad5e8cef56790d4741b62f7b6c971af53b2ad4c2d1603f8ffe86872763147aa" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.13.2", "heck", "indexmap", "wit-component", @@ -6720,7 +7113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "284555904d42017e049fd57c05810b7975075c322481c045f99b07385fb7a5ba" dependencies = [ "async-trait", - "bitflags", + "bitflags 2.13.2", "bytes", "cap-primitives", "futures", @@ -6829,6 +7222,107 @@ dependencies = [ "url", ] +[[package]] +name = "webauthn-attestation-ca" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6475c0bbd1a3f04afaa3e98880408c5be61680c5e6bd3c6f8c250990d5d3e18e" +dependencies = [ + "base64urlsafedata", + "openssl", + "openssl-sys", + "serde", + "tracing", + "uuid", +] + +[[package]] +name = "webauthn-authenticator-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779e9c80ff248c7e12ea967f909249101f6e86f70fccd742d4b66c490c1710b4" +dependencies = [ + "async-stream", + "async-trait", + "base64 0.21.7", + "base64urlsafedata", + "bitflags 1.3.2", + "futures", + "hex", + "nom", + "num-derive", + "num-traits", + "openssl", + "openssl-sys", + "serde", + "serde_bytes", + "serde_cbor_2", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", + "unicode-normalization", + "url", + "uuid", + "webauthn-rs-core", + "webauthn-rs-proto", +] + +[[package]] +name = "webauthn-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c548915e0e92ee946bbf2aecf01ea21bef53d974b0793cc6732ba81a03fc422" +dependencies = [ + "base64urlsafedata", + "serde", + "tracing", + "url", + "uuid", + "webauthn-rs-core", +] + +[[package]] +name = "webauthn-rs-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "296d2d501feb715d80b8e186fb88bab1073bca17f460303a1013d17b673bea6a" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "der-parser 9.0.0", + "hex", + "nom", + "openssl", + "openssl-sys", + "rand 0.9.5", + "rand_chacha 0.9.0", + "serde", + "serde_cbor_2", + "serde_json", + "thiserror 1.0.69", + "tracing", + "url", + "uuid", + "webauthn-attestation-ca", + "webauthn-rs-proto", + "x509-parser 0.16.0", +] + +[[package]] +name = "webauthn-rs-proto" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c37393beac9c1ed1ca6dbb30b1e01783fb316ab3a45d90ecd48c99052dd7ef1e" +dependencies = [ + "base64 0.21.7", + "base64urlsafedata", + "serde", + "serde_json", + "url", +] + [[package]] name = "webpki-roots" version = "1.0.11" @@ -6868,7 +7362,7 @@ version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad3dcdc2980b91d0e7db6975caee4f553c6e37fd8e1464a38b09baceed3fe568" dependencies = [ - "bitflags", + "bitflags 2.13.2", "thiserror 2.0.21", "tracing", "wasmtime", @@ -7155,7 +7649,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "windows-sys 0.59.0", ] @@ -7165,7 +7659,7 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" dependencies = [ - "bitflags", + "bitflags 2.13.2", ] [[package]] @@ -7175,7 +7669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.13.2", "indexmap", "log", "serde", @@ -7251,12 +7745,29 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7069fba5b66b9193bd2c5d3d4ff12b839118f6bcbef5328efafafb5395cf63da" dependencies = [ - "asn1-rs", + "asn1-rs 0.5.2", "data-encoding", - "der-parser", + "der-parser 8.2.0", "lazy_static", "nom", - "oid-registry", + "oid-registry 0.6.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom", + "oid-registry 0.7.1", "rusticata-macros", "thiserror 1.0.69", "time", diff --git a/host/Cargo.toml b/host/Cargo.toml index 1c7244e..88863a7 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -33,7 +33,26 @@ wasmtime-wasi-http = "=49.0.3" serde = { version = "1.0.229", features = ["derive"] } serde_yaml_ng = "0.10.0" rustls = "0.23" +chacha20poly1305 = "0.10" +zeroize = "1" +hex = "0.4" +http = "1" +url = "2" +thiserror = "2" +base64 = "0.22" +hmac = "0.12" +sha2 = "0.10" +p256 = { version = "0.13", features = ["ecdh", "ecdsa", "pem"] } +ed25519-dalek = { version = "2", features = ["pkcs8", "pem"] } +rsa = { version = "0.9", features = ["sha2"] } +aes-gcm = "0.10" +hkdf = "0.12" +ssh-key = { version = "0.6.7", features = ["encryption", "ed25519", "ecdsa", "rsa", "p256"] } +uuid = { version = "1.29.0", features = ["v4"] } +webauthn-rs = "0.5" +openssl = { version = "0.10.82", features = ["vendored"] } [dev-dependencies] tempfile = "3" wat = "1" +webauthn-authenticator-rs = { version = "0.5", features = ["softpasskey"] } diff --git a/host/src/cred/keys.rs b/host/src/cred/keys.rs new file mode 100644 index 0000000..b19d814 --- /dev/null +++ b/host/src/cred/keys.rs @@ -0,0 +1,305 @@ +//! Key readers and signature/public-key codecs. No grant or storage policy here. +use super::sign::{Result, SignResult, error, invalid_key}; +use base64::{ + Engine, + engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, +}; +use ed25519_dalek::{Signer, SigningKey}; +use hmac::{Hmac, Mac}; +use rsa::{ + pkcs1::DecodeRsaPrivateKey, + pkcs8::{DecodePrivateKey, EncodePublicKey}, + traits::PublicKeyParts, +}; +use serde_json::json; +use sha2::{Digest, Sha256}; +use sha2::{Sha384, Sha512}; +use zeroize::Zeroizing; + +pub(super) enum Key { + Ed(SigningKey), + Ec(p256::ecdsa::SigningKey), + Rsa(rsa::RsaPrivateKey), + Mac(Zeroizing>), +} +fn decode(s: &str) -> Result>> { + Ok(Zeroizing::new( + STANDARD + .decode(s) + .or_else(|_| URL_SAFE_NO_PAD.decode(s)) + .map_err(|_| invalid_key())?, + )) +} +fn family(format: &str) -> &str { + match format { + "ed25519-raw" => "ed25519", + "rs256" | "rsa-sha2-256" | "rsa-sha2-512" => "rsa", + s if s.starts_with("hmac-") => "hmac", + s => s, + } +} +pub(super) fn read(value: &str, format: &str, passphrase: Option<&str>) -> Result { + let value = value.trim(); + let family = family(format); + let key = if value.starts_with("-----BEGIN OPENSSH PRIVATE KEY-----") { + let key = ssh_key::PrivateKey::from_openssh(value).map_err(|_| invalid_key())?; + let key = if key.is_encrypted() { + key.decrypt(passphrase.ok_or_else(invalid_key)?) + .map_err(|_| invalid_key())? + } else { + key + }; + match key.key_data() { + ssh_key::private::KeypairData::Ed25519(k) => { + Key::Ed(SigningKey::from_bytes(&k.private.to_bytes())) + } + ssh_key::private::KeypairData::Ecdsa( + k @ ssh_key::private::EcdsaKeypair::NistP256 { .. }, + ) => Key::Ec( + p256::ecdsa::SigningKey::from_slice(k.private_key_bytes()) + .map_err(|_| invalid_key())?, + ), + ssh_key::private::KeypairData::Rsa(k) => { + // ssh-key 0.6.7's TryFrom<&RsaKeypair> accidentally passes p twice. + // Convert the parsed components explicitly, preserving p and q. + let int = |n: &ssh_key::Mpint| rsa::BigUint::try_from(n).map_err(|_| invalid_key()); + let key = rsa::RsaPrivateKey::from_components( + int(&k.public.n)?, + int(&k.public.e)?, + int(&k.private.d)?, + vec![int(&k.private.p)?, int(&k.private.q)?], + ) + .map_err(|_| invalid_key())?; + key.validate().map_err(|_| invalid_key())?; + Key::Rsa(key) + } + _ => return Err(invalid_key()), + } + } else if value.starts_with("-----BEGIN ") { + match family { + "ed25519" => Key::Ed(SigningKey::from_pkcs8_pem(value).map_err(|_| invalid_key())?), + "es256" => { + let key = if value.starts_with("-----BEGIN EC PRIVATE KEY-----") { + p256::SecretKey::from_sec1_pem(value).map_err(|_| invalid_key())? + } else { + p256::SecretKey::from_pkcs8_pem(value).map_err(|_| invalid_key())? + }; + Key::Ec(key.into()) + } + "rsa" => Key::Rsa(if value.starts_with("-----BEGIN RSA PRIVATE KEY-----") { + rsa::RsaPrivateKey::from_pkcs1_pem(value).map_err(|_| invalid_key())? + } else { + rsa::RsaPrivateKey::from_pkcs8_pem(value).map_err(|_| invalid_key())? + }), + _ => return Err(invalid_key()), + } + } else if value.starts_with('{') { + let j: serde_json::Value = serde_json::from_str(value).map_err(|_| invalid_key())?; + let bytes = |name: &str| -> Result>> { + decode(j[name].as_str().ok_or_else(invalid_key)?) + }; + match j["kty"].as_str() { + Some("oct") => Key::Mac(bytes("k")?), + Some("OKP") if j["crv"] == "Ed25519" => { + let d = bytes("d")?; + let k = SigningKey::from_bytes(d.as_slice().try_into().map_err(|_| invalid_key())?); + if bytes("x")?.as_slice() != k.verifying_key().as_bytes() { + return Err(invalid_key()); + } + Key::Ed(k) + } + Some("EC") if j["crv"] == "P-256" => { + let k = + p256::ecdsa::SigningKey::from_slice(&bytes("d")?).map_err(|_| invalid_key())?; + let point = k.verifying_key().to_encoded_point(false); + if bytes("x")?.as_slice() != point.x().ok_or_else(invalid_key)?.as_slice() + || bytes("y")?.as_slice() != point.y().ok_or_else(invalid_key)?.as_slice() + { + return Err(invalid_key()); + } + Key::Ec(k) + } + Some("RSA") => { + let int = |name| -> Result { + Ok(rsa::BigUint::from_bytes_be(&bytes(name)?)) + }; + let k = rsa::RsaPrivateKey::from_components( + int("n")?, + int("e")?, + int("d")?, + vec![int("p")?, int("q")?], + ) + .map_err(|_| invalid_key())?; + k.validate().map_err(|_| invalid_key())?; + Key::Rsa(k) + } + _ => return Err(invalid_key()), + } + } else { + let bytes = decode(value)?; + match family { + "ed25519" => Key::Ed(SigningKey::from_bytes( + bytes.as_slice().try_into().map_err(|_| invalid_key())?, + )), + "es256" => { + Key::Ec(p256::ecdsa::SigningKey::from_slice(&bytes).map_err(|_| invalid_key())?) + } + "hmac" => Key::Mac(bytes), + _ => return Err(invalid_key()), + } + }; + if !matches!( + (&key, family), + (Key::Ed(_), "ed25519") + | (Key::Ec(_), "es256") + | (Key::Rsa(_), "rsa") + | (Key::Mac(_), "hmac") + ) { + return Err(invalid_key()); + } + Ok(key) +} +pub(super) fn check(format: &str, padding: Option<&str>, hash: Option<&str>) -> Result<()> { + match format { + "ed25519" | "ed25519-raw" | "es256" | "hmac-sha256" | "hmac-sha384" | "hmac-sha512" + if padding.is_none() && hash.is_none() => + { + Ok(()) + } + "rs256" | "rsa-sha2-256" | "rsa-sha2-512" + if padding == Some("pkcs1v15") + && hash + == Some(if format == "rsa-sha2-512" { + "sha512" + } else { + "sha256" + }) => + { + Ok(()) + } + _ => Err(error( + "invalid_params", + "unknown algorithm or inconsistent padding/hash; RSA requires padding=pkcs1v15 and explicit sha256/sha512 matching the algorithm", + )), + } +} +pub(super) fn sign_key(key: &Key, format: &str, payload: &[u8]) -> Result> { + Ok(match key { + Key::Ed(k) => k.sign(payload).to_bytes().to_vec(), + Key::Ec(k) => { + let s: p256::ecdsa::Signature = k.sign(payload); + s.to_bytes().to_vec() + } + Key::Rsa(k) => if format == "rsa-sha2-512" { + k.sign(rsa::Pkcs1v15Sign::new::(), &Sha512::digest(payload)) + } else { + k.sign(rsa::Pkcs1v15Sign::new::(), &Sha256::digest(payload)) + } + .map_err(|_| invalid_key())?, + Key::Mac(k) => { + macro_rules! mac { + ($h:ty) => {{ + let mut mac = Hmac::<$h>::new_from_slice(k).map_err(|_| invalid_key())?; + mac.update(payload); + mac.finalize().into_bytes().to_vec() + }}; + } + match format { + "hmac-sha384" => mac!(Sha384), + "hmac-sha512" => mac!(Sha512), + _ => mac!(Sha256), + } + } + }) +} +fn string(out: &mut Vec, bytes: &[u8]) { + out.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); + out.extend_from_slice(bytes); +} +fn mpint(out: &mut Vec, bytes: &[u8]) { + let bytes = &bytes[bytes.iter().position(|b| *b != 0).unwrap_or(bytes.len())..]; + let mut positive = Vec::new(); + if bytes.first().is_some_and(|b| b & 128 != 0) { + positive.push(0); + } + positive.extend_from_slice(bytes); + string(out, &positive); +} +fn wire_signature(format: &str, signature: &[u8]) -> Result> { + let (algorithm, signature) = match format { + "ed25519" | "ed25519-raw" => ("ssh-ed25519", signature.to_vec()), + "rs256" | "rsa-sha2-256" => ("rsa-sha2-256", signature.to_vec()), + "rsa-sha2-512" => ("rsa-sha2-512", signature.to_vec()), + "es256" => { + let mut s = Vec::new(); + mpint(&mut s, &signature[..32]); + mpint(&mut s, &signature[32..]); + ("ecdsa-sha2-nistp256", s) + } + _ => { + return Err(error( + "invalid_params", + "SSH wire output requires an asymmetric key", + )); + } + }; + let mut out = Vec::new(); + string(&mut out, algorithm.as_bytes()); + string(&mut out, &signature); + Ok(out) +} +pub(super) fn output(format: &str, output: &str, signature: Vec) -> Result { + let (signature, bytes) = match output { + "raw" => (String::new(), Some(signature)), + "hex" => (hex::encode(signature), None), + "jws" => (URL_SAFE_NO_PAD.encode(signature), None), + "ssh" => (STANDARD.encode(wire_signature(format, &signature)?), None), + "base64" => (STANDARD.encode(signature), None), + _ => return Err(error("invalid_params", "unknown output format")), + }; + Ok(SignResult { signature, bytes }) +} +pub(super) fn public(key: &Key, output: &str) -> Result { + let raw = match key { + Key::Ed(k) => k.verifying_key().to_bytes().to_vec(), + Key::Ec(k) => k + .verifying_key() + .to_encoded_point(false) + .as_bytes() + .to_vec(), + Key::Rsa(k) => k + .to_public_key() + .to_public_key_der() + .map_err(|_| invalid_key())? + .as_bytes() + .to_vec(), + Key::Mac(_) => return Err(error("invalid_params", "symmetric keys have no public key")), + }; + Ok(match output { + "base64" => STANDARD.encode(&raw), + "hex" => hex::encode(&raw), + "pem" => match key { + Key::Ed(k) => k.verifying_key().to_public_key_pem(Default::default()), + Key::Ec(k) => k.verifying_key().to_public_key_pem(Default::default()), + Key::Rsa(k) => k.to_public_key().to_public_key_pem(Default::default()), + _ => unreachable!(), + }.map_err(|_| invalid_key())?, + "jwk" => match key { + Key::Ed(_) => json!({"kty":"OKP","crv":"Ed25519","x":URL_SAFE_NO_PAD.encode(&raw)}), + Key::Ec(_) => json!({"kty":"EC","crv":"P-256","x":URL_SAFE_NO_PAD.encode(&raw[1..33]),"y":URL_SAFE_NO_PAD.encode(&raw[33..])}), + Key::Rsa(k) => json!({"kty":"RSA","n":URL_SAFE_NO_PAD.encode(k.n().to_bytes_be()),"e":URL_SAFE_NO_PAD.encode(k.e().to_bytes_be())}), + _ => unreachable!(), + }.to_string(), + "ssh" => { + let mut out = Vec::new(); + match key { + Key::Ed(_) => { string(&mut out, b"ssh-ed25519"); string(&mut out, &raw); }, + Key::Ec(_) => { string(&mut out, b"ecdsa-sha2-nistp256"); string(&mut out, b"nistp256"); string(&mut out, &raw); }, + Key::Rsa(k) => { string(&mut out, b"ssh-rsa"); mpint(&mut out, &k.e().to_bytes_be()); mpint(&mut out, &k.n().to_bytes_be()); }, + _ => unreachable!(), + } + STANDARD.encode(out) + } + _ => return Err(error("invalid_params", "unknown public key output")), + }) +} diff --git a/host/src/cred/mod.rs b/host/src/cred/mod.rs index 282e060..5740c6c 100644 --- a/host/src/cred/mod.rs +++ b/host/src/cred/mod.rs @@ -1,3 +1,36 @@ //! The credentials manager (P2, plan §4.4): items, grants, requests and audit in the host, //! outside the world. Programs in the world never see secret values. +//! +//! `Cred::open` loads `cred.sqlite` and the host master key. Read methods return public snapshots. +//! `Caller` carries a binary path and the nearest-first conversation ancestry. +//! `types::Subject` binds grants to a conversation subtree or a binary path. +//! `file_request` normalizes item identities and replays keyed requests. Edit requests carry public diffs. +//! `approve` and `deny` trust the route layer's verified approver identity. +//! That identity can be `None` during bootstrap, before any passkey registration. +//! Each decision commits one SQLite transaction. `subscribe` reports committed decisions across threads. +//! Subscribers must read SQLite after a lag or restart. The durable task layer calls `expire_requests`. +//! `inject` transforms one request's URL and headers. The body stays outside this library. +//! An unmatched request forwards unchanged. An unauthorized match names the item and its filed request. +//! `audit_sign` records signature metadata without the signed payload. +pub mod entry; +pub mod webauthn; +pub mod store; +pub mod schema; +pub mod seal; +pub mod items; +pub mod types; +pub mod grants; +pub mod requests; +pub mod edits; +pub mod summary; +pub mod audit; +pub mod inject; +pub mod error; +pub use store::Cred; +pub use error::{Error, Result}; +pub use types::Caller; pub mod script; +pub mod keys; +pub mod sign; +pub mod oauth2; +pub mod v8; diff --git a/host/src/cred/oauth2.rs b/host/src/cred/oauth2.rs new file mode 100644 index 0000000..6ad2be0 --- /dev/null +++ b/host/src/cred/oauth2.rs @@ -0,0 +1,119 @@ +//! OAuth2 refresh exchange and a process-local cache keyed by item and version. +use std::{ + collections::BTreeMap, + time::{Duration, Instant}, +}; +use zeroize::Zeroizing; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Error; +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("OAuth refresh failed or returned an invalid response") + } +} +impl std::error::Error for Error {} + +/// HTTP adapter. Implementations must reject unsuccessful HTTP status codes. +pub trait Http: Sync { + fn send( + &self, + request: &Request, + deadline: Instant, + ) -> impl std::future::Future>, Error>> + Send; +} +/// Contains credentials. Deliberately does not implement Debug or Serialize. +pub struct Request { + pub url: String, + pub method: &'static str, + pub headers: Vec<(&'static str, String)>, + pub body: Zeroizing, +} +pub fn request( + token_url: &str, + refresh_token: &str, + client_id: &str, + client_secret: &str, +) -> Result { + let target = url::Url::parse(token_url).map_err(|_| Error)?; + if target.scheme() != "https" + || !target.username().is_empty() + || target.password().is_some() + || target.fragment().is_some() + || [refresh_token, client_id, client_secret].contains(&"") + { + return Err(Error); + } + let body = url::form_urlencoded::Serializer::new(String::new()) + .append_pair("grant_type", "refresh_token") + .append_pair("refresh_token", refresh_token) + .append_pair("client_id", client_id) + .append_pair("client_secret", client_secret) + .finish(); + Ok(Request { + url: token_url.into(), + method: "POST", + headers: vec![ + ("Content-Type", "application/x-www-form-urlencoded".into()), + ("Content-Length", body.len().to_string()), + ], + body: Zeroizing::new(body), + }) +} +#[derive(Clone)] +pub struct Token { + pub value: Zeroizing, + pub scopes: Option>, + pub expires: Instant, +} +pub fn decode(bytes: &[u8]) -> Result { + #[derive(serde::Deserialize)] + struct Response { + access_token: String, + token_type: String, + expires_in: u64, + scope: Option, + } + let r: Response = serde_json::from_slice(bytes).map_err(|_| Error)?; + if r.access_token.is_empty() + || !r.token_type.eq_ignore_ascii_case("bearer") + || r.expires_in == 0 + || http::header::HeaderValue::from_str(&format!("Bearer {}", r.access_token)).is_err() + { + return Err(Error); + } + Ok(Token { + value: Zeroizing::new(r.access_token), + scopes: r + .scope + .map(|s| s.split_whitespace().map(str::to_owned).collect()), + expires: Instant::now() + Duration::from_secs(r.expires_in.min(86400).saturating_sub(30)), + }) +} +/// The caller owns synchronization and endpoint policy. No lock crosses an HTTP call here. +#[derive(Default)] +pub struct Cache { + tokens: BTreeMap<(String, i64), Token>, +} +impl Cache { + pub fn clear(&mut self) { + self.tokens.clear(); + } + pub async fn refresh( + &mut self, + item: &str, + version: i64, + request: &Request, + http: &impl Http, + deadline: Instant, + ) -> Result { + let key = (item.to_owned(), version); + if let Some(t) = self.tokens.get(&key).filter(|t| t.expires > Instant::now()) { + return Ok(t.clone()); + } + self.tokens.retain(|(id, _), _| id != item); + let token = decode(&http.send(request, deadline).await?)?; + self.tokens.insert(key, token.clone()); + Ok(token) + } +} diff --git a/host/src/cred/sign.rs b/host/src/cred/sign.rs new file mode 100644 index 0000000..c2c0a12 --- /dev/null +++ b/host/src/cred/sign.rs @@ -0,0 +1,61 @@ +//! Pure signing and public-key export over decrypted field values. No storage or grant policy. +use super::keys; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Error { + pub kind: &'static str, + pub message: &'static str, +} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.message) + } +} +impl std::error::Error for Error {} +pub type Result = std::result::Result; +pub(super) fn error(kind: &'static str, message: &'static str) -> Error { + Error { kind, message } +} +pub(super) fn invalid_key() -> Error { + error("invalid_key", "invalid signing key encoding or length") +} + +#[derive(Debug, serde::Serialize)] +pub struct SignResult { + pub signature: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub bytes: Option>, +} + +/// Sign bytes. RSA requires explicit, matching padding and digest parameters. +pub fn sign( + value: &str, + passphrase: Option<&str>, + format: &str, + payload: &[u8], + output: &str, + padding: Option<&str>, + hash: Option<&str>, +) -> Result { + keys::check(format, padding, hash)?; + let key = keys::read(value, format, passphrase)?; + keys::output(format, output, keys::sign_key(&key, format, payload)?) +} + +/// Export only the public part of an asymmetric key. +pub fn public_key( + value: &str, + passphrase: Option<&str>, + format: &str, + output: &str, +) -> Result { + keys::public(&keys::read(value, format, passphrase)?, output) +} + +/// Generate a P-256 private key in PKCS#8 PEM format for ES256. +pub fn generate_es256() -> Result> { + use p256::pkcs8::EncodePrivateKey; + p256::SecretKey::random(&mut rsa::rand_core::OsRng) + .to_pkcs8_pem(Default::default()) + .map_err(|_| invalid_key()) +} diff --git a/host/src/cred/v8.rs b/host/src/cred/v8.rs new file mode 100644 index 0000000..f437791 --- /dev/null +++ b/host/src/cred/v8.rs @@ -0,0 +1,196 @@ +//! Fresh, bounded V8 isolates for credential scripts. Each call owns a dedicated worker thread. +use super::script::{Error, ErrorKind, Limits, Script}; +use deno_core::{JsRuntime, RuntimeOptions, v8}; +use std::{ + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + mpsc, + }, + time::Duration, +}; + +#[derive(Default)] +pub struct V8; +fn failure(kind: ErrorKind) -> Error { + Error { + kind, + message: match kind { + ErrorKind::Limit => "script time, memory or stack limit exceeded", + ErrorKind::Syntax => "invalid script or JSON", + ErrorKind::Throw => "script threw or rejected", + ErrorKind::NonJson => "result is not JSON", + ErrorKind::Engine => "script engine failure", + } + .into(), + } +} +impl Script for V8 { + fn evaluate( + &self, + source: &str, + input: &str, + export: Option<&str>, + limits: Limits, + ) -> Result { + let source = source.to_owned(); + let input = input.to_owned(); + let export = export.unwrap_or("default").to_owned(); + std::thread::spawn(move || evaluate(source, input, export, limits)) + .join() + .map_err(|_| failure(ErrorKind::Engine))? + } +} +fn evaluate( + source: String, + input: String, + export: String, + limits: Limits, +) -> Result { + let heap = limits + .max_memory_bytes + .unwrap_or(8 * 1024 * 1024) + .clamp(256 * 1024, 32 * 1024 * 1024); + let mut runtime = JsRuntime::new(RuntimeOptions { + // Bootstrap before imposing the caller's budget: V8 cannot recover from + // bootstrap OOM because the runtime cannot install its callback yet. + create_params: Some(v8::CreateParams::default().heap_limits(0, 32 * 1024 * 1024)), + ..Default::default() + }); + if runtime.v8_isolate().get_heap_statistics().total_heap_size() > heap { + return Err(failure(ErrorKind::Limit)); + } + // Removing a temporary callback sets the heap limit after bootstrap. + runtime.add_near_heap_limit_callback(|current, _| current); + runtime.remove_near_heap_limit_callback(heap); + let handle = runtime.v8_isolate().thread_safe_handle(); + let limited = Arc::new(AtomicBool::new(false)); + let flag = limited.clone(); + let heap_handle = handle.clone(); + runtime.add_near_heap_limit_callback(move |current, _| { + flag.store(true, Ordering::SeqCst); + heap_handle.terminate_execution(); + // V8 needs emergency headroom to unwind after termination. + current + 16 * 1024 * 1024 + }); + let (done, wait) = mpsc::channel(); + let flag = limited.clone(); + let watchdog = std::thread::spawn(move || { + if wait + .recv_timeout(Duration::from_millis( + limits.max_time_ms.unwrap_or(100).clamp(1, 1000), + )) + .is_err() + { + flag.store(true, Ordering::SeqCst); + handle.terminate_execution(); + } + }); + let task = async { + runtime + .execute_script("sandbox.js", BOOTSTRAP) + .map_err(|_| failure(ErrorKind::Engine))?; + let input = runtime + .execute_script( + "input.js", + format!("JSON.parse({})", serde_json::to_string(&input).unwrap()), + ) + .map_err(|_| failure(ErrorKind::Syntax))?; + let spec = deno_core::ModuleSpecifier::parse("file:///credential.js").unwrap(); + let module = runtime + .load_main_es_module_from_code(&spec, source) + .await + .map_err(|_| failure(ErrorKind::Syntax))?; + let evaluated = runtime.mod_evaluate(module); + runtime + .with_event_loop_promise(evaluated, Default::default()) + .await + .map_err(|e| { + if e.to_string().contains("call stack") { + failure(ErrorKind::Limit) + } else { + failure(ErrorKind::Throw) + } + })?; + let ns = runtime + .get_module_namespace(module) + .map_err(|_| failure(ErrorKind::Engine))?; + { + deno_core::scope!(scope, &mut runtime); + let ns = v8::Local::new(scope, ns); + let global = scope.get_current_context().global(scope); + let key = + v8::String::new(scope, "__module").ok_or_else(|| failure(ErrorKind::Limit))?; + global.set(scope, key.into(), ns.into()); + let input = v8::Local::new(scope, input); + let key = v8::String::new(scope, "__input").ok_or_else(|| failure(ErrorKind::Limit))?; + global.set(scope, key.into(), input); + } + let code = format!( + r#"(async()=>{{ + const f=__module[{}]; + if(typeof f!=='function') return JSON.stringify({{kind:'Engine'}}); + let value; + try {{ value=await f(__input); }} catch(e) {{ const message=String(e?.message); return JSON.stringify({{kind: e instanceof RangeError && message.includes('call stack') ? 'Limit':'Throw'}}); }} + try {{ const text=JSON.stringify(value); return JSON.stringify(text===undefined?{{kind:'NonJson'}}:{{text}}); }} catch(e) {{ return JSON.stringify({{kind:e instanceof RangeError && String(e.message).includes('call stack') ? 'Limit':'NonJson'}}); }} + }})()"#, + serde_json::to_string(&export).unwrap() + ); + let value = runtime + .execute_script("call.js", code) + .map_err(|_| failure(ErrorKind::Throw))?; + let resolve = runtime.resolve(value); + let value = runtime + .with_event_loop_promise(resolve, Default::default()) + .await + .map_err(|_| failure(ErrorKind::Engine))?; + let text = { + deno_core::scope!(scope, &mut runtime); + v8::Local::new(scope, value).to_rust_string_lossy(scope) + }; + let result: serde_json::Value = + serde_json::from_str(&text).map_err(|_| failure(ErrorKind::Engine))?; + match result["text"].as_str() { + Some(s) => Ok(s.to_owned()), + None => Err(failure(match result["kind"].as_str() { + Some("NonJson") => ErrorKind::NonJson, + Some("Throw") => ErrorKind::Throw, + Some("Limit") => ErrorKind::Limit, + _ => ErrorKind::Engine, + })), + } + }; + let result = tokio::runtime::Builder::new_current_thread() + .build() + .map_err(|_| failure(ErrorKind::Engine))? + .block_on(task); + let _ = done.send(()); + let _ = watchdog.join(); + if limited.load(Ordering::SeqCst) { + Err(failure(ErrorKind::Limit)) + } else { + result + } +} +const BOOTSTRAP: &str = r#" +delete globalThis.Date; delete Math.random; +for (const name of ['Deno','__bootstrap','console','performance']) delete globalThis[name]; +const alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; +globalThis.btoa = value => { + const s=String(value); let out=''; + for(let i=0;i255||b>255||c>255) throw Error('invalid base64 input'); + const n=(a<<16)|((b||0)<<8)|(c||0); + out+=alphabet[(n>>>18)&63]+alphabet[(n>>>12)&63]+(i+1>>6)&63]:'=')+(i+2 { + let s=String(value).replace(/[\t\n\f\r ]/g,''); + if(s.length%4===0) s=s.replace(/==?$/,''); + if(s.length%4===1||/[^A-Za-z0-9+/]/.test(s)) throw Error('invalid base64 input'); + let out='', bits=0, n=0; + for(const c of s){n=(n<<6)|alphabet.indexOf(c);bits+=6;if(bits>=8){bits-=8;out+=String.fromCharCode((n>>>bits)&255);}} + return out; +}; +"#; diff --git a/host/tests/cred_oauth2.rs b/host/tests/cred_oauth2.rs new file mode 100644 index 0000000..81f52e8 --- /dev/null +++ b/host/tests/cred_oauth2.rs @@ -0,0 +1,100 @@ +//! Refresh form encoding, response validation, and revision-aware cache behavior. +use sausage::cred::oauth2::*; +use std::{ + sync::atomic::{AtomicUsize, Ordering}, + time::{Duration, Instant}, +}; +use zeroize::Zeroizing; +struct Fake(AtomicUsize); +impl Http for Fake { + async fn send(&self, request: &Request, _: Instant) -> Result>, Error> { + assert_eq!(request.method, "POST"); + self.0.fetch_add(1, Ordering::SeqCst); + Ok(Zeroizing::new(br#"{"access_token":"SYNTHETIC","token_type":"Bearer","expires_in":3600,"scope":"one two"}"#.to_vec())) + } +} +#[tokio::test] +async fn encoding_and_cache() { + let req = request( + "https://tokens.example.test/token", + "REFRESH&=+", + "CLIENT", + "SECRET&=+", + ) + .unwrap(); + assert!( + req.body.as_str() + == "grant_type=refresh_token&refresh_token=REFRESH%26%3D%2B&client_id=CLIENT&client_secret=SECRET%26%3D%2B" + ); + assert_eq!(req.headers[1].1, req.body.len().to_string()); + let fake = Fake(AtomicUsize::new(0)); + let mut cache = Cache::default(); + let deadline = Instant::now() + Duration::from_secs(1); + let t = cache.refresh("id", 1, &req, &fake, deadline).await.unwrap(); + assert_eq!(t.scopes.unwrap(), vec!["one", "two"]); + cache.refresh("id", 1, &req, &fake, deadline).await.unwrap(); + assert_eq!(fake.0.load(Ordering::SeqCst), 1); + cache.refresh("id", 2, &req, &fake, deadline).await.unwrap(); + assert_eq!(fake.0.load(Ordering::SeqCst), 2); + cache.clear(); + cache.refresh("id", 2, &req, &fake, deadline).await.unwrap(); + assert_eq!(fake.0.load(Ordering::SeqCst), 3); + for url in [ + "http://tokens.example.test/token", + "https://user@tokens.example.test/token", + "https://tokens.example.test/token#x", + ] { + assert!(request(url, "r", "c", "s").is_err()); + } +} +#[tokio::test] +async fn expiry_and_failure() { + struct Expired(AtomicUsize); + impl Http for Expired { + async fn send(&self, _: &Request, _: Instant) -> Result>, Error> { + self.0.fetch_add(1, Ordering::SeqCst); + Ok(Zeroizing::new( + br#"{"access_token":"SYNTHETIC","token_type":"Bearer","expires_in":1}"#.to_vec(), + )) + } + } + struct Failed; + impl Http for Failed { + async fn send(&self, _: &Request, _: Instant) -> Result>, Error> { + Err(Error) + } + } + let req = request("https://tokens.example.test/token", "r", "c", "s").unwrap(); + let mut cache = Cache::default(); + let http = Expired(AtomicUsize::new(0)); + let deadline = Instant::now() + Duration::from_secs(1); + cache.refresh("id", 1, &req, &http, deadline).await.unwrap(); + cache.refresh("id", 1, &req, &http, deadline).await.unwrap(); + assert_eq!(http.0.load(Ordering::SeqCst), 2); + assert!( + cache + .refresh("id", 2, &req, &Failed, deadline) + .await + .is_err() + ); + cache.refresh("id", 2, &req, &http, deadline).await.unwrap(); + assert_eq!(http.0.load(Ordering::SeqCst), 3); +} + +#[test] +fn validation() { + for data in [ + br#"{"error":"invalid_grant","secret":"DO NOT ECHO"}"#.as_slice(), + br#"{"access_token":"","token_type":"Bearer","expires_in":3600}"#, + br#"{"access_token":"x","token_type":"MAC","expires_in":3600}"#, + br#"{"access_token":"x","token_type":"Bearer","expires_in":0}"#, + br#"{"access_token":"x\r\ny","token_type":"Bearer","expires_in":3600}"#, + ] { + let e = decode(data).err().unwrap(); + assert!(!e.to_string().contains("DO NOT ECHO")); + } + assert!( + decode(br#"{"access_token":"x","token_type":"Bearer","expires_in":18446744073709551615}"#) + .is_ok() + ); +} diff --git a/host/tests/cred_sign.rs b/host/tests/cred_sign.rs new file mode 100644 index 0000000..debac77 --- /dev/null +++ b/host/tests/cred_sign.rs @@ -0,0 +1,283 @@ +//! RFC vectors and public codecs for pure signing. +use base64::{ + Engine, + engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, +}; +use sausage::cred::sign::*; +fn raw(key: &[u8], alg: &str, payload: &[u8]) -> Vec { + sign(&STANDARD.encode(key), None, alg, payload, "raw", None, None) + .unwrap() + .bytes + .unwrap() +} +#[test] +fn ed25519_readers_and_wire() { + use ed25519_dalek::pkcs8::EncodePrivateKey; + let key = ed25519_dalek::SigningKey::from_bytes(&[7; 32]); + let pem = key.to_pkcs8_pem(Default::default()).unwrap(); + let jwk = serde_json::json!({"kty":"OKP","crv":"Ed25519","d":URL_SAFE_NO_PAD.encode(key.to_bytes()),"x":URL_SAFE_NO_PAD.encode(key.verifying_key().as_bytes())}).to_string(); + let ssh = ssh_key::PrivateKey::new( + ssh_key::private::Ed25519Keypair::from(&key).into(), + "synthetic", + ) + .unwrap() + .to_openssh(Default::default()) + .unwrap(); + let expected = raw(&key.to_bytes(), "ed25519", b"message"); + for value in [pem.as_str(), jwk.as_str(), ssh.as_str()] { + let sig = sign(value, None, "ed25519", b"message", "ssh", None, None).unwrap(); + let wire = STANDARD.decode(sig.signature).unwrap(); + assert_eq!(&wire[..19], b"\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x40"); + assert_eq!(&wire[19..], expected); + for output in ["pem", "jwk", "ssh", "hex", "base64"] { + assert!(public_key(value, None, "ed25519", output).is_ok()); + } + } + assert!(public_key(&STANDARD.encode([7; 32]), None, "hmac-sha256", "hex").is_err()); +} + +#[test] +fn rfc8032_and_4231() { + let seed = + hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60").unwrap(); + assert_eq!( + hex::encode(raw(&seed, "ed25519", b"")), + concat!( + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155", + "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b" + ) + ); + for (alg, expected) in [ + ( + "hmac-sha256", + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", + ), + ( + "hmac-sha384", + "afd03944d84895626b0825f4ab46907f15f9dadbe4101ec682aa034c7cebc59cfaea9ea9076ede7f4af152e8b2fa9cb6", + ), + ( + "hmac-sha512", + "87aa7cdea5ef619d4ff0b4241a1d6cb02379f4e2ce4ec2787ad0b30545e17cde daa833b7d6b8a702038b274eaea3f4e4be9d914eeb61f1702e696c203a126854", + ), + ] { + assert_eq!( + hex::encode(raw(&[0x0b; 20], alg, b"Hi There")), + expected.replace(' ', "") + ); + } +} +#[test] +fn ec_readers() { + use p256::pkcs8::EncodePrivateKey; + let secret = p256::SecretKey::from_slice(&[7; 32]).unwrap(); + let public = secret.public_key(); + use p256::elliptic_curve::sec1::ToEncodedPoint; + let point = public.to_encoded_point(false); + let jwk = serde_json::json!({"kty":"EC","crv":"P-256","d":URL_SAFE_NO_PAD.encode(secret.to_bytes()),"x":URL_SAFE_NO_PAD.encode(point.x().unwrap()),"y":URL_SAFE_NO_PAD.encode(point.y().unwrap())}).to_string(); + let pkcs8 = secret.to_pkcs8_pem(Default::default()).unwrap(); + let sec1 = secret.to_sec1_pem(Default::default()).unwrap(); + let expected = raw(&[7; 32], "es256", b"message"); + let ssh = ssh_key::PrivateKey::from(ssh_key::private::EcdsaKeypair::NistP256 { + public: point, + private: ssh_key::private::EcdsaPrivateKey::from(secret.clone()), + }) + .to_openssh(Default::default()) + .unwrap(); + for value in [jwk.as_str(), pkcs8.as_str(), sec1.as_str(), ssh.as_str()] { + assert_eq!( + sign(value, None, "es256", b"message", "raw", None, None) + .unwrap() + .bytes + .unwrap(), + expected + ); + let public = ssh_key::PublicKey::from_bytes( + &STANDARD + .decode(public_key(value, None, "es256", "ssh").unwrap()) + .unwrap(), + ) + .unwrap(); + let wire = STANDARD + .decode( + sign(value, None, "es256", b"message", "ssh", None, None) + .unwrap() + .signature, + ) + .unwrap(); + let signature = ssh_key::Signature::try_from(wire.as_slice()).unwrap(); + ed25519_dalek::Verifier::verify(&public, b"message", &signature).unwrap(); + } + let mut bad: serde_json::Value = serde_json::from_str(&jwk).unwrap(); + bad["x"] = serde_json::json!(URL_SAFE_NO_PAD.encode([0; 32])); + assert!( + sign( + &bad.to_string(), + None, + "es256", + b"message", + "raw", + None, + None + ) + .is_err() + ); +} + +#[test] +fn rfc6979_and_formats() { + let key = + hex::decode("c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721").unwrap(); + let expected = concat!( + "efd48b2aacb6a8fd1140dd9cd45e81d69d2c877b56aaf991c34d0ea84eaf3716", + "f7cb1c942d657c41d436c7a1b6e29f65f3e900dbb9aff4064dc4ab2f843acda8" + ); + assert_eq!(hex::encode(raw(&key, "es256", b"sample")), expected); + let key = STANDARD.encode(key); + for output in ["hex", "base64", "jws", "ssh"] { + let result = sign(&key, None, "es256", b"sample", output, None, None).unwrap(); + assert!(!result.signature.is_empty()); + } + for output in ["hex", "base64", "pem", "jwk", "ssh"] { + assert!(!public_key(&key, None, "es256", output).unwrap().is_empty()); + } + let pem = generate_es256().unwrap(); + assert!(public_key(&pem, None, "es256", "jwk").is_ok()); + for bad in [vec![], vec![1; 31], vec![0; 32], vec![255; 32]] { + assert!(sign(&STANDARD.encode(bad), None, "es256", b"", "raw", None, None).is_err()); + } +} +#[test] +fn rsa_readers_and_ssh_wire() { + use rsa::{ + pkcs1::EncodeRsaPrivateKey, + pkcs8::EncodePrivateKey, + traits::{PrivateKeyParts, PublicKeyParts}, + }; + use sha2::{Digest, Sha256, Sha512}; + let key = rsa::RsaPrivateKey::new(&mut rsa::rand_core::OsRng, 1024).unwrap(); + let pem = key.to_pkcs8_pem(Default::default()).unwrap(); + let pkcs1 = key.to_pkcs1_pem(Default::default()).unwrap(); + let ssh = ssh_key::PrivateKey::new( + ssh_key::private::RsaKeypair::try_from(&key).unwrap().into(), + "synthetic", + ) + .unwrap(); + let openssh = ssh.to_openssh(Default::default()).unwrap(); + let encrypted = ssh + .encrypt(&mut rsa::rand_core::OsRng, "synthetic") + .unwrap() + .to_openssh(Default::default()) + .unwrap(); + let jwk = serde_json::json!({"kty":"RSA", "n":URL_SAFE_NO_PAD.encode(key.n().to_bytes_be()), "e":URL_SAFE_NO_PAD.encode(key.e().to_bytes_be()), "d":URL_SAFE_NO_PAD.encode(key.d().to_bytes_be()), "p":URL_SAFE_NO_PAD.encode(key.primes()[0].to_bytes_be()), "q":URL_SAFE_NO_PAD.encode(key.primes()[1].to_bytes_be())}).to_string(); + for (value, pass) in [ + (pem.as_str(), None), + (pkcs1.as_str(), None), + (openssh.as_str(), None), + (encrypted.as_str(), Some("synthetic")), + (jwk.as_str(), None), + ] { + for (alg, hash) in [ + ("rs256", "sha256"), + ("rsa-sha2-256", "sha256"), + ("rsa-sha2-512", "sha512"), + ] { + let result = sign( + value, + pass, + alg, + b"message", + "raw", + Some("pkcs1v15"), + Some(hash), + ) + .unwrap(); + let sig = result.bytes.unwrap(); + if hash == "sha512" { + key.to_public_key() + .verify( + rsa::Pkcs1v15Sign::new::(), + &Sha512::digest(b"message"), + &sig, + ) + .unwrap(); + } else { + key.to_public_key() + .verify( + rsa::Pkcs1v15Sign::new::(), + &Sha256::digest(b"message"), + &sig, + ) + .unwrap(); + } + let wire = STANDARD + .decode( + sign( + value, + pass, + alg, + b"message", + "ssh", + Some("pkcs1v15"), + Some(hash), + ) + .unwrap() + .signature, + ) + .unwrap(); + let len = u32::from_be_bytes(wire[..4].try_into().unwrap()) as usize; + assert_eq!( + &wire[4..4 + len], + if hash == "sha512" { + b"rsa-sha2-512" + } else { + b"rsa-sha2-256" + } + ); + assert_eq!(&wire[8 + len..], sig); + } + for output in ["pem", "jwk", "ssh", "base64", "hex"] { + assert!(public_key(value, pass, "rs256", output).is_ok()); + } + } + assert!(sign(&pem, None, "rs256", b"", "raw", None, None).is_err()); +} + +#[test] +fn malformed_and_mismatched_keys() { + for value in [ + "not a key", + "{broken", + "{\"kty\":\"unknown\"}", + "-----BEGIN UNKNOWN PRIVATE KEY-----\nAAAA\n-----END UNKNOWN PRIVATE KEY-----", + "-----BEGIN OPENSSH PRIVATE KEY-----\ninvalid", + "AAAA", + ] { + assert!(sign(value, None, "ed25519", b"", "raw", None, None).is_err()); + } + let bad=serde_json::json!({"kty":"OKP","crv":"Ed25519","d":URL_SAFE_NO_PAD.encode([1;32]),"x":URL_SAFE_NO_PAD.encode([2;32])}).to_string(); + assert!(sign(&bad, None, "ed25519", b"", "raw", None, None).is_err()); + let oct = serde_json::json!({"kty":"oct","k":URL_SAFE_NO_PAD.encode([11;20])}).to_string(); + assert!(sign(&oct, None, "hmac-sha384", b"Hi There", "raw", None, None).is_ok()); + assert!(sign(&oct, None, "ed25519", b"", "raw", None, None).is_err()); + assert!(sign(&oct, None, "hmac-sha384", b"", "ssh", None, None).is_err()); + assert!(sign(&oct, None, "hmac-sha384", b"", "unknown", None, None).is_err()); +} + +#[test] +fn rfc8292() { + use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier}; + let public=URL_SAFE_NO_PAD.decode("BA1Hxzyi1RUM1b5wjxsn7nGxAszw2u61m164i3MrAIxHF6YK5h4SDYic-dRuU_RCPCfA5aq9ojSwk5Y2EmClBPs").unwrap(); + let signature=URL_SAFE_NO_PAD.decode("i3CYb7t4xfxCDquptFOepC9GAu_HLGkMlMuCGSK2rpiUfnK9ojFwDXb1JrErtmysazNjjvW2L9OkSSHzvoD1oA").unwrap(); + let input = concat!( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.", + "eyJhdWQiOiJodHRwczovL3B1c2guZXhhbXBsZS5uZXQiLCJleHAiOjE0NTM1MjM3NjgsInN1YiI6Im1haWx0bzpwdXNoQGV4YW1wbGUuY29tIn0" + ); + VerifyingKey::from_sec1_bytes(&public) + .unwrap() + .verify( + input.as_bytes(), + &Signature::from_slice(&signature).unwrap(), + ) + .unwrap(); +} diff --git a/host/tests/cred_v8.rs b/host/tests/cred_v8.rs new file mode 100644 index 0000000..6f7f3f5 --- /dev/null +++ b/host/tests/cred_v8.rs @@ -0,0 +1,217 @@ +//! Behavioral contract for the credential-only isolate. +use sausage::cred::{ + script::{ErrorKind, Limits, Script}, + v8::V8, +}; +use std::time::{Duration, Instant}; +fn run(s: &str) -> Result { + V8.evaluate(s, "null", None, Limits::default()) +} +#[test] +fn modules_promises_and_json() { + assert_eq!( + V8.evaluate( + "JSON.parse=()=>false; export default x=>x", + "42", + None, + Limits::default() + ) + .unwrap(), + "42" + ); + let error = run("export default ()=>{throw Error('DO NOT ECHO')}").unwrap_err(); + assert!(!error.message.contains("DO NOT ECHO")); + assert_eq!( + V8.evaluate( + "export default x=>({value:x}); export const other=x=>x+1", + "42", + None, + Limits::default() + ) + .unwrap(), + "{\"value\":42}" + ); + assert_eq!( + V8.evaluate( + "export const other=x=>x+1", + "42", + Some("other"), + Limits::default() + ) + .unwrap(), + "43" + ); + assert_eq!( + run("await Promise.resolve(); export default async x=>{await Promise.resolve();return 42}") + .unwrap(), + "42" + ); + for (s, k) in [ + ("export default (", ErrorKind::Syntax), + ("export default ()=>{throw Error('oops')}", ErrorKind::Throw), + ("export default ()=>Promise.reject('no')", ErrorKind::Throw), + ("export default ()=>undefined", ErrorKind::NonJson), + ("export default ()=>Symbol()", ErrorKind::NonJson), + ("export default ()=>()=>1", ErrorKind::NonJson), + ( + "export default ()=>{let a={};a.a=a;return a}", + ErrorKind::NonJson, + ), + ("export const x=1", ErrorKind::Engine), + ("export default 1", ErrorKind::Engine), + ("export default ()=>new Promise(()=>{})", ErrorKind::Engine), + ] { + assert_eq!(run(s).unwrap_err().kind, k, "{s}"); + } + assert_eq!( + V8.evaluate("export default x=>x", "invalid", None, Limits::default()) + .unwrap_err() + .kind, + ErrorKind::Syntax + ); +} +#[test] +fn sandbox_and_isolation() { + for name in [ + "Date", + "Math.random", + "fetch", + "setTimeout", + "setInterval", + "WebSocket", + "XMLHttpRequest", + "std", + "os", + "fs", + "Deno", + "__bootstrap", + "console", + "performance", + "require", + "process", + "TextEncoder", + "Buffer", + "Blob", + "File", + "crypto", + ] { + assert_eq!( + run(&format!("export default ()=>typeof {name}")).unwrap(), + "\"undefined\"", + "{name}" + ); + } + assert!(run("import x from 'fs'; export default ()=>x").is_err()); + assert!(run("import {core} from 'ext:core/mod.js'; export default ()=>typeof core").is_err()); + assert_eq!( + run("export default ()=>import('fs')").unwrap_err().kind, + ErrorKind::Throw + ); + run("export default ()=>{globalThis.leak=1;btoa.leak=1;return true}").unwrap(); + assert_eq!( + run("export default ()=>[typeof leak,typeof btoa.leak]").unwrap(), + "[\"undefined\",\"undefined\"]" + ); + for (expr, expected) in [ + ("btoa('user:pass')", "\"dXNlcjpwYXNz\""), + ("atob('dXNlcjpwYXNz')", "\"user:pass\""), + ("btoa('')", "\"\""), + ("atob('AP+A') === '\\x00\\xff\\x80'", "true"), + ] { + assert_eq!( + run(&format!("export default ()=>{expr}")).unwrap(), + expected + ); + } + for expr in ["atob('!')", "btoa('雪')"] { + assert_eq!( + run(&format!("export default ()=>{expr}")).unwrap_err().kind, + ErrorKind::Throw + ); + } +} +#[test] +fn independent_deadlines() { + let barrier = std::sync::Arc::new(std::sync::Barrier::new(2)); + let other = barrier.clone(); + let runaway = std::thread::spawn(move || { + other.wait(); + assert_eq!( + V8.evaluate( + "export default ()=>{while(true){}}", + "null", + None, + Limits { + max_time_ms: Some(1000), + ..Limits::default() + } + ) + .unwrap_err() + .kind, + ErrorKind::Limit + ); + }); + barrier.wait(); + let start = Instant::now(); + assert_eq!(run("export default ()=>true").unwrap(), "true"); + assert!(start.elapsed() < Duration::from_millis(500)); + runaway.join().unwrap(); +} + +#[test] +fn clamped_limits() { + for limits in [ + Limits { + max_time_ms: Some(0), + max_memory_bytes: Some(0), + }, + Limits { + max_time_ms: Some(u64::MAX), + max_memory_bytes: Some(usize::MAX), + }, + ] { + match V8.evaluate("export default ()=>true", "null", None, limits) { + Ok(value) => assert_eq!(value, "true"), + Err(error) => assert_eq!(error.kind, ErrorKind::Limit), + } + } + assert_eq!( + V8.evaluate( + "export default ()=>{let a=[];while(1)a.push('abcdefghijklmnopqrstuvwxyz'+a.length)}", + "null", + None, + Limits { + max_time_ms: Some(1000), + max_memory_bytes: Some(1024 * 1024) + } + ) + .unwrap_err() + .kind, + ErrorKind::Limit + ); +} + +#[test] +fn limits_and_parallel_calls() { + let mut workers = Vec::new(); + for s in [ + "export default ()=>{while(true){}}", + "while(true){};export default x=>x", + "export default ()=>({toJSON(){while(true){}}})", + "export default ()=>{throw {get message(){while(true){}}}}", + "export default ()=>{function f(){return f()+1}return f()}", + "export default ()=>new Promise(()=>{function f(){Promise.resolve().then(f)}f()})", + "export default ()=>{let a=[];while(1)a.push('abcdefghijklmnopqrstuvwxyz'+a.length)}", + ] { + workers.push(std::thread::spawn(move || { + let start = Instant::now(); + assert_eq!(run(s).unwrap_err().kind, ErrorKind::Limit); + assert!(start.elapsed() < Duration::from_secs(2)); + })); + } + let normal = std::thread::spawn(|| assert_eq!(run("export default ()=>true").unwrap(), "true")); + for worker in workers { + worker.join().unwrap(); + } + normal.join().unwrap(); +}