diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d87c2a7..d08fdbf 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -117,6 +117,17 @@ dependencies = [ "winit", ] +[[package]] +name = "addchain" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" +dependencies = [ + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + [[package]] name = "adler2" version = "2.0.1" @@ -195,6 +206,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aes-kw" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" +dependencies = [ + "aes 0.9.3", + "const-oid 0.10.2", +] + [[package]] name = "ahash" version = "0.8.12" @@ -345,6 +366,44 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.21", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure 0.13.2", +] + +[[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 = "async-broadcast" version = "0.7.2" @@ -369,6 +428,25 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-dnssd" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d49ffe175ab45bbfd74b548313d9d7cdfff27161a94b007b52eeeb5f9aaa15e" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-util", + "libc", + "log", + "pin-utils", + "pkg-config", + "tokio", + "winapi", +] + [[package]] name = "async-executor" version = "1.14.0" @@ -487,6 +565,15 @@ dependencies = [ "syn 3.0.7", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -536,6 +623,29 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base16ct" version = "1.0.0" @@ -586,6 +696,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -601,6 +717,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -912,6 +1040,15 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "cmov" version = "0.5.4" @@ -955,6 +1092,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-oid" version = "0.10.2" @@ -1044,6 +1187,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-utils" version = "0.8.23" @@ -1095,6 +1244,16 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "crypto-mac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +dependencies = [ + "generic-array 0.14.7", + "subtle", +] + [[package]] name = "crypto-primes" version = "0.7.2" @@ -1105,6 +1264,28 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "cryptoki" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625cf4599c43d69a16996ce2573fc80745a0b0300c962f0bfa11d55b6cab2b2d" +dependencies = [ + "bitflags 2.13.2", + "cryptoki-sys", + "libloading", + "log", + "secrecy", +] + +[[package]] +name = "cryptoki-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fd850498411e4057f1cba79e6e2bc7cbe960544c1046ab46d4685c403a1121" +dependencies = [ + "libloading", +] + [[package]] name = "ctr" version = "0.9.2" @@ -1227,17 +1408,59 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "zeroize", +] + [[package]] name = "der" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a878c850e9e421b20262e9b41f9c860e4785fa07541c266b62ff9d1ef998a80a" dependencies = [ - "const-oid", + "const-oid 0.10.2", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "des" version = "0.9.0" @@ -1265,7 +1488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ "block-buffer 0.12.1", - "const-oid", + "const-oid 0.10.2", "crypto-common 0.2.2", "ctutils", ] @@ -1288,6 +1511,17 @@ dependencies = [ "objc2 0.6.5", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + [[package]] name = "dlib" version = "0.5.3" @@ -1318,18 +1552,24 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "ecdsa" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ - "der", + "der 0.8.2", "digest 0.11.3", "elliptic-curve", "rfc6979", "signature", - "spki", + "spki 0.8.1", "zeroize", ] @@ -1736,6 +1976,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.10" @@ -1743,6 +1989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide 0.9.1", "zlib-rs", ] @@ -1796,6 +2043,27 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.34" @@ -1946,9 +2214,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -2146,6 +2416,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -2188,6 +2467,19 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "spin", + "stable_deref_trait", +] + [[package]] name = "hermit-abi" version = "0.5.3" @@ -2309,6 +2601,110 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "image" version = "0.25.10" @@ -2365,6 +2761,196 @@ dependencies = [ "mach2", ] +[[package]] +name = "ironrdp" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f910b8dc8e7b8e001c61fd742be442e8604fb2499ded713cadf911e7645ade4" +dependencies = [ + "ironrdp-connector", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-graphics", + "ironrdp-input", + "ironrdp-pdu", + "ironrdp-session", + "ironrdp-svc", +] + +[[package]] +name = "ironrdp-blocking" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f69f98a65a1aaf7b359ccec058a8dc8a227f8905d4e472a757b2b701f74071" +dependencies = [ + "bytes", + "ironrdp-connector", + "ironrdp-core", + "ironrdp-pdu", + "tracing", +] + +[[package]] +name = "ironrdp-bulk" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e548d9fd162558a5a8aaed72e528556ce88e77773634e3722b8d01d6f170388" + +[[package]] +name = "ironrdp-connector" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5898b3f1fcaca0f9b923b1463e158aeb64dfdec4ac361b7c086492466ff341c" +dependencies = [ + "ironrdp-core", + "ironrdp-error", + "ironrdp-pdu", + "ironrdp-svc", + "picky", + "picky-asn1-der", + "picky-asn1-x509", + "rand 0.9.5", + "sspi", + "tracing", + "url", +] + +[[package]] +name = "ironrdp-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0875b98275068b88652e49ba2e0a1150a1390aad69d26c942c8c59e8ec918e" +dependencies = [ + "ironrdp-error", +] + +[[package]] +name = "ironrdp-displaycontrol" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a111a6fd25abbe51b6a15276fe980c6b9eee50a1421224b0c3f45848d45bcf" +dependencies = [ + "ironrdp-core", + "ironrdp-dvc", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-dvc" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5de64988ddabf96928e2f042e1772a5f7201f0001ea99012129c73a105fc52" +dependencies = [ + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd344ce9518ab83f6f7568ca4f1bc6dc8c55bd2da04cb5ee7b3e8740d5041734" + +[[package]] +name = "ironrdp-graphics" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7493e426b6a8104cd497e518ba7781a9c7fc9a5f58a9cc0c1111e6d66f63bcc" +dependencies = [ + "bit_field", + "bitflags 2.13.2", + "bitvec", + "byteorder", + "ironrdp-core", + "ironrdp-pdu", + "num-derive", + "num-traits", + "yuv", +] + +[[package]] +name = "ironrdp-input" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1658a0c1d2911b767b71c2f9e07550d7a267bffaaaaeeec868d1752c8ec0f4" +dependencies = [ + "bitvec", + "ironrdp-pdu", + "smallvec", +] + +[[package]] +name = "ironrdp-pdu" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccd1179a4d106df1930347701388b5c79bc3725fa7dab4438d57db0d9d19347" +dependencies = [ + "bit_field", + "bitflags 2.13.2", + "byteorder", + "der-parser", + "ironrdp-core", + "ironrdp-error", + "md-5 0.10.6", + "num-bigint 0.4.8", + "num-derive", + "num-integer", + "num-traits", + "pkcs1 0.7.5", + "sha1 0.10.7", + "tap", + "x509-cert", +] + +[[package]] +name = "ironrdp-session" +version = "0.11.0" +dependencies = [ + "ironrdp-bulk", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-error", + "ironrdp-graphics", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-svc" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c36b82ab0f7fef2668fb7004008a0f3c100a3d9a7b18bd4495a71aba55b796" +dependencies = [ + "bitflags 2.13.2", + "ironrdp-core", + "ironrdp-pdu", +] + +[[package]] +name = "iso7816" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd3c7e91da489667bb054f9cd2f1c60cc2ac4478a899f403d11dbc62189215b0" +dependencies = [ + "heapless", +] + +[[package]] +name = "iso7816-tlv" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7660d28d24a831d690228a275d544654a30f3b167a8e491cf31af5fe5058b546" +dependencies = [ + "untrusted", +] + [[package]] name = "itertools" version = "0.15.0" @@ -2574,6 +3160,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linebender_resource_handle" version = "0.1.1" @@ -2592,6 +3189,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "1.0.0" @@ -2622,6 +3225,35 @@ dependencies = [ "libc", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "md4" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da5ac363534dce5fabf69949225e174fbf111a498bf0ff794c8ea1fba9f3dda" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "md5" version = "0.8.1" @@ -2652,6 +3284,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2827,6 +3465,8 @@ dependencies = [ "flate2", "hmac 0.13.0", "image", + "ironrdp", + "ironrdp-blocking", "keyring", "libc", "parking_lot", @@ -2846,6 +3486,7 @@ dependencies = [ "ureq", "uuid", "webpki-roots", + "x509-cert", "zeroize", ] @@ -2903,6 +3544,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num" version = "0.4.3" @@ -2917,6 +3568,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.8" @@ -2948,6 +3610,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-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" @@ -3295,6 +3974,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3528,6 +4216,122 @@ dependencies = [ "siphasher", ] +[[package]] +name = "picky" +version = "7.0.0-rc.25" +dependencies = [ + "aes 0.9.3", + "aes-gcm 0.11.1", + "aes-kw", + "base64 0.22.1", + "cbc 0.2.1", + "crypto-bigint", + "crypto-common 0.2.2", + "ctr 0.10.1", + "curve25519-dalek", + "des", + "digest 0.11.3", + "ecdsa", + "ed25519-dalek", + "hex", + "hmac 0.13.0", + "http", + "inout 0.2.2", + "md-5 0.11.0", + "p256", + "p384", + "p521", + "pbkdf2", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "pkcs1 0.8.0-rc.4", + "primeorder", + "rand 0.10.3", + "rand_core 0.10.1", + "rc2", + "rsa", + "rustcrypto-ff", + "rustcrypto-ff_derive", + "rustcrypto-group", + "serde", + "serde_json", + "sha1 0.11.0", + "sha2 0.11.0", + "sha3 0.12.0", + "thiserror 2.0.21", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "picky-asn1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff038f9360b934342fb3c0a1d6e82c438a2624b51c3c6e3e6d7cf252b6f3ee3" +dependencies = [ + "oid", + "serde", + "serde_bytes", + "time", + "zeroize", +] + +[[package]] +name = "picky-asn1-der" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4467118a2c640f4040aea27d01293620984b0b3f3ef0c53577c3bbf6bab4256" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f110226d163cea4380b0e5ef698a107061d198b279cc27d2b2c1e95c08cd9b0" +dependencies = [ + "base64 0.23.1", + "crypto-bigint", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", + "widestring", + "zeroize", +] + +[[package]] +name = "picky-krb" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d188f3192356068dbdba54bddbca6fd0f7a09565d3861eeb8efe1ab77ae8e97" +dependencies = [ + "aes 0.9.3", + "block-padding 0.4.2", + "byteorder", + "cbc 0.2.1", + "cipher 0.5.2", + "crypto-bigint", + "des", + "hmac 0.13.0", + "inout 0.2.2", + "oid", + "pbkdf2", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "rand 0.10.3", + "rand_core 0.10.1", + "serde", + "sha1 0.11.0", + "thiserror 2.0.21", + "uuid", +] + [[package]] name = "pin-project" version = "1.1.13" @@ -3554,6 +4358,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113" + [[package]] name = "piper" version = "0.2.5" @@ -3565,14 +4375,24 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + [[package]] name = "pkcs1" version = "0.8.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ - "der", - "spki", + "der 0.8.2", + "spki 0.8.1", ] [[package]] @@ -3584,12 +4404,12 @@ dependencies = [ "aes 0.9.3", "aes-gcm 0.11.1", "cbc 0.2.1", - "der", + "der 0.8.2", "pbkdf2", "rand_core 0.10.1", "scrypt", "sha2 0.11.0", - "spki", + "spki 0.8.1", ] [[package]] @@ -3598,10 +4418,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der", + "der 0.8.2", "pkcs5", "rand_core 0.10.1", - "spki", + "spki 0.8.1", ] [[package]] @@ -3729,6 +4549,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3831,6 +4666,12 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.8" @@ -3838,10 +4679,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.3" @@ -3863,6 +4714,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -3872,6 +4733,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -3884,6 +4754,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rc2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceda21af1ae61033b63175653a1af86cae399d79cd03ca80ba347eb3a6c4a7fe" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "read-fonts" version = "0.41.0" @@ -4028,16 +4907,16 @@ version = "0.10.0-rc.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" dependencies = [ - "const-oid", + "const-oid 0.10.2", "crypto-bigint", "crypto-primes", "digest 0.11.3", - "pkcs1", + "pkcs1 0.8.0-rc.4", "pkcs8", "rand_core 0.10.1", "sha2 0.11.0", "signature", - "spki", + "spki 0.8.1", "zeroize", ] @@ -4073,7 +4952,7 @@ dependencies = [ "curve25519-dalek", "data-encoding", "delegate", - "der", + "der 0.8.2", "digest 0.11.3", "ecdsa", "ed25519-dalek", @@ -4098,7 +4977,7 @@ dependencies = [ "p521", "pageant", "pbkdf2", - "pkcs1", + "pkcs1 0.8.0-rc.4", "pkcs5", "pkcs8", "polyval 0.7.3", @@ -4115,7 +4994,7 @@ dependencies = [ "sha2 0.11.0", "sha3 0.12.0", "signature", - "spki", + "spki 0.8.1", "ssh-encoding", "ssh-key", "subtle", @@ -4191,6 +5070,53 @@ dependencies = [ "semver", ] +[[package]] +name = "rustcrypto-ff" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" +dependencies = [ + "bitvec", + "rand_core 0.10.1", + "rustcrypto-ff_derive", + "subtle", +] + +[[package]] +name = "rustcrypto-ff_derive" +version = "0.14.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cda22ea03582974ab5687fc131eba2dc78e258e7eef4d7e01bcd0522ed79f66" +dependencies = [ + "addchain", + "num-bigint 0.3.3", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rustcrypto-group" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" +dependencies = [ + "rand_core 0.10.1", + "rustcrypto-ff", + "subtle", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -4234,6 +5160,7 @@ version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ + "aws-lc-rs", "log", "once_cell", "ring", @@ -4258,6 +5185,7 @@ version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4320,12 +5248,21 @@ checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", "ctutils", - "der", + "der 0.8.2", "hybrid-array", "subtle", "zeroize", ] +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "secret-service" version = "4.0.0" @@ -4766,6 +5703,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + [[package]] name = "spki" version = "0.8.1" @@ -4773,7 +5729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef958a98b9d5da290cfc78946e9f3e61e1e62a18db0d92cac0b83cc161491a9" dependencies = [ "base64ct", - "der", + "der 0.8.2", ] [[package]] @@ -4841,6 +5797,67 @@ dependencies = [ "zeroize", ] +[[package]] +name = "sspi" +version = "0.21.3" +dependencies = [ + "async-dnssd", + "async-recursion", + "bitflags 2.13.2", + "bytemuck", + "byteorder", + "cfg-if", + "crypto-bigint", + "crypto-mac", + "cryptoki", + "curve25519-dalek", + "ed25519-dalek", + "futures", + "getrandom 0.3.4", + "hmac 0.13.0", + "md-5 0.11.0", + "md4", + "num-derive", + "num-traits", + "oid", + "p256", + "p384", + "p521", + "picky", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "picky-krb", + "pkcs1 0.8.0-rc.4", + "primeorder", + "rand 0.10.3", + "rand_core 0.10.1", + "rsa", + "rustcrypto-ff", + "rustcrypto-ff_derive", + "rustcrypto-group", + "rustls", + "serde", + "sha1 0.11.0", + "sha2 0.11.0", + "time", + "tokio", + "tracing", + "url", + "uuid", + "widestring", + "windows", + "windows-registry", + "winscard", + "zeroize", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "static_assertions" version = "1.1.0" @@ -4853,6 +5870,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.119" @@ -4875,6 +5903,34 @@ dependencies = [ "unicode-ident", ] +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.27.0" @@ -4942,6 +5998,37 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "js-sys", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny_http" version = "0.12.0" @@ -4954,6 +6041,37 @@ dependencies = [ "log", ] +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tokio" version = "1.53.2" @@ -5200,12 +6318,30 @@ dependencies = [ "log", ] +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8-zero" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.28.0" @@ -5641,6 +6777,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -5755,6 +6897,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -6026,6 +7179,29 @@ dependencies = [ "version_check", ] +[[package]] +name = "winscard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dafb3c1468d0a3f5440e21e51614b53d1fdc62c9f82cc861c447906d09c69a" +dependencies = [ + "bitflags 2.13.2", + "crypto-bigint", + "flate2", + "iso7816", + "iso7816-tlv", + "num-derive", + "num-traits", + "picky", + "picky-asn1-x509", + "rsa", + "sha1 0.11.0", + "time", + "tracing", + "uuid", + "widestring", +] + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -6044,6 +7220,21 @@ dependencies = [ "primefield", ] +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" @@ -6076,6 +7267,29 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek", + "rand_core 0.10.1", + "zeroize", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + [[package]] name = "xcursor" version = "0.3.11" @@ -6117,6 +7331,38 @@ version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", + "synstructure 0.14.0", +] + +[[package]] +name = "yuv" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a295923535c00a4e72a50a0a1130097f93160b39892dd4d2d006b1e2b90d6bf9" +dependencies = [ + "num-traits", +] + [[package]] name = "zbus" version = "4.4.0" @@ -6306,6 +7552,27 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", + "synstructure 0.14.0", +] + [[package]] name = "zeroize" version = "1.9.1" @@ -6326,6 +7593,39 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.7", +] + [[package]] name = "zlib-rs" version = "0.6.8" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 983f2a5..48b9f84 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -15,3 +15,9 @@ opt-level = 2 [profile.release] lto = "thin" + +# IronRDP compatibility patches; see vendor/README.md for what each changes and when to drop it. +[patch.crates-io] +picky = { path = "vendor/picky" } +sspi = { path = "vendor/sspi" } +ironrdp-session = { path = "vendor/ironrdp-session" } diff --git a/rust/core/Cargo.toml b/rust/core/Cargo.toml index e2bbc76..75bb549 100644 --- a/rust/core/Cargo.toml +++ b/rust/core/Cargo.toml @@ -31,6 +31,9 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] } rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } webpki-roots = "1" +ironrdp = { version = "0.17", features = ["connector", "session", "graphics", "pdu", "svc", "dvc", "displaycontrol", "input"] } +ironrdp-blocking = "0.10" +x509-cert = { version = "0.2", default-features = false, features = ["std"] } [dev-dependencies] tiny_http = "0.12" diff --git a/rust/core/src/clients/mod.rs b/rust/core/src/clients/mod.rs index 2d531f8..32a40a3 100644 --- a/rust/core/src/clients/mod.rs +++ b/rust/core/src/clients/mod.rs @@ -39,6 +39,7 @@ impl Callbacks { pub mod proxy; pub mod raw; +pub mod rdp; pub mod serial; pub mod telnet; pub mod vnc; diff --git a/rust/core/src/clients/rdp.rs b/rust/core/src/clients/rdp.rs new file mode 100644 index 0000000..118912a --- /dev/null +++ b/rust/core/src/clients/rdp.rs @@ -0,0 +1,492 @@ +//! RDP client on IronRDP: TLS with NLA (CredSSP), the server certificate pinned through the +//! caller's host-key check, any transport the caller dials (direct, SOCKS5/HTTP proxy or an SSH +//! tunnel), and the desktop decoded into the same RGBA framebuffer the VNC client uses. Like the +//! other clients, `connect` blocks and the session runs on its own thread. + +use super::vnc::{FrameFn, Framebuffer}; +use super::{CloseFn, ErrorFn}; +use crate::Result; +use ironrdp::connector::connection_activation::ConnectionActivationState; +use ironrdp::connector::{self, ClientConnector, ConnectionResult, Credentials, Sequence, ServerName}; +use ironrdp::displaycontrol::client::DisplayControlClient; +use ironrdp::dvc::DrdynvcClient; +use ironrdp::graphics::image_processing::PixelFormat; +use ironrdp::input::Database; +pub use ironrdp::input::{MouseButton, MousePosition, Operation, Scancode, WheelRotations}; +use ironrdp::pdu::gcc::KeyboardType; +use ironrdp::pdu::rdp::capability_sets::MajorPlatformType; +use ironrdp::pdu::rdp::client_info::{PerformanceFlags, TimezoneInfo}; +use ironrdp::session::image::DecodedImage; +use ironrdp::session::{ActiveStageBuilder, ActiveStageOutput, fast_path}; +use ironrdp_blocking::Framed; +use parking_lot::Mutex; +use sha2::{Digest, Sha256}; +use std::net::TcpStream; +use std::sync::Arc; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::time::Duration; + +/// Opens the TCP stream to the RDP server (directly, via a proxy or through an SSH tunnel). +pub type Dial = Arc Result + Send + Sync>; +/// Decides whether to trust the server certificate, given its `SHA256:` fingerprint. +pub type CertCheck = Arc std::result::Result<(), String> + Send + Sync>; + +#[derive(Clone)] +pub struct Config { + /// Used for the TLS server name and CredSSP's target name. + pub host: String, + pub username: String, + pub domain: String, + pub password: String, + pub width: u16, + pub height: u16, + /// Also allow plain TLS (graphical logon) for servers without NLA, such as xrdp. + pub allow_tls_only: bool, + pub dial: Dial, + pub check_cert: CertCheck, + pub connect_timeout: Duration, + pub on_frame: FrameFn, + pub on_error: ErrorFn, + pub on_close: CloseFn, +} + +enum Input { + Ops(Vec), + Resize(u16, u16), + Close, +} + +pub struct Client { + cfg: Config, + fb: Arc>, + tx: Mutex>>, +} + +type Tls = rustls::StreamOwned; + +/// How often the session thread looks for queued input while waiting for server data. +const POLL: Duration = Duration::from_millis(15); + +impl Client { + pub fn new(cfg: Config) -> Arc { + Arc::new(Client { cfg, fb: Arc::default(), tx: Mutex::new(None) }) + } + + pub fn framebuffer(&self) -> Arc> { + self.fb.clone() + } + + /// Connects and logs on, then runs the session on a new thread. + pub fn connect(self: &Arc) -> Result<()> { + let (framed, result) = self.handshake()?; + let (tx, rx) = mpsc::channel(); + *self.tx.lock() = Some(tx); + let me = self.clone(); + std::thread::Builder::new().name("nexus-rdp".into()).spawn(move || { + let r = me.run(framed, result, rx); + let closing = me.tx.lock().take().is_none(); + if let Err(e) = r + && !closing + { + (me.cfg.on_error)(e.to_string()); + } + (me.cfg.on_close)(); + })?; + Ok(()) + } + + /// Mouse and keyboard input, applied in order. `Database` turns repeats into no-ops. + pub fn input(&self, ops: Vec) { + self.send(Input::Ops(ops)); + } + + /// Asks the server to resize the desktop (Display Control channel); ignored when it can't. + pub fn resize(&self, width: u16, height: u16) { + self.send(Input::Resize(width, height)); + } + + pub fn close(&self) { + if let Some(tx) = self.tx.lock().take() { + let _ = tx.send(Input::Close); + } + } + + fn send(&self, i: Input) { + if let Some(tx) = &*self.tx.lock() { + let _ = tx.send(i); + } + } + + fn connector_config(&self) -> connector::Config { + let c = &self.cfg; + connector::Config { + credentials: Credentials::UsernamePassword { username: c.username.clone(), password: c.password.clone() }, + domain: (!c.domain.is_empty()).then(|| c.domain.clone()), + enable_tls: c.allow_tls_only, + enable_credssp: true, + keyboard_type: KeyboardType::IbmEnhanced, + keyboard_subtype: 0, + keyboard_layout: 0, + keyboard_functional_keys_count: 12, + ime_file_name: String::new(), + dig_product_id: String::new(), + desktop_size: connector::DesktopSize { width: c.width.clamp(200, 8192), height: c.height.clamp(200, 8192) }, + bitmap: None, + client_build: 0, + client_name: "Nexus Terminal".into(), + client_dir: "C:\\Windows\\System32\\mstscax.dll".into(), + platform: if cfg!(windows) { + MajorPlatformType::WINDOWS + } else if cfg!(target_os = "macos") { + MajorPlatformType::MACINTOSH + } else { + MajorPlatformType::UNIX + }, + enable_server_pointer: false, + request_data: None, + // Also hands the credentials to TLS-only servers (xrdp), which would otherwise show their own login screen. + autologon: !c.password.is_empty(), + enable_audio_playback: false, + // ponytail: no bulk compression (the bitmap codecs already compress); enabling it means + // carrying the decompressor across reactivations. + compression_type: None, + pointer_software_rendering: true, + multitransport_flags: None, + performance_flags: PerformanceFlags::default(), + desktop_scale_factor: 0, + hardware_id: None, + license_cache: None, + timezone_info: TimezoneInfo::default(), + alternate_shell: String::new(), + work_dir: String::new(), + } + } + + fn handshake(&self) -> Result<(Framed, ConnectionResult)> { + let tcp = (self.cfg.dial)()?; + tcp.set_read_timeout(Some(self.cfg.connect_timeout.max(Duration::from_secs(5))))?; + tcp.set_nodelay(true)?; + let local = tcp.local_addr()?; + let mut framed = Framed::new(tcp); + let drdynvc = DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))); + let mut connector = ClientConnector::new(self.connector_config(), local).with_static_channel(drdynvc); + let should_upgrade = ironrdp_blocking::connect_begin(&mut framed, &mut connector).map_err(|e| format!("RDP connection failed: {e}"))?; + + let tcp = framed.into_inner_no_leftover(); + let (tls, cert) = tls_upgrade(tcp, &self.cfg.host)?; + // Pin before CredSSP: no credentials leave the machine for an untrusted certificate. + (self.cfg.check_cert)(&fingerprint(&cert))?; + let upgraded = ironrdp_blocking::mark_as_upgraded(should_upgrade, &mut connector); + let mut framed = Framed::new(tls); + let result = ironrdp_blocking::connect_finalize( + upgraded, + connector, + &mut framed, + &mut NoNetwork, + ServerName::new(self.cfg.host.clone()), + server_public_key(&cert)?, + None, + ) + .map_err(|e| format!("RDP logon failed: {}", describe(&e)))?; + Ok((framed, result)) + } + + fn run(&self, mut framed: Framed, result: ConnectionResult, rx: Receiver) -> Result<()> { + framed.get_inner_mut().0.sock.set_read_timeout(Some(POLL))?; + let size = result.desktop_size; + let mut image = DecodedImage::new(PixelFormat::RgbA32, size.width, size.height); + *self.fb.lock() = Framebuffer::new_checked(size.width, size.height)?; + let factory = result.activation_factory; + let mut stage = ActiveStageBuilder { + static_channels: result.static_channels, + user_channel_id: result.user_channel_id, + io_channel_id: result.io_channel_id, + message_channel_id: result.message_channel_id, + share_id: result.share_id, + compression_type: result.compression_type, + enable_server_pointer: result.enable_server_pointer, + pointer_software_rendering: result.pointer_software_rendering, + } + .build(); + let mut input = Database::new(); + loop { + let mut outputs = Vec::new(); + loop { + match rx.try_recv() { + Ok(Input::Ops(ops)) => { + let events = input.apply(ops); + if !events.is_empty() { + outputs.extend(stage.process_fastpath_input(&mut image, &events)?); + } + } + Ok(Input::Resize(w, h)) => { + let (w, h) = ironrdp::displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size(w.into(), h.into()); + if let Some(frame) = stage.encode_resize(w, h, None, None) { + outputs.push(ActiveStageOutput::ResponseFrame(frame?)); + } + } + Ok(Input::Close) | Err(mpsc::TryRecvError::Disconnected) => { + for o in stage.graceful_shutdown()? { + if let ActiveStageOutput::ResponseFrame(f) = o { + let _ = framed.write_all(&f); + } + } + return Ok(()); + } + Err(mpsc::TryRecvError::Empty) => break, + } + } + match framed.read_pdu() { + Ok((action, payload)) => outputs.extend(stage.process(&mut image, action, &payload)?), + Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {} + Err(e) => return Err(format!("RDP connection lost: {e}").into()), + } + for out in outputs { + match out { + ActiveStageOutput::ResponseFrame(frame) => framed.write_all(&frame)?, + ActiveStageOutput::GraphicsUpdate(r) => { + copy_rect(&image, &mut self.fb.lock(), r.left, r.top, r.right, r.bottom); + (self.cfg.on_frame)(); + } + ActiveStageOutput::DeactivateAll => { + // Deactivation-Reactivation (e.g. after a resize): new desktop size and channel state. + framed.get_inner_mut().0.sock.set_read_timeout(Some(self.cfg.connect_timeout.max(Duration::from_secs(5))))?; + let mut seq = factory.create(); + let mut buf = ironrdp::core::WriteBuf::new(); + loop { + sequence_step(&mut framed, &mut seq, &mut buf).map_err(|e| format!("RDP reactivation failed: {e}"))?; + if let ConnectionActivationState::Finalized { desktop_size, share_id, enable_server_pointer, pointer_software_rendering } = + seq.connection_activation_state() + { + image = DecodedImage::new(PixelFormat::RgbA32, desktop_size.width, desktop_size.height); + *self.fb.lock() = Framebuffer::new_checked(desktop_size.width, desktop_size.height)?; + stage.set_fastpath_processor( + fast_path::ProcessorBuilder { + io_channel_id: seq.io_channel_id(), + user_channel_id: seq.user_channel_id(), + share_id, + enable_server_pointer, + pointer_software_rendering, + bulk_decompressor: None, + } + .build(), + ); + stage.set_share_id(share_id); + stage.set_enable_server_pointer(enable_server_pointer); + break; + } + } + framed.get_inner_mut().0.sock.set_read_timeout(Some(POLL))?; + (self.cfg.on_frame)(); + } + ActiveStageOutput::Terminate(reason) => return Err(format!("RDP session ended: {reason}").into()), + _ => {} + } + } + } + } +} + +/// One step of a connection sequence: read the PDU it waits for (if any), answer if it says to. +fn sequence_step(framed: &mut Framed, seq: &mut dyn Sequence, buf: &mut ironrdp::core::WriteBuf) -> Result<()> { + buf.clear(); + let written = match seq.next_pdu_hint() { + Some(hint) => { + let pdu = framed.read_by_hint(hint)?; + seq.step(&pdu, buf)? + } + None => seq.step_no_input(buf)?, + }; + if written.size().is_some() { + framed.write_all(buf.filled())?; + } + Ok(()) +} + +/// Copies an updated rectangle (inclusive bounds) from the decoder's image into the shared +/// framebuffer, forcing opaque alpha (RDP leaves it undefined). +fn copy_rect(image: &DecodedImage, fb: &mut Framebuffer, left: u16, top: u16, right: u16, bottom: u16) { + if (fb.width, fb.height) != (image.width(), image.height()) { + return; + } + let (w, stride) = (fb.width as usize, image.stride()); + let (x0, x1) = (left as usize, (right as usize).min(w.saturating_sub(1))); + let (y0, y1) = (top as usize, (bottom as usize).min(fb.height as usize - 1)); + if x0 > x1 || y0 > y1 { + return; + } + let data = image.data(); + for y in y0..=y1 { + let (s, d) = (y * stride + x0 * 4, (y * w + x0) * 4); + let n = (x1 - x0 + 1) * 4; + fb.rgba[d..d + n].copy_from_slice(&data[s..s + n]); + fb.rgba[d..d + n].as_chunks_mut::<4>().0.iter_mut().for_each(|p| p[3] = 255); + } + fb.dirty = true; +} + +/// `SHA256:` of the certificate, the format SSH host keys use. +pub fn fingerprint(cert_der: &[u8]) -> String { + use base64::Engine; + format!("SHA256:{}", base64::engine::general_purpose::STANDARD_NO_PAD.encode(Sha256::digest(cert_der))) +} + +fn server_public_key(cert_der: &[u8]) -> Result> { + use x509_cert::der::Decode; + let cert = x509_cert::Certificate::from_der(cert_der).map_err(|e| format!("RDP server certificate is unreadable: {e}"))?; + Ok(cert.tbs_certificate.subject_public_key_info.subject_public_key.as_bytes().ok_or("RDP server public key is malformed")?.to_vec()) +} + +/// TLS without CA validation (RDP servers use self-signed certificates): the handshake still +/// proves possession of the certificate's key, and the caller pins its fingerprint. +fn tls_upgrade(mut tcp: TcpStream, host: &str) -> Result<(Tls, Vec)> { + let provider = Arc::new(rustls::crypto::ring::default_provider()); + let config = rustls::ClientConfig::builder_with_provider(provider.clone()) + .with_safe_default_protocol_versions()? + .dangerous() + .with_custom_certificate_verifier(Arc::new(Pinned(provider))) + .with_no_client_auth(); + let name = rustls::pki_types::ServerName::try_from(host.to_string()).map_err(|e| format!("invalid RDP host name {host:?}: {e}"))?; + let mut conn = rustls::ClientConnection::new(Arc::new(config), name)?; + while conn.is_handshaking() { + conn.complete_io(&mut tcp).map_err(|e| format!("RDP TLS handshake failed: {e}"))?; + } + let cert = conn.peer_certificates().and_then(|c| c.first()).ok_or("RDP server sent no certificate")?.to_vec(); + Ok((rustls::StreamOwned::new(conn, tcp), cert)) +} + +#[derive(Debug)] +struct Pinned(Arc); + +impl rustls::client::danger::ServerCertVerifier for Pinned { + fn verify_server_cert( + &self, + _: &rustls::pki_types::CertificateDer<'_>, + _: &[rustls::pki_types::CertificateDer<'_>], + _: &rustls::pki_types::ServerName<'_>, + _: &[u8], + _: rustls::pki_types::UnixTime, + ) -> std::result::Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &rustls::pki_types::CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &rustls::pki_types::CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> std::result::Result { + rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} + +/// CredSSP asks for this only for Kerberos (talking to a KDC); NTLM never uses it. +struct NoNetwork; + +impl connector::sspi::network_client::NetworkClient for NoNetwork { + fn send(&self, _: &connector::sspi::NetworkRequest) -> connector::sspi::Result> { + Err(connector::sspi::Error::new(connector::sspi::ErrorKind::NoAuthenticatingAuthority, "Kerberos is not supported; use NTLM")) + } +} + +/// Connector errors carry their cause in a chain; surface the useful part. +fn describe(e: &connector::ConnectorError) -> String { + let mut s = e.to_string(); + let mut src = std::error::Error::source(e); + while let Some(c) = src { + s = format!("{s}: {c}"); + src = c.source(); + } + s +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fingerprints_like_ssh() { + assert_eq!(fingerprint(b"abc"), "SHA256:ungWv48Bz+pBQUDeXa4iI7ADYaOWF3qctBD/YfIAFa0"); + } + + #[test] + fn copies_inclusive_rectangles_with_opaque_alpha() { + let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3); + let mut fb = Framebuffer::new_checked(4, 3).unwrap(); + fb.rgba.fill(0); + copy_rect(&image, &mut fb, 1, 1, 2, 1); + let alpha: Vec = fb.rgba.chunks(4).map(|p| p[3]).collect(); + assert_eq!(alpha, [0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0]); + // Out-of-range bounds are clipped, a size mismatch is ignored. + copy_rect(&image, &mut fb, 3, 2, 99, 99); + assert_eq!(fb.rgba[(2 * 4 + 3) * 4 + 3], 255); + image = DecodedImage::new(PixelFormat::RgbA32, 8, 8); + copy_rect(&image, &mut fb, 0, 0, 7, 7); + assert_eq!(fb.rgba[3], 0); + } + + /// `NEXUS_RDP_TEST="host port user password" cargo test -p nexus-core rdp::tests::live -- --ignored --nocapture` + #[test] + #[ignore] + fn live() { + let spec = std::env::var("NEXUS_RDP_TEST").expect("NEXUS_RDP_TEST"); + let v: Vec<&str> = spec.split_whitespace().collect(); + let (host, port) = (v[0].to_string(), v[1].parse::().unwrap()); + let frames = Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let (f2, h2) = (frames.clone(), host.clone()); + let seen_cert = Arc::new(Mutex::new(String::new())); + let sc = seen_cert.clone(); + let c = Client::new(Config { + host: host.clone(), + username: v[2].into(), + domain: String::new(), + password: v[3].into(), + width: 1024, + height: 768, + allow_tls_only: true, + dial: Arc::new(move || Ok(TcpStream::connect((h2.as_str(), port))?)), + check_cert: Arc::new(move |fp| { + *sc.lock() = fp.to_string(); + Ok(()) + }), + connect_timeout: Duration::from_secs(15), + on_frame: Arc::new(move || { + f2.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + }), + on_error: Arc::new(|e| eprintln!("on_error: {e}")), + on_close: Arc::new(|| eprintln!("on_close")), + }); + c.connect().unwrap(); + eprintln!("cert {}", seen_cert.lock()); + let t = std::time::Instant::now(); + while frames.load(std::sync::atomic::Ordering::SeqCst) < 5 && t.elapsed() < Duration::from_secs(20) { + std::thread::sleep(Duration::from_millis(100)); + } + c.input(vec![Operation::MouseMove(MousePosition { x: 100, y: 100 })]); + c.input(vec![Operation::KeyPressed(Scancode::from_u8(false, 0x1e)), Operation::KeyReleased(Scancode::from_u8(false, 0x1e))]); + std::thread::sleep(Duration::from_secs(2)); + let fb = c.framebuffer().lock().clone(); + eprintln!("frames {} size {}x{}", frames.load(std::sync::atomic::Ordering::SeqCst), fb.width, fb.height); + if let Ok(out) = std::env::var("NEXUS_RDP_PNG") { + image::DynamicImage::ImageRgba8(image::RgbaImage::from_raw(fb.width.into(), fb.height.into(), fb.rgba.clone()).unwrap()) + .to_rgb8() + .save(out) + .unwrap(); + } + assert!(frames.load(std::sync::atomic::Ordering::SeqCst) > 0); + c.close(); + } +} diff --git a/rust/core/src/clients/vnc.rs b/rust/core/src/clients/vnc.rs index 1cb157a..0d11a4b 100644 --- a/rust/core/src/clients/vnc.rs +++ b/rust/core/src/clients/vnc.rs @@ -86,7 +86,7 @@ impl Framebuffer { } /// A framebuffer of a server-announced size, refusing sizes that would exhaust memory. - fn new_checked(width: u16, height: u16) -> crate::Result { + pub(crate) fn new_checked(width: u16, height: u16) -> crate::Result { if width == 0 || height == 0 || width as usize * height as usize > MAX_FB_PIXELS { return Err(format!("unsupported desktop size {width}x{height}").into()); } diff --git a/rust/vendor/README.md b/rust/vendor/README.md new file mode 100644 index 0000000..62b6e5d --- /dev/null +++ b/rust/vendor/README.md @@ -0,0 +1,19 @@ +# Vendored crates + +Patched copies used through `[patch.crates-io]` in `rust/Cargo.toml`. Remove each once an +IronRDP release includes the fix it carries. + +| Crate | Upstream | Change | +|---|---|---| +| `picky` | 7.0.0-rc.25 (crates.io) | `=x.y.z-rc.N` RustCrypto pins relaxed to `^x.y.z-rc.N` (so the stable releases russh uses satisfy them); one unused import removed. | +| `sspi` | 0.21.3 (crates.io) | Same pin relaxation. | +| `ironrdp-session` | 0.11.0 (crates.io) | Upstream fixes not yet released: #1486 "preserve bitmap source stride" (commit `80bb81b344`, applied as-is) and the RDP6 orientation flip from #1524 (`8f1737a287`). Without them xrdp desktops render sheared and upside-down tile by tile. | + +Why: IronRDP pins `picky = "=7.0.0-rc.25"`, which pins `curve25519-dalek = "=5.0.0-rc.1"` and +other release candidates; russh needs the stable `curve25519-dalek 5`, and Cargo can't build two +semver-compatible versions of one crate. With the pins relaxed both resolve to the stable +releases, and the `picky` and `sspi` unit suites (402 tests) pass against them. + +The `ironrdp-session` fixes are verified by the live test in `core/src/clients/rdp.rs` +(`NEXUS_RDP_TEST="host port user password"`), run against xrdp; the published crate ships no +unit tests. diff --git a/rust/vendor/ironrdp-session/CHANGELOG.md b/rust/vendor/ironrdp-session/CHANGELOG.md new file mode 100644 index 0000000..240f419 --- /dev/null +++ b/rust/vendor/ironrdp-session/CHANGELOG.md @@ -0,0 +1,214 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[0.11.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.10.0...ironrdp-session-v0.11.0)] - 2026-07-10 + +### Security + +- [**breaking**] Send NetworkAutoDetect over the MCS message channel ([#1348](https://github.com/Devolutions/IronRDP/issues/1348)) ([8a1fd0118e](https://github.com/Devolutions/IronRDP/commit/8a1fd0118e0bac214c9050b6ca6b36a040046dd3)) + + Corrects Network Auto-Detect framing and routing to match MS-RDPBCGR by + moving it off the I/O channel slow-path Share Data PDUs and onto the MCS + message channel with the required Basic Security Header + (SEC_AUTODETECT_REQ / SEC_AUTODETECT_RSP). This aligns IronRDP with + mstsc/xfreerdp behavior and enables both connect-time and continuous + auto-detection to actually function. + +### Bug Fixes + +- Propagate caller location through error constructor helpers ([#1392](https://github.com/Devolutions/IronRDP/issues/1392)) ([d6990d81a1](https://github.com/Devolutions/IronRDP/commit/d6990d81a17e8349e52768ad8a82f673b1e1462d)) + + The error constructor helpers in several crates wrap the #[track_caller] + ironrdp_error::Error::new, but were not themselves marked + #[track_caller]. As a result, the captured location pointed at the + helper body instead of the real call site, giving misleading "@ + file:line" info in error reports. + +- Reduce dependency on ironrdp-connector ([#1419](https://github.com/Devolutions/IronRDP/issues/1419)) ([5c22f86a71](https://github.com/Devolutions/IronRDP/commit/5c22f86a7150bc10c26a3be39bfaebf84c67d781)) + + Drops session's reliance on ironrdp-connector legacy helpers, now sourced from ironrdp-pdu. + +- [**breaking**] Remove ironrdp-connector dependency ([#1435](https://github.com/Devolutions/IronRDP/issues/1435)) ([c6a0286dcb](https://github.com/Devolutions/IronRDP/commit/c6a0286dcb49d9ac54c65c4f9325b41e05d541b8)) + + Removes the last ironrdp-connector coupling from ironrdp-session by + turning Deactivate-All handling into a bare signal and shifting ownership + of the Deactivation-Reactivation activation sequence back to each consumer. + It introduces a ConnectionActivationFactory (fresh sequence per reactivation) + and an ActiveStageBuilder so session construction no longer depends on + ConnectionResult. + + + +## [[0.10.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.9.0...ironrdp-session-v0.10.0)] - 2026-06-05 + +### Bug Fixes + +- Decode RGBA QOI bitmaps instead of dropping the frame ([#1341](https://github.com/Devolutions/IronRDP/issues/1341)) ([ef20ea4e90](https://github.com/Devolutions/IronRDP/commit/ef20ea4e90455d6c6db0d3521f6522d1e960c0bb)) + + Fixes the client-side QOI decode path in ironrdp-session so RGBA-channel QOI frames are decoded and applied to the framebuffer instead of being dropped, improving interoperability with third-party RDP servers and older ironrdp-server builds that emit RGBA QOI. + +### Build + +- [**breaking**] Update `ironrdp-dvc` public dependency + + + +## [[0.9.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.8.0...ironrdp-session-v0.9.0)] - 2026-05-27 + +### Features + +- Dispatch multitransport PDUs on IO channel ([#1096](https://github.com/Devolutions/IronRDP/issues/1096)) ([7853e3cc6f](https://github.com/Devolutions/IronRDP/commit/7853e3cc6f26acaf3da000c6177ca3cef6ef85fd)) + + `decode_io_channel()` assumes all IO channel PDUs begin with + a`ShareControlHeader`. Multitransport Request PDUs use a + `BasicSecurityHeader` with `SEC_TRANSPORT_REQ` instead ([MS-RDPBCGR] + 2.2.15.1). + + This adds a peek-based dispatch: check the first `u16` + for`TRANSPORT_REQ`, decode as `MultitransportRequestPdu` if set, + otherwise fall through to the existing `decode_share_control()` path + unchanged. + + The new variant is propagated through `ProcessorOutput` and + 'ActiveStageOutput` so applications can handle multitransport requests. + Client and web consumers log the request (no UDP transport yet). + +- Add bulk compression and wire negotiation ([ebf5da5f33](https://github.com/Devolutions/IronRDP/commit/ebf5da5f3380a3355f6c95814d669f8190425ded)) + + - add ironrdp-bulk crate with MPPC/NCRUSH/XCRUSH, bitstream, benches, and metrics + - advertise compression in Client Info and plumb compression_type through connector + - decode compressed FastPath/ShareData updates using BulkCompressor + - update CLI to numeric compression flags (enabled by default, level 0-3) + - extend screenshot example with compression options and negotiated logging + - refresh tests, FFI/web configs, typos, and Cargo.lock + +- Complete pixel format support for bitmap updates ([#1134](https://github.com/Devolutions/IronRDP/issues/1134)) ([a6b41093ce](https://github.com/Devolutions/IronRDP/commit/a6b41093ce4ece081d2538c157f6bc547c3b2607)) + + Wires missing bitmap pixel formats (8/15/24bpp) into the session rendering + pipeline so bitmap updates at those depths are rendered instead of being + dropped, and adds fast-path palette update parsing to support 8bpp indexed + color sessions. + +- Handle Auto-Detect Request PDUs from server ([#1178](https://github.com/Devolutions/IronRDP/issues/1178)) ([4dcad09980](https://github.com/Devolutions/IronRDP/commit/4dcad09980e4f5354e4e435a134cc0956e2fcf9e)) + + Fixes a crash when the server sends Auto-Detect Request PDUs during an + active session. After #1176 added ShareDataPdu::AutoDetectReq routing, + these PDUs decode correctly but hit the catch-all error path in the x224 + processor: "unhandled PDU: Auto-Detect Request PDU". + +- Handle slow-path graphics and pointer updates ([#1132](https://github.com/Devolutions/IronRDP/issues/1132)) ([9383380292](https://github.com/Devolutions/IronRDP/commit/938338029290f1be82a7f784d544bb77ac797aeb)) + + Adds support for slow-path graphics and pointer updates to IronRDP, fixing connectivity issues with servers like XRDP that use slow-path output instead of fast-path. The implementation parses slow-path framing headers and routes the inner payload structures through the existing fast-path processing pipeline by extracting shared bitmap and pointer processing methods. + +### Bug Fixes + +- Fix pixel format handling in bitmap decoders ([#1101](https://github.com/Devolutions/IronRDP/issues/1101)) ([75863245ab](https://github.com/Devolutions/IronRDP/commit/75863245ab376f15e35c00df434860c93b123633)) + +- Handle row padding in uncompressed bitmap updates ([4262ae75ff](https://github.com/Devolutions/IronRDP/commit/4262ae75ffa5cb1fabb4ca07d598e33d855e8fdd)) + + Uncompressed bitmap data has rows padded to 4-byte boundaries per + [MS-RDPBCGR] 2.2.9.1.1.3.1.2.2, but the bitmap apply functions + expect tightly packed pixel data. Strip the per-row padding before + passing raw bitmap data to the apply functions. + + This fixes garbled bitmap rendering when connecting to servers that + send uncompressed bitmaps with non-aligned row widths, such as XRDP + at 16 bpp. + +- Skip bitmap updates that exceed bounds ([#1146](https://github.com/Devolutions/IronRDP/issues/1146)) ([2b97a95e6d](https://github.com/Devolutions/IronRDP/commit/2b97a95e6da8833e8a84e9f42960da91eee87cd6)) + + After a desktop resize, an RDP server can send a burst of bitmap updates + for the old resolution before its rendering pipeline has fully + transitioned to the new one. These updates reference coordinates beyond + the current image buffer in `DecodedImage`, causing index-out-of-bounds + panics in the `apply_*` methods. On the server side, the same stale + bitmaps can reach the encoder with dimensions exceeding the negotiated + desktop size, panicking in `NoneHandler::handle()`. + + This commit adds bounds checks at two levels: + - `DecodedImage::rect_fits()` guard at the entry of each `apply_*` + method, returning an empty rectangle when the update doesn't fit + - Encoder-level guard in `EncoderIter::next()` that drops + `BitmapUpdate`s exceeding the current desktop size + +- Propagate negotiated share_id to all outgoing ShareDataPdu ([#1147](https://github.com/Devolutions/IronRDP/issues/1147)) ([2b24e9664d](https://github.com/Devolutions/IronRDP/commit/2b24e9664dd05620ff63a24d092377477fdde863)) + +## [[0.8.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.7.0...ironrdp-session-v0.8.0)] - 2025-12-18 + + +## [[0.6.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.5.0...ironrdp-session-v0.6.0)] - 2025-08-29 + +### Features + +- Add QOI image codec ([613fd51f26](https://github.com/Devolutions/IronRDP/commit/613fd51f26315d8212662c46f8e625c541e4bb59)) + + The Quite OK Image format ([1]) losslessly compresses images to a similar size + of PNG, while offering 20x-50x faster encoding and 3x-4x faster decoding. + +- Add QOIZ image codec ([87df67fdc7](https://github.com/Devolutions/IronRDP/commit/87df67fdc76ff4f39d4b83521e34bf3b5e2e73bb)) + + Add a new QOIZ codec for SetSurface command. The PDU data contains the same + data as the QOI codec, with zstd compression. + +## [[0.4.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.4.0...ironrdp-session-v0.4.1)] - 2025-06-27 + +### Features + +- More functions on `ActiveStage` (#791) ([5482365655](https://github.com/Devolutions/IronRDP/commit/5482365655e5c171cd967eda401b01161a9f6602)) + - `get_dvc_by_channel_id` + - `encode_dvc_messages` + + +## [[0.4.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.3.0...ironrdp-session-v0.4.0)] - 2025-05-27 + +### Features + +- [**breaking**] Make DecodedImage Send ([45f66117ba](https://github.com/Devolutions/IronRDP/commit/45f66117ba05170d95b21ec7d97017b44b954f28)) + +- Add DecodeImage helpers ([cd7a60ba45](https://github.com/Devolutions/IronRDP/commit/cd7a60ba45a0241be4ecf3860ec4f82b431a7ce2)) + +### Bug Fixes + +- Update rectangle when applying None codecs updates (#728) ([a50cd643dc](https://github.com/Devolutions/IronRDP/commit/a50cd643dce9621f314231b7598d2fd31e4718c6)) + +- Return the correct updated region ([7507a152f1](https://github.com/Devolutions/IronRDP/commit/7507a152f14db594e4067bbc01e243cfba77770f)) + + "update_rectangle" is set to empty(). The surface updates are then added + by "union". But a union with an empty rectangle at (0,0) is still a + rectangle at (0,0). We end up with big region updates rooted at (0,0)... + +- Decrease verbosity of Rfx frame_index ([b31b99eafb](https://github.com/Devolutions/IronRDP/commit/b31b99eafb0aac2a5e5a610af21a4027ae5cd698)) + +- Decrease verbosity of FastPath header ([f9b6992e74](https://github.com/Devolutions/IronRDP/commit/f9b6992e74abb929f3001e76abaff5d7215e1cb4)) + + +## [[0.3.0](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.3...ironrdp-session-v0.3.0)] - 2025-03-12 + +### Build + +- Bump ironrdp-pdu + +## [[0.2.3](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.2...ironrdp-session-v0.2.3)] - 2025-03-12 + +### Build + +- Update dependencies (#695) ([c21fa44fd6](https://github.com/Devolutions/IronRDP/commit/c21fa44fd6f3c6a6b74788ff68e83133c1314caa)) + + +## [[0.2.2](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.1...ironrdp-session-v0.2.2)] - 2025-01-28 + +### Documentation + +- Use CDN URLs instead of the blob storage URLs for Devolutions logo (#631) ([dd249909a8](https://github.com/Devolutions/IronRDP/commit/dd249909a894004d4f728d30b3a4aa77a0f8193b)) + + +## [[0.2.1](https://github.com/Devolutions/IronRDP/compare/ironrdp-session-v0.2.0...ironrdp-session-v0.2.1)] - 2024-12-14 + +### Other + +- Symlinks to license files in packages ([#604](https://github.com/Devolutions/IronRDP/pull/604)) ([6c2de344c2](https://github.com/Devolutions/IronRDP/commit/6c2de344c2dd93ce9621834e0497ed7c3bfaf91a)) diff --git a/rust/vendor/ironrdp-session/Cargo.lock b/rust/vendor/ironrdp-session/Cargo.lock new file mode 100644 index 0000000..8b6338c --- /dev/null +++ b/rust/vendor/ironrdp-session/Cargo.lock @@ -0,0 +1,735 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[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", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "ironrdp-bulk" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e548d9fd162558a5a8aaed72e528556ce88e77773634e3722b8d01d6f170388" + +[[package]] +name = "ironrdp-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef0875b98275068b88652e49ba2e0a1150a1390aad69d26c942c8c59e8ec918e" +dependencies = [ + "ironrdp-error", +] + +[[package]] +name = "ironrdp-displaycontrol" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a111a6fd25abbe51b6a15276fe980c6b9eee50a1421224b0c3f45848d45bcf" +dependencies = [ + "ironrdp-core", + "ironrdp-dvc", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-dvc" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a5de64988ddabf96928e2f042e1772a5f7201f0001ea99012129c73a105fc52" +dependencies = [ + "ironrdp-core", + "ironrdp-pdu", + "ironrdp-svc", + "tracing", +] + +[[package]] +name = "ironrdp-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd344ce9518ab83f6f7568ca4f1bc6dc8c55bd2da04cb5ee7b3e8740d5041734" + +[[package]] +name = "ironrdp-graphics" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7493e426b6a8104cd497e518ba7781a9c7fc9a5f58a9cc0c1111e6d66f63bcc" +dependencies = [ + "bit_field", + "bitflags", + "bitvec", + "byteorder", + "ironrdp-core", + "ironrdp-pdu", + "num-derive", + "num-traits", + "yuv", +] + +[[package]] +name = "ironrdp-pdu" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccd1179a4d106df1930347701388b5c79bc3725fa7dab4438d57db0d9d19347" +dependencies = [ + "bit_field", + "bitflags", + "byteorder", + "der-parser", + "ironrdp-core", + "ironrdp-error", + "md-5", + "num-bigint", + "num-derive", + "num-integer", + "num-traits", + "pkcs1", + "sha1", + "tap", + "x509-cert", +] + +[[package]] +name = "ironrdp-session" +version = "0.11.0" +dependencies = [ + "ironrdp-bulk", + "ironrdp-core", + "ironrdp-displaycontrol", + "ironrdp-dvc", + "ironrdp-error", + "ironrdp-graphics", + "ironrdp-pdu", + "ironrdp-svc", + "qoicoubeh", + "tracing", + "zstd-safe", +] + +[[package]] +name = "ironrdp-svc" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c36b82ab0f7fef2668fb7004008a0f3c100a3d9a7b18bd4495a71aba55b796" +dependencies = [ + "bitflags", + "ironrdp-core", + "ironrdp-pdu", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qoicoubeh" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b82aa3fef8a980075775b8c46f874823b5b4a15de327d2dbb3b6fd818480ba" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der", + "spki", + "tls_codec", +] + +[[package]] +name = "yuv" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d85a782d94ee43f078bcfd6fa82d4e6a5b2d1cfbbad168e4df5a9f7b39ef48c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/rust/vendor/ironrdp-session/Cargo.toml b/rust/vendor/ironrdp-session/Cargo.toml new file mode 100644 index 0000000..de7afb3 --- /dev/null +++ b/rust/vendor/ironrdp-session/Cargo.toml @@ -0,0 +1,210 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.89" +name = "ironrdp-session" +version = "0.11.0" +authors = [ + "Devolutions Inc. ", + "Teleport ", +] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "State machines to drive an RDP session" +homepage = "https://github.com/Devolutions/IronRDP" +readme = "README.md" +keywords = [ + "rdp", + "remote-desktop", + "network", + "client", + "protocol", +] +categories = ["network-programming"] +license = "MIT OR Apache-2.0" +repository = "https://github.com/Devolutions/IronRDP" +resolver = "2" + +[features] +default = [] +qoi = [ + "dep:qoicoubeh", + "ironrdp-pdu/qoi", +] +qoiz = [ + "dep:zstd-safe", + "qoi", +] + +[lib] +name = "ironrdp_session" +path = "src/lib.rs" +test = false +doctest = false + +[dependencies.ironrdp-bulk] +version = "0.1" + +[dependencies.ironrdp-core] +version = "0.2" + +[dependencies.ironrdp-displaycontrol] +version = "0.8" + +[dependencies.ironrdp-dvc] +version = "0.8" + +[dependencies.ironrdp-error] +version = "0.2" + +[dependencies.ironrdp-graphics] +version = "0.9" + +[dependencies.ironrdp-pdu] +version = "0.9" +features = ["std"] + +[dependencies.ironrdp-svc] +version = "0.8" + +[dependencies.qoicoubeh] +version = "0.5" +optional = true + +[dependencies.tracing] +version = "0.1" +features = ["log"] + +[dependencies.zstd-safe] +version = "7.2" +features = ["std"] +optional = true + +[lints.clippy] +alloc_instead_of_core = "warn" +allow_attributes = "warn" +as_conversions = "warn" +as_pointer_underscore = "warn" +as_ptr_cast_mut = "warn" +as_underscore = "warn" +cast_lossless = "warn" +cast_possible_truncation = "warn" +cast_possible_wrap = "warn" +cast_ptr_alignment = "warn" +cast_sign_loss = "warn" +cfg_not_test = "warn" +checked_conversions = "warn" +clone_on_ref_ptr = "warn" +cloned_instead_of_copied = "warn" +collection_is_never_read = "warn" +copy_iterator = "warn" +dbg_macro = "warn" +deref_by_slicing = "warn" +disallowed_script_idents = "warn" +doc_include_without_cfg = "warn" +empty_drop = "warn" +empty_enum_variants_with_brackets = "warn" +expl_impl_clone_on_copy = "warn" +filetype_is_file = "warn" +float_cmp = "warn" +float_cmp_const = "warn" +fn_to_numeric_cast_any = "warn" +get_unwrap = "warn" +implicit_clone = "warn" +infinite_loop = "warn" +inline_always = "warn" +large_futures = "warn" +large_include_file = "warn" +large_stack_frames = "warn" +large_types_passed_by_value = "warn" +lossy_float_literal = "warn" +map_with_unused_argument_over_ranges = "warn" +mem_forget = "warn" +missing_panics_doc = "warn" +missing_safety_doc = "warn" +mixed_read_write_in_expression = "warn" +multiple_inherent_impl = "warn" +multiple_unsafe_ops_per_block = "warn" +needless_raw_strings = "warn" +non_ascii_literal = "warn" +non_zero_suggestions = "warn" +or_fun_call = "warn" +panic = "warn" +partial_pub_fields = "warn" +precedence_bits = "warn" +print_stderr = "warn" +print_stdout = "warn" +ptr_cast_constness = "warn" +pub_without_shorthand = "warn" +range_plus_one = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +redundant_clone = "warn" +redundant_type_annotations = "warn" +renamed_function_params = "warn" +rest_pat_in_fully_bound_structs = "warn" +return_self_not_must_use = "warn" +same_name_method = "warn" +self_named_module_files = "warn" +semicolon_outside_block = "warn" +separated_literal_suffix = "warn" +similar_names = "warn" +std_instead_of_core = "warn" +str_to_string = "warn" +string_add = "warn" +string_lit_chars_any = "warn" +string_slice = "warn" +suspicious_xor_used_as_pow = "warn" +todo = "warn" +trait_duplication_in_bounds = "warn" +transmute_ptr_to_ptr = "warn" +try_err = "warn" +type_repetition_in_bounds = "warn" +undocumented_unsafe_blocks = "warn" +unnecessary_box_returns = "warn" +unnecessary_safety_comment = "warn" +unnecessary_self_imports = "warn" +unused_result_ok = "warn" +unused_self = "warn" +unused_trait_names = "warn" +unwrap_used = "warn" +useless_let_if_seq = "warn" +wildcard_dependencies = "warn" +wildcard_imports = "warn" + +[lints.rust] +absolute_paths_not_starting_with_crate = "warn" +ambiguous_negative_literals = "warn" +elided_lifetimes_in_paths = "warn" +invalid_reference_casting = "warn" +keyword_idents = "warn" +macro_use_extern_crate = "warn" +missing_unsafe_on_extern = "warn" +noop_method_call = "warn" +redundant_imports = "warn" +redundant_lifetimes = "warn" +single_use_lifetimes = "warn" +trivial_numeric_casts = "warn" +unit_bindings = "warn" +unreachable_pub = "warn" +unsafe_attr_outside_unsafe = "warn" +unsafe_op_in_unsafe_fn = "warn" +unused_crate_dependencies = "warn" +unused_lifetimes = "warn" +unused_macro_rules = "warn" +unused_qualifications = "warn" +unused_unsafe = "warn" diff --git a/rust/vendor/ironrdp-session/Cargo.toml.orig b/rust/vendor/ironrdp-session/Cargo.toml.orig new file mode 100644 index 0000000..dd29ab9 --- /dev/null +++ b/rust/vendor/ironrdp-session/Cargo.toml.orig @@ -0,0 +1,38 @@ +[package] +name = "ironrdp-session" +version = "0.11.0" +readme = "README.md" +description = "State machines to drive an RDP session" +edition.workspace = true +rust-version = "1.89" +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +doctest = false +test = false + +[features] +default = [] +qoi = ["dep:qoicoubeh", "ironrdp-pdu/qoi"] +qoiz = ["dep:zstd-safe", "qoi"] + +[dependencies] +ironrdp-bulk = { path = "../ironrdp-bulk", version = "0.1" } +ironrdp-core = { path = "../ironrdp-core", version = "0.2" } # public +ironrdp-svc = { path = "../ironrdp-svc", version = "0.8" } # public +ironrdp-dvc = { path = "../ironrdp-dvc", version = "0.8" } # public +ironrdp-error = { path = "../ironrdp-error", version = "0.2" } # public +ironrdp-graphics = { path = "../ironrdp-graphics", version = "0.9" } # public +ironrdp-pdu = { path = "../ironrdp-pdu", version = "0.9", features = ["std"] } # public +ironrdp-displaycontrol = { path = "../ironrdp-displaycontrol", version = "0.8" } +tracing = { version = "0.1", features = ["log"] } +qoicoubeh = { version = "0.5", optional = true } +zstd-safe = { version = "7.2", optional = true, features = ["std"] } + +[lints] +workspace = true diff --git a/rust/vendor/ironrdp-session/LICENSE-APACHE b/rust/vendor/ironrdp-session/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/rust/vendor/ironrdp-session/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/rust/vendor/ironrdp-session/LICENSE-MIT b/rust/vendor/ironrdp-session/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/rust/vendor/ironrdp-session/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/rust/vendor/ironrdp-session/README.md b/rust/vendor/ironrdp-session/README.md new file mode 100644 index 0000000..dfaa137 --- /dev/null +++ b/rust/vendor/ironrdp-session/README.md @@ -0,0 +1,7 @@ +# IronRDP Session + +Abstract state machine to drive an RDP session. + +This crate is part of the [IronRDP] project. + +[IronRDP]: https://github.com/Devolutions/IronRDP diff --git a/rust/vendor/ironrdp-session/src/active_stage.rs b/rust/vendor/ironrdp-session/src/active_stage.rs new file mode 100644 index 0000000..f1b712e --- /dev/null +++ b/rust/vendor/ironrdp-session/src/active_stage.rs @@ -0,0 +1,469 @@ +use std::sync::Arc; + +use ironrdp_bulk::BulkCompressor; +use ironrdp_core::{ReadCursor, WriteBuf}; +use ironrdp_displaycontrol::client::DisplayControlClient; +use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel}; +use ironrdp_graphics::pointer::DecodedPointer; +use ironrdp_pdu::geometry::InclusiveRectangle; +use ironrdp_pdu::input::fast_path::{FastPathInput, FastPathInputEvent}; +use ironrdp_pdu::rdp::autodetect::AutoDetectRequest; +use ironrdp_pdu::rdp::client_info::CompressionType as PduCompressionType; +use ironrdp_pdu::rdp::headers::ShareDataPdu; +use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu; +use ironrdp_pdu::slow_path::{self, GraphicsUpdateType}; +use ironrdp_pdu::{Action, mcs}; +use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages}; +use tracing::{debug, info, warn}; + +use crate::fast_path::UpdateKind; +use crate::image::DecodedImage; +use crate::{SessionError, SessionErrorExt as _, SessionResult, fast_path, x224}; + +/// Converts the PDU-layer compression type to the bulk crate's compression type. +fn to_bulk_compression_type(ct: PduCompressionType) -> ironrdp_bulk::CompressionType { + match ct { + PduCompressionType::K8 => ironrdp_bulk::CompressionType::Rdp4, + PduCompressionType::K64 => ironrdp_bulk::CompressionType::Rdp5, + PduCompressionType::Rdp6 => ironrdp_bulk::CompressionType::Rdp6, + PduCompressionType::Rdp61 => ironrdp_bulk::CompressionType::Rdp61, + } +} + +pub struct ActiveStage { + x224_processor: x224::Processor, + fast_path_processor: fast_path::Processor, + enable_server_pointer: bool, +} + +/// Builder for [`ActiveStage`]. +/// +/// All fields are required; they are typically taken straight from `ironrdp-connector`’s +/// `ConnectionResult` once the connection sequence is finalized. +pub struct ActiveStageBuilder { + pub static_channels: StaticChannelSet, + pub user_channel_id: u16, + pub io_channel_id: u16, + pub message_channel_id: Option, + pub share_id: u32, + /// The bulk compression type that was negotiated, if any. + pub compression_type: Option, + /// Enable server-side pointer updates (client-side pointer rendering). + pub enable_server_pointer: bool, + /// Use software rendering mode for pointer bitmap generation. + pub pointer_software_rendering: bool, +} + +impl ActiveStageBuilder { + pub fn build(self) -> ActiveStage { + let Self { + static_channels, + user_channel_id, + io_channel_id, + message_channel_id, + share_id, + compression_type, + enable_server_pointer, + pointer_software_rendering, + } = self; + + let x224_processor = x224::Processor::new( + static_channels, + user_channel_id, + io_channel_id, + message_channel_id, + share_id, + ); + + // Create bulk decompressor if compression was negotiated + let bulk_decompressor = compression_type.and_then(|ct| { + let bulk_ct = to_bulk_compression_type(ct); + match BulkCompressor::new(bulk_ct) { + Ok(compressor) => { + info!(compression_type = %bulk_ct, "Bulk decompressor initialized for FastPath"); + Some(compressor) + } + Err(e) => { + tracing::error!(error = %e, "Failed to create bulk decompressor, compression disabled"); + None + } + } + }); + + let fast_path_processor = fast_path::ProcessorBuilder { + io_channel_id, + user_channel_id, + share_id, + enable_server_pointer, + pointer_software_rendering, + bulk_decompressor, + } + .build(); + + ActiveStage { + x224_processor, + fast_path_processor, + enable_server_pointer, + } + } +} + +impl ActiveStage { + pub fn update_mouse_pos(&mut self, x: u16, y: u16) { + self.fast_path_processor.update_mouse_pos(x, y); + } + + /// Encodes outgoing input events and modifies image if necessary (e.g for client-side pointer + /// rendering). + pub fn process_fastpath_input( + &mut self, + image: &mut DecodedImage, + events: &[FastPathInputEvent], + ) -> SessionResult> { + if events.is_empty() { + return Ok(Vec::new()); + } + + // Mouse move events are prevalent, so we can preallocate space for + // response frame + graphics update + let mut output = Vec::with_capacity(2); + + // Encoding fastpath response frame + // PERF: unnecessary copy + let fastpath_input = FastPathInput::new(events.to_vec()).map_err(SessionError::decode)?; + let frame = ironrdp_core::encode_vec(&fastpath_input).map_err(SessionError::encode)?; + output.push(ActiveStageOutput::ResponseFrame(frame)); + + // If pointer rendering is disabled - we can skip the rest + if !self.enable_server_pointer { + return Ok(output); + } + + // If mouse was moved by client - we should update framebuffer to reflect new + // pointer position + let mouse_pos = events.iter().find_map(|event| match event { + FastPathInputEvent::MouseEvent(event) => Some((event.x_position, event.y_position)), + FastPathInputEvent::MouseEventEx(event) => Some((event.x_position, event.y_position)), + _ => None, + }); + + let (mouse_x, mouse_y) = match mouse_pos { + Some(mouse_pos) => mouse_pos, + None => return Ok(output), + }; + + // Graphics update is only sent when update is visually changed the framebuffer + if let Some(rect) = image.move_pointer(mouse_x, mouse_y)? { + output.push(ActiveStageOutput::GraphicsUpdate(rect)); + } + + Ok(output) + } + + /// Process a frame received from the server. + pub fn process( + &mut self, + image: &mut DecodedImage, + action: Action, + frame: &[u8], + ) -> SessionResult> { + let (mut stage_outputs, processor_updates) = match action { + Action::FastPath => { + let mut output = WriteBuf::new(); + let processor_updates = self.fast_path_processor.process(image, frame, &mut output)?; + ( + vec![ActiveStageOutput::ResponseFrame(output.into_inner())], + processor_updates, + ) + } + Action::X224 => { + let x224_outputs = self.x224_processor.process(frame)?; + let mut stage_outputs = Vec::new(); + let mut processor_updates = Vec::new(); + + for output in x224_outputs { + match output { + x224::ProcessorOutput::GraphicsUpdate(data) => { + let updates = process_slow_path_graphics(&mut self.fast_path_processor, image, &data)?; + processor_updates.extend(updates); + } + x224::ProcessorOutput::PointerUpdate(data) => { + let updates = process_slow_path_pointer(&mut self.fast_path_processor, image, &data)?; + processor_updates.extend(updates); + } + other => { + stage_outputs.push(ActiveStageOutput::try_from(other)?); + } + } + } + + (stage_outputs, processor_updates) + } + }; + + for update in processor_updates { + match update { + UpdateKind::None => {} + UpdateKind::Region(region) => { + stage_outputs.push(ActiveStageOutput::GraphicsUpdate(region)); + } + UpdateKind::PointerDefault => { + stage_outputs.push(ActiveStageOutput::PointerDefault); + } + UpdateKind::PointerHidden => { + stage_outputs.push(ActiveStageOutput::PointerHidden); + } + UpdateKind::PointerPosition { x, y } => { + stage_outputs.push(ActiveStageOutput::PointerPosition { x, y }); + } + UpdateKind::PointerBitmap(pointer) => { + stage_outputs.push(ActiveStageOutput::PointerBitmap(pointer)); + } + } + } + + Ok(stage_outputs) + } + + pub fn set_fastpath_processor(&mut self, processor: fast_path::Processor) { + self.fast_path_processor = processor; + } + + /// Updates the share_id used by the x224 processor for encoding ShareDataPdu. + /// Must be called during Deactivation-Reactivation if the server assigns a new share_id. + pub fn set_share_id(&mut self, share_id: u32) { + self.x224_processor.set_share_id(share_id); + } + + pub fn set_enable_server_pointer(&mut self, enable_server_pointer: bool) { + self.enable_server_pointer = enable_server_pointer; + } + + /// Encodes client-side graceful shutdown request. Note that upon sending this request, + /// client should wait for server's ShutdownDenied PDU before closing the connection. + /// + /// Client-side graceful shutdown is defined in [MS-RDPBCGR] + /// + /// [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + pub fn graceful_shutdown(&self) -> SessionResult> { + let mut frame = WriteBuf::new(); + self.x224_processor + .encode_static(&mut frame, ShareDataPdu::ShutdownRequest)?; + + Ok(vec![ActiveStageOutput::ResponseFrame(frame.into_inner())]) + } + + /// Send a pdu on the static global channel. Typically used to send input events + pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult { + self.x224_processor.encode_static(output, pdu) + } + + pub fn get_svc_processor(&mut self) -> Option<&T> { + self.x224_processor.get_svc_processor() + } + + pub fn get_svc_processor_mut(&mut self) -> Option<&mut T> { + self.x224_processor.get_svc_processor_mut() + } + + pub fn get_dvc(&mut self) -> Option<&DynamicVirtualChannel> { + self.x224_processor.get_dvc::() + } + + pub fn get_dvc_by_channel_id(&mut self, channel_id: u32) -> Option<&DynamicVirtualChannel> { + self.x224_processor.get_dvc_by_channel_id(channel_id) + } + + /// Completes user's SVC request with data, required to sent it over the network and returns + /// a buffer with encoded data. + pub fn process_svc_processor_messages( + &self, + messages: SvcProcessorMessages, + ) -> SessionResult> { + self.x224_processor.process_svc_processor_messages(messages) + } + + /// Fully encodes a resize request for sending over the Display Control Virtual Channel. + /// + /// If the Display Control Virtual Channel is not available, or not yet connected, this method + /// will return `None`. + /// + /// Per [2.2.2.2.1]: + /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value. + /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels. + /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent. + /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm. + /// + /// Use [`ironrdp_displaycontrol::pdu::MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function + /// to ensure the display size is within the valid range. + /// + /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c + pub fn encode_resize( + &mut self, + width: u32, + height: u32, + scale_factor: Option, + physical_dims: Option<(u32, u32)>, + ) -> Option>> { + if let Some(dvc) = self.get_dvc::() { + if let Some(channel_id) = dvc.channel_id() { + let display_control = dvc.channel_processor_downcast_ref::()?; + let svc_messages = match display_control.encode_single_primary_monitor( + channel_id, + width, + height, + scale_factor, + physical_dims, + ) { + Ok(messages) => messages, + Err(e) => return Some(Err(SessionError::encode(e))), + }; + + return Some( + self.process_svc_processor_messages(SvcProcessorMessages::::new(svc_messages)), + ); + } else { + debug!("Could not encode a resize: Display Control Virtual Channel is not yet connected"); + } + } else { + debug!("Could not encode a resize: Display Control Virtual Channel is not available"); + } + + None + } + + pub fn encode_dvc_messages(&mut self, messages: Vec) -> SessionResult> { + self.process_svc_processor_messages(SvcProcessorMessages::::new(messages)) + } +} + +#[derive(Debug)] +pub enum ActiveStageOutput { + ResponseFrame(Vec), + GraphicsUpdate(InclusiveRectangle), + PointerDefault, + PointerHidden, + PointerPosition { + x: u16, + y: u16, + }, + PointerBitmap(Arc), + Terminate(GracefulDisconnectReason), + /// Received a Server Deactivate All PDU. The consumer should execute the [Deactivation-Reactivation Sequence]. + /// + /// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + DeactivateAll, + /// Server Initiate Multitransport Request. The application should establish a + /// sideband UDP transport using the provided request parameters. + /// + /// See [\[MS-RDPBCGR\] 2.2.15.1]. + /// + /// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490 + MultitransportRequest(MultitransportRequestPdu), + /// Server-reported network characteristics ([\[MS-RDPBCGR\] 2.2.14.1.5]). + /// + /// Contains an [`AutoDetectRequest::NetworkCharacteristicsResult`] with + /// RTT and/or bandwidth measurements computed by the server. + /// + /// See [\[MS-RDPBCGR\] 2.2.14.1.5]. + /// + /// [\[MS-RDPBCGR\] 2.2.14.1.5]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/228ffc5c-b60c-4d3e-9781-ac613f822fdf + AutoDetect(AutoDetectRequest), +} + +impl TryFrom for ActiveStageOutput { + type Error = SessionError; + + fn try_from(value: x224::ProcessorOutput) -> Result { + match value { + x224::ProcessorOutput::ResponseFrame(frame) => Ok(Self::ResponseFrame(frame)), + x224::ProcessorOutput::Disconnect(desc) => { + let desc = match desc { + x224::DisconnectDescription::McsDisconnect(reason) => match reason { + mcs::DisconnectReason::ProviderInitiated => GracefulDisconnectReason::ServerInitiated, + mcs::DisconnectReason::UserRequested => GracefulDisconnectReason::UserInitiated, + other => GracefulDisconnectReason::Other(other.description().to_owned()), + }, + x224::DisconnectDescription::ErrorInfo(info) => GracefulDisconnectReason::Other(info.description()), + }; + + Ok(Self::Terminate(desc)) + } + x224::ProcessorOutput::DeactivateAll => Ok(Self::DeactivateAll), + x224::ProcessorOutput::MultitransportRequest(pdu) => Ok(Self::MultitransportRequest(pdu)), + x224::ProcessorOutput::AutoDetect(request) => Ok(Self::AutoDetect(request)), + // GraphicsUpdate and PointerUpdate are consumed in ActiveStage::process() + // before reaching this conversion. + x224::ProcessorOutput::GraphicsUpdate(_) | x224::ProcessorOutput::PointerUpdate(_) => Err( + SessionError::general("slow-path graphics/pointer updates should be handled before this conversion"), + ), + } + } +} + +/// Reasons for graceful disconnect. This type provides GUI-friendly descriptions for +/// disconnect reasons. +#[derive(Debug, Clone)] +pub enum GracefulDisconnectReason { + UserInitiated, + ServerInitiated, + Other(String), +} + +impl GracefulDisconnectReason { + pub fn description(&self) -> String { + match self { + GracefulDisconnectReason::UserInitiated => "user initiated disconnect".to_owned(), + GracefulDisconnectReason::ServerInitiated => "server initiated disconnect".to_owned(), + GracefulDisconnectReason::Other(description) => description.clone(), + } + } +} + +impl core::fmt::Display for GracefulDisconnectReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(&self.description()) + } +} + +/// Parse and process a slow-path graphics update through the shared bitmap pipeline. +fn process_slow_path_graphics( + fast_path_processor: &mut fast_path::Processor, + image: &mut DecodedImage, + data: &[u8], +) -> SessionResult> { + let mut src = ReadCursor::new(data); + let update_type = slow_path::read_graphics_update_type(&mut src).map_err(SessionError::decode)?; + + match update_type { + GraphicsUpdateType::Bitmap => { + let bitmap = slow_path::decode_slow_path_bitmap(&mut src).map_err(SessionError::decode)?; + fast_path_processor.process_bitmap_update(image, bitmap) + } + GraphicsUpdateType::Orders => { + warn!("Slow-path drawing orders not supported (MS-RDPEGDI)"); + Ok(Vec::new()) + } + GraphicsUpdateType::Palette => { + warn!("Slow-path palette update not supported (8bpp)"); + Ok(Vec::new()) + } + // Synchronize is an artifact from the T.128 multipoint protocol + // and carries no data. Safe to ignore. + GraphicsUpdateType::Synchronize => { + debug!("Ignoring slow-path synchronize update"); + Ok(Vec::new()) + } + } +} + +/// Parse and process a slow-path pointer update through the shared pointer pipeline. +fn process_slow_path_pointer( + fast_path_processor: &mut fast_path::Processor, + image: &mut DecodedImage, + data: &[u8], +) -> SessionResult> { + let mut src = ReadCursor::new(data); + let pointer = slow_path::decode_slow_path_pointer(&mut src).map_err(SessionError::decode)?; + fast_path_processor.process_pointer_update(image, pointer) +} diff --git a/rust/vendor/ironrdp-session/src/fast_path.rs b/rust/vendor/ironrdp-session/src/fast_path.rs new file mode 100644 index 0000000..483ca6c --- /dev/null +++ b/rust/vendor/ironrdp-session/src/fast_path.rs @@ -0,0 +1,821 @@ +use std::sync::Arc; + +use ironrdp_bulk::BulkCompressor; +use ironrdp_core::{DecodeErrorKind, ReadCursor, WriteBuf, decode_cursor}; +use ironrdp_graphics::image_processing::PixelFormat; +use ironrdp_graphics::pointer::{DecodedPointer, PointerBitmapTarget}; +use ironrdp_graphics::rdp6::BitmapStreamDecoder; +use ironrdp_graphics::rle::RlePixelFormat; +use ironrdp_pdu::bitmap::BitmapUpdateData; +use ironrdp_pdu::codecs::rfx::FrameAcknowledgePdu; +use ironrdp_pdu::fast_path::{FastPathHeader, FastPathUpdate, FastPathUpdatePdu, Fragmentation}; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use ironrdp_pdu::pointer::PointerUpdateData; +use ironrdp_pdu::rdp::capability_sets::{CODEC_ID_NONE, CODEC_ID_REMOTEFX, CodecId}; +use ironrdp_pdu::rdp::headers::{CompressionFlags, ShareDataPdu}; +use ironrdp_pdu::surface_commands::{FrameAction, FrameMarkerPdu, SurfaceCommand}; +use tracing::{debug, trace, warn}; + +use crate::image::DecodedImage; +use crate::palette::Palette; +use crate::pointer::PointerCache; +use crate::{SessionError, SessionErrorExt as _, SessionResult, custom_err, reason_err, rfx}; + +#[cfg(test)] +mod tests { + use ironrdp_pdu::bitmap::{BitmapData, Compression}; + + use super::*; + + #[test] + fn raw_bitmap_removes_byte_padding_without_changing_source_stride() { + let mut processor = ProcessorBuilder { + io_channel_id: 0, + user_channel_id: 0, + share_id: 0, + enable_server_pointer: false, + pointer_software_rendering: false, + bulk_decompressor: None, + } + .build(); + let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3); + + // Two bottom-up BGR rows of three source pixels each. The final source + // column is outside the destination and each row has three pad bytes. + let bitmap_data = [ + 3, 2, 1, 6, 5, 4, 9, 8, 7, 0xff, 0xff, 0xff, // bottom row + 15, 14, 13, 18, 17, 16, 21, 20, 19, 0xff, 0xff, 0xff, // top row + ]; + let bitmap_update = BitmapUpdateData { + rectangles: vec![BitmapData { + rectangle: InclusiveRectangle { + left: 1, + top: 0, + right: 2, + bottom: 1, + }, + width: 3, + height: 2, + bits_per_pixel: 24, + compression_flags: Compression::empty(), + compressed_data_header: None, + bitmap_data: &bitmap_data, + }], + }; + + processor.process_bitmap_update(&mut image, bitmap_update).unwrap(); + + let pixel = |x: usize, y: usize| -> [u8; 4] { + let offset = (y * usize::from(image.width()) + x) * 4; + image.data()[offset..offset + 4] + .try_into() + .expect("pixel has four channels") + }; + assert_eq!(pixel(1, 0), [13, 14, 15, 255]); + assert_eq!(pixel(2, 0), [16, 17, 18, 255]); + assert_eq!(pixel(1, 1), [1, 2, 3, 255]); + assert_eq!(pixel(2, 1), [4, 5, 6, 255]); + assert_eq!(pixel(3, 0), [0, 0, 0, 0]); + } +} + +#[derive(Debug)] +pub enum UpdateKind { + None, + Region(InclusiveRectangle), + PointerDefault, + PointerHidden, + PointerPosition { x: u16, y: u16 }, + PointerBitmap(Arc), +} + +pub struct Processor { + complete_data: CompleteData, + rfx_handler: rfx::DecodingContext, + marker_processor: FrameMarkerProcessor, + bitmap_stream_decoder: BitmapStreamDecoder, + pointer_cache: PointerCache, + use_system_pointer: bool, + mouse_pos_update: Option<(u16, u16)>, + enable_server_pointer: bool, + pointer_software_rendering: bool, + /// Bulk decompressor for server-to-client compressed PDUs. + /// `None` when compression was not negotiated. + bulk_decompressor: Option, + /// Current 8bpp color palette. Updated by Palette fast-path updates. + palette: Palette, + #[cfg(feature = "qoiz")] + zdctx: zstd_safe::DCtx<'static>, +} + +impl Processor { + pub fn update_mouse_pos(&mut self, x: u16, y: u16) { + self.mouse_pos_update = Some((x, y)); + } + + /// Process input fast path frame and return list of updates. + pub fn process( + &mut self, + image: &mut DecodedImage, + input: &[u8], + output: &mut WriteBuf, + ) -> SessionResult> { + let mut processor_updates = Vec::new(); + + if let Some((x, y)) = self.mouse_pos_update.take() { + if let Some(rect) = image.move_pointer(x, y)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + + let mut input = ReadCursor::new(input); + + let header = decode_cursor::(&mut input).map_err(SessionError::decode)?; + trace!(fast_path_header = ?header, "Received Fast-Path packet"); + + // A single FastPath output PDU can contain multiple updates. + // Loop over all updates within the PDU payload. + while !input.is_empty() { + let update_result = self.process_single_update(&mut input, image, output)?; + processor_updates.extend(update_result); + } + + Ok(processor_updates) + } + + /// Process a single FastPath update from the cursor, advancing past it. + fn process_single_update( + &mut self, + input: &mut ReadCursor<'_>, + image: &mut DecodedImage, + output: &mut WriteBuf, + ) -> SessionResult> { + let mut processor_updates = Vec::new(); + + let update_pdu = decode_cursor::>(input).map_err(SessionError::decode)?; + trace!(fast_path_update_fragmentation = ?update_pdu.fragmentation); + + // Decompress the payload if the server sent it compressed. + let decompressed_data; + let payload = if let Some(flags) = update_pdu.compression_flags { + if flags.contains(CompressionFlags::COMPRESSED) || flags.contains(CompressionFlags::FLUSHED) { + let bulk_flags = + u32::from(flags.bits()) | u32::from(update_pdu.compression_type.map_or(0, |ct| ct.as_u8())); + + if let Some(ref mut decompressor) = self.bulk_decompressor { + let decompressed = decompressor + .decompress(update_pdu.data, bulk_flags) + .map_err(|e| reason_err!("FastPath", "bulk decompression failed: {}", e))?; + // Copy decompressed data before accessing metrics (releases the mutable borrow). + decompressed_data = decompressed.to_vec(); + debug!( + compressed_size = update_pdu.data.len(), + decompressed_size = decompressed_data.len(), + compression_type = ?update_pdu.compression_type, + compression_ratio = format_args!("{:.2}x", decompressor.compression_ratio()), + total_compressed = decompressor.total_compressed_bytes(), + total_uncompressed = decompressor.total_uncompressed_bytes(), + "Decompressed FastPath update" + ); + decompressed_data.as_slice() + } else { + warn!("Received compressed FastPath data but no decompressor is configured"); + update_pdu.data + } + } else { + // Compression flags present but COMPRESSED bit not set — pass data through. + // Still need to inform the decompressor of FLUSHED/AT_FRONT flags even + // without compressed payload. + update_pdu.data + } + } else { + update_pdu.data + }; + + let processed_complete_data = self.complete_data.process_data(payload, update_pdu.fragmentation); + + let update_code = update_pdu.update_code; + + let Some(data) = processed_complete_data else { + return Ok(processor_updates); + }; + + let update = FastPathUpdate::decode_with_code(data.as_slice(), update_code); + + match update { + Ok(FastPathUpdate::SurfaceCommands(surface_commands)) => { + trace!("Received Surface Commands: {} pieces", surface_commands.len()); + let update_region = self.process_surface_commands(image, output, surface_commands)?; + processor_updates.push(UpdateKind::Region(update_region)); + } + Ok(FastPathUpdate::Bitmap(bitmap_update)) => { + trace!("Received bitmap update"); + let updates = self.process_bitmap_update(image, bitmap_update)?; + processor_updates.extend(updates); + } + Ok(FastPathUpdate::Pointer(update)) => { + let updates = self.process_pointer_update(image, update)?; + processor_updates.extend(updates); + } + Ok(FastPathUpdate::Palette(palette_data)) => { + trace!("Received palette update"); + self.palette.process_update(palette_data); + } + Err(e) => { + // FIXME: This seems to be a way of special-handling the error case in FastPathUpdate::decode_cursor_with_code + // to ignore the unsupported update PDUs, but this is a fragile logic and the rationale behind it is not + // obvious. + if let DecodeErrorKind::InvalidField { field, reason } = e.kind() { + warn!(field, reason, "Received invalid Fast-Path update"); + processor_updates.push(UpdateKind::None); + } else { + return Err(custom_err!("Fast-Path", e)); + } + } + }; + + Ok(processor_updates) + } + + /// Process a bitmap update, shared between fast-path and slow-path pipelines. + pub fn process_bitmap_update( + &mut self, + image: &mut DecodedImage, + bitmap_update: BitmapUpdateData<'_>, + ) -> SessionResult> { + let mut buf = Vec::new(); + let mut update_kind = UpdateKind::None; + + for update in bitmap_update.rectangles { + trace!("{update:?}"); + buf.clear(); + + let width = update.width.min(update.rectangle.width()); + let height = update.height.min(update.rectangle.height()); + if width == 0 || height == 0 { + warn!( + bitmap_width = update.width, + bitmap_height = update.height, + destination = ?update.rectangle, + "Skipping bitmap with an empty source or destination" + ); + continue; + } + + // `width` and `height` describe the source bitmap layout, while + // destination bounds describe where its visible portion is drawn. + // Servers can send source rows wider than the destination; preserve + // that source stride and discard only the excess source extent. + let bitmap_rectangle = InclusiveRectangle { + left: update.rectangle.left, + top: update.rectangle.top, + right: update.rectangle.left + width - 1, + bottom: update.rectangle.top + height - 1, + }; + + // Bitmap data is either compressed or uncompressed, depending + // on whether the BITMAP_COMPRESSION flag is present in the + // flags field. + let update_rectangle = if update + .compression_flags + .contains(ironrdp_pdu::bitmap::Compression::BITMAP_COMPRESSION) + { + if update.bits_per_pixel == 32 { + // Compressed bitmaps at a color depth of 32 bpp are compressed using RDP 6.0 + // Bitmap Compression and stored inside an RDP 6.0 Bitmap Compressed Stream + // structure ([MS-RDPEGDI] section 2.2.2.5.1). + debug!("32 bpp compressed RDP6_BITMAP_STREAM"); + + match self.bitmap_stream_decoder.decode_bitmap_stream_to_rgb24( + update.bitmap_data, + &mut buf, + usize::from(update.width), + usize::from(update.height), + ) { + // RDP6 bitmap streams are bottom-up (upstream #1524). + Ok(()) => image.apply_rgb24(&buf, &bitmap_rectangle, update.width, true)?, + Err(err) => { + warn!("Invalid RDP6_BITMAP_STREAM: {err}"); + update.rectangle.clone() + } + } + } else { + // Compressed bitmaps not in 32 bpp format are compressed using Interleaved + // RLE and encapsulated in an RLE Compressed Bitmap Stream structure (section + // 2.2.9.1.1.3.1.2.4). + debug!(bpp = update.bits_per_pixel, "Non-32 bpp compressed RLE_BITMAP_STREAM",); + + match ironrdp_graphics::rle::decompress( + update.bitmap_data, + &mut buf, + usize::from(update.width), + usize::from(update.height), + usize::from(update.bits_per_pixel), + ) { + Ok(RlePixelFormat::Rgb16) => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?, + Ok(RlePixelFormat::Rgb15) => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?, + Ok(RlePixelFormat::Rgb24) => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?, + Ok(RlePixelFormat::Rgb8) => image.apply_rgb8_with_palette( + &buf, + &bitmap_rectangle, + self.palette.colors(), + update.width, + )?, + + Err(e) => { + warn!("Invalid RLE-compressed bitmap: {e}"); + update.rectangle.clone() + } + } + } + } else { + // Uncompressed bitmap data is formatted as a bottom-up, left-to-right series of + // pixels. Each pixel is a whole number of bytes. Each row contains a multiple of + // four bytes (including up to three bytes of padding, as necessary). + // [MS-RDPBCGR] 2.2.9.1.1.3.1.2.2 + trace!("Uncompressed raw bitmap"); + + let bpp = usize::from(update.bits_per_pixel); + let width = usize::from(update.width); + let bytes_per_pixel = bpp.div_ceil(8); + let row_bytes = width * bytes_per_pixel; + let padded_row_bytes = (row_bytes + 3) & !3; + + if padded_row_bytes != row_bytes { + // Strip only byte padding; the tightly packed rows still retain + // `update.width` pixels and therefore their source stride. + buf.clear(); + for row in update + .bitmap_data + .chunks_exact(padded_row_bytes) + .take(usize::from(update.height)) + { + buf.extend_from_slice(&row[..row_bytes]); + } + + match update.bits_per_pixel { + 8 => image.apply_rgb8_with_palette( + &buf, + &bitmap_rectangle, + self.palette.colors(), + update.width, + )?, + 15 => image.apply_rgb15_bitmap(&buf, &bitmap_rectangle, update.width)?, + 16 => image.apply_rgb16_bitmap(&buf, &bitmap_rectangle, update.width)?, + 24 => image.apply_bgr24_bitmap(&buf, &bitmap_rectangle, update.width)?, + 32 => image.apply_rgb32_bitmap(&buf, PixelFormat::BgrX32, &bitmap_rectangle, update.width)?, + _ => { + warn!("Unsupported uncompressed bitmap depth: {bpp} bpp"); + update.rectangle.clone() + } + } + } else { + match update.bits_per_pixel { + 8 => image.apply_rgb8_with_palette( + update.bitmap_data, + &bitmap_rectangle, + self.palette.colors(), + update.width, + )?, + 15 => image.apply_rgb15_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?, + 16 => image.apply_rgb16_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?, + 24 => image.apply_bgr24_bitmap(update.bitmap_data, &bitmap_rectangle, update.width)?, + 32 => image.apply_rgb32_bitmap( + update.bitmap_data, + PixelFormat::BgrX32, + &bitmap_rectangle, + update.width, + )?, + _ => { + warn!("Unsupported uncompressed bitmap depth: {bpp} bpp"); + update.rectangle.clone() + } + } + } + }; + + match update_kind { + UpdateKind::Region(current) => update_kind = UpdateKind::Region(current.union(&update_rectangle)), + _ => update_kind = UpdateKind::Region(update_rectangle), + } + } + + Ok(vec![update_kind]) + } + + /// Process a pointer update, shared between fast-path and slow-path pipelines. + pub fn process_pointer_update( + &mut self, + image: &mut DecodedImage, + update: PointerUpdateData<'_>, + ) -> SessionResult> { + let mut processor_updates = Vec::new(); + + if !self.enable_server_pointer { + return Ok(processor_updates); + } + + let bitmap_target = if self.pointer_software_rendering { + PointerBitmapTarget::Software + } else { + PointerBitmapTarget::Accelerated + }; + + match update { + PointerUpdateData::SetHidden => { + processor_updates.push(UpdateKind::PointerHidden); + if self.pointer_software_rendering && !self.use_system_pointer { + self.use_system_pointer = true; + if let Some(rect) = image.hide_pointer()? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + } + PointerUpdateData::SetDefault => { + processor_updates.push(UpdateKind::PointerDefault); + if self.pointer_software_rendering && !self.use_system_pointer { + self.use_system_pointer = true; + if let Some(rect) = image.hide_pointer()? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + } + PointerUpdateData::SetPosition(position) => { + if self.use_system_pointer || !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerPosition { + x: position.x, + y: position.y, + }); + } else if let Some(rect) = image.move_pointer(position.x, position.y)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + PointerUpdateData::Color(pointer) => { + let cache_index = pointer.cache_index; + + let decoded_pointer = Arc::new( + DecodedPointer::decode_color_pointer_attribute(&pointer, bitmap_target) + .map_err(|e| SessionError::custom("failed to decode color pointer attribute", e))?, + ); + + let _ = self + .pointer_cache + .insert(usize::from(cache_index), Arc::clone(&decoded_pointer)); + + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer))); + } else if let Some(rect) = image.update_pointer(decoded_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + PointerUpdateData::Cached(cached) => { + let cache_index = cached.cache_index; + + if let Some(cached_pointer) = self.pointer_cache.get(usize::from(cache_index)) { + // Disable system pointer + processor_updates.push(UpdateKind::PointerHidden); + self.use_system_pointer = false; + // Send graphics update + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&cached_pointer))); + } else if let Some(rect) = image.update_pointer(cached_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } else { + // In case pointer was hidden previously + if let Some(rect) = image.show_pointer()? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + } else { + warn!("Cached pointer not found {}", cache_index); + } + } + PointerUpdateData::New(pointer) => { + let cache_index = pointer.color_pointer.cache_index; + + let decoded_pointer = Arc::new( + DecodedPointer::decode_pointer_attribute(&pointer, bitmap_target) + .map_err(|e| SessionError::custom("failed to decode pointer attribute", e))?, + ); + + let _ = self + .pointer_cache + .insert(usize::from(cache_index), Arc::clone(&decoded_pointer)); + + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer))); + } else if let Some(rect) = image.update_pointer(decoded_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + PointerUpdateData::Large(pointer) => { + let cache_index = pointer.cache_index; + + let decoded_pointer: Arc = Arc::new( + DecodedPointer::decode_large_pointer_attribute(&pointer, bitmap_target) + .map_err(|e| SessionError::custom("failed to decode large pointer attribute", e))?, + ); + + let _ = self + .pointer_cache + .insert(usize::from(cache_index), Arc::clone(&decoded_pointer)); + + if !self.pointer_software_rendering { + processor_updates.push(UpdateKind::PointerBitmap(Arc::clone(&decoded_pointer))); + } else if let Some(rect) = image.update_pointer(decoded_pointer)? { + processor_updates.push(UpdateKind::Region(rect)); + } + } + }; + + Ok(processor_updates) + } + + fn process_surface_commands( + &mut self, + image: &mut DecodedImage, + output: &mut WriteBuf, + surface_commands: Vec>, + ) -> SessionResult { + let mut update_rectangle = None; + + for command in surface_commands { + match command { + SurfaceCommand::SetSurfaceBits(bits) | SurfaceCommand::StreamSurfaceBits(bits) => { + let codec_id = CodecId::from_u8(bits.extended_bitmap_data.codec_id).ok_or_else(|| { + reason_err!( + "Fast-Path", + "unexpected codec ID: {:x}", + bits.extended_bitmap_data.codec_id + ) + })?; + + trace!(?codec_id, "Surface bits"); + + let destination = bits.destination; + // TODO(@pacmancoder): Correct rectangle conversion logic should + // be revisited when `rectangle_processing.rs` from + // `ironrdp-graphics` will be refactored to use generic `Rectangle` + // trait instead of hardcoded `InclusiveRectangle`. + let destination = InclusiveRectangle { + left: destination.left, + top: destination.top, + right: destination.right - 1, + bottom: destination.bottom - 1, + }; + match codec_id { + CODEC_ID_NONE => { + let ext_data = bits.extended_bitmap_data; + let source_width = destination.width(); + let rectangle = match ext_data.bpp { + 8 => image.apply_rgb8_with_palette( + ext_data.data, + &destination, + self.palette.colors(), + source_width, + )?, + 15 => image.apply_rgb15_bitmap(ext_data.data, &destination, source_width)?, + 16 => image.apply_rgb16_bitmap(ext_data.data, &destination, source_width)?, + 24 => image.apply_bgr24_bitmap(ext_data.data, &destination, source_width)?, + 32 => image.apply_rgb32_bitmap( + ext_data.data, + PixelFormat::BgrX32, + &destination, + source_width, + )?, + bpp => { + warn!("Unsupported surface CODEC_ID_NONE bpp: {bpp}"); + continue; + } + }; + update_rectangle = update_rectangle + .map(|rect: InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + CODEC_ID_REMOTEFX => { + let mut data = ReadCursor::new(bits.extended_bitmap_data.data); + while !data.is_empty() { + let (_frame_id, rectangle) = self.rfx_handler.decode(image, &destination, &mut data)?; + update_rectangle = update_rectangle + .map(|rect: InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + } + } + #[cfg(feature = "qoi")] + ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOI => { + qoi_apply( + image, + destination, + bits.extended_bitmap_data.data, + &mut update_rectangle, + )?; + } + #[cfg(feature = "qoiz")] + ironrdp_pdu::rdp::capability_sets::CODEC_ID_QOIZ => { + let compressed = &bits.extended_bitmap_data.data; + let mut input = zstd_safe::InBuffer::around(compressed); + let mut data = vec![0; compressed.len() * 4]; + let mut pos = 0; + loop { + let mut output = zstd_safe::OutBuffer::around_pos(data.as_mut_slice(), pos); + self.zdctx + .decompress_stream(&mut output, &mut input) + .map_err(zstd_safe::get_error_name) + .map_err(|e| reason_err!("zstd", "{}", e))?; + pos = output.pos(); + if pos == output.capacity() { + data.resize(data.capacity() * 2, 0); + } else { + break; + } + } + + qoi_apply(image, destination, &data, &mut update_rectangle)?; + } + _ => { + warn!("Unsupported codec ID: {}", bits.extended_bitmap_data.codec_id); + } + } + } + SurfaceCommand::FrameMarker(marker) => { + trace!( + "Frame marker: action {:?} with ID #{}", + marker.frame_action, + marker.frame_id.unwrap_or(0) + ); + self.marker_processor.process(&marker, output)?; + } + } + } + + Ok(update_rectangle.unwrap_or_else(InclusiveRectangle::empty)) + } +} + +#[cfg(feature = "qoi")] +fn qoi_apply( + image: &mut DecodedImage, + destination: InclusiveRectangle, + data: &[u8], + update_rectangle: &mut Option, +) -> SessionResult<()> { + let (header, decoded) = qoi::decode_to_vec(data).map_err(|e| reason_err!("QOI decode", "{}", e))?; + + // Guard against a decoded buffer that doesn't match the destination + // rectangle. `apply_rgb24`/`apply_rgba32` derive the row count from the + // decoded length, and the only bounds check downstream (`rect_fits`) + // validates the rectangle against the image, not the buffer against the + // rectangle. A malformed/oversized QOI payload would otherwise drive the + // per-row index past `self.data` and panic (client-side DoS). + let channels = match header.channels { + qoi::Channels::Rgb => 3, + qoi::Channels::Rgba => 4, + }; + let expected = usize::from(destination.width()) * usize::from(destination.height()) * channels; + if decoded.len() != expected { + return Err(reason_err!( + "QOI decode", + "decoded {} bytes, expected {} for {}x{} ({} channels)", + decoded.len(), + expected, + destination.width(), + destination.height(), + channels + )); + } + + let rectangle = match header.channels { + qoi::Channels::Rgb => image.apply_rgb24(&decoded, &destination, destination.width(), false)?, + qoi::Channels::Rgba => image.apply_rgba32(&decoded, &destination, false)?, + }; + + *update_rectangle = update_rectangle + .as_ref() + .map(|rect: &InclusiveRectangle| rect.union(&rectangle)) + .or(Some(rectangle)); + Ok(()) +} + +pub struct ProcessorBuilder { + pub io_channel_id: u16, + pub user_channel_id: u16, + pub share_id: u32, + /// Ignore server pointer updates. + pub enable_server_pointer: bool, + /// Use software rendering mode for pointer bitmap generation. When this option is active, + /// `UpdateKind::PointerBitmap` will not be generated. Remote pointer will be drawn + /// via software rendering on top of the output image. + pub pointer_software_rendering: bool, + /// Bulk decompressor for server-to-client compressed PDUs. + /// `None` when compression was not negotiated. + pub bulk_decompressor: Option, +} + +impl ProcessorBuilder { + pub fn build(self) -> Processor { + Processor { + complete_data: CompleteData::new(), + rfx_handler: rfx::DecodingContext::new(), + marker_processor: FrameMarkerProcessor::new(self.user_channel_id, self.io_channel_id, self.share_id), + bitmap_stream_decoder: BitmapStreamDecoder::default(), + pointer_cache: PointerCache::default(), + use_system_pointer: true, + mouse_pos_update: None, + enable_server_pointer: self.enable_server_pointer, + pointer_software_rendering: self.pointer_software_rendering, + bulk_decompressor: self.bulk_decompressor, + palette: Palette::system_default(), + #[cfg(feature = "qoiz")] + zdctx: zstd_safe::DCtx::default(), + } + } +} + +#[derive(Debug, PartialEq)] +struct CompleteData { + fragmented_data: Option>, +} + +impl CompleteData { + fn new() -> Self { + Self { fragmented_data: None } + } + + fn process_data(&mut self, data: &[u8], fragmentation: Fragmentation) -> Option> { + match fragmentation { + Fragmentation::Single => { + self.check_data_is_empty(); + + Some(data.to_vec()) + } + Fragmentation::First => { + self.check_data_is_empty(); + + self.fragmented_data = Some(data.to_vec()); + + None + } + Fragmentation::Next => { + self.append_data(data); + + None + } + Fragmentation::Last => { + self.append_data(data); + + self.fragmented_data.take() + } + } + } + + fn check_data_is_empty(&mut self) { + if self.fragmented_data.is_some() { + warn!("Skipping pending Fast-Path Update internal multiple elements data"); + self.fragmented_data = None; + } + } + + fn append_data(&mut self, data: &[u8]) { + if let Some(fragmented_data) = self.fragmented_data.as_mut() { + fragmented_data.extend_from_slice(data); + } else { + warn!("Got unexpected Next fragmentation PDU without prior First fragmentation PDU"); + } + } +} + +struct FrameMarkerProcessor { + user_channel_id: u16, + io_channel_id: u16, + share_id: u32, +} + +impl FrameMarkerProcessor { + fn new(user_channel_id: u16, io_channel_id: u16, share_id: u32) -> Self { + Self { + user_channel_id, + io_channel_id, + share_id, + } + } + + fn process(&mut self, marker: &FrameMarkerPdu, output: &mut WriteBuf) -> SessionResult<()> { + match marker.frame_action { + FrameAction::Begin => Ok(()), + FrameAction::End => { + ironrdp_pdu::rdp::headers::encode_share_data( + self.user_channel_id, + self.io_channel_id, + self.share_id, + ShareDataPdu::FrameAcknowledge(FrameAcknowledgePdu { + frame_id: marker.frame_id.unwrap_or(0), + }), + output, + ) + .map_err(SessionError::encode)?; + + Ok(()) + } + } + } +} diff --git a/rust/vendor/ironrdp-session/src/image.rs b/rust/vendor/ironrdp-session/src/image.rs new file mode 100644 index 0000000..dc6349a --- /dev/null +++ b/rust/vendor/ironrdp-session/src/image.rs @@ -0,0 +1,1038 @@ +use std::sync::Arc; + +use ironrdp_core::assert_impl; +use ironrdp_graphics::color_conversion::{rdp_15bit_to_rgb, rdp_16bit_to_rgb}; +use ironrdp_graphics::image_processing::{ImageRegion, ImageRegionMut, PixelFormat}; +use ironrdp_graphics::pointer::DecodedPointer; +use ironrdp_graphics::rectangle_processing::Region; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use tracing::{debug, trace}; + +use crate::{SessionResult, custom_err}; + +const TILE_SIZE: u16 = 64; + +pub struct DecodedImage { + pixel_format: PixelFormat, + data: Vec, + + /// Part of the pointer image which should be drawn + pointer_src_rect: InclusiveRectangle, + /// X position of the pointer sprite on the screen + pointer_draw_x: u16, + /// Y position of the pointer sprite on the screen + pointer_draw_y: u16, + + pointer_x: u16, + pointer_y: u16, + + pointer: Option>, + /// Image data, overridden by pointer. Used to restore image after pointer was hidden or moved + pointer_backbuffer: Vec, + /// Whether to show pointer or not + show_pointer: bool, + /// Whether pointer is visible on the screen or its sprite is currently out of bounds + pointer_visible_on_screen: bool, + + width: u16, + height: u16, +} + +assert_impl!(DecodedImage: Send); + +impl core::fmt::Debug for DecodedImage { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("DecodedImage") + .field("pixel_format", &self.pixel_format) + .field("data_len", &self.data.len()) + .field("pointer_src_rect", &self.pointer_src_rect) + .field("pointer_draw_x", &self.pointer_draw_x) + .field("pointer_draw_y", &self.pointer_draw_y) + .field("pointer_x", &self.pointer_x) + .field("pointer_y", &self.pointer_y) + .field("pointer", &self.pointer) + .field("pointer_backbuffer", &self.pointer_backbuffer) + .field("show_pointer", &self.show_pointer) + .field("pointer_visible_on_screen", &self.pointer_visible_on_screen) + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} + +#[derive(PartialEq, Eq)] +enum PointerLayer { + Background, + Pointer, +} + +struct PointerRenderingState { + redraw: bool, + update_rectangle: InclusiveRectangle, +} + +#[expect(clippy::too_many_arguments)] +fn copy_cursor_data( + from: &[u8], + from_pos: (usize, usize), + from_stride: usize, + to: &mut [u8], + to_stride: usize, + to_pos: (usize, usize), + size: (usize, usize), + dst_size: (usize, usize), + composite: bool, +) { + const PIXEL_SIZE: usize = 4; + + if to_pos.0 + size.0 > dst_size.0 || to_pos.1 + size.1 > dst_size.1 { + // Perform clipping + return; + } + + let (from_x, from_y) = from_pos; + let (to_x, to_y) = to_pos; + let (width, height) = size; + + for y in 0..height { + let from_start = (from_y + y) * from_stride + from_x * PIXEL_SIZE; + let to_start = (to_y + y) * to_stride + to_x * PIXEL_SIZE; + + if composite { + for pixel in 0..width { + let dest_r = to[to_start + pixel * PIXEL_SIZE]; + let dest_g = to[to_start + pixel * PIXEL_SIZE + 1]; + let dest_b = to[to_start + pixel * PIXEL_SIZE + 2]; + + let src_r = from[from_start + pixel * PIXEL_SIZE]; + let src_g = from[from_start + pixel * PIXEL_SIZE + 1]; + let src_b = from[from_start + pixel * PIXEL_SIZE + 2]; + let src_a = from[from_start + pixel * PIXEL_SIZE + 3]; + + // Inverted pixel, this color has a special meaning when encoded by ironrdp-graphics + if src_a == 0 && src_r == 255 && src_g == 255 && src_b == 255 { + to[to_start + pixel * PIXEL_SIZE] = 255 - dest_r; + to[to_start + pixel * PIXEL_SIZE + 1] = 255 - dest_g; + to[to_start + pixel * PIXEL_SIZE + 2] = 255 - dest_b; + to[to_start + pixel * PIXEL_SIZE + 3] = 255; + continue; + } + + // Skip 100% transparent pixels + if src_a == 0 { + continue; + } + + #[expect(clippy::as_conversions, reason = "(u16 >> 8) fits into u8 + hot loop")] + { + // Integer alpha blending, source represented as premultiplied alpha color, calculation in floating point + to[to_start + pixel * PIXEL_SIZE] = + src_r + ((u16::from(dest_r) * u16::from(255 - src_a)) >> 8) as u8; + to[to_start + pixel * PIXEL_SIZE + 1] = + src_g + ((u16::from(dest_g) * u16::from(255 - src_a)) >> 8) as u8; + to[to_start + pixel * PIXEL_SIZE + 2] = + src_b + ((u16::from(dest_b) * u16::from(255 - src_a)) >> 8) as u8; + // Framebuffer is always opaque, so we can skip alpha channel change + } + } + } else { + to[to_start..to_start + width * PIXEL_SIZE] + .copy_from_slice(&from[from_start..from_start + width * PIXEL_SIZE]); + } + } +} + +impl DecodedImage { + pub fn new(pixel_format: PixelFormat, width: u16, height: u16) -> Self { + let len = usize::from(width) * usize::from(height) * usize::from(pixel_format.bytes_per_pixel()); + + Self { + pixel_format, + data: vec![0; len], + width, + height, + + pointer_src_rect: InclusiveRectangle { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + pointer_x: 0, + pointer_y: 0, + pointer_draw_x: 0, + pointer_draw_y: 0, + pointer_backbuffer: Vec::new(), + pointer: None, + show_pointer: false, + pointer_visible_on_screen: true, + } + } + + pub fn pixel_format(&self) -> PixelFormat { + self.pixel_format + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn width(&self) -> u16 { + self.width + } + + pub fn bytes_per_pixel(&self) -> usize { + usize::from(self.pixel_format.bytes_per_pixel()) + } + + pub fn stride(&self) -> usize { + usize::from(self.width) * self.bytes_per_pixel() + } + + pub fn data_for_rect(&self, rect: &InclusiveRectangle) -> &[u8] { + let start = usize::from(rect.left) * self.bytes_per_pixel() + usize::from(rect.top) * self.stride(); + let end = + start + usize::from(rect.height() - 1) * self.stride() + usize::from(rect.width()) * self.bytes_per_pixel(); + &self.data[start..end] + } + + pub fn height(&self) -> u16 { + self.height + } + + /// Returns `true` if the rectangle fits entirely within the image bounds. + fn rect_fits(&self, rect: &InclusiveRectangle) -> bool { + rect.right < self.width && rect.bottom < self.height + } + + fn apply_pointer_layer(&mut self, layer: PointerLayer) -> SessionResult> { + // Pointer is not hidden, but its texture is not visible on the screen, so we don't + // need to render it + if layer == PointerLayer::Pointer && !self.pointer_visible_on_screen { + return Ok(None); + } + + if self.data.is_empty() { + return Ok(None); + } + + let pointer = if let Some(pointer) = &self.pointer { + pointer + } else { + return Ok(None); + }; + + if self.pointer_src_rect.width() == 0 || self.pointer_src_rect.height() == 0 { + return Ok(None); + } + + let dest_rect = InclusiveRectangle { + left: self.pointer_draw_x, + top: self.pointer_draw_y, + right: self.pointer_draw_x + self.pointer_src_rect.width() - 1, + bottom: self.pointer_draw_y + self.pointer_src_rect.height() - 1, + }; + + if dest_rect.width() == 0 || dest_rect.height() == 0 { + return Ok(None); + } + + let pointer_src_rect_width = usize::from(self.pointer_src_rect.width()); + let pointer_src_rect_height = usize::from(self.pointer_src_rect.height()); + let pointer_draw_x = usize::from(self.pointer_draw_x); + let pointer_draw_y = usize::from(self.pointer_draw_y); + let width = usize::from(self.width); + let height = usize::from(self.height); + + match &layer { + PointerLayer::Background => { + if self.pointer_backbuffer.is_empty() { + // Backbuffer were previously empty + return Ok(None); + } + + copy_cursor_data( + &self.pointer_backbuffer, + (0, 0), + pointer_src_rect_width * 4, + &mut self.data, + width * 4, + (pointer_draw_x, pointer_draw_y), + (pointer_src_rect_width, pointer_src_rect_height), + (width, height), + false, + ); + } + PointerLayer::Pointer => { + // Copy current background to backbuffer + let buffer_size = self + .pointer_backbuffer + .len() + .max(pointer_src_rect_width * pointer_src_rect_height * 4); + self.pointer_backbuffer.resize(buffer_size, 0); + + copy_cursor_data( + &self.data, + (pointer_draw_x, pointer_draw_y), + width * 4, + &mut self.pointer_backbuffer, + pointer_src_rect_width * 4, + (0, 0), + (pointer_src_rect_width, pointer_src_rect_height), + (width, height), + false, + ); + + // Draw pointer (with compositing) + copy_cursor_data( + pointer.bitmap_data.as_slice(), + ( + usize::from(self.pointer_src_rect.left), + usize::from(self.pointer_src_rect.top), + ), + usize::from(pointer.width) * 4, + &mut self.data, + width * 4, + (pointer_draw_x, pointer_draw_y), + (pointer_src_rect_width, pointer_src_rect_height), + (width, height), + true, + ); + } + } + + // Request redraw of the changed area + Ok(Some(dest_rect)) + } + + pub(crate) fn show_pointer(&mut self) -> SessionResult> { + if !self.show_pointer { + self.show_pointer = true; + self.apply_pointer_layer(PointerLayer::Pointer) + } else { + Ok(None) + } + } + + pub(crate) fn hide_pointer(&mut self) -> SessionResult> { + if self.show_pointer { + self.show_pointer = false; + self.apply_pointer_layer(PointerLayer::Background) + } else { + Ok(None) + } + } + + fn recalculate_pointer_geometry(&mut self) { + let x = self.pointer_x; + let y = self.pointer_y; + + let pointer = match &self.pointer { + Some(pointer) if self.show_pointer => pointer, + _ => return, + }; + + let left_virtual = i32::from(x) - i32::from(pointer.hotspot_x); + let top_virtual = i32::from(y) - i32::from(pointer.hotspot_y); + let right_virtual = left_virtual + i32::from(pointer.width) - 1; + let bottom_virtual = top_virtual + i32::from(pointer.height) - 1; + + let (left, draw_x) = if left_virtual < 0 { + // Cut left side if required + (pointer.hotspot_x - x, 0) + } else { + (0, x - pointer.hotspot_x) + }; + + let (top, draw_y) = if top_virtual < 0 { + // Cut top side if required + (pointer.hotspot_y - y, 0) + } else { + (0, y - pointer.hotspot_y) + }; + + // Cut right side if required + let right = if right_virtual >= i32::from(self.width - 1) { + if draw_x + 1 >= self.width { + // Pointer is completely out of bounds horizontally + self.pointer_visible_on_screen = false; + return; + } else { + self.width - (draw_x + 1) + } + } else { + pointer.width - 1 + }; + + // Cut bottom side if required + let bottom = if bottom_virtual >= i32::from(self.height - 1) { + if (draw_y + 1) >= self.height { + // Pointer is completely out of bounds vertically + self.pointer_visible_on_screen = false; + return; + } else { + self.height - (draw_y + 1) + } + } else { + pointer.height - 1 + }; + + self.pointer_visible_on_screen = true; + + let pointer_src_rect = InclusiveRectangle { + left, + top, + right, + bottom, + }; + + self.pointer_src_rect = pointer_src_rect; + self.pointer_draw_x = draw_x; + self.pointer_draw_y = draw_y; + } + + pub(crate) fn move_pointer(&mut self, x: u16, y: u16) -> SessionResult> { + self.pointer_x = x; + self.pointer_y = y; + + if self.pointer.is_some() && self.show_pointer { + let old_rect = self.apply_pointer_layer(PointerLayer::Background)?; + self.recalculate_pointer_geometry(); + let new_rect = self.apply_pointer_layer(PointerLayer::Pointer)?; + + match (old_rect, new_rect) { + (None, None) => Ok(None), + (None, Some(rect)) => Ok(Some(rect)), + (Some(rect), None) => Ok(Some(rect)), + (Some(a), Some(b)) => Ok(Some(a.union(&b))), + } + } else { + Ok(None) + } + } + + pub(crate) fn update_pointer(&mut self, pointer: Arc) -> SessionResult> { + self.show_pointer = true; + + // Remove old pointer from frame buffer + let old_rect = if self.pointer.is_some() { + self.apply_pointer_layer(PointerLayer::Background)? + } else { + None + }; + + self.pointer = Some(pointer); + self.recalculate_pointer_geometry(); + + // Draw new pointer + let new_rect = self.apply_pointer_layer(PointerLayer::Pointer)?; + + match (old_rect, new_rect) { + (None, None) => Ok(None), + (None, Some(rect)) => Ok(Some(rect)), + (Some(rect), None) => Ok(Some(rect)), + (Some(a), Some(b)) => Ok(Some(a.union(&b))), + } + } + + fn is_pointer_redraw_required(&self, update_rectangle: &InclusiveRectangle) -> bool { + let pointer_dest_rect = InclusiveRectangle { + left: self.pointer_draw_x, + top: self.pointer_draw_y, + right: self.pointer_draw_x + self.pointer_src_rect.width() - 1, + bottom: self.pointer_draw_y + self.pointer_src_rect.height() - 1, + }; + + update_rectangle.intersect(&pointer_dest_rect).is_some() && self.show_pointer + } + + /// This method should be called BEFORE and framebuffer updates, with the update rectangle, + /// to determine if the pointer needs to be redrawn (overlapping with the update rectangle). + fn pointer_rendering_begin( + &mut self, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + if !self.is_pointer_redraw_required(update_rectangle) || self.pointer.is_none() { + return Ok(PointerRenderingState { + redraw: false, + update_rectangle: update_rectangle.clone(), + }); + } + + let state = self + .apply_pointer_layer(PointerLayer::Background)? + .map(|cursor_erase_rect| PointerRenderingState { + redraw: true, + update_rectangle: cursor_erase_rect.union(update_rectangle), + }) + .unwrap_or_else(|| PointerRenderingState { + redraw: false, + update_rectangle: update_rectangle.clone(), + }); + + Ok(state) + } + + fn pointer_rendering_end( + &mut self, + pointer_rendering_state: PointerRenderingState, + ) -> SessionResult { + if !pointer_rendering_state.redraw { + return Ok(pointer_rendering_state.update_rectangle); + } + + let update_rectangle = self + .apply_pointer_layer(PointerLayer::Pointer)? + .map(|pointer_draw_rectangle| pointer_draw_rectangle.union(&pointer_rendering_state.update_rectangle)) + .unwrap_or_else(|| pointer_rendering_state.update_rectangle); + + Ok(update_rectangle) + } + + // To apply the buffer, we need to un-apply previously drawn cursor, and then apply it again + // in other position. + + pub(crate) fn apply_tile( + &mut self, + tile_output: &[u8], + pixel_format: PixelFormat, + clipping_rectangles: &Region, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult { + trace!("Tile: {:?}", update_rectangle); + + if !self.rect_fits(&clipping_rectangles.extents) { + debug!( + "Skipping tile update {:?} outside image bounds {}x{}", + clipping_rectangles.extents, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + let pointer_rendering_state = self.pointer_rendering_begin(&clipping_rectangles.extents)?; + + let update_region = clipping_rectangles.intersect_rectangle(update_rectangle); + for region_rectangle in &update_region.rectangles { + let source_x = region_rectangle.left - update_rectangle.left; + let source_y = region_rectangle.top - update_rectangle.top; + let stride = u16::from(pixel_format.bytes_per_pixel()) * TILE_SIZE; + let source_image_region = ImageRegion { + region: InclusiveRectangle { + left: source_x, + top: source_y, + right: source_x + region_rectangle.width() - 1, + bottom: source_y + region_rectangle.height() - 1, + }, + data: tile_output, + step: stride, + pixel_format, + }; + + let mut destination_image_region = ImageRegionMut { + region: region_rectangle.clone(), + step: self.width() * u16::from(self.pixel_format.bytes_per_pixel()), + pixel_format: self.pixel_format, + data: &mut self.data, + }; + + trace!("Source image region: {:?}", source_image_region.region); + trace!("Destination image region: {:?}", destination_image_region.region); + + source_image_region + .copy_to(&mut destination_image_region) + .map_err(|e| custom_err!("copy_to", e))?; + } + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + pub(crate) fn apply_rgb16_bitmap( + &mut self, + rgb16: &[u8], + update_rectangle: &InclusiveRectangle, + source_width: u16, + ) -> SessionResult { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping rgb16 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const SRC_COLOR_DEPTH: usize = 2; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let source_width = usize::from(source_width); + let rectangle_height = usize::from(update_rectangle.height()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + rgb16 + .chunks_exact(source_width * SRC_COLOR_DEPTH) + .rev() + .take(rectangle_height) + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .take(rectangle_width) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let rgb16_value = u16::from_le_bytes( + src_pixel + .try_into() + .expect("src_pixel contains exactly two u8 elements"), + ); + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + let [r, g, b] = rdp_16bit_to_rgb(rgb16_value); + self.data[dst_idx + ri] = r; + self.data[dst_idx + gi] = g; + self.data[dst_idx + bi] = b; + self.data[dst_idx + ai] = 0xff; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + /// Apply a 15-bit (RGB555) bitmap. Bottom-up row order, 2 bytes per pixel. + pub(crate) fn apply_rgb15_bitmap( + &mut self, + rgb15: &[u8], + update_rectangle: &InclusiveRectangle, + source_width: u16, + ) -> SessionResult { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping rgb15 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const SRC_COLOR_DEPTH: usize = 2; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let source_width = usize::from(source_width); + let rectangle_height = usize::from(update_rectangle.height()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + rgb15 + .chunks_exact(source_width * SRC_COLOR_DEPTH) + .rev() + .take(rectangle_height) + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .take(rectangle_width) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let rgb15_value = u16::from_le_bytes( + src_pixel + .try_into() + .expect("src_pixel contains exactly two u8 elements"), + ); + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + let [r, g, b] = rdp_15bit_to_rgb(rgb15_value); + self.data[dst_idx + ri] = r; + self.data[dst_idx + gi] = g; + self.data[dst_idx + bi] = b; + self.data[dst_idx + ai] = 0xff; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + /// Apply a 24-bit BGR bitmap. RLE 24bpp decompresses to BGR byte order, + /// and uncompressed 24bpp bitmaps are also BGR per MS-RDPBCGR. + /// Bottom-up row order, 3 bytes per pixel. + pub(crate) fn apply_bgr24_bitmap( + &mut self, + bgr24: &[u8], + update_rectangle: &InclusiveRectangle, + source_width: u16, + ) -> SessionResult { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping bgr24 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const SRC_COLOR_DEPTH: usize = 3; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let source_width = usize::from(source_width); + let rectangle_height = usize::from(update_rectangle.height()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + bgr24 + .chunks_exact(source_width * SRC_COLOR_DEPTH) + .rev() + .take(rectangle_height) + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .take(rectangle_width) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + // BGR -> RGB channel swap + self.data[dst_idx + ri] = src_pixel[2]; + self.data[dst_idx + gi] = src_pixel[1]; + self.data[dst_idx + bi] = src_pixel[0]; + self.data[dst_idx + ai] = 0xff; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + /// Apply an 8-bit palette-indexed bitmap. Each source byte is a palette index. + /// Bottom-up row order. + pub(crate) fn apply_rgb8_with_palette( + &mut self, + indexed: &[u8], + update_rectangle: &InclusiveRectangle, + palette: &[[u8; 3]; 256], + source_width: u16, + ) -> SessionResult { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping rgb8 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let source_width = usize::from(source_width); + let rectangle_height = usize::from(update_rectangle.height()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + indexed + .chunks_exact(source_width) + .rev() + .take(rectangle_height) + .enumerate() + .for_each(|(row_idx, row)| { + row.iter() + .take(rectangle_width) + .enumerate() + .for_each(|(col_idx, &index)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + let [r, g, b] = palette[usize::from(index)]; + self.data[dst_idx + ri] = r; + self.data[dst_idx + gi] = g; + self.data[dst_idx + bi] = b; + self.data[dst_idx + ai] = 0xff; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + fn apply_rgb24_iter<'a, I>( + &mut self, + rgb24: I, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult + where + I: Iterator, + { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping rgb24 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const SRC_COLOR_DEPTH: usize = 3; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + rgb24.enumerate().for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .take(usize::from(update_rectangle.width())) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + self.data[dst_idx + ri] = src_pixel[0]; + self.data[dst_idx + gi] = src_pixel[1]; + self.data[dst_idx + bi] = src_pixel[2]; + self.data[dst_idx + ai] = 0xFF; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + pub(crate) fn apply_rgb24( + &mut self, + rgb24: &[u8], + update_rectangle: &InclusiveRectangle, + source_width: u16, + flip: bool, + ) -> SessionResult { + const SRC_COLOR_DEPTH: usize = 3; + let source_width = usize::from(source_width); + let rectangle_height = usize::from(update_rectangle.height()); + let lines = rgb24.chunks_exact(source_width * SRC_COLOR_DEPTH); + if flip { + self.apply_rgb24_iter(lines.rev().take(rectangle_height), update_rectangle) + } else { + self.apply_rgb24_iter(lines.take(rectangle_height), update_rectangle) + } + } + + #[cfg(feature = "qoi")] + fn apply_rgba32_iter<'a, I>( + &mut self, + rgba32: I, + update_rectangle: &InclusiveRectangle, + ) -> SessionResult + where + I: Iterator, + { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping rgba32 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const SRC_COLOR_DEPTH: usize = 4; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + rgba32.enumerate().for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + self.data[dst_idx + ri] = src_pixel[0]; + self.data[dst_idx + gi] = src_pixel[1]; + self.data[dst_idx + bi] = src_pixel[2]; + self.data[dst_idx + ai] = src_pixel[3]; + }) + }); + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } + + #[cfg(feature = "qoi")] + pub(crate) fn apply_rgba32( + &mut self, + rgba32: &[u8], + update_rectangle: &InclusiveRectangle, + flip: bool, + ) -> SessionResult { + const SRC_COLOR_DEPTH: usize = 4; + let rectangle_width = usize::from(update_rectangle.width()); + let lines = rgba32.chunks_exact(rectangle_width * SRC_COLOR_DEPTH); + if flip { + self.apply_rgba32_iter(lines.rev(), update_rectangle) + } else { + self.apply_rgba32_iter(lines, update_rectangle) + } + } + + pub(crate) fn apply_rgb32_bitmap( + &mut self, + rgb32: &[u8], + format: PixelFormat, + update_rectangle: &InclusiveRectangle, + source_width: u16, + ) -> SessionResult { + if !self.rect_fits(update_rectangle) { + debug!( + "Skipping rgb32 update {:?} outside image bounds {}x{}", + update_rectangle, self.width, self.height, + ); + return Ok(InclusiveRectangle::empty()); + } + + const SRC_COLOR_DEPTH: usize = 4; + const DST_COLOR_DEPTH: usize = 4; + + let image_width = usize::from(self.width); + let rectangle_width = usize::from(update_rectangle.width()); + let source_width = usize::from(source_width); + let rectangle_height = usize::from(update_rectangle.height()); + let top = usize::from(update_rectangle.top); + let left = usize::from(update_rectangle.left); + + let pointer_rendering_state = self.pointer_rendering_begin(update_rectangle)?; + + if format == self.pixel_format { + rgb32 + .chunks_exact(source_width * SRC_COLOR_DEPTH) + .rev() + .take(rectangle_height) + .enumerate() + .for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .take(rectangle_width) + .enumerate() + .for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + self.data[dst_idx..dst_idx + SRC_COLOR_DEPTH].copy_from_slice(src_pixel); + }) + }); + } else { + let [ri, gi, bi, ai] = self.pixel_format.channel_offsets(); + rgb32 + .chunks_exact(source_width * SRC_COLOR_DEPTH) + .rev() + .take(rectangle_height) + .enumerate() + .try_for_each(|(row_idx, row)| { + row.chunks_exact(SRC_COLOR_DEPTH) + .take(rectangle_width) + .enumerate() + .try_for_each(|(col_idx, src_pixel)| { + let dst_idx = ((top + row_idx) * image_width + left + col_idx) * DST_COLOR_DEPTH; + + let c = format + .read_color(src_pixel) + .map_err(|err| custom_err!("read color", err))?; + + self.data[dst_idx + ri] = c.r; + self.data[dst_idx + gi] = c.g; + self.data[dst_idx + bi] = c.b; + self.data[dst_idx + ai] = c.a; + + Ok(()) + })?; + + Ok(()) + })?; + } + + let update_rectangle = self.pointer_rendering_end(pointer_rendering_state)?; + + Ok(update_rectangle) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn pixel(image: &DecodedImage, x: usize, y: usize) -> [u8; 4] { + let offset = (y * usize::from(image.width()) + x) * 4; + image.data()[offset..offset + 4] + .try_into() + .expect("pixel has four channels") + } + + #[test] + fn bgr_bitmap_crops_source_stride_and_preserves_bottom_up_orientation() { + let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3); + let rectangle = InclusiveRectangle { + left: 1, + top: 0, + right: 2, + bottom: 1, + }; + + // The wire data is bottom-up, with two extra source columns per row. + let bgr = [ + 3, 2, 1, 6, 5, 4, 9, 8, 7, 12, 11, 10, // bottom row + 15, 14, 13, 18, 17, 16, 21, 20, 19, 24, 23, 22, // top row + ]; + + image.apply_bgr24_bitmap(&bgr, &rectangle, 4).unwrap(); + + assert_eq!(pixel(&image, 1, 0), [13, 14, 15, 255]); + assert_eq!(pixel(&image, 2, 0), [16, 17, 18, 255]); + assert_eq!(pixel(&image, 1, 1), [1, 2, 3, 255]); + assert_eq!(pixel(&image, 2, 1), [4, 5, 6, 255]); + assert_eq!(pixel(&image, 3, 0), [0, 0, 0, 0]); + } + + #[test] + fn rgb24_bitmap_crops_source_stride_and_preserves_top_down_orientation() { + let mut image = DecodedImage::new(PixelFormat::RgbA32, 4, 3); + let rectangle = InclusiveRectangle { + left: 1, + top: 0, + right: 2, + bottom: 1, + }; + + // RDP6 decoding produces row-major RGB data, so no row flip is required. + let rgb = [ + 1, 2, 3, 4, 5, 6, 90, 91, 92, 93, 94, 95, // top row + 13, 14, 15, 16, 17, 18, 96, 97, 98, 99, 100, 101, // bottom row + ]; + + image.apply_rgb24(&rgb, &rectangle, 4, false).unwrap(); + + assert_eq!(pixel(&image, 1, 0), [1, 2, 3, 255]); + assert_eq!(pixel(&image, 2, 0), [4, 5, 6, 255]); + assert_eq!(pixel(&image, 1, 1), [13, 14, 15, 255]); + assert_eq!(pixel(&image, 2, 1), [16, 17, 18, 255]); + assert_eq!(pixel(&image, 3, 1), [0, 0, 0, 0]); + } +} diff --git a/rust/vendor/ironrdp-session/src/lib.rs b/rust/vendor/ironrdp-session/src/lib.rs new file mode 100644 index 0000000..4d45fad --- /dev/null +++ b/rust/vendor/ironrdp-session/src/lib.rs @@ -0,0 +1,130 @@ +#![cfg_attr(doc, doc = include_str!("../README.md"))] +#![doc(html_logo_url = "https://cdnweb.devolutions.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg")] +#![allow(clippy::arithmetic_side_effects)] // FIXME: remove + +mod macros; + +pub mod fast_path; +pub mod image; +pub mod pointer; +pub mod rfx; // FIXME: maybe this module should not be in this crate +pub mod x224; + +mod active_stage; +mod palette; + +use core::fmt; + +pub use active_stage::{ActiveStage, ActiveStageBuilder, ActiveStageOutput, GracefulDisconnectReason}; + +pub type SessionResult = Result; + +#[non_exhaustive] +#[derive(Debug)] +pub enum SessionErrorKind { + Pdu(ironrdp_pdu::PduError), + Encode(ironrdp_core::EncodeError), + Decode(ironrdp_core::DecodeError), + Reason(String), + General, + Custom, +} + +impl fmt::Display for SessionErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self { + SessionErrorKind::Pdu(_) => write!(f, "PDU error"), + SessionErrorKind::Encode(_) => write!(f, "encode error"), + SessionErrorKind::Decode(_) => write!(f, "decode error"), + SessionErrorKind::Reason(description) => write!(f, "reason: {description}"), + SessionErrorKind::General => write!(f, "general error"), + SessionErrorKind::Custom => write!(f, "custom error"), + } + } +} + +impl core::error::Error for SessionErrorKind { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match &self { + SessionErrorKind::Pdu(e) => Some(e), + SessionErrorKind::Encode(e) => Some(e), + SessionErrorKind::Decode(e) => Some(e), + SessionErrorKind::Reason(_) => None, + SessionErrorKind::General => None, + SessionErrorKind::Custom => None, + } + } +} + +pub type SessionError = ironrdp_error::Error; + +pub trait SessionErrorExt { + fn pdu(error: ironrdp_pdu::PduError) -> Self; + fn encode(error: ironrdp_core::EncodeError) -> Self; + fn decode(error: ironrdp_core::DecodeError) -> Self; + fn general(context: &'static str) -> Self; + fn reason(context: &'static str, reason: impl Into) -> Self; + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl SessionErrorExt for SessionError { + #[track_caller] + fn pdu(error: ironrdp_pdu::PduError) -> Self { + Self::new("payload error", SessionErrorKind::Pdu(error)) + } + + #[track_caller] + fn encode(error: ironrdp_core::EncodeError) -> Self { + Self::new("encode error", SessionErrorKind::Encode(error)) + } + + #[track_caller] + fn decode(error: ironrdp_core::DecodeError) -> Self { + Self::new("decode error", SessionErrorKind::Decode(error)) + } + + #[track_caller] + fn general(context: &'static str) -> Self { + Self::new(context, SessionErrorKind::General) + } + + #[track_caller] + fn reason(context: &'static str, reason: impl Into) -> Self { + Self::new(context, SessionErrorKind::Reason(reason.into())) + } + + #[track_caller] + fn custom(context: &'static str, e: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + Self::new(context, SessionErrorKind::Custom).with_source(e) + } +} + +pub trait SessionResultExt { + #[must_use] + fn with_context(self, context: &'static str) -> Self; + #[must_use] + fn with_source(self, source: E) -> Self + where + E: core::error::Error + Sync + Send + 'static; +} + +impl SessionResultExt for SessionResult { + fn with_context(self, context: &'static str) -> Self { + self.map_err(|mut e| { + e.set_context(context); + e + }) + } + + fn with_source(self, source: E) -> Self + where + E: core::error::Error + Sync + Send + 'static, + { + self.map_err(|e| e.with_source(source)) + } +} diff --git a/rust/vendor/ironrdp-session/src/macros.rs b/rust/vendor/ironrdp-session/src/macros.rs new file mode 100644 index 0000000..5695913 --- /dev/null +++ b/rust/vendor/ironrdp-session/src/macros.rs @@ -0,0 +1,57 @@ +/// Creates a `SessionError` with `General` kind +/// +/// Shorthand for +/// ```ignore +/// ::general(context) +/// ``` +#[macro_export] +macro_rules! general_err { + ( $context:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::general($context) }}; +} + +/// Creates a `SessionError` with `Reason` kind +/// +/// Shorthand for +/// ```ignore +/// ::reason(context, reason) +/// ``` +#[macro_export] +macro_rules! reason_err { + ( $context:expr, $($arg:tt)* ) => {{ + <$crate::SessionError as $crate::SessionErrorExt>::reason($context, format!($($arg)*)) + }}; +} + +/// Creates a `SessionError` with `Custom` kind and a source error attached to it +/// +/// Shorthand for +/// ```ignore +/// ::custom(context, source) +/// ``` +#[macro_export] +macro_rules! custom_err { + ( $context:expr, $source:expr $(,)? ) => {{ <$crate::SessionError as $crate::SessionErrorExt>::custom($context, $source) }}; +} + +#[macro_export] +macro_rules! eof_try { + ($e:expr) => { + match $e { + Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => { + return Ok(None); + } + result => result, + } + }; +} + +#[macro_export] +macro_rules! try_ready { + ($e:expr) => { + match $e { + Ok(Some(v)) => Ok(v), + Ok(None) => return Ok(None), + Err(e) => Err(e), + } + }; +} diff --git a/rust/vendor/ironrdp-session/src/palette.rs b/rust/vendor/ironrdp-session/src/palette.rs new file mode 100644 index 0000000..6df8b66 --- /dev/null +++ b/rust/vendor/ironrdp-session/src/palette.rs @@ -0,0 +1,89 @@ +use tracing::{debug, warn}; + +/// 8bpp color palette (256 RGB entries). +/// +/// Initialized with the default Windows system palette (VGA colors) +/// per MS-RDPBCGR 2.2.9.1.1.3.1.1. Updated by TS_UPDATE_PALETTE_DATA +/// fast-path updates during the session. +#[derive(Debug, Clone)] +pub(crate) struct Palette { + colors: [[u8; 3]; 256], +} + +impl Palette { + /// Create a palette initialized with the 20 static colors from the + /// Windows default system palette. Indices 0-9 and 246-255 are the + /// reserved static colors; the middle 236 entries (10-245) are black. + /// + /// Reference: + pub(crate) fn system_default() -> Self { + let mut colors = [[0u8; 3]; 256]; + // Lower 10 static colors (indices 0-9) + colors[0] = [0, 0, 0]; // Black + colors[1] = [128, 0, 0]; // Dark Red + colors[2] = [0, 128, 0]; // Dark Green + colors[3] = [128, 128, 0]; // Dark Yellow + colors[4] = [0, 0, 128]; // Dark Blue + colors[5] = [128, 0, 128]; // Dark Magenta + colors[6] = [0, 128, 128]; // Dark Cyan + colors[7] = [192, 192, 192]; // Light Gray + colors[8] = [192, 220, 192]; // Money Green + colors[9] = [166, 202, 240]; // Sky Blue + // Upper 10 static colors (indices 246-255) + colors[246] = [255, 251, 240]; // Cream + colors[247] = [160, 160, 164]; // Medium Gray + colors[248] = [128, 128, 128]; // Dark Gray + colors[249] = [255, 0, 0]; // Red + colors[250] = [0, 255, 0]; // Green + colors[251] = [255, 255, 0]; // Yellow + colors[252] = [0, 0, 255]; // Blue + colors[253] = [255, 0, 255]; // Magenta + colors[254] = [0, 255, 255]; // Cyan + colors[255] = [255, 255, 255]; // White + Self { colors } + } + + /// Parse TS_UPDATE_PALETTE_DATA and update palette entries. + /// Wire format: pad(2) + numberColors(u32) + N x TS_COLOR_QUAD [B, G, R, pad]. + pub(crate) fn process_update(&mut self, data: &[u8]) { + if data.len() < 6 { + warn!("Palette update too short: {} bytes", data.len()); + return; + } + + let raw_count = u32::from_le_bytes([data[2], data[3], data[4], data[5]]); + // Palette can have at most 256 entries; clamp before any arithmetic + // to prevent overflow on untrusted input + let clamped = raw_count.min(256); + let number_colors = usize::try_from(clamped).unwrap_or(256); + let entry_data = &data[6..]; + + let Some(required_len) = number_colors.checked_mul(4) else { + warn!("Palette entry count overflow"); + return; + }; + + if entry_data.len() < required_len { + warn!( + "Palette data truncated: expected {} bytes for {} colors, got {}", + required_len, + number_colors, + entry_data.len() + ); + return; + } + + for i in 0..number_colors { + let offset = i * 4; + // TS_COLOR_QUAD: Blue, Green, Red, Pad + self.colors[i] = [entry_data[offset + 2], entry_data[offset + 1], entry_data[offset]]; + } + + debug!("Updated palette with {} colors", number_colors); + } + + /// Borrow the underlying color table for bitmap application. + pub(crate) fn colors(&self) -> &[[u8; 3]; 256] { + &self.colors + } +} diff --git a/rust/vendor/ironrdp-session/src/pointer.rs b/rust/vendor/ironrdp-session/src/pointer.rs new file mode 100644 index 0000000..192ae2f --- /dev/null +++ b/rust/vendor/ironrdp-session/src/pointer.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use ironrdp_graphics::pointer::DecodedPointer; + +#[derive(Debug, Clone, Default)] +pub struct PointerCache { + // TODO(@pacancoder) maybe use Vec> instead? + cache: HashMap>, +} + +impl PointerCache { + pub fn insert(&mut self, id: usize, pointer: Arc) -> Option> { + self.cache.insert(id, pointer) + } + + pub fn get(&self, id: usize) -> Option> { + self.cache.get(&id).cloned() + } + + pub fn is_cached(&self, id: usize) -> bool { + self.cache.contains_key(&id) + } +} diff --git a/rust/vendor/ironrdp-session/src/rfx.rs b/rust/vendor/ironrdp-session/src/rfx.rs new file mode 100644 index 0000000..6a5fcba --- /dev/null +++ b/rust/vendor/ironrdp-session/src/rfx.rs @@ -0,0 +1,286 @@ +use core::cmp::min; + +use ironrdp_graphics::color_conversion::{self, YCbCrBuffer}; +use ironrdp_graphics::image_processing::PixelFormat; +use ironrdp_graphics::rectangle_processing::Region; +use ironrdp_graphics::{dwt, quantization, rlgr, subband_reconstruction}; +use ironrdp_pdu::codecs::rfx::{self, EntropyAlgorithm, Quant, RfxRectangle, Tile}; +use ironrdp_pdu::geometry::{InclusiveRectangle, Rectangle as _}; +use ironrdp_pdu::{Decode as _, ReadCursor, decode_cursor}; +use tracing::{instrument, trace}; + +use crate::image::DecodedImage; +use crate::{SessionResult, custom_err, general_err, reason_err}; + +const TILE_SIZE: u16 = 64; + +pub type FrameId = u32; + +pub struct DecodingContext { + context: rfx::ContextPdu, + channels: rfx::ChannelsPdu, + decoding_tiles: DecodingTileContext, +} + +impl Default for DecodingContext { + fn default() -> Self { + Self { + context: rfx::ContextPdu { + flags: rfx::OperatingMode::empty(), + entropy_algorithm: EntropyAlgorithm::Rlgr1, + }, + channels: rfx::ChannelsPdu(Vec::new()), + decoding_tiles: DecodingTileContext::new(), + } + } +} + +impl DecodingContext { + pub fn new() -> Self { + Self::default() + } + + pub fn decode( + &mut self, + image: &mut DecodedImage, + destination: &InclusiveRectangle, + input: &mut ReadCursor<'_>, + ) -> SessionResult<(FrameId, InclusiveRectangle)> { + loop { + let block = rfx::Block::decode(input).map_err(|e| custom_err!("decode block", e))?; + + match block { + rfx::Block::Sync(_) => { + self.process_sync(input)?; + } + rfx::Block::CodecChannel(rfx::CodecChannel::FrameBegin(f)) => { + return self.process_frame(f, input, image, destination); + } + _ => { + return Err(reason_err!( + "rfx::DecodingContext", + "unexpected RFX block type: {:?}", + block.block_type() + )); + } + } + } + } + + fn process_sync(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> { + self.process_headers(input) + } + + fn process_headers(&mut self, input: &mut ReadCursor<'_>) -> SessionResult<()> { + let mut context = None; + let mut channels = None; + + // headers can appear in any order: CodecVersions, Channels, Context + for _ in 0..3 { + match decode_cursor(input).map_err(|e| custom_err!("decode headers", e))? { + rfx::Block::CodecChannel(rfx::CodecChannel::Context(c)) => context = Some(c), + rfx::Block::Channels(c) => channels = Some(c), + rfx::Block::CodecVersions(_) => (), + _ => { + return Err(general_err!("unexpected RFX block type")); + } + } + } + + let context = context.ok_or_else(|| general_err!("context header is missing"))?; + let channels = channels.ok_or_else(|| general_err!("channels header is missing"))?; + + if channels.0.is_empty() { + return Err(general_err!("no RFX channel announced")); + } + + self.context = context; + self.channels = channels; + + Ok(()) + } + + #[instrument(skip_all)] + fn process_frame( + &mut self, + frame_begin: rfx::FrameBeginPdu, + input: &mut ReadCursor<'_>, + image: &mut DecodedImage, + destination: &InclusiveRectangle, + ) -> SessionResult<(FrameId, InclusiveRectangle)> { + let channel = self + .channels + .0 + .first() + .ok_or_else(|| general_err!("no RFX channel found"))?; + let width = channel.width.try_into().map_err(|_| general_err!("invalid width"))?; + let height = channel.height.try_into().map_err(|_| general_err!("invalid height"))?; + let entropy_algorithm = self.context.entropy_algorithm; + + let region: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode region", e))?; + let mut region = match region { + rfx::Block::CodecChannel(rfx::CodecChannel::Region(region)) => region, + _ => return Err(general_err!("unexpected block type")), + }; + let tile_set: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode tile_set", e))?; + let tile_set = match tile_set { + rfx::Block::CodecChannel(rfx::CodecChannel::TileSet(t)) => t, + _ => return Err(general_err!("unexpected block type")), + }; + let frame_end: rfx::Block<'_> = decode_cursor(input).map_err(|e| custom_err!("decode frame_end", e))?; + if !matches!(frame_end, rfx::Block::CodecChannel(rfx::CodecChannel::FrameEnd(_))) { + return Err(general_err!("unexpected block type")); + } + + if region.rectangles.is_empty() { + region.rectangles = vec![RfxRectangle { + x: 0, + y: 0, + width, + height, + }]; + } + let region = region; + + trace!(frame_index = frame_begin.index); + trace!(destination_rectangle = ?destination); + trace!(context = ?self.context); + trace!(channels = ?self.channels); + trace!(?region); + + let clipping_rectangles = clipping_rectangles(region.rectangles.as_slice(), destination, width, height); + trace!("Clipping rectangles: {:?}", clipping_rectangles); + + let mut final_update_rectangle = clipping_rectangles.extents.clone(); + + for (update_rectangle, tile_data) in tiles_to_rectangles(tile_set.tiles.as_slice(), destination) + .zip(map_tiles_data(tile_set.tiles.as_slice(), tile_set.quants.as_slice())) + { + decode_tile( + &tile_data, + entropy_algorithm, + self.decoding_tiles.tile_output.as_mut(), + self.decoding_tiles.ycbcr_buffer.as_mut(), + self.decoding_tiles.ycbcr_temp_buffer.as_mut(), + )?; + + let current_update_rectangle = image.apply_tile( + &self.decoding_tiles.tile_output, + PixelFormat::RgbA32, + &clipping_rectangles, + &update_rectangle, + )?; + + final_update_rectangle = final_update_rectangle.union(¤t_update_rectangle); + } + + Ok((frame_begin.index, final_update_rectangle)) + } +} + +#[derive(Debug, Clone)] +struct DecodingTileContext { + tile_output: Vec, + ycbcr_buffer: Vec>, + ycbcr_temp_buffer: Vec, +} + +impl DecodingTileContext { + fn new() -> Self { + let tile_size = usize::from(TILE_SIZE); + Self { + tile_output: vec![0; tile_size * tile_size * 4], + ycbcr_buffer: vec![vec![0; tile_size * tile_size]; 3], + ycbcr_temp_buffer: vec![0; tile_size * tile_size], + } + } +} + +fn decode_tile( + tile: &TileData<'_>, + entropy_algorithm: EntropyAlgorithm, + output: &mut [u8], + ycbcr_temp: &mut [Vec], + temp: &mut [i16], +) -> SessionResult<()> { + for ((quant, data), ycbcr_buffer) in tile.quants.iter().zip(tile.data.iter()).zip(ycbcr_temp.iter_mut()) { + decode_component(quant, entropy_algorithm, data, ycbcr_buffer.as_mut_slice(), temp)?; + } + + let ycbcr_buffer = YCbCrBuffer { + y: ycbcr_temp[0].as_slice(), + cb: ycbcr_temp[1].as_slice(), + cr: ycbcr_temp[2].as_slice(), + }; + + color_conversion::ycbcr_to_rgba(ycbcr_buffer, output).map_err(|e| custom_err!("decode_tile", e))?; + + Ok(()) +} + +fn decode_component( + quant: &Quant, + entropy_algorithm: EntropyAlgorithm, + data: &[u8], + output: &mut [i16], + temp: &mut [i16], +) -> SessionResult<()> { + rlgr::decode(entropy_algorithm, data, output).map_err(|e| custom_err!("decode_component", e))?; + subband_reconstruction::decode(&mut output[4032..]); + quantization::decode(output, quant); + dwt::decode(output, temp); + + Ok(()) +} + +fn clipping_rectangles( + rectangles: &[RfxRectangle], + destination: &InclusiveRectangle, + width: u16, + height: u16, +) -> Region { + let mut clipping_rectangles = Region::new(); + + rectangles + .iter() + .map(|r| InclusiveRectangle { + left: min(destination.left + r.x, width - 1), + top: min(destination.top + r.y, height - 1), + right: min(destination.left + r.x + r.width - 1, width - 1), + bottom: min(destination.top + r.y + r.height - 1, height - 1), + }) + .for_each(|r| clipping_rectangles.union_rectangle(r)); + + clipping_rectangles +} + +fn tiles_to_rectangles<'a>( + tiles: &'a [Tile<'_>], + destination: &'a InclusiveRectangle, +) -> impl Iterator + 'a { + tiles.iter().map(|t| InclusiveRectangle { + left: destination.left + t.x * TILE_SIZE, + top: destination.top + t.y * TILE_SIZE, + right: destination.left + t.x * TILE_SIZE + TILE_SIZE - 1, + bottom: destination.top + t.y * TILE_SIZE + TILE_SIZE - 1, + }) +} + +fn map_tiles_data<'a>(tiles: &[Tile<'a>], quants: &[Quant]) -> Vec> { + tiles + .iter() + .map(|t| TileData { + quants: [ + quants[usize::from(t.y_quant_index)].clone(), + quants[usize::from(t.cb_quant_index)].clone(), + quants[usize::from(t.cr_quant_index)].clone(), + ], + data: [t.y_data, t.cb_data, t.cr_data], + }) + .collect() +} + +struct TileData<'a> { + quants: [Quant; 3], + data: [&'a [u8]; 3], +} diff --git a/rust/vendor/ironrdp-session/src/x224/mod.rs b/rust/vendor/ironrdp-session/src/x224/mod.rs new file mode 100644 index 0000000..b2a6927 --- /dev/null +++ b/rust/vendor/ironrdp-session/src/x224/mod.rs @@ -0,0 +1,290 @@ +use ironrdp_core::{WriteBuf, decode}; +use ironrdp_dvc::{DrdynvcClient, DvcProcessor, DynamicVirtualChannel}; +use ironrdp_pdu::mcs::{DisconnectProviderUltimatum, DisconnectReason, McsMessage, SendDataIndicationCtx}; +use ironrdp_pdu::rdp::autodetect::{AutoDetectReqPdu, AutoDetectRequest, AutoDetectResponse, AutoDetectRspPdu}; +use ironrdp_pdu::rdp::headers::ShareDataPdu; +use ironrdp_pdu::rdp::multitransport::MultitransportRequestPdu; +use ironrdp_pdu::rdp::server_error_info::{ErrorInfo, ProtocolIndependentCode, ServerSetErrorInfoPdu}; +use ironrdp_pdu::x224::X224; +use ironrdp_svc::{StaticChannelSet, SvcMessage, SvcProcessor, SvcProcessorMessages, client_encode_svc_messages}; +use tracing::debug; + +use crate::{SessionError, SessionErrorExt as _, SessionResult, reason_err}; + +/// X224 Processor output +#[derive(Debug, Clone)] +pub enum ProcessorOutput { + /// A buffer with encoded data to send to the server. + ResponseFrame(Vec), + /// A graceful disconnect notification. Client should close the connection upon receiving this. + Disconnect(DisconnectDescription), + /// Received a [`ironrdp_pdu::rdp::headers::ServerDeactivateAll`] PDU. Client should execute the + /// [Deactivation-Reactivation Sequence]. + /// + /// [Deactivation-Reactivation Sequence]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dfc234ce-481a-4674-9a5d-2a7bafb14432 + DeactivateAll, + /// Server Initiate Multitransport Request. The application should establish a + /// sideband UDP transport using the request ID and security cookie, then send + /// a [`MultitransportResponsePdu`] back on the IO channel. + /// + /// See [\[MS-RDPBCGR\] 2.2.15.1]. + /// + /// [\[MS-RDPBCGR\] 2.2.15.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/de783158-8b01-4818-8fb0-62523a5b3490 + /// [`MultitransportResponsePdu`]: ironrdp_pdu::rdp::multitransport::MultitransportResponsePdu + MultitransportRequest(MultitransportRequestPdu), + /// Auto-detect network characteristics from server ([\[MS-RDPBCGR\] 2.2.14]). + /// + /// Currently only surfaces [`AutoDetectRequest::NetworkCharacteristicsResult`]. + /// RTT requests are handled internally with automatic responses. + /// + /// [\[MS-RDPBCGR\] 2.2.14]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/dc672839-4f4e-40b1-a71c-cd6a959baa38 + AutoDetect(AutoDetectRequest), + /// Slow-path graphics update ([MS-RDPBCGR] 2.2.9.1.1.3). + /// Raw update payload starting with `updateType(u16)`. + GraphicsUpdate(Vec), + /// Slow-path pointer update ([MS-RDPBCGR] 2.2.9.1.1.4). + /// Raw pointer payload starting with `messageType(u16) + pad(u16)`. + PointerUpdate(Vec), +} + +#[derive(Debug, Clone)] +pub enum DisconnectDescription { + /// Includes the reason from the MCS Disconnect Provider Ultimatum. + /// This is the least-specific disconnect reason and is only used + /// when a more specific disconnect code is not available. + McsDisconnect(DisconnectReason), + + /// Includes the error information sent by the RDP server when there + /// is a connection or disconnection failure. + ErrorInfo(ErrorInfo), +} + +pub struct Processor { + static_channels: StaticChannelSet, + user_channel_id: u16, + io_channel_id: u16, + message_channel_id: Option, + share_id: u32, +} + +impl Processor { + pub fn new( + static_channels: StaticChannelSet, + user_channel_id: u16, + io_channel_id: u16, + message_channel_id: Option, + share_id: u32, + ) -> Self { + Self { + static_channels, + user_channel_id, + io_channel_id, + message_channel_id, + share_id, + } + } + + pub fn set_share_id(&mut self, share_id: u32) { + self.share_id = share_id; + } + + pub fn get_svc_processor(&self) -> Option<&T> { + self.static_channels + .get_by_type::() + .and_then(|svc| svc.channel_processor_downcast_ref()) + } + + pub fn get_svc_processor_mut(&mut self) -> Option<&mut T> { + self.static_channels + .get_by_type_mut::() + .and_then(|svc| svc.channel_processor_downcast_mut()) + } + + /// Completes user's SVC request with data, required to sent it over the network and returns + /// a buffer with encoded data. + pub fn process_svc_processor_messages( + &self, + messages: SvcProcessorMessages, + ) -> SessionResult> { + let channel_id = self + .static_channels + .get_channel_id_by_type::() + .ok_or_else(|| reason_err!("SVC", "channel not found"))?; + + process_svc_messages(messages.into(), channel_id, self.user_channel_id) + } + + pub fn get_dvc(&self) -> Option<&DynamicVirtualChannel> { + self.get_svc_processor::()?.get_dvc_by_type_id::() + } + + pub fn get_dvc_by_channel_id(&self, channel_id: u32) -> Option<&DynamicVirtualChannel> { + self.get_svc_processor::()? + .get_dvc_by_channel_id(channel_id) + } + + /// Processes a received PDU. Returns a vector of [`ProcessorOutput`] that must be processed + /// in the returned order. + pub fn process(&mut self, frame: &[u8]) -> SessionResult> { + let data_ctx: SendDataIndicationCtx<'_> = + ironrdp_pdu::mcs::decode_send_data_indication(frame).map_err(SessionError::decode)?; + let channel_id = data_ctx.channel_id; + + if channel_id == self.io_channel_id { + self.process_io_channel(data_ctx) + } else if self.message_channel_id == Some(channel_id) { + self.process_message_channel(data_ctx) + } else if let Some(svc) = self.static_channels.get_by_channel_id_mut(channel_id) { + let response_pdus = svc.process(data_ctx.user_data).map_err(SessionError::pdu)?; + process_svc_messages(response_pdus, channel_id, data_ctx.initiator_id) + .map(|data| vec![ProcessorOutput::ResponseFrame(data)]) + } else { + Err(reason_err!("X224", "unexpected channel received: ID {channel_id}")) + } + } + + fn process_io_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult> { + debug_assert_eq!(data_ctx.channel_id, self.io_channel_id); + + let io_channel = ironrdp_pdu::rdp::headers::decode_io_channel(data_ctx).map_err(SessionError::decode)?; + + match io_channel { + ironrdp_pdu::rdp::headers::IoChannelPdu::Data(ctx) => { + match ctx.pdu { + ShareDataPdu::SaveSessionInfo(session_info) => { + debug!("Got Session Save Info PDU: {session_info:?}"); + Ok(Vec::new()) + } + // FIXME: workaround fix to not terminate the session on "unhandled PDU: Set Keyboard Indicators PDU" + ShareDataPdu::SetKeyboardIndicators(data) => { + debug!("Got Keyboard Indicators PDU: {data:?}"); + Ok(Vec::new()) + } + ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(ErrorInfo::ProtocolIndependentCode( + ProtocolIndependentCode::None, + ))) => { + debug!("Received None server error"); + Ok(Vec::new()) + } + ShareDataPdu::ServerSetErrorInfo(ServerSetErrorInfoPdu(e)) => { + // This is a part of server-side graceful disconnect procedure defined + // in [MS-RDPBCGR]. + // + // [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/149070b0-ecec-4c20-af03-934bbc48adb8 + let desc = DisconnectDescription::ErrorInfo(e); + Ok(vec![ProcessorOutput::Disconnect(desc)]) + } + ShareDataPdu::ShutdownDenied => { + debug!("ShutdownDenied received, session will be closed"); + + // As defined in [MS-RDPBCGR], when `ShareDataPdu::ShutdownDenied` is received, we + // need to send a disconnect ultimatum to the server if we want to proceed with the + // session shutdown. + // + // [MS-RDPBCGR]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/27915739-8f77-487e-9927-55008af7fd68 + let ultimatum = McsMessage::DisconnectProviderUltimatum( + DisconnectProviderUltimatum::from_reason(DisconnectReason::UserRequested), + ); + + let encoded_pdu = ironrdp_core::encode_vec(&X224(ultimatum)).map_err(SessionError::encode); + + Ok(vec![ + ProcessorOutput::ResponseFrame(encoded_pdu?), + ProcessorOutput::Disconnect(DisconnectDescription::McsDisconnect( + DisconnectReason::UserRequested, + )), + ]) + } + // TODO: slow-path payloads may be bulk-compressed when + // ClientInfoFlags::COMPRESSION is negotiated. Decompression + // should happen here before passing data downstream. Currently + // IronRDP does not wire bulk decompression into this path. + // FIXME: until this is wired, the client deliberately defaults to the simple, + // stateless-friendly MPPC 64K (RDP5) compression level rather than XCRUSH; a + // stateful codec would risk silent corruption on slow-path updates. + ShareDataPdu::Update(data) => { + debug!("Got slow-path graphics update ({} bytes)", data.len()); + Ok(vec![ProcessorOutput::GraphicsUpdate(data)]) + } + ShareDataPdu::Pointer(data) => { + debug!("Got slow-path pointer update ({} bytes)", data.len()); + Ok(vec![ProcessorOutput::PointerUpdate(data)]) + } + _ => Err(reason_err!( + "IO channel", + "unhandled PDU: {:?}", + ctx.pdu.as_short_name() + )), + } + } + ironrdp_pdu::rdp::headers::IoChannelPdu::MultitransportRequest(pdu) => { + debug!( + "Received Initiate Multitransport Request: request_id={}", + pdu.request_id + ); + Ok(vec![ProcessorOutput::MultitransportRequest(pdu)]) + } + ironrdp_pdu::rdp::headers::IoChannelPdu::DeactivateAll(_) => Ok(vec![ProcessorOutput::DeactivateAll]), + } + } + + /// Process an auto-detect request received on the MCS message channel. + /// + /// During continuous auto-detection ([MS-RDPBCGR] 2.2.14) the server sends + /// RTT (and bandwidth) requests on the message channel; the client answers + /// RTT requests and surfaces the final Network Characteristics Result. + fn process_message_channel(&self, data_ctx: SendDataIndicationCtx<'_>) -> SessionResult> { + let Some(message_channel_id) = self.message_channel_id else { + return Err(reason_err!("message channel", "no message channel negotiated")); + }; + + let req = decode::(data_ctx.user_data).map_err(SessionError::decode)?; + + match req.request { + AutoDetectRequest::RttRequest { sequence_number, .. } => { + let response = AutoDetectRspPdu::new(AutoDetectResponse::RttResponse { sequence_number }); + let mut frame = WriteBuf::new(); + ironrdp_pdu::mcs::encode_send_data_request( + self.user_channel_id, + message_channel_id, + &response, + &mut frame, + ) + .map_err(SessionError::encode)?; + debug!(sequence_number, "Responded to auto-detect RTT request"); + Ok(vec![ProcessorOutput::ResponseFrame(frame.into_inner())]) + } + req @ AutoDetectRequest::NetworkCharacteristicsResult { .. } => { + debug!(?req, "Received network characteristics from server"); + Ok(vec![ProcessorOutput::AutoDetect(req)]) + } + req => { + debug!(?req, "Auto-detect request not yet implemented"); + Ok(Vec::new()) + } + } + } + + /// Send a pdu on the static global channel. Typically used to send input events + pub fn encode_static(&self, output: &mut WriteBuf, pdu: ShareDataPdu) -> SessionResult { + let written = ironrdp_pdu::rdp::headers::encode_share_data( + self.user_channel_id, + self.io_channel_id, + self.share_id, + pdu, + output, + ) + .map_err(SessionError::encode)?; + Ok(written) + } +} + +/// Processes a vector of [`SvcMessage`] in preparation for sending them to the server on the `channel_id` channel. +/// +/// This includes chunkifying the messages, adding MCS, x224, and tpkt headers, and encoding them into a buffer. +/// The messages returned here are ready to be sent to the server. +/// +/// The caller is responsible for ensuring that the `channel_id` corresponds to the correct channel. +fn process_svc_messages(messages: Vec, channel_id: u16, initiator_id: u16) -> SessionResult> { + client_encode_svc_messages(messages, channel_id, initiator_id).map_err(SessionError::encode) +} diff --git a/rust/vendor/picky/CHANGELOG.md b/rust/vendor/picky/CHANGELOG.md new file mode 100644 index 0000000..5f913bd --- /dev/null +++ b/rust/vendor/picky/CHANGELOG.md @@ -0,0 +1,380 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[7.0.0-rc.25](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.24...picky-v7.0.0-rc.25)] - 2026-06-26 + +### Build + +- Align RustCrypto ECC dependency versions and remove unnecessary transitive pins ([#506](https://github.com/Devolutions/picky-rs/issues/506)) ([a4346f2853](https://github.com/Devolutions/picky-rs/commit/a4346f2853e726eb05b1fb5de2fa9351c8c44e2e)) + + + +## [[7.0.0-rc.24](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.23...picky-v7.0.0-rc.24)] - 2026-06-19 + +### Build + +- Bump sha3 from 0.11.0 to 0.12.0 ([#495](https://github.com/Devolutions/picky-rs/issues/495)) ([0b065a59fb](https://github.com/Devolutions/picky-rs/commit/0b065a59fb1e65c354084016166a7822ab10f7bf)) + +- Advance RustCrypto pins to the current rc.18/rc.33/rc.10 cluster ([#501](https://github.com/Devolutions/picky-rs/issues/501)) ([7d4b2675b9](https://github.com/Devolutions/picky-rs/commit/7d4b2675b91f7803905d972501130fc09d42ce1c)) + +## [[7.0.0-rc.23](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.22...picky-v7.0.0-rc.23)] - 2026-04-21 + +### Build + +- Upgrade RustCrypto dependencies ([#476](https://github.com/Devolutions/picky-rs/issues/476)) ([8ce838e347](https://github.com/Devolutions/picky-rs/commit/8ce838e3470ac605c912f61dc67c1c6d388b140d)) + +## [[7.0.0-rc.22](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.21...picky-v7.0.0-rc.22)] - 2026-02-27 + +### Bug Fixes + +- Bump `sha1` to v0.11.0-rc.3 ([#463](https://github.com/Devolutions/picky-rs/issues/463)) ([e40aee72bd](https://github.com/Devolutions/picky-rs/commit/e40aee72bd248bda84e6fe9e84a648784a6374ed)) + +## [[7.0.0-rc.21](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.20...picky-v7.0.0-rc.21)] - 2026-02-02 + +### Build + +- Update crypto dependencies ([#448](https://github.com/Devolutions/picky-rs/issues/448)) ([0bf42c1c4b](https://github.com/Devolutions/picky-rs/commit/0bf42c1c4bd727eaf5b4b0a877e8698986e0bd37)) + +- Remove transitive pem-rfc7468 crate ([#449](https://github.com/Devolutions/picky-rs/issues/449)) ([cc4caf455d](https://github.com/Devolutions/picky-rs/commit/cc4caf455defd38ef37fc15434b173275937d534)) + +- Bump reqwest from 0.12.24 to 0.13.1 ([#453](https://github.com/Devolutions/picky-rs/issues/453)) ([9cbaab9395](https://github.com/Devolutions/picky-rs/commit/9cbaab93955f2fef8f96a589b22e2582d4a8cbf0)) + +## [[7.0.0-rc.20](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.19...picky-v7.0.0-rc.20)] - 2025-11-07 + +### Bug Fixes + +- Pin leftover pre-release crypto crates ([#430](https://github.com/Devolutions/picky-rs/issues/430)) ([0fe43dae7b](https://github.com/Devolutions/picky-rs/commit/0fe43dae7b463a16d48b00b08ca6ce21a013af74)) + + The patch version upgrade is not allowed to bring breaking changes, but + this rule doesn't work for an `rc` version. So we should pin the `rc` + versions, to not allow _cargo_ update to a new `rc` automatically. + +## [[7.0.0-rc.19](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.18...picky-v7.0.0-rc.19)] - 2025-10-10 + +### Features + +- Support parsing certs with MLDSA public keys ([#411](https://github.com/Devolutions/picky-rs/issues/411)) ([abc63dde88](https://github.com/Devolutions/picky-rs/commit/abc63dde88360e35ef225cfd1c279958f47c3abc)) + +### Build + +- Pin RustCrypto release candidate crates ([#417](https://github.com/Devolutions/picky-rs/issues/417)) ([8a79282bbc](https://github.com/Devolutions/picky-rs/commit/8a79282bbc0dae9df222f16d261b7dd1f03cd66f)) + +## [[7.0.0-rc.18](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.17...picky-v7.0.0-rc.18)] - 2025-09-26 + +### Build + +- Bump the crypto group across 1 directory with 3 updates (#388) ([58d179a0c3](https://github.com/Devolutions/picky-rs/commit/58d179a0c39d701025a363c3f294912c2881a8f5)) + +## [[7.0.0-rc.17](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.16...picky-v7.0.0-rc.17)] - 2025-08-18 + +### Build + +- Bump rstest from 0.25.0 to 0.26.1 (#391) ([2a169deba2](https://github.com/Devolutions/picky-rs/commit/2a169deba2ba177685d58550f4214f59f25ebec0)) + +## [[7.0.0-rc.16](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.15...picky-v7.0.0-rc.16)] - 2025-07-15 + +### Security + +- Reject issuer certificates missing basic constraints (#385) ([4d8be23912](https://github.com/Devolutions/picky-rs/commit/4d8be239126cd34843a74de017118823699b8aa7)) + + As per RFC 5280: + + > The cA boolean indicates whether the certified public key may be used + > to verify certificate signatures. If the cA boolean is not asserted, + > then the keyCertSign bit in the key usage extension MUST NOT be + > asserted. If the basic constraints extension is not present in a + > version 3 certificate, or the extension is present but the cA boolean + > is not asserted, then the certified public key MUST NOT be used to + > verify certificate signatures. + +## [[7.0.0-rc.15](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.14...picky-v7.0.0-rc.15)] - 2025-06-24 + +### Build + +- Bump MSVC to 1.85 (#379) ([302df9912d](https://github.com/Devolutions/picky-rs/commit/302df9912d6f038f21b64fc916bd82836944c382)) + +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [[7.0.0-rc.14](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.13...picky-v7.0.0-rc.14)] - 2025-05-14 + +### Features + +- Add HttpSignature::to_signing_string() method ([fe054c47f0](https://github.com/Devolutions/picky-rs/commit/fe054c47f04920383d93371bda948a961cbefdfd)) + + Equivalent to HttpSignature::to_string(). + +## [[7.0.0-rc.13](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.12...picky-v7.0.0-rc.13)] - 2025-03-03 + +### Bug Fixes + +- [**breaking**] Comply with draft-cavage-http-signatures-12 when signing HTTP messages (#351) ([f043d04e14](https://github.com/Devolutions/picky-rs/commit/f043d04e1462cf0eddd9140f22a4d364cb3ad9cd)) + + Change request target handling to comply with draft-cavage-http-signatures-12 when signing HTTP messages. + +## [[7.0.0-rc.12](https://github.com/Devolutions/picky-rs/compare/picky-v7.0.0-rc.11...picky-v7.0.0-rc.12)] - 2025-01-16 + +### Bug Fixes + +- Symlinks to license files in packages (#339) ([1834c04f39](https://github.com/Devolutions/picky-rs/commit/1834c04f3930fb1bbf040deb6525b166e378b8aa)) + + Use symlinks instead of copying files to avoid a “dirty” state during + cargo publish and preserve VCS info. With #337 merged, CI handles + publishing consistently, so developer environments no longer matter. + + +## [7.0.0-rc.1 to 7.0.0-rc.11](https://github.com/Devolutions/picky-rs/compare/picky-6.4.0...picky-v7.0.0-rc.11) + +### Added + +- Support for ECDSA p256 p384 and p521 signatures +- Support for MD5 hashing +- CTL implementation behind `ctl` feature +- CTL fetching over HTTP is behind `ctl_http_fetch` feature +- `Pkcs7::digest_algorithms` +- `Pkcs7::signer_infos` +- `Pkcs7::encapsulated_content_info` +- `Pkcs7::decode_certificates` +- `impl From for Pkcs7` +- `impl From for Pkcs7Certificate` +- `AuthenticodeSignature` struct +- `AuthenticodeSignature::new` +- `AuthenticodeSignature::from_der` +- `AuthenticodeSignature::from_pem` +- `AuthenticodeSignature::from_pem_str` +- `AuthenticodeSignature::to_der` +- `AuthenticodeSignature::to_pem` +- `AuthenticodeSignature::signing_certificate` +- `AuthenticodeSignature::authenticode_verifier` +- `AuthenticodeSignature::file_hash` +- `Authenticated_attributes::authenticated_attributes` +- `Authenticated_attributes::unauthenticated_attributes` +- `impl From for AuthenticodeSignature` +- `From for Pkcs7` +- Authenticode validation +- Support for `AuthenticodeSignature` timestamping: + - Method `timestamp` to `AuthenticodeSignature` + - `Timestamper` trait. + - Timestamping implementation using reqwest is behind `http_timestamp` feature +- Authenticode timestamp request struct - `TimestampRequest` +- `AuthenticodeBuilder` for easier `AuthenticodeSignature` creation +- `SignatureAlgorithm::hash_algorithm` +- Support for `time 0.3` types conversions behind `time_conversion` feature gate +- `PrivateKey::to_pem_str` +- `PrivateKey::from_ec_components` +- `PrivateKey::from_ec_der_with_curve` +- `PrivateKey::from_ec_der_with_curve_oid` +- `PrivateKey::generate_ec` +- `PrivateKey::generate_ed` +- `PrivateKey::from_ec_encoded_components` +- `PrivateKey::from_ed_encoded_components` +- `PublicKey::to_pem_str` +- Support SSH keys and certificates +- `CheckedJwtEnc::new_with_cty` +- `CheckedJwtSig::new_with_cty` +- Support for JWT additional header parameters +- Support for EC private/public keys +- JWS and JWK EC curves support +- EC curves support for SSH keys and certificates +- EC x509 certificates verification/signing +- `JwkKeyType::new_ec_key` +- `JwkKeyType::new_ed_key` +- `JwkKeyType::as_ec` +- `JwkKeyType::as_ed` +- `JwkKeyType::is_ec` +- `JwkKeyType::is_ed` +- `SshPrivateKey::generate_ec` +- `SshPrivateKey::generate_ed` +- `JwkEcPublicKeyCurve` enum +- `JwkEdPublicKeyAlgorithm` enum +- `JwsAlg::EdDSA` variant +- `JwsAlg::ED25519` variant (`#[deprecated]` from the start, just for compatibility with other libs) +- Support for X25519 and Ed25519 public/private keys +- JWS and JWK Ed25519 keys/signing support +- Ed25519 support for SSH keys and certificates +- Ed25519 x509 certificates verification/signing +- Support for `ECDH-ES` JWE algorithm (P256, P384 and P521 EC keys, X25519 ED keys) +- Support for `PKCS12`(PFX) files parsing/building +- PuTTY PPK key format support + +### Changed + +- Bump minimal rustc version to 1.70 +- Updated `p256`/`p384` to latest version +- (Breaking) Move Authenticode related code from `picky::x509::wincert` to `picky::x509::pkcs7::authenticode` module +- (Breaking) Authenticode implementation is now behind `pkcs7` feature +- (Breaking) `PrivateKey::to_pem` and `PublicKey::to_pem` now return a `Pem` +- (Breaking) Separate JWT validation from decoding in JOSE API (this makes API more convenient to first process header + and then decide what kind of validation should be applied, or what claims type to deserialize into) +- (Breaking) `PrivateKey` now have private struct representation instead of open enum +- (Breaking) Now all `PrivateKey::to_public` and all related APIs for private keys return `Result` + instead of plain `PublicKey`. This change was required because added support for EC/ED keys is not + infallible in regard to public key extraction (public key could be missing from the file and could + not be generated because of an unsupported algorithm/curve) +- "picky-test-data" is a new dev-dependency. + +### Removed + +- (Breaking) `ec` non-default cargo feature (EC operations now enabled by default) + +### Fixed + +- `BufReader` panic in `WinCertificate::decode` and `WinCertificate::encode` if data len is bigger than default capacity. +- `WinCertificate` encoding: `length` wasn’t correct. +- Leading zeros in JWK encoding. + + JWK encoding of a value is the unsigned big-endian representation as an octet sequence. + The octet sequence MUST utilize the minimum number of octets needed to represent the value. + That is: **no leading zero** must be present. + + See issue [#140](https://github.com/Devolutions/picky-rs/issues/140) + +- Fetch curve info from private_key_algorithm ([#143](https://github.com/Devolutions/picky-rs/issues/143)) + + Pick curve info from private_key_algorithm field. + +- Missing `zeroize` on drop of internal `EcdsaKeypair` structure +- Fixed invalid padding logic of encrypted SSH keys +- Fixed invalid generated SSH EC keys + +### Removed + +- (Breaking) `Jwt::new_encrypted` +- (Breaking) `Jwt::new_signed` + +## [6.4.0](https://github.com/Devolutions/picky-rs/compare/picky-6.3.0...picky-6.4.0) – 2021-08-10 + +### Changed + +- Bump minimum supported Rust version to 1.51 +- Update `rsa` dependency to `0.5` +- Update `picky-asn1` dependency to `0.4` +- More robust certification validation (see commit [f5f8cb60e41](https://github.com/Devolutions/picky-rs/commit/f5f8cb60e410ffe49aabace131f7b802e206ced0) for details) + +## [6.3.0](https://github.com/Devolutions/picky-rs/compare/picky-6.2.0...picky-6.3.0) – 2021-05-27 + +### Added + +- PKCS7 implementation behind `pkcs7` feature +- `WinCertificate` for authenticode behind `wincert` feature + +### Changed + +- Update `aes-gcm` dependency to `0.9` + +## [6.2.0](https://github.com/Devolutions/picky-rs/compare/picky-6.1.1...picky-6.2.0) – 2021-03-04 + +### Added + +- `Csr::generate_with_attributes` to generate CSR with attributes. +- `CertificateBuilder::inherit_extensions_from_csr_attributes` to allow certificate to inherit extensions requested by CSR attribute. +- Various API additions to `GeneralNames` for improved ergonomics. + +## 6.1.2 – 2021-01-11 + +### Fixed + +- Fix bad `use`s statements to `serde::export` + +## 6.1.1 – 2020-12-11 + +### Fixed + +- Fix `HttpSignatureError`'s Display trait implementation (`certificate expired` → `signature expired`). +- Fix certificate validity period that MUST be encoded as `UTCTime` through the year 2049 as per RFC 5280. + Previously, they were always encoded as `GeneralizedTime`. + +## 6.1.0 – 2020-10-21 + +### Added + +- `CertificateBuilder::serial_number` can be used to provide a custom serial number instead of generating one. + +### Fixed + +- Bad generation for second exponent (`pq`) when generating PKCS#8 structure. +- Serial number was sometimes generated as negative. + +## 6.0.0 – 2020-10-13 + +### Added + +- Implementation of `Jwe` (JSON Web Encryption) RFC. +- Email attribute can be added to `DirectoryName` using `add_email` method. +- `from_pem_str` method to `Cert`, `Csr`, `PrivateKey` and `PublicKey`. + +### Changed + +- Separate `SignatureHashType` into two separate enums `HashAlgorithm` and `SignatureAlgorithm`. +- `KeyIdGenError` is now wrapping a `HashAlgorithm`. +- Update `rsa` dependency. +- `HttpSignature` store custom `algorithm` names instead of just ignoring them. +- Major `jose` module changes + - JOSE header fields are renamed to be identical to RFCs when possible. + - `Jwt` is now divided into `Jws` (JSON Web Signature) and `Jwe` (JSON Web Encryption) + - `Jws` provides an API to sign any kind of data (binary). JSON claims are part of `Jwt` only. + - `Jwe` provides an API to encrypt any kind of data (binary). JSON claims are part of `Jwt` only. +- Typo in `CertificateBuilder` API: `valididy` has been renamed to `validity`. + +### Fixed + +- RSA private key generation ([#53](https://github.com/Devolutions/picky-rs/issues/53)). + +## 5.1.1 – 2020-07-13 + +### Changed + +- Better `CaChainError::AuthorityKeyIdMismatch` display. + +## 5.1.0 – 2020-07-07 + +### Added + +- Add `BufRead`-based API to read PEM streams: `pem::Pem::read_from` and `pem::read_pem`. + +### Changed + +- Some internal types are moved to a new `picky_asn1_x509` crate but API is unchanged. +- Dependencies clean up. + +## 5.0.0 – 2020-05-06 + +### Added + +- Add `into_public_key` method to `x509::Cert`. +- Support for the ["algorithm" http signature parameter](https://tools.ietf.org/html/draft-cavage-http-signatures-12#section-2.1.3). + +### Changed + +- `Jwt` has no generic lifetime parameter anymore. + +### Removed + +- `Cert::verify` and `Cert::verify_chain` methods (previously deprecated) +- `HttpRequest` trait impl for `http` crate v0.1 + +### Fixed + +- Add missing parameters for AES and SHA object identifiers ([668c06e8d](https://github.com/Devolutions/picky-rs/commit/668c06e8d8e8a0caae8bd13cf81c189bbc2e4918)) + +## 4.7.0 – 2020-04-16 + +### Added + +- Implement `From` trait on `DateTime`. +- Support for leeway in HTTP signature verifier. +- Verifier API to X509 Cert. + +### Deprecated + +- `Cert::verify` and `Cert::verify_chain` methods in favor of the `Cert::verifier` method. diff --git a/rust/vendor/picky/Cargo.lock b/rust/vendor/picky/Cargo.lock new file mode 100644 index 0000000..a43ed10 --- /dev/null +++ b/rust/vendor/picky/Cargo.lock @@ -0,0 +1,2564 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addchain" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da8c919c118108f144adecad74b425b804ad075580d605d9b33c2d6d1c62a2f8" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aes-kw" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ac571010bd60765c56085a4f1d412012a9be2663b1a2f2b19b49318653fd0d" +dependencies = [ + "aes", + "const-oid", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "any_ascii" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" + +[[package]] +name = "argon2" +version = "0.6.0-rc.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitvec" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.11.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "blowfish" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" + +[[package]] +name = "cab" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2" +dependencies = [ + "byteorder", + "flate2", + "lzxd", + "time", +] + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer", + "crypto-common", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.3", + "hybrid-array", + "num-traits", + "rand_core", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core", +] + +[[package]] +name = "crypto-primes" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f" +dependencies = [ + "crypto-bigint", + "rand_core", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c906a87e53a36ff795d72e06e8162a83c5436e3ea89e942a9cb9fc083f0a384f" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "ecdsa" +version = "0.17.0-rc.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c72d1455753a703ad4b90ed2a759f2bc4562024a303176439cf6e593b5ade4" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1685663e23882cd8517dcbcb1c23a6ebff4433c22dfb681d760219b62cd1b849" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3273f1195b6f6253ebda493d6742c8baa9b26a291674cd96d92a0f09e90e9b46" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common", + "digest", + "ff", + "group", + "hkdf", + "hybrid-array", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "expect-test" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63af43ff4431e848fb47472a920f14fa71c24de13255a5692e93d4e90302acb0" +dependencies = [ + "dissimilar", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures", +] + +[[package]] +name = "lexical-sort" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c09e4591611e231daf4d4c685a66cb0410cc1e502027a20ae55f2bb9e997207a" +dependencies = [ + "any_ascii", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lzxd" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17f346186eccb574ba5581acefc514f0c70a642db4f96e245034a0a158a7168" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c19903c598813dba001b53beeae59bb77ad4892c5c1b9b3500ce4293a0d06c2" +dependencies = [ + "serde", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "p256" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c855a8d2ffd346aa03122626f22e96e3aa75e3bfe64e6bf6cb82f71821ed6ae7" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941b68907ddf996ac20f0debf700c236ccc3d874637731a93c631129ca042f" +dependencies = [ + "ecdsa", + "elliptic-curve", + "fiat-crypto", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd6f2fe6e76c8d5e8828e92aafa463777d1e72e70b78acc724214757e92479a" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2", +] + +[[package]] +name = "password-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" +dependencies = [ + "getrandom 0.4.3", + "phc", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", + "getrandom 0.4.3", +] + +[[package]] +name = "picky" +version = "7.0.0-rc.25" +dependencies = [ + "aes", + "aes-gcm", + "aes-kw", + "argon2", + "base64", + "bcrypt-pbkdf", + "blake2", + "byteorder", + "cab", + "cbc", + "cfg-if", + "chrono", + "crypto-bigint", + "crypto-common", + "ctr", + "curve25519-dalek", + "des", + "digest", + "ecdsa", + "ed25519-dalek", + "expect-test", + "hex", + "hmac", + "http", + "inout", + "lexical-sort", + "md-5", + "p256", + "p384", + "p521", + "pbkdf2", + "picky-asn1", + "picky-asn1-der", + "picky-asn1-x509", + "picky-test-data", + "pkcs1", + "pretty_assertions", + "primeorder", + "rand", + "rand_chacha", + "rand_core", + "rc2", + "reqwest", + "ring", + "rsa", + "rstest", + "rustcrypto-ff", + "rustcrypto-ff_derive", + "rustcrypto-group", + "serde", + "serde_json", + "sha1", + "sha2", + "sha3", + "tempfile", + "thiserror", + "time", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "picky-asn1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff038f9360b934342fb3c0a1d6e82c438a2624b51c3c6e3e6d7cf252b6f3ee3" +dependencies = [ + "chrono", + "oid", + "serde", + "serde_bytes", + "time", + "zeroize", +] + +[[package]] +name = "picky-asn1-der" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d413165e4bf7f808b9a27cbaba657657a2921f0965db833f488c4d4be96dcd2e" +dependencies = [ + "picky-asn1", + "serde", + "serde_bytes", +] + +[[package]] +name = "picky-asn1-x509" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d4117bd1b1dc5646359ee7243c50c5000c0920ea2d1fb120335a2f4c684b8" +dependencies = [ + "base64", + "crypto-bigint", + "oid", + "picky-asn1", + "picky-asn1-der", + "serde", + "widestring", + "zeroize", +] + +[[package]] +name = "picky-test-data" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352ab61f19a5d88d1a8b33cc01bcfa9c119a7e3aa6afa38062ed9e714a1d357" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.8.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "polyval" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +dependencies = [ + "cpubits", + "cpufeatures", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common", + "ff", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-rc.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e56e6d67fdf5744e9e245ae571450fe584b91f5af261d0e40163b618e53a1f6" +dependencies = [ + "elliptic-curve", + "once_cell", + "primefield", + "serdect", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rc2" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceda21af1ae61033b63175653a1af86cae399d79cd03ca80ba347eb3a6c4a7fe" +dependencies = [ + "cipher", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rfc6979" +version = "0.6.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9935425142ac6e252364413291d96c8bc9898d0876a801824c7af4eae397b689" +dependencies = [ + "ctutils", + "hmac", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.10.0-rc.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" +dependencies = [ + "const-oid", + "crypto-bigint", + "crypto-primes", + "digest", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "zeroize", +] + +[[package]] +name = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn 2.0.118", + "unicode-ident", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustcrypto-ff" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" +dependencies = [ + "bitvec", + "rand_core", + "rustcrypto-ff_derive", + "subtle", +] + +[[package]] +name = "rustcrypto-ff_derive" +version = "0.14.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cda22ea03582974ab5687fc131eba2dc78e258e7eef4d7e01bcd0522ed79f66" +dependencies = [ + "addchain", + "num-bigint", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rustcrypto-group" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" +dependencies = [ + "rand_core", + "rustcrypto-ff", + "subtle", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der", + "hybrid-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" +dependencies = [ + "digest", + "keccak", + "sponge-cursor", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sponge-cursor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[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.118", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "3.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee64e8620caa64914d669b1f68f858aaff54e2d0f9ad3b30a613b58a1baa83e" +dependencies = [ + "curve25519-dalek", + "rand_core", + "zeroize", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/vendor/picky/Cargo.toml b/rust/vendor/picky/Cargo.toml new file mode 100644 index 0000000..b3c41f5 --- /dev/null +++ b/rust/vendor/picky/Cargo.toml @@ -0,0 +1,353 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.85" +name = "picky" +version = "7.0.0-rc.25" +authors = [ + "Benoît CORTIER ", + "Jonathan Trepanier ", + "François Dubois ", + "Richard Markiewicz ", + "Ionut Mihalcea ", + "Kim Altintop ", + "Alexandr Yusuk ", + "Flavio Castelli ", + "Xynnn_ ", +] +build = false +include = [ + "src/**/*", + "README.md", + "CHANGELOG.md", + "LICENSE-*", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Portable X.509, PKI, JOSE and HTTP signature implementation." +readme = "README.md" +keywords = [ + "x509", + "jwt", + "signature", + "jose", + "pki", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/Devolutions/picky-rs" +resolver = "2" + +[package.metadata.docs.rs] +all-features = true + +[features] +chrono_conversion = [ + "dep:chrono", + "picky-asn1/chrono_conversion", +] +ctl = [ + "picky-asn1-x509/ctl", + "pkcs7", + "chrono_conversion", +] +ctl_http_fetch = [ + "dep:reqwest", + "dep:cab", + "ctl", +] +default = [ + "x509", + "jose", + "http_signature", + "http_trait_impl", + "pkcs12", +] +http_signature = [] +http_timestamp = ["dep:reqwest"] +http_trait_impl = ["dep:http"] +jose = [ + "dep:serde_json", + "dep:aes-gcm", + "dep:ctr", + "dep:cbc", + "dep:aes-kw", + "dep:aes", +] +pkcs12 = [ + "picky-asn1-x509/pkcs12", + "dep:des", + "dep:rc2", + "dep:cbc", + "dep:hmac", + "dep:aes", + "x509", + "dep:pbkdf2", +] +pkcs7 = [ + "x509", + "picky-asn1-x509/pkcs7", +] +putty = [ + "dep:argon2", + "dep:hmac", + "ssh", + "dep:blake2", +] +ssh = [ + "dep:byteorder", + "dep:aes", + "dep:ctr", + "dep:cbc", + "dep:bcrypt-pbkdf", + "dep:lexical-sort", + "dep:hmac", + "dep:pbkdf2", +] +time_conversion = [ + "dep:time", + "picky-asn1/time_conversion", +] +wincert = [ + "x509", + "dep:byteorder", +] +x509 = [] + +[lib] +name = "picky" +path = "src/lib.rs" + +[dependencies.aes] +version = "0.9" +optional = true + +[dependencies.aes-gcm] +version = "^0.11.0-rc.4" +optional = true + +[dependencies.aes-kw] +version = "0.3" +optional = true + +[dependencies.argon2] +version = "^0.6.0-rc.8" +optional = true + +[dependencies.base64] +version = "0.22" + +[dependencies.bcrypt-pbkdf] +version = "0.11" +optional = true + +[dependencies.blake2] +version = "^0.11.0-rc.6" +optional = true + +[dependencies.byteorder] +version = "1.5" +optional = true + +[dependencies.cab] +version = "0.6" +optional = true + +[dependencies.cbc] +version = "0.2" +features = ["alloc"] +optional = true + +[dependencies.chrono] +version = "0.4" +features = ["clock"] +optional = true +default-features = false + +[dependencies.crypto-bigint] +version = "0.7" + +[dependencies.crypto-common] +version = "0.2" + +[dependencies.ctr] +version = "0.10" +optional = true + +[dependencies.curve25519-dalek] +version = "^5.0.0-rc.1" + +[dependencies.des] +version = "0.9" +optional = true + +[dependencies.digest] +version = "0.11" + +[dependencies.ecdsa] +version = "^0.17.0-rc.22" + +[dependencies.ed25519-dalek] +version = "^3.0.0-rc.1" +features = [ + "hazmat", + "rand_core", +] + +[dependencies.hex] +version = "0.4" + +[dependencies.hmac] +version = "0.13" +optional = true + +[dependencies.http] +version = "1.4" +optional = true + +[dependencies.inout] +version = "0.2.2" + +[dependencies.lexical-sort] +version = "0.3" +optional = true + +[dependencies.md5] +version = "0.11" +features = ["oid"] +package = "md-5" + +[dependencies.p256] +version = "^0.14.0-rc.14" +features = ["ecdh"] + +[dependencies.p384] +version = "^0.14.0-rc.14" +features = ["ecdh"] + +[dependencies.p521] +version = "^0.14.0-rc.14" +features = ["ecdh"] + +[dependencies.pbkdf2] +version = "0.13" +optional = true + +[dependencies.picky-asn1] +version = "0.10" +features = ["zeroize"] + +[dependencies.picky-asn1-der] +version = "0.5" + +[dependencies.picky-asn1-x509] +version = "0.15" +features = [ + "legacy", + "zeroize", +] + +[dependencies.pkcs1] +version = "^0.8.0-rc.4" + +[dependencies.primeorder] +version = "^0.14.0-rc.14" + +[dependencies.rand] +version = "0.10" + +[dependencies.rand_core] +version = "0.10" + +[dependencies.rc2] +version = "0.9" +optional = true + +[dependencies.reqwest] +version = "0.13" +features = ["blocking"] +optional = true +default-features = false + +[dependencies.rsa] +version = "^0.10.0-rc.18" +features = ["std"] + +[dependencies.rustcrypto-ff] +version = "^0.14.0-rc.1" + +[dependencies.rustcrypto-ff_derive] +version = "^0.14.0-rc.0" + +[dependencies.rustcrypto-group] +version = "^0.14.0-rc.1" + +[dependencies.serde] +version = "1" +features = ["derive"] + +[dependencies.serde_json] +version = "1" +optional = true + +[dependencies.sha1] +version = "0.11" +features = ["oid"] + +[dependencies.sha2] +version = "0.11" +features = ["oid"] + +[dependencies.sha3] +version = "0.12" +features = ["oid"] + +[dependencies.thiserror] +version = "2" + +[dependencies.time] +version = "0.3" +optional = true + +[dependencies.x25519-dalek] +version = "^3.0.0-rc.1" +features = ["static_secrets"] + +[dependencies.zeroize] +version = "1.8" + +[dev-dependencies.cfg-if] +version = "1.0" + +[dev-dependencies.expect-test] +version = "1" + +[dev-dependencies.picky-test-data] +version = "0.1" + +[dev-dependencies.pretty_assertions] +version = "1.4" + +[dev-dependencies.rand_chacha] +version = "0.10" + +[dev-dependencies.ring] +version = "0.17" + +[dev-dependencies.rstest] +version = "0.26" + +[dev-dependencies.tempfile] +version = "3.22" diff --git a/rust/vendor/picky/Cargo.toml.orig b/rust/vendor/picky/Cargo.toml.orig new file mode 100644 index 0000000..90d7489 --- /dev/null +++ b/rust/vendor/picky/Cargo.toml.orig @@ -0,0 +1,128 @@ +[package] +name = "picky" +version = "7.0.0-rc.25" +authors = [ + "Benoît CORTIER ", + "Jonathan Trepanier ", + "François Dubois ", + "Richard Markiewicz ", + "Ionut Mihalcea ", + "Kim Altintop ", + "Alexandr Yusuk ", + "Flavio Castelli ", + "Xynnn_ ", +] +description = "Portable X.509, PKI, JOSE and HTTP signature implementation." +keywords = ["x509", "jwt", "signature", "jose", "pki"] +edition = "2024" +rust-version = "1.85" +license = "MIT OR Apache-2.0" +repository = "https://github.com/Devolutions/picky-rs" +include = ["src/**/*", "README.md", "CHANGELOG.md", "LICENSE-*"] + +[dependencies] +picky-asn1 = { version = "0.10", path = "../picky-asn1", features = ["zeroize"] } +picky-asn1-der = { version = "0.5", path = "../picky-asn1-der" } +picky-asn1-x509 = { version = "0.15", path = "../picky-asn1-x509", features = ["legacy", "zeroize"] } +serde = { version = "1", features = ["derive"] } +base64 = "0.22" +thiserror = "2" +byteorder = { version = "1.5", optional = true } +chrono = { version = "0.4", default-features = false, features = ["clock"], optional = true } +time = { version = "0.3", optional = true } +serde_json = { version = "1", optional = true } +hex = "0.4" +http = { version = "1.4", optional = true } +cab = { version = "0.6", optional = true } +lexical-sort = { version = "0.3", optional = true } +zeroize = "1.8" + +# FIXME: either use ureq, or even better: do not require this kind of dependency at all to let user decide which lib to use. +# (currently users should *really* not forget to use `spawn_blocking` when calling associated functions from async context) +reqwest = { version = "0.13", default-features = false, features = ["blocking"], optional = true } + +# /!\ ===== cryptography dependencies ===== /!\ +# These should be updated as soon as possible. +# /!\ ===================================== /!\ + +rand = "0.10" +rand_core = "0.10" +crypto-bigint = "0.7" + +ed25519-dalek = { version = "=3.0.0-rc.1", features = ["hazmat", "rand_core"] } +x25519-dalek = { version = "=3.0.0-rc.1", features = ["static_secrets"] } + +p256 = { version = "=0.14.0-rc.14", features = ["ecdh"] } +p384 = { version = "=0.14.0-rc.14", features = ["ecdh"] } +p521 = { version = "=0.14.0-rc.14", features = ["ecdh"] } + +rsa = { version = "=0.10.0-rc.18", features = ["std"] } + +digest = "0.11" +md5 = { package = "md-5", version = "0.11", features = ["oid"] } +sha1 = { version = "0.11", features = ["oid"] } +sha2 = { version = "0.11", features = ["oid"] } +sha3 = { version = "0.12", features = ["oid"] } + +aes-gcm = { version = "=0.11.0-rc.4", optional = true } +aes = { version = "0.9", optional = true } +aes-kw = { version = "0.3", optional = true } +argon2 = { version = "=0.6.0-rc.8", optional = true } +ctr = { version = "0.10", optional = true } +cbc = { version = "0.2", optional = true, features = ["alloc"] } +bcrypt-pbkdf = { version = "0.11", optional = true } +des = { version = "0.9", optional = true } +rc2 = { version = "0.9", optional = true } +pbkdf2 = { version = "0.13", optional = true } +hmac = { version = "0.13", optional = true } +crypto-common = "0.2" +inout = "0.2.2" + +# Pin transitive dependencies versions. +# TODO: Remove when stable versions will be released. +blake2 = { version = "=0.11.0-rc.6", optional = true } +ecdsa = "=0.17.0-rc.22" +pkcs1 = "=0.8.0-rc.4" +primeorder = "=0.14.0-rc.14" +rustcrypto-ff = "=0.14.0-rc.1" +rustcrypto-ff_derive = "=0.14.0-rc.0" +rustcrypto-group = "=0.14.0-rc.1" +curve25519-dalek = "=5.0.0-rc.1" + +[dev-dependencies] +pretty_assertions = "1.4" +cfg-if = "1.0" +rand_chacha = "0.10" +ring = "0.17" +rstest = "0.26" +expect-test = "1" +tempfile = "3.22" +picky-test-data = { path = "../picky-test-data", version = "0.1" } + +[features] +default = ["x509", "jose", "http_signature", "http_trait_impl", "pkcs12"] + +# main features +x509 = [] +jose = ["dep:serde_json", "dep:aes-gcm", "dep:ctr", "dep:cbc", "dep:aes-kw", "dep:aes"] +http_signature = [] +pkcs12 = ["picky-asn1-x509/pkcs12", "dep:des", "dep:rc2", "dep:cbc", "dep:hmac", "dep:aes", "x509", "dep:pbkdf2"] + +# secondary features +pkcs7 = ["x509", "picky-asn1-x509/pkcs7"] +http_timestamp = ["dep:reqwest"] +ctl = ["picky-asn1-x509/ctl", "pkcs7", "chrono_conversion"] +ctl_http_fetch = ["dep:reqwest", "dep:cab", "ctl"] +wincert = ["x509", "dep:byteorder"] +ssh = ["dep:byteorder", "dep:aes", "dep:ctr", "dep:cbc", "dep:bcrypt-pbkdf", "dep:lexical-sort", "dep:hmac", "dep:pbkdf2"] +http_trait_impl = ["dep:http"] +chrono_conversion = ["dep:chrono", "picky-asn1/chrono_conversion"] +time_conversion = ["dep:time", "picky-asn1/time_conversion"] +putty = ["dep:argon2", "dep:hmac", "ssh", + # Transitive dependencies + "dep:blake2" +] + +[package.metadata.docs.rs] +# Enable all features when building documentation for docs.rs +all-features = true diff --git a/rust/vendor/picky/LICENSE-APACHE b/rust/vendor/picky/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/rust/vendor/picky/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/rust/vendor/picky/LICENSE-MIT b/rust/vendor/picky/LICENSE-MIT new file mode 100644 index 0000000..31aa793 --- /dev/null +++ b/rust/vendor/picky/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/rust/vendor/picky/README.md b/rust/vendor/picky/README.md new file mode 100644 index 0000000..99c9088 --- /dev/null +++ b/rust/vendor/picky/README.md @@ -0,0 +1,22 @@ +[![Crates.io](https://img.shields.io/crates/v/picky.svg)](https://crates.io/crates/picky) +[![docs.rs](https://docs.rs/picky/badge.svg)](https://docs.rs/picky) +![Crates.io](https://img.shields.io/crates/l/picky) + +Compatible with rustc 1.85. +Minimal rustc version bumps happen [only with minor number bumps in this project](https://github.com/Devolutions/picky-rs/issues/89#issuecomment-868303478). + +# picky + +Portable X.509, PKI, JOSE and HTTP signature implementation. + +## X.509 / PKI + +[See doc](https://docs.rs/picky/latest/picky/x509/index.html) for tested examples. + +## HTTP signature + +[See doc](https://docs.rs/picky/latest/picky/http/index.html) for tested examples. + +## JOSE + +Doc doesn't have example yet, but [tests](https://github.com/Devolutions/picky-rs/blob/master/picky/src/jose/jwt.rs#L438) are good reference. diff --git a/rust/vendor/picky/src/hash.rs b/rust/vendor/picky/src/hash.rs new file mode 100644 index 0000000..04af2a9 --- /dev/null +++ b/rust/vendor/picky/src/hash.rs @@ -0,0 +1,101 @@ +//! Hash algorithms supported by picky + +use digest::Digest; +use picky_asn1_x509::ShaVariant; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt; + +/// unsupported algorithm +#[derive(Debug)] +pub struct UnsupportedHashAlgorithmError { + pub algorithm: String, +} + +impl fmt::Display for UnsupportedHashAlgorithmError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "unsupported algorithm: {}", self.algorithm) + } +} + +impl Error for UnsupportedHashAlgorithmError {} + +/// Supported hash algorithms +#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum HashAlgorithm { + MD5, + SHA1, + SHA2_224, + SHA2_256, + SHA2_384, + SHA2_512, + SHA3_384, + SHA3_512, +} + +impl TryFrom for ShaVariant { + type Error = UnsupportedHashAlgorithmError; + + fn try_from(v: HashAlgorithm) -> Result { + match v { + HashAlgorithm::MD5 => Ok(ShaVariant::MD5), + HashAlgorithm::SHA1 => Ok(ShaVariant::SHA1), + HashAlgorithm::SHA2_256 => Ok(ShaVariant::SHA2_256), + HashAlgorithm::SHA2_384 => Ok(ShaVariant::SHA2_384), + HashAlgorithm::SHA2_512 => Ok(ShaVariant::SHA2_512), + HashAlgorithm::SHA3_384 => Ok(ShaVariant::SHA3_384), + HashAlgorithm::SHA3_512 => Ok(ShaVariant::SHA3_512), + _ => Err(UnsupportedHashAlgorithmError { + algorithm: format!("{v:?}"), + }), + } + } +} + +impl TryFrom for HashAlgorithm { + type Error = UnsupportedHashAlgorithmError; + + fn try_from(v: ShaVariant) -> Result { + match v { + ShaVariant::MD5 => Ok(HashAlgorithm::MD5), + ShaVariant::SHA1 => Ok(HashAlgorithm::SHA1), + ShaVariant::SHA2_256 => Ok(HashAlgorithm::SHA2_256), + ShaVariant::SHA2_384 => Ok(HashAlgorithm::SHA2_384), + ShaVariant::SHA2_512 => Ok(HashAlgorithm::SHA2_512), + ShaVariant::SHA3_384 => Ok(HashAlgorithm::SHA3_384), + ShaVariant::SHA3_512 => Ok(HashAlgorithm::SHA3_512), + _ => Err(UnsupportedHashAlgorithmError { + algorithm: format!("{v:?}"), + }), + } + } +} + +impl HashAlgorithm { + pub fn digest(self, msg: &[u8]) -> Vec { + match self { + Self::MD5 => md5::Md5::digest(msg).as_slice().to_vec(), + Self::SHA1 => sha1::Sha1::digest(msg).as_slice().to_vec(), + Self::SHA2_224 => sha2::Sha224::digest(msg).as_slice().to_vec(), + Self::SHA2_256 => sha2::Sha256::digest(msg).as_slice().to_vec(), + Self::SHA2_384 => sha2::Sha384::digest(msg).as_slice().to_vec(), + Self::SHA2_512 => sha2::Sha512::digest(msg).as_slice().to_vec(), + Self::SHA3_384 => sha3::Sha3_384::digest(msg).as_slice().to_vec(), + Self::SHA3_512 => sha3::Sha3_512::digest(msg).as_slice().to_vec(), + } + } + + pub fn output_size(self) -> usize { + match self { + Self::MD5 => md5::Md5::output_size(), + Self::SHA1 => sha1::Sha1::output_size(), + Self::SHA2_224 => sha2::Sha224::output_size(), + Self::SHA2_256 => sha2::Sha256::output_size(), + Self::SHA2_384 => sha2::Sha384::output_size(), + Self::SHA2_512 => sha2::Sha512::output_size(), + Self::SHA3_384 => sha3::Sha3_384::output_size(), + Self::SHA3_512 => sha3::Sha3_512::output_size(), + } + } +} diff --git a/rust/vendor/picky/src/http/http_request.rs b/rust/vendor/picky/src/http/http_request.rs new file mode 100644 index 0000000..7c09ed8 --- /dev/null +++ b/rust/vendor/picky/src/http/http_request.rs @@ -0,0 +1,150 @@ +use std::borrow::Cow; +use thiserror::Error; + +#[derive(Debug, Error, Clone)] +#[non_exhaustive] +pub enum HttpRequestError { + /// couldn't convert a http header value to string + #[error("couldn't convert http header value to string for header key {key}")] + HeaderValueToStr { key: String }, + + /// unexpected error occurred + #[error("unexpected error: {reason}")] + Unexpected { reason: String }, +} + +pub trait HttpRequest { + fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result, HttpRequestError>; + fn get_lowercased_method(&self) -> Result, HttpRequestError>; + fn get_target(&self) -> Result, HttpRequestError>; +} + +#[cfg(feature = "http_trait_impl")] +mod http_trait_impl { + use super::*; + + impl HttpRequest for http::request::Parts { + fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result, HttpRequestError> { + let mut values = Vec::new(); + let all_values = self.headers.get_all(header_name); + for value in all_values { + let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr { + key: header_name.to_owned(), + })?; + values.push(value_str.trim()); + } + Ok(Cow::Owned(values.join(", "))) + } + + fn get_lowercased_method(&self) -> Result, HttpRequestError> { + Ok(Cow::Owned(self.method.as_str().to_lowercase())) + } + + fn get_target(&self) -> Result, HttpRequestError> { + Ok(Cow::Borrowed(self.uri.path())) + } + } + impl HttpRequest for http::request::Request { + fn get_header_concatenated_values<'a>(&'a self, header_name: &str) -> Result, HttpRequestError> { + let mut values = Vec::new(); + let all_values = self.headers().get_all(header_name); + for value in all_values { + let value_str = value.to_str().map_err(|_| HttpRequestError::HeaderValueToStr { + key: header_name.to_owned(), + })?; + values.push(value_str.trim()); + } + Ok(Cow::Owned(values.join(", "))) + } + + fn get_lowercased_method(&self) -> Result, HttpRequestError> { + Ok(Cow::Owned(self.method().as_str().to_lowercase())) + } + + fn get_target(&self) -> Result, HttpRequestError> { + Ok(Cow::Borrowed(self.uri().path())) + } + } + + #[cfg(test)] + mod tests { + use super::*; + use http::method::Method; + use http::{header, request}; + + #[test] + fn http_request_parts() { + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header("Host", "example.org") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .header("X-Example", " Example header with some whitespace. ") + .header("X-EmptyHeader", "") + .header(header::CACHE_CONTROL, "max-age=60") + .header(header::CACHE_CONTROL, "must-revalidate") + .body(()) + .expect("couldn't build request"); + + let (parts, _) = req.into_parts(); + + assert_eq!(parts.get_target().expect("target"), "/foo"); + assert_eq!(parts.get_lowercased_method().expect("method"), "get"); + assert_eq!( + parts.get_header_concatenated_values("host").expect("host"), + "example.org" + ); + assert_eq!( + parts.get_header_concatenated_values("date").expect("date"), + "Tue, 07 Jun 2014 20:51:35 GMT" + ); + assert_eq!( + parts.get_header_concatenated_values("x-example").expect("example"), + "Example header with some whitespace." + ); + assert_eq!( + parts.get_header_concatenated_values("X-EmptyHeader").expect("empty"), + "" + ); + assert_eq!( + parts + .get_header_concatenated_values(header::CACHE_CONTROL.as_str()) + .expect("cache control"), + "max-age=60, must-revalidate" + ); + } + + #[test] + fn http_request_request() { + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header("Host", "example.org") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .header("X-Example", " Example header with some whitespace. ") + .header("X-EmptyHeader", "") + .header(header::CACHE_CONTROL, "max-age=60") + .header(header::CACHE_CONTROL, "must-revalidate") + .body(()) + .expect("couldn't build request"); + + assert_eq!(req.get_target().expect("target"), "/foo"); + assert_eq!(req.get_lowercased_method().expect("method"), "get"); + assert_eq!(req.get_header_concatenated_values("host").expect("host"), "example.org"); + assert_eq!( + req.get_header_concatenated_values("date").expect("date"), + "Tue, 07 Jun 2014 20:51:35 GMT" + ); + assert_eq!( + req.get_header_concatenated_values("x-example").expect("example"), + "Example header with some whitespace." + ); + assert_eq!(req.get_header_concatenated_values("X-EmptyHeader").expect("empty"), ""); + assert_eq!( + req.get_header_concatenated_values(header::CACHE_CONTROL.as_str()) + .expect("cache control"), + "max-age=60, must-revalidate" + ); + } + } +} diff --git a/rust/vendor/picky/src/http/http_signature.rs b/rust/vendor/picky/src/http/http_signature.rs new file mode 100644 index 0000000..21c3f3b --- /dev/null +++ b/rust/vendor/picky/src/http/http_signature.rs @@ -0,0 +1,1313 @@ +use crate::hash::HashAlgorithm; +use crate::http::http_request::{HttpRequest, HttpRequestError}; +use crate::key::{PrivateKey, PublicKey}; +use crate::signature::{SignatureAlgorithm, SignatureError}; +use base64::engine::general_purpose; +use base64::{DecodeError, Engine as _}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt::{self, Debug}; +use std::str::FromStr; +use thiserror::Error; + +// === error type === // + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum HttpSignatureError { + /// couldn't decode base64 + #[error("couldn't decode base64: {source}")] + Base64Decoding { source: DecodeError }, + + /// signature is not yet valid + #[error("signature is not yet valid (created: {created}, now: {now})")] + NotYetValid { created: u64, now: u64 }, + + /// signature expired + #[error("signature expired (not after: {not_after}, now: {now})")] + Expired { not_after: u64, now: u64 }, + + /// signature error occurred + #[error("signature error: {source}")] + Signature { source: SignatureError }, + + /// couldn't generate signing string + #[error("couldn't generate signing string: {source}")] + SigningStringGeneration { source: HttpRequestError }, + + /// invalid signing string + #[error("signing string invalid for line `{line}`")] + InvalidSigningString { line: String }, + + /// missing required builder argument + #[error("missing required builder argument `{arg}`")] + MissingBuilderArgument { arg: &'static str }, + + /// builder requires a non empty `headers` parameter + #[error("builder requires a non empty `headers` parameter")] + BuilderEmptyHeaders, + + /// `headers` parameter shouldn't be provided when using builder with a pre-generated signing string + #[error("`headers` parameter shouldn't be provided when using builder with a pre-generated signing string")] + BuilderHeadersProvidedWithPreGenerated, + + /// required parameter is missing from http signature string + #[error("required parameter is missing from http signature string: {parameter}")] + MissingRequiredParameter { parameter: &'static str }, + + /// a parameter is present but invalid + #[error("invalid parameter: {parameter}")] + InvalidParameter { parameter: &'static str }, + + /// incompatible 'algorithm' parameter with provided signature verification method + #[error("incompatible 'algorithm' parameter: {value:?}")] + IncompatibleAlgorithm { value: SignatureAlgorithm }, +} + +impl From for HttpSignatureError { + fn from(e: DecodeError) -> Self { + Self::Base64Decoding { source: e } + } +} + +impl From for HttpSignatureError { + fn from(e: SignatureError) -> Self { + Self::Signature { source: e } + } +} + +impl From for HttpSignatureError { + fn from(e: HttpRequestError) -> Self { + Self::SigningStringGeneration { source: e } + } +} + +// === header parameter === + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum Header { + /// Lowercased HTTP header field name + Name(String), + /// Special `(request-target)` header field + RequestTarget, + /// Special `(created)` header field + Created, + /// Special `(expires)` header field + Expires, +} + +impl Header { + pub const REQUEST_TARGET_STR: &'static str = "(request-target)"; + pub const CREATED_STR: &'static str = "(created)"; + pub const EXPIRES_STR: &'static str = "(expires)"; + + pub fn new_name(mut name: String) -> Self { + name.make_ascii_lowercase(); + Self::Name(name) + } + + pub fn as_str(&self) -> &str { + match self { + Header::Name(header_name) => header_name.as_str(), + Header::RequestTarget => Self::REQUEST_TARGET_STR, + Header::Created => Self::CREATED_STR, + Header::Expires => Self::EXPIRES_STR, + } + } +} + +impl fmt::Display for Header { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl From<&str> for Header { + fn from(s: &str) -> Self { + match s { + Self::REQUEST_TARGET_STR => Self::RequestTarget, + Self::CREATED_STR => Self::Created, + Self::EXPIRES_STR => Self::Expires, + _ => Self::new_name(s.to_owned()), + } + } +} + +// === signature algorithm === // + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum HttpSigAlgorithm { + Known(SignatureAlgorithm), + Custom(String), +} + +impl HttpSigAlgorithm { + pub fn as_known(&self) -> Option { + if let Self::Known(algo) = self { + Some(*algo) + } else { + None + } + } + + pub fn is_known(&self) -> bool { + self.as_known().is_some() + } + + pub fn as_custom(&self) -> Option<&str> { + if let Self::Custom(name) = self { + Some(name.as_str()) + } else { + None + } + } + + pub fn is_custom(&self) -> bool { + self.as_custom().is_some() + } + + pub fn as_str(&self) -> &str { + match self { + Self::Known(algo) => to_http_sig_algo_str(*algo), + Self::Custom(name) => name, + } + } +} + +// === http signature === + +/// Contains signature parameters. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct HttpSignature { + /// An opaque string that the server can + /// use to look up the component they need to validate the signature. + pub key_id: String, + + /// In original string format, `headers` should be a lowercased, quoted list of HTTP header + /// fields, separated by a single space character. + /// + /// For instance : `(request-target) (created) host date cache-control x-emptyheader x-example`. + pub headers: Vec
, + + /// The `created` field expresses when the signature was + /// created. The value MUST be a Unix timestamp integer value. A + /// signature with a `created` timestamp value that is in the future MUST + /// NOT be processed. + pub created: Option, + + /// The `expires` field expresses when the signature ceases to + /// be valid. The value MUST be a Unix timestamp integer value. A + /// signature with an `expires` timestamp value that is in the past MUST + /// NOT be processed. + pub expires: Option, + + /// Base 64 encoded digital signature, as described in RFC4648, Section 4. The + /// client uses the `algorithm` and `headers` signature parameters to + /// form a canonicalized `signing string`. This `signing string` is then + /// signed with the key associated with `key_id` and the algorithm + /// corresponding to `algorithm`. The `signature` parameter is then set + /// to the base 64 encoding of the signature. + pub signature: String, + + /// Used to specify the signature string construction mechanism. + /// Implementers SHOULD derive the digital signature algorithm used by an implementation from + /// the key metadata identified by the `keyId` rather than from this field. If `algorithm` + /// is provided and differs from the key metadata identified by the `keyId`, for example + /// `rsa-sha256` but an EdDSA key is identified via `keyId`, then an implementation + /// MUST produce an error. + /// Note: as of draft 12 there is only one signature string construction mechanism. As such + /// this parameter is only used to hint the digital signature algorithm. + pub algorithm: Option, + + legacy: bool, +} + +impl HttpSignature { + pub fn verifier(&self) -> HttpSignatureVerifier<'_> { + HttpSignatureVerifier { + http_signature: self, + inner: Default::default(), + } + } + + pub fn to_signing_string(&self) -> String { + let mut acc = Vec::with_capacity(5); + + if self.legacy { + acc.push(format!( + "{} {}={}", + HTTP_SIGNATURE_HEADER, HTTP_SIGNATURE_KEY_ID, self.key_id + )); + } else { + acc.push(format!( + "{} {}=\"{}\"", + HTTP_SIGNATURE_HEADER, HTTP_SIGNATURE_KEY_ID, self.key_id + )); + + match &self.algorithm { + Some(HttpSigAlgorithm::Custom(algorithm_name)) => { + acc.push(format!("{HTTP_SIGNATURE_ALGORITHM}=\"{algorithm_name}\"")); + } + Some(HttpSigAlgorithm::Known(algorithm)) => { + acc.push(format!( + "{}=\"{}\"", + HTTP_SIGNATURE_ALGORITHM, + to_http_sig_algo_str(*algorithm) + )); + } + None => {} + } + } + + if let Some(created) = self.created { + acc.push(format!("{HTTP_SIGNATURE_CREATED}={created}")); + } + + if let Some(expires) = self.expires { + acc.push(format!("{HTTP_SIGNATURE_EXPIRES}={expires}")); + } + + if self.legacy { + acc.push(format!( + "{}={}", + HTTP_SIGNATURE_HEADERS, + self.headers + .iter() + .map(|header| header.as_str()) + .collect::>() + .join(" "), + )); + + acc.push(format!("{}={}", HTTP_SIGNATURE_SIGNATURE, self.signature)); + } else { + acc.push(format!( + "{}=\"{}\"", + HTTP_SIGNATURE_HEADERS, + self.headers + .iter() + .map(|header| header.as_str()) + .collect::>() + .join(" "), + )); + + acc.push(format!("{}=\"{}\"", HTTP_SIGNATURE_SIGNATURE, self.signature)); + } + + acc.join(",") + } +} + +const HTTP_SIGNATURE_HEADER: &str = "Signature"; +const HTTP_SIGNATURE_KEY_ID: &str = "keyId"; +const HTTP_SIGNATURE_SIGNATURE: &str = "signature"; +const HTTP_SIGNATURE_CREATED: &str = "created"; +const HTTP_SIGNATURE_EXPIRES: &str = "expires"; +const HTTP_SIGNATURE_HEADERS: &str = "headers"; +const HTTP_SIGNATURE_ALGORITHM: &str = "algorithm"; + +impl fmt::Display for HttpSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_signing_string()) + } +} + +impl FromStr for HttpSignature { + type Err = HttpSignatureError; + + fn from_str(http_authorization_header: &str) -> Result { + let items = http_authorization_header + .trim_start_matches(HTTP_SIGNATURE_HEADER) + .split(',') + .collect::>(); + let mut keys = HashMap::new(); + for item in items { + if let Some(index) = item.find('=') { + let (key, value) = item.split_at(index); + let value = value[1..].trim().trim_matches('"'); + keys.insert(key.trim(), value.trim().to_owned()); + } + } + + let headers = { + if let Some(headers_str) = keys.remove(HTTP_SIGNATURE_HEADERS) { + let headers_str_vec = headers_str.split(' ').collect::>(); + let mut headers = Vec::with_capacity(headers_str_vec.len()); + for header_str in headers_str_vec { + headers.push(Header::from(header_str)); + } + headers + } else { + vec![] + } + }; + + let created = if let Some(created) = keys.remove(HTTP_SIGNATURE_CREATED) { + Some( + created + .parse::() + .map_err(|_| HttpSignatureError::InvalidParameter { + parameter: HTTP_SIGNATURE_CREATED, + })?, + ) + } else { + None + }; + + let expires = if let Some(created) = keys.remove(HTTP_SIGNATURE_EXPIRES) { + Some( + created + .parse::() + .map_err(|_| HttpSignatureError::InvalidParameter { + parameter: HTTP_SIGNATURE_EXPIRES, + })?, + ) + } else { + None + }; + + let algorithm = keys.remove(HTTP_SIGNATURE_ALGORITHM).map(|val| { + if let Some(algo) = from_http_sig_algo_str(&val) { + HttpSigAlgorithm::Known(algo) + } else { + HttpSigAlgorithm::Custom(val) + } + }); + + let signature = keys + .remove(HTTP_SIGNATURE_SIGNATURE) + .ok_or(HttpSignatureError::MissingRequiredParameter { + parameter: HTTP_SIGNATURE_SIGNATURE, + })?; + + let legacy = !signature.contains(['/', '+']); + + Ok(HttpSignature { + key_id: keys + .remove(HTTP_SIGNATURE_KEY_ID) + .ok_or(HttpSignatureError::MissingRequiredParameter { + parameter: HTTP_SIGNATURE_KEY_ID, + })?, + headers, + created, + expires, + signature, + algorithm, + legacy, + }) + } +} + +// === http signature builder === // + +macro_rules! builder_argument_missing_err { + ($field:ident) => {{ + const _: fn() = || { + let HttpSignatureBuilderInner { $field: _, .. }; + }; + + HttpSignatureError::MissingBuilderArgument { + arg: stringify!($field), + } + }}; +} + +#[derive(Clone)] +enum SigningStringGenMethod<'a> { + PreGenerated(&'a str), + FromHttpRequest(&'a dyn HttpRequest), +} + +impl Debug for SigningStringGenMethod<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SigningStringGenMethod::")?; + match self { + SigningStringGenMethod::PreGenerated(signing_string) => write!(f, "PreGenerated({signing_string})"), + SigningStringGenMethod::FromHttpRequest(_) => write!(f, "FromHttpRequest(...)"), + } + } +} + +#[derive(Default, Clone, Debug)] +struct HttpSignatureBuilderInner<'a> { + key_id: Option, + signature_method: Option<(&'a PrivateKey, SignatureAlgorithm)>, + created: Option, + expires: Option, + headers: Vec
, + signing_string_generation: Option>, + legacy: bool, +} + +#[derive(Default, Clone, Debug)] +/// Utility to generate `HttpSignature`s +pub struct HttpSignatureBuilder<'a> { + inner: RefCell>, +} + +impl<'a> HttpSignatureBuilder<'a> { + pub fn new() -> Self { + Self::default() + } + + #[inline] + /// Required + pub fn key_id>(&self, key_id: S) -> &Self { + self.inner.borrow_mut().key_id = Some(key_id.into()); + self + } + + #[inline] + /// Required + pub fn signature_method(&self, private_key: &'a PrivateKey, signature_type: SignatureAlgorithm) -> &Self { + self.inner.borrow_mut().signature_method = Some((private_key, signature_type)); + self + } + + #[inline] + /// If generating signing string, at least one of `created`, `expires`, `request_target` + /// or `http_header` is required otherwise DO NOT provide. + pub fn created(&self, unix_timestamp: u64) -> &Self { + let mut inner_mut = self.inner.borrow_mut(); + inner_mut.created = Some(unix_timestamp); + inner_mut.headers.push(Header::Created); + drop(inner_mut); + self + } + + #[inline] + /// If generating signing string, at least one of `created`, `expires`, `request_target` + /// or `http_header` is required otherwise DO NOT provide. + pub fn expires(&self, unix_timestamp: u64) -> &Self { + let mut inner_mut = self.inner.borrow_mut(); + inner_mut.expires = Some(unix_timestamp); + inner_mut.headers.push(Header::Expires); + drop(inner_mut); + self + } + + #[inline] + /// If generating signing string, at least one of `created`, `expires`, `request_target` + /// or `http_header` is required otherwise DO NOT provide. + pub fn request_target(&self) -> &Self { + self.inner.borrow_mut().headers.push(Header::RequestTarget); + self + } + + #[inline] + /// If generating signing string, at least one of `created`, `expires`, `request_target` + /// or `http_header` is required otherwise DO NOT provide. + pub fn http_header>(&self, header: S) -> &Self { + self.inner.borrow_mut().headers.push(Header::new_name(header.into())); + self + } + + #[inline] + /// Required (alternative: `pre_generated_signing_string`). + pub fn generate_signing_string_using_http_request(&self, http_request: &'a dyn HttpRequest) -> &Self { + self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::FromHttpRequest(http_request)); + self + } + + #[inline] + /// Required (alternative: `generate_signing_string_using_http_request`). + pub fn pre_generated_signing_string(&self, signing_string: &'a str) -> &Self { + self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::PreGenerated(signing_string)); + self + } + + #[inline] + #[doc(hidden)] + pub fn legacy(&self) -> &Self { + self.inner.borrow_mut().legacy = true; + self + } + + pub fn build(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + + let (private_key, signature_type) = { + inner + .signature_method + .take() + .ok_or(builder_argument_missing_err!(signature_method))? + }; + let key_id = inner.key_id.take().ok_or(builder_argument_missing_err!(key_id))?; + + let signing_string_generation = inner + .signing_string_generation + .take() + .ok_or(builder_argument_missing_err!(signing_string_generation))?; + + let mut created = inner.created.take(); + let mut expires = inner.expires.take(); + let mut headers: Vec
= inner.headers.drain(..).collect(); + let legacy = inner.legacy; + + drop(inner); + + let signature_binary = + match signing_string_generation { + SigningStringGenMethod::PreGenerated(signing_string) => { + if !headers.is_empty() { + return Err(HttpSignatureError::BuilderHeadersProvidedWithPreGenerated); + } + + // parse pre-generated signing string to fill our HttpSignature struct properly. + + for line in signing_string.lines() { + let mut split = line.split(':'); + let key = split.next().expect("there is always at least one element in the split"); + if let Some(value) = split.next() { + match key { + Header::REQUEST_TARGET_STR => { + headers.push(Header::RequestTarget); + } + Header::CREATED_STR => { + headers.push(Header::Created); + created = Some(value.trim().parse().map_err(|_| { + HttpSignatureError::InvalidSigningString { line: line.to_owned() } + })?); + } + Header::EXPIRES_STR => { + headers.push(Header::Expires); + expires = Some(value.trim().parse().map_err(|_| { + HttpSignatureError::InvalidSigningString { line: line.to_owned() } + })?); + } + header_name => headers.push(Header::new_name(header_name.to_owned())), + } + } else { + return Err(HttpSignatureError::InvalidSigningString { line: line.to_owned() }); + } + } + + signature_type.sign(signing_string.as_bytes(), private_key)? + } + SigningStringGenMethod::FromHttpRequest(http_request) => { + // Generate signing string. + // See https://tools.ietf.org/html/draft-cavage-http-signatures-12#section-2.3 + + if headers.is_empty() { + return Err(HttpSignatureError::BuilderEmptyHeaders); + } + + let mut acc = Vec::with_capacity(headers.len()); + for header in &headers { + match header { + Header::Name(header_name) => { + let concatenated_values = http_request.get_header_concatenated_values(header_name)?; + if concatenated_values.is_empty() { + acc.push(format!("{}:", header_name.as_str())); + } else { + acc.push(format!("{}: {}", header_name.as_str(), concatenated_values)); + } + } + Header::RequestTarget => { + acc.push(format!( + "{}: {} {}", + header.as_str(), + http_request.get_lowercased_method()?, + http_request.get_target()? + )); + } + Header::Created => acc.push(format!( + "{}: {}", + header.as_str(), + created.expect("Some by builder construction") + )), + Header::Expires => acc.push(format!( + "{}: {}", + header.as_str(), + expires.expect("Some by builder construction") + )), + } + } + + let signing_string = acc.join("\n"); + + signature_type.sign(signing_string.as_bytes(), private_key)? + } + }; + + Ok(HttpSignature { + key_id, + headers, + created, + expires, + signature: if legacy { + general_purpose::URL_SAFE_NO_PAD.encode(&signature_binary) + } else { + general_purpose::STANDARD.encode(&signature_binary) + }, + algorithm: Some(HttpSigAlgorithm::Known(signature_type)), + legacy, + }) + } +} + +// === http signature verifier === // + +macro_rules! verifier_argument_missing_err { + ($field:ident) => {{ + const _: fn() = || { + let HttpSignatureVerifierInner { $field: _, .. }; + }; + + HttpSignatureError::MissingBuilderArgument { + arg: stringify!($field), + } + }}; +} + +#[derive(Default, Clone, Debug)] +struct HttpSignatureVerifierInner<'a> { + now: Option, + leeway: u64, + signature_method: Option<(&'a PublicKey, SignatureAlgorithm)>, + signing_string_generation: Option>, +} + +#[derive(Clone, Debug)] +/// Utility to verify `HttpSignature`s +pub struct HttpSignatureVerifier<'a> { + http_signature: &'a HttpSignature, + inner: RefCell>, +} + +impl<'a> HttpSignatureVerifier<'a> { + #[inline] + /// Optional. Required only if http signature contains (expires) or (created) parameters. + pub fn now(&self, unix_timestamp: u64) -> &Self { + self.inner.borrow_mut().now = Some(unix_timestamp); + self + } + + #[inline] + /// Optional. Add leeway to check expiration and creation times. + pub fn leeway(&self, leeway: u64) -> &Self { + self.inner.borrow_mut().leeway = leeway; + self + } + + #[inline] + /// Required + pub fn signature_method(&self, public_key: &'a PublicKey, signature_type: SignatureAlgorithm) -> &Self { + self.inner.borrow_mut().signature_method = Some((public_key, signature_type)); + self + } + + #[inline] + /// Required (alternative: `pre_generated_signing_string`). + pub fn generate_signing_string_using_http_request(&self, http_request: &'a dyn HttpRequest) -> &Self { + self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::FromHttpRequest(http_request)); + self + } + + #[inline] + /// Required (alternative: `generate_signing_string_using_http_request`). + pub fn pre_generated_signing_string(&self, signing_string: &'a str) -> &Self { + self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::PreGenerated(signing_string)); + self + } + + pub fn verify(&self) -> Result<(), HttpSignatureError> { + let mut inner = self.inner.borrow_mut(); + + let (public_key, signature_type) = { + inner + .signature_method + .take() + .ok_or(verifier_argument_missing_err!(signature_method))? + }; + + // Sanity checks based on optional http signature parameter "algorithm" + if let Some(HttpSigAlgorithm::Known(http_sig_algo)) = self.http_signature.algorithm { + if http_sig_algo != signature_type || !is_algo_compatible_with_key(http_sig_algo, public_key) { + return Err(HttpSignatureError::IncompatibleAlgorithm { value: http_sig_algo }); + } + } + + let signing_string_generation = inner + .signing_string_generation + .take() + .ok_or(verifier_argument_missing_err!(signing_string_generation))?; + + if let Some(expires) = self.http_signature.expires { + let now = inner.now.ok_or(verifier_argument_missing_err!(now))?; + if now - inner.leeway > expires { + return Err(HttpSignatureError::Expired { + not_after: expires, + now, + }); + } + } + + if let Some(created) = self.http_signature.created { + let now = inner.now.ok_or(verifier_argument_missing_err!(now))?; + if now + inner.leeway < created { + return Err(HttpSignatureError::NotYetValid { created, now }); + } + } + + drop(inner); + + let signing_string = match signing_string_generation { + SigningStringGenMethod::PreGenerated(signing_string) => Cow::Borrowed(signing_string), + SigningStringGenMethod::FromHttpRequest(http_request) => { + let headers = if self.http_signature.headers.is_empty() { + &[Header::Created][..] + } else { + self.http_signature.headers.as_slice() + }; + + let mut acc = Vec::with_capacity(headers.len()); + for header in headers { + match header { + Header::Name(header_name) => { + let concatenated_values = http_request.get_header_concatenated_values(header_name)?; + if concatenated_values.is_empty() { + acc.push(format!("{}:", header_name.as_str())); + } else { + acc.push(format!("{}: {}", header_name.as_str(), concatenated_values)); + } + } + Header::RequestTarget => { + acc.push(format!( + "{}: {} {}", + header.as_str(), + http_request.get_lowercased_method()?, + http_request.get_target()? + )); + } + Header::Created => acc.push(format!( + "{}: {}", + header.as_str(), + self.http_signature + .created + .ok_or(HttpSignatureError::MissingRequiredParameter { + parameter: HTTP_SIGNATURE_CREATED, + })? + )), + Header::Expires => acc.push(format!( + "{}: {}", + header.as_str(), + self.http_signature + .expires + .ok_or(HttpSignatureError::MissingRequiredParameter { + parameter: HTTP_SIGNATURE_EXPIRES, + })? + )), + } + } + + Cow::Owned(acc.join("\n")) + } + }; + + let decoded_signature = if self.http_signature.legacy { + general_purpose::URL_SAFE_NO_PAD.decode(&self.http_signature.signature)? + } else { + general_purpose::STANDARD.decode(&self.http_signature.signature)? + }; + + signature_type.verify(public_key, signing_string.as_bytes(), &decoded_signature)?; + + Ok(()) + } +} + +// === http signature algorithms === // +const HTTP_SIG_ALGO_RSA_MD5: &str = "rsa-md5"; +const HTTP_SIG_ALGO_RSA_SHA_1: &str = "rsa-sha1"; + +const HTTP_SIG_ALGO_RSA_SHA_224: &str = "rsa-sha224"; +const HTTP_SIG_ALGO_RSA_SHA_256: &str = "rsa-sha256"; +const HTTP_SIG_ALGO_RSA_SHA_384: &str = "rsa-sha384"; +const HTTP_SIG_ALGO_RSA_SHA_512: &str = "rsa-sha512"; +const HTTP_SIG_ALGO_RSA_SHA2_224: &str = "rsa-sha2-224"; +const HTTP_SIG_ALGO_RSA_SHA2_256: &str = "rsa-sha2-256"; +const HTTP_SIG_ALGO_RSA_SHA2_384: &str = "rsa-sha2-384"; +const HTTP_SIG_ALGO_RSA_SHA2_512: &str = "rsa-sha2-512"; + +const HTTP_SIG_ALGO_RSA_SHA3_384: &str = "rsa-sha3-384"; +const HTTP_SIG_ALGO_RSA_SHA3_512: &str = "rsa-sha3-512"; + +const HTTP_SIG_ALGO_ECDSA_SHA_256: &str = "ecdsa-sha256"; +const HTTP_SIG_ALGO_ECDSA_SHA_384: &str = "ecdsa-sha384"; + +const HTTP_SIG_ALGO_ED25519_SHA512: &str = "ed25519-sha512"; + +fn to_http_sig_algo_str(algo: SignatureAlgorithm) -> &'static str { + match algo { + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => HTTP_SIG_ALGO_RSA_MD5, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) => HTTP_SIG_ALGO_RSA_SHA_1, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) => HTTP_SIG_ALGO_RSA_SHA_224, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_RSA_SHA_256, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_RSA_SHA_384, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => HTTP_SIG_ALGO_RSA_SHA_512, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => HTTP_SIG_ALGO_RSA_SHA3_384, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => HTTP_SIG_ALGO_RSA_SHA3_512, + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_ECDSA_SHA_256, + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_ECDSA_SHA_384, + SignatureAlgorithm::Ed25519 => HTTP_SIG_ALGO_ED25519_SHA512, + SignatureAlgorithm::Ecdsa(_) => "ECDSA unsupported algorithm", + } +} + +fn from_http_sig_algo_str(s: &str) -> Option { + match s { + HTTP_SIG_ALGO_RSA_MD5 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5)), + HTTP_SIG_ALGO_RSA_SHA_1 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1)), + HTTP_SIG_ALGO_RSA_SHA_224 | HTTP_SIG_ALGO_RSA_SHA2_224 => { + Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) + } + HTTP_SIG_ALGO_RSA_SHA_256 | HTTP_SIG_ALGO_RSA_SHA2_256 => { + Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)) + } + HTTP_SIG_ALGO_RSA_SHA_384 | HTTP_SIG_ALGO_RSA_SHA2_384 => { + Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)) + } + HTTP_SIG_ALGO_RSA_SHA_512 | HTTP_SIG_ALGO_RSA_SHA2_512 => { + Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) + } + HTTP_SIG_ALGO_RSA_SHA3_384 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384)), + HTTP_SIG_ALGO_RSA_SHA3_512 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512)), + HTTP_SIG_ALGO_ECDSA_SHA_256 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)), + HTTP_SIG_ALGO_ECDSA_SHA_384 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)), + HTTP_SIG_ALGO_ED25519_SHA512 => Some(SignatureAlgorithm::Ed25519), + _ => None, + } +} + +fn is_algo_compatible_with_key(algo: SignatureAlgorithm, key: &PublicKey) -> bool { + use picky_asn1_x509::oids::*; + + let key_algo = Into::::into(key.as_inner().algorithm.oid()); + match algo { + // Currently, SignatureHashType only contains RSA methods, so this is a an auto-win + _ if key_algo == RSA_ENCRYPTION => true, + + // Otherwise we need to check for specific hash algorithm + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) if key_algo == SHA1_WITH_RSA_ENCRYPTION => true, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) if key_algo == SHA224_WITH_RSA_ENCRYPTION => true, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) if key_algo == SHA256_WITH_RSA_ENCRYPTION => true, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) if key_algo == SHA384_WITH_RSA_ENCRYPTION => true, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) if key_algo == SHA512_WITH_RSA_ENCRYPTION => true, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) if key_algo == ID_RSASSA_PKCS1_V1_5_WITH_SHA3_384 => { + true + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) if key_algo == ID_RSASSA_PKCS1_V1_5_WITH_SHA3_512 => { + true + } + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) if key_algo == ECDSA_WITH_SHA256 => true, + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) if key_algo == ECDSA_WITH_SHA384 => true, + SignatureAlgorithm::Ed25519 if key_algo == ED25519 => true, + + // Key metadata is incompatible with this algorithm + _ => false, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pem::Pem; + use http::method::Method; + use http::{header, request}; + use picky_asn1_x509::{AlgorithmIdentifier, SubjectPublicKeyInfo}; + + const HTTP_SIGNATURE_EXAMPLE: &str = "Signature keyId=\"my-rsa-key\",algorithm=\"rsa-sha256\"\ + ,created=1402170695,headers=\"(request-target) (created) date\",\ + signature=\"bw579lDtTDsp7zif/F7Fy93KXrM6qUfCb43JMJtiL4+3nazIPlxcxVsRJEgZzK/QQPDoeUQ\ + p4BYCzi2CbthYhHJMn/Wv008gNMcQQTuEw/KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIa\ + OHvrvEoQhl3sqpv1z7yCVbQtJHwnPOWoy/11p+SU3X2ARJXN555q5wSn+DykM0Ohq1cXD84MHXP5ulI0Fa8\ + 4zQ5waxoXsieex4FI+zXSlngGmchBPXMUC437u2wXA1zLA4KGUL/uNScL1MKrTMqgV0MK4o6sR0LHOqHmIi\ + MJ7h++UmOW/0Iw74CL2UGQ==\""; + + const HTTP_SIGNATURE_WEIRD_FORMAT: &str = "Signature keyId = my-rsa-key ,created= \"1402170695\",\ + ,algorithm =\"rsa-sha256 \",headers=(request-target) (created) date ,\ + signature=bw579lDtTDsp7zif/F7Fy93KXrM6qUfCb43JMJtiL4+3nazIPlxcxVsRJEgZzK/QQPDoeUQ\ + p4BYCzi2CbthYhHJMn/Wv008gNMcQQTuEw/KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIa\ + OHvrvEoQhl3sqpv1z7yCVbQtJHwnPOWoy/11p+SU3X2ARJXN555q5wSn+DykM0Ohq1cXD84MHXP5ulI0Fa8\ + 4zQ5waxoXsieex4FI+zXSlngGmchBPXMUC437u2wXA1zLA4KGUL/uNScL1MKrTMqgV0MK4o6sR0LHOqHmIi\ + MJ7h++UmOW/0Iw74CL2UGQ=="; + + fn private_key_1() -> PrivateKey { + let pem = picky_test_data::RSA_2048_PK_7.parse::().expect("pem 1"); + PrivateKey::from_pem(&pem).expect("private key 1") + } + + fn private_key_2() -> PrivateKey { + let pem = picky_test_data::RSA_2048_PK_1.parse::().expect("pem 2"); + PrivateKey::from_pem(&pem).expect("private key 2") + } + + #[test] + fn sign() { + let private_key = private_key_1(); + let http_signature_builder = HttpSignatureBuilder::new(); + http_signature_builder + .key_id("my-rsa-key") + .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)) + .request_target() + .created(1402170695) + .http_header("Date"); + + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .header(header::CACHE_CONTROL, "max-age=60") // unused for signature + .header(header::CACHE_CONTROL, "must-revalidate") // unused for signature + .body(()) + .expect("couldn't build request"); + let (parts, _) = req.into_parts(); + + let http_signature = http_signature_builder + .clone() + .generate_signing_string_using_http_request(&parts) + .build() + .expect("couldn't generate http signature"); + let http_signature_str = http_signature.to_signing_string(); + + pretty_assertions::assert_eq!(http_signature_str, HTTP_SIGNATURE_EXAMPLE); + + // changing unused headers should not change signature + + let req_2 = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .header(header::CACHE_CONTROL, "max-age=222") // unused for signature + .body(()) + .expect("couldn't build request"); + let (parts_2, _) = req_2.into_parts(); + + let http_signature_2 = http_signature_builder + .generate_signing_string_using_http_request(&parts_2) + .build() + .expect("couldn't generate http signature 2"); + let http_signature_str_2 = http_signature_2.to_signing_string(); + + pretty_assertions::assert_eq!(http_signature_str_2, http_signature_str); + } + + #[test] + fn verify() { + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .header("something-else", "owowo") // unused for signature + .body(()) + .expect("couldn't build request"); + let (parts, _) = req.into_parts(); + + for http_signature in &[ + HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature example"), + HttpSignature::from_str(HTTP_SIGNATURE_WEIRD_FORMAT).expect("http signature weird format"), + ] { + assert!(!http_signature.legacy); + http_signature + .verifier() + .now(1402170700) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect("couldn't verify"); + } + } + + #[test] + fn invalid_signature_err() { + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .body(()) + .expect("couldn't build request"); + let (parts, _) = req.into_parts(); + + let http_signature = HttpSignatureBuilder::new() + .key_id("my-rsa-key") + .signature_method( + &private_key_1(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .request_target() + .created(1402170695) + .expires(1402170705) + .http_header("Date") + .generate_signing_string_using_http_request(&parts) + .build() + .expect("couldn't generate http signature"); + + let err = http_signature + .verifier() + .now(1402170700) + .signature_method( + &private_key_2().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect_err("verify"); + assert_eq!(err.to_string(), "signature error: invalid signature"); + + let err = http_signature + .verifier() + .now(1402170700) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect_err("verify"); + assert_eq!( + err.to_string(), + "incompatible \'algorithm\' parameter: RsaPkcs1v15(SHA2_256)" + ); + + let err = http_signature + .verifier() + .now(1402170710) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect_err("verify"); + assert_eq!( + err.to_string(), + "signature expired (not after: 1402170705, now: 1402170710)" + ); + + let err = http_signature + .verifier() + .now(1402170600) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect_err("verify"); + assert_eq!( + err.to_string(), + "signature is not yet valid (created: 1402170695, now: 1402170600)" + ); + + let req_2 = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 08 Jun 2014 20:51:35 GMT") + .body(()) + .expect("couldn't build request"); + let (parts_2, _) = req_2.into_parts(); + + let err = http_signature + .verifier() + .now(1402170700) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .generate_signing_string_using_http_request(&parts_2) + .verify() + .expect_err("verify"); + assert_eq!(err.to_string(), "signature error: invalid signature"); + + let mut invalid_algorithm_http_sig = http_signature; + invalid_algorithm_http_sig.algorithm = None; + let err = invalid_algorithm_http_sig + .verifier() + .now(1402170700) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect_err("verify"); + assert_eq!(err.to_string(), "signature error: invalid signature"); + } + + #[test] + fn sign_with_pre_generated_signing_string() { + let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT"; + let http_signature = HttpSignatureBuilder::new() + .key_id("my-rsa-key") + .signature_method( + &private_key_1(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .pre_generated_signing_string(signing_string) + .build() + .expect("couldn't generate http signature using pre-generated signing string"); + let http_signature_str = http_signature.to_signing_string(); + assert_eq!(http_signature_str, HTTP_SIGNATURE_EXAMPLE); + } + + #[test] + fn verify_with_pre_generated_signing_string() { + let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT"; + let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature"); + http_signature + .verifier() + .now(1402170700) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .pre_generated_signing_string(signing_string) + .verify() + .expect("couldn't verify"); + } + + #[test] + fn verify_with_leeway() { + let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT"; + let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature"); + http_signature + .verifier() + .now(1402170690) + .leeway(10) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .pre_generated_signing_string(signing_string) + .verify() + .expect("couldn't verify"); + } + + fn parse_err(http_signature: &str) -> String { + http_signature + .parse::() + .expect_err("no parse error") + .to_string() + } + + #[test] + fn http_signature_parse_err() { + pretty_assertions::assert_eq!( + parse_err("Signature signature=\"some sig\""), + "required parameter is missing from http signature string: keyId" + ); + + pretty_assertions::assert_eq!( + parse_err( + "Signature keyId=\"my-rsa-key\", created=\"HQHQHQ\", \ + signature=\"some sig\"" + ), + "invalid parameter: created" + ); + } + + const HTTP_SIGNATURE_LEGACY: &str = "Signature keyId=my-rsa-key,created=1402170695,\ + headers=(request-target) (created) date,\ + signature=bw579lDtTDsp7zif_F7Fy93KXrM6qUfCb43JMJtiL4-3nazIPlxcxVsRJEgZzK_QQPDoeUQp4B\ + YCzi2CbthYhHJMn_Wv008gNMcQQTuEw_KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIaOHvr\ + vEoQhl3sqpv1z7yCVbQtJHwnPOWoy_11p-SU3X2ARJXN555q5wSn-DykM0Ohq1cXD84MHXP5ulI0Fa84zQ5w\ + axoXsieex4FI-zXSlngGmchBPXMUC437u2wXA1zLA4KGUL_uNScL1MKrTMqgV0MK4o6sR0LHOqHmIiMJ7h--\ + UmOW_0Iw74CL2UGQ"; + + #[test] + fn legacy() { + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .body(()) + .expect("couldn't build request"); + let (parts, _) = req.into_parts(); + + { + // sign + let private_key = private_key_1(); + let http_signature = HttpSignatureBuilder::new() + .key_id("my-rsa-key") + .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)) + .request_target() + .created(1402170695) + .generate_signing_string_using_http_request(&parts) + .http_header("Date") + .legacy() + .build() + .expect("build http signature"); + + pretty_assertions::assert_eq!(http_signature.to_signing_string(), HTTP_SIGNATURE_LEGACY); + } + + { + // verify + let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_LEGACY).expect("http signature legacy"); + http_signature + .verifier() + .now(1402170700) + .signature_method( + &private_key_1().to_public_key().unwrap(), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect("couldn't verify"); + } + } + + #[test] + fn incompatible_algorithm_err() { + let req = request::Builder::new() + .method(Method::GET) + .uri("/foo") + .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") + .body(()) + .expect("couldn't build request"); + let (parts, _) = req.into_parts(); + + let private_key = private_key_1(); + let http_signature = HttpSignatureBuilder::new() + .key_id("my-rsa-key") + .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)) + .request_target() + .created(1402170695) + .generate_signing_string_using_http_request(&parts) + .http_header("Date") + .build() + .expect("build http signature"); + + let mut spki = SubjectPublicKeyInfo::from(private_key_1().to_public_key().unwrap()); + spki.algorithm = AlgorithmIdentifier::new_sha512_with_rsa_encryption(); + let sha512_only_key = PublicKey::from(spki); + + let err = http_signature + .verifier() + .now(1402170700) + .signature_method( + &sha512_only_key, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384), + ) + .generate_signing_string_using_http_request(&parts) + .verify() + .expect_err("verify"); + assert_eq!( + err.to_string(), + "incompatible 'algorithm' parameter: RsaPkcs1v15(SHA2_384)" + ); + } + + const HTTP_SIGNATURE_UNKNOWN_ALGO: &str = "Signature keyId=\"my-rsa-key\",algorithm=\"magical-algo\",\ + headers=\"(request-target)\",signature=\"GARBAGE\""; + + #[test] + fn unknown_algorithms_are_ignored() { + let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_UNKNOWN_ALGO).expect("from str"); + assert_eq!(http_signature.algorithm.unwrap().as_str(), "magical-algo"); + } +} diff --git a/rust/vendor/picky/src/http/mod.rs b/rust/vendor/picky/src/http/mod.rs new file mode 100644 index 0000000..c16e3d3 --- /dev/null +++ b/rust/vendor/picky/src/http/mod.rs @@ -0,0 +1,143 @@ +//! Signing HTTP Messages +//! +//! This module provides an implementation of a subset of +//! [draft-cavage-http-signatures-12 RFC](https://tools.ietf.org/html/draft-cavage-http-signatures-12). +//! +//! # Example +//! ``` +//! use picky::{ +//! http::http_signature::{HttpSignatureBuilder, HttpSignature}, +//! signature::SignatureAlgorithm, +//! hash::HashAlgorithm, +//! key::PrivateKey, +//! pem::parse_pem, +//! }; +//! use http::{request, header::{self, HeaderName}, method::Method}; +//! +//! // all you need to generate a http signature +//! +//! let private_rsa_key: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ +//! MIIEpgIBAAKCAQEApDx0MjvRzmxYXKfqHy0gN1znX6rSU2EnsDTbZaU1UcsMmRNx\n\ +//! L+FqDNzwNutCSQlkujzR37+bHGTxOnRvvSG3lwRvDBZepYWPum9WDqa9T5gTS/Cj\n\ +//! luq/oSsOyt/tUDO/GcNPTTfUQlgtOZ+zRo6FA0rpAQ8CrQm7XzGQ0DMoDU1SVNnu\n\ +//! tFJowlece9Y4NtAfhA+kJ5IEmcE9AgwxJY/iCyCxUEBUe7biwbUafLdtA3+3S8Bu\n\ +//! hBXAr+1BING3qS0vl08+3eaFq5q7f7VwcYOhUmH13itqSGwDznCk4oDQl+qn9DQZ\n\ +//! X9/09KtsgxuIcozxj0RwGKX8qkz4TlAGJw+oNwIDAQABAoIBAQCN9IrimH3iFBfU\n\ +//! Dnb4d4KvF6gNMpMU6pbpYOZ51vBdQEolTX65yfZmI9mlPndOtcXQi51D7lNdmYo/\n\ +//! 4kBqk2giKfzpz7QDEYyHspAJnelnkKStMNPVMBZucc8ZX6+5cOCunfg/YBAhQCHm\n\ +//! +rh0Nd+WVvtKpPTFJ/JCd48Zxf3KcDZD+AsWTjPt4zte8KdcwxiD3MrFunxgeujX\n\ +//! n0U0/f7hvX/7JBQ20gu2tD9whEaS2Gn8E4WpEV8wC6Ah1pU9mZNZ0u8clW9SV0de\n\ +//! ay0mHw8y/Wx6rkEMvrecK6mWbwSQGfRq+crI9PCwA5wn/EZmpQrQs9r5MLtDKVsQ\n\ +//! r9axQrSRAoGBANB53u3mxY9ByiYGT5Ge/33+BjANXxmIPG0TWgV1D8MhwFiaiHF+\n\ +//! tiEzoz4vi23Q+GeHyoM1wxw8VurDX+vcIJbZ0dyGOM/6F0eago7ZAtvHMdUdahAO\n\ +//! X+klqG8kIysgFXSzaU2w76816iIaXiZlDZUghrnd3wmgu9jhl3HCUlltAoGBAMms\n\ +//! 2uufuk26nssF+woQuy017lgLUNFCRrO9F3iwIiyY5R/q372gsx8HVzjYGKY8CF6v\n\ +//! m6JFfxogp44ZcafYOeu+iXbqoCAK4BTdbFB7/D3rX7WgidaxUlLGoXsNFIUIVubR\n\ +//! jaRA7l3tl3fkpdqUAye6zosMKp2oybQLyX5hLAWzAoGBAJKhVUIA8W1cOaFbCPYE\n\ +//! XfExDQsZLI1ZvB5/4O47srVtdMsdDeC93b4mgqfHawr3UvAGm1KEKtIeQofmmP3c\n\ +//! mvNfCvNPWIA3h84uB6wPSKpqRUt+382hPqZOfVSGl1HKxCyL0AH78+lJQ39vCk94\n\ +//! /f+om/n46tnrupPFv+4cXi1VAoGBALzSSmYxtozwHZyYjOJvp9A8nltwvMov82J1\n\ +//! uHQW9OgsftnTXoh83Tg/9zoRmYKK0otUf7L+vnIIANjamb88g35ldu8P3bwicosW\n\ +//! hUMV0qVmqsWy+Vs5yooVzzsWlA+6LyMNMECJSqRGv3pRabesvQeFr7wgOAZE8hTQ\n\ +//! tGbPNBhhAoGBAIkXxIJT0OMKSt/A7wDE9wd3dtC8mbkqr5aZTvwiuD6OvNDdXb/J\n\ +//! i03ns56mIflifVLPYVmCEXdYIzSv7HfeR4d78bAvqiMFfnQ2PF3tuoKMSvQM0m8/\n\ +//! f3VhEFFMrUTTRMX/9PR0ITQtnZlWIDfBVgXPmWqTqCGOMYsRPv70LGse\n\ +//! -----END RSA PRIVATE KEY-----"; +//! let pem = parse_pem(private_rsa_key).expect("couldn't parse pem"); +//! let private_key = PrivateKey::from_pem(&pem).expect("couldn't parse private key"); +//! +//! let req = request::Builder::new() +//! .method(Method::GET) +//! .uri("/foo") +//! .header("Host", "example.org") +//! .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") +//! .header("X-Example", " Example header with some whitespace. ") +//! .header("X-EmptyHeader", "") +//! .header(header::CACHE_CONTROL, "max-age=60") +//! .header(header::CACHE_CONTROL, "must-revalidate") +//! .body(()) +//! .expect("couldn't build request"); +//! +//! let (parts, _) = req.into_parts(); +//! +//! // generate http signature +//! +//! let http_signature = HttpSignatureBuilder::new() +//! .key_id("my-rsa-key") +//! .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) +//! // `picky::http::http_request::HttpRequest` trait is implemented for `http::request::Parts` +//! // for `http` crate with `http_trait_impl` feature gate +//! .generate_signing_string_using_http_request(&parts) +//! .request_target() +//! .created(1402170695) +//! .http_header("host") +//! .http_header(header::DATE.as_str()) +//! .http_header(header::CACHE_CONTROL.as_str()) +//! .http_header("x-emptyheader") +//! .http_header("X-EXAMPLE") +//! .build() +//! .expect("couldn't generate http signature"); +//! +//! let http_signature_str = http_signature.to_string(); +//! +//! assert_eq!( +//! http_signature_str, +//! "Signature keyId=\"my-rsa-key\",algorithm=\"rsa-sha224\",created=1402170695,\ +//! headers=\"(request-target) (created) host date cache-control x-emptyheader x-example\",\ +//! signature=\"JueyecQbV5rQ3TI1EfqZRjAZMMOb4ABZNS0yDcBDgyfbOLORYipT2An2MCH8n/HequVJkEE\ +//! 86/vj9ZFLbyqFkV3a8uQGB6gaE79l9YNdzVeO5k7GBb1jskwBXnqVtGmn8aT2f+cJzkDtu6ptg+UtaU\ +//! ZOQKdutc8aHq1NCLwvqMbA410XP0pA5r/VTbMg/yW8rHguue0Trh0WYCw8zHfNuZtheWxvWGLdYxvC0\ +//! u5oJA0PdxFceqVd/304+RQsrDGLtX8J9vSeqEsQJfvswFyMTkdl1gDbP/YdXp7ADzc2D9IefT9zqvFd\ +//! yEDDXEXKmZm+22395xRtnFmeWXu/+PM6wg==\"" +//! ); +//! +//! // parse a http signature and verify it +//! +//! let parsed_http_signature = http_signature_str.parse::() +//! .expect("couldn't parse http signature"); +//! +//! assert_eq!(parsed_http_signature, http_signature); +//! +//! parsed_http_signature.verifier() +//! .signature_method(&private_key.to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) +//! .generate_signing_string_using_http_request(&parts) +//! .now(1402170695) +//! .verify() +//! .expect("couldn't verify signature"); +//! +//! // alternatively you can provide a pre-generated signing string +//! +//! let signing_string = +//! "(request-target): get /foo\n\ +//! (created): 1402170695\n\ +//! host: example.org\n\ +//! date: Tue, 07 Jun 2014 20:51:35 GMT\n\ +//! cache-control: max-age=60, must-revalidate\n\ +//! x-emptyheader:\n\ +//! x-example: Example header with some whitespace."; +//! +//! let http_signature_pre_generated = HttpSignatureBuilder::new() +//! .key_id("my-rsa-key") +//! .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) +//! .pre_generated_signing_string(signing_string) +//! .build() +//! .expect("couldn't generate http signature using pre-generated signing string"); +//! +//! let http_signature_pre_generated_str = http_signature_pre_generated.to_string(); +//! +//! assert_eq!(http_signature_pre_generated, http_signature); +//! assert_eq!(http_signature_pre_generated_str, http_signature_str); +//! +//! parsed_http_signature.verifier() +//! .signature_method(&private_key.to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) +//! .pre_generated_signing_string(signing_string) +//! .now(1402170695) +//! .verify() +//! .expect("couldn't verify signature using pre-generated signing string"); +//! ``` + +pub mod http_request; +pub mod http_signature; + +pub use http_request::HttpRequest; +pub use http_signature::HttpSignature; diff --git a/rust/vendor/picky/src/jose/jwe.rs b/rust/vendor/picky/src/jose/jwe.rs new file mode 100644 index 0000000..216835e --- /dev/null +++ b/rust/vendor/picky/src/jose/jwe.rs @@ -0,0 +1,1602 @@ +//! JSON Web Encryption (JWE) represents encrypted content using JSON-based data structures. +//! +//! See [RFC7516](https://tools.ietf.org/html/rfc7516). + +use crate::jose::jwk::{Jwk, JwkError}; +use crate::key::ec::{EcComponent, EcdsaKeypair, EcdsaPublicKey, NamedEcCurve}; +use crate::key::ed::{EdKeypair, EdPublicKey, NamedEdAlgorithm, X25519_FIELD_ELEMENT_SIZE}; +use crate::key::{EcCurve, EdAlgorithm, KeyError, PrivateKey, PrivateKeyKind, PublicKey}; + +use aes::cipher::Array; +use aes::cipher::typenum::Unsigned; +use aes_gcm::{AeadInOut, Aes128Gcm, Aes256Gcm, KeyInit, KeySizeUser}; +use aes_kw::AesKw; +use base64::engine::general_purpose; +use base64::{DecodeError, Engine as _}; +use crypto_common::Generate as _; +use rand::rngs::{StdRng, SysRng}; +use rand_core::{Rng as _, SeedableRng as _}; +use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::collections::HashMap; +use thiserror::Error; +use zeroize::Zeroizing; + +type Aes192Gcm = aes_gcm::AesGcm; + +// === error type === // + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum JweError { + /// JWK conversion error + #[error("JWK conversion error")] + Jwk { + #[from] + source: JwkError, + }, + + /// RSA error + #[error("RSA error: {context}")] + Rsa { context: String }, + + /// AES-GCM error (opaque) + #[error("AES-GCM error (opaque)")] + AesGcm, + + /// AES-KW error + #[error("AES-KW error")] + AesKw { source: aes_kw::Error }, + + /// Json error + #[error("JSON error: {source}")] + Json { source: serde_json::Error }, + + /// Key error + #[error("Key error: {source}")] + Key { source: crate::key::KeyError }, + + /// Invalid token encoding + #[error("input isn't a valid token string: {input}")] + InvalidEncoding { input: String }, + + /// Couldn't decode base64 + #[error("couldn't decode base64: {source}")] + Base64Decoding { source: DecodeError }, + + /// Input isn't valid utf8 + #[error("input isn't valid utf8: {source}, input: {input:?}")] + InvalidUtf8 { + source: std::string::FromUtf8Error, + input: Vec, + }, + + /// Unsupported algorithm + #[error("unsupported algorithm: {algorithm}")] + UnsupportedAlgorithm { algorithm: String }, + + /// Invalid size + #[error("invalid size for {ty}: expected {expected}, got {got}")] + InvalidSize { + ty: &'static str, + expected: usize, + got: usize, + }, + + #[error("private and public key algorithms don't match: {context}")] + KeyAlgorithmsMismatch { context: String }, + + #[error("missing `epk` header parameter required for ECDH-ES algorithm")] + MissingEpk, + + #[error("invalid encrypted key size: expected {expected}, got {got}")] + InvalidEncryptedKeySize { expected: usize, got: usize }, + + #[error("invalid decryption key size: expected {expected}, got {got}")] + InvalidDecryptionKeySize { expected: usize, got: usize }, + + #[error(transparent)] + RandError(#[from] rand::rngs::SysError), +} + +impl From for JweError { + fn from(e: rsa::errors::Error) -> Self { + Self::Rsa { context: e.to_string() } + } +} + +impl From for JweError { + fn from(_: aes_gcm::Error) -> Self { + Self::AesGcm + } +} + +impl From for JweError { + fn from(e: serde_json::Error) -> Self { + Self::Json { source: e } + } +} + +impl From for JweError { + fn from(e: crate::key::KeyError) -> Self { + Self::Key { source: e } + } +} + +impl From for JweError { + fn from(e: DecodeError) -> Self { + Self::Base64Decoding { source: e } + } +} + +impl From for JweError { + fn from(e: aes_kw::Error) -> Self { + Self::AesKw { source: e } + } +} + +type KekAes128 = AesKw; +type KekAes192 = AesKw; +type KekAes256 = AesKw; + +// === JWE algorithms === // + +/// `alg` header parameter values for JWE used to determine the Content Encryption Key (CEK) +/// +/// [JSON Web Algorithms (JWA) draft-ietf-jose-json-web-algorithms-40 #4](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-4.1) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum JweAlg { + /// RSAES-PKCS1-V1_5 + /// + /// Recommended- by RFC + #[serde(rename = "RSA1_5")] + RsaPkcs1v15, + + /// RSAES OAEP using default parameters + /// + /// Recommended+ by RFC + #[serde(rename = "RSA-OAEP")] + RsaOaep, + + /// RSAES OAEP using SHA-256 and MGF1 with SHA-256 + #[serde(rename = "RSA-OAEP-256")] + RsaOaep256, + + /// AES Key Wrap with default initial value using 128 bit key (unsupported) + /// + /// Recommended by RFC + #[serde(rename = "A128KW")] + AesKeyWrap128, + + /// AES Key Wrap with default initial value using 192 bit key (unsupported) + #[serde(rename = "A192KW")] + AesKeyWrap192, + + /// AES Key Wrap with default initial value using 256 bit key (unsupported) + /// + /// Recommended by RFC + #[serde(rename = "A256KW")] + AesKeyWrap256, + + /// Direct use of a shared symmetric key as the CEK + #[serde(rename = "dir")] + Direct, + + /// Elliptic Curve Diffie-Hellman Ephemeral Static key agreement using Concat KDF (unsupported) + /// + /// Recommended+ by RFC + #[serde(rename = "ECDH-ES")] + EcdhEs, + + /// ECDH-ES using Concat KDF and CEK wrapped with "A128KW" (unsupported) + /// + /// Recommended by RFC + /// + /// Additional header used: "epk", "apu", "apv" + #[serde(rename = "ECDH-ES+A128KW")] + EcdhEsAesKeyWrap128, + + /// ECDH-ES using Concat KDF and CEK wrapped with "A192KW" (unsupported) + /// + /// Additional header used: "epk", "apu", "apv" + #[serde(rename = "ECDH-ES+A192KW")] + EcdhEsAesKeyWrap192, + + /// ECDH-ES using Concat KDF and CEK wrapped with "A256KW" (unsupported) + /// + /// Recommended by RFC + /// + /// Additional header used: "epk", "apu", "apv" + #[serde(rename = "ECDH-ES+A256KW")] + EcdhEsAesKeyWrap256, +} + +#[derive(Debug, Clone, Copy)] +enum KeyWrappingAlg { + Aes128, + Aes192, + Aes256, +} + +impl KeyWrappingAlg { + fn key_size(self) -> usize { + match self { + KeyWrappingAlg::Aes128 => 16, + KeyWrappingAlg::Aes192 => 24, + KeyWrappingAlg::Aes256 => 32, + } + } + + /// Decrypts wrapped CEK using the given AES decryption key + /// + /// ### Panics: + /// + /// - Caller must unsure `decryption_key` size matches the wrapping algorithm + fn decrypt_key( + &self, + cek_alg: JweEnc, + encrypted_cek: &[u8], + decryption_key: &[u8], + ) -> Result>, JweError> { + let mut cek = Zeroizing::new(vec![0u8; cek_alg.key_size()]); + + let expected_wrapped_cek_size = cek.len() + aes_kw::IV_LEN; + if encrypted_cek.len() != expected_wrapped_cek_size { + return Err(JweError::InvalidEncryptedKeySize { + expected: expected_wrapped_cek_size, + got: encrypted_cek.len(), + }); + } + + match self { + KeyWrappingAlg::Aes128 => { + let kek = + KekAes128::new_from_slice(decryption_key).map_err(|_| JweError::InvalidDecryptionKeySize { + expected: self.key_size(), + got: decryption_key.len(), + })?; + kek.unwrap_key(encrypted_cek, &mut cek)?; + } + KeyWrappingAlg::Aes192 => { + let kek = + KekAes192::new_from_slice(decryption_key).map_err(|_| JweError::InvalidDecryptionKeySize { + expected: self.key_size(), + got: decryption_key.len(), + })?; + kek.unwrap_key(encrypted_cek, &mut cek)?; + } + KeyWrappingAlg::Aes256 => { + let kek = + KekAes256::new_from_slice(decryption_key).map_err(|_| JweError::InvalidDecryptionKeySize { + expected: self.key_size(), + got: decryption_key.len(), + })?; + kek.unwrap_key(encrypted_cek, &mut cek)?; + } + }; + + Ok(cek) + } + + /// Encrypts the given CEK using the given AES encryption key + /// + /// ### Panics: + /// + /// - Caller must ensure `encryption_key` size matches the wrapping algorithm + fn encrypt_key(&self, cek_alg: JweEnc, cek: &[u8], encryption_key: &[u8]) -> Result, JweError> { + let mut wrapped_key = vec![0u8; cek_alg.key_size() + aes_kw::IV_LEN]; + match self { + KeyWrappingAlg::Aes128 => { + let kek = KekAes128::new_from_slice(encryption_key).map_err(|_| JweError::InvalidEncryptedKeySize { + expected: self.key_size(), + got: encryption_key.len(), + })?; + kek.wrap_key(cek, &mut wrapped_key)?; + } + KeyWrappingAlg::Aes192 => { + let kek = KekAes192::new_from_slice(encryption_key).map_err(|_| JweError::InvalidEncryptedKeySize { + expected: self.key_size(), + got: encryption_key.len(), + })?; + kek.wrap_key(cek, &mut wrapped_key)?; + } + KeyWrappingAlg::Aes256 => { + let kek = KekAes256::new_from_slice(encryption_key).map_err(|_| JweError::InvalidEncryptedKeySize { + expected: self.key_size(), + got: encryption_key.len(), + })?; + kek.wrap_key(cek, &mut wrapped_key)?; + } + }; + + Ok(wrapped_key) + } +} + +impl JweAlg { + /// Get algorithm string representation + fn name(&self) -> String { + serde_json::to_value(self) + .expect("BUG: JweAlg is always convertible to serde_json::Value") + .as_str() + .expect("BUG: JweAlg is always represented as a string in JSON") + .to_string() + } + + fn key_wrapping_alg(&self) -> Option { + let alg = match self { + JweAlg::AesKeyWrap128 => KeyWrappingAlg::Aes128, + JweAlg::AesKeyWrap192 => KeyWrappingAlg::Aes192, + JweAlg::AesKeyWrap256 => KeyWrappingAlg::Aes256, + JweAlg::EcdhEsAesKeyWrap128 => KeyWrappingAlg::Aes128, + JweAlg::EcdhEsAesKeyWrap192 => KeyWrappingAlg::Aes192, + JweAlg::EcdhEsAesKeyWrap256 => KeyWrappingAlg::Aes256, + _ => { + return None; + } + }; + + Some(alg) + } +} + +// === JWE header === // + +/// `enc` header parameter values for JWE to encrypt content +/// +/// [JSON Web Algorithms (JWA) draft-ietf-jose-json-web-algorithms-40 #5](https://www.rfc-editor.org/rfc/rfc7518.html#section-5.1) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum JweEnc { + /// AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm. (unsupported) + /// + /// Required by RFC + #[serde(rename = "A128CBC-HS256")] + Aes128CbcHmacSha256, + + /// AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm. (unsupported) + #[serde(rename = "A192CBC-HS384")] + Aes192CbcHmacSha384, + + /// AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm. (unsupported) + /// + /// Required by RFC + #[serde(rename = "A256CBC-HS512")] + Aes256CbcHmacSha512, + + /// AES GCM using 128-bit key. + /// + /// Recommended by RFC + #[serde(rename = "A128GCM")] + Aes128Gcm, + + /// AES GCM using 192-bit key. + #[serde(rename = "A192GCM")] + Aes192Gcm, + + /// AES GCM using 256-bit key. + /// + /// Recommended by RFC + #[serde(rename = "A256GCM")] + Aes256Gcm, +} + +impl JweEnc { + /// Get algorithm string representation + fn name(&self) -> String { + serde_json::to_value(self) + .expect("BUG: JweEnc is always convertible to serde_json::Value") + .as_str() + .expect("BUG: JweEnc is always represented as a string in JSON") + .to_string() + } + + pub fn key_size(self) -> usize { + match self { + Self::Aes128CbcHmacSha256 | Self::Aes128Gcm => ::KeySize::to_usize(), + Self::Aes192CbcHmacSha384 | Self::Aes192Gcm => ::KeySize::to_usize(), + Self::Aes256CbcHmacSha512 | Self::Aes256Gcm => ::KeySize::to_usize(), + } + } + + pub fn nonce_size(self) -> usize { + match self { + Self::Aes128Gcm | Self::Aes192Gcm | Self::Aes256Gcm => 12usize, + Self::Aes128CbcHmacSha256 | Self::Aes192CbcHmacSha384 | Self::Aes256CbcHmacSha512 => 16usize, + } + } + + pub fn tag_size(self) -> usize { + match self { + Self::Aes128Gcm | Self::Aes192Gcm | Self::Aes256Gcm => 16usize, + Self::Aes128CbcHmacSha256 => 32usize, + Self::Aes192CbcHmacSha384 => 48usize, + Self::Aes256CbcHmacSha512 => 64usize, + } + } +} + +// === JWE header === // + +/// JWE specific part of JOSE header +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct JweHeader { + // -- specific to JWE -- // + /// Algorithm used to encrypt or determine the Content Encryption Key (CEK) (key wrapping...) + pub alg: JweAlg, + + /// Content encryption algorithm to use + /// + /// This must be a *symmetric* Authenticated Encryption with Associated Data (AEAD) algorithm. + pub enc: JweEnc, + + // -- common with JWS -- // + /// JWK Set URL + /// + /// URI that refers to a resource for a set of JSON-encoded public keys, + /// one of which corresponds to the key used to digitally sign the JWK. + #[serde(skip_serializing_if = "Option::is_none")] + pub jku: Option, + + /// JSON Web Key + /// + /// The public key that corresponds to the key used to digitally sign the JWS. + /// This key is represented as a JSON Web Key (JWK). + #[serde(skip_serializing_if = "Option::is_none")] + pub jwk: Option, + + /// Type header + /// + /// Used by JWE applications to declare the media type [IANA.MediaTypes] of this complete JWE. + #[serde(skip_serializing_if = "Option::is_none")] + pub typ: Option, + + /// Content Type header + /// + /// Used by JWE applications to declare the media type [IANA.MediaTypes] of the secured content (the payload). + #[serde(skip_serializing_if = "Option::is_none")] + pub cty: Option, + + // -- common with all -- // + /// Key ID Header + /// + /// A hint indicating which key was used. + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, + + /// X.509 URL Header + /// + /// URI that refers to a resource for an X.509 public key certificate or certificate chain. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5u: Option, + + /// X.509 Certificate Chain + /// + /// Chain of one or more PKIX certificates. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5c: Option>, + + /// X.509 Certificate SHA-1 Thumbprint + /// + /// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5t: Option, + + /// X.509 Certificate SHA-256 Thumbprint + /// + /// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + #[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")] + pub x5t_s256: Option, + + /// Ephemeral Public Key for `ECDH-ES` encryption algorithm. It is generated by the sender + /// during JWT generation and set automatically. + pub epk: Option, + + /// Agreement PartyUInfo value for key agreement algorithms + /// using it (such as "ECDH-ES"), represented as a base64url-encoded + /// string. When used, the PartyUInfo value contains information about + /// the producer. Use of this Header Parameter is OPTIONAL. + pub apu: Option, + + /// Agreement PartyVInfo value for key agreement algorithms + /// using it (such as "ECDH-ES"), represented as a base64url encoded + /// string. When used, the PartyVInfo value contains information about + /// the recipient. Use of this Header Parameter is OPTIONAL. + pub apv: Option, + + // -- extra parameters -- // + /// Additional header parameters (both public and private) + #[serde(flatten)] + pub additional: HashMap, +} + +impl JweHeader { + pub fn new(alg: JweAlg, enc: JweEnc) -> Self { + Self { + alg, + enc, + jku: None, + jwk: None, + typ: None, + cty: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + apu: None, + apv: None, + epk: None, + additional: HashMap::default(), + } + } + + pub fn new_with_cty(alg: JweAlg, enc: JweEnc, cty: impl Into) -> Self { + Self { + cty: Some(cty.into()), + ..Self::new(alg, enc) + } + } +} + +// === json web encryption === // + +/// Provides an API to encrypt any kind of data (binary). JSON claims are part of `Jwt` only. +#[derive(Debug, Clone)] +pub struct Jwe { + pub header: JweHeader, + pub payload: Vec, +} + +impl Jwe { + pub fn new(alg: JweAlg, enc: JweEnc, payload: Vec) -> Self { + Self { + header: JweHeader::new(alg, enc), + payload, + } + } + + /// Encodes with CEK encrypted and included in the token using asymmetric cryptography. + pub fn encode(self, asymmetric_key: &PublicKey) -> Result { + encode_impl(self, EncoderMode::Asymmetric(asymmetric_key)) + } + + /// Encodes with provided CEK (a symmetric key). This will ignore `alg` value and override it with "dir". + pub fn encode_direct(self, cek: &[u8]) -> Result { + encode_impl(self, EncoderMode::Direct(cek)) + } + + /// Decodes with CEK encrypted and included in the token using asymmetric cryptography. + pub fn decode(compact_repr: &str, key: &PrivateKey) -> Result { + RawJwe::decode(compact_repr).and_then(|jwe| jwe.decrypt(key)) + } + + /// Decodes with provided CEK (a symmetric key). + pub fn decode_direct(compact_repr: &str, cek: &[u8]) -> Result { + RawJwe::decode(compact_repr).and_then(|jwe| jwe.decrypt_direct(cek)) + } +} + +/// Raw low-level interface to the yet to be decoded JWE token. +/// +/// This is useful to inspect the structure before performing further processing. +/// For most usecases, use `Jwe` directly. +#[derive(Debug, Clone)] +pub struct RawJwe<'repr> { + pub compact_repr: Cow<'repr, str>, + pub header: JweHeader, + pub encrypted_key: Vec, + pub initialization_vector: Vec, + pub ciphertext: Vec, + pub authentication_tag: Vec, +} + +/// An owned `RawJws` for convenience. +pub type OwnedRawJwe = RawJwe<'static>; + +impl<'repr> RawJwe<'repr> { + /// Decodes a JWE in compact representation. + pub fn decode(compact_repr: impl Into>) -> Result { + decode_impl(compact_repr.into()) + } + + /// Decrypts the ciphertext using asymmetric cryptography and returns a verified `Jwe` structure. + pub fn decrypt(self, key: &PrivateKey) -> Result { + decrypt_impl(self, DecoderMode::Normal(key)) + } + + /// Decrypts the ciphertext using the provided CEK (a symmetric key). + pub fn decrypt_direct(self, cek: &[u8]) -> Result { + decrypt_impl(self, DecoderMode::Direct(cek)) + } +} + +fn decode_impl(compact_repr: Cow<'_, str>) -> Result, JweError> { + fn parse_compact_repr(compact_repr: &str) -> Option<(&str, &str, &str, &str, &str)> { + let mut split = compact_repr.splitn(5, '.'); + + let protected_header = split.next()?; + let encrypted_key = split.next()?; + let initialization_vector = split.next()?; + let ciphertext = split.next()?; + let authentication_tag = split.next()?; + + Some(( + protected_header, + encrypted_key, + initialization_vector, + ciphertext, + authentication_tag, + )) + } + + let (protected_header, encrypted_key, initialization_vector, ciphertext, authentication_tag) = + parse_compact_repr(&compact_repr).ok_or_else(|| JweError::InvalidEncoding { + input: compact_repr.clone().into_owned(), + })?; + + let protected_header = general_purpose::URL_SAFE_NO_PAD.decode(protected_header)?; + let header = serde_json::from_slice::(&protected_header)?; + + Ok(RawJwe { + header, + encrypted_key: general_purpose::URL_SAFE_NO_PAD.decode(encrypted_key)?, + initialization_vector: general_purpose::URL_SAFE_NO_PAD.decode(initialization_vector)?, + ciphertext: general_purpose::URL_SAFE_NO_PAD.decode(ciphertext)?, + authentication_tag: general_purpose::URL_SAFE_NO_PAD.decode(authentication_tag)?, + compact_repr, + }) +} + +// encoder + +#[derive(Debug, Clone)] +enum EncoderMode<'a> { + Asymmetric(&'a PublicKey), + Direct(&'a [u8]), +} + +fn encode_impl(mut jwe: Jwe, mode: EncoderMode) -> Result { + use picky_asn1_x509::PublicKey as RfcPublicKey; + + let (encrypted_key_base64, jwe_cek) = match mode { + EncoderMode::Direct(symmetric_key) => { + if symmetric_key.len() != jwe.header.enc.key_size() { + return Err(JweError::InvalidSize { + ty: "symmetric key", + expected: jwe.header.enc.key_size(), + got: symmetric_key.len(), + }); + } + + // Override `alg` header with "dir" + jwe.header.alg = JweAlg::Direct; + + (String::new(), Zeroizing::new(symmetric_key.to_vec())) + } + EncoderMode::Asymmetric(public_key) => match &public_key.as_inner().subject_public_key { + RfcPublicKey::Rsa(_) => { + let rsa_public_key = RsaPublicKey::try_from(public_key)?; + + let padding = match jwe.header.alg { + JweAlg::RsaPkcs1v15 => RsaPaddingScheme::Pkcs1v15Encrypt, + JweAlg::RsaOaep => RsaPaddingScheme::Oaep(Oaep::::new()), + JweAlg::RsaOaep256 => RsaPaddingScheme::Oaep256(Oaep::::new()), + unsupported => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("{unsupported:?}"), + }); + } + }; + + let cek = generate_cek(jwe.header.enc)?; + + let encrypted_key = match rsa_public_key.encrypt(&mut StdRng::try_from_rng(&mut SysRng)?, padding, &cek) + { + Ok(encrypted_key) => encrypted_key, + Err(err) => { + return Err(err.into()); + } + }; + + (general_purpose::URL_SAFE_NO_PAD.encode(encrypted_key), cek) + } + RfcPublicKey::Ec(_) | RfcPublicKey::Ed(_) => { + let JweEcdhEncryptionContext { + jwe_cek, + encrypted_key, + epk, + } = prepare_ecdh_encryption_key(&jwe, public_key)?; + + jwe.header.epk = Some(Jwk::from_public_key(&epk)?); + let encrypted_key_base64 = if encrypted_key.is_empty() { + String::new() + } else { + general_purpose::URL_SAFE_NO_PAD.encode(encrypted_key) + }; + + (encrypted_key_base64, jwe_cek) + } + RfcPublicKey::Mldsa(_) => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: "mldsa".to_string(), + }); + } + }, + }; + + // Note that header could be modified by code above: + // - `alg` header could be overridden with "dir" + // - `epk` header could be set for ECDH-ES + let protected_header_base64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&jwe.header)?); + + let mut buffer = jwe.payload; + let nonce = as From<[u8; 12]>>::from(rand::random()); // 96-bits nonce for all AES-GCM variants + let aad = protected_header_base64.as_bytes(); // The Additional Authenticated Data value used for AES-GCM. + let authentication_tag = match jwe.header.enc { + JweEnc::Aes128Gcm => { + let algo = Aes128Gcm::new_from_slice(&jwe_cek).map_err(|_| JweError::AesGcm)?; + algo.encrypt_inout_detached(&nonce, aad, buffer.as_mut_slice().into())? + } + JweEnc::Aes192Gcm => { + let algo = Aes192Gcm::new_from_slice(&jwe_cek).map_err(|_| JweError::AesGcm)?; + algo.encrypt_inout_detached(&nonce, aad, buffer.as_mut_slice().into())? + } + JweEnc::Aes256Gcm => { + let algo = Aes256Gcm::new_from_slice(&jwe_cek).map_err(|_| JweError::AesGcm)?; + algo.encrypt_inout_detached(&nonce, aad, buffer.as_mut_slice().into())? + } + unsupported => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("{unsupported:?}"), + }); + } + }; + + let initialization_vector_base64 = general_purpose::URL_SAFE_NO_PAD.encode(nonce.as_slice()); + let ciphertext_base64 = general_purpose::URL_SAFE_NO_PAD.encode(&buffer); + let authentication_tag_base64 = general_purpose::URL_SAFE_NO_PAD.encode(authentication_tag); + + Ok([ + protected_header_base64, + encrypted_key_base64, + initialization_vector_base64, + ciphertext_base64, + authentication_tag_base64, + ] + .join(".")) +} + +struct JweEcdhEncryptionContext { + jwe_cek: Zeroizing>, + encrypted_key: Vec, + epk: PublicKey, +} + +fn prepare_ecdh_encryption_key(jwe: &Jwe, public_key: &PublicKey) -> Result { + let header = &jwe.header; + + let (encrypted_key, jwe_cek, epk) = match header.alg { + JweAlg::EcdhEs => { + // In case of ECDH Direct mode, we use JweEnc algorithm name for KDF + let alg_name = header.enc.name(); + // Use DH shared secret as CEK + let (cek, epk) = generate_ecdh_shared_secret( + header.apu.as_deref(), + header.apv.as_deref(), + &alg_name, + public_key, + header.enc.key_size(), + )?; + // Encrypted key should be empty octet sequence in direct mode + (vec![], cek, epk) + } + JweAlg::EcdhEsAesKeyWrap128 | JweAlg::EcdhEsAesKeyWrap192 | JweAlg::EcdhEsAesKeyWrap256 => { + let alg_name = header.alg.name(); + + let wrapping_alg = header + .alg + .key_wrapping_alg() + .expect("BUG: ECDH-ES+AxKW algorithm should have a wrapping algorithm"); + + // Generate share key with size equal to wrapping algorithm key size + let (shared_secret, epk) = generate_ecdh_shared_secret( + header.apu.as_deref(), + header.apv.as_deref(), + &alg_name, + public_key, + wrapping_alg.key_size(), + )?; + + let cek = generate_cek(header.enc)?; + let wrapped_key = wrapping_alg.encrypt_key(header.enc, &cek, &shared_secret)?; + + (wrapped_key, cek, epk) + } + _ => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("Algorithm `{}` is not supported for EC & ED keys", header.alg.name()), + }); + } + }; + + Ok(JweEcdhEncryptionContext { + jwe_cek, + encrypted_key, + epk, + }) +} + +// decoder + +#[derive(Clone)] +enum DecoderMode<'a> { + Normal(&'a PrivateKey), + Direct(&'a [u8]), +} + +fn decrypt_impl(raw: RawJwe<'_>, mode: DecoderMode<'_>) -> Result { + let RawJwe { + compact_repr, + header, + encrypted_key, + initialization_vector, + ciphertext, + authentication_tag, + } = raw; + + let protected_header_base64 = compact_repr + .split('.') + .next() + .ok_or_else(|| JweError::InvalidEncoding { + input: compact_repr.clone().into_owned(), + })?; + + let jwe_cek = match mode { + DecoderMode::Direct(symmetric_key) => Zeroizing::new(symmetric_key.to_vec()), + DecoderMode::Normal(private_key) => match &private_key.as_kind() { + PrivateKeyKind::Rsa => { + let rsa_private_key = RsaPrivateKey::try_from(private_key)?; + + let padding = match header.alg { + JweAlg::RsaPkcs1v15 => RsaPaddingScheme::Pkcs1v15Encrypt, + JweAlg::RsaOaep => RsaPaddingScheme::Oaep(Oaep::::new()), + JweAlg::RsaOaep256 => RsaPaddingScheme::Oaep256(Oaep::::new()), + unsupported => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("{unsupported:?}"), + }); + } + }; + + Zeroizing::new(rsa_private_key.decrypt(padding, &encrypted_key)?) + } + PrivateKeyKind::Ec { .. } | PrivateKeyKind::Ed { .. } => { + let sender_public_key = header + .epk + .as_ref() + .ok_or_else(|| JweError::MissingEpk)? + .to_public_key()?; + + prepare_ecdh_decryption_key(&header, &encrypted_key, &sender_public_key, private_key)? + } + }, + }; + + if jwe_cek.len() != header.enc.key_size() { + return Err(JweError::InvalidSize { + ty: "symmetric key", + expected: header.enc.key_size(), + got: jwe_cek.len(), + }); + } + + if initialization_vector.len() != header.enc.nonce_size() { + return Err(JweError::InvalidSize { + ty: "initialization vector (nonce)", + expected: header.enc.nonce_size(), + got: initialization_vector.len(), + }); + } + + if authentication_tag.len() != header.enc.tag_size() { + return Err(JweError::InvalidSize { + ty: "authentication tag", + expected: header.enc.tag_size(), + got: authentication_tag.len(), + }); + } + + let mut buffer = ciphertext; + let nonce = Array::try_from(&initialization_vector).expect("can't panic since the size is checked before"); + let aad = protected_header_base64.as_bytes(); // The Additional Authenticated Data value used for AES-GCM. + let authentication_tag = + Array::try_from(&authentication_tag).expect("can't panic since the size is checked before"); + match header.enc { + JweEnc::Aes128Gcm => { + let algo = Aes128Gcm::new_from_slice(&jwe_cek).map_err(|_| JweError::AesGcm)?; + algo.decrypt_inout_detached(&nonce, aad, buffer.as_mut_slice().into(), &authentication_tag)?; + } + JweEnc::Aes192Gcm => { + let algo = Aes192Gcm::new_from_slice(&jwe_cek).map_err(|_| JweError::AesGcm)?; + algo.decrypt_inout_detached(&nonce, aad, buffer.as_mut_slice().into(), &authentication_tag)?; + } + JweEnc::Aes256Gcm => { + let algo = Aes256Gcm::new_from_slice(&jwe_cek).map_err(|_| JweError::AesGcm)?; + algo.decrypt_inout_detached(&nonce, aad, buffer.as_mut_slice().into(), &authentication_tag)?; + } + unsupported => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("{unsupported:?}"), + }); + } + }; + + Ok(Jwe { + header, + payload: buffer, + }) +} + +fn prepare_ecdh_decryption_key( + header: &JweHeader, + encrypted_key: &[u8], + sender_public_key: &PublicKey, + receiver_private_key: &PrivateKey, +) -> Result>, JweError> { + let apu = header.apu.as_deref(); + let apv = header.apv.as_deref(); + + match header.alg { + JweAlg::EcdhEs => { + let alg_name = header.enc.name(); + // Use DH shared secret as CEK directly + calculate_ecdh_shared_secret( + apu, + apv, + &alg_name, + sender_public_key, + receiver_private_key, + header.enc.key_size(), + ) + } + JweAlg::EcdhEsAesKeyWrap128 | JweAlg::EcdhEsAesKeyWrap192 | JweAlg::EcdhEsAesKeyWrap256 => { + let wrapping_alg = header + .alg + .key_wrapping_alg() + .expect("BUG: ECDH-ES+AxKW algorithm should have a wrapping algorithm"); + + let alg_name = header.alg.name(); + + // We need to unwrap CEK from encrypted key + let shared_secret = calculate_ecdh_shared_secret( + apu, + apv, + &alg_name, + sender_public_key, + receiver_private_key, + wrapping_alg.key_size(), + )?; + + wrapping_alg.decrypt_key(header.enc, encrypted_key, &shared_secret) + } + _ => Err(JweError::UnsupportedAlgorithm { + algorithm: format!("Algorithm `{}` is not supported for EC & ED keys", header.alg.name()), + }), + } +} + +/// Expands the shared secret into a key of the desired size using the ECDH Concat KDF +fn ecdh_concat_kdf( + alg: &str, + shared_key_len: usize, + derived_key: &[u8], + apu: Option<&str>, + apv: Option<&str>, +) -> Result>, JweError> { + use sha2::{Digest, Sha256}; + + let apu = apu + .map(|val| general_purpose::URL_SAFE_NO_PAD.decode(val)) + .transpose()?; + + let apv = apv + .map(|val| general_purpose::URL_SAFE_NO_PAD.decode(val)) + .transpose()?; + + // Size of the resulting key in BITS + let shared_key_len_bytes = ((shared_key_len * 8) as u32).to_be_bytes(); + + let alg = alg.as_bytes(); + let alg_len_bytes = (alg.len() as u32).to_be_bytes(); + + let apu_len_bytes = apu.as_ref().map(|val| val.len() as u32).unwrap_or(0).to_be_bytes(); + let apv_len_bytes = apv.as_ref().map(|val| val.len() as u32).unwrap_or(0).to_be_bytes(); + + let block_size = Sha256::output_size(); + + let count = shared_key_len.div_ceil(block_size); + let mut shared_key = Zeroizing::new(Vec::with_capacity(block_size * count)); + + let mut hasher = Sha256::new(); + + for i in 0..count { + hasher.update(((i + 1) as u32).to_be_bytes()); + hasher.update(derived_key); + hasher.update(alg_len_bytes); + hasher.update(alg); + hasher.update(apu_len_bytes); + if let Some(val) = apu.as_deref() { + hasher.update(val); + } + hasher.update(apv_len_bytes); + if let Some(val) = apv.as_deref() { + hasher.update(val); + } + hasher.update(shared_key_len_bytes); + + shared_key.extend_from_slice(hasher.finalize_reset().as_slice()); + } + + if shared_key.len() > shared_key_len { + shared_key.truncate(shared_key_len); + } + + // `sha2` crate currently doesn't perform any zeroize operations on finalization/reset, so we + // doing a hack here, messing up with internal state of the hasher to make its data useless + hasher.update(&shared_key); + + Ok(shared_key) +} + +/// Returns ECDH ephemeral public key and shared secret required to build encrypted JWE +fn generate_ecdh_shared_secret( + apu: Option<&str>, + apv: Option<&str>, + alg: &str, + receiver_public_key: &PublicKey, + cek_key_len: usize, +) -> Result<(Zeroizing>, PublicKey), JweError> { + use picky_asn1_x509::PublicKey as RfcPublicKey; + + let (shared_secret, epk) = match &receiver_public_key.as_inner().subject_public_key { + RfcPublicKey::Ec(_) => { + let ec = EcdsaPublicKey::try_from(receiver_public_key)?; + + match ec.curve() { + NamedEcCurve::Known(EcCurve::NistP256) => { + let public_key = p256::PublicKey::from_sec1_bytes(ec.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p256 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret = + p256::ecdh::EphemeralSecret::generate_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + + let shared_secret = Zeroizing::new(secret.diffie_hellman(&public_key).raw_secret_bytes().to_vec()); + let epk = PublicKey::from_ec_encoded_components( + &NamedEcCurve::Known(EcCurve::NistP256).into(), + secret.public_key().to_sec1_bytes().as_ref(), + ); + + (shared_secret, epk) + } + NamedEcCurve::Known(EcCurve::NistP384) => { + let public_key = p384::PublicKey::from_sec1_bytes(ec.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p384 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret = + p384::ecdh::EphemeralSecret::generate_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + + let shared_secret = Zeroizing::new(secret.diffie_hellman(&public_key).raw_secret_bytes().to_vec()); + let epk = PublicKey::from_ec_encoded_components( + &NamedEcCurve::Known(EcCurve::NistP384).into(), + secret.public_key().to_sec1_bytes().as_ref(), + ); + + (shared_secret, epk) + } + NamedEcCurve::Known(EcCurve::NistP521) => { + let public_key = p521::PublicKey::from_sec1_bytes(ec.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p521 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret = + p521::ecdh::EphemeralSecret::generate_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + + let shared_secret = Zeroizing::new(secret.diffie_hellman(&public_key).raw_secret_bytes().to_vec()); + let epk = PublicKey::from_ec_encoded_components( + &NamedEcCurve::Known(EcCurve::NistP521).into(), + secret.public_key().to_sec1_bytes().as_ref(), + ); + + (shared_secret, epk) + } + NamedEcCurve::Unsupported(oid) => { + let source = KeyError::unsupported_curve(oid, "ECDH-ES JWE algorithm"); + return Err(JweError::Key { source }); + } + } + } + RfcPublicKey::Ed(_) => { + let ed = EdPublicKey::try_from(receiver_public_key)?; + + match ed.algorithm() { + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => { + let public_key_data: [u8; X25519_FIELD_ELEMENT_SIZE] = ed.data().try_into().map_err(|e| { + let source = KeyError::ED { + context: format!("Cannot parse x25519 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let public_key = x25519_dalek::PublicKey::from(public_key_data); + + let secret = + x25519_dalek::EphemeralSecret::random_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + + let epk = PublicKey::from_ed_encoded_components( + &EdAlgorithm::X25519.into(), + x25519_dalek::PublicKey::from(&secret).as_bytes().as_slice(), + ); + let shared_secret = Zeroizing::new(secret.diffie_hellman(&public_key).as_bytes().to_vec()); + + (shared_secret, epk) + } + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: "Ed25519 can't be used for ECDH".to_string(), + }); + } + NamedEdAlgorithm::Unsupported(oid) => { + let source = KeyError::unsupported_ed_algorithm(oid, "ECDH-ES JWE algorithm"); + return Err(JweError::Key { source }); + } + } + } + RfcPublicKey::Rsa(_) => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("RSA key can't be used with `{alg:?}` algorithm"), + }); + } + RfcPublicKey::Mldsa(_) => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("MLDSA key can't be used with `{alg:?}` algorithm"), + }); + } + }; + + // Apply concact KDF to raw shared secret + Ok((ecdh_concat_kdf(alg, cek_key_len, &shared_secret, apu, apv)?, epk)) +} + +/// Calculates ECDH shared secret using given keys and jwe header fields +fn calculate_ecdh_shared_secret( + apu: Option<&str>, + apv: Option<&str>, + alg: &str, + sender_public_key: &PublicKey, + receiver_private_key: &PrivateKey, + cek_key_len: usize, +) -> Result>, JweError> { + let shared_secret = match &receiver_private_key.as_kind() { + PrivateKeyKind::Ec { .. } => { + let private_key = EcdsaKeypair::try_from(receiver_private_key)?; + + let public_key = + EcdsaPublicKey::try_from(sender_public_key).map_err(|source| JweError::KeyAlgorithmsMismatch { + context: source.to_string(), + })?; + + if private_key.curve() != public_key.curve() { + return Err(JweError::KeyAlgorithmsMismatch { + context: format!( + "Receiver key have EC curve `{}`, but sender key have `{}` curve", + private_key.curve(), + public_key.curve() + ), + }); + } + + match private_key.curve() { + NamedEcCurve::Known(EcCurve::NistP256) => { + let public_key = p256::PublicKey::from_sec1_bytes(public_key.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p256 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret_bytes_validated = + EcCurve::NistP256.validate_component(EcComponent::Secret(private_key.secret()))?; + + let secret = p256::SecretKey::from_slice(secret_bytes_validated).map_err(|e| KeyError::EC { + context: format!("Cannot parse p256 secret from bytes: {e}"), + })?; + + // p256 crate doesn't have high level API for static ECDH secrets + let shared_secret = + p256::elliptic_curve::ecdh::diffie_hellman(secret.to_nonzero_scalar(), public_key.as_affine()) + .raw_secret_bytes() + .to_vec(); + + Zeroizing::new(shared_secret) + } + NamedEcCurve::Known(EcCurve::NistP384) => { + let public_key = p384::PublicKey::from_sec1_bytes(public_key.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p384 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret_bytes_validated = + EcCurve::NistP384.validate_component(EcComponent::Secret(private_key.secret()))?; + + let secret = p384::SecretKey::from_slice(secret_bytes_validated).map_err(|e| KeyError::EC { + context: format!("Cannot parse p384 secret from bytes: {e}"), + })?; + + // p384 crate doesn't have high level API for static ECDH secrets + let shared_secret = + p384::elliptic_curve::ecdh::diffie_hellman(secret.to_nonzero_scalar(), public_key.as_affine()) + .raw_secret_bytes() + .to_vec(); + + Zeroizing::new(shared_secret) + } + NamedEcCurve::Known(EcCurve::NistP521) => { + let public_key = p521::PublicKey::from_sec1_bytes(public_key.encoded_point()).map_err(|e| { + let source = KeyError::EC { + context: format!("Cannot parse p521 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret_bytes_validated = + EcCurve::NistP521.validate_component(EcComponent::Secret(private_key.secret()))?; + + let secret = p521::SecretKey::from_slice(secret_bytes_validated).map_err(|e| KeyError::EC { + context: format!("Cannot parse p521 secret from bytes: {e}"), + })?; + + // p521 crate doesn't have high level API for static ECDH secrets + let shared_secret = + p521::elliptic_curve::ecdh::diffie_hellman(secret.to_nonzero_scalar(), public_key.as_affine()) + .raw_secret_bytes() + .to_vec(); + + Zeroizing::new(shared_secret) + } + NamedEcCurve::Unsupported(oid) => { + let source = KeyError::unsupported_curve(oid, "ECDH-ES JWE algorithm"); + return Err(JweError::Key { source }); + } + } + } + PrivateKeyKind::Ed { .. } => { + let public_key = EdPublicKey::try_from(sender_public_key).map_err(|source| JweError::Key { source })?; + + let private_key = + EdKeypair::try_from(receiver_private_key).map_err(|source| JweError::KeyAlgorithmsMismatch { + context: source.to_string(), + })?; + + if private_key.algorithm() != public_key.algorithm() { + return Err(JweError::KeyAlgorithmsMismatch { + context: format!( + "Receiver key have ED algorithm `{}`, but sender key have `{}` algorithm", + private_key.algorithm(), + public_key.algorithm() + ), + }); + } + + match private_key.algorithm() { + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => { + let public_key_data: [u8; X25519_FIELD_ELEMENT_SIZE] = + public_key.data().try_into().map_err(|e| { + let source = KeyError::ED { + context: format!("Cannot parse x25519 encoded point from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let public_key = x25519_dalek::PublicKey::from(public_key_data); + + let private_key_data: [u8; X25519_FIELD_ELEMENT_SIZE] = + private_key.secret().try_into().map_err(|e| { + let source = KeyError::ED { + context: format!("Cannot parse x25519 secret from bytes: {e}"), + }; + JweError::Key { source } + })?; + + let secret = x25519_dalek::StaticSecret::from(private_key_data); + + let shared_secret = secret.diffie_hellman(&public_key).as_bytes().to_vec(); + + Zeroizing::new(shared_secret) + } + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: "Ed25519 can't be used for ECDH".to_string(), + }); + } + NamedEdAlgorithm::Unsupported(oid) => { + return Err(KeyError::unsupported_ed_algorithm(oid, "ECDH-ES JWE algorithm").into()); + } + } + } + PrivateKeyKind::Rsa => { + return Err(JweError::UnsupportedAlgorithm { + algorithm: format!("RSA key can't be used with `{alg:?}` algorithm"), + }); + } + }; + + // Apply concact KDF to raw shared secret + ecdh_concat_kdf(alg, cek_key_len, &shared_secret, apu, apv) +} + +/// Generate content encryption key (CEK) for given algorithm and wraps it with zeroize-on-drop container +fn generate_cek(alg: JweEnc) -> Result>, JweError> { + let mut cek = Zeroizing::new(vec![0u8; alg.key_size()]); + let mut rng = StdRng::try_from_rng(&mut SysRng)?; + rng.fill_bytes(&mut cek); + Ok(cek) +} + +enum RsaPaddingScheme { + Pkcs1v15Encrypt, + Oaep(Oaep), + Oaep256(Oaep), +} + +impl rsa::traits::PaddingScheme for RsaPaddingScheme { + fn decrypt( + self, + rng: Option<&mut Rng>, + priv_key: &RsaPrivateKey, + ciphertext: &[u8], + ) -> rsa::Result> { + match self { + RsaPaddingScheme::Pkcs1v15Encrypt => { + rsa::traits::PaddingScheme::decrypt(Pkcs1v15Encrypt, rng, priv_key, ciphertext) + } + RsaPaddingScheme::Oaep(oaep) => rsa::traits::PaddingScheme::decrypt(oaep, rng, priv_key, ciphertext), + RsaPaddingScheme::Oaep256(oaep) => rsa::traits::PaddingScheme::decrypt(oaep, rng, priv_key, ciphertext), + } + } + + fn encrypt( + self, + rng: &mut Rng, + pub_key: &RsaPublicKey, + msg: &[u8], + ) -> rsa::Result> { + match self { + RsaPaddingScheme::Pkcs1v15Encrypt => { + rsa::traits::PaddingScheme::encrypt(Pkcs1v15Encrypt, rng, pub_key, msg) + } + RsaPaddingScheme::Oaep(oaep) => rsa::traits::PaddingScheme::encrypt(oaep, rng, pub_key, msg), + RsaPaddingScheme::Oaep256(oaep) => rsa::traits::PaddingScheme::encrypt(oaep, rng, pub_key, msg), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::PrivateKey; + use crate::pem::Pem; + use rstest::rstest; + + fn get_private_key_1() -> PrivateKey { + let pk_pem = picky_test_data::RSA_2048_PK_1.parse::().unwrap(); + PrivateKey::from_pem(&pk_pem).expect("private_key 1") + } + + fn get_private_key_2() -> PrivateKey { + let pk_pem = picky_test_data::RSA_2048_PK_7.parse::().unwrap(); + PrivateKey::from_pem(&pk_pem).expect("private_key 7") + } + + #[test] + fn rsa_oaep_aes_128_gcm() { + let payload = "何だと?……無駄な努力だ?……百も承知だ!だがな、勝つ望みがある時ばかり、戦うのとは訳が違うぞ!" + .as_bytes() + .to_vec(); + + let private_key = get_private_key_1(); + let public_key = private_key.to_public_key().unwrap(); + + let jwe = Jwe::new(JweAlg::RsaOaep, JweEnc::Aes128Gcm, payload); + let encoded = jwe.clone().encode(&public_key).unwrap(); + + let decoded = Jwe::decode(&encoded, &private_key).unwrap(); + + assert_eq!(jwe.payload, decoded.payload); + assert_eq!(jwe.header, decoded.header); + } + + #[test] + fn rsa_pkcs1v15_aes_128_gcm_bad_key() { + let payload = "そうとも! 負けると知って戦うのが、遙かに美しいのだ!" + .as_bytes() + .to_vec(); + + let private_key = get_private_key_1(); + let public_key = get_private_key_2().to_public_key().unwrap(); + + let jwe = Jwe::new(JweAlg::RsaPkcs1v15, JweEnc::Aes128Gcm, payload); + let encoded = jwe.encode(&public_key).unwrap(); + + let err = Jwe::decode(&encoded, &private_key).err().unwrap(); + assert_eq!(err.to_string(), "RSA error: decryption error"); + } + + #[test] + fn direct_aes_256_gcm() { + let payload = "さあ、取れ、取るがいい!だがな、貴様たちがいくら騒いでも、あの世へ、俺が持って行くものが一つある!それはな…".as_bytes().to_vec(); + + let key = "わたしの……心意気だ!!"; + + let jwe = Jwe::new(JweAlg::Direct, JweEnc::Aes256Gcm, payload); + let encoded = jwe.clone().encode_direct(key.as_bytes()).unwrap(); + + let decoded = Jwe::decode_direct(&encoded, key.as_bytes()).unwrap(); + + assert_eq!(jwe.payload, decoded.payload); + assert_eq!(jwe.header, decoded.header); + } + + #[test] + fn direct_aes_192_gcm_bad_key() { + let payload = "和解をしよう? 俺が? 真っ平だ! 真っ平御免だ!".as_bytes().to_vec(); + + let jwe = Jwe::new(JweAlg::Direct, JweEnc::Aes192Gcm, payload); + let encoded = jwe.encode_direct(b"abcdefghabcdefghabcdefgh").unwrap(); + + let err = Jwe::decode_direct(&encoded, b"zzzzzzzzabcdefghzzzzzzzz").err().unwrap(); + assert_eq!(err.to_string(), "AES-GCM error (opaque)"); + } + + #[test] + #[ignore = "this is not directly using picky code"] + fn rfc7516_example_using_rsaes_oaep_and_aes_gcm() { + // See: https://tools.ietf.org/html/rfc7516#appendix-A.1 + + let plaintext = b"The true sign of intelligence is not knowledge but imagination."; + let jwe = Jwe::new(JweAlg::RsaOaep, JweEnc::Aes256Gcm, plaintext.to_vec()); + + // 1: JOSE header + + let protected_header_base64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&jwe.header).unwrap()); + assert_eq!( + protected_header_base64, + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ" + ); + + // 2: Content Encryption Key (CEK) + + let cek = [ + 177, 161, 244, 128, 84, 143, 225, 115, 63, 180, 3, 255, 107, 154, 212, 246, 138, 7, 110, 91, 112, 46, 34, + 105, 47, 130, 203, 46, 122, 234, 64, 252, + ]; + + // 3: Key Encryption + + let encrypted_key_base64 = "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg"; + + // 4: Initialization Vector + + let iv_base64 = "48V1_ALb6US04U3b"; + let iv = general_purpose::URL_SAFE_NO_PAD.decode(iv_base64).unwrap(); + + // 5: AAD + + let aad = protected_header_base64.as_bytes(); + + // 6: Content Encryption + + let mut buffer = plaintext.to_vec(); + let algo = Aes256Gcm::new_from_slice(&cek).unwrap(); + let tag = algo + .encrypt_inout_detached(&Array::try_from(iv).unwrap(), aad, buffer.as_mut_slice().into()) + .unwrap(); + let ciphertext = buffer; + + assert_eq!( + ciphertext, + [ + 229, 236, 166, 241, 53, 191, 115, 196, 174, 43, 73, 109, 39, 122, 233, 96, 140, 206, 120, 52, 51, 237, + 48, 11, 190, 219, 186, 80, 111, 104, 50, 142, 47, 167, 59, 61, 181, 127, 196, 21, 40, 82, 242, 32, 123, + 143, 168, 226, 73, 216, 176, 144, 138, 247, 106, 60, 16, 205, 160, 109, 64, 63, 192 + ] + .to_vec() + ); + assert_eq!( + tag.as_slice(), + &[ + 92, 80, 104, 49, 133, 25, 161, 215, 173, 101, 219, 211, 136, 91, 210, 145 + ] + ); + + // 7: Complete Representation + + let token = format!( + "{}.{}.{}.{}.{}", + protected_header_base64, + encrypted_key_base64, + iv_base64, + general_purpose::URL_SAFE_NO_PAD.encode(&ciphertext), + general_purpose::URL_SAFE_NO_PAD.encode(tag), + ); + + assert_eq!( + token, + "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ" + ); + } + + #[rstest] + // Different asymmetrical keys and different symmetrical key sizes + #[case(picky_test_data::EC_NIST256_PK_1, JweAlg::EcdhEs, JweEnc::Aes256Gcm)] + #[case(picky_test_data::EC_NIST384_PK_1, JweAlg::EcdhEs, JweEnc::Aes192Gcm)] + #[case(picky_test_data::X25519_PEM_PK_1, JweAlg::EcdhEs, JweEnc::Aes128Gcm)] + // With key wrapping + #[case(picky_test_data::X25519_PEM_PK_1, JweAlg::EcdhEsAesKeyWrap128, JweEnc::Aes256Gcm)] + #[case(picky_test_data::EC_NIST256_PK_1, JweAlg::EcdhEsAesKeyWrap128, JweEnc::Aes128Gcm)] + #[case(picky_test_data::EC_NIST384_PK_1, JweAlg::EcdhEsAesKeyWrap192, JweEnc::Aes256Gcm)] + #[case(picky_test_data::X25519_PEM_PK_1, JweAlg::EcdhEsAesKeyWrap192, JweEnc::Aes128Gcm)] + #[case(picky_test_data::EC_NIST256_PK_1, JweAlg::EcdhEsAesKeyWrap256, JweEnc::Aes256Gcm)] + #[case(picky_test_data::X25519_PEM_PK_1, JweAlg::EcdhEsAesKeyWrap256, JweEnc::Aes128Gcm)] + fn jwe_ecdh_es_roundtrip(#[case] key_pem: &str, #[case] alg: JweAlg, #[case] enc: JweEnc) { + let private = PrivateKey::from_pem_str(key_pem).unwrap(); + let public = private.to_public_key().unwrap(); + + let payload = b"Hello, world!".to_vec(); + + let encoded = Jwe::new(alg, enc, payload.clone()) + .encode(&public) + .expect("JWE encode failed"); + + let decoded = Jwe::decode(&encoded, &private).expect("JWE decode failed"); + + assert_eq!(decoded.payload, payload); + } + + #[rstest] + #[case(picky_test_data::JOSE_JWE_GCM256_EC_P256_ECDH, picky_test_data::EC_NIST256_PK_1)] + #[case( + picky_test_data::JOSE_JWE_GCM128_EC_P384_ECDH_KW192, + picky_test_data::EC_NIST384_PK_1 + )] + fn picky_understands_jwcrypto(#[case] token: &str, #[case] key_pem: &str) { + // Tokens were generated via `jwcrypto` library. To generate tokens use the following + // code snippet: + // ```python + // from jwcrypto import jwe, jwk + // from jwcrypto.common import json_encode + // pem = "" + // jwk = jwk.JWK.from_pem(pem) + // jwe = jwe.JWE(b'Hello world!', json_encode({'alg': 'ECDH-ES+A256KW', 'enc': 'A192GCM'})) + // jwe.add_recipient(jwk) + // print(jwe.serialize(compact=True)) + // ``` + + let private = PrivateKey::from_pem_str(key_pem).unwrap(); + let decoded = Jwe::decode(token, &private).expect("JWE decode failed"); + assert_eq!(String::from_utf8(decoded.payload).unwrap(), "Hello world!"); + } +} diff --git a/rust/vendor/picky/src/jose/jwk.rs b/rust/vendor/picky/src/jose/jwk.rs new file mode 100644 index 0000000..d1d4951 --- /dev/null +++ b/rust/vendor/picky/src/jose/jwk.rs @@ -0,0 +1,767 @@ +//! A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. +//! +//! See [RFC7517](https://tools.ietf.org/html/rfc7517). + +use crate::jose::jwe::{JweAlg, JweEnc}; +use crate::jose::jws::JwsAlg; +use crate::key::ec::{EcdsaPublicKey, NamedEcCurve}; +use crate::key::ed::{EdPublicKey, NamedEdAlgorithm}; +use crate::key::{EcCurve, EdAlgorithm, PublicKey}; +use base64::engine::general_purpose; +use base64::{DecodeError, Engine as _}; +use crypto_bigint::BoxedUint; +use picky_asn1::wrapper::IntegerAsn1; +use picky_asn1_x509::SubjectPublicKeyInfo; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +// === error type === // + +#[derive(Debug, Error)] +pub enum JwkError { + /// Json error + #[error("JSON error: {source}")] + Json { source: serde_json::Error }, + + /// couldn't decode base64 + #[error("couldn't decode base64: {source}")] + Base64Decoding { source: DecodeError }, + + /// unsupported algorithm + #[error("unsupported algorithm: {algorithm}")] + UnsupportedAlgorithm { algorithm: &'static str }, + + #[error("invalid ec public key: {cause}")] + InvalidEcPublicKey { cause: String }, + + #[error("invalid ec point coordinates in JWK")] + InvalidEcPointCoordinates, + + #[error("invalid ed public key: {cause}")] + InvalidEdPublicKey { cause: String }, +} + +impl From for JwkError { + fn from(e: serde_json::Error) -> Self { + Self::Json { source: e } + } +} + +impl From for JwkError { + fn from(e: DecodeError) -> Self { + Self::Base64Decoding { source: e } + } +} + +// === key type === // + +/// Algorithm type for JWK +/// +/// See [RFC7518 #6](https://tools.ietf.org/html/rfc7518#section-6.1) +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(tag = "kty")] +pub enum JwkKeyType { + /// Edwards curve-based cryptography + /// + /// Defined by separate [RFC 8037](https://www.rfc-editor.org/rfc/rfc8037) + #[serde(rename = "OKP")] + Ed(JwkPublicEdKey), + /// Elliptic Curve + /// + /// Recommended+ by RFC + #[serde(rename = "EC")] + Ec(JwkPublicEcKey), + /// Elliptic Curve + /// + /// Required by RFC + #[serde(rename = "RSA")] + Rsa(JwkPublicRsaKey), + /// Octet sequence (used to represent symmetric keys) (unsupported) + /// + /// Required by RFC + #[serde(rename = "oct")] + Oct, +} + +impl JwkKeyType { + /// Build a JWK key from RSA components. + /// + /// Each argument is the unsigned big-endian representation as an octet sequence of the value. + /// If a signed representation is provided, leading zero is removed for any number bigger than 0x7F. + pub fn new_rsa_key(modulus: &[u8], public_exponent: &[u8]) -> Self { + let modulus = h_strip_unrequired_leading_zero(modulus); + let public_exponent = h_strip_unrequired_leading_zero(public_exponent); + Self::Rsa(JwkPublicRsaKey { + n: general_purpose::URL_SAFE_NO_PAD.encode(modulus), + e: general_purpose::URL_SAFE_NO_PAD.encode(public_exponent), + }) + } + + /// Build a JWK key from EC components. + /// + /// `x` and `y` are big-endian representation of the affine point coordinates. + pub fn new_ec_key(curve: JwkEcPublicKeyCurve, x: &[u8], y: &[u8]) -> Self { + let x = h_strip_unrequired_leading_zero(x); + let y = h_strip_unrequired_leading_zero(y); + Self::Ec(JwkPublicEcKey { + crv: curve, + x: general_purpose::URL_SAFE_NO_PAD.encode(x), + y: general_purpose::URL_SAFE_NO_PAD.encode(y), + }) + } + + /// Build a JWK key from Edwards curve components. + /// + /// `crv` is ed-based algorithm name. + /// `x` is raw public key bytes. + pub fn new_ed_key(crv: JwkEdPublicKeyAlgorithm, x: &[u8]) -> Self { + Self::Ed(JwkPublicEdKey { + crv, + x: general_purpose::URL_SAFE_NO_PAD.encode(x), + }) + } + + /// Build a JWK key from RSA components already encoded following base64 url format. + /// + /// Each argument is the unsigned big-endian representation as an octet sequence of the value. + /// The octet sequence MUST utilize the minimum number of octets needed to represent the value. + /// That is: **no leading zero** must be present. + /// + /// See definition for term `Base64urlUInt` in [RFC7518 section 2](https://datatracker.ietf.org/doc/html/rfc7518#section-2) + pub fn new_rsa_key_from_base64_url(modulus: String, public_exponent: String) -> Self { + Self::Rsa(JwkPublicRsaKey { + n: modulus, + e: public_exponent, + }) + } + + pub fn as_rsa(&self) -> Option<&JwkPublicRsaKey> { + match self { + JwkKeyType::Rsa(rsa) => Some(rsa), + _ => None, + } + } + + pub fn as_ec(&self) -> Option<&JwkPublicEcKey> { + match self { + JwkKeyType::Ec(ec) => Some(ec), + _ => None, + } + } + + pub fn as_ed(&self) -> Option<&JwkPublicEdKey> { + match self { + JwkKeyType::Ed(ed) => Some(ed), + _ => None, + } + } + + pub fn is_rsa(&self) -> bool { + self.as_rsa().is_some() + } + + pub fn is_ec(&self) -> bool { + self.as_ec().is_some() + } + + pub fn is_ed(&self) -> bool { + self.as_ed().is_some() + } +} + +/// Strips leading zero for any number bigger than 0x7F. +fn h_strip_unrequired_leading_zero(value: &[u8]) -> &[u8] { + if let [0x00, rest @ ..] = value { rest } else { value } +} + +/// Big integers from 0x00 to 0x7F are all base64-encoded using two ASCII characters ranging from "AA" to "fw". +/// We know the required capacity is _exactly_ of one byte. +/// The value 0 is valid and is represented as the array [0x00] ("AA"). +/// For numbers greater than 0x7F, logic is a bit more complex. +/// There is no leading zero in JWK keys because _unsigned_ numbers are used. +/// As such, there is no need to disambiguate the high-order bit (0x80) +/// which is used as the sign bit for _signed_ numbers. +/// The high-order bit is set when base64 encoding's leading character matches [g-z0-9_-]. +fn h_allocate_signed_big_int_buffer(base64_url_encoding: &str) -> Vec { + match base64_url_encoding.chars().next() { + // The leading zero is re-introduced for any number whose high-order bit is set + Some('g'..='z' | '0'..='9' | '_' | '-') => vec![0], + // Otherwise, there is nothing more to do + _ => Vec::with_capacity(1), + } +} + +// === public key use === // + +/// Public Key Use, identifies the intended use of the public key. +/// +/// See [RFC7517 #4](https://tools.ietf.org/html/rfc7517#section-4.2) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum JwkPubKeyUse { + #[serde(rename = "sig")] + Signature, + #[serde(rename = "enc")] + Encryption, +} + +// === key operations === // + +/// Key Operations, identifies the operation(s) for which the key is intended to be used. +/// +/// See [RFC7517 #4](https://tools.ietf.org/html/rfc7517#section-4.3) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum JwkKeyOps { + #[serde(rename = "sign")] + Sign, + #[serde(rename = "verify")] + Verify, + #[serde(rename = "encrypt")] + Encrypt, + #[serde(rename = "decrypt")] + Decrypt, + #[serde(rename = "wrapKey")] + WrapKey, + #[serde(rename = "unwrapKey")] + UnwrapKey, + #[serde(rename = "deriveKey")] + DeriveKey, + #[serde(rename = "deriveBits")] + DeriveBits, +} + +// === algorithms === // + +/// JOSE algorithms names as defined by [RFC7518](https://tools.ietf.org/html/rfc7518) +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Jwa { + Sig(JwsAlg), + Enc(JweEnc), + CEKAlg(JweAlg), +} + +// === json web key === // + +/// Represents a cryptographic key as defined by [RFC7517](https://tools.ietf.org/html/rfc7517). +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Jwk { + // -- specific to JWK -- // + #[serde(flatten)] + pub key: JwkKeyType, + + /// Identifies the algorithm intended for use with the key. + #[serde(skip_serializing_if = "Option::is_none")] + pub alg: Option, + + /// Public Key Use + /// + /// Intended use of the public key. + #[serde(rename = "use", skip_serializing_if = "Option::is_none")] + pub key_use: Option, + + /// Key Operations + /// + /// identifies the operation(s) for which the key is intended to be used. + #[serde(skip_serializing_if = "Option::is_none")] + pub key_ops: Option>, + + // -- common with all -- // + /// Key ID Header + /// + /// A hint indicating which key was used. + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, + + /// X.509 URL Header + /// + /// URI that refers to a resource for an X.509 public key certificate or certificate chain. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5u: Option, + + /// X.509 Certificate Chain + /// + /// Chain of one or more PKIX certificates. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5c: Option>, + + /// X.509 Certificate SHA-1 Thumbprint + /// + /// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5t: Option, + + /// X.509 Certificate SHA-256 Thumbprint + /// + /// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + #[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")] + pub x5t_s256: Option, +} + +impl Jwk { + pub fn new(key: JwkKeyType) -> Self { + Jwk { + key, + alg: None, + key_use: None, + key_ops: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + } + } + + pub fn from_json(json: &str) -> Result { + Ok(serde_json::from_str(json)?) + } + + pub fn from_public_key(public_key: &PublicKey) -> Result { + use picky_asn1::wrapper::BitStringAsn1Container; + use picky_asn1_x509::PublicKey as SerdePublicKey; + + match &public_key.as_inner().subject_public_key { + SerdePublicKey::Rsa(BitStringAsn1Container(rsa)) => { + let modulus = rsa.modulus.as_signed_bytes_be(); + let public_exponent = rsa.public_exponent.as_signed_bytes_be(); + Ok(Self::new(JwkKeyType::new_rsa_key(modulus, public_exponent))) + } + SerdePublicKey::Ec(_) => { + let ec_key = EcdsaPublicKey::try_from(public_key) + .map_err(|e| JwkError::InvalidEcPublicKey { cause: e.to_string() })?; + + match ec_key.curve() { + NamedEcCurve::Known(EcCurve::NistP256) => { + let point = p256::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| { + JwkError::InvalidEcPublicKey { + cause: "invalid P-256 EC point encoding".to_string(), + } + })?; + + match (point.x(), point.y()) { + (Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key( + JwkEcPublicKeyCurve::P256, + x.as_slice(), + y.as_slice(), + ))), + _ => Err(JwkError::InvalidEcPublicKey { + cause: "Invalid P-256 curve EC public point coordinates".to_string(), + }), + } + } + NamedEcCurve::Known(EcCurve::NistP384) => { + let point = p384::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| { + JwkError::InvalidEcPublicKey { + cause: "invalid P-384 EC point encoding".to_string(), + } + })?; + + match (point.x(), point.y()) { + (Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key( + JwkEcPublicKeyCurve::P384, + x.as_slice(), + y.as_slice(), + ))), + _ => Err(JwkError::InvalidEcPublicKey { + cause: "Invalid P-384 curve EC public point coordinates".to_string(), + }), + } + } + NamedEcCurve::Known(EcCurve::NistP521) => { + let point = p521::Sec1Point::from_bytes(ec_key.encoded_point()).map_err(|_| { + JwkError::InvalidEcPublicKey { + cause: "invalid P-521 EC point encoding".to_string(), + } + })?; + + match (point.x(), point.y()) { + (Some(x), Some(y)) => Ok(Self::new(JwkKeyType::new_ec_key( + JwkEcPublicKeyCurve::P521, + x.as_slice(), + y.as_slice(), + ))), + _ => Err(JwkError::InvalidEcPublicKey { + cause: "Invalid P-521 curve EC public point coordinates".to_string(), + }), + } + } + NamedEcCurve::Unsupported(_) => Err(JwkError::UnsupportedAlgorithm { + algorithm: "Unsupported EC curve", + }), + } + } + SerdePublicKey::Ed(_) => { + let ed_key = EdPublicKey::try_from(public_key) + .map_err(|e| JwkError::InvalidEdPublicKey { cause: e.to_string() })?; + + let algorithm = match ed_key.algorithm() { + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => JwkEdPublicKeyAlgorithm::Ed25519, + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => JwkEdPublicKeyAlgorithm::X25519, + NamedEdAlgorithm::Unsupported(_) => { + return Err(JwkError::UnsupportedAlgorithm { + algorithm: "Unsupported ED algorithm", + }); + } + }; + + Ok(Self::new(JwkKeyType::new_ed_key(algorithm, ed_key.data()))) + } + SerdePublicKey::Mldsa(_) => Err(JwkError::UnsupportedAlgorithm { + algorithm: "JWK unsupported with MLDSA keys", + }), + } + } + + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } + + pub fn to_json_pretty(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } + + pub fn to_public_key(&self) -> Result { + match &self.key { + JwkKeyType::Rsa(rsa) => { + let modulus = IntegerAsn1::from_bytes_be_signed(rsa.modulus_signed_bytes_be()?); + let public_exponent = IntegerAsn1::from_bytes_be_signed(rsa.public_exponent_signed_bytes_be()?); + let spki = SubjectPublicKeyInfo::new_rsa_key(modulus, public_exponent); + Ok(spki.into()) + } + JwkKeyType::Ec(ec) => { + let curve = match ec.crv { + JwkEcPublicKeyCurve::P256 => EcCurve::NistP256, + JwkEcPublicKeyCurve::P384 => EcCurve::NistP384, + JwkEcPublicKeyCurve::P521 => EcCurve::NistP521, + }; + + let x = BoxedUint::from_be_slice_vartime(&ec.x_signed_bytes_be()?); + let y = BoxedUint::from_be_slice_vartime(&ec.y_signed_bytes_be()?); + + PublicKey::from_ec_components(curve, &x, &y).map_err(|_| JwkError::InvalidEcPointCoordinates) + } + JwkKeyType::Ed(ed) => { + let algorithm = match ed.crv { + JwkEdPublicKeyAlgorithm::Ed25519 => Ok(EdAlgorithm::Ed25519), + JwkEdPublicKeyAlgorithm::X25519 => Ok(EdAlgorithm::X25519), + JwkEdPublicKeyAlgorithm::Ed448 => Err("ed448 algorithm"), + JwkEdPublicKeyAlgorithm::X448 => Err("x448 algorithm"), + } + .map_err(|algorithm| JwkError::UnsupportedAlgorithm { algorithm })?; + + Ok(PublicKey::from_ed_encoded_components( + &algorithm.into(), + ed.public_key_bytes()?.as_ref(), + )) + } + JwkKeyType::Oct => Err(JwkError::UnsupportedAlgorithm { + algorithm: "octet sequence", + }), + } + } +} + +// === jwk set === // + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct JwkSet { + pub keys: Vec, +} + +impl JwkSet { + pub fn from_json(json: &str) -> Result { + Ok(serde_json::from_str(json)?) + } + + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string(self)?) + } + + pub fn to_json_pretty(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } +} + +// === public rsa key === // + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct JwkPublicRsaKey { + n: String, + e: String, +} + +impl JwkPublicRsaKey { + pub fn modulus_signed_bytes_be(&self) -> Result, JwkError> { + let mut buf = h_allocate_signed_big_int_buffer(&self.n); + general_purpose::URL_SAFE_NO_PAD + .decode_vec(&self.n, &mut buf) + .map_err(JwkError::from)?; + Ok(buf) + } + + pub fn modulus_unsigned_bytes_be(&self) -> Result, JwkError> { + general_purpose::URL_SAFE_NO_PAD.decode(&self.n).map_err(JwkError::from) + } + + pub fn public_exponent_signed_bytes_be(&self) -> Result, JwkError> { + let mut buf = h_allocate_signed_big_int_buffer(&self.e); + general_purpose::URL_SAFE_NO_PAD + .decode_vec(&self.e, &mut buf) + .map_err(JwkError::from)?; + Ok(buf) + } + + pub fn public_exponent_unsigned_bytes_be(&self) -> Result, JwkError> { + general_purpose::URL_SAFE_NO_PAD.decode(&self.e).map_err(JwkError::from) + } +} + +/// The key type of a JWK defined in +/// [RFC 7518, section 6.1](https://tools.ietf.org/html/rfc7518#section-6.1). +/// +/// Note that P521 is not supported yet for signning and verification. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum JwkEcPublicKeyCurve { + #[serde(rename = "P-256")] + P256, + #[serde(rename = "P-384")] + P384, + #[serde(rename = "P-521")] + P521, +} + +// === public ec key === // +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct JwkPublicEcKey { + crv: JwkEcPublicKeyCurve, + x: String, + y: String, +} + +impl JwkPublicEcKey { + pub fn x_signed_bytes_be(&self) -> Result, JwkError> { + let mut buf = h_allocate_signed_big_int_buffer(&self.x); + general_purpose::URL_SAFE_NO_PAD + .decode_vec(&self.x, &mut buf) + .map_err(JwkError::from)?; + Ok(buf) + } + + pub fn y_signed_bytes_be(&self) -> Result, JwkError> { + let mut buf = h_allocate_signed_big_int_buffer(&self.y); + general_purpose::URL_SAFE_NO_PAD + .decode_vec(&self.y, &mut buf) + .map_err(JwkError::from)?; + Ok(buf) + } +} + +/// Defined in [RFC 8037](https://tools.ietf.org/html/rfc8037) +/// +/// Note that X25519, Ed448 and X448 are not yet supported by picky for jws/jwe. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum JwkEdPublicKeyAlgorithm { + #[serde(rename = "Ed25519")] + Ed25519, + #[serde(rename = "Ed448")] + Ed448, + #[serde(rename = "X25519")] + X25519, + #[serde(rename = "X448")] + X448, +} + +// === public ed key === // + +/// Defined in [RFC 8037](https://tools.ietf.org/html/rfc8037) +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct JwkPublicEdKey { + /// NOTE: "crv" defines not exactly the curve but the algorithm. + crv: JwkEdPublicKeyAlgorithm, + /// In contrast to EC keys, the `x` coordinate is an octet string, not an encoded big integer. + x: String, +} + +impl JwkPublicEdKey { + pub fn public_key_bytes(&self) -> Result, JwkError> { + let mut buf = Vec::new(); + general_purpose::URL_SAFE_NO_PAD + .decode_vec(&self.x, &mut buf) + .map_err(JwkError::from)?; + Ok(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::jose::jws::JwsAlg; + use crate::pem::Pem; + use rstest::rstest; + + const RSA_MODULUS: &str = "rpJjxW0nNZiq1mPC3ZAxqf9qNjmKurP7XuKrpWrfv3IOUldqChQVPNg8zCvDOMZIO-ZDuRmVH\ + EZ5E1vz5auHNACnpl6AvDGJ-4qyX42vfUDMNZx8i86d7bQpwJkO_MVMLj8qMGmTVbQ8zqVw2z\ + MyKUFfa2V83nvx2wz4FJh2Thw2uZX2P7h8nlDVSuXO0wJ_OY_2qtqRIAnNXMzL5BF5pEFh4hi\ + JIFiMTNkhVtUjT1QSB9E8DtDme8g4u769Oc0My45fgqSNE7kKKyaDhTfqSovyhj-qWiD-X_Gw\ + pWkW4ungpHzz_97-ZDB3yQ7AMwKAsw5EW2cMqseAp3f-kf159w"; + + const RSA_PUBLIC_EXPONENT: &str = "AQAB"; + + const X509_SHA1_THUMBPRINT: &str = "N3ORVnr9T6opxpS9iRbkKGwKiQI"; + + const X509_CERT_0: &str = "MIIDWjCCAkKgAwIBAgIUWRsBqKmpXGP/OwrwLWicwxhuCFowDQYJKoZIhvc\ + NAQELBQAwKjEoMCYGA1UEAwwfbG9naW4uZGV2b2x1dGlvbnMuY29tIEF1dG\ + hvcml0eTAeFw0xOTAzMTMxMzE1MzVaFw0yMDAzMTIxMzE1MzVaMCYxJDAiB\ + gNVBAMMG2xvZ2luLmRldm9sdXRpb25zLmNvbSBUb2tlbjCCASIwDQYJKoZI\ + hvcNAQEBBQADggEPADCCAQoCggEBAK6SY8VtJzWYqtZjwt2QMan/ajY5irq\ + z+17iq6Vq379yDlJXagoUFTzYPMwrwzjGSDvmQ7kZlRxGeRNb8+WrhzQAp6\ + ZegLwxifuKsl+Nr31AzDWcfIvOne20KcCZDvzFTC4/KjBpk1W0PM6lcNszM\ + ilBX2tlfN578dsM+BSYdk4cNrmV9j+4fJ5Q1UrlztMCfzmP9qrakSAJzVzM\ + y+QReaRBYeIYiSBYjEzZIVbVI09UEgfRPA7Q5nvIOLu+vTnNDMuOX4KkjRO\ + 5Cismg4U36kqL8oY/qlog/l/xsKVpFuLp4KR88//e/mQwd8kOwDMCgLMORF\ + tnDKrHgKd3/pH9efcCAwEAAaN8MHowCQYDVR0TBAIwADAOBgNVHQ8BAf8EB\ + AMCBeAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW\ + BBQQW2Cx8HUpXfFM3B76WzBb/BhCBDAfBgNVHSMEGDAWgBRWAUlOiE4Z3ww\ + aHgz284/sYB9NaDANBgkqhkiG9w0BAQsFAAOCAQEAkliCiJF9Z/Y57V6Rrn\ + gHCBBWtqR+N/A+KHQqWxP2MmJiHVBBnZAueVPsvykO+EfbazNEkUoPVKhUd\ + 5NxEmTEMOBu9HUEzlmA5xDjl5xS7fejJIr7pgbxIup4m+DsNsPVnF1Snk56\ + F6660RhRb9fsHQ0pgvWuG+tQXJ4J1Zi0cp+xi4yze6hJGAyAqj6wU46AUiL\ + 6kUr9GUVHqEsl5mNMIW18JT4KM/s5DWxFGO2soSTkaVHwGSkMBQSTgHMWs0\ + L3bBfimjw9FwjwwHAbe1W5QU6uVXGApuKANRsXxgCn566QkE/BuV3WVR6uy\ + n2P1J/vU9hxasgRIcjf3jHC4lGpew=="; + + const X509_CERT_1: &str = "MIIDRjCCAi6gAwIBAgIUUqhc3/U6OhKtEk1b8JfX3GL0FPYwDQYJKoZIhvc\ + NAQELBQAwKDEmMCQGA1UEAwwdbG9naW4uZGV2b2x1dGlvbnMuY29tIFJvb3\ + QgQ0EwHhcNMTkwMzEzMTMxNTM1WhcNMjAwMzEyMTMxNTM1WjAqMSgwJgYDV\ + QQDDB9sb2dpbi5kZXZvbHV0aW9ucy5jb20gQXV0aG9yaXR5MIIBIjANBgkq\ + hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlbRwXVPc/WH4t/Yti5qv24pAu8Q\ + m0eOVvbum23bYtfJDbCSDh7sY/vvQXgIkM8/0C3tFZ3XaXHbyDHAMn6OC+S\ + Obzs6SjpfKk9s69Yo/aWFl9oRnAK/+dZ0Y6MTdZO1w+PpR81q5QOFMLpWX1\ + YNdahaZec31sBmsHqlW04OrHUhGOTGdWNots9/PWvN//x++FL+Sqgh/jxF7\ + khbgfAuz1QKa8P0ZlE4cOcRIs5bSnUFwtoytKH02/YZnCJD7I/iXFuCPV/+\ + LZO6yobkTREE3npeXvAKr1OKF2F0JVORMhHiYyguh9t3bMwHTCFqmfQkIMD\ + GjaTJD7bd8y2Au+eDzgwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDV\ + R0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUVgFJTohOGd8MGh4M9vOP7GAf\ + TWgwHwYDVR0jBBgwFoAU42BA1coGHUUPUSeacQfTzicjosgwDQYJKoZIhvc\ + NAQELBQADggEBAKyyDs+uIughmloEmlf8s1cSP8cLtC1Di2TfYSG0bpEM3B\ + EPTond/7ujDlv0eug9NRurvWd5v7bWvy9VlJo+x2rLBmkzaNcBSVHZ4UbFU\ + 90MSvHjxNZ7VbUfbWsJVeaYHtqf1m3z0fYT0tUor3chD+wbSqraWw4+t54h\ + fJl22jExTWS9X0F5/Gf3LQOiOvtjHP+b3VkpXkEPIBbvIO/X6kgoGDLm/lA\ + IPdZmpI956z5+acLHu3AQkxNXQPzCjSSdJphLVU1XeHXOMWldVtE9BqSMVI\ + HZ6oCz/FtMA4F6R7WiVXXGR+ywRwFyeiFoRea2ImUK9TRWFsaXKeOBMm+TL\ + bk="; + + const X509_CERT_2: &str = "MIIDRDCCAiygAwIBAgIUCAKwhsjTttdG4koEAV7zqlnI7wkwDQYJKoZIhvc\ + NAQELBQAwKDEmMCQGA1UEAwwdbG9naW4uZGV2b2x1dGlvbnMuY29tIFJvb3\ + QgQ0EwHhcNMTkwMzEzMTMxNTM1WhcNMjQwMzExMTMxNTM1WjAoMSYwJAYDV\ + QQDDB1sb2dpbi5kZXZvbHV0aW9ucy5jb20gUm9vdCBDQTCCASIwDQYJKoZI\ + hvcNAQEBBQADggEPADCCAQoCggEBANRZxxg9eTCMVr4DsIUcytQOLnlZ7tl\ + uliP+jM76mjJEuWqizHzZ1ZoPcEbdW9sV8kgWdPHL3KOlXAr0DEobnhQsNx\ + uzJ8B73TcV7AKp2HR+xCTKPEha1gVHgQMmzQyCIgLEsdcjhsFeFYqMflELZ\ + rMy+7DBSZWWf3wCnxiKbzTL01wKqylVWeSiXsniTpsoUSSk8Fe2/Li8dBMY\ + he1vTb57GI8ta24P4lfJv6CPTNTVsr+6ue3lRuY/UIMNTybhBSc00qbuo0K\ + ahWHyzDgY+iNEaALbyWeNOoTBQIO8lp4mhHcO/Znh2PxdqCi/FSCB2+A1Xd\ + uOArn+MKegU5aVJN0CAwEAAaNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBAjAOB\ + gNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFONgQNXKBh1FD1EnmnEH084nI6LI\ + MB8GA1UdIwQYMBaAFONgQNXKBh1FD1EnmnEH084nI6LIMA0GCSqGSIb3DQE\ + BCwUAA4IBAQB+v34Vk/+qQgA7eWlczWNVWM0J67om+QwtMEo+VgzE2OHNID\ + 2o5QXsxcck0j8dANutkoqsUXpos/RG+QPNng5RBWA/sWUYWdfwZgrE30rBK\ + waP8Yi8gVsZpz3/RClbPcfkUXI12ANw3bRI1TscOK165p1TV6nmeEus5LZq\ + CJV37/WRt47CccsDNZaqSN7T5lQ045jsZVYpfgx/I1l9Q/fICrTOFwqYbXJ\ + 9DTe1v8C+LFbtTNcEzRGwZefLTNH2yuZjGy1/t4+cnmFJUzmC4abOoZcpkr\ + z6U68caCbQA+wdmFs4XaO2bFaiyM+m0LVMOQfLuX/0RZc2KB7fAbb7oHQl"; + + fn get_jwk_set() -> JwkSet { + JwkSet { + keys: vec![Jwk { + alg: Some(Jwa::Sig(JwsAlg::RS256)), + key_ops: Some(vec![JwkKeyOps::Verify]), + kid: Some("bG9naW4uZGV2b2x1dGlvbnMuY29tIFRva2VuLk1hciAxMyAxMzoxNTozNSAyMDE5IEdNVA".to_owned()), + x5t: Some(X509_SHA1_THUMBPRINT.to_owned()), + x5c: Some(vec![ + X509_CERT_0.to_owned(), + X509_CERT_1.to_owned(), + X509_CERT_2.to_owned(), + ]), + ..Jwk::new(JwkKeyType::new_rsa_key_from_base64_url( + RSA_MODULUS.into(), + RSA_PUBLIC_EXPONENT.into(), + )) + }], + } + } + + #[test] + fn rsa_key() { + let expected = get_jwk_set(); + let decoded = JwkSet::from_json(picky_test_data::JOSE_JWK_SET).unwrap(); + pretty_assertions::assert_eq!(decoded, expected); + + let encoded = expected.to_json_pretty().unwrap(); + let decoded = JwkSet::from_json(&encoded).unwrap(); + pretty_assertions::assert_eq!(decoded, expected); + } + + #[rstest] + #[case(picky_test_data::JOSE_JWK_EC_P256_JSON)] + #[case(picky_test_data::JOSE_JWK_EC_P384_JSON)] + #[case(picky_test_data::JOSE_JWK_EC_P521_JSON)] + fn ecdsa_key_roundtrip(#[case] json: &str) { + let decoded = Jwk::from_json(json).unwrap(); + let encoded = decoded.to_json().unwrap(); + pretty_assertions::assert_eq!(encoded, json); + } + + #[rstest] + #[case(picky_test_data::JOSE_JWK_ED25519_JSON)] + #[case(picky_test_data::JOSE_JWK_X25519_JSON)] + fn ed_key_roundtrip(#[case] json: &str) { + let decoded = Jwk::from_json(json).unwrap(); + let encoded = decoded.to_json().unwrap(); + pretty_assertions::assert_eq!(encoded, json); + } + + const PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA61BjmfXGEvWmegnBGSuS ++rU9soUg2FnODva32D1AqhwdziwHINFaD1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBS +EVCgJjtHAGZIm5GL/KA86KDp/CwDFMSwluowcXwDwoyinmeOY9eKyh6aY72xJh7n +oLBBq1N0bWi1e2i+83txOCg4yV2oVXhBo8pYEJ8LT3el6Smxol3C1oFMVdwPgc0v +Tl25XucMcG/ALE/KNY6pqC2AQ6R2ERlVgPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeu +lmCpGSynXNcpZ/06+vofGi/2MlpQZNhHAo8eayMp6FcvNucIpUndo1X8dKMv3Y26 +ZQIDAQAB +-----END PUBLIC KEY-----"#; + + #[test] + fn x509_and_jwk_conversion_rsa() { + let initial_key = PublicKey::from_pem(&PUBLIC_KEY_PEM.parse::().expect("pem")).expect("public key"); + let jwk = Jwk::from_public_key(&initial_key).unwrap(); + if let JwkKeyType::Rsa(rsa_key) = &jwk.key { + let modulus = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.n).unwrap(); + assert_ne!(modulus[0], 0x00); + let public_exponent = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.e).unwrap(); + assert_ne!(public_exponent[0], 0x00); + } else { + panic!("Unexpected key type"); + } + let from_jwk_key = jwk.to_public_key().unwrap(); + assert_eq!(from_jwk_key, initial_key); + } + + #[rstest] + #[case(picky_test_data::EC_NIST256_PK_1_PUB)] + #[case(picky_test_data::EC_NIST384_PK_1_PUB)] + fn x509_and_jwk_conversion_ec(#[case] pem: &str) { + let initial_key = PublicKey::from_pem(&pem.parse::().expect("pem")).expect("public key"); + let jwk = Jwk::from_public_key(&initial_key).unwrap(); + if let JwkKeyType::Ec(rsa_key) = &jwk.key { + let x = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.x).unwrap(); + assert_ne!(x[0], 0x00); + let y = general_purpose::URL_SAFE_NO_PAD.decode(&rsa_key.y).unwrap(); + assert_ne!(y[0], 0x00); + } else { + panic!("Unexpected key type"); + } + let from_jwk_key = jwk.to_public_key().unwrap(); + assert_eq!(from_jwk_key, initial_key); + } +} diff --git a/rust/vendor/picky/src/jose/jws.rs b/rust/vendor/picky/src/jose/jws.rs new file mode 100644 index 0000000..2148c9a --- /dev/null +++ b/rust/vendor/picky/src/jose/jws.rs @@ -0,0 +1,668 @@ +//! JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based data structures. +//! +//! See [RFC7515](https://tools.ietf.org/html/rfc7515). + +use crate::hash::HashAlgorithm; +use crate::jose::jwk::Jwk; +use crate::key::{EcCurve, PrivateKey, PublicKey}; +use crate::signature::{SignatureAlgorithm, SignatureError}; +use base64::engine::general_purpose; +use base64::{DecodeError, Engine as _}; +use picky_asn1::wrapper::IntegerAsn1; +use picky_asn1_x509::signature::EcdsaSignatureValue; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::collections::HashMap; +use thiserror::Error; + +// === error type === // + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum JwsError { + /// RSA error + #[error("RSA error: {context}")] + Rsa { context: String }, + + /// Json error + #[error("JSON error: {source}")] + Json { source: serde_json::Error }, + + /// signature error + #[error("signature error: {source}")] + Signature { source: SignatureError }, + + /// invalid token encoding + #[error("input isn't a valid token string: {input}")] + InvalidEncoding { input: String }, + + /// couldn't decode base64 + #[error("couldn't decode base64: {source}")] + Base64Decoding { source: DecodeError }, + + /// input isn't valid utf8 + #[error("input isn't valid utf8: {source}, input: {input:?}")] + InvalidUtf8 { + source: std::string::FromUtf8Error, + input: Vec, + }, +} + +impl From for JwsError { + fn from(e: rsa::errors::Error) -> Self { + Self::Rsa { context: e.to_string() } + } +} + +impl From for JwsError { + fn from(e: serde_json::Error) -> Self { + Self::Json { source: e } + } +} + +impl From for JwsError { + fn from(e: SignatureError) -> Self { + Self::Signature { source: e } + } +} + +impl From for JwsError { + fn from(e: DecodeError) -> Self { + Self::Base64Decoding { source: e } + } +} + +// === JWS algorithms === // + +mod jws_alg { + // We use #[deprecated] for `JwsAlg::ED22519` to tell users that this algorithm is invalid + // (`JwsAlg::EdDSA` should be used instead) but we should keep it inside enum to allow decoding + // of invalid tokens generated by other libraries such as `golang-jws`. However, to avoid + // compilation warnings caused by enum derives, we need to allow deprecated items inside this + // module. + #![allow(deprecated)] + + use super::*; + + /// `alg` header parameter values for JWS + /// + /// [JSON Web Algorithms (JWA) draft-ietf-jose-json-web-algorithms-40 #3](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40#section-3.1) + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] + pub enum JwsAlg { + /// HMAC using SHA-256 (unsupported) + /// + /// Required by RFC + HS256, + + /// HMAC using SHA-384 (unsupported) + HS384, + + /// HMAC using SHA-512 (unsupported) + HS512, + + /// RSASSA-PKCS-v1_5 using SHA-256 + /// + /// Recommended by RFC + RS256, + + /// RSASSA-PKCS-v1_5 using SHA-384 + RS384, + + /// RSASSA-PKCS-v1_5 using SHA-512 + RS512, + + /// ECDSA using P-256 and SHA-256 + /// + /// Recommended+ by RFC + ES256, + + /// ECDSA using P-384 and SHA-384 + ES384, + + /// ECDSA using P-521 and SHA-512 + ES512, + + /// RSASSA-PSS using SHA-256 and MGF1 with SHA-256 (unsupported) + PS256, + + /// RSASSA-PSS using SHA-384 and MGF1 with SHA-384 (unsupported) + PS384, + + /// RSASSA-PSS using SHA-512 and MGF1 with SHA-512 (unsupported) + PS512, + + /// EdDSA using Ed25519/Ed448 + EdDSA, + + /// [DO NOT USE] EdDSA using Ed25519 + /// + /// This value is used by some popular libraries (e.g. `golang-jwt) instead of `EdDSA` due to + /// mistake in the implementation. This value is deprecated and should not be used. + #[deprecated(note = "You should not use this value, but it may appear in the wild")] + ED25519, + } +} +// Hack to localize #![allow(deprecated)] +pub use jws_alg::JwsAlg; + +impl TryFrom for JwsAlg { + type Error = SignatureError; + + fn try_from(v: SignatureAlgorithm) -> Result { + match v { + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => Ok(Self::RS256), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => Ok(Self::RS384), + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => Ok(Self::RS512), + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => Ok(Self::ES256), + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => Ok(Self::ES384), + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512) => Ok(Self::ES512), + SignatureAlgorithm::Ed25519 => Ok(Self::EdDSA), + unsupported => Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("{unsupported:?}"), + }), + } + } +} + +impl TryFrom for SignatureAlgorithm { + type Error = SignatureError; + + fn try_from(v: JwsAlg) -> Result { + match v { + JwsAlg::RS256 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)), + JwsAlg::RS384 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)), + JwsAlg::RS512 => Ok(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)), + JwsAlg::ES256 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)), + JwsAlg::ES384 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)), + JwsAlg::ES512 => Ok(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512)), + JwsAlg::EdDSA => Ok(SignatureAlgorithm::Ed25519), + #[allow(deprecated)] + JwsAlg::ED25519 => Ok(SignatureAlgorithm::Ed25519), + unsupported => Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("{unsupported:?}"), + }), + } + } +} + +// === JWS header === // + +/// JOSE header of a JWS +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct JwsHeader { + // -- specific to JWS -- // + /// Algorithm Header + /// + /// identifies the cryptographic algorithm used to secure the JWS. + pub alg: JwsAlg, + + // -- common with JWE -- // + /// JWK Set URL + /// + /// URI that refers to a resource for a set of JSON-encoded public keys, + /// one of which corresponds to the key used to digitally sign the JWS. + #[serde(skip_serializing_if = "Option::is_none")] + pub jku: Option, + + /// JSON Web Key + /// + /// The public key that corresponds to the key used to digitally sign the JWS. + /// This key is represented as a JSON Web Key (JWK). + #[serde(skip_serializing_if = "Option::is_none")] + pub jwk: Option, + + /// Type header + /// + /// Used by JWS applications to declare the media type [IANA.MediaTypes] of this complete JWS. + #[serde(skip_serializing_if = "Option::is_none")] + pub typ: Option, + + /// Content Type header + /// + /// Used by JWS applications to declare the media type [IANA.MediaTypes] of the secured content (the payload). + #[serde(skip_serializing_if = "Option::is_none")] + pub cty: Option, + + // -- common with all -- // + /// Key ID Header + /// + /// A hint indicating which key was used. + #[serde(skip_serializing_if = "Option::is_none")] + pub kid: Option, + + /// X.509 URL Header + /// + /// URI that refers to a resource for an X.509 public key certificate or certificate chain. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5u: Option, + + /// X.509 Certificate Chain + /// + /// Chain of one or more PKIX certificates. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5c: Option>, + + /// X.509 Certificate SHA-1 Thumbprint + /// + /// base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + #[serde(skip_serializing_if = "Option::is_none")] + pub x5t: Option, + + /// X.509 Certificate SHA-256 Thumbprint + /// + /// base64url-encoded SHA-256 thumbprint (a.k.a. digest) of the DER encoding of an X.509 certificate. + #[serde(rename = "x5t#S256", alias = "x5t#s256", skip_serializing_if = "Option::is_none")] + pub x5t_s256: Option, + + // -- extra parameters -- // + /// Additional header parameters (both public and private) + #[serde(flatten)] + pub additional: HashMap, +} + +impl JwsHeader { + pub fn new(alg: JwsAlg) -> Self { + Self { + alg, + jku: None, + jwk: None, + typ: None, + cty: None, + kid: None, + x5u: None, + x5c: None, + x5t: None, + x5t_s256: None, + additional: HashMap::new(), + } + } + + pub fn new_with_cty(alg: JwsAlg, cty: impl Into) -> Self { + Self { + cty: Some(cty.into()), + ..Self::new(alg) + } + } +} + +// === json web signature === // + +/// Provides an API to sign any kind of data (binary). JSON claims are part of `Jwt` only. +#[derive(Debug, Clone)] +pub struct Jws { + pub header: JwsHeader, + pub payload: Vec, +} + +impl Jws { + pub fn new(alg: JwsAlg, payload: Vec) -> Self { + Self { + header: JwsHeader::new(alg), + payload, + } + } + + pub fn encode(&self, private_key: &PrivateKey) -> Result { + let header_base64 = general_purpose::URL_SAFE_NO_PAD.encode(serde_json::to_vec(&self.header)?); + let payload_base64 = general_purpose::URL_SAFE_NO_PAD.encode(&self.payload); + let header_and_payload = [header_base64, payload_base64].join("."); + let signature_algo = SignatureAlgorithm::try_from(self.header.alg)?; + let signature = signature_algo.sign(header_and_payload.as_bytes(), private_key)?; + + let signature = match self.header.alg { + // EC sugnatures have special encoding rules (RFC 7518, section 3.4) + JwsAlg::ES256 | JwsAlg::ES384 | JwsAlg::ES512 => { + // Parse signature as ASN.1 DER sequence + let signature: EcdsaSignatureValue = + picky_asn1_der::from_bytes(&signature).map_err(|e| SignatureError::Ec { + context: format!("Invalid EC DER signature encoding: {e}"), + })?; + + let curve = match self.header.alg { + JwsAlg::ES256 => EcCurve::NistP256, + JwsAlg::ES384 => EcCurve::NistP384, + JwsAlg::ES512 => EcCurve::NistP521, + _ => unreachable!("Checked in match above"), + }; + + let signature_component_size = curve.field_bytes_size(); + + let r = signature.r.as_unsigned_bytes_be(); + let s = signature.s.as_unsigned_bytes_be(); + + // We should add zero padding (leading zeros) for R & S components to match the + // size of the curve, as ASN.1 DER encoding removes leading zeros. + let mut jws_signature = Vec::with_capacity(signature_component_size * 2); + + let r_padding = signature_component_size - r.len(); + (0..r_padding).for_each(|_| jws_signature.push(0)); + jws_signature.extend_from_slice(r); + + let s_padding = signature_component_size - s.len(); + (0..s_padding).for_each(|_| jws_signature.push(0)); + jws_signature.extend_from_slice(s); + + jws_signature + } + _ => signature, + }; + + let signature_base64 = general_purpose::URL_SAFE_NO_PAD.encode(signature); + Ok([header_and_payload, signature_base64].join(".")) + } + + /// Verifies signature and returns decoded JWS payload. + pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result { + RawJws::decode(encoded_token).and_then(|raw_jws| raw_jws.verify(public_key)) + } +} + +/// Raw low-level interface to the yet to be verified JWS token. +/// +/// This is useful to inspect the structure before performing further processing. +/// For most usecases, use `Jws` directly. +#[derive(Debug, Clone)] +pub struct RawJws<'repr> { + pub compact_repr: Cow<'repr, str>, + pub header: JwsHeader, + payload: Vec, + pub signature: Vec, +} + +/// An owned `RawJws` for convenience. +pub type OwnedRawJws = RawJws<'static>; + +impl<'repr> RawJws<'repr> { + /// Decodes a JWS in compact representation. + pub fn decode(compact_repr: impl Into>) -> Result { + decode_impl(compact_repr.into()) + } + + /// Peeks the payload before signature verification. + pub fn peek_payload(&self) -> &[u8] { + &self.payload + } + + /// Verifies signature and returns a verified `Jws` structure. + pub fn verify(self, public_key: &PublicKey) -> Result { + verify_signature(&self.compact_repr, public_key, self.header.alg)?; + Ok(self.discard_signature()) + } + + /// Discards the signature without verifying it and hands a `Jws` structure. + /// + /// Generally, you should not do that. + pub fn discard_signature(self) -> Jws { + Jws { + header: self.header, + payload: self.payload, + } + } +} + +fn decode_impl(compact_repr: Cow<'_, str>) -> Result, JwsError> { + let first_dot_idx = compact_repr.find('.').ok_or_else(|| JwsError::InvalidEncoding { + input: compact_repr.clone().into_owned(), + })?; + + let last_dot_idx = compact_repr.rfind('.').ok_or_else(|| JwsError::InvalidEncoding { + input: compact_repr.clone().into_owned(), + })?; + + if first_dot_idx == last_dot_idx || compact_repr.starts_with('.') || compact_repr.ends_with('.') { + return Err(JwsError::InvalidEncoding { + input: compact_repr.into_owned(), + }); + } + + let header_json = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[..first_dot_idx])?; + let header = serde_json::from_slice::(&header_json)?; + + let signature = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[last_dot_idx + 1..])?; + + let payload = general_purpose::URL_SAFE_NO_PAD.decode(&compact_repr[first_dot_idx + 1..last_dot_idx])?; + + Ok(RawJws { + compact_repr, + header, + payload, + signature, + }) +} + +/// JWS verification primitive +pub fn verify_signature(encoded_token: &str, public_key: &PublicKey, algorithm: JwsAlg) -> Result<(), JwsError> { + let last_dot_idx = encoded_token.rfind('.').ok_or_else(|| JwsError::InvalidEncoding { + input: encoded_token.to_owned(), + })?; + + if encoded_token.ends_with('.') { + return Err(JwsError::InvalidEncoding { + input: encoded_token.to_owned(), + }); + } + + let signature = general_purpose::URL_SAFE_NO_PAD.decode(&encoded_token[last_dot_idx + 1..])?; + let signature_algo = SignatureAlgorithm::try_from(algorithm)?; + + let signature = match algorithm { + // Special decoding rules for ECDSA + JwsAlg::ES256 | JwsAlg::ES384 | JwsAlg::ES512 => { + let curve = match algorithm { + JwsAlg::ES256 => EcCurve::NistP256, + JwsAlg::ES384 => EcCurve::NistP384, + JwsAlg::ES512 => EcCurve::NistP521, + _ => unreachable!("Checked in match above"), + }; + + let component_size = curve.field_bytes_size(); + let jws_encoded_signature_size = component_size * 2; + + if signature.len() != jws_encoded_signature_size { + return Err(SignatureError::Ec { + context: format!( + "Invalid JWS EC signature size. Expected: {}; Actual: {}", + jws_encoded_signature_size, + signature.len() + ), + } + .into()); + } + + let (r, s) = signature.split_at(component_size); + + let signature = EcdsaSignatureValue { + r: IntegerAsn1::from_bytes_be_unsigned(r.to_vec()), + s: IntegerAsn1::from_bytes_be_unsigned(s.to_vec()), + }; + + picky_asn1_der::to_vec(&signature).map_err(|e| SignatureError::Ec { + context: format!("Failed to encode EC signature to DER format: {e}"), + })? + } + _ => signature, + }; + + signature_algo.verify(public_key, &encoded_token.as_bytes()[..last_dot_idx], &signature)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pem::Pem; + use rstest::rstest; + + const PAYLOAD: &str = r#"{"sub":"1234567890","name":"John Doe","admin":true,"iat":1516239022}"#; + + fn get_private_key_1() -> PrivateKey { + let pk_pem = picky_test_data::RSA_2048_PK_1.parse::().unwrap(); + PrivateKey::from_pem(&pk_pem).unwrap() + } + + fn get_private_key_2() -> PrivateKey { + let pk_pem = picky_test_data::RSA_2048_PK_7.parse::().unwrap(); + PrivateKey::from_pem(&pk_pem).unwrap() + } + + #[test] + fn encode_rsa_sha256() { + let jwt = Jws { + header: JwsHeader { + typ: Some(String::from("JWT")), + ..JwsHeader::new(JwsAlg::RS256) + }, + payload: PAYLOAD.as_bytes().to_vec(), + }; + let encoded = jwt.encode(&get_private_key_1()).unwrap(); + assert_eq!(encoded, picky_test_data::JOSE_JWT_SIG_EXAMPLE); + } + + #[rstest] + #[case(JwsAlg::ES256, picky_test_data::EC_NIST256_PK_1)] + #[case(JwsAlg::ES384, picky_test_data::EC_NIST384_PK_1)] + #[case(JwsAlg::ES512, picky_test_data::EC_NIST521_PK_1)] + fn ecdsa_sign_verify(#[case] alg: JwsAlg, #[case] key_pem: &str) { + let key = PrivateKey::from_pem_str(key_pem).unwrap(); + + let jwt = Jws { + header: JwsHeader { + typ: Some(String::from("JWT")), + ..JwsHeader::new(alg) + }, + payload: PAYLOAD.as_bytes().to_vec(), + }; + + // Check encode + sign + let encoded = jwt.encode(&key).unwrap(); + + // Check decode + verify + let jws = RawJws::decode(&encoded).unwrap(); + jws.clone().verify(&key.to_public_key().unwrap()).unwrap(); + + assert_eq!(&jws.header, &jwt.header); + assert_eq!(&jws.payload, &jwt.payload); + } + + #[rstest] + #[case(picky_test_data::EC_NIST256_PK_1, picky_test_data::JOSE_JWT_SIG_ES256)] + #[case(picky_test_data::EC_NIST384_PK_1, picky_test_data::JOSE_JWT_SIG_ES384)] + #[case(picky_test_data::EC_NIST521_PK_1, picky_test_data::JOSE_JWT_SIG_ES512)] + fn ecdsa_parse_and_verify(#[case] key_pem: &str, #[case] signature: &str) { + let key = PrivateKey::from_pem_str(key_pem).unwrap(); + + // Check decode + verify + let jws = RawJws::decode(signature).unwrap(); + jws.clone().verify(&key.to_public_key().unwrap()).unwrap(); + } + + const JWT_ED25519_BODY: &str = r#"{"username":"kataras"}"#; + const JWT_ED25519_GO_BODY: &str = r#"{"foo":"bar"}"#; + + /// Validate that invalid header with alg set to "ED25519" could be parsed by picky + #[rstest] + #[allow(deprecated)] + #[case( + picky_test_data::JOSE_JWT_SIG_ED25519_GO_PRIVATE_KEY, + picky_test_data::JOSE_JWT_SIG_ED25519_GO, + JwsAlg::ED25519, + JWT_ED25519_GO_BODY + )] + #[case( + picky_test_data::JOSE_JWT_SIG_ED25519_PRIVATE_KEY, + picky_test_data::JOSE_JWT_SIG_ED25519, + JwsAlg::EdDSA, + JWT_ED25519_BODY + )] + fn ed25519_algorithm(#[case] key: &str, #[case] encoded_expected: &str, #[case] alg: JwsAlg, #[case] body: &str) { + let key = PrivateKey::from_pem_str(key).unwrap(); + + let jwt = Jws { + header: JwsHeader { + typ: Some(String::from("JWT")), + ..JwsHeader::new(alg) + }, + payload: body.as_bytes().to_vec(), + }; + + // Check encode + sign + let encoded = jwt.encode(&key).unwrap(); + assert_eq!(encoded, encoded_expected); + + // Check decode + verify + let jws = RawJws::decode(&encoded).unwrap(); + jws.verify(&key.to_public_key().unwrap()).unwrap(); + } + + #[test] + fn decode_rsa_sha256() { + let public_key = get_private_key_1().to_public_key().unwrap(); + let jwt = Jws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key).unwrap(); + assert_eq!(jwt.payload.as_slice(), PAYLOAD.as_bytes()); + } + + #[test] + fn decode_rsa_sha256_delayed_signature_check() { + let jws = RawJws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE).unwrap(); + println!("{}", String::from_utf8_lossy(&jws.payload)); + assert_eq!(jws.peek_payload(), PAYLOAD.as_bytes()); + + let public_key = get_private_key_2().to_public_key().unwrap(); + let err = jws.verify(&public_key).err().unwrap(); + assert_eq!(err.to_string(), "signature error: invalid signature"); + } + + #[test] + fn decode_rsa_sha256_invalid_signature_err() { + let public_key = get_private_key_2().to_public_key().unwrap(); + let err = Jws::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key) + .err() + .unwrap(); + assert_eq!(err.to_string(), "signature error: invalid signature"); + } + + #[test] + fn decode_invalid_base64_err() { + let public_key = get_private_key_1().to_public_key().unwrap(); + let err = Jws::decode("aieoè~†.tésp.à", &public_key).err().unwrap(); + assert_eq!( + err.to_string(), + "couldn\'t decode base64: Invalid symbol 195, offset 4." + ); + } + + #[test] + fn decode_invalid_json_err() { + let public_key = get_private_key_1().to_public_key().unwrap(); + + let err = Jws::decode("abc.abc.abc", &public_key).err().unwrap(); + assert_eq!(err.to_string(), "JSON error: expected value at line 1 column 1"); + + let err = Jws::decode("eyAiYWxnIjogIkhTMjU2IH0K.abc.abc", &public_key) + .err() + .unwrap(); + assert_eq!( + err.to_string(), + "JSON error: control character (\\u0000-\\u001F) \ + found while parsing a string at line 2 column 0" + ); + } + + #[test] + fn decode_invalid_encoding_err() { + let public_key = get_private_key_1().to_public_key().unwrap(); + + let err = Jws::decode(".abc.abc", &public_key).err().unwrap(); + assert_eq!(err.to_string(), "input isn\'t a valid token string: .abc.abc"); + + let err = Jws::decode("abc.abc.", &public_key).err().unwrap(); + assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc."); + + let err = Jws::decode("abc.abc", &public_key).err().unwrap(); + assert_eq!(err.to_string(), "input isn\'t a valid token string: abc.abc"); + + let err = Jws::decode("abc", &public_key).err().unwrap(); + assert_eq!(err.to_string(), "input isn\'t a valid token string: abc"); + } +} diff --git a/rust/vendor/picky/src/jose/jwt.rs b/rust/vendor/picky/src/jose/jwt.rs new file mode 100644 index 0000000..f83b81a --- /dev/null +++ b/rust/vendor/picky/src/jose/jwt.rs @@ -0,0 +1,690 @@ +use super::jwe::Jwe; +use crate::jose::jwe::{JweAlg, JweEnc, JweError, JweHeader}; +use crate::jose::jws::{Jws, JwsAlg, JwsError, JwsHeader}; +use crate::key::{PrivateKey, PublicKey}; +use core::fmt; +use serde::Serialize; +use serde::de::DeserializeOwned; +use thiserror::Error; + +// === error type === // + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum JwtError { + /// JWS error + #[error("JWS error: {source}")] + Jws { source: JwsError }, + + /// JWE error + #[error("JWE error: {source}")] + Jwe { source: JweError }, + + /// Json error + #[error("JSON error: {source}")] + Json { source: serde_json::Error }, + + /// registered claim type is invalid + #[error("registered claim `{claim}` has invalid type")] + InvalidRegisteredClaimType { claim: &'static str }, + + /// a required claim is missing + #[error("required claim `{claim}` is missing")] + RequiredClaimMissing { claim: &'static str }, + + /// token not yet valid + #[error("token not yet valid (not before: {}, now: {} [leeway: {}])", not_before, now.numeric_date, now.leeway)] + NotYetValid { not_before: i64, now: JwtDate }, + + /// token expired + #[error("token expired (not after: {}, now: {} [leeway: {}])", not_after, now.numeric_date, now.leeway)] + Expired { not_after: i64, now: JwtDate }, + + /// validator is invalid + #[error("invalid validator: {description}")] + InvalidValidator { description: &'static str }, +} + +impl From for JwtError { + fn from(s: JwsError) -> Self { + Self::Jws { source: s } + } +} + +impl From for JwtError { + fn from(e: serde_json::Error) -> Self { + Self::Json { source: e } + } +} + +impl From for JwtError { + fn from(s: JweError) -> Self { + Self::Jwe { source: s } + } +} + +// === Validation states === // + +pub struct CheckedState { + pub claims: C, +} + +impl Clone for CheckedState +where + C: Clone, +{ + fn clone(&self) -> Self { + Self { + claims: self.claims.clone(), + } + } +} + +impl fmt::Debug for CheckedState +where + C: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ValidatedClaims({:?})", self.claims) + } +} + +#[derive(Clone, Debug)] +pub struct UncheckedState { + payload: Vec, +} + +// === JWT date === // + +/// Represent date as defined by [RFC7519](https://tools.ietf.org/html/rfc7519#section-2). +/// +/// A leeway can be configured to account clock skew when comparing with another date. +/// Should be small (less than 120). +#[derive(Clone, Debug)] +pub struct JwtDate { + pub numeric_date: i64, + pub leeway: u16, +} + +impl JwtDate { + pub const fn new(numeric_date: i64) -> Self { + Self { + numeric_date, + leeway: 0, + } + } + + pub const fn new_with_leeway(numeric_date: i64, leeway: u16) -> Self { + Self { numeric_date, leeway } + } + + pub const fn is_before(&self, other_numeric_date: i64) -> bool { + self.numeric_date <= other_numeric_date + self.leeway as i64 + } + + pub const fn is_before_strict(&self, other_numeric_date: i64) -> bool { + self.numeric_date < other_numeric_date + self.leeway as i64 + } + + pub const fn is_after(&self, other_numeric_date: i64) -> bool { + self.numeric_date >= other_numeric_date - self.leeway as i64 + } + + pub const fn is_after_strict(&self, other_numeric_date: i64) -> bool { + self.numeric_date > other_numeric_date - self.leeway as i64 + } +} + +// === validator === // + +#[derive(Debug, Clone, Copy)] +enum CheckStrictness { + Ignored, + Optional, + Required, +} + +#[derive(Debug, Clone)] +pub struct JwtValidator { + current_date: Option, + expiration_claim: CheckStrictness, + not_before_claim: CheckStrictness, +} + +pub const NO_CHECK_VALIDATOR: JwtValidator = JwtValidator::no_check(); + +impl JwtValidator { + /// Check signature and the registered exp and nbf claims. If a claim is missing token is rejected. + pub const fn strict(current_date: JwtDate) -> Self { + Self { + current_date: Some(current_date), + expiration_claim: CheckStrictness::Required, + not_before_claim: CheckStrictness::Required, + } + } + + /// Check signature and the registered exp and nbf claims. Token isn't rejected if a claim is missing. + pub const fn lenient(current_date: JwtDate) -> Self { + Self { + current_date: Some(current_date), + expiration_claim: CheckStrictness::Optional, + not_before_claim: CheckStrictness::Optional, + } + } + + /// No check. + pub const fn no_check() -> Self { + Self { + current_date: None, + expiration_claim: CheckStrictness::Ignored, + not_before_claim: CheckStrictness::Ignored, + } + } + + pub fn current_date(self, current_date: JwtDate) -> Self { + Self { + current_date: Some(current_date), + expiration_claim: CheckStrictness::Required, + not_before_claim: CheckStrictness::Required, + } + } + + pub fn expiration_check_required(self) -> Self { + Self { + expiration_claim: CheckStrictness::Required, + ..self + } + } + + pub fn expiration_check_optional(self) -> Self { + Self { + expiration_claim: CheckStrictness::Optional, + ..self + } + } + + pub fn expiration_check_ignored(self) -> Self { + Self { + expiration_claim: CheckStrictness::Ignored, + ..self + } + } + + pub fn not_before_check_required(self) -> Self { + Self { + not_before_claim: CheckStrictness::Required, + ..self + } + } + + pub fn not_before_check_optional(self) -> Self { + Self { + not_before_claim: CheckStrictness::Optional, + ..self + } + } + + pub fn not_before_check_ignored(self) -> Self { + Self { + not_before_claim: CheckStrictness::Ignored, + ..self + } + } +} + +// === JWT === // + +const JWT_TYPE: &str = "JWT"; +const EXPIRATION_TIME_CLAIM: &str = "exp"; +const NOT_BEFORE_CLAIM: &str = "nbf"; + +pub struct Jwt { + pub header: H, + pub state: State, +} + +pub type JwtSig = Jwt; +pub type CheckedJwtSig = Jwt>; +pub type JwtEnc = Jwt; +pub type CheckedJwtEnc = Jwt>; + +impl Clone for Jwt +where + H: Clone, + State: Clone, +{ + fn clone(&self) -> Self { + Self { + header: self.header.clone(), + state: self.state.clone(), + } + } +} + +impl fmt::Debug for Jwt +where + H: fmt::Debug, + State: fmt::Debug, +{ + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("Jwt") + .field("header", &self.header) + .field("state", &self.state) + .finish() + } +} + +impl Jwt> { + pub fn new_with_header(header: H, claims: C) -> Self { + Jwt { + header, + state: CheckedState { claims }, + } + } +} + +impl CheckedJwtSig { + pub fn new(alg: JwsAlg, claims: C) -> Self { + Jwt { + header: JwsHeader { + typ: Some(JWT_TYPE.to_owned()), + ..JwsHeader::new(alg) + }, + state: CheckedState { claims }, + } + } + + pub fn new_with_cty(alg: JwsAlg, cty: impl Into, claims: C) -> Self { + Jwt { + header: JwsHeader { + typ: Some(JWT_TYPE.to_owned()), + ..JwsHeader::new_with_cty(alg, cty) + }, + state: CheckedState { claims }, + } + } +} + +impl CheckedJwtSig +where + C: Serialize, +{ + pub fn encode(self, private_key: &PrivateKey) -> Result { + let jws = Jws { + header: self.header, + payload: serde_json::to_vec(&self.state.claims)?, + }; + let encoded = jws.encode(private_key)?; + Ok(encoded) + } +} + +impl JwtSig { + pub fn encode(self, private_key: &PrivateKey) -> Result { + let jws = Jws { + header: self.header, + payload: self.state.payload, + }; + let encoded = jws.encode(private_key)?; + Ok(encoded) + } +} + +impl JwtSig { + /// Verifies signature and returns decoded JWS payload. + pub fn decode(encoded_token: &str, public_key: &PublicKey) -> Result { + let jws = Jws::decode(encoded_token, public_key)?; + Ok(Self::from(jws)) + } +} + +impl From for JwtSig { + fn from(jws: Jws) -> Self { + Self { + header: jws.header, + state: UncheckedState { payload: jws.payload }, + } + } +} + +impl CheckedJwtEnc { + pub fn new(alg: JweAlg, enc: JweEnc, claims: C) -> Self { + Jwt { + header: JweHeader { + typ: Some(JWT_TYPE.to_owned()), + ..JweHeader::new(alg, enc) + }, + state: CheckedState { claims }, + } + } + + pub fn new_with_cty(alg: JweAlg, enc: JweEnc, cty: impl Into, claims: C) -> Self { + Jwt { + header: JweHeader { + typ: Some(JWT_TYPE.to_owned()), + ..JweHeader::new_with_cty(alg, enc, cty) + }, + state: CheckedState { claims }, + } + } +} + +impl CheckedJwtEnc +where + C: Serialize, +{ + /// Encode with CEK encrypted and included in the token using asymmetric cryptography. + pub fn encode(self, asymmetric_key: &PublicKey) -> Result { + let jwe = Jwe { + header: self.header, + payload: serde_json::to_vec(&self.state.claims)?, + }; + let encoded = jwe.encode(asymmetric_key)?; + Ok(encoded) + } + + /// Encode with provided CEK (a symmetric key). This will ignore `alg` value and override it with "dir". + pub fn encode_direct(self, cek: &[u8]) -> Result { + let jwe = Jwe { + header: self.header, + payload: serde_json::to_vec(&self.state.claims)?, + }; + let encoded = jwe.encode_direct(cek)?; + Ok(encoded) + } +} + +impl JwtEnc { + /// Decode using asymmetric cryptography. + pub fn decode(encoded_token: &str, key: &PrivateKey) -> Result { + let jwe = Jwe::decode(encoded_token, key)?; + Ok(Self::from(jwe)) + } + + /// Decode with provided CEK (a symmetric key). + pub fn decode_direct(encoded_token: &str, cek: &[u8]) -> Result { + let jwe = Jwe::decode_direct(encoded_token, cek)?; + Ok(Self::from(jwe)) + } +} + +impl From for JwtEnc { + fn from(jwe: Jwe) -> Self { + Self { + header: jwe.header, + state: UncheckedState { payload: jwe.payload }, + } + } +} + +impl Jwt { + /// Validate JWT claims using validator and convert payload to a user-defined typed struct. + pub fn validate(self, validator: &JwtValidator) -> Result>, JwtError> + where + C: DeserializeOwned, + { + Ok(Jwt { + header: self.header, + state: CheckedState { + claims: h_decode_and_validate_claims(&self.state.payload, validator)?, + }, + }) + } +} + +fn h_decode_and_validate_claims( + claims_json: &[u8], + validator: &JwtValidator, +) -> Result { + let claims = match ( + &validator.current_date, + validator.not_before_claim, + validator.expiration_claim, + ) { + (None, CheckStrictness::Required, _) | (None, _, CheckStrictness::Required) => { + return Err(JwtError::InvalidValidator { + description: "current date is missing", + }); + } + (Some(current_date), nbf_strictness, exp_strictness) => { + let claims = serde_json::from_slice::(claims_json)?; + + let nbf_opt = claims.get(NOT_BEFORE_CLAIM); + match (nbf_strictness, nbf_opt) { + (CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {} + (CheckStrictness::Required, None) => { + return Err(JwtError::RequiredClaimMissing { + claim: NOT_BEFORE_CLAIM, + }); + } + (_, Some(nbf)) => { + let nbf_i64 = nbf.as_i64().ok_or(JwtError::InvalidRegisteredClaimType { + claim: NOT_BEFORE_CLAIM, + })?; + if !current_date.is_after(nbf_i64) { + return Err(JwtError::NotYetValid { + not_before: nbf_i64, + now: current_date.clone(), + }); + } + } + } + + let exp_opt = claims.get(EXPIRATION_TIME_CLAIM); + match (exp_strictness, exp_opt) { + (CheckStrictness::Ignored, _) | (CheckStrictness::Optional, None) => {} + (CheckStrictness::Required, None) => { + return Err(JwtError::RequiredClaimMissing { + claim: EXPIRATION_TIME_CLAIM, + }); + } + (_, Some(exp)) => { + let exp_i64 = exp.as_i64().ok_or(JwtError::InvalidRegisteredClaimType { + claim: EXPIRATION_TIME_CLAIM, + })?; + if !current_date.is_before_strict(exp_i64) { + return Err(JwtError::Expired { + not_after: exp_i64, + now: current_date.clone(), + }); + } + } + } + + serde_json::value::from_value(claims)? + } + (None, _, _) => serde_json::from_slice(claims_json)?, + }; + + Ok(claims) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::jose::jws::RawJws; + use crate::pem::Pem; + use serde::Deserialize; + use std::borrow::Cow; + + #[derive(Serialize, Deserialize, Debug, PartialEq)] + struct MyClaims { + sub: Cow<'static, str>, + name: Cow<'static, str>, + admin: bool, + iat: i32, + } + + const fn get_strongly_typed_claims() -> MyClaims { + MyClaims { + sub: Cow::Borrowed("1234567890"), + name: Cow::Borrowed("John Doe"), + admin: true, + iat: 1516239022, + } + } + + fn get_private_key_1() -> PrivateKey { + let pk_pem = picky_test_data::RSA_2048_PK_1.parse::().unwrap(); + PrivateKey::from_pkcs8(pk_pem.data()).unwrap() + } + + #[test] + fn encode_jws_rsa_sha256() { + let claims = get_strongly_typed_claims(); + let jwt = CheckedJwtSig::new(JwsAlg::RS256, claims); + let encoded = jwt.encode(&get_private_key_1()).unwrap(); + assert_eq!(encoded, picky_test_data::JOSE_JWT_SIG_EXAMPLE); + } + + #[test] + fn decode_jws_rsa_sha256() { + let public_key = get_private_key_1().to_public_key().unwrap(); + let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key) + .unwrap() + .validate::(&JwtValidator::no_check()) + .unwrap(); + assert_eq!(jwt.state.claims, get_strongly_typed_claims()); + + // exp and nbf claims aren't present but this should pass with lenient validator + let now = JwtDate::new(0); + JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key) + .unwrap() + .validate::(&JwtValidator::lenient(now)) + .unwrap(); + } + + #[test] + fn decode_jws_invalid_validator_err() { + let public_key = get_private_key_1().to_public_key().unwrap(); + let validator = JwtValidator::no_check() + .expiration_check_required() + .not_before_check_optional(); + let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key) + .unwrap() + .validate::(&validator) + .err() + .unwrap(); + assert_eq!(err.to_string(), "invalid validator: current date is missing"); + } + + #[test] + fn decode_jws_required_claim_missing_err() { + let public_key = get_private_key_1().to_public_key().unwrap(); + let now = JwtDate::new(0); + let validator = JwtValidator::strict(now); + let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key) + .unwrap() + .validate::(&validator) + .err() + .unwrap(); + assert_eq!(err.to_string(), "required claim `nbf` is missing"); + } + + #[test] + fn decode_jws_rsa_sha256_using_json_value_claims() { + let public_key = get_private_key_1().to_public_key().unwrap(); + let validator = JwtValidator::no_check(); + let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_EXAMPLE, &public_key) + .unwrap() + .validate::(&validator) + .unwrap(); + assert_eq!(jwt.state.claims["sub"].as_str().expect("sub"), "1234567890"); + assert_eq!(jwt.state.claims["name"].as_str().expect("name"), "John Doe"); + assert!(jwt.state.claims["admin"].as_bool().expect("sub")); + assert_eq!(jwt.state.claims["iat"].as_i64().expect("iat"), 1516239022); + } + + #[test] + fn jwe_direct_aes_256_gcm() { + let claims = get_strongly_typed_claims(); + let key = crate::hash::HashAlgorithm::SHA2_256.digest(b"magic_password"); + let jwt = CheckedJwtEnc::new(JweAlg::Direct, JweEnc::Aes256Gcm, claims); + let encoded = jwt.encode_direct(&key).unwrap(); + let decoded = JwtEnc::decode_direct(&encoded, &key) + .unwrap() + .validate::(&NO_CHECK_VALIDATOR) + .unwrap(); + assert_eq!(decoded.state.claims, get_strongly_typed_claims()); + } + + #[derive(Deserialize)] + struct MyExpirableClaims { + exp: i64, + nbf: i64, + msg: String, + } + + #[test] + fn decode_jws_not_expired() { + let public_key = get_private_key_1().to_public_key().unwrap(); + + let jwt = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key) + .unwrap() + .validate::(&JwtValidator::strict(JwtDate::new(1545263999))) + .expect("couldn't decode jwt without leeway"); + + assert_eq!(jwt.state.claims.exp, 1545264000); + assert_eq!(jwt.state.claims.nbf, 1545263000); + assert_eq!(jwt.state.claims.msg, "THIS IS TIME SENSITIVE DATA"); + + // alternatively, a leeway can account for small clock skew + JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key) + .unwrap() + .validate::(&JwtValidator::strict(JwtDate::new_with_leeway(1545264001, 10))) + .expect("couldn't decode jwt with leeway for exp"); + + JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key) + .unwrap() + .validate::(&JwtValidator::strict(JwtDate::new_with_leeway(1545262999, 10))) + .expect("couldn't decode jwt with leeway for nbf"); + } + + #[test] + fn decode_jws_invalid_date_err() { + let public_key = get_private_key_1().to_public_key().unwrap(); + + let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key) + .unwrap() + .validate::(&JwtValidator::strict(JwtDate::new(1545264001))) + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + "token expired (not after: 1545264000, now: 1545264001 [leeway: 0])" + ); + + let err = JwtSig::decode(picky_test_data::JOSE_JWT_SIG_WITH_EXP, &public_key) + .unwrap() + .validate::(&JwtValidator::strict(JwtDate::new_with_leeway(1545262998, 1))) + .err() + .unwrap(); + + assert_eq!( + err.to_string(), + "token not yet valid (not before: 1545263000, now: 1545262998 [leeway: 1])" + ); + } + + #[test] + fn decode_step_cli_generated_token() { + #[derive(Deserialize)] + struct SomeJetClaims { + jet_ap: String, + prx_usr: String, + nbf: i64, + } + + let jws = RawJws::decode("eyJhbGciOiJSUzI1NiIsImtpZCI6InUzQkF1b3lrZ21FY0F2Z21ydm5PVWxNZUYxN2JjS09EbGYweFdHcDhMY2MiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE2NTkxMTg1NjMsImpldF9hcCI6InJkcCIsImp0aSI6IjY1YjkwZmQwMjM2YWU3Mjg1OWE1YWZlZTM3MTEzOTdjOWU4NTI1YzA4YzIyNjE4N2NlNjJjOWQwNTEzNDUzOTUiLCJuYmYiOjE2NTkxMTg1NjMsInByeF91c3IiOiJ1c2VybmFtZSJ9.MzULmkNyVY48nOgN7zbtN9q8Ni8JRavpkbw34aD-lMfqJzl5pFEJQPV9G1iM1HCbcMPRJfMDjVP31dAHOVtsu-gqGRx9qw1ogpNffcJI0nh5-VPPnqBbT5u8H2rJ7WeXO5kx4KAnD2Fbc45Nb6YEM-f_s9RyFipub0LI5AwiUHcbicJno0Lxz0dFKMiSA4cTNOe22vY7STf-E52LnsdHhnTt3JKDPP-7i5FzL1wOdBHzvxhRpyLqNU1kcSXrV_1L07XekeR6Kp3JoWaaJsIWm1Sk27W13Q575gS0a9OJgGX0bumq9fCneOJgLU8HrelUP8-qRM2IaGV81NRAr5HasQ") + .map(RawJws::discard_signature) + .map(JwtSig::from) + .unwrap() + .validate::(&JwtValidator::no_check()) + .unwrap(); + + assert_eq!(jws.state.claims.jet_ap, "rdp"); + assert_eq!(jws.state.claims.prx_usr, "username"); + assert_eq!(jws.state.claims.nbf, 1659118563); + } +} diff --git a/rust/vendor/picky/src/jose/mod.rs b/rust/vendor/picky/src/jose/mod.rs new file mode 100644 index 0000000..d4fa75b --- /dev/null +++ b/rust/vendor/picky/src/jose/mod.rs @@ -0,0 +1,14 @@ +//! JOSE framework subset implementation. +//! +//! A Json Web Token (JWT) comes in two flavors, roughly: +//! - Json Web Encryption (JWE), used to transfer data securely +//! - Json Web Signature (JWS), used to assert one's identity +//! +//! Common part is known as the "JOSE header". +//! +//! JSON Web Key (JWK) are used to represent cryptographic keys using JSON. + +pub mod jwe; +pub mod jwk; +pub mod jws; +pub mod jwt; diff --git a/rust/vendor/picky/src/key/ec.rs b/rust/vendor/picky/src/key/ec.rs new file mode 100644 index 0000000..30f3ed6 --- /dev/null +++ b/rust/vendor/picky/src/key/ec.rs @@ -0,0 +1,396 @@ +use crate::key::{KeyError, PrivateKey, PrivateKeyKind, PublicKey}; +use crate::oid::ObjectIdentifier; + +use picky_asn1::wrapper::BitStringAsn1; +use picky_asn1_x509::{EcParameters, oids}; +use std::fmt::Display; +use zeroize::Zeroize; + +#[derive(Debug)] +pub(crate) struct EcdsaKeypair { + curve: NamedEcCurve, + private_key: Vec, + public_key: Option>, +} + +impl EcdsaKeypair { + pub fn curve(&self) -> &NamedEcCurve { + &self.curve + } + + pub fn secret(&self) -> &[u8] { + &self.private_key + } +} + +impl Drop for EcdsaKeypair { + fn drop(&mut self) { + self.private_key.zeroize(); + } +} + +pub(crate) enum EcComponent<'a> { + PointX(&'a [u8]), + PointY(&'a [u8]), + Secret(&'a [u8]), +} + +/// Elliptic curve name to use for curve operations which require curve-specific arithmetic. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EcCurve { + /// NIST P-256 curve (secp256r1) + NistP256, + /// NIST P-384 curve (secp384r1) + NistP384, + /// NIST P-521 curve (secp521r1) + NistP521, +} + +impl EcCurve { + /// Get size of field compoennet in bytes (e.g. X and Y point values, Secret key, + /// R and S signature values) + pub(crate) fn field_bytes_size(self) -> usize { + match self { + EcCurve::NistP256 => { + use p256::elliptic_curve::FieldBytesSize; + use p256::elliptic_curve::array::typenum::Unsigned; + as Unsigned>::USIZE + } + EcCurve::NistP384 => { + use p384::elliptic_curve::FieldBytesSize; + use p384::elliptic_curve::array::typenum::Unsigned; + as Unsigned>::USIZE + } + EcCurve::NistP521 => { + use p521::elliptic_curve::FieldBytesSize; + use p521::elliptic_curve::array::typenum::Unsigned; + as Unsigned>::USIZE + } + } + } + + /// We need to validate input data sizes to prevent panics in the underlying `generic_array` + /// library code. + pub(crate) fn validate_component<'a>(&self, component: EcComponent<'a>) -> Result<&'a [u8], KeyError> { + let (buffer, error_message) = match component { + EcComponent::PointX(buf) => (buf, "Invalid `point.x` component size"), + EcComponent::PointY(buf) => (buf, "Invalid `point.y` component size"), + EcComponent::Secret(buf) => (buf, "Invalid `secret` component size"), + }; + + if buffer.len() != self.field_bytes_size() { + return Err(KeyError::EC { + context: error_message.to_string(), + }); + } + + Ok(buffer) + } +} + +/// Describes the curve type of an ECDSA keypair +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum NamedEcCurve { + Known(EcCurve), + Unsupported(ObjectIdentifier), +} + +impl Display for EcCurve { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NistP256 => write!(f, "NIST-P256"), + Self::NistP384 => write!(f, "NIST-P384"), + Self::NistP521 => write!(f, "NIST-P521"), + } + } +} + +impl Display for NamedEcCurve { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Known(curve) => curve.fmt(f), + Self::Unsupported(oid) => { + let oid: String = oid.into(); + write!(f, "Unsupported(OID: {oid})") + } + } + } +} + +impl From<&'_ ObjectIdentifier> for NamedEcCurve { + fn from(value: &ObjectIdentifier) -> Self { + let oid: String = value.into(); + match oid.as_str() { + oids::SECP256R1 => NamedEcCurve::Known(EcCurve::NistP256), + oids::SECP384R1 => NamedEcCurve::Known(EcCurve::NistP384), + oids::SECP521R1 => NamedEcCurve::Known(EcCurve::NistP521), + _ => NamedEcCurve::Unsupported(value.clone()), + } + } +} + +impl From for ObjectIdentifier { + fn from(value: NamedEcCurve) -> Self { + match value { + NamedEcCurve::Known(curve) => match curve { + EcCurve::NistP256 => oids::secp256r1(), + EcCurve::NistP384 => oids::secp384r1(), + EcCurve::NistP521 => oids::secp521r1(), + }, + NamedEcCurve::Unsupported(oid) => oid, + } + } +} + +impl<'a> TryFrom<&'a PrivateKey> for EcdsaKeypair { + type Error = KeyError; + + fn try_from(v: &'a PrivateKey) -> Result { + match &v.kind { + PrivateKeyKind::Ec { + public_key, + private_key, + curve_oid, + .. + } => Ok(Self { + curve: NamedEcCurve::from(curve_oid), + private_key: private_key.clone(), + public_key: public_key.clone(), + }), + _ => Err(KeyError::EC { + context: "EC keypair cannot be built from Non-EC private key".to_string(), + }), + } + } +} + +pub(crate) fn calculate_public_ec_key( + curve_oid: &ObjectIdentifier, + private_key: &[u8], + compress: bool, +) -> Result>, KeyError> { + let curve = NamedEcCurve::from(curve_oid); + + match curve { + NamedEcCurve::Known(EcCurve::NistP256) => { + use p256::elliptic_curve::sec1::ToSec1Point as _; + + let private_key_validated = EcCurve::NistP256.validate_component(EcComponent::Secret(private_key))?; + + let secret_bytes = + p256::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC { + context: format!( + "validated private key is the not right length(expected: {}, actual: {})", + EcCurve::NistP256.field_bytes_size(), + private_key_validated.len() + ), + })?; + let secret_key = p256::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC { + context: "Failed to construct P256 SecretKey from private key bytes".to_string(), + })?; + + // Calculate public key from secret key + let public_key = secret_key.public_key().as_affine().to_sec1_point(compress); + + Ok(Some(public_key.to_bytes().to_vec())) + } + NamedEcCurve::Known(EcCurve::NistP384) => { + use p384::elliptic_curve::sec1::ToSec1Point as _; + + let private_key_validated = EcCurve::NistP384.validate_component(EcComponent::Secret(private_key))?; + + let secret_bytes = + p384::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC { + context: format!( + "validated private key is the not right length(expected: {}, actual: {})", + EcCurve::NistP384.field_bytes_size(), + private_key_validated.len() + ), + })?; + let secret_key = p384::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC { + context: "Failed to construct P384 SecretKey from private key bytes".to_string(), + })?; + + // Calculate public key from secret key + let public_key = secret_key.public_key().as_affine().to_sec1_point(compress); + + Ok(Some(public_key.to_bytes().to_vec())) + } + NamedEcCurve::Known(EcCurve::NistP521) => { + use p521::elliptic_curve::sec1::ToSec1Point as _; + + let private_key_validated = EcCurve::NistP521.validate_component(EcComponent::Secret(private_key))?; + + let secret_bytes = + p521::elliptic_curve::array::Array::try_from(private_key_validated).map_err(|_| KeyError::EC { + context: format!( + "validated private key is the not right length(expected: {}, actual: {})", + EcCurve::NistP521.field_bytes_size(), + private_key_validated.len() + ), + })?; + let secret_key = p521::SecretKey::from_bytes(&secret_bytes).map_err(|_| KeyError::EC { + context: "Failed to construct P521 SecretKey from private key bytes".to_string(), + })?; + + // Calculate public key from secret key + let public_key = secret_key.public_key().as_affine().to_sec1_point(compress); + + Ok(Some(public_key.to_bytes().to_vec())) + } + NamedEcCurve::Unsupported(_) => Ok(None), + } +} + +#[derive(Debug)] +pub(crate) struct EcdsaPublicKey<'a> { + data: &'a [u8], + curve: NamedEcCurve, +} + +impl EcdsaPublicKey<'_> { + pub fn curve(&self) -> &NamedEcCurve { + &self.curve + } + + pub fn encoded_point(&self) -> &[u8] { + self.data + } +} + +impl<'a> TryFrom<&'a PublicKey> for EcdsaPublicKey<'a> { + type Error = KeyError; + + fn try_from(v: &'a PublicKey) -> Result { + use picky_asn1_x509::PublicKey as InnerPublicKey; + + let curve_oid = match &v.as_inner().algorithm.parameters() { + picky_asn1_x509::AlgorithmIdentifierParameters::Ec(EcParameters::NamedCurve(curve_oid)) => { + curve_oid.0.clone() + } + _ => { + return Err(KeyError::EC { + context: "EC public key cannot be constructed from non-EC public key".to_string(), + }); + } + }; + + match &v.as_inner().subject_public_key { + InnerPublicKey::Rsa(_) => Err(KeyError::EC { + context: "EC public key cannot be constructed from RSA public key".to_string(), + }), + InnerPublicKey::Ec(BitStringAsn1(bitstring)) => { + let data = bitstring.payload_view(); + + Ok(EcdsaPublicKey { + data, + curve: NamedEcCurve::from(&curve_oid), + }) + } + InnerPublicKey::Ed(_) => Err(KeyError::EC { + context: "EC public key cannot be constructed from ED25519 public key".to_string(), + }), + InnerPublicKey::Mldsa(_) => Err(KeyError::EC { + context: "EC public key cannot be constructed from MLDSA public key".to_string(), + }), + } + } +} + +impl<'a> TryFrom<&'a EcdsaKeypair> for EcdsaPublicKey<'a> { + type Error = KeyError; + + fn try_from(v: &'a EcdsaKeypair) -> Result { + match v.public_key.as_ref() { + Some(key) => Ok(Self { + data: key.as_slice(), + curve: v.curve.clone(), + }), + None => Err(KeyError::EC { + context: "EC public key cannot be constructed from EC private key without public key".to_string(), + }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::*; + + const RSA_PUBLIC_KEY_PEM: &str = "-----BEGIN RSA PUBLIC KEY-----\n\ + MIIBCgKCAQEA61BjmfXGEvWmegnBGSuS+rU9soUg2FnODva32D1AqhwdziwHINFa\n\ + D1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBSEVCgJjtHAGZIm5GL/KA86KDp/CwDFMSw\n\ + luowcXwDwoyinmeOY9eKyh6aY72xJh7noLBBq1N0bWi1e2i+83txOCg4yV2oVXhB\n\ + o8pYEJ8LT3el6Smxol3C1oFMVdwPgc0vTl25XucMcG/ALE/KNY6pqC2AQ6R2ERlV\n\ + gPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeulmCpGSynXNcpZ/06+vofGi/2MlpQZNhH\n\ + Ao8eayMp6FcvNucIpUndo1X8dKMv3Y26ZQIDAQAB\n\ + -----END RSA PUBLIC KEY-----"; + + #[rstest] + #[case(picky_test_data::EC_NIST256_DER_PK_1)] + #[case(picky_test_data::EC_NIST384_DER_PK_1)] + #[case(picky_test_data::EC_NIST521_DER_PK_1)] + #[case(picky_test_data::EC_NIST256_PK_1)] // PKCS8 + fn private_key_from_ec_pem(#[case] key_pem: &str) { + PrivateKey::from_pem_str(key_pem).unwrap(); + } + + #[rstest] + #[case(picky_test_data::EC_NIST256_NOPUBLIC_DER_PK_1)] + #[case(picky_test_data::EC_NIST384_NOPUBLIC_DER_PK_1)] + #[case(picky_test_data::EC_NIST521_NOPUBLIC_DER_PK_1)] + fn ecdsa_private_key_without_public(#[case] key_pem: &str) { + // This should succeed for supported curves + let key = PrivateKey::from_pem_str(key_pem).unwrap(); + key.to_public_key().unwrap().to_pem_str().unwrap(); + } + + #[rstest] + // Known curves + #[case(picky_test_data::EC_NIST256_PK_1_PUB)] + #[case(picky_test_data::EC_NIST384_PK_1_PUB)] + #[case(picky_test_data::EC_NIST521_PK_1_PUB)] + // Unsupported curve, should still work as long as pem contains the public key + // (in that case no arithmetic operations are performed on the key) + #[case(picky_test_data::EC_PUBLIC_KEY_SECP256K1_PEM)] + fn ecdsa_public_valid_key_conversions(#[case] key_pem: &str) { + let pk: &PublicKey = &PublicKey::from_pem_str(key_pem).unwrap(); + let epk: Result = pk.try_into(); + assert!(epk.is_ok()); + } + + #[test] + fn ecdsa_public_invalid_key_conversions() { + // PEM public key conversion fails with an error + let pk: &PublicKey = &PublicKey::from_pem_str(RSA_PUBLIC_KEY_PEM).unwrap(); + let epk: Result = pk.try_into(); + assert!(epk.is_err()); + assert!(matches!(epk, Err(KeyError::EC { context: _ }))); + + // TODO: add check for attempted conversion from ED keys - which are not supported yet + } + + #[rstest] + #[case(picky_test_data::EC_NIST256_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP256))] + #[case(picky_test_data::EC_NIST384_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP384))] + #[case(picky_test_data::EC_NIST521_DER_PK_1, NamedEcCurve::Known(EcCurve::NistP521))] + fn ecdsa_key_pair_from_ec_private_key(#[case] key: &str, #[case] curve: NamedEcCurve) { + let pk = PrivateKey::from_pem_str(key).unwrap(); + let pair = EcdsaKeypair::try_from(&pk).unwrap(); + assert_eq!(curve, pair.curve); + } + + #[test] + fn ring_ecdsa_pkcs8_keys_could_be_parsed() { + let algo = &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING; + let rng = ring::rand::SystemRandom::new(); + let pkcs8_bytes = ring::signature::EcdsaKeyPair::generate_pkcs8(algo, &rng).unwrap(); + // Validate that missing `parameters` field from ECPriavteKeyInfo is handled correctly. + // rings skips it during pkcs8 serialization + let key = PrivateKey::from_pkcs8(&pkcs8_bytes).unwrap(); + let pair = EcdsaKeypair::try_from(&key).unwrap(); + + assert_eq!(pair.curve(), &NamedEcCurve::Known(EcCurve::NistP256)) + } +} diff --git a/rust/vendor/picky/src/key/ed.rs b/rust/vendor/picky/src/key/ed.rs new file mode 100644 index 0000000..6ac6a05 --- /dev/null +++ b/rust/vendor/picky/src/key/ed.rs @@ -0,0 +1,240 @@ +use crate::key::{KeyError, PrivateKey, PrivateKeyKind, PublicKey}; +use crate::oid::ObjectIdentifier; + +use picky_asn1::wrapper::BitStringAsn1; +use picky_asn1_x509::oids; +use std::fmt::Display; +use zeroize::Zeroize; + +pub(crate) const X25519_FIELD_ELEMENT_SIZE: usize = 32; +pub(crate) type X25519FieldElement = [u8; X25519_FIELD_ELEMENT_SIZE]; + +/// Name of supported Curve25519 and Curve448 based algorithms. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum EdAlgorithm { + /// Curve25519-based EdDSA algorithm + Ed25519, + /// Curve25519-based ECDH algorithm (mainly used for jwe key agreement) + X25519, + // (Unsupported) Ed448 -- Curve448-based EdDSA algorithm + // (Unsupported) X448 -- Curve448-based ECDH algorithm (mainly used for jwe key agreement) +} + +// Describes Edwards curve-based EC algorithm +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum NamedEdAlgorithm { + Known(EdAlgorithm), + Unsupported(ObjectIdentifier), +} + +impl Display for NamedEdAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NamedEdAlgorithm::Known(alg) => write!(f, "{alg}"), + NamedEdAlgorithm::Unsupported(oid) => { + // We don't support Ed448 and X448 algorithms, but we can still print their named + // representation of OID to make errrs more readable. + if oid == &oids::ed448() { + write!(f, "Ed448") + } else if oid == &oids::x448() { + write!(f, "X448") + } else { + let oid: String = oid.into(); + write!(f, "Unsupported(OID: {oid})") + } + } + } + } +} + +impl Display for EdAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Ed25519 => write!(f, "Ed25519"), + Self::X25519 => write!(f, "X25519"), + } + } +} + +impl From<&'_ ObjectIdentifier> for NamedEdAlgorithm { + fn from(value: &'_ ObjectIdentifier) -> Self { + let oid: String = value.into(); + match oid.as_str() { + oids::ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519), + oids::X25519 => NamedEdAlgorithm::Known(EdAlgorithm::X25519), + _ => NamedEdAlgorithm::Unsupported(value.clone()), + } + } +} + +impl From for ObjectIdentifier { + fn from(value: EdAlgorithm) -> Self { + match value { + EdAlgorithm::Ed25519 => oids::ed25519(), + EdAlgorithm::X25519 => oids::x25519(), + } + } +} + +impl From for ObjectIdentifier { + fn from(value: NamedEdAlgorithm) -> Self { + match value { + NamedEdAlgorithm::Known(alg) => alg.into(), + NamedEdAlgorithm::Unsupported(oid) => oid, + } + } +} + +#[derive(Debug)] +pub(crate) struct EdKeypair { + algorithm: NamedEdAlgorithm, + private_key: Vec, + public_key: Option>, +} + +impl EdKeypair { + pub fn algorithm(&self) -> &NamedEdAlgorithm { + &self.algorithm + } + + pub fn secret(&self) -> &[u8] { + &self.private_key + } +} + +impl Drop for EdKeypair { + fn drop(&mut self) { + self.private_key.zeroize(); + } +} + +impl<'a> TryFrom<&'a PrivateKey> for EdKeypair { + type Error = KeyError; + + fn try_from(value: &'a PrivateKey) -> Result { + match &value.kind { + PrivateKeyKind::Ed { + public_key, + private_key, + algorithm_oid, + } => Ok(Self { + algorithm: NamedEdAlgorithm::from(algorithm_oid), + private_key: private_key.clone(), + public_key: public_key.clone(), + }), + _ => Err(KeyError::ED { + context: "Ed keypair cannot be constructed from non-Ed private key".to_string(), + }), + } + } +} + +#[derive(Debug)] +pub(crate) struct EdPublicKey<'a> { + data: &'a [u8], + algorithm: NamedEdAlgorithm, +} + +impl EdPublicKey<'_> { + pub fn algorithm(&self) -> &NamedEdAlgorithm { + &self.algorithm + } + + pub fn data(&self) -> &[u8] { + self.data + } +} + +impl<'a> TryFrom<&'a EdKeypair> for EdPublicKey<'a> { + type Error = KeyError; + + fn try_from(v: &'a EdKeypair) -> Result { + match v.public_key.as_ref() { + Some(key) => Ok(Self { + data: key.as_slice(), + algorithm: v.algorithm.clone(), + }), + None => Err(KeyError::ED { + context: "Ed public key cannot be constructed from Ed private key without public key".to_string(), + }), + } + } +} + +impl<'a> TryFrom<&'a PublicKey> for EdPublicKey<'a> { + type Error = KeyError; + + fn try_from(v: &'a PublicKey) -> Result { + use picky_asn1_x509::PublicKey as InnerPublicKey; + + let oid = v.as_inner().algorithm.oid(); + + match &v.as_inner().subject_public_key { + InnerPublicKey::Rsa(_) => Err(KeyError::ED { + context: "Ed public key cannot be constructed from RSA public key".to_string(), + }), + InnerPublicKey::Ec(_) => Err(KeyError::ED { + context: "Ed public key cannot be constructed from Ec public key".to_string(), + }), + InnerPublicKey::Ed(BitStringAsn1(bitstring)) => { + let data = bitstring.payload_view(); + + Ok(EdPublicKey { + data, + algorithm: NamedEdAlgorithm::from(oid), + }) + } + InnerPublicKey::Mldsa(_) => Err(KeyError::ED { + context: "Ed public key cannot be constructed from Mldsa public key".to_string(), + }), + } + } +} + +#[cfg(test)] +mod tests { + use crate::key::{PrivateKey, PublicKey}; + use rstest::rstest; + + #[rstest] + #[case(picky_test_data::ED25519_PEM_PK_1)] + #[case(picky_test_data::X25519_PEM_PK_1)] + // Although X448 and ED448 are not supported, we should still be able to decode and encode them + #[case(picky_test_data::ED448_PEM_PK_1)] + #[case(picky_test_data::X448_PEM_PK_1)] + fn private_key_roundtrip(#[case] key_pem: &str) { + let decoded = PrivateKey::from_pem_str(key_pem).unwrap(); + let encoded = decoded.to_pem_str().unwrap(); + assert_eq!(encoded.as_str(), key_pem); + } + + #[rstest] + #[case(picky_test_data::ED25519_PEM_PK_1_PUB)] + #[case(picky_test_data::X25519_PEM_PK_1_PUB)] + // Although X448 and ED448 are not supported, we should still be able to decode and encode them + #[case(picky_test_data::ED448_PEM_PK_1_PUB)] + #[case(picky_test_data::X448_PEM_PK_1_PUB)] + fn public_key_roundtrip(#[case] key_pem: &str) { + let decoded = PublicKey::from_pem_str(key_pem).unwrap(); + let encoded = decoded.to_pem_str().unwrap(); + assert_eq!(encoded.as_str(), key_pem); + } + + #[rstest] + #[case(picky_test_data::ED25519_PEM_PK_1, picky_test_data::ED25519_PEM_PK_1_PUB)] + #[case(picky_test_data::X25519_PEM_PK_1, picky_test_data::X25519_PEM_PK_1_PUB)] + fn extract_public_key(#[case] key_pem: &str, #[case] expected_public_pem: &str) { + let private = PrivateKey::from_pem_str(key_pem).unwrap(); + let public = private.to_public_key().unwrap(); + let public_expected = PublicKey::from_pem_str(expected_public_pem).unwrap(); + assert_eq!(public, public_expected); + } + + #[rstest] + #[case(picky_test_data::ED448_PEM_PK_1)] + #[case(picky_test_data::X448_PEM_PK_1)] + fn extract_public_key_for_unsupported_algorithm_fails(#[case] key_pem: &str) { + let private = PrivateKey::from_pem_str(key_pem).unwrap(); + assert!(private.to_public_key().is_err()); + } +} diff --git a/rust/vendor/picky/src/key/mod.rs b/rust/vendor/picky/src/key/mod.rs new file mode 100644 index 0000000..e7115fd --- /dev/null +++ b/rust/vendor/picky/src/key/mod.rs @@ -0,0 +1,1299 @@ +//! Wrappers around public and private keys raw data providing an easy to use API +pub(crate) mod ec; +pub(crate) mod ed; + +use crate::oid::ObjectIdentifier; +use crate::pem::{Pem, PemError, parse_pem}; +use crypto_bigint::{BoxedUint, NonZero}; +use crypto_common::Generate as _; +use picky_asn1::bit_string::BitString; +use picky_asn1::wrapper::{BitStringAsn1Container, IntegerAsn1, OctetStringAsn1Container}; +use picky_asn1_der::Asn1DerError; +use picky_asn1_x509::{ + ECPrivateKey, PRIVATE_KEY_INFO_VERSION_1, PrivateKeyInfo, PrivateKeyValue, SubjectPublicKeyInfo, private_key_info, +}; +use rand::rngs::{StdRng, SysRng}; +use rand_core::SeedableRng as _; +use rsa::traits::{PrivateKeyParts as _, PublicKeyParts as _}; +use rsa::{RsaPrivateKey, RsaPublicKey}; +use thiserror::Error; +use zeroize::Zeroize; + +use ec::{EcComponent, NamedEcCurve, calculate_public_ec_key}; +use ed::{NamedEdAlgorithm, X25519_FIELD_ELEMENT_SIZE, X25519FieldElement}; + +pub use ec::EcCurve; +pub use ed::EdAlgorithm; + +#[derive(Debug, Error)] +pub enum KeyError { + /// ASN1 serialization error + #[error("(ASN1) couldn't serialize {element}: {source}")] + Asn1Serialization { + element: &'static str, + source: Asn1DerError, + }, + + /// ASN1 deserialization error + #[error("(ASN1) couldn't deserialize {element}: {source}")] + Asn1Deserialization { + element: &'static str, + source: Asn1DerError, + }, + + /// RSA error + #[error("RSA error: {context}")] + Rsa { context: String }, + + /// EC error + #[error("EC error: {context}")] + EC { context: String }, + + /// ED error + #[error("ED error: {context}")] + ED { context: String }, + + /// invalid PEM label error + #[error("invalid PEM label: {label}")] + InvalidPemLabel { label: String }, + + /// unsupported algorithm + #[error("unsupported algorithm: {algorithm}")] + UnsupportedAlgorithm { algorithm: &'static str }, + + /// invalid PEM provided + #[error("invalid PEM provided: {source}")] + Pem { source: PemError }, + + #[error(transparent)] + RandError(#[from] rand::rngs::SysError), +} + +impl KeyError { + pub(crate) fn unsupported_curve(curve_oid: &ObjectIdentifier, context: &'static str) -> Self { + let curve_oid: String = curve_oid.into(); + Self::EC { + context: format!("EC curve with oid `{curve_oid}` is not supported in context of {context}"), + } + } + + pub(crate) fn unsupported_ed_algorithm(oid: &ObjectIdentifier, context: &'static str) -> Self { + let oid: String = oid.into(); + Self::ED { + context: format!( + "Algorithm with oid `{oid}` based on Edwards curves is not supported in context of {context}", + ), + } + } +} + +impl From for KeyError { + fn from(e: rsa::errors::Error) -> Self { + Self::Rsa { context: e.to_string() } + } +} + +impl From for KeyError { + fn from(e: PemError) -> Self { + Self::Pem { source: e } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum KeyKind { + Rsa, + Ec, + Ed, + Mldsa, +} + +// === private key === // + +const PRIVATE_KEY_PEM_LABEL: &str = "PRIVATE KEY"; +const RSA_PRIVATE_KEY_PEM_LABEL: &str = "RSA PRIVATE KEY"; +const EC_PRIVATE_KEY_LABEL: &str = "EC PRIVATE KEY"; + +// We dont compress EC points by default to avoid potential interoperability issues. +// Namely, `ring` library has bug in it, which causes it to fail when validating +// encoded public key, comparing it with generated one (It assumes uncompressed point). +// [https://github.com/briansmith/ring/blob/155231fb017acaaa94a044f124bb34a777d115ef/src/ec/suite_b.rs#L221-L225] +const COMPRESS_EC_POINT_BY_DEFAULT: bool = false; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PrivateKeyKind { + Rsa, + Ec { + public_key: Option>, + private_key: Vec, + curve_oid: ObjectIdentifier, + }, + Ed { + public_key: Option>, + private_key: Vec, + algorithm_oid: ObjectIdentifier, + }, +} + +impl Drop for PrivateKeyKind { + fn drop(&mut self) { + match self { + PrivateKeyKind::Rsa => {} + PrivateKeyKind::Ec { private_key, .. } => { + private_key.zeroize(); + } + PrivateKeyKind::Ed { private_key, .. } => { + private_key.zeroize(); + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrivateKey { + /// Inner key details. This should never be puiblicly exposed. + kind: PrivateKeyKind, + /// Inner representation in Pkcs8 + inner: PrivateKeyInfo, +} + +impl TryFrom<&'_ PrivateKey> for RsaPrivateKey { + type Error = KeyError; + + fn try_from(v: &PrivateKey) -> Result { + match &v.as_inner().private_key { + private_key_info::PrivateKeyValue::Rsa(OctetStringAsn1Container(key)) => { + let p1 = BoxedUint::from_be_slice_vartime(key.prime_1.as_unsigned_bytes_be()); + let p2 = BoxedUint::from_be_slice_vartime(key.prime_2.as_unsigned_bytes_be()); + + RsaPrivateKey::from_components( + BoxedUint::from_be_slice_vartime(key.modulus.as_unsigned_bytes_be()), + BoxedUint::from_be_slice_vartime(key.public_exponent.as_unsigned_bytes_be()), + BoxedUint::from_be_slice_vartime(key.private_exponent.as_unsigned_bytes_be()), + vec![p1, p2], + ) + .map_err(|e| KeyError::Rsa { + context: format!("failed to construct private key from components: {e}"), + }) + } + _ => Err(KeyError::Rsa { + context: "RSA private key cannot be constructed from non-RSA private key.".to_owned(), + }), + } + } +} + +impl TryFrom<&'_ PrivateKey> for RsaPublicKey { + type Error = KeyError; + + fn try_from(v: &PrivateKey) -> Result { + match &v.as_inner().private_key { + private_key_info::PrivateKeyValue::Rsa(OctetStringAsn1Container(key)) => { + Ok(RsaPublicKey::new_with_max_size( + BoxedUint::from_be_slice_vartime(key.modulus.as_unsigned_bytes_be()), + BoxedUint::from_be_slice_vartime(key.public_exponent.as_unsigned_bytes_be()), + 8192, + )?) + } + _ => Err(KeyError::Rsa { + context: "RSA public key cannot be constructed from non-RSA private key.".to_string(), + }), + } + } +} + +impl PrivateKey { + pub fn from_rsa_components( + modulus: &BoxedUint, + public_exponent: &BoxedUint, + private_exponent: &BoxedUint, + primes: &[BoxedUint], + ) -> Result { + let mut primes_it = primes.iter(); + let prime_1 = primes_it.next().ok_or_else(|| KeyError::Rsa { + context: format!("invalid number of primes provided: expected 2, got: {}", primes.len()), + })?; + let prime_2 = primes_it.next().ok_or_else(|| KeyError::Rsa { + context: format!("invalid number of primes provided: expected 2, got: {}", primes.len()), + })?; + + let exponent_1 = private_exponent + % NonZero::new(prime_1 - 1u8).into_option().ok_or_else(|| KeyError::Rsa { + context: "the first prime is not valid".to_string(), + })?; + let exponent_2 = private_exponent + % NonZero::new(prime_2 - 1u8).into_option().ok_or_else(|| KeyError::Rsa { + context: "the second prime is not valid".to_string(), + })?; + + let prime_1 = NonZero::new(prime_1.clone()) + .into_option() + .ok_or_else(|| KeyError::Rsa { + context: "the first prime is not valid".to_string(), + })?; + let coefficient = prime_2 + .invert_mod(&prime_1) + .into_option() + .ok_or_else(|| KeyError::Rsa { + context: "no modular inverse for prime 1".to_string(), + })?; + + let inner = PrivateKeyInfo::new_rsa_encryption( + IntegerAsn1::from_bytes_be_unsigned(modulus.to_be_bytes_trimmed_vartime().into_vec()), + IntegerAsn1::from_bytes_be_unsigned(public_exponent.to_be_bytes_trimmed_vartime().into_vec()), + IntegerAsn1::from_bytes_be_unsigned(private_exponent.to_be_bytes_trimmed_vartime().into_vec()), + ( + // primes + IntegerAsn1::from_bytes_be_unsigned(prime_1.to_be_bytes_trimmed_vartime().into_vec()), + IntegerAsn1::from_bytes_be_unsigned(prime_2.to_be_bytes_trimmed_vartime().into_vec()), + ), + ( + // exponents + IntegerAsn1::from_bytes_be_unsigned(exponent_1.to_be_bytes_trimmed_vartime().into_vec()), + IntegerAsn1::from_bytes_be_unsigned(exponent_2.to_be_bytes_trimmed_vartime().into_vec()), + ), + IntegerAsn1::from_bytes_be_unsigned(coefficient.to_be_bytes_trimmed_vartime().into_vec()), + ); + + Ok(Self { + kind: PrivateKeyKind::Rsa, + inner, + }) + } + + /// Builds new EC key from given components. Note that only curves, declared in [`EcCurve`] + /// are supported for key generation. + pub fn from_ec_components( + curve: EcCurve, + secret: &BoxedUint, + point_x: &BoxedUint, + point_y: &BoxedUint, + ) -> Result { + let curve_oid: ObjectIdentifier = NamedEcCurve::Known(curve).into(); + let px_bytes = point_x.to_be_bytes_trimmed_vartime().into_vec(); + let py_bytes = point_y.to_be_bytes_trimmed_vartime().into_vec(); + + let px_validated = curve.validate_component(EcComponent::PointX(&px_bytes))?; + let py_validated = curve.validate_component(EcComponent::PointY(&py_bytes))?; + + let point_bytes = match curve { + EcCurve::NistP256 => { + let x = p256::elliptic_curve::array::Array::try_from(px_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PX slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + px_validated.len(), + ), + })?; + let y = p256::elliptic_curve::array::Array::try_from(py_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PY slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + py_validated.len(), + ), + })?; + let point = p256::Sec1Point::from_affine_coordinates(&x, &y, COMPRESS_EC_POINT_BY_DEFAULT); + point.as_bytes().to_vec() + } + EcCurve::NistP384 => { + let x = p384::elliptic_curve::array::Array::try_from(px_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PX slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + px_validated.len(), + ), + })?; + let y = p384::elliptic_curve::array::Array::try_from(py_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PY slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + py_validated.len(), + ), + })?; + let point = p384::Sec1Point::from_affine_coordinates(&x, &y, COMPRESS_EC_POINT_BY_DEFAULT); + point.as_bytes().to_vec() + } + EcCurve::NistP521 => { + let x = p521::elliptic_curve::array::Array::try_from(px_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PX slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + px_validated.len(), + ), + })?; + let y = p521::elliptic_curve::array::Array::try_from(py_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PY slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + py_validated.len(), + ), + })?; + let point = p521::Sec1Point::from_affine_coordinates(&x, &y, COMPRESS_EC_POINT_BY_DEFAULT); + point.to_bytes().into_vec() + } + }; + + let secret = secret.to_be_bytes_trimmed_vartime().into_vec(); + + let inner = PrivateKeyInfo::new_ec_encryption( + curve_oid.clone(), + secret.clone(), + Some(BitString::with_bytes(point_bytes.as_slice())), + false, + ); + + let kind = PrivateKeyKind::Ec { + curve_oid, + public_key: Some(point_bytes), + private_key: secret, + }; + + Ok(Self { kind, inner }) + } + + /// Infallible method to create new EC key from given components. Note that no checks performed + /// on the validity of the secret and point bytes representation in regards to selected + /// curve oid. + pub fn from_ec_encoded_components(curve_oid: ObjectIdentifier, secret: &[u8], point: Option<&[u8]>) -> Self { + let inner = PrivateKeyInfo::new_ec_encryption( + curve_oid.clone(), + secret.to_vec(), + point.map(BitString::with_bytes), + false, + ); + + let kind = PrivateKeyKind::Ec { + curve_oid, + public_key: point.map(|point| point.to_vec()), + private_key: secret.to_vec(), + }; + + Self { kind, inner } + } + + pub fn from_ed_encoded_components( + algorithm_oid: ObjectIdentifier, + secret: &[u8], + public_key: Option<&[u8]>, + ) -> Self { + let public_key_bit_string = public_key.map(BitString::with_bytes); + + let inner = PrivateKeyInfo::new_ed_encryption(algorithm_oid.clone(), secret.to_vec(), public_key_bit_string); + + let kind = PrivateKeyKind::Ed { + algorithm_oid, + public_key: public_key.map(|key| key.to_vec()), + private_key: secret.to_vec(), + }; + + Self { kind, inner } + } + + pub fn from_pem(pem: &Pem) -> Result { + match pem.label() { + PRIVATE_KEY_PEM_LABEL => Self::from_pkcs8(pem.data()), + RSA_PRIVATE_KEY_PEM_LABEL => Self::from_pkcs1(pem.data()), + EC_PRIVATE_KEY_LABEL => Self::from_ec_der(pem.data()), + _ => Err(KeyError::InvalidPemLabel { + label: pem.label().to_owned(), + }), + } + } + + pub fn from_pem_str(pem_str: &str) -> Result { + let pem = parse_pem(pem_str)?; + Self::from_pem(&pem) + } + + pub fn from_pkcs8>(pkcs8: &T) -> Result { + let inner: PrivateKeyInfo = + picky_asn1_der::from_bytes(pkcs8.as_ref()).map_err(|e| KeyError::Asn1Deserialization { + source: e, + element: "private key info (pkcs8)", + })?; + + match &inner.private_key { + PrivateKeyValue::Rsa(_) => Ok(Self { + kind: PrivateKeyKind::Rsa, + inner, + }), + PrivateKeyValue::EC(OctetStringAsn1Container(key)) => { + let curve_oid = match inner.private_key_algorithm.parameters() { + picky_asn1_x509::AlgorithmIdentifierParameters::Ec(params) => params.curve_oid().clone(), + _ => { + return Err(KeyError::EC { + context: "Specified private key parameters are not EC parameters".to_string(), + }); + } + }; + + Self::from_ec_decoded_der_with_curve_oid(curve_oid, key) + } + PrivateKeyValue::ED(OctetStringAsn1Container(key)) => { + let algorithm = NamedEdAlgorithm::from(inner.private_key_algorithm.oid()); + let private_key = key.0.clone(); + let public_key = match &algorithm { + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => { + let private_key = private_key.as_slice().try_into().map_err(|e| KeyError::ED { + context: format!("invalid size for private key: {e}"), + })?; + let private_key = ed25519_dalek::SigningKey::from_bytes(private_key); + + let public_key = private_key.verifying_key(); + + Some(public_key.to_bytes().to_vec()) + } + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => { + let len = private_key.len(); + + let secret: X25519FieldElement = + private_key.as_slice().try_into().map_err(|_| KeyError::ED { + context: format!( + "Invalid X25519 private key size. Expected: {X25519_FIELD_ELEMENT_SIZE}, actual: {len}" + ), + })?; + + let secret = x25519_dalek::StaticSecret::from(secret); + let public_key = x25519_dalek::PublicKey::from(&secret); + + Some(public_key.to_bytes().to_vec()) + } + NamedEdAlgorithm::Unsupported(_) => { + // We can't generate public key from private key for unsupported algorithms + None + } + }; + + Ok(Self { + kind: PrivateKeyKind::Ed { + algorithm_oid: algorithm.into(), + public_key, + private_key, + }, + inner, + }) + } + } + } + + /// Decodes a DER-encoded RSA private key + pub fn from_pkcs1>(der: &T) -> Result { + use picky_asn1_x509::{AlgorithmIdentifier, RsaPrivateKey}; + + let private_key = + picky_asn1_der::from_bytes::(der.as_ref()).map_err(|e| KeyError::Asn1Deserialization { + source: e, + element: "rsa private key", + })?; + + let inner = PrivateKeyInfo { + version: PRIVATE_KEY_INFO_VERSION_1, + private_key_algorithm: AlgorithmIdentifier::new_rsa_encryption(), + private_key: PrivateKeyValue::Rsa(private_key.into()), + public_key: None, + }; + + Ok(Self { + kind: PrivateKeyKind::Rsa, + inner, + }) + } + + /// Loads an EC private key from a DER-encoded private key with supported curve. Also see + /// [`Self::from_ec_der_with_curve_oid`] for loading keys with unsupported curves. + pub fn from_ec_der_with_curve>(der: &T, curve: EcCurve) -> Result { + Self::from_ec_der_with_curve_oid(der, NamedEcCurve::Known(curve).into()) + } + + /// Internal method to load an EC private key from ASN.1 structure [`ECPrivateKey`] and the + /// given curve OID. (Curve id is required as [`ECPrivateKey`] does not guarantee that the + /// cureve parameters are present). If public key is absent in the ASN.1 structure, it will be + /// calculated from the private key (Only if curve is supported. In other case - throws error) + fn from_ec_decoded_der_with_curve_oid( + curve_oid: ObjectIdentifier, + decoded: &ECPrivateKey, + ) -> Result { + // Generate the public key if it's not present in the `ECPrivateKey` representation + let (public_key, public_key_is_generated) = match &decoded.public_key.0.0 { + Some(bit_string) => (Some(bit_string.payload_view().to_vec()), false), + None => ( + calculate_public_ec_key(&curve_oid, &decoded.private_key.0, COMPRESS_EC_POINT_BY_DEFAULT)?, + true, + ), + }; + let private_key = decoded.private_key.0.clone(); + // if the public key is generated, we need to skip it when encoding, to preserve the + // original `ECPrivateKey` structure in encoded representation + let public_key_encoded = public_key + .as_deref() + .and_then(|public_key| (!public_key_is_generated).then(|| BitString::with_bytes(public_key))); + // if the parameters are missing during parsing, we need to skip them when encoding + let der_skip_parameters = decoded.parameters.0.is_none(); + + let inner = PrivateKeyInfo::new_ec_encryption( + curve_oid.clone(), + private_key.clone(), + public_key_encoded, + der_skip_parameters, + ); + + let kind = PrivateKeyKind::Ec { + curve_oid, + public_key, + private_key, + }; + + Ok(Self { kind, inner }) + } + + /// Same as [`Self::from_ec_der_with_curve`], but with manually specified curve OID. Arithmetic + /// operations are not available for unknown curves, but this method allows to load key from + /// DER-encoded data to perfor non-arithmetic operations like extracting public key or + /// re-encoding into pkcs8. + pub fn from_ec_der_with_curve_oid>( + der: &T, + curve_oid: ObjectIdentifier, + ) -> Result { + let private_key = + picky_asn1_der::from_bytes::(der.as_ref()).map_err(|e| KeyError::Asn1Deserialization { + source: e, + element: "ec private key", + })?; + + Self::from_ec_decoded_der_with_curve_oid(curve_oid, &private_key) + } + + /// Returns the private key as a DER-encoded EC private key. Note that generally, DER-encoded + /// EC keys do not contain the curve parameters, so this method will return if it cannot find + /// such parameters. + /// + /// Usually, EC keys are encoded in PKCS#8 format, which contain all required + /// information to reconstruct the key. See [`Self::from_pkcs8`] + /// + /// However, if the key is encoded in the DER format, and the curve parameters are missing, you + /// could load it via [`Self::from_ec_der_with_curve`] and specify the curve manually. + /// + /// Also, if public key is absent is missing in the parsed file, it will be calculated from the + /// private key (Only if curve is supported. In other case - throws error) + pub fn from_ec_der>(der: &T) -> Result { + let private_key = + picky_asn1_der::from_bytes::(der.as_ref()).map_err(|e| KeyError::Asn1Deserialization { + source: e, + element: "ec private key", + })?; + + // By specification (https://www.rfc-editor.org/rfc/rfc5915) `parameters` files SHOULD + // be present when EC key is encoded as standalone DER. However, some implementations + // do not include parameters, so we have to check for that. + let curve_oid = match &private_key.parameters.0.0 { + Some(params) => params.curve_oid().clone(), + None => { + return Err(KeyError::EC { + context: "EC parameters are missing from DER-encoded private key".into(), + }); + } + }; + + Self::from_ec_decoded_der_with_curve_oid(curve_oid, &private_key) + } + + pub fn to_pkcs8(&self) -> Result, KeyError> { + picky_asn1_der::to_vec(self.as_inner()).map_err(|e| KeyError::Asn1Serialization { + source: e, + element: "private key info (pkcs8)", + }) + } + + pub fn to_pkcs1(&self) -> Result, KeyError> { + let picky_asn1_x509::PrivateKeyValue::Rsa(OctetStringAsn1Container(rsa_private_key)) = &self.inner.private_key + else { + return Err(KeyError::Rsa { + context: String::from("can’t export a non-RSA key to PKCS#1 format"), + }); + }; + + picky_asn1_der::to_vec(rsa_private_key).map_err(|e| KeyError::Asn1Serialization { + source: e, + element: "RSA private key (pkcs1)", + }) + } + + pub fn to_pem(&self) -> Result, KeyError> { + let pkcs8 = self.to_pkcs8()?; + Ok(Pem::new(PRIVATE_KEY_PEM_LABEL, pkcs8)) + } + + pub fn to_pem_str(&self) -> Result { + self.to_pem().map(|pem| pem.to_string()) + } + + pub fn to_pkcs1_pem(&self) -> Result, KeyError> { + let pkcs1 = self.to_pkcs1()?; + Ok(Pem::new(RSA_PRIVATE_KEY_PEM_LABEL, pkcs1)) + } + + pub fn to_pkcs1_pem_str(&self) -> Result { + self.to_pkcs1_pem().map(|pem| pem.to_string()) + } + + pub fn to_public_key(&self) -> Result { + let key = match &self.kind { + PrivateKeyKind::Rsa => match &self.inner.private_key { + PrivateKeyValue::Rsa(OctetStringAsn1Container(key)) => { + SubjectPublicKeyInfo::new_rsa_key(key.modulus.clone(), key.public_exponent.clone()).into() + } + _ => unreachable!("BUG: Non-RSA key data in RSA private key"), + }, + PrivateKeyKind::Ec { + public_key, curve_oid, .. + } => match public_key { + Some(data) => { + let point = picky_asn1::bit_string::BitString::with_bytes(data.as_slice()); + SubjectPublicKeyInfo::new_ec_key(curve_oid.clone(), point).into() + } + None => { + return Err(KeyError::EC { + context: "Public key can't be calculated for unknown EC algorithms".into(), + }); + } + }, + PrivateKeyKind::Ed { + public_key, + algorithm_oid, + .. + } => match public_key { + Some(data) => { + let point = picky_asn1::bit_string::BitString::with_bytes(data.as_slice()); + SubjectPublicKeyInfo::new_ed_key(algorithm_oid.clone(), point).into() + } + None => { + return Err(KeyError::ED { + context: "Public key can't be calculated for unknown edwards curves-based algorithms".into(), + }); + } + }, + }; + + Ok(key) + } + + /// **Beware**: this is insanely slow in debug builds. + pub fn generate_rsa(bits: usize) -> Result { + let key = RsaPrivateKey::new(&mut StdRng::try_from_rng(&mut SysRng)?, bits)?; + + let modulus = key.n(); + let public_exponent = key.e(); + let private_exponent = key.d(); + + Self::from_rsa_components(modulus, public_exponent, private_exponent, key.primes()) + } + + /// Generates new ec key pair with specified supported curve. + pub fn generate_ec(curve: EcCurve) -> Result { + let curve_oid: ObjectIdentifier = NamedEcCurve::Known(curve).into(); + + let (secret, point) = match curve { + EcCurve::NistP256 => { + use p256::elliptic_curve::sec1::ToSec1Point; + + let key = p256::SecretKey::generate_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + let secret = key.to_bytes().to_vec(); + let point = key + .public_key() + .to_sec1_point(COMPRESS_EC_POINT_BY_DEFAULT) + .as_bytes() + .to_vec(); + (secret, point) + } + EcCurve::NistP384 => { + use p384::elliptic_curve::sec1::ToSec1Point; + + let key = p384::SecretKey::generate_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + let secret = key.to_bytes().to_vec(); + let point = key + .public_key() + .to_sec1_point(COMPRESS_EC_POINT_BY_DEFAULT) + .as_bytes() + .to_vec(); + (secret, point) + } + EcCurve::NistP521 => { + use p521::elliptic_curve::sec1::ToSec1Point; + + let key = p521::SecretKey::generate_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + let secret = key.to_bytes().to_vec(); + let point = key + .public_key() + .to_sec1_point(COMPRESS_EC_POINT_BY_DEFAULT) + .as_bytes() + .to_vec(); + (secret, point) + } + }; + + let inner = PrivateKeyInfo::new_ec_encryption( + curve_oid.clone(), + secret.clone(), + Some(BitString::with_bytes(point.as_slice())), + false, + ); + + let kind = PrivateKeyKind::Ec { + curve_oid, + public_key: Some(point), + private_key: secret, + }; + + Ok(Self { kind, inner }) + } + + /// Generates new ed key pair with specified supported algorithm. + /// + /// `write_public_key` specifies whether to include public key in the private key file. + /// Note that OpenSSL does not support ed keys with public key included. + pub fn generate_ed(algorithm: EdAlgorithm, write_public_key: bool) -> Result { + let algorithm_oid: ObjectIdentifier = NamedEdAlgorithm::Known(algorithm).into(); + + let (private_key, public_key) = match algorithm { + EdAlgorithm::Ed25519 => { + let private = ed25519_dalek::SigningKey::generate(&mut StdRng::try_from_rng(&mut SysRng)?); + let public = private.verifying_key(); + (private.to_bytes().to_vec(), public.to_bytes().to_vec()) + } + EdAlgorithm::X25519 => { + let private = x25519_dalek::StaticSecret::random_from_rng(&mut StdRng::try_from_rng(&mut SysRng)?); + let public = x25519_dalek::PublicKey::from(&private); + (private.to_bytes().to_vec(), public.to_bytes().to_vec()) + } + }; + + let public_key_bit_string = write_public_key.then(|| BitString::with_bytes(public_key.as_slice())); + + let inner = + PrivateKeyInfo::new_ed_encryption(algorithm_oid.clone(), private_key.clone(), public_key_bit_string); + + let kind = PrivateKeyKind::Ed { + algorithm_oid, + public_key: Some(public_key), + private_key, + }; + + Ok(Self { kind, inner }) + } + + pub fn kind(&self) -> KeyKind { + match self.kind { + PrivateKeyKind::Rsa => KeyKind::Rsa, + PrivateKeyKind::Ec { .. } => KeyKind::Ec, + PrivateKeyKind::Ed { .. } => KeyKind::Ed, + } + } + + pub(crate) fn as_inner(&self) -> &PrivateKeyInfo { + &self.inner + } + + #[cfg(any(feature = "ssh", feature = "jose"))] + pub(crate) fn as_kind(&self) -> &PrivateKeyKind { + &self.kind + } +} + +// === public key === // + +const PUBLIC_KEY_PEM_LABEL: &str = "PUBLIC KEY"; +const RSA_PUBLIC_KEY_PEM_LABEL: &str = "RSA PUBLIC KEY"; +const EC_PUBLIC_KEY_PEM_LABEL: &str = "EC PUBLIC KEY"; + +#[derive(Clone, Debug, PartialEq, Eq)] +#[repr(transparent)] +pub struct PublicKey(SubjectPublicKeyInfo); + +impl<'a> From<&'a SubjectPublicKeyInfo> for &'a PublicKey { + #[inline] + fn from(spki: &'a SubjectPublicKeyInfo) -> Self { + unsafe { &*(spki as *const SubjectPublicKeyInfo as *const PublicKey) } + } +} + +impl<'a> From<&'a PublicKey> for &'a SubjectPublicKeyInfo { + #[inline] + fn from(key: &'a PublicKey) -> Self { + unsafe { &*(key as *const PublicKey as *const SubjectPublicKeyInfo) } + } +} + +impl From for PublicKey { + #[inline] + fn from(spki: SubjectPublicKeyInfo) -> Self { + Self(spki) + } +} + +impl From for SubjectPublicKeyInfo { + #[inline] + fn from(key: PublicKey) -> Self { + key.0 + } +} +impl TryFrom for PublicKey { + type Error = KeyError; + + #[inline] + fn try_from(key: PrivateKey) -> Result { + key.to_public_key() + } +} + +impl AsRef for PublicKey { + #[inline] + fn as_ref(&self) -> &SubjectPublicKeyInfo { + self.into() + } +} + +impl AsRef for PublicKey { + #[inline] + fn as_ref(&self) -> &PublicKey { + self + } +} + +impl TryFrom<&'_ PublicKey> for RsaPublicKey { + type Error = KeyError; + + fn try_from(v: &PublicKey) -> Result { + use picky_asn1_x509::PublicKey as InnerPublicKey; + + match &v.as_inner().subject_public_key { + InnerPublicKey::Rsa(BitStringAsn1Container(key)) => Ok(RsaPublicKey::new_with_max_size( + BoxedUint::from_be_slice_vartime(key.modulus.as_unsigned_bytes_be()), + BoxedUint::from_be_slice_vartime(key.public_exponent.as_unsigned_bytes_be()), + 8192, + )?), + InnerPublicKey::Ec(_) => Err(KeyError::UnsupportedAlgorithm { + algorithm: "elliptic curves", + }), + InnerPublicKey::Ed(_) => Err(KeyError::UnsupportedAlgorithm { + algorithm: "edwards curves", + }), + InnerPublicKey::Mldsa(_) => Err(KeyError::UnsupportedAlgorithm { algorithm: "mldsa" }), + } + } +} + +impl PublicKey { + pub fn from_rsa_components(modulus: &BoxedUint, public_exponent: &BoxedUint) -> Self { + PublicKey(SubjectPublicKeyInfo::new_rsa_key( + IntegerAsn1::from_bytes_be_unsigned(modulus.to_be_bytes_trimmed_vartime().into_vec()), + IntegerAsn1::from_bytes_be_unsigned(public_exponent.to_be_bytes_trimmed_vartime().into_vec()), + )) + } + + /// `point` is SEC1 encoded point data + pub fn from_ec_encoded_components(curve: &ObjectIdentifier, point: &[u8]) -> Self { + let point = picky_asn1::bit_string::BitString::with_bytes(point); + PublicKey(SubjectPublicKeyInfo::new_ec_key(curve.clone(), point)) + } + + /// `public_key` is raw edwards curve public key + pub fn from_ed_encoded_components(algorithm: &ObjectIdentifier, public_key: &[u8]) -> Self { + let point = picky_asn1::bit_string::BitString::with_bytes(public_key); + PublicKey(SubjectPublicKeyInfo::new_ed_key(algorithm.clone(), point)) + } + + /// Creates public key from its raw components. Only curves declared in [`EcCurve`] are + /// supported. For correct encoding of the point, we need to know which curve-specific + /// arithmetic crate to use. If you want to use a curve that is not declared in [`EcCurve`], + /// and encoded representation of the point is available - use [`Self::from_ec_encoded_components`] + pub fn from_ec_components(curve: EcCurve, x: &BoxedUint, y: &BoxedUint) -> Result { + let px_bytes = x.to_be_bytes_trimmed_vartime(); + let py_bytes = y.to_be_bytes_trimmed_vartime(); + + let px_validated = curve.validate_component(EcComponent::PointX(&px_bytes))?; + let py_validated = curve.validate_component(EcComponent::PointY(&py_bytes))?; + + match curve { + EcCurve::NistP256 => { + let p = p256::Sec1Point::from_affine_coordinates( + &p256::elliptic_curve::array::Array::try_from(px_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PX slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + px_validated.len(), + ), + })?, + &p256::elliptic_curve::array::Array::try_from(py_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PY slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + py_validated.len(), + ), + })?, + COMPRESS_EC_POINT_BY_DEFAULT, + ); + + Ok(Self::from_ec_encoded_components( + &NamedEcCurve::Known(curve).into(), + p.as_bytes(), + )) + } + EcCurve::NistP384 => { + let p = p384::Sec1Point::from_affine_coordinates( + &p384::elliptic_curve::array::Array::try_from(px_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PX slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + px_validated.len(), + ), + })?, + &p384::elliptic_curve::array::Array::try_from(py_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PY slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + py_validated.len(), + ), + })?, + COMPRESS_EC_POINT_BY_DEFAULT, + ); + + Ok(Self::from_ec_encoded_components( + &NamedEcCurve::Known(curve).into(), + p.as_bytes(), + )) + } + EcCurve::NistP521 => { + let p = p521::Sec1Point::from_affine_coordinates( + &p521::elliptic_curve::array::Array::try_from(px_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PX slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + px_validated.len(), + ), + })?, + &p521::elliptic_curve::array::Array::try_from(py_validated).map_err(|_| KeyError::EC { + context: format!( + "validated PY slice is not right length(expected: {}, actual: {})", + curve.field_bytes_size(), + py_validated.len(), + ), + })?, + COMPRESS_EC_POINT_BY_DEFAULT, + ); + + Ok(Self::from_ec_encoded_components( + &NamedEcCurve::Known(curve).into(), + p.as_bytes(), + )) + } + } + } + + pub fn to_der(&self) -> Result, KeyError> { + picky_asn1_der::to_vec(&self.0).map_err(|e| KeyError::Asn1Serialization { + source: e, + element: "subject public key info", + }) + } + + pub fn to_pkcs1(&self) -> Result, KeyError> { + let picky_asn1_x509::PublicKey::Rsa(BitStringAsn1Container(rsa_public_key)) = &self.0.subject_public_key else { + return Err(KeyError::Rsa { + context: String::from("can’t export a non-RSA key to PKCS#1 format"), + }); + }; + + picky_asn1_der::to_vec(rsa_public_key).map_err(|e| KeyError::Asn1Serialization { + source: e, + element: "RSA public key", + }) + } + + pub fn to_pem(&self) -> Result, KeyError> { + let der = self.to_der()?; + Ok(Pem::new(PUBLIC_KEY_PEM_LABEL, der)) + } + + pub fn to_pem_str(&self) -> Result { + self.to_pem().map(|pem| pem.to_string()) + } + + pub fn to_pkcs1_pem(&self) -> Result, KeyError> { + let pkcs1 = self.to_pkcs1()?; + Ok(Pem::new(RSA_PUBLIC_KEY_PEM_LABEL, pkcs1)) + } + + pub fn to_pkcs1_pem_str(&self) -> Result { + self.to_pkcs1_pem().map(|pem| pem.to_string()) + } + + pub fn from_pem(pem: &Pem) -> Result { + match pem.label() { + PUBLIC_KEY_PEM_LABEL | EC_PUBLIC_KEY_PEM_LABEL => Self::from_der(pem.data()), + RSA_PUBLIC_KEY_PEM_LABEL => Self::from_pkcs1(pem.data()), + _ => Err(KeyError::InvalidPemLabel { + label: pem.label().to_owned(), + }), + } + } + + pub fn from_pem_str(pem_str: &str) -> Result { + let pem = parse_pem(pem_str)?; + Self::from_pem(&pem) + } + + pub fn from_der>(der: &T) -> Result { + Ok(Self(picky_asn1_der::from_bytes(der.as_ref()).map_err(|e| { + KeyError::Asn1Deserialization { + source: e, + element: "subject public key info", + } + })?)) + } + + pub fn from_pkcs1>(der: &T) -> Result { + use picky_asn1_x509::{AlgorithmIdentifier, PublicKey, RsaPublicKey}; + + let public_key = + picky_asn1_der::from_bytes::(der.as_ref()).map_err(|e| KeyError::Asn1Deserialization { + source: e, + element: "rsa public key", + })?; + + Ok(Self(SubjectPublicKeyInfo { + algorithm: AlgorithmIdentifier::new_rsa_encryption(), + subject_public_key: PublicKey::Rsa(public_key.into()), + })) + } + + pub fn kind(&self) -> KeyKind { + match self.0.subject_public_key { + picky_asn1_x509::PublicKey::Rsa(_) => KeyKind::Rsa, + picky_asn1_x509::PublicKey::Ec(_) => KeyKind::Ec, + picky_asn1_x509::PublicKey::Ed(_) => KeyKind::Ed, + picky_asn1_x509::PublicKey::Mldsa(_) => KeyKind::Mldsa, + } + } + + pub(crate) fn as_inner(&self) -> &SubjectPublicKeyInfo { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hash::HashAlgorithm; + use crate::key::ed::EdKeypair; + use crate::signature::SignatureAlgorithm; + use rsa::traits::PublicKeyParts; + use rstest::rstest; + + cfg_if::cfg_if! { if #[cfg(feature = "x509")] { + use crate::x509::{certificate::CertificateBuilder, date::UtcDate, name::DirectoryName}; + + fn generate_certificate_from_pk(private_key: PrivateKey) { + // validity + let valid_from = UtcDate::ymd(2019, 10, 10).unwrap(); + let valid_to = UtcDate::ymd(2019, 10, 11).unwrap(); + + CertificateBuilder::new() + .validity(valid_from, valid_to) + .self_signed(DirectoryName::new_common_name("Test Root CA"), &private_key) + .ca(true) + .build() + .expect("couldn't build root ca"); + } + } else { + fn generate_certificate_from_pk(_: PrivateKey) {} + }} + + /// Generating RSA keys in debug is very slow. Therefore, this test is ignored in debug builds + #[test] + #[cfg_attr(debug_assertions, ignore)] + fn generate_rsa_key() { + let private_key = PrivateKey::generate_rsa(4096).expect("couldn't generate rsa key"); + generate_certificate_from_pk(private_key); + } + + const PKCS1_PEM: &str = "-----BEGIN RSA PRIVATE KEY-----\n\ + MIIEpAIBAAKCAQEA5Kz4i/+XZhiE+fyrgtx/4yI3i6C6HXbC4QJYpDuSUEKN2bO9\n\ + RsE+Fnds/FizHtJVWbvya9ktvKdDPBdy58+CIM46HEKJhYLnBVlkEcg9N2RNgR3x\n\ + HnpRbKfv+BmWjOpSmWrmJSDLY0dbw5X5YL8TU69ImoouCUfStyCgrpwkctR0GD3G\n\ + fcGjbZRucV7VvVH9bS1jyaT/9yORyzPOSTwb+K9vOr6XlJX0CGvzQeIOcOimejHx\n\ + ACFOCnhEKXiwMsmL8FMz0drkGeMuCODY/OHVmAdXDE5UhroL0oDhSmIrdZ8CxngO\n\ + xHr1WD2yC0X0jAVP/mrxjSSfBwmmqhSMmONlvQIDAQABAoIBAQCJrBl3L8nWjayB\n\ + VL1ta5MTC+alCX8DfhyVmvQC7FqKN4dvKecqUe0vWXcj9cLhK4B3JdAtXfNLQOgZ\n\ + pYRoS2XsmjwiB20EFGtBrS+yBPvV/W0r7vrbfojHAdRXahBZhjl0ZAdrEvNgMfXt\n\ + Kr2YoXDhUQZFBCvzKmqSFfKnLRpEhsCBOsp+Sx0ZbP3yVPASXnqiZmKblpY4qcE5\n\ + KfYUO0nUWBSzY8I5c/29IY5oBbOUGS1DTMkx3R7V0BzbH/xmskVACn+cMzf467vp\n\ + yupTKG9hIX8ff0QH4Ggx88uQTRTI9IvfrAMnICFtR6U7g70hLN6j9ujXkPNhmycw\n\ + E5nQCmuBAoGBAPVbYtGBvnlySN73UrlyJ1NItUmOGhBt/ezpRjMIdMkJ6dihq7i2\n\ + RpE76sRvwHY9Tmw8oxR/V1ITK3dM2jZP1SRcm1mn5Y1D3K38jwFS0C47AXzIN2N+\n\ + LExekI1J4YOPV9o378vUKQuWpbQrQOOvylQBkRJ0Cd8DI3xhiBT/AVGbAoGBAO6Y\n\ + WBP3GMloO2v6PHijhRqrNdaI0qht8tDhO5L1troFLst3sfpK9fUP/KTlhHOzNVBF\n\ + fIJnNdcYAe9BISBbfSat+/R9F+GoUvpoC4j8ygHTQkT6ZMcMDfR8RQ4BlqGHIDKZ\n\ + YaAJoPZVkg7hNRMcvIruYpzFrheDE/4xvnC51GeHAoGAHzCFyFIw72lKwCU6e956\n\ + B0lH2ljZEVuaGuKwjM43YlMDSgmLNcjeAZpXRq9aDO3QKUwwAuwJIqLTNLAtURgm\n\ + 5R9slCIWuTV2ORvQ5f8r/aR8lOsyt1ATu4WN5JgOtdWj+laAAi4vJYz59YRGFGuF\n\ + UdZ9JZZgptvUR/xx+xFLjp8CgYBMRzghaeXqvgABTUb36o8rL4FOzP9MCZqPXPKG\n\ + 0TdR0UZcli+4LS7k4e+LaDUoKCrrNsvPhN+ZnHtB2jiU96rTKtxaFYQFCKM+mvTV\n\ + HrwWSUvucX62hAwSFYieKbPWgDSy+IZVe76SAllnmGg3bAB7CitMo4Y8zhMeORkB\n\ + QOe/EQKBgQDgeNgRud7S9BvaT3iT7UtizOr0CnmMfoF05Ohd9+VE4ogvLdAoDTUF\n\ + JFtdOT/0naQk0yqIwLDjzCjhe8+Ji5Y/21pjau8bvblTnASq26FRRjv5+hV8lmcR\n\ + zzk3Y05KXvJL75ksJdomkzZZb0q+Omf3wyjMR8Xl5WueJH1fh4hpBw==\n\ + -----END RSA PRIVATE KEY-----"; + + #[test] + fn private_key_from_rsa_pem() { + PrivateKey::from_pem(&PKCS1_PEM.parse::().expect("pem")).expect("private key"); + } + + #[test] + fn check_pkcs1() { + let private_pkcs1_pem = PKCS1_PEM.parse::().expect("pem"); + let private = PrivateKey::from_pem(&private_pkcs1_pem).expect("private key"); + + let private_pkcs1 = private.to_pkcs1().unwrap(); + PrivateKey::from_pkcs1(&private_pkcs1).unwrap(); + assert_eq!(private_pkcs1, private_pkcs1_pem.data()); + + let public = private.to_public_key().unwrap(); + let public_pkcs1 = public.to_pkcs1().unwrap(); + PublicKey::from_pkcs1(&public_pkcs1).unwrap(); + } + + const PUBLIC_KEY_PEM: &str = "-----BEGIN PUBLIC KEY-----\n\ + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA61BjmfXGEvWmegnBGSuS\n\ + +rU9soUg2FnODva32D1AqhwdziwHINFaD1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBS\n\ + EVCgJjtHAGZIm5GL/KA86KDp/CwDFMSwluowcXwDwoyinmeOY9eKyh6aY72xJh7n\n\ + oLBBq1N0bWi1e2i+83txOCg4yV2oVXhBo8pYEJ8LT3el6Smxol3C1oFMVdwPgc0v\n\ + Tl25XucMcG/ALE/KNY6pqC2AQ6R2ERlVgPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeu\n\ + lmCpGSynXNcpZ/06+vofGi/2MlpQZNhHAo8eayMp6FcvNucIpUndo1X8dKMv3Y26\n\ + ZQIDAQAB\n\ + -----END PUBLIC KEY-----"; + + #[test] + fn public_key_from_pem() { + PublicKey::from_pem(&PUBLIC_KEY_PEM.parse::().expect("pem")).expect("public key"); + } + + #[test] + fn public_key_to_and_from_pkcs1() { + let public_key = PublicKey::from_pem(&PUBLIC_KEY_PEM.parse::().expect("pem")).expect("public key"); + let pkcs1 = public_key.to_pkcs1().expect("PKCS1"); + let public_key_round_trip = PublicKey::from_pkcs1(&pkcs1).expect("round trip parse"); + assert_eq!(public_key_round_trip, public_key); + } + + const RSA_PUBLIC_KEY_PEM: &str = "-----BEGIN RSA PUBLIC KEY-----\n\ + MIIBCgKCAQEA61BjmfXGEvWmegnBGSuS+rU9soUg2FnODva32D1AqhwdziwHINFa\n\ + D1MVlcrYG6XRKfkcxnaXGfFDWHLEvNBSEVCgJjtHAGZIm5GL/KA86KDp/CwDFMSw\n\ + luowcXwDwoyinmeOY9eKyh6aY72xJh7noLBBq1N0bWi1e2i+83txOCg4yV2oVXhB\n\ + o8pYEJ8LT3el6Smxol3C1oFMVdwPgc0vTl25XucMcG/ALE/KNY6pqC2AQ6R2ERlV\n\ + gPiUWOPatVkt7+Bs3h5Ramxh7XjBOXeulmCpGSynXNcpZ/06+vofGi/2MlpQZNhH\n\ + Ao8eayMp6FcvNucIpUndo1X8dKMv3Y26ZQIDAQAB\n\ + -----END RSA PUBLIC KEY-----"; + + #[test] + fn public_key_from_rsa_pem() { + PublicKey::from_pem(&RSA_PUBLIC_KEY_PEM.parse::().expect("pem")).expect("public key"); + } + + const GARBAGE_PEM: &str = "-----BEGIN GARBAGE-----R0FSQkFHRQo=-----END GARBAGE-----"; + + #[test] + fn public_key_from_garbage_pem_err() { + let err = PublicKey::from_pem(&GARBAGE_PEM.parse::().expect("pem")).expect_err("key error"); + assert_eq!(err.to_string(), "invalid PEM label: GARBAGE"); + } + + fn check_pk(pem_str: &str) { + const MSG: &[u8] = b"abcde"; + + let pem = pem_str.parse::().expect("pem"); + let pk = PrivateKey::from_pem(&pem).expect("private key"); + let algo = SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256); + let signed_rsa = algo.sign(MSG, &pk).expect("rsa sign"); + algo.verify(&pk.to_public_key().unwrap(), MSG, &signed_rsa) + .expect("rsa verify rsa"); + + println!("Success!"); + } + + #[test] + fn invalid_coeff_private_key_regression() { + println!("2048 PK 7"); + check_pk(picky_test_data::RSA_2048_PK_7); + println!("4096 PK 3"); + check_pk(picky_test_data::RSA_4096_PK_3); + } + + #[test] + fn rsa_crate_private_key_conversion() { + use rsa::pkcs8::DecodePrivateKey; + + let pk_pem = picky_test_data::RSA_2048_PK_1.parse::().unwrap(); + let pk = PrivateKey::from_pem(&pk_pem).unwrap(); + let converted_rsa_private_key = RsaPrivateKey::try_from(&pk).unwrap(); + let expected_rsa_private_key = RsaPrivateKey::from_pkcs8_der(pk_pem.data()).unwrap(); + + assert_eq!(converted_rsa_private_key.n(), expected_rsa_private_key.n()); + assert_eq!(converted_rsa_private_key.e(), expected_rsa_private_key.e()); + assert_eq!(converted_rsa_private_key.d(), expected_rsa_private_key.d()); + + let converted_primes = converted_rsa_private_key.primes(); + let expected_primes = expected_rsa_private_key.primes(); + assert_eq!(converted_primes.len(), expected_primes.len()); + for (converted_prime, expected_prime) in converted_primes.iter().zip(expected_primes.iter()) { + assert_eq!(converted_prime, expected_prime); + } + } + + #[test] + #[cfg_attr(debug_assertions, ignore)] // this test is slow in debug + fn ring_understands_picky_pkcs8_rsa() { + // Make sure we're generating pkcs8 understood by the `ring` crate + let key = PrivateKey::generate_rsa(2048).unwrap(); + let pkcs8 = key.to_pkcs8().unwrap(); + ring::signature::RsaKeyPair::from_pkcs8(&pkcs8).unwrap(); + } + + #[rstest] + #[case(EcCurve::NistP256, &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING)] + #[case(EcCurve::NistP384, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING)] + fn ring_understands_picky_pkcs8_ec( + #[case] curve: EcCurve, + #[case] signing_alg: &'static ring::signature::EcdsaSigningAlgorithm, + ) { + // Make sure we're generating pkcs8 understood by the `ring` crate + let key = PrivateKey::generate_ec(curve).unwrap(); + let pkcs8 = key.to_pkcs8().unwrap(); + let rng = ring::rand::SystemRandom::new(); + + ring::signature::EcdsaKeyPair::from_pkcs8(signing_alg, &pkcs8, &rng).unwrap(); + } + + // Read from x25519 keys is not supported in `ring`, because it is mainly used for key + // exchange for which key serialization/deserialization is not needed at all. But we support, + // just to be consistent with OpenSSL and RFC https://www.rfc-editor.org/rfc/rfc8410 + #[test] + fn ring_understands_picky_pkcs8_ed25519() { + // Make sure we're generating pkcs8 understood by the `ring` crate. + // `ring` is very specific about the format of the ED25519 private key, and in contrast + // to OpenSSL, it uses newer v2 version of `PrivateKeyInfo` structure (`OneAsymmetricKey`) + // which always includes public key in the private key structure. + let key = PrivateKey::generate_ed(EdAlgorithm::Ed25519, true).unwrap(); + let pkcs8 = key.to_pkcs8().unwrap(); + + ring::signature::Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap(); + } + + #[test] + fn ring_ed25519_pkcs8_keys_could_be_parsed() { + let rng = ring::rand::SystemRandom::new(); + let pkcs8_bytes = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); + + let key = PrivateKey::from_pkcs8(&pkcs8_bytes).unwrap(); + let _pair = EdKeypair::try_from(&key).unwrap(); + } +} diff --git a/rust/vendor/picky/src/lib.rs b/rust/vendor/picky/src/lib.rs new file mode 100644 index 0000000..bde0dfb --- /dev/null +++ b/rust/vendor/picky/src/lib.rs @@ -0,0 +1,31 @@ +//! [![Crates.io](https://img.shields.io/crates/v/picky.svg)](https://crates.io/crates/picky) +//! [![docs.rs](https://docs.rs/picky/badge.svg)](https://docs.rs/picky) +//! ![Crates.io](https://img.shields.io/crates/l/picky) +//! # picky +//! +//! Portable X.509, PKI, JOSE and HTTP signature implementation. + +#[cfg(feature = "http_signature")] +pub mod http; + +#[cfg(feature = "jose")] +pub mod jose; + +#[cfg(feature = "x509")] +pub mod x509; + +#[cfg(feature = "ssh")] +pub mod ssh; + +#[cfg(feature = "pkcs12")] +pub mod pkcs12; + +#[cfg(feature = "putty")] +pub mod putty; + +pub mod hash; +pub mod key; +pub mod pem; +pub mod signature; + +pub use picky_asn1_x509::{AlgorithmIdentifier, oid, oids}; diff --git a/rust/vendor/picky/src/pem.rs b/rust/vendor/picky/src/pem.rs new file mode 100644 index 0000000..e7d6743 --- /dev/null +++ b/rust/vendor/picky/src/pem.rs @@ -0,0 +1,292 @@ +//! Privacy-Enhanced Mail (PEM) format utilities +//! +//! Based on the RFC-7468 +//! ([Textual Encodings of PKIX, PKCS, and CMS Structures](https://tools.ietf.org/html/rfc7468)). + +use base64::engine::general_purpose; +use base64::{DecodeError, Engine as _}; +use std::borrow::Cow; +use std::fmt; +use std::io::BufRead; +use std::str::FromStr; +use thiserror::Error; + +const PEM_HEADER_START: &str = "-----BEGIN"; +const PEM_FOOTER_START: &str = "-----END"; +const PEM_DASHES_BOUNDARIES: &str = "-----"; + +#[derive(Debug, Clone, Error)] +pub enum PemError { + /// header not found + #[error("header not found")] + HeaderNotFound, + + /// invalid pem header + #[error("invalid pem header")] + InvalidHeader, + + /// footer not found + #[error("footer not found")] + FooterNotFound, + + /// couldn't decode base64 + #[error("couldn't decode base64: {source}")] + Base64Decoding { source: DecodeError }, +} + +/// Privacy-Enhanced Mail (PEM) format structured representation +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pem<'a> { + label: String, + data: Cow<'a, [u8]>, +} + +impl<'a> Pem<'a> { + pub fn new, D: Into>>(label: S, data: D) -> Self { + Self { + label: label.into(), + data: data.into(), + } + } + + pub fn label(&self) -> &str { + &self.label + } + + pub fn data(&self) -> &[u8] { + &self.data + } + + pub fn into_data(self) -> Cow<'a, [u8]> { + self.data + } +} + +impl Pem<'static> { + pub fn read_from(reader: &mut impl BufRead) -> Result { + read_pem(reader) + } +} + +impl FromStr for Pem<'static> { + type Err = PemError; + + fn from_str(s: &str) -> Result { + parse_pem(s.as_bytes()) + } +} + +impl fmt::Display for Pem<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{} {}-----", PEM_HEADER_START, self.label)?; + + let encoded = general_purpose::STANDARD.encode(&self.data); + let bytes = encoded.as_bytes(); + for chunk in bytes.chunks(64) { + let chunk = std::str::from_utf8(chunk).map_err(|_| fmt::Error)?; + writeln!(f, "{chunk}")?; + } + + write!(f, "{} {}-----", PEM_FOOTER_START, self.label)?; + + Ok(()) + } +} + +impl From> for String { + fn from(pem: Pem<'_>) -> Self { + pem.to_string() + } +} + +/// Parse a PEM-encoded stream from a [u8] representation +/// +/// If the input contains line ending characters (`\r`, `\n`), a copy of input +/// is allocated striping these. If you can strip these with minimal data copy +/// you should do it beforehand. +pub fn parse_pem>(input: &T) -> Result, PemError> { + parse_pem_impl(input.as_ref()) +} + +fn parse_pem_impl(input: &[u8]) -> Result, PemError> { + let header_start_idx = h_find(input, PEM_HEADER_START.as_bytes()).ok_or(PemError::HeaderNotFound)?; + + let label_start_idx = header_start_idx + PEM_HEADER_START.len(); + let label_end_idx = h_find(&input[label_start_idx..], b"-").ok_or(PemError::InvalidHeader)? + label_start_idx; + let label = String::from_utf8_lossy(&input[label_start_idx..label_end_idx]) + .trim() + .to_owned(); + + let header_end_idx = h_find(&input[label_end_idx..], PEM_DASHES_BOUNDARIES.as_bytes()) + .ok_or(PemError::InvalidHeader)? + + label_end_idx + + PEM_DASHES_BOUNDARIES.len(); + + let footer_start_idx = + h_find(&input[header_end_idx..], PEM_FOOTER_START.as_bytes()).ok_or(PemError::FooterNotFound)? + header_end_idx; + + let raw_data = &input[header_end_idx..footer_start_idx]; + + let data = if h_find(raw_data, b"\n").is_some() { + // Line ending characters should be striped... Sadly, this means we need to copy and allocate. + let striped_raw_data: Vec = raw_data + .iter() + .copied() + .filter(|&byte| byte != b'\r' && byte != b'\n') + .collect(); + general_purpose::STANDARD + .decode(striped_raw_data) + .map_err(|source| PemError::Base64Decoding { source })? + } else { + // Can be decoded as is! + general_purpose::STANDARD + .decode(raw_data) + .map_err(|source| PemError::Base64Decoding { source })? + }; + + Ok(Pem { + label, + data: Cow::Owned(data), + }) +} + +fn h_find(buffer: &[u8], value: &[u8]) -> Option { + buffer.windows(value.len()).position(|window| window == value) +} + +/// Parse a PEM-encoded stream from a BufRead object. +/// +/// Maybe slower than the AsRef<[u8]>-based implementation because additional copies are incurred, +/// but in most cases it's probably easier to work with and not that bad anyway. +pub fn read_pem(reader: &mut impl BufRead) -> Result, PemError> { + let mut buf = Vec::with_capacity(1024); + + // skip until start of header + h_read_until(reader, PEM_HEADER_START.as_bytes(), &mut buf).ok_or(PemError::HeaderNotFound)?; + buf.clear(); + + // read until end of header + h_read_until(reader, PEM_DASHES_BOUNDARIES.as_bytes(), &mut buf).ok_or(PemError::InvalidHeader)?; + let buf_utf8 = core::str::from_utf8(&buf).map_err(|_| PemError::InvalidHeader)?; + let label = buf_utf8.trim_end_matches(PEM_DASHES_BOUNDARIES).trim().to_owned(); + buf.clear(); + + // read to footer + h_read_until(reader, PEM_FOOTER_START.as_bytes(), &mut buf).ok_or(PemError::FooterNotFound)?; + let base64_data: Vec = h_trim_end_matches(&buf, PEM_FOOTER_START.as_bytes()) + .iter() + .cloned() + .filter(|&byte| byte != b'\r' && byte != b'\n') + .collect(); + let data = general_purpose::STANDARD + .decode(base64_data) + .map_err(|source| PemError::Base64Decoding { source })?; + + // read until end of footer + h_read_until(reader, PEM_DASHES_BOUNDARIES.as_bytes(), &mut buf).ok_or(PemError::FooterNotFound)?; + + Ok(Pem { + label, + data: Cow::Owned(data), + }) +} + +// Helper to read until some pattern is matched. Returns None on any error +// (cannot be copy pasted for any purpose and should stay private!). +fn h_read_until(reader: &mut impl BufRead, pat: &[u8], buf: &mut Vec) -> Option { + let mut read = 0; + let first_delim = *pat.first()?; + 'outer: loop { + read += reader.read_until(first_delim, buf).ok()?; + + for &next_delim in &pat[1..] { + let mut next = [0]; + reader.read_exact(&mut next).ok()?; + buf.push(next[0]); + read += 1; + + if next[0] != next_delim { + continue 'outer; + } + } + + break Some(read); + } +} + +// Helper to trim trailing characters matching the given pattern for bytes slice +fn h_trim_end_matches<'a>(slice: &'a [u8], pat: &[u8]) -> &'a [u8] { + for (&slice_elem, &pat_elem) in slice.iter().rev().zip(pat.iter().rev()) { + if slice_elem != pat_elem { + return slice; // pattern doesn't match, return all the slice + } + } + + // pattern did match, return sub-slice + &slice[..slice.len() - pat.len()] +} + +/// Build a PEM-encoded structure into a String. +pub fn to_pem(label: S, data: &T) -> String +where + S: Into, + T: ?Sized + AsRef<[u8]>, +{ + Pem::new(label, data.as_ref()).to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::BufReader; + + #[test] + fn parse() { + let pem_from_bytes = parse_pem(picky_test_data::PEM_BYTES).unwrap(); + assert_eq!(pem_from_bytes.label, "CERTIFICATE"); + + let pem_from_str = picky_test_data::PEM_STR.parse::().unwrap(); + pretty_assertions::assert_eq!(pem_from_bytes, pem_from_str); + } + + #[test] + fn reader_based() { + let mut reader = BufReader::new(picky_test_data::PEM_BYTES); + + let pem_from_reader = read_pem(&mut reader).unwrap(); + assert_eq!(pem_from_reader.label, "CERTIFICATE"); + + let pem_from_str = picky_test_data::PEM_STR.parse::().unwrap(); + pretty_assertions::assert_eq!(pem_from_reader, pem_from_str); + } + + // This test should not run on Windows. writeln! add `/r` ending character to Pem in String format on Windows targets. + #[cfg(not(windows))] + #[test] + fn to_string() { + let pem = picky_test_data::PEM_STR.parse::().unwrap(); + let reconverted_pem = pem.to_string(); + pretty_assertions::assert_eq!(reconverted_pem, picky_test_data::PEM_STR); + } + + const FLATTENED_PEM: &str = "-----BEGIN GARBAGE-----R0FSQkFHRQo=-----END GARBAGE-----"; + + #[test] + fn flattened() { + FLATTENED_PEM.parse::().unwrap(); + read_pem(&mut BufReader::new(FLATTENED_PEM.as_bytes())).unwrap(); + } + + const MULTIPLE_PEM: &str = "-----BEGIN GARBAGE1-----R0FSQkFHRQo=-----END GARBAGE1-----\ + -----BEGIN GARBAGE2-----R0FSQkFHRQo=-----END GARBAGE2-----"; + + #[test] + fn multiple() { + // reading multiple PEM from some bytes stream is easier with read-based API + let mut reader = BufReader::new(MULTIPLE_PEM.as_bytes()); + let pem1 = read_pem(&mut reader).unwrap(); + assert_eq!(pem1.label, "GARBAGE1"); + let pem2 = read_pem(&mut reader).unwrap(); + assert_eq!(pem2.label, "GARBAGE2"); + } +} diff --git a/rust/vendor/picky/src/pkcs12/attribute.rs b/rust/vendor/picky/src/pkcs12/attribute.rs new file mode 100644 index 0000000..a55fcab --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/attribute.rs @@ -0,0 +1,249 @@ +use crate::pkcs12::Pkcs12Error; +use picky_asn1::restricted_string::BmpString; +use picky_asn1::wrapper::OctetStringAsn1; +use picky_asn1_der::Asn1RawDer; +use picky_asn1_x509::oid::ObjectIdentifier; +use picky_asn1_x509::pkcs12::Pkcs12Attribute as Pkcs12AttributeAsn1; +use serde::{Deserialize, Serialize}; + +/// Represents a PKCS#12 attributes which can be used to store additional information about safe +/// bag contents (e.g. private key or certificate). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pkcs12Attribute { + kind: Pkcs12AttributeKind, + inner: Pkcs12AttributeAsn1, +} + +impl Pkcs12Attribute { + pub(crate) fn from_asn1(asn1: Pkcs12AttributeAsn1) -> Self { + let kind = match &asn1 { + Pkcs12AttributeAsn1::FriendlyName(value) => Pkcs12AttributeKind::FriendlyName(value.clone()), + Pkcs12AttributeAsn1::LocalKeyId(value) => Pkcs12AttributeKind::LocalKeyId(value.0.clone()), + Pkcs12AttributeAsn1::Unknown { oid, value } => Pkcs12AttributeKind::Custom(CustomPkcs12Attribute { + oid: oid.clone(), + value: value.clone(), + }), + }; + + Self { kind, inner: asn1 } + } + + /// Creates a new `friendly name` attribute. This attribute is used to store a human-readable + /// name of the safe bag contents (e.g. certificate name). + pub fn new_friendly_name(value: BmpString) -> Self { + let kind = Pkcs12AttributeKind::FriendlyName(value); + let inner = kind.to_inner(); + Self { kind, inner } + } + + /// Creates a new `local key id` attribute. This attribute is used to indicate relation between + /// private key and certificate (when set to same value on both objects). + pub fn new_local_key_id(value: impl Into>) -> Self { + let kind = Pkcs12AttributeKind::LocalKeyId(value.into()); + let inner = kind.to_inner(); + Self { kind, inner } + } + + /// Create a new custom attribute (e.g. Microsoft-specific attributes). + pub fn new_custom(attr: CustomPkcs12Attribute) -> Self { + let kind = Pkcs12AttributeKind::Custom(attr); + let inner = kind.to_inner(); + Self { kind, inner } + } + + pub fn kind(&self) -> &Pkcs12AttributeKind { + &self.kind + } + + pub fn inner(&self) -> &Pkcs12AttributeAsn1 { + &self.inner + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Pkcs12AttributeKind { + FriendlyName(BmpString), + LocalKeyId(Vec), + Custom(CustomPkcs12Attribute), +} + +impl Pkcs12AttributeKind { + pub(crate) fn to_inner(&self) -> Pkcs12AttributeAsn1 { + match self { + Self::FriendlyName(value) => Pkcs12AttributeAsn1::FriendlyName(value.clone()), + Self::LocalKeyId(value) => Pkcs12AttributeAsn1::LocalKeyId(OctetStringAsn1::from(value.clone())), + Self::Custom(value) => Pkcs12AttributeAsn1::Unknown { + oid: value.oid.clone(), + value: value.value.clone(), + }, + } + } +} + +impl From for Pkcs12AttributeKind { + fn from(value: Pkcs12AttributeAsn1) -> Self { + match value { + Pkcs12AttributeAsn1::FriendlyName(value) => Self::FriendlyName(value), + Pkcs12AttributeAsn1::LocalKeyId(value) => Self::LocalKeyId(value.0), + Pkcs12AttributeAsn1::Unknown { oid, value } => CustomPkcs12Attribute { oid, value }.into(), + } + } +} + +impl From for Pkcs12AttributeKind { + fn from(value: CustomPkcs12Attribute) -> Self { + Self::Custom(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomPkcs12Attribute { + oid: ObjectIdentifier, + value: Vec, +} + +impl CustomPkcs12Attribute { + /// Get attribute oid. + pub fn oid(&self) -> &ObjectIdentifier { + &self.oid + } + + pub fn new_empty(oid: ObjectIdentifier) -> Self { + Self { oid, value: Vec::new() } + } + + pub fn has_value(&self) -> bool { + !self.value.is_empty() + } + + pub fn new_raw(oid: ObjectIdentifier, value: Vec) -> Self { + Self { oid, value } + } + + /// Creates a new custom attribute from any DER-serializable value. Ut is advised to use types + /// from `picky-asn1-der` crate. + pub fn new_single_value(oid: ObjectIdentifier, value: &T) -> Result { + let encoded = picky_asn1_der::to_vec(value)?; + Ok(Self { + oid, + value: vec![Asn1RawDer(encoded)], + }) + } + + /// Creates a new custom attribute from multiple any DER-serializable value (PKCS#12 allows + /// attribute to have list of values). It is advised to use types from `picky-asn1-der` crate. + pub fn new_multiple_values<'a, T: Serialize + 'a>( + oid: ObjectIdentifier, + values: impl IntoIterator, + ) -> Result { + let mut encoded_values = Vec::new(); + for value in values { + let encoded = picky_asn1_der::to_vec(value)?; + encoded_values.push(Asn1RawDer(encoded)); + } + Ok(Self { + oid, + value: encoded_values, + }) + } + + /// Convert inner value to any DER-deserializable value. It is advised to use types from + /// `picky-asn1-der` crate. + pub fn to_single_value<'a, T: Deserialize<'a>>(&'a self) -> Result { + if self.value.len() != 1 { + return Err(Pkcs12Error::UnexpectedAttributeValuesCount { + expected: 1, + actual: self.value.len(), + }); + } + let deserialized = picky_asn1_der::from_bytes(&self.value[0].0)?; + Ok(deserialized) + } + + /// Convert inner value to multiple DER-deserializable values. It is advised to use types from + /// `picky-asn1-der` crate. + pub fn to_multiple_values<'a, T: Deserialize<'a>>(&'a self) -> Result, Pkcs12Error> { + let values_result: Result, _> = self + .value + .iter() + .map(|value| picky_asn1_der::from_bytes(&value.0)) + .collect(); + values_result.map_err(Into::into) + } + + pub fn raw_data(&self) -> &[Asn1RawDer] { + &self.value + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + use expect_test::expect; + + fn fake_oid() -> ObjectIdentifier { + "1.3.6.1.4.1.311.17.1".to_string().try_into().unwrap() + } + + #[test] + fn single_custom_attribute_roundtrip() { + let value = BmpString::from_str("Microsoft Software Key Storage Provider").unwrap(); + let attr = CustomPkcs12Attribute::new_single_value(fake_oid(), &value).unwrap(); + let decoded = attr.to_single_value::().unwrap(); + assert_eq!(decoded, value); + } + + #[test] + fn single_custom_attribute_roundtrip_no_value() { + let attr = CustomPkcs12Attribute::new_empty(fake_oid()); + assert!(!attr.has_value()); + let decoded = attr.to_single_value::(); + expect![[r#" + Err( + UnexpectedAttributeValuesCount { + expected: 1, + actual: 0, + }, + ) + "#]] + .assert_debug_eq(&decoded); + } + + #[test] + fn single_custom_attribute_roundtrip_too_many_values() { + let value = vec![OctetStringAsn1(vec![0x01]), OctetStringAsn1(vec![0x02])]; + let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap(); + let decoded = attr.to_single_value::(); + expect![[r#" + Err( + UnexpectedAttributeValuesCount { + expected: 1, + actual: 2, + }, + ) + "#]] + .assert_debug_eq(&decoded); + } + + #[test] + fn multiple_custom_attributes_roundtrip() { + let value = vec![ + OctetStringAsn1(vec![0x01, 0x02, 0x03]), + OctetStringAsn1(vec![0x04, 0x05, 0x06]), + ]; + + let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap(); + let decoded = attr.to_multiple_values::().unwrap(); + assert_eq!(decoded, value); + } + + #[test] + fn multiple_custom_attributes_empty_list_allowed() { + let value: Vec = vec![]; + let attr = CustomPkcs12Attribute::new_multiple_values(fake_oid(), &value).unwrap(); + let decoded = attr.to_multiple_values::().unwrap(); + assert_eq!(decoded, value); + } +} diff --git a/rust/vendor/picky/src/pkcs12/encryption.rs b/rust/vendor/picky/src/pkcs12/encryption.rs new file mode 100644 index 0000000..49281f2 --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/encryption.rs @@ -0,0 +1,674 @@ +use crate::pkcs12::{Pbkdf1Usage, Pkcs12Error, Pkcs12HashAlgorithm, pbkdf1}; +use picky_asn1::restricted_string::BmpString; +use picky_asn1::wrapper::OctetStringAsn1; +pub use picky_asn1_x509::pkcs12::Pbes1AlgorithmKind as Pbes1Cipher; +use picky_asn1_x509::pkcs12::{ + Pbes1Params as Pbes1ParamsAsn1, Pbes2AesCbcEncryption as Pbes2AesCbcEncryptionAsn1, + Pbes2EncryptionScheme as Pbes2EncryptionSchemeAsn1, Pbes2KeyDerivationFunc as Pbes2KeyDerivationFuncAsn1, + Pbes2Params as Pbes2ParamsAsn1, Pbkdf2Params as Pbkdf2ParamsAsn1, Pbkdf2Prf as Pbkdf2PrfAsn1, + Pbkdf2SaltSource as Pbkdf2SaltSourceAsn1, Pkcs12EncryptionAlgorithm as Pkcs12EncryptionAsn1, +}; +use rand::rngs::{StdRng, SysRng}; +use rand_core::SeedableRng as _; +use std::str::FromStr as _; + +/// Same default KDF iterations as in OpenSSL +const DEFAULT_KDF_ITERATIONS: usize = 2048; +const DEFAULT_SALT_SIZE: usize = 8; +const AES_BLOCK_SIZE: usize = 16; + +/// Crypto operations context for PFX file parsing/building. Contains password inside as a secure +/// string and RNG. +pub struct Pkcs12CryptoContext { + password: zeroize::Zeroizing, + rng: Box, +} + +impl Pkcs12CryptoContext { + /// Creates new context with given password and default + pub fn new_with_password(password: &str) -> Result { + Ok(Self { + password: password.to_string().into(), + rng: Box::new(StdRng::try_from_rng(&mut SysRng)?), + }) + } + + /// Sets RNG for this context + pub fn with_rng(mut self, rng: impl rand::CryptoRng + 'static) -> Self { + self.rng = Box::new(rng); + self + } + + /// Creates new context with empty password and default RNG + pub fn new_without_password() -> Result { + Ok(Self { + password: String::new().into(), + rng: Box::new(StdRng::try_from_rng(&mut SysRng)?), + }) + } + + /// Returns password in PBES1 password representation - UCS2 encoded string with null terminator + pub(crate) fn password_bytes_pbes1(&self) -> Result>, Pkcs12Error> { + let mut bmp = zeroize::Zeroizing::new(BmpString::from_str(&self.password)?.into_bytes()); + bmp.extend_from_slice(&[0, 0]); + + Ok(bmp) + } + + /// Returns password in PBES2 password representation - UTF8 encoded string + pub(crate) fn password_bytes_pbes2(&self) -> &[u8] { + self.password.as_bytes() + } + + pub(crate) fn generate_bytes(&mut self, len: usize) -> Vec { + let mut data = vec![0u8; len]; + self.rng.fill_bytes(&mut data); + data + } +} + +/// This type holds all information required to encrypt/decrypt data in PKCS#12 file, including +/// encryption algorithm, salt, IV and KDF iterations. +/// +/// This type is not cloneable, because it is intended to be used only once for each encryptable +/// object. If for some reason you need to have exactly the same salt/IV values for multiple +/// encryptable objects, you should create crypto context with custom RNG set to same seed and +/// create multiple Pkcs12Encryption objects from it. +#[derive(Debug, PartialEq, Eq)] +pub struct Pkcs12Encryption { + kind: Pkcs12EncryptionKind, + inner: Pkcs12EncryptionAsn1, +} + +impl Pkcs12Encryption { + /// Clone-like operation only could be performed internally when cloning higher level structures. + /// Pkcs12Encryption non-cloneable nature is intentional to provide hint to the user that this + /// structure should be used only once for each encryptable object. + pub(crate) fn duplicate(&self) -> Self { + Self { + kind: self.kind.clone(), + inner: self.inner.clone(), + } + } + + pub(crate) fn from_asn1(inner: Pkcs12EncryptionAsn1) -> Result { + let kind = match &inner { + Pkcs12EncryptionAsn1::Pbes1 { kind, params } => Pkcs12EncryptionKind::Pbes1(Pbes1Encryption { + cipher: *kind, + kdf_iterations: Some(params.iterations), + }), + Pkcs12EncryptionAsn1::Pbes2(Pbes2ParamsAsn1 { + key_derivation_func: + Pbes2KeyDerivationFuncAsn1::Pbkdf2(Pbkdf2ParamsAsn1 { + iteration_count, prf, .. + }), + encryption_scheme: Pbes2EncryptionSchemeAsn1::AesCbc { kind, .. }, + }) => { + let hmac_kdf = match &prf { + Some(algorithm) => Pkcs12HashAlgorithm::from_asn1_pbkdf2_prf(algorithm)?, + None => Pkcs12HashAlgorithm::Sha1, + }; + Pkcs12EncryptionKind::Pbes2(Pbes2Encryption { + cipher: (*kind).into(), + hmac_kdf, + kdf_iterations: Some(*iteration_count), + }) + } + _ => Pkcs12EncryptionKind::Unknown, + }; + + Ok(Self { kind, inner }) + } + + /// Create new legacy PBES1 encryption (Not recommended for new files) + pub fn new_pbes1(encryption: Pbes1Encryption, context: &mut Pkcs12CryptoContext) -> Self { + let salt = context.generate_bytes(DEFAULT_SALT_SIZE); + let inner = Pkcs12EncryptionAsn1::Pbes1 { + kind: encryption.cipher, + params: Pbes1ParamsAsn1 { + salt: OctetStringAsn1(salt), + iterations: encryption.kdf_iterations.unwrap_or(DEFAULT_KDF_ITERATIONS as u32), + }, + }; + + Self { + kind: Pkcs12EncryptionKind::Pbes1(encryption), + inner, + } + } + + /// Create new PBES2 encryption (It is advised to use PBES2 for new files) + pub fn new_pbes2(encryption: Pbes2Encryption, context: &mut Pkcs12CryptoContext) -> Self { + let iv = context.generate_bytes(AES_BLOCK_SIZE); + let encryption_scheme = Pbes2EncryptionSchemeAsn1::AesCbc { + kind: encryption.cipher.into(), + iv: OctetStringAsn1(iv), + }; + + // Skip serialization if set to SHA1 as specified in RFC + let prf = match Pbkdf2PrfAsn1::from(encryption.hmac_kdf) { + Pbkdf2PrfAsn1::HmacWithSha1 => None, + value => Some(value), + }; + + let kdf_params = Pbkdf2ParamsAsn1 { + salt: Pbkdf2SaltSourceAsn1::Specified(OctetStringAsn1(context.generate_bytes(DEFAULT_SALT_SIZE))), + iteration_count: encryption.kdf_iterations.unwrap_or(DEFAULT_KDF_ITERATIONS as u32), + // key length is not set by most implementations + key_length: None, + prf, + }; + + let pbes2_params = Pbes2ParamsAsn1 { + key_derivation_func: Pbes2KeyDerivationFuncAsn1::Pbkdf2(kdf_params), + encryption_scheme, + }; + + let inner = Pkcs12EncryptionAsn1::Pbes2(pbes2_params); + + Self { + kind: Pkcs12EncryptionKind::Pbes2(encryption), + inner, + } + } + + /// Parsed encryption representation + pub fn kind(&self) -> &Pkcs12EncryptionKind { + &self.kind + } + + pub fn inner(&self) -> &Pkcs12EncryptionAsn1 { + &self.inner + } + + pub(crate) fn decrypt(&self, data: &[u8], context: &Pkcs12CryptoContext) -> Result, Pkcs12Error> { + match self.inner() { + Pkcs12EncryptionAsn1::Pbes1 { kind, params } => { + let password = context.password_bytes_pbes1()?; + decrypt_pbes1( + *kind, + password.as_slice(), + params.salt.as_slice(), + params.iterations as usize, + data, + ) + } + Pkcs12EncryptionAsn1::Pbes2(params) => { + let password = context.password_bytes_pbes2(); + decrypt_pbes2(params, password, data) + } + Pkcs12EncryptionAsn1::Unknown(raw) => { + let oid = raw.algorithm().clone(); + Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid), + context: "decryption".to_string(), + }) + } + } + } + + pub(crate) fn encrypt(&self, data: &[u8], context: &Pkcs12CryptoContext) -> Result, Pkcs12Error> { + match self.inner() { + Pkcs12EncryptionAsn1::Pbes1 { kind, params } => { + let password = context.password_bytes_pbes1()?; + encrypt_pbes1( + *kind, + password.as_slice(), + params.salt.as_slice(), + params.iterations as usize, + data, + ) + } + Pkcs12EncryptionAsn1::Pbes2(params) => { + let password = context.password_bytes_pbes2(); + encrypt_pbes2(params, password, data) + } + Pkcs12EncryptionAsn1::Unknown(raw) => { + let oid = raw.algorithm().clone(); + Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid), + context: "encryption".to_string(), + }) + } + } + } +} + +/// Supported PKCS12 encryption algorithm descriptor. +/// If parsed PFX file contains unknown encryption algorithm, [`Pkcs12EncryptionKind::Unknown`] +/// variant is returned instead. If such encryption is encountered, then we can't use encrypted PFX +/// nodes, but unencrypted data still could be extracted. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Pkcs12EncryptionKind { + Pbes1(Pbes1Encryption), + Pbes2(Pbes2Encryption), + Unknown, +} + +/// PBES1 encryption descriptor. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pbes1Encryption { + cipher: Pbes1Cipher, + kdf_iterations: Option, +} + +impl Pbes1Encryption { + /// Creates new PBES1 encryption descriptor with given cipher algorithm. + pub fn new(cipher: Pbes1Cipher) -> Self { + Self { + cipher, + kdf_iterations: None, + } + } + + /// Sets KDF iteraions count to `iterations`. If not set, [`DEFAULT_KDF_ITERATIONS`] is used + /// instead. + pub fn with_kdf_iterations(mut self, iterations: u32) -> Self { + self.kdf_iterations = Some(iterations); + self + } +} + +/// PBES2 encryption descriptor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Pbes2Encryption { + cipher: Pbes2Cipher, + hmac_kdf: Pkcs12HashAlgorithm, + kdf_iterations: Option, +} + +impl Pbes2Encryption { + /// Creates new PBES2 encryption descriptor with given kdf and cipher algorithms. + pub fn new(cipher: Pbes2Cipher, hmac_kdf: Pkcs12HashAlgorithm) -> Self { + Self { + cipher, + hmac_kdf, + kdf_iterations: None, + } + } + + /// Sets KDF iteraions count to `iterations`. If not set, [`DEFAULT_KDF_ITERATIONS`] is used + /// instead. + pub fn with_kdf_iterations(mut self, iterations: u32) -> Self { + self.kdf_iterations = Some(iterations); + self + } +} + +/// PBES2 cipher algorithm +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Pbes2Cipher { + Aes128Cbc, + Aes192Cbc, + Aes256Cbc, +} + +impl Pbes2Cipher { + /// Returns cipher key size in bytes. + pub fn key_size(self) -> usize { + match self { + Self::Aes128Cbc => 16, + Self::Aes192Cbc => 24, + Self::Aes256Cbc => 32, + } + } +} + +impl From for Pbes2AesCbcEncryptionAsn1 { + fn from(value: Pbes2Cipher) -> Self { + match value { + Pbes2Cipher::Aes128Cbc => Self::Aes128, + Pbes2Cipher::Aes192Cbc => Self::Aes192, + Pbes2Cipher::Aes256Cbc => Self::Aes256, + } + } +} + +impl From for Pbes2Cipher { + fn from(value: Pbes2AesCbcEncryptionAsn1) -> Self { + match value { + Pbes2AesCbcEncryptionAsn1::Aes128 => Self::Aes128Cbc, + Pbes2AesCbcEncryptionAsn1::Aes192 => Self::Aes192Cbc, + Pbes2AesCbcEncryptionAsn1::Aes256 => Self::Aes256Cbc, + } + } +} + +impl From for Pbkdf2PrfAsn1 { + fn from(value: Pkcs12HashAlgorithm) -> Self { + match value { + Pkcs12HashAlgorithm::Sha1 => Self::HmacWithSha1, + Pkcs12HashAlgorithm::Sha224 => Self::HmacWithSha224, + Pkcs12HashAlgorithm::Sha256 => Self::HmacWithSha256, + Pkcs12HashAlgorithm::Sha384 => Self::HmacWithSha384, + Pkcs12HashAlgorithm::Sha512 => Self::HmacWithSha512, + } + } +} + +struct Pbes2CipherInputs { + key: Vec, + iv: Vec, + cipher: Pbes2Cipher, +} + +fn prepare_pbes2_cipher_inputs( + params: &Pbes2ParamsAsn1, + password: &[u8], + cipher_context: &str, +) -> Result { + let (salt, kdf_iterations, prf) = match ¶ms.key_derivation_func { + Pbes2KeyDerivationFuncAsn1::Pbkdf2(kdf) => { + let salt = match &kdf.salt { + Pbkdf2SaltSourceAsn1::Specified(salt) => salt.0.clone(), + Pbkdf2SaltSourceAsn1::OtherSource(raw) => { + let oid = raw.algorithm().clone(); + return Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid), + context: format!("pbes2 {cipher_context} (kdf salt source)"), + }); + } + }; + + let prf = match &kdf.prf { + None => Pkcs12HashAlgorithm::Sha1, + Some(Pbkdf2PrfAsn1::HmacWithSha1) => Pkcs12HashAlgorithm::Sha1, + Some(Pbkdf2PrfAsn1::HmacWithSha224) => Pkcs12HashAlgorithm::Sha224, + Some(Pbkdf2PrfAsn1::HmacWithSha256) => Pkcs12HashAlgorithm::Sha256, + Some(Pbkdf2PrfAsn1::HmacWithSha384) => Pkcs12HashAlgorithm::Sha384, + Some(Pbkdf2PrfAsn1::HmacWithSha512) => Pkcs12HashAlgorithm::Sha512, + Some(Pbkdf2PrfAsn1::Unknown(raw)) => { + let oid = raw.algorithm().clone(); + return Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid), + context: format!("pbes2 {cipher_context} (kdf prf)"), + }); + } + }; + + (salt, kdf.iteration_count, prf) + } + Pbes2KeyDerivationFuncAsn1::Unknown(raw) => { + let oid = raw.algorithm().clone(); + return Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid), + context: format!("pbes2 {cipher_context} (kdf)"), + }); + } + }; + + let (cipher, iv) = match ¶ms.encryption_scheme { + Pbes2EncryptionSchemeAsn1::AesCbc { kind, iv } => (Pbes2Cipher::from(*kind), iv.0.clone()), + Pbes2EncryptionSchemeAsn1::Unknown(raw) => { + let oid = raw.algorithm().clone(); + return Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: super::UnsupportedPkcs12Algorithm::Oid(oid), + context: format!("pbes2 {cipher_context} (cipher)"), + }); + } + }; + + let calculate_kdf = match prf { + Pkcs12HashAlgorithm::Sha1 => pbkdf2::pbkdf2_hmac::, + Pkcs12HashAlgorithm::Sha224 => pbkdf2::pbkdf2_hmac::, + Pkcs12HashAlgorithm::Sha256 => pbkdf2::pbkdf2_hmac::, + Pkcs12HashAlgorithm::Sha384 => pbkdf2::pbkdf2_hmac::, + Pkcs12HashAlgorithm::Sha512 => pbkdf2::pbkdf2_hmac::, + }; + + let mut key = vec![0u8; cipher.key_size()]; + calculate_kdf(password, salt.as_slice(), kdf_iterations, key.as_mut_slice()); + + Ok(Pbes2CipherInputs { key, iv, cipher }) +} + +fn decrypt_pbes2(params: &Pbes2ParamsAsn1, password: &[u8], data: &[u8]) -> Result, Pkcs12Error> { + let Pbes2CipherInputs { key, iv, cipher } = prepare_pbes2_cipher_inputs(params, password, "decryption")?; + + use aes::cipher::BlockModeDecrypt; + use cbc::Decryptor; + use cbc::cipher::KeyIvInit; + use cbc::cipher::block_padding::Pkcs7; + + let decrypted = match cipher { + Pbes2Cipher::Aes128Cbc => { + use aes::Aes128; + type Aes128Cbc = Decryptor; + + let aes = Aes128Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES128 decryptor initialization failed".to_string(), + })?; + + aes.decrypt_padded_vec::(data).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES128 decryption with padding failed".to_string(), + })? + } + Pbes2Cipher::Aes192Cbc => { + use aes::Aes192; + type Aes192Cbc = Decryptor; + + let aes = Aes192Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES192 decryptor initialization failed".to_string(), + })?; + + aes.decrypt_padded_vec::(data).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES192 decryption with padding failed".to_string(), + })? + } + Pbes2Cipher::Aes256Cbc => { + use aes::Aes256; + type Aes256Cbc = Decryptor; + + let aes = Aes256Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES256 decryptor initialization failed".to_string(), + })?; + + aes.decrypt_padded_vec::(data).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES256 decryption with padding failed".to_string(), + })? + } + }; + + Ok(decrypted) +} + +fn encrypt_pbes2(params: &Pbes2ParamsAsn1, password: &[u8], data: &[u8]) -> Result, Pkcs12Error> { + let Pbes2CipherInputs { key, iv, cipher } = prepare_pbes2_cipher_inputs(params, password, "encryption")?; + + use aes::cipher::BlockModeEncrypt; + use cbc::Encryptor; + use cbc::cipher::KeyIvInit; + use cbc::cipher::block_padding::Pkcs7; + + let encrypted = match cipher { + Pbes2Cipher::Aes128Cbc => { + use aes::Aes128; + type Aes128Cbc = Encryptor; + + let aes = Aes128Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES128 encryptor initialization failed".to_string(), + })?; + + aes.encrypt_padded_vec::(data) + } + Pbes2Cipher::Aes192Cbc => { + use aes::Aes192; + type Aes192Cbc = Encryptor; + + let aes = Aes192Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES192 encryptor initialization failed".to_string(), + })?; + + aes.encrypt_padded_vec::(data) + } + Pbes2Cipher::Aes256Cbc => { + use aes::Aes256; + type Aes256Cbc = Encryptor; + + let aes = Aes256Cbc::new_from_slices(key.as_slice(), iv.as_slice()).map_err(|_| Pkcs12Error::Pbes2 { + context: "AES256 encryptor initialization failed".to_string(), + })?; + + aes.encrypt_padded_vec::(data) + } + }; + + Ok(encrypted) +} + +fn generate_pbes1_key_and_iv( + cipher: Pbes1Cipher, + password: &[u8], + salt: &[u8], + kdf_iterations: usize, +) -> (Vec, Vec) { + let (key_size, iv_size) = match cipher { + Pbes1Cipher::ShaAnd40BitRc2Cbc => (5, 8), + Pbes1Cipher::ShaAnd3Key3DesCbc => (24, 8), + }; + + let key = pbkdf1( + Pkcs12HashAlgorithm::Sha1, + password, + salt, + kdf_iterations, + Pbkdf1Usage::Key, + key_size, + ); + let iv = pbkdf1( + Pkcs12HashAlgorithm::Sha1, + password, + salt, + kdf_iterations, + Pbkdf1Usage::Iv, + iv_size, + ); + + (key, iv) +} + +fn encrypt_pbes1( + scheme: Pbes1Cipher, + password: &[u8], + salt: &[u8], + kdf_iterations: usize, + data: &[u8], +) -> Result, Pkcs12Error> { + use cbc::Encryptor; + use cbc::cipher::block_padding::Pkcs7; + use cbc::cipher::{BlockModeEncrypt, KeyIvInit}; + + let (dk, iv) = generate_pbes1_key_and_iv(scheme, password, salt, kdf_iterations); + + match scheme { + Pbes1Cipher::ShaAnd40BitRc2Cbc => { + use rc2::Rc2; + type Rc2Cbc = Encryptor; + + let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 { + context: "RC2 encryption initialization failed".to_string(), + })?; + Ok(rc2.encrypt_padded_vec::(data)) + } + Pbes1Cipher::ShaAnd3Key3DesCbc => { + use des::TdesEde3; + type TDesCbc = Encryptor; + + let tdes = TDesCbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 { + context: "3DES encryptor initialization failed".to_string(), + })?; + Ok(tdes.encrypt_padded_vec::(data)) + } + } +} + +fn decrypt_pbes1( + scheme: Pbes1Cipher, + password: &[u8], + salt: &[u8], + kdf_iterations: usize, + data: &[u8], +) -> Result, Pkcs12Error> { + use cbc::Decryptor; + use cbc::cipher::block_padding::Pkcs7; + use cbc::cipher::{BlockModeDecrypt, KeyIvInit}; + + let (dk, iv) = generate_pbes1_key_and_iv(scheme, password, salt, kdf_iterations); + + match scheme { + Pbes1Cipher::ShaAnd40BitRc2Cbc => { + use rc2::Rc2; + type Rc2Cbc = Decryptor; + + let rc2 = Rc2Cbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 { + context: "RC2 decryptor initialization failed".to_string(), + })?; + rc2.decrypt_padded_vec::(data).map_err(|_| Pkcs12Error::Pbes1 { + context: "RC2 decryption with padding failed".to_string(), + }) + } + Pbes1Cipher::ShaAnd3Key3DesCbc => { + use des::TdesEde3; + type TDesCbc = Decryptor; + + let tdes = TDesCbc::new_from_slices(&dk, &iv).map_err(|_| Pkcs12Error::Pbes1 { + context: "3DES decryptor initialization failed".to_string(), + })?; + tdes.decrypt_padded_vec::(data).map_err(|_| Pkcs12Error::Pbes1 { + context: "3DES decryption with padding failed".to_string(), + }) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[test] + fn pbes1_3des_roundtrip() { + let password = b"\0a\0b\0c\0\0"; + let salt = (0..8).collect::>(); + let iterations = 2000; + let data = (0..123).collect::>(); + let encrypted = encrypt_pbes1(Pbes1Cipher::ShaAnd3Key3DesCbc, password, &salt, iterations, &data).unwrap(); + let decrypted = decrypt_pbes1(Pbes1Cipher::ShaAnd3Key3DesCbc, password, &salt, iterations, &encrypted).unwrap(); + assert_eq!(decrypted, data); + } + + #[test] + fn pbes1_rc2_roundtrip() { + let password = b"\0b\0c\0a\0\0"; + let salt = (0..8).collect::>(); + let iterations = 2048; + let data = (0..124).collect::>(); + let encrypted = encrypt_pbes1(Pbes1Cipher::ShaAnd40BitRc2Cbc, password, &salt, iterations, &data).unwrap(); + let decrypted = decrypt_pbes1(Pbes1Cipher::ShaAnd40BitRc2Cbc, password, &salt, iterations, &encrypted).unwrap(); + assert_eq!(decrypted, data); + } + + #[rstest] + #[case(Pbes2AesCbcEncryptionAsn1::Aes128)] + #[case(Pbes2AesCbcEncryptionAsn1::Aes192)] + #[case(Pbes2AesCbcEncryptionAsn1::Aes256)] + fn pbes2_aes256_roundtrip(#[case] aes: Pbes2AesCbcEncryptionAsn1) { + let password = b"test"; + let data = (0..123).collect::>(); + let params = Pbes2ParamsAsn1 { + key_derivation_func: Pbes2KeyDerivationFuncAsn1::Pbkdf2(Pbkdf2ParamsAsn1 { + salt: Pbkdf2SaltSourceAsn1::Specified(OctetStringAsn1((0..8).collect())), + iteration_count: 2000, + key_length: None, + prf: Some(Pbkdf2PrfAsn1::HmacWithSha256), + }), + encryption_scheme: Pbes2EncryptionSchemeAsn1::AesCbc { + kind: aes, + iv: OctetStringAsn1((0..16).collect()), + }, + }; + let encrypted = encrypt_pbes2(¶ms, password, &data).unwrap(); + let decrypted = decrypt_pbes2(¶ms, password, &encrypted).unwrap(); + assert_eq!(decrypted, data); + } +} diff --git a/rust/vendor/picky/src/pkcs12/mac.rs b/rust/vendor/picky/src/pkcs12/mac.rs new file mode 100644 index 0000000..d612417 --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/mac.rs @@ -0,0 +1,196 @@ +use crate::pkcs12::{Pbkdf1Usage, Pkcs12CryptoContext, Pkcs12Error, Pkcs12HashAlgorithm, pbkdf1}; +use hmac::KeyInit; +use picky_asn1::wrapper::OctetStringAsn1; +use picky_asn1_x509::pkcs12::{MacData as MacDataAsn1, Pkcs12DigestInfo as Pkcs12DigestInfoAsn1}; +use thiserror::Error; + +const DEFAULT_MAC_KDF_ITERATIONS: u32 = 1; +const DEFAULT_SALT_SIZE: usize = 20; + +#[derive(Debug, Clone, Error)] +pub enum Pkcs12MacError { + #[error("Invalid hmac input size")] + InvalidHmacInputSize, + #[error("MAC validation failed (wrong password or corrupted data)")] + MacValidation, +} + +/// HMAC algorithm parameters (used for PFX integrity data) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pkcs12MacAlgorithmHmac { + hash_algorithm: Pkcs12HashAlgorithm, + iterations: Option, +} + +impl Pkcs12MacAlgorithmHmac { + pub fn new(hash_algorithm: Pkcs12HashAlgorithm) -> Self { + Self { + hash_algorithm, + iterations: None, + } + } + + pub fn with_iterations(mut self, iterations: u32) -> Self { + self.iterations = Some(iterations); + self + } + + pub fn hash_algorithm(&self) -> Pkcs12HashAlgorithm { + self.hash_algorithm + } + + pub fn iterations(&self) -> Option { + self.iterations + } +} + +/// Parsed MAC algorithm parameters +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Pkcs12MacAlgorithm { + Hmac(Pkcs12MacAlgorithmHmac), + Unknown, +} + +/// Parsed PFX MAC data +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Pkcs12MacData { + algorithm: Pkcs12MacAlgorithm, + inner: MacDataAsn1, +} + +impl Pkcs12MacData { + pub(crate) fn from_asn1(inner: MacDataAsn1, skip_unknown_hash_algorithm: bool) -> Result { + let hash_algorithm = match Pkcs12HashAlgorithm::from_asn1_digest_algorithm(&inner.mac.digest_algorithm) { + Ok(algorithm) => algorithm, + Err(_) if skip_unknown_hash_algorithm => { + return Ok(Self { + algorithm: Pkcs12MacAlgorithm::Unknown, + inner, + }); + } + Err(e) => { + return Err(e); + } + }; + + let kdf_iterations = inner.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS); + + Ok(Self { + algorithm: Pkcs12MacAlgorithm::Hmac(Pkcs12MacAlgorithmHmac { + hash_algorithm, + iterations: Some(kdf_iterations), + }), + inner, + }) + } + + pub(crate) fn new_hmac( + algorithm: Pkcs12MacAlgorithmHmac, + context: &mut Pkcs12CryptoContext, + data: &[u8], + ) -> Result { + let hash_algorithm = algorithm.hash_algorithm; + let kdf_iterations = algorithm.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS); + let salt = context.generate_bytes(DEFAULT_SALT_SIZE); + + // PKCS12 MAC uses BMPString as password representation + let password = context.password_bytes_pbes1()?; + + let digest = Self::calculate_digest( + hash_algorithm, + kdf_iterations, + password.as_slice(), + salt.as_slice(), + data, + )?; + + Ok(Self { + algorithm: Pkcs12MacAlgorithm::Hmac(algorithm), + inner: MacDataAsn1 { + mac: Pkcs12DigestInfoAsn1 { + digest_algorithm: hash_algorithm.into(), + digest: OctetStringAsn1(digest), + }, + salt: OctetStringAsn1(salt), + iterations: Some(kdf_iterations), + }, + }) + } + + pub(crate) fn validate(&self, context: &Pkcs12CryptoContext, data: &[u8]) -> Result<(), Pkcs12Error> { + let hash_algorithm = Pkcs12HashAlgorithm::from_asn1_digest_algorithm(&self.inner.mac.digest_algorithm)?; + let kdf_iterations = self.inner.iterations.unwrap_or(DEFAULT_MAC_KDF_ITERATIONS); + let salt = self.inner.salt.0.as_slice(); + + // PKCS12 MAC uses BMPString as password representation + let password = context.password_bytes_pbes1()?; + + let digest = Self::calculate_digest(hash_algorithm, kdf_iterations, password.as_slice(), salt, data)?; + + if digest == self.inner.mac.digest.0.as_slice() { + Ok(()) + } else { + Err(Pkcs12MacError::MacValidation.into()) + } + } + + fn calculate_digest( + hash_algorithm: Pkcs12HashAlgorithm, + kdf_iterations: u32, + password: &[u8], + salt: &[u8], + data: &[u8], + ) -> Result, Pkcs12Error> { + let key = pbkdf1( + hash_algorithm, + password, + salt, + kdf_iterations as usize, + Pbkdf1Usage::Mac, + hash_algorithm.digest_size(), + ); + + use hmac::Mac; + + let map_hmac_err = |_| Pkcs12MacError::InvalidHmacInputSize; + + let mac = match hash_algorithm { + Pkcs12HashAlgorithm::Sha1 => { + let mut hmac = hmac::Hmac::::new_from_slice(&key).map_err(map_hmac_err)?; + hmac.update(data); + hmac.finalize().into_bytes().to_vec() + } + Pkcs12HashAlgorithm::Sha224 => { + let mut hmac = hmac::Hmac::::new_from_slice(&key).map_err(map_hmac_err)?; + hmac.update(data); + hmac.finalize().into_bytes().to_vec() + } + Pkcs12HashAlgorithm::Sha256 => { + let mut hmac = hmac::Hmac::::new_from_slice(&key).map_err(map_hmac_err)?; + hmac.update(data); + hmac.finalize().into_bytes().to_vec() + } + Pkcs12HashAlgorithm::Sha384 => { + let mut hmac = hmac::Hmac::::new_from_slice(&key).map_err(map_hmac_err)?; + hmac.update(data); + hmac.finalize().into_bytes().to_vec() + } + Pkcs12HashAlgorithm::Sha512 => { + let mut hmac = hmac::Hmac::::new_from_slice(&key).map_err(map_hmac_err)?; + hmac.update(data); + hmac.finalize().into_bytes().to_vec() + } + }; + + Ok(mac) + } + + pub fn inner(&self) -> &MacDataAsn1 { + &self.inner + } + + /// Parsed MAC algorithm parameters + pub fn algorithm(&self) -> &Pkcs12MacAlgorithm { + &self.algorithm + } +} diff --git a/rust/vendor/picky/src/pkcs12/mod.rs b/rust/vendor/picky/src/pkcs12/mod.rs new file mode 100644 index 0000000..02b4966 --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/mod.rs @@ -0,0 +1,614 @@ +//! This module provides types for parsing and building PKCS#12 files (PFX). +//! +//! # PFX file structure +//! +//! PFX files are constructed from `safe contents` on the highest level, which are containers for +//! various types of `safe bags`. They could be encrypted or not and contain one or more `safe bags`. +//! +//! `Safe bags` are polymorphic containers for various types of data, such as private keys, certificates, +//! custom data (`secrets`), CRLs, etc. They could also be nested, which means that one `safe bag` could +//! contain multiple `safe bags` inside, making it possible to represent complex tree-like structures. +//! +//! Each `safe bag` could also contain `attributes`, which are key-value pairs of various types of data. +//! High-level API defines only `friendly name` and `local key id` attributes, but it is possible to +//! parse/build any custom attributes as well. +//! +//! There are also special `safe bags` called `shrouded key bags`, which are basically encrypted +//! PKCS8 private keys. Encryption on `safe bag` level is performed to allow PFX files that have +//! unencrypted certificates and encrypted private keys, to be preprocessed by the software without +//! knowing PFX password (e.g. check if PFX contains private key for the certificate or not). Usually +//! relations between contained certificates and private keys are marked with `local key id` attribute +//! which is set to the same value for both certificate and private key. +//! +//! # Parsing +//! +//! Almost any kind of PKCS#12 object specified by [RFC](https://datatracker.ietf.org/doc/html/rfc7292) +//! could be parsed or constructed. However, only the most used PFX types are wrapped in high level +//! convenient API such as private keys, certificates, nested pfx nodes, etc. If parsed +//! data was in fact some rarely used PFX type, it is still possible to access it via low level API +//! via `.inner()` method usually returns ASN.1 DER encoded data (`picky-asn1-x509` crate types). +//! +//! Parsing process could be controlled even more with [`Pkcs12ParsingParams`], which allows to skip +//! some parsing errors (e.g. Skip failed mapping of private key ASN.1 structure to high-level picky +//! wrapper) +//! +//! Parsing of PFX file is simple as calling [`Pfx::from_der`] method with required params +//! (crypto context which holds PFX password, parsing params and data itself), after which its safe +//! bags could be inspected and data extracted from PFX. +//! +//! # Building +//! +//! Building process of PFX files is down-to-top, in contrast to parsing. It starts with +//! building of required safe bags/attributes structures, wrapping them inside safe contents +//! (encrypted or unencrypted) and then wrapping them in PFX structure. This allows to keep API +//! flexible while representing Pfx file both after parsing and building as the same type ([`Pfx]) +//! +//! # Encryption +//! +//! - It is advised to always use Pbes2 AES-based encryption for PFX files. The only use case for new +//! PFX files with Pbes1 encryption is to support legacy software that does not support Pbes2. +//! - Usually PFX files without passwords are actually encrypted with empty-string passwords if +//! generated by modern software by default. (e.g. certmgr or OpenSSL). However, it is possible to +//! create PFX files without any encryption and MAC in picky if PFX will be used just as a plain +//! container for certificates and private keys and stored somewhere securely (e.g. wrapped in +//! another encryption layer). + +mod attribute; +mod encryption; +mod mac; +mod pbkdf1; +mod safe_bag; +mod safe_contents; + +use picky_asn1::restricted_string::CharSetError; +use picky_asn1_der::Asn1RawDer; +use picky_asn1_x509::oid::ObjectIdentifier; +use picky_asn1_x509::pkcs12::{ + AuthenticatedSafeContentInfo as AuthenticatedSafeContentInfoAsn1, + ParsedAuthenticatedSafeDataRepr as ParsedAuthenticatedSafeDataReprAsn1, Pbkdf2Prf as Pbkdf2PrfAsn1, Pfx as PfxAsn1, + Pkcs12DigestAlgorithm as Pkcs12DigestAlgorithmAsn1, Pkcs12DigestAlgorithm, + RawAuthenticatedSafeContentInfo as RawAuthenticatedSafeContentInfoAsn1, RawPfx as RawPfxAsn1, + SafeContentsContentInfo as SafeContentsContentInfoAsn1, +}; +use std::fmt::Display; +use thiserror::Error; + +pub(crate) use pbkdf1::{Pbkdf1Usage, pbkdf1}; + +pub use attribute::{CustomPkcs12Attribute, Pkcs12Attribute, Pkcs12AttributeKind}; +pub use encryption::{ + Pbes1Cipher, Pbes1Encryption, Pbes2Cipher, Pbes2Encryption, Pkcs12CryptoContext, Pkcs12Encryption, + Pkcs12EncryptionKind, +}; +pub use mac::{Pkcs12MacAlgorithm, Pkcs12MacAlgorithmHmac, Pkcs12MacData, Pkcs12MacError}; +pub use safe_bag::{SafeBag, SafeBagKind, SecretSafeBag}; +pub use safe_contents::{SafeContents, SafeContentsKind}; + +const PFX_VERSION: u8 = 3; + +/// Parsed PFX (PKCS12 archive). See module docs for more info on PFX file structure and API usage. +#[derive(Debug)] +pub struct Pfx { + safe_contents: Vec, + mac_data: Option, + /// Pre-serialized auth safe data. Just an optimization to avoid re-serializing the auth safe + /// data if it was already serialized for the MAC calculation. + auth_safe_data: Option>, +} + +impl Pfx { + /// Create new PFX file with HMAC MAC algorithm. Usually this is the default in modern software + pub fn new_with_hmac( + safe_contents: Vec, + mac: Pkcs12MacAlgorithmHmac, + crypto_context: &mut Pkcs12CryptoContext, + ) -> Result { + let safe_contents_asn1 = safe_contents.iter().map(|sc| sc.inner().clone()).collect::>(); + + let serialized_auth_safe = picky_asn1_der::to_vec(&safe_contents_asn1)?; + let mac_data = Pkcs12MacData::new_hmac(mac, crypto_context, &serialized_auth_safe)?; + + Ok(Self { + safe_contents, + mac_data: Some(mac_data), + auth_safe_data: Some(serialized_auth_safe), + }) + } + + /// Create new PFX file without MAC algorithm if needed for some reason (e.g. MAC is performed + /// on the higher level) + pub fn new_without_mac(safe_contents: Vec) -> Self { + Self { + safe_contents, + mac_data: None, + auth_safe_data: None, + } + } + + /// Serialize PFX file to DER bytes + pub fn to_der(&self) -> Result, Pkcs12Error> { + match &self.auth_safe_data { + Some(auth_safe) => { + let pfx_asn1 = RawPfxAsn1 { + version: PFX_VERSION, + auth_safe: RawAuthenticatedSafeContentInfoAsn1::Data(Asn1RawDer(auth_safe.clone())), + mac_data: self.mac_data.as_ref().map(|mac_data| mac_data.inner().clone()), + }; + + picky_asn1_der::to_vec(&pfx_asn1).map_err(Into::into) + } + None => { + let pfx_asn1 = PfxAsn1 { + version: PFX_VERSION, + auth_safe: AuthenticatedSafeContentInfoAsn1::::Data( + self.safe_contents + .iter() + .map(|sc| sc.inner().clone()) + .collect::>(), + ), + mac_data: self.mac_data.as_ref().map(|mac_data| mac_data.inner().clone()), + }; + + picky_asn1_der::to_vec(&pfx_asn1).map_err(Into::into) + } + } + } + + /// Parses a PKCS12 archive (PFX) from its DER representation. + pub fn from_der( + data: &[u8], + crypto_context: &Pkcs12CryptoContext, + parsing_params: &Pkcs12ParsingParams, + ) -> Result { + let (auth_safe, mac_data) = if parsing_params.skip_mac_validation { + let pfx_asn1: PfxAsn1 = picky_asn1_der::from_bytes(data)?; + if pfx_asn1.version != PFX_VERSION { + return Err(Pkcs12Error::InvalidVersion(pfx_asn1.version)); + } + let mac_data = pfx_asn1 + .mac_data + .map(|asn1| Pkcs12MacData::from_asn1(asn1, true)) + .transpose()?; + + let auth_safe = match pfx_asn1.auth_safe { + AuthenticatedSafeContentInfoAsn1::Data(data) => data, + AuthenticatedSafeContentInfoAsn1::Unknown { content_type, .. } => { + return Err(Pkcs12Error::InvalidAuthenticatedSafeContentType(content_type.into())); + } + }; + + (auth_safe, mac_data) + } else { + let pfx_asn1: RawPfxAsn1 = picky_asn1_der::from_bytes(data)?; + if pfx_asn1.version != PFX_VERSION { + return Err(Pkcs12Error::InvalidVersion(pfx_asn1.version)); + } + + let mac_data = pfx_asn1 + .mac_data + .map(|asn1| Pkcs12MacData::from_asn1(asn1, false)) + .transpose()?; + + let auth_safe = match pfx_asn1.auth_safe { + AuthenticatedSafeContentInfoAsn1::Data(data) => { + if let Some(mac_data) = &mac_data { + mac_data.validate(crypto_context, data.0.as_slice())?; + } + + let parsed: Vec = picky_asn1_der::from_bytes(data.0.as_slice())?; + parsed + } + AuthenticatedSafeContentInfoAsn1::Unknown { content_type, .. } => { + return Err(Pkcs12Error::InvalidAuthenticatedSafeContentType(content_type.into())); + } + }; + + (auth_safe, mac_data) + }; + + auth_safe + .into_iter() + .map(|safe_contents| SafeContents::from_asn1(safe_contents, crypto_context, parsing_params)) + .collect::, _>>() + .map(|safe_contents| Self { + safe_contents, + mac_data, + auth_safe_data: None, + }) + } + + /// Inspect parsed PFX data + pub fn safe_contents(&self) -> &[SafeContents] { + &self.safe_contents + } + + /// Inspect parsed MAC data + pub fn mac_data(&self) -> Option<&Pkcs12MacData> { + self.mac_data.as_ref() + } +} + +/// Parameters which control some aspects of PFX file parsing process +#[derive(Debug, Clone, Default)] +pub struct Pkcs12ParsingParams { + /// Continue parsing if conversion to high level picky data structure fails (e.g. due to + /// unsupported private key or certificate kind) + pub skip_soft_parsing_errors: bool, + + /// Continue parsing if decryption fails and keep data in encrypted form + pub skip_decryption_errors: bool, + + /// Continue parsing if MAC validation fails. + /// + /// This is useful for parsing available unencrypted data from + /// password-protected PFX files. Also could be useful if PFX integrity has been intentionally + /// violated for testing purposes. + pub skip_mac_validation: bool, +} + +/// Hashing algorithm used for MAC or KDF in PFX file +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Pkcs12HashAlgorithm { + Sha1, + Sha224, + Sha256, + Sha384, + Sha512, +} + +impl Pkcs12HashAlgorithm { + pub(crate) fn pbkdf1_u_bits(self) -> usize { + match self { + Self::Sha1 => 160, + Self::Sha224 => 224, + Self::Sha256 => 256, + Self::Sha384 => 384, + Self::Sha512 => 512, + } + } + + pub(crate) fn pbkdf1_v_bits(self) -> usize { + match self { + Self::Sha1 => 512, + Self::Sha224 => 512, + Self::Sha256 => 512, + Self::Sha384 => 1024, + Self::Sha512 => 1024, + } + } + + pub(crate) fn digest_size(self) -> usize { + match self { + Self::Sha1 => 20, + Self::Sha224 => 28, + Self::Sha256 => 32, + Self::Sha384 => 48, + Self::Sha512 => 64, + } + } + + pub(crate) fn from_asn1_pbkdf2_prf(value: &Pbkdf2PrfAsn1) -> Result { + let algorithm = match value { + Pbkdf2PrfAsn1::HmacWithSha1 => Pkcs12HashAlgorithm::Sha1, + Pbkdf2PrfAsn1::HmacWithSha224 => Pkcs12HashAlgorithm::Sha224, + Pbkdf2PrfAsn1::HmacWithSha256 => Pkcs12HashAlgorithm::Sha256, + Pbkdf2PrfAsn1::HmacWithSha384 => Pkcs12HashAlgorithm::Sha384, + Pbkdf2PrfAsn1::HmacWithSha512 => Pkcs12HashAlgorithm::Sha512, + Pbkdf2PrfAsn1::Unknown(raw) => { + let oid = raw.algorithm().clone(); + return Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: UnsupportedPkcs12Algorithm::Oid(oid), + context: "Crypto operation (pbkdf2 prf algorithm)".to_string(), + }); + } + }; + + Ok(algorithm) + } + + pub(crate) fn from_asn1_digest_algorithm(value: &Pkcs12DigestAlgorithmAsn1) -> Result { + let algorithm = match value { + Pkcs12DigestAlgorithm::Sha1 => Pkcs12HashAlgorithm::Sha1, + Pkcs12DigestAlgorithm::Sha224 => Pkcs12HashAlgorithm::Sha224, + Pkcs12DigestAlgorithm::Sha256 => Pkcs12HashAlgorithm::Sha256, + Pkcs12DigestAlgorithm::Sha384 => Pkcs12HashAlgorithm::Sha384, + Pkcs12DigestAlgorithm::Sha512 => Pkcs12HashAlgorithm::Sha512, + Pkcs12DigestAlgorithm::Unknown(raw) => { + let oid = raw.algorithm().clone(); + return Err(Pkcs12Error::NotSupportedAlgorithm { + algorithm: UnsupportedPkcs12Algorithm::Oid(oid), + context: "MAC calculation (pbkdf1 prf algorithm)".to_string(), + }); + } + }; + + Ok(algorithm) + } +} + +impl From for Pkcs12DigestAlgorithm { + fn from(value: Pkcs12HashAlgorithm) -> Self { + match value { + Pkcs12HashAlgorithm::Sha1 => Self::Sha1, + Pkcs12HashAlgorithm::Sha224 => Self::Sha224, + Pkcs12HashAlgorithm::Sha256 => Self::Sha256, + Pkcs12HashAlgorithm::Sha384 => Self::Sha384, + Pkcs12HashAlgorithm::Sha512 => Self::Sha512, + } + } +} + +#[derive(Debug, Error)] +pub enum Pkcs12Error { + #[error("Not supported algorithm `{algorithm}` in context of {context}")] + NotSupportedAlgorithm { + algorithm: UnsupportedPkcs12Algorithm, + context: String, + }, + #[error("Failed to perform PBES1 crypto operation: {context}")] + Pbes1 { context: String }, + #[error("Failed to perform PBES2 crypto operation: {context}")] + Pbes2 { context: String }, + #[error(transparent)] + CharSet(#[from] CharSetError), + #[error(transparent)] + Mac(#[from] mac::Pkcs12MacError), + #[error("Invalid ASN.1 DER encoding")] + Asn1Der(#[from] picky_asn1_der::Asn1DerError), + #[error(transparent)] + Key(#[from] crate::key::KeyError), + #[error(transparent)] + Certificate(#[from] crate::x509::certificate::CertError), + #[error(transparent)] + RandError(#[from] rand::rngs::SysError), + #[error("Not supported or invalid PFX version: {0}")] + InvalidVersion(u8), + #[error("Invalid PFX AuthenticatedSafe content type: {0}")] + InvalidAuthenticatedSafeContentType(UnsupportedPkcs12Algorithm), + #[error("Unexpected attribute values count. Expected: `{expected}`, got: `{actual}`")] + UnexpectedAttributeValuesCount { expected: usize, actual: usize }, +} + +#[derive(Debug, Clone)] +pub enum UnsupportedPkcs12Algorithm { + Named(&'static str), + Oid(ObjectIdentifier), +} + +impl From<&'static str> for UnsupportedPkcs12Algorithm { + fn from(name: &'static str) -> Self { + Self::Named(name) + } +} + +impl From for UnsupportedPkcs12Algorithm { + fn from(oid: ObjectIdentifier) -> Self { + Self::Oid(oid) + } +} + +impl Display for UnsupportedPkcs12Algorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnsupportedPkcs12Algorithm::Named(name) => f.write_str(name), + UnsupportedPkcs12Algorithm::Oid(oid) => { + write!(f, "OID({})", format_oid(oid)) + } + } + } +} + +impl Pkcs12Error { + pub fn unsupported_algorithm(algorithm: impl Into, context: impl Into) -> Self { + Self::NotSupportedAlgorithm { + algorithm: algorithm.into(), + context: context.into(), + } + } +} + +fn format_oid(oid: &ObjectIdentifier) -> String { + let oid_str: String = oid.clone().into(); + format!("OID({oid_str})") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::key::PrivateKey; + use crate::pem::Pem; + use rstest::rstest; + + #[test] + fn pfx_certmgr_aes256() { + let encoded = picky_test_data::CERTMGR_AES256; + let crypto_context = Pkcs12CryptoContext::new_with_password("test").unwrap(); + let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap(); + } + + #[test] + fn pfx_certmgr_3des() { + let encoded = picky_test_data::CERTMGR_3DES; + let crypto_context = Pkcs12CryptoContext::new_with_password("test").unwrap(); + let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap(); + } + + #[test] + fn pfx_certmgr_rc2() { + let encoded = picky_test_data::LEAF_PASSWORD_IS_ABC; + let crypto_context = Pkcs12CryptoContext::new_with_password("abc").unwrap(); + let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap(); + } + + #[test] + fn pfx_certmgr_rc2_empty_pass() { + let encoded = picky_test_data::LEAF_EMPTY_PASSWORD; + let crypto_context = Pkcs12CryptoContext::new_without_password().unwrap(); + let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap(); + } + + #[test] + fn pfx_openssl_aes_empty_pass() { + let encoded = picky_test_data::OPENSSL_NOCRYPT; + let crypto_context = Pkcs12CryptoContext::new_without_password().unwrap(); + let _decoded = Pfx::from_der(encoded, &crypto_context, &Pkcs12ParsingParams::default()).unwrap(); + } + + fn stable_rand() -> impl rand_core::CryptoRng { + use rand_core::SeedableRng as _; + rand_chacha::ChaChaRng::seed_from_u64(42) + } + + fn build_cert_bags() -> [SafeBag; 3] { + let leaf = crate::x509::Cert::from_der(picky_test_data::ASSERT_LEAF).unwrap(); + let intermediate = crate::x509::Cert::from_der(picky_test_data::ASSERT_INTERMEDIATE).unwrap(); + let root = crate::x509::Cert::from_der(picky_test_data::ASSERT_ROOT).unwrap(); + + let leaf_cert_bag = SafeBag::new_certificate(leaf, build_leaf_attributes()).unwrap(); + + let intermediate_cert_bag = SafeBag::new_certificate( + intermediate, + vec![Pkcs12Attribute::new_friendly_name( + "PICKY_INTERMEDIATE".parse().unwrap(), + )], + ) + .unwrap(); + + let root_cert_bag = SafeBag::new_certificate( + root, + vec![Pkcs12Attribute::new_friendly_name("PICKY_ROOT".parse().unwrap())], + ) + .unwrap(); + + [leaf_cert_bag, intermediate_cert_bag, root_cert_bag] + } + + fn build_leaf_attributes() -> Vec { + vec![ + Pkcs12Attribute::new_local_key_id([0x01, 0x00, 0x00, 0x00]), + Pkcs12Attribute::new_friendly_name("PICKY_LEAF".parse().unwrap()), + ] + } + + fn make_crypto_context(password: Option<&str>) -> Pkcs12CryptoContext { + if let Some(password) = password { + Pkcs12CryptoContext::new_with_password(password) + .unwrap() + .with_rng(stable_rand()) + } else { + Pkcs12CryptoContext::new_without_password() + .unwrap() + .with_rng(stable_rand()) + } + } + + fn validate_pfx(der_data: &[u8], password: Option<&str>) { + let crypto_context = make_crypto_context(password); + + // Check that we can decode PFX encoded by picky itself + let _decoded = Pfx::from_der(der_data, &crypto_context, &Pkcs12ParsingParams::default()).unwrap(); + + #[cfg(windows)] + { + let temp_path = tempfile::NamedTempFile::new().unwrap().into_temp_path(); + + std::fs::write(&temp_path, der_data).unwrap(); + + let certutil_args = vec![ + "-dump".to_string(), + "-p".to_string(), + password.unwrap_or("").to_string(), + temp_path.to_str().unwrap().to_string(), + ]; + + let certutil_output = std::process::Command::new("certutil") + .args(certutil_args) + .output() + .unwrap(); + + assert!( + certutil_output.status.success(), + "certutil failed: {}", + String::from_utf8_lossy(&certutil_output.stdout) + ); + } + } + + fn encryption_3des(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption { + Pkcs12Encryption::new_pbes1(Pbes1Encryption::new(Pbes1Cipher::ShaAnd3Key3DesCbc), crypto_context) + } + + fn encryption_rc2(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption { + Pkcs12Encryption::new_pbes1(Pbes1Encryption::new(Pbes1Cipher::ShaAnd40BitRc2Cbc), crypto_context) + } + + fn encryption_aes256(crypto_context: &mut Pkcs12CryptoContext) -> Pkcs12Encryption { + Pkcs12Encryption::new_pbes2( + Pbes2Encryption::new(Pbes2Cipher::Aes256Cbc, Pkcs12HashAlgorithm::Sha256), + crypto_context, + ) + } + + pub fn leaf_private_key_rsa() -> PrivateKey { + let pem = picky_test_data::RSA_2048_PK_3.parse::().unwrap(); + PrivateKey::from_pkcs8(pem.data()).unwrap() + } + + type EncryptionFn = fn(&mut Pkcs12CryptoContext) -> Pkcs12Encryption; + + #[rstest] + #[case(encryption_3des, Some("test"), Pkcs12HashAlgorithm::Sha1)] + #[case(encryption_rc2, Some("test"), Pkcs12HashAlgorithm::Sha1)] + #[case(encryption_aes256, Some("test"), Pkcs12HashAlgorithm::Sha256)] + #[case(encryption_3des, None, Pkcs12HashAlgorithm::Sha1)] + // RC2 uses same KDF as 3DES so we could skip case of RC2 without password + #[case(encryption_aes256, None, Pkcs12HashAlgorithm::Sha256)] + fn build_pfx_encrypted( + #[case] encryption_fn: EncryptionFn, + #[case] password: Option<&'static str>, + #[case] hmac_algorithm: Pkcs12HashAlgorithm, + ) { + let leaf_key = leaf_private_key_rsa(); + + let [leaf_cert_bag, intermediate_cert_bag, root_cert_bag] = build_cert_bags(); + + let mut crypto_context = make_crypto_context(password); + + let key_encryption = encryption_fn(&mut crypto_context); + + let leaf_key_bag = + SafeBag::new_encrypted_key(leaf_key, build_leaf_attributes(), key_encryption, &crypto_context).unwrap(); + + let cert_encryption = encryption_fn(&mut crypto_context); + + let cert_safe_contents = SafeContents::new_encrypted( + vec![leaf_cert_bag, intermediate_cert_bag, root_cert_bag], + cert_encryption, + &crypto_context, + ) + .unwrap(); + + let key_safe_contents = SafeContents::new(vec![leaf_key_bag]); + + let secret_safe_bag = SafeBag::new_secret( + SecretSafeBag::new(picky_asn1_x509::oids::content_info_type_data(), &42u8).unwrap(), + vec![Pkcs12Attribute::new_custom( + CustomPkcs12Attribute::new_single_value(picky_asn1_x509::oids::content_info_type_data(), &256u64) + .unwrap(), + )], + ); + + let secret_safe_contents = SafeContents::new(vec![secret_safe_bag]); + + let pfx = Pfx::new_with_hmac( + vec![cert_safe_contents, key_safe_contents, secret_safe_contents], + Pkcs12MacAlgorithmHmac::new(hmac_algorithm), + &mut crypto_context, + ) + .unwrap(); + + let der_data = pfx.to_der().unwrap(); + + validate_pfx(&der_data, password); + } +} diff --git a/rust/vendor/picky/src/pkcs12/pbkdf1.rs b/rust/vendor/picky/src/pkcs12/pbkdf1.rs new file mode 100644 index 0000000..2e78446 --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/pbkdf1.rs @@ -0,0 +1,114 @@ +use crate::pkcs12::Pkcs12HashAlgorithm; + +pub enum Pbkdf1Usage { + Key, + Iv, + Mac, +} + +impl Pbkdf1Usage { + fn to_id_byte(&self) -> u8 { + match self { + Pbkdf1Usage::Key => 1, + Pbkdf1Usage::Iv => 2, + Pbkdf1Usage::Mac => 3, + } + } +} + +/// PBKDF1 implementation for PKCS#12 as defined in [RFC](https://datatracker.ietf.org/doc/html/rfc7292#appendix-B.2) +pub fn pbkdf1( + hash: Pkcs12HashAlgorithm, + password: &[u8], + salt: &[u8], + kdf_iterations: usize, + usage: Pbkdf1Usage, + output_size: usize, +) -> Vec { + let u = hash.pbkdf1_u_bits() / 8; + let v = hash.pbkdf1_v_bits() / 8; + + let hash_round = match hash { + Pkcs12HashAlgorithm::Sha1 => pbkdf1_hash_round::, + Pkcs12HashAlgorithm::Sha224 => pbkdf1_hash_round::, + Pkcs12HashAlgorithm::Sha256 => pbkdf1_hash_round::, + Pkcs12HashAlgorithm::Sha384 => pbkdf1_hash_round::, + Pkcs12HashAlgorithm::Sha512 => pbkdf1_hash_round::, + }; + + // Construct "diversifier" string + let d = vec![usage.to_id_byte(); v]; + + let expanded_length = |len: usize| v * len.div_ceil(v); + + // Expand salt and password length to multiple of V + let expanded_salt = salt.iter().cycle().take(expanded_length(salt.len())); + let expanded_password = password.iter().cycle().take(expanded_length(password.len())); + + // I = S || P + let mut key_material: Vec = expanded_salt.chain(expanded_password).cloned().collect(); + + let c = output_size.div_ceil(u); + + let mut output: Vec = vec![]; + + // Temporary buffer for key blocks produced by SHA1 + let mut key_block = vec![]; + + let mut b = vec![]; + + for _ in 1..c { + hash_round(&d, &key_material, kdf_iterations, &mut key_block); + output.extend_from_slice(&key_block); + + // Create concatenated string B of length V + b.clear(); + b.extend(key_block.iter().cycle().take(v).copied()); + + // Pretty convoluted operation which is defined in RFC as follows: + // + // C. Treating I as a concatenation I_0, I_1, ..., I_(k-1) of v-bit + // blocks, where k=ceiling(s/v)+ceiling(p/v), modify I by + // setting I_j=(I_j+B+1) mod 2^v for each j. + // + // Implementation of this part has been borrowed from [p12 crate](https://github.com/hjiayz/p12) + let b_iter = b.iter().rev().cycle().take(key_material.len()); + let i_b_iter = key_material.iter_mut().rev().zip(b_iter); + let mut inc = 1u8; + for (i3, (ii, bi)) in i_b_iter.enumerate() { + if (i3 % v) == 0 { + inc = 1; + } + let (ii2, inc2) = ii.overflowing_add(*bi); + let (ii3, inc3) = ii2.overflowing_add(inc); + inc = (inc2 || inc3) as u8; + *ii = ii3; + } + } + + hash_round(&d, &key_material, kdf_iterations, &mut key_block); + output.extend_from_slice(&key_block); + + // Truncate to output_size + output.resize(output_size, 0); + output +} + +fn pbkdf1_hash_round( + d: &[u8], + i: &[u8], + iterations: usize, + output_buffer: &mut Vec, +) { + let mut hasher = H::new(); + output_buffer.clear(); + output_buffer.extend_from_slice(d); + output_buffer.extend_from_slice(i); + + for _ in 0..iterations { + digest::Digest::update(&mut hasher, &output_buffer); + let hash = hasher.finalize_reset(); + output_buffer.clear(); + output_buffer.extend_from_slice(&hash[..]); + } +} diff --git a/rust/vendor/picky/src/pkcs12/safe_bag.rs b/rust/vendor/picky/src/pkcs12/safe_bag.rs new file mode 100644 index 0000000..a04784e --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/safe_bag.rs @@ -0,0 +1,331 @@ +use crate::key::PrivateKey; +use crate::pkcs12::{Pkcs12Attribute, Pkcs12CryptoContext, Pkcs12Encryption, Pkcs12Error, Pkcs12ParsingParams}; +use crate::x509::Cert; +use picky_asn1::wrapper::OctetStringAsn1; +use picky_asn1_der::Asn1RawDer; +use picky_asn1_x509::oid::ObjectIdentifier; +use picky_asn1_x509::pkcs12::{ + CertificateBag as CertificateBagAsn1, EncryptedKeyBag as EncryptedKeyBagAsn1, + Pkcs12Attribute as Pkcs12AttributeAsn1, SafeBag as SafeBagAsn1, SafeBagKind as SafeBagKindAsn1, + SafeContents as SafeContentsAsn1, SecretBag as SecretBagAsn1, +}; +use serde::{Deserialize, Serialize}; + +/// PFX safe bag, see module docs for more information +#[derive(Debug, Clone)] +pub struct SafeBag { + kind: SafeBagKind, + attributes: Vec, + inner: SafeBagAsn1, +} + +impl SafeBag { + /// Create new safe bag holding a private key + pub fn new_key(key: PrivateKey, attributes: Vec) -> Result { + // Convert to `PrivateKeyInfo` structure in DER representation + let der_data = key.to_pkcs8()?; + + let inner = SafeBagAsn1 { + kind: SafeBagKindAsn1::Key(Asn1RawDer(der_data)), + attributes: attributes_to_asn1(&attributes), + }; + + Ok(Self { + kind: SafeBagKind::PrivateKey(key), + attributes, + inner, + }) + } + + /// Create new safe bag with encrypted key. Note that attributes are not encrypted. + pub fn new_encrypted_key( + key: PrivateKey, + attributes: Vec, + encryption: Pkcs12Encryption, + crypto_context: &Pkcs12CryptoContext, + ) -> Result { + let der_data = key.to_pkcs8()?; + let encrypted = encryption.encrypt(&der_data, crypto_context)?; + + let inner = SafeBagAsn1 { + kind: SafeBagKindAsn1::EncryptedKey(EncryptedKeyBagAsn1 { + algorithm: encryption.inner().clone(), + encrypted_data: OctetStringAsn1(encrypted), + }), + attributes: attributes_to_asn1(&attributes), + }; + + Ok(Self { + kind: SafeBagKind::EncryptedPrivateKey { encryption, key }, + attributes, + inner, + }) + } + + /// Create new safe bag with certificate + pub fn new_certificate(cert: Cert, attributes: Vec) -> Result { + let der_data = cert.to_der()?; + + let inner = SafeBagAsn1 { + kind: SafeBagKindAsn1::Certificate(CertificateBagAsn1::X509(OctetStringAsn1(der_data))), + attributes: attributes_to_asn1(&attributes), + }; + + Ok(Self { + kind: SafeBagKind::Certificate(cert), + attributes, + inner, + }) + } + + /// Creates new [`SecretSafeBag`] bag + pub fn new_secret(secret: SecretSafeBag, attributes: Vec) -> Self { + let inner = SafeBagAsn1 { + kind: SafeBagKindAsn1::Secret(SecretBagAsn1 { + type_id: secret.oid.clone(), + value: secret.data.clone(), + }), + attributes: attributes_to_asn1(&attributes), + }; + + Self { + kind: SafeBagKind::Secret(secret), + attributes, + inner, + } + } + + /// Creates safe bag with nested safe bag list. + pub fn new_nested(safe_bags: Vec, attributes: Vec) -> Self { + let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner.clone()).collect()); + + let inner = SafeBagAsn1 { + kind: SafeBagKindAsn1::SafeContents(safe_contents), + attributes: attributes_to_asn1(&attributes), + }; + + Self { + kind: SafeBagKind::Nested(safe_bags), + attributes, + inner, + } + } + + /// PKCS#12 allows for arbitrary SafeBags to be included in the PKCS#12 file as long as they + /// a unique OID. + pub fn new_custom(oid: ObjectIdentifier, value: Asn1RawDer, attributes: Vec) -> Self { + let inner = SafeBagAsn1 { + kind: SafeBagKindAsn1::Unknown { type_id: oid, value }, + attributes: attributes_to_asn1(&attributes), + }; + + Self { + kind: SafeBagKind::Unknown, + attributes, + inner, + } + } + + pub(crate) fn from_asn1( + safe_bag: SafeBagAsn1, + crypto_context: &Pkcs12CryptoContext, + parsing_params: &Pkcs12ParsingParams, + ) -> Result { + let attributes = safe_bag + .attributes + .clone() + .unwrap_or_default() + .into_iter() + .map(Pkcs12Attribute::from_asn1) + .collect::>(); + + let to_unparsed = |inner, attributes| Self { + kind: SafeBagKind::Unknown, + attributes, + inner, + }; + + let kind = match &safe_bag.kind { + SafeBagKindAsn1::Key(Asn1RawDer(der_data)) => { + let key = match PrivateKey::from_pkcs8(&der_data) { + Ok(key) => key, + Err(_) if parsing_params.skip_soft_parsing_errors => { + return Ok(to_unparsed(safe_bag, attributes)); + } + Err(e) => return Err(e.into()), + }; + + SafeBagKind::PrivateKey(key) + } + SafeBagKindAsn1::EncryptedKey(encrypted_key) => { + let encryption = match Pkcs12Encryption::from_asn1(encrypted_key.algorithm.clone()) { + Ok(encryption) => encryption, + Err(_) if parsing_params.skip_decryption_errors => { + return Ok(to_unparsed(safe_bag, attributes)); + } + Err(e) => return Err(e), + }; + + let der_data = match encryption.decrypt(&encrypted_key.encrypted_data.0, crypto_context) { + Ok(der_data) => der_data, + Err(_) if parsing_params.skip_decryption_errors => { + return Ok(to_unparsed(safe_bag, attributes)); + } + Err(e) => return Err(e), + }; + + let key = match PrivateKey::from_pkcs8(&der_data) { + Ok(key) => key, + Err(_) if parsing_params.skip_soft_parsing_errors => { + return Ok(to_unparsed(safe_bag, attributes)); + } + Err(e) => return Err(e.into()), + }; + + SafeBagKind::EncryptedPrivateKey { encryption, key } + } + SafeBagKindAsn1::Certificate(CertificateBagAsn1::X509(OctetStringAsn1(der_data))) => { + let cert = match Cert::from_der(&der_data) { + Ok(cert) => cert, + Err(_) if parsing_params.skip_soft_parsing_errors => { + return Ok(to_unparsed(safe_bag, attributes)); + } + Err(e) => return Err(e.into()), + }; + + SafeBagKind::Certificate(cert) + } + SafeBagKindAsn1::Secret(SecretBagAsn1 { type_id, value }) => { + let secret = SecretSafeBag { + oid: type_id.clone(), + data: value.clone(), + }; + + SafeBagKind::Secret(secret) + } + SafeBagKindAsn1::SafeContents(safe_contents) => { + let safe_bags = safe_contents + .0 + .iter() + .map(|sb| Self::from_asn1(sb.clone(), crypto_context, parsing_params)) + .collect::, _>>()?; + + SafeBagKind::Nested(safe_bags) + } + SafeBagKindAsn1::Crl(_) + | SafeBagKindAsn1::Certificate(CertificateBagAsn1::Unknown { .. }) + | SafeBagKindAsn1::Unknown { .. } => { + return Ok(to_unparsed(safe_bag, attributes)); + } + }; + + Ok(Self { + kind, + attributes, + inner: safe_bag, + }) + } + + /// Adds a PKCS12 attribute to this safe bag. + /// + /// Note that there is an additional performance cost: the inner DER representation must be updated. + pub fn add_attribute(&mut self, attribute: Pkcs12Attribute) { + self.attributes.push(attribute); + self.inner.attributes = attributes_to_asn1(&self.attributes); + } + + pub fn attributes(&self) -> &[Pkcs12Attribute] { + &self.attributes + } + + pub fn kind(&self) -> &SafeBagKind { + &self.kind + } + + pub fn into_kind(self) -> SafeBagKind { + self.kind + } + + pub fn inner(&self) -> &SafeBagAsn1 { + &self.inner + } + + pub fn into_inner(self) -> SafeBagAsn1 { + self.inner + } +} + +/// Parsed safe bag representation. +#[derive(Debug)] +pub enum SafeBagKind { + PrivateKey(PrivateKey), + EncryptedPrivateKey { + encryption: Pkcs12Encryption, + key: PrivateKey, + }, + Certificate(Cert), + Secret(SecretSafeBag), + Nested(Vec), + Unknown, +} + +impl Clone for SafeBagKind { + fn clone(&self) -> Self { + match self { + Self::PrivateKey(key) => Self::PrivateKey(key.clone()), + Self::EncryptedPrivateKey { encryption, key } => Self::EncryptedPrivateKey { + encryption: encryption.duplicate(), + key: key.clone(), + }, + Self::Certificate(cert) => Self::Certificate(cert.clone()), + Self::Secret(secret) => Self::Secret(secret.clone()), + Self::Nested(nested) => Self::Nested(nested.clone()), + Self::Unknown => Self::Unknown, + } + } +} + +/// Secret bag which could contain any user-defined data, as long as it could be DER-encoded. +/// It is advised to use types from `picky-asn1-der` crate. +#[derive(Debug, Clone)] +pub struct SecretSafeBag { + oid: ObjectIdentifier, + data: Asn1RawDer, +} + +impl SecretSafeBag { + pub fn new_raw(oid: ObjectIdentifier, data: Asn1RawDer) -> Self { + Self { oid, data } + } + + /// Create new secret bag from serializable data. + pub fn new(oid: ObjectIdentifier, value: &T) -> Result { + let encoded = picky_asn1_der::to_vec(value)?; + Ok(Self { + oid, + data: Asn1RawDer(encoded), + }) + } + + pub fn oid(&self) -> &ObjectIdentifier { + &self.oid + } + + pub fn raw_data(&self) -> &[u8] { + &self.data.0 + } + + /// Get secret bag data as deserialized type. + pub fn get_data<'a, T: Deserialize<'a>>(&'a self) -> Result { + let deserialized = picky_asn1_der::from_bytes(&self.data.0)?; + Ok(deserialized) + } +} + +fn attributes_to_asn1(attributes: &[Pkcs12Attribute]) -> Option> { + if attributes.is_empty() { + None + } else { + Some(attributes.iter().map(|a| a.inner().clone()).collect()) + } +} diff --git a/rust/vendor/picky/src/pkcs12/safe_contents.rs b/rust/vendor/picky/src/pkcs12/safe_contents.rs new file mode 100644 index 0000000..84c4d71 --- /dev/null +++ b/rust/vendor/picky/src/pkcs12/safe_contents.rs @@ -0,0 +1,170 @@ +use crate::pkcs12::{Pkcs12CryptoContext, Pkcs12Encryption, Pkcs12Error, Pkcs12ParsingParams, SafeBag}; +use picky_asn1::wrapper::OctetStringAsn1; +use picky_asn1_x509::pkcs12::{ + EncryptedSafeContents as EncryptedSafeContentsAsn1, SafeContents as SafeContentsAsn1, + SafeContentsContentInfo as SafeContentsContentInfoAsn1, +}; + +/// Top-level PFX container object, which holds list of safe bags and could be encrypted or not. +#[derive(Debug, Clone)] +pub struct SafeContents { + kind: SafeContentsKind, + inner: SafeContentsContentInfoAsn1, +} + +impl SafeContents { + pub(crate) fn from_asn1( + inner: SafeContentsContentInfoAsn1, + crypto_context: &Pkcs12CryptoContext, + parsing_params: &Pkcs12ParsingParams, + ) -> Result { + let to_unparsed = |inner| Self { + kind: SafeContentsKind::Unknown, + inner, + }; + + let kind = match &inner { + SafeContentsContentInfoAsn1::Data(data) => { + let safe_bags = data + .0 + .iter() + .map(|sb| SafeBag::from_asn1(sb.clone(), crypto_context, parsing_params)) + .collect::, _>>()?; + + Self { + kind: SafeContentsKind::SafeBags(safe_bags), + inner, + } + } + SafeContentsContentInfoAsn1::EncryptedData(encrypted) => { + let encryption = match Pkcs12Encryption::from_asn1(encrypted.algorithm.clone()) { + Ok(encryption) => encryption, + Err(_) if parsing_params.skip_decryption_errors => { + return Ok(to_unparsed(inner)); + } + Err(e) => { + return Err(e); + } + }; + + let encrypted_content = match encrypted.encrypted_content.as_ref() { + Some(content) => content.0.as_slice(), + None => { + return Ok(Self { + // No content to decrypt + kind: SafeContentsKind::EncryptedSafeBags { + encryption, + safe_bags: vec![], + }, + inner, + }); + } + }; + + let decrypted = match encryption.decrypt(encrypted_content, crypto_context) { + Ok(decrypted) => decrypted, + Err(_) if parsing_params.skip_decryption_errors => { + return Ok(to_unparsed(inner)); + } + Err(e) => { + return Err(e); + } + }; + + let safe_bags_asn1 = match picky_asn1_der::from_bytes::(&decrypted) { + Ok(safe_contents) => safe_contents.0, + Err(_) if parsing_params.skip_decryption_errors => { + return Ok(to_unparsed(inner)); + } + Err(e) => { + return Err(e.into()); + } + }; + + let safe_bags = safe_bags_asn1 + .into_iter() + .map(|sb| SafeBag::from_asn1(sb, crypto_context, parsing_params)) + .collect::, _>>()?; + + Self { + kind: SafeContentsKind::EncryptedSafeBags { encryption, safe_bags }, + inner, + } + } + SafeContentsContentInfoAsn1::Unknown { .. } => return Ok(to_unparsed(inner)), + }; + + Ok(kind) + } + + pub fn new(safe_bags: Vec) -> Self { + let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner().clone()).collect()); + Self { + kind: SafeContentsKind::SafeBags(safe_bags), + inner: SafeContentsContentInfoAsn1::Data(safe_contents), + } + } + + pub fn new_encrypted( + safe_bags: Vec, + encryption: Pkcs12Encryption, + crypto_context: &Pkcs12CryptoContext, + ) -> Result { + let safe_contents = SafeContentsAsn1(safe_bags.iter().map(|sb| sb.inner().clone()).collect()); + let der_data = picky_asn1_der::to_vec(&safe_contents)?; + let encrypted = encryption.encrypt(&der_data, crypto_context)?; + + let inner = SafeContentsContentInfoAsn1::EncryptedData(EncryptedSafeContentsAsn1 { + algorithm: encryption.inner().clone(), + encrypted_content: Some(OctetStringAsn1(encrypted)), + }); + + Ok(Self { + kind: SafeContentsKind::EncryptedSafeBags { encryption, safe_bags }, + inner, + }) + } + + pub fn kind(&self) -> &SafeContentsKind { + &self.kind + } + + pub fn into_kind(self) -> SafeContentsKind { + self.kind + } + + pub fn inner(&self) -> &SafeContentsContentInfoAsn1 { + &self.inner + } + + pub fn into_inner(self) -> SafeContentsContentInfoAsn1 { + self.inner + } +} + +// Clippy triggers lint because of relatively big `Pkcs12Encryption` (~200 bytes) in comparison with +// `SafeContentsKind::Unknown` (0 bytes), but just to keep it consistent with other enums, we do allow +// such difference in size. +#[allow(clippy::large_enum_variant)] +#[derive(Debug)] +pub enum SafeContentsKind { + SafeBags(Vec), + EncryptedSafeBags { + encryption: Pkcs12Encryption, + safe_bags: Vec, + }, + Unknown, +} + +impl Clone for SafeContentsKind { + fn clone(&self) -> Self { + match self { + Self::SafeBags(bags) => Self::SafeBags(bags.clone()), + Self::EncryptedSafeBags { encryption, safe_bags } => Self::EncryptedSafeBags { + encryption: encryption.duplicate(), + safe_bags: safe_bags.clone(), + }, + Self::Unknown => Self::Unknown, + } + } +} diff --git a/rust/vendor/picky/src/putty/error.rs b/rust/vendor/picky/src/putty/error.rs new file mode 100644 index 0000000..ba70fe3 --- /dev/null +++ b/rust/vendor/picky/src/putty/error.rs @@ -0,0 +1,53 @@ +#[derive(thiserror::Error, Debug)] +pub enum PuttyError { + #[error("end of input")] + EndOfInput, + #[error("invalid input")] + InvalidInput { + context: &'static str, + expected: &'static str, + actual: String, + }, + #[error("invalid key value format")] + InvalidKeyValueFormat, + #[error("AES encryption failed")] + Aes, + #[error("invalid argon2 params")] + Argon2, + #[error("MAC validation failed (wrong password or corrupted data)")] + MacValidation, + #[error("public and private key mismatch")] + PublicAndPrivateKeyMismatch, + #[error("invalid private key data")] + InvalidPrivateKeyData, + #[error("invalid public key data")] + InvalidPublicKeyData, + #[error("invalid public key container")] + InvalidPublicKeyContainer, + #[error("invalid public key comment")] + InvalidPublicKeyComment, + #[error("private key is already decrypted")] + AlreadyDecrypted, + #[error("private key is already encrypted")] + AlreadyEncrypted, + #[error("private key decryption is required prior to this operation")] + Encrypted, + #[error("unsupported feature")] + NotSupported { feature: &'static str }, + #[error("RSA params precomputation failed")] + RsaPrecompute, + #[error("RSA primes count should be exactly 2")] + RsaInvalidPrimesCount { count: usize }, + #[error(transparent)] + SshPublicKey(#[from] crate::ssh::public_key::SshPublicKeyError), + #[error(transparent)] + SshPivateKey(#[from] crate::ssh::private_key::SshPrivateKeyError), + #[error(transparent)] + KeyError(#[from] crate::key::KeyError), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + OutIsTooSmallError(#[from] inout::OutIsTooSmallError), + #[error(transparent)] + RandError(#[from] rand::rngs::SysError), +} diff --git a/rust/vendor/picky/src/putty/key_value/macros.rs b/rust/vendor/picky/src/putty/key_value/macros.rs new file mode 100644 index 0000000..0aea800 --- /dev/null +++ b/rust/vendor/picky/src/putty/key_value/macros.rs @@ -0,0 +1,152 @@ +macro_rules! ppk_const { + ($name:ident, $key:expr) => { + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] + pub struct $name; + + impl std::str::FromStr for $name { + type Err = $crate::putty::key_value::PpkValueParsingError; + + fn from_str(s: &str) -> Result { + if s == $key { + Ok(Self) + } else { + Err($crate::putty::key_value::PpkValueParsingError { + expected: $key, + actual: s.to_string(), + }) + } + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str($key) + } + } + + impl PpkLiteral for $name { + fn context() -> &'static str { + stringify!($name) + } + + fn as_static_str(&self) -> &'static str { + $key + } + } + }; +} + +macro_rules! impl_ppk_enum_expected_str { + ($first:expr, $($keys:expr),+) => { + concat!($first, ", ", $($keys),+) + }; + ($first:expr) => { + $first + }; +} + +macro_rules! ppk_enum { + ($name:ident, $($variant:ident => $key:expr),+) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum $name { + $($variant),+ + } + + impl std::str::FromStr for $name { + type Err = $crate::putty::key_value::PpkValueParsingError; + + fn from_str(s: &str) -> Result { + match s { + $($key => Ok(Self::$variant),)+ + _ => Err($crate::putty::key_value::PpkValueParsingError { + expected: concat!("[", impl_ppk_enum_expected_str!($($key),+), "]"), + actual: s.to_string() + }) + } + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + $(Self::$variant => f.write_str($key),)+ + } + } + } + + impl PpkLiteral for $name { + fn context() -> &'static str { + stringify!($name) + } + + fn as_static_str(&self) -> &'static str { + match self { + $(Self::$variant => $key,)+ + } + } + } + }; +} + +macro_rules! ppk_generic_value { + ($name:ident, $type:ident) => { + pub struct $name($type); + + impl std::str::FromStr for $name { + type Err = $crate::putty::key_value::PpkValueParsingError; + + fn from_str(s: &str) -> Result { + s.parse() + .map(Self) + .map_err(|_| $crate::putty::key_value::PpkValueParsingError { + expected: concat!(""), + actual: s.to_string(), + }) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + impl From<$type> for $name { + fn from(value: $type) -> Self { + Self(value) + } + } + + impl From<$name> for $type { + fn from(value: $name) -> Self { + value.0 + } + } + }; +} + +macro_rules! ppk_key_value { + ($name:ident, $key:ident, $value:ident) => { + pub struct $name; + + impl $crate::putty::key_value::PpkKeyValue for $name { + type Key = $key; + type Value = $value; + } + }; +} + +macro_rules! ppk_multiline_key_value { + ($name:ident, $key:ident, $value:ident) => { + pub struct $name; + + impl $crate::putty::key_value::PpkMultilineKeyValue for $name { + type Key = $key; + type Value = $value; + } + }; +} + +pub(crate) use { + impl_ppk_enum_expected_str, ppk_const, ppk_enum, ppk_generic_value, ppk_key_value, ppk_multiline_key_value, +}; diff --git a/rust/vendor/picky/src/putty/key_value/mod.rs b/rust/vendor/picky/src/putty/key_value/mod.rs new file mode 100644 index 0000000..eb1ce69 --- /dev/null +++ b/rust/vendor/picky/src/putty/key_value/mod.rs @@ -0,0 +1,195 @@ +//! This module provides a set of traits and macros and types to parse and write PuTTY key-value +//! format in strongly-typed manner. + +mod macros; +mod reader; +mod writer; + +use std::fmt; +use std::str::FromStr; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; + +use self::macros::*; + +pub(crate) use reader::PuttyKvReader; +pub(crate) use writer::PuttyKvWriter; + +pub struct PpkValueParsingError { + pub expected: &'static str, + pub actual: String, +} + +const KV_DELIMITER: &str = ": "; + +/// Trait for keys/values that could be represented as singular or set of static strings. +pub(crate) trait PpkLiteral { + fn context() -> &'static str; + fn as_static_str(&self) -> &'static str; +} + +/// Trait for key-value pairs that use multiline format. +pub(crate) trait PpkMultilineKeyValue { + type Key: FromStr + ToString + PpkLiteral; + type Value: FromStr + ToString; +} + +/// Trait for key-value pairs that use single-line format. +pub(crate) trait PpkKeyValue { + type Key: FromStr + ToString + PpkLiteral; + type Value: FromStr + ToString; +} + +/// Wrapper type for base64 multiline data inside PPK file. +pub(crate) struct Base64PpkValue(Vec); + +impl FromStr for Base64PpkValue { + type Err = PpkValueParsingError; + + fn from_str(s: &str) -> Result { + BASE64_ENGINE + .decode(s) + .map_err(|_| PpkValueParsingError { + expected: "", + actual: s.to_string(), + }) + .map(Self) + } +} + +impl fmt::Display for Base64PpkValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", BASE64_ENGINE.encode(&self.0)) + } +} + +impl From> for Base64PpkValue { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for Vec { + fn from(value: Base64PpkValue) -> Self { + value.0 + } +} + +/// Wrapper type for hex-string multiline data inside PPK file. +pub(crate) struct HexPpkValue(Vec); + +impl FromStr for HexPpkValue { + type Err = PpkValueParsingError; + + fn from_str(s: &str) -> Result { + hex::decode(s) + .map_err(|_| PpkValueParsingError { + expected: "", + actual: s.to_string(), + }) + .map(Self) + } +} + +impl fmt::Display for HexPpkValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + +impl From> for HexPpkValue { + fn from(value: Vec) -> Self { + Self(value) + } +} + +impl From for Vec { + fn from(value: HexPpkValue) -> Self { + value.0 + } +} + +// Key value type definitions + +ppk_enum!( + PpkVersionKey, + V2 => "PuTTY-User-Key-File-2", + V3 => "PuTTY-User-Key-File-3" +); +ppk_enum!( + PpkKeyAlgorithmValue, + Rsa => "ssh-rsa", + Dss => "ssh-dss", + EcdsaSha2Nistp256 => "ecdsa-sha2-nistp256", + EcdsaSha2Nistp384 => "ecdsa-sha2-nistp384", + EcdsaSha2Nistp521 => "ecdsa-sha2-nistp521", + Ed25519 => "ssh-ed25519", + Ed448 => "ssh-ed448" +); +ppk_key_value!(PpkHeader, PpkVersionKey, PpkKeyAlgorithmValue); + +ppk_const!(PpkEncryptionKey, "Encryption"); +ppk_enum!( + PpkEncryptionValue, + None => "none", + Aes256Cbc => "aes256-cbc" +); +ppk_key_value!(PpkEncryption, PpkEncryptionKey, PpkEncryptionValue); + +ppk_const!(PpkCommentKey, "Comment"); +ppk_generic_value!(PpkCommentValue, String); +ppk_key_value!(PpkComment, PpkCommentKey, PpkCommentValue); + +ppk_const!(PpkPublicLinesKey, "Public-Lines"); +ppk_multiline_key_value!(PpkPublicLines, PpkPublicLinesKey, Base64PpkValue); + +ppk_const!(PpkKeyDerivationKey, "Key-Derivation"); +ppk_enum!( + Argon2FlavourValue, + Argon2d => "Argon2d", + Argon2i => "Argon2i", + Argon2id => "Argon2id" +); +ppk_key_value!(PpkKeyDerivation, PpkKeyDerivationKey, Argon2FlavourValue); +ppk_const!(PpkArgon2MemoryKey, "Argon2-Memory"); +ppk_generic_value!(PpkArgon2MemoryValue, u32); +ppk_key_value!(PpkArgon2Memory, PpkArgon2MemoryKey, PpkArgon2MemoryValue); +ppk_const!(PpkArgon2PassesKey, "Argon2-Passes"); +ppk_generic_value!(PpkArgon2PassesValue, u32); +ppk_key_value!(PpkArgon2Passes, PpkArgon2PassesKey, PpkArgon2PassesValue); +ppk_const!(PpkArgon2ParallelismKey, "Argon2-Parallelism"); +ppk_generic_value!(PpkArgon2ParallelismValue, u32); +ppk_key_value!(PpkArgon2Parallelism, PpkArgon2ParallelismKey, PpkArgon2ParallelismValue); +ppk_const!(PpkArgon2SaltKey, "Argon2-Salt"); +ppk_key_value!(PpkArgon2Salt, PpkArgon2SaltKey, HexPpkValue); + +ppk_const!(PpkPrivateLinesKey, "Private-Lines"); +ppk_multiline_key_value!(PpkPrivateLines, PpkPrivateLinesKey, Base64PpkValue); + +ppk_const!(PpkPrivateMacKey, "Private-MAC"); +ppk_key_value!(PpkPrivateMac, PpkPrivateMacKey, HexPpkValue); + +impl From for argon2::Algorithm { + fn from(value: Argon2FlavourValue) -> Self { + match value { + Argon2FlavourValue::Argon2d => argon2::Algorithm::Argon2d, + Argon2FlavourValue::Argon2i => argon2::Algorithm::Argon2i, + Argon2FlavourValue::Argon2id => argon2::Algorithm::Argon2id, + } + } +} + +impl PpkKeyAlgorithmValue { + pub fn key_mpint_values_count(&self) -> usize { + match self { + PpkKeyAlgorithmValue::Rsa => 4, + PpkKeyAlgorithmValue::Dss + | PpkKeyAlgorithmValue::EcdsaSha2Nistp256 + | PpkKeyAlgorithmValue::EcdsaSha2Nistp384 + | PpkKeyAlgorithmValue::EcdsaSha2Nistp521 + | PpkKeyAlgorithmValue::Ed25519 + | PpkKeyAlgorithmValue::Ed448 => 1, + } + } +} diff --git a/rust/vendor/picky/src/putty/key_value/reader.rs b/rust/vendor/picky/src/putty/key_value/reader.rs new file mode 100644 index 0000000..08bef72 --- /dev/null +++ b/rust/vendor/picky/src/putty/key_value/reader.rs @@ -0,0 +1,87 @@ +use super::{KV_DELIMITER, PpkKeyValue, PpkLiteral, PpkMultilineKeyValue, PpkValueParsingError}; +use crate::putty::PuttyError; + +/// Reader for PPK key-value format. +pub struct PuttyKvReader<'a> { + input: std::str::Lines<'a>, +} + +impl<'a> PuttyKvReader<'a> { + pub fn from_str(input: &'a str) -> Self { + Self { input: input.lines() } + } + + pub fn next_value(&mut self) -> Result { + let (_, value) = self.next_key_value::()?; + Ok(value) + } + + pub fn next_key_value(&mut self) -> Result<(T::Key, T::Value), PuttyError> { + let line = self.input.next().ok_or(PuttyError::EndOfInput)?; + let (key, value) = line.split_once(KV_DELIMITER).ok_or(PuttyError::InvalidKeyValueFormat)?; + + let parsed_key = key + .parse() + .map_err(|e: PpkValueParsingError| PuttyError::InvalidInput { + context: T::Key::context(), + expected: e.expected, + actual: e.actual, + })?; + + let parsed_value = value + .parse() + .map_err(|e: PpkValueParsingError| PuttyError::InvalidInput { + context: T::Key::context(), + expected: e.expected, + actual: e.actual, + })?; + + Ok((parsed_key, parsed_value)) + } + + pub fn next_multiline_value(&mut self) -> Result { + let (_, value) = self.next_multiline_key_value::()?; + Ok(value) + } + + pub fn next_multiline_key_value(&mut self) -> Result<(T::Key, T::Value), PuttyError> { + let line = self.input.next().ok_or(PuttyError::EndOfInput)?; + let (key, value) = line.split_once(KV_DELIMITER).ok_or(PuttyError::InvalidKeyValueFormat)?; + + // Parse key early to check if it's valid before reading multiline value + let parsed_key: T::Key = key + .parse() + .map_err(|e: PpkValueParsingError| PuttyError::InvalidInput { + context: T::Key::context(), + expected: e.expected, + actual: e.actual, + })?; + + // NOTE: u16 is enough for multiline fields, as in PPK format they are storing + // base64-encoded private/public key data, and 65535 lines is more than enough for any + // supported PPK key type. + let lines_count: u16 = value.parse().map_err(|_| PuttyError::InvalidInput { + context: T::Key::context(), + expected: "", + actual: value.to_string(), + })?; + + let mut encoded = String::new(); + + for _ in 0..lines_count { + // NOTE: we do not preserve newlines for multiline fields as in PPK they are + // only used to split base64-encoded data into lines. + encoded.push_str(self.input.next().ok_or(PuttyError::EndOfInput)?); + } + + let parsed_value = encoded + .parse() + .map_err(|e: PpkValueParsingError| PuttyError::InvalidInput { + context: T::Key::context(), + expected: e.expected, + actual: e.actual, + })?; + + Ok((parsed_key, parsed_value)) + } +} diff --git a/rust/vendor/picky/src/putty/key_value/writer.rs b/rust/vendor/picky/src/putty/key_value/writer.rs new file mode 100644 index 0000000..346e0d2 --- /dev/null +++ b/rust/vendor/picky/src/putty/key_value/writer.rs @@ -0,0 +1,67 @@ +use super::{KV_DELIMITER, PpkKeyValue, PpkLiteral as _, PpkMultilineKeyValue}; + +/// Writer for PPK key-value format. +pub(crate) struct PuttyKvWriter { + output: String, + line_end: &'static str, +} + +impl PuttyKvWriter { + pub fn new(crlf: bool) -> Self { + Self { + output: String::new(), + line_end: if crlf { "\r\n" } else { "\n" }, + } + } + + fn write_key_value_impl(&mut self, key: &str, value: &str) { + self.output.push_str(key); + self.output.push_str(KV_DELIMITER); + self.output.push_str(value); + self.output.push_str(self.line_end); + } + + pub fn write_value(&mut self, value: T::Value) + where + T::Key: Default, + { + self.write_key_value::(T::Key::default(), value); + } + + pub fn write_key_value(&mut self, key: T::Key, value: T::Value) { + self.write_key_value_impl(key.as_static_str(), &value.to_string()); + } + + pub fn write_multiline_value(&mut self, value: T::Value) + where + T::Key: Default, + { + self.write_multiline_key_value::(T::Key::default(), value); + } + + pub fn write_multiline_key_value(&mut self, key: T::Key, value: T::Value) { + // PuTTY uses a maximum of 64 characters per line + const MAX_CHARS_PER_LINE: usize = 64; + + let value = value.to_string(); + + let lines_count = value.len() / MAX_CHARS_PER_LINE + (value.len() % MAX_CHARS_PER_LINE != 0) as usize; + + self.write_key_value_impl(key.as_static_str(), &lines_count.to_string()); + + let mut value_remaining = value.as_str(); + + while !value_remaining.is_empty() { + let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE); + let (line, remaining) = value_remaining.split_at(line_len); + value_remaining = remaining; + + self.output.push_str(line); + self.output.push_str(self.line_end); + } + } + + pub fn finish(self) -> String { + self.output + } +} diff --git a/rust/vendor/picky/src/putty/mod.rs b/rust/vendor/picky/src/putty/mod.rs new file mode 100644 index 0000000..082e010 --- /dev/null +++ b/rust/vendor/picky/src/putty/mod.rs @@ -0,0 +1,18 @@ +//! PuTTY key format described in [Appendix C][1] of the PuTTY User Manual. +//! +//! Both private([`Ppk`]) and public([`PuttyPublicKey`]) keys are supported. +//! +//! [1]: https://the.earth.li/~sgtatham/putty/0.75/htmldoc/AppendixC.html#ppk + +mod error; +mod key_value; +mod ppk; +mod private_key; +mod public_key; + +pub use error::PuttyError; +pub use key_value::{ + Argon2FlavourValue as Argon2Flavour, PpkKeyAlgorithmValue as PpkKeyAlgorithm, PpkVersionKey as PpkVersion, +}; +pub use ppk::{Argon2Params, Ppk, PpkEncryptionConfig, PpkEncryptionConfigBuilder}; +pub use public_key::PuttyPublicKey; diff --git a/rust/vendor/picky/src/putty/ppk/aes.rs b/rust/vendor/picky/src/putty/ppk/aes.rs new file mode 100644 index 0000000..dac2479 --- /dev/null +++ b/rust/vendor/picky/src/putty/ppk/aes.rs @@ -0,0 +1,47 @@ +//! AES encryption and decryption utilities. + +use crate::putty::PuttyError; +use aes::cipher::KeyIvInit; +use aes::cipher::block_padding::NoPadding; +use cbc::cipher::{BlockModeDecrypt, BlockModeEncrypt}; +use inout::InOutBufReserved; +use rand_core::Rng; + +pub const KEY_SIZE: usize = 32; +pub const BLOCK_SIZE: usize = 16; + +/// Adds padding to the message if it is not a multiple of the AES block size. +pub fn make_padding(mut message: Vec, mut rng: R) -> Vec { + if message.len() % BLOCK_SIZE != 0 { + let unpadded_size = message.len(); + let padding_size = BLOCK_SIZE - (unpadded_size % BLOCK_SIZE); + + message.resize(unpadded_size + padding_size, 0); + rng.fill_bytes(&mut message[unpadded_size..]); + } + + message +} + +/// Encrypts the message in-place using AES-256 in CBC mode. +pub fn encrypt(message: &mut [u8], key: &[u8], iv: &[u8]) -> Result<(), PuttyError> { + let encryptor = cbc::Encryptor::::new_from_slices(key, iv).map_err(|_| PuttyError::Aes)?; + + let inout = InOutBufReserved::from_mut_slice(message, message.len())?; + encryptor + .encrypt_padded_inout::(inout) + .map_err(|_| PuttyError::Aes)?; + + Ok(()) +} + +/// Decrypts the message in-place using AES-256 in CBC mode. +pub fn decrypt(message: &mut [u8], key: &[u8], iv: &[u8]) -> Result<(), PuttyError> { + let decryptor = cbc::Decryptor::::new_from_slices(key, iv).map_err(|_| PuttyError::Aes)?; + + let _ = decryptor + .decrypt_padded_inout::(message.into()) + .map_err(|_| PuttyError::Aes); + + Ok(()) +} diff --git a/rust/vendor/picky/src/putty/ppk/encoding.rs b/rust/vendor/picky/src/putty/ppk/encoding.rs new file mode 100644 index 0000000..6d28abe --- /dev/null +++ b/rust/vendor/picky/src/putty/ppk/encoding.rs @@ -0,0 +1,101 @@ +//! PPK encoding and decoding functions. + +use crate::putty::key_value::{ + Base64PpkValue, HexPpkValue, PpkArgon2Memory, PpkArgon2MemoryValue, PpkArgon2Parallelism, + PpkArgon2ParallelismValue, PpkArgon2Passes, PpkArgon2PassesValue, PpkArgon2Salt, PpkComment, PpkCommentValue, + PpkEncryption, PpkEncryptionValue, PpkHeader, PpkKeyDerivation, PpkPrivateLines, PpkPrivateMac, PpkPublicLines, + PpkVersionKey, PuttyKvReader, PuttyKvWriter, +}; +use crate::putty::ppk::encryption::PpkEncryptionKind; +use crate::putty::{Argon2Params, Ppk, PuttyError}; +use std::str::FromStr; + +impl FromStr for Ppk { + type Err = PuttyError; + + fn from_str(input: &str) -> Result { + let mut reader = PuttyKvReader::from_str(input); + + let (version, algorithm) = reader.next_key_value::()?; + let encryption = reader.next_value::()?; + let comment = reader.next_value::()?; + let public_key = reader.next_multiline_value::()?; + + let encryption = match encryption { + PpkEncryptionValue::None => None, + PpkEncryptionValue::Aes256Cbc if version == PpkVersionKey::V2 => Some(PpkEncryptionKind::Aes256CbcV2), + PpkEncryptionValue::Aes256Cbc => { + let argon2_flavor = reader.next_value::()?; + let argon2_memory = reader.next_value::()?; + let argon2_passes = reader.next_value::()?; + let argon2_parallelism = reader.next_value::()?; + let argon2_salt = reader.next_value::()?; + + Some(PpkEncryptionKind::Aes256CbcV3(Argon2Params { + flavor: argon2_flavor, + memory: argon2_memory.into(), + passes: argon2_passes.into(), + parallelism: argon2_parallelism.into(), + salt: argon2_salt.into(), + })) + } + }; + + let private_key = reader.next_multiline_value::()?; + let mac = reader.next_value::()?; + + let ppk = Ppk { + version, + algorithm, + encryption, + comment: comment.into(), + public_key: public_key.into(), + private_key: private_key.into(), + mac: mac.into(), + }; + + // Validate MAC for file integrity check + if ppk.encryption.is_none() { + let mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?; + + if mac.as_slice() != ppk.mac.as_slice() { + return Err(PuttyError::MacValidation); + } + } + + Ok(ppk) + } +} + +impl Ppk { + /// Encodes the PPK key to a string. + pub fn to_string(&self) -> Result { + // NOTE: V2 uses CRLF line endings, V3 uses LF + let crlf = match self.version { + PpkVersionKey::V2 => true, + PpkVersionKey::V3 => false, + }; + + let mut writer = PuttyKvWriter::new(crlf); + + writer.write_key_value::(self.version, self.algorithm); + writer.write_value::((self.encryption.as_ref()).into()); + writer.write_value::(PpkCommentValue::from(self.comment.clone())); + writer.write_multiline_value::(Base64PpkValue::from(self.public_key.clone())); + match &self.encryption { + Some(PpkEncryptionKind::Aes256CbcV3(argon2)) => { + writer.write_value::(argon2.flavor); + writer.write_value::(PpkArgon2MemoryValue::from(argon2.memory)); + writer.write_value::(PpkArgon2PassesValue::from(argon2.passes)); + writer.write_value::(PpkArgon2ParallelismValue::from(argon2.parallelism)); + writer.write_value::(HexPpkValue::from(argon2.salt.clone())); + } + None | Some(PpkEncryptionKind::Aes256CbcV2) => {} + } + + writer.write_multiline_value::(Base64PpkValue::from(self.private_key.clone())); + writer.write_value::(HexPpkValue::from(self.mac.clone())); + + Ok(writer.finish()) + } +} diff --git a/rust/vendor/picky/src/putty/ppk/encryption.rs b/rust/vendor/picky/src/putty/ppk/encryption.rs new file mode 100644 index 0000000..50f6693 --- /dev/null +++ b/rust/vendor/picky/src/putty/ppk/encryption.rs @@ -0,0 +1,240 @@ +//! PPK encryption/decryption types and fucntions + +use crate::putty::PuttyError; +use crate::putty::key_value::{Argon2FlavourValue, PpkEncryptionValue, PpkVersionKey}; +use crate::putty::ppk::kdf::{self, KeyMaterialV2}; +use crate::putty::ppk::{Argon2Params, Ppk, aes as ppk_aes}; +use crate::ssh::decode::SshReadExt; +use rand::rngs::{StdRng, SysRng}; +use rand_core::SeedableRng as _; + +/// PPK encryption configuration builder. +/// +/// Could be constructed via [`PpkEncryptionConfig::builder()`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PpkEncryptionConfigBuilder { + inner: PpkEncryptionConfig, +} + +impl PpkEncryptionConfigBuilder { + pub fn argon2_flavour(mut self, argon2_flavour: Argon2FlavourValue) -> Self { + self.inner.argon2_flavour = argon2_flavour; + self + } + + pub fn argon2_memory(mut self, argon2_memory: u32) -> Self { + self.inner.argon2_memory = argon2_memory; + self + } + + pub fn argon2_passes(mut self, argon2_passes: u32) -> Self { + self.inner.argon2_passes = argon2_passes; + self + } + + pub fn argon2_parallelism(mut self, argon2_parallelism: u32) -> Self { + self.inner.argon2_parallelism = argon2_parallelism; + self + } + + pub fn argon2_salt_size(mut self, argon2_salt_size: u32) -> Self { + self.inner.argon2_salt_size = argon2_salt_size; + self + } + + pub fn build(self) -> PpkEncryptionConfig { + self.inner + } +} + +/// PPK encryption configuration. +/// +/// Could be either constructed via [`Default::default()`] or [`PpkEncryptionConfig::builder()`] +/// +/// Defaults are the same as in PuTTY. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PpkEncryptionConfig { + argon2_flavour: Argon2FlavourValue, + argon2_memory: u32, + argon2_passes: u32, + argon2_parallelism: u32, + argon2_salt_size: u32, +} + +impl Default for PpkEncryptionConfig { + fn default() -> Self { + Self { + argon2_flavour: Argon2FlavourValue::Argon2id, + argon2_memory: 8192, + argon2_passes: 34, + argon2_parallelism: 1, + argon2_salt_size: 16, + } + } +} + +impl PpkEncryptionConfig { + pub fn builder() -> PpkEncryptionConfigBuilder { + PpkEncryptionConfigBuilder { + inner: Default::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PpkEncryptionKind { + Aes256CbcV2, + Aes256CbcV3(Argon2Params), +} + +impl From> for PpkEncryptionValue { + fn from(params: Option<&PpkEncryptionKind>) -> Self { + match params { + None => PpkEncryptionValue::None, + Some(PpkEncryptionKind::Aes256CbcV2) => PpkEncryptionValue::Aes256Cbc, + Some(PpkEncryptionKind::Aes256CbcV3(_)) => PpkEncryptionValue::Aes256Cbc, + } + } +} + +impl Ppk { + /// Returns true if the key is encrypted + pub fn is_encrypted(&self) -> bool { + self.encryption.is_some() + } + + /// Argon2 KDF parameters if encryption is used (V3 only) + pub fn argon2_params(&self) -> Option<&Argon2Params> { + match &self.encryption { + Some(PpkEncryptionKind::Aes256CbcV3(params)) => Some(params), + _ => None, + } + } + + /// Returns PPK key encrypted with the specified passphrase and config. + pub fn encrypt(&self, passphrase: &str, config: PpkEncryptionConfig) -> Result { + self.encrypt_with_rng(passphrase, config, StdRng::try_from_rng(&mut SysRng)?) + } + + /// Returns PPK key encrypted with the specified passphrase, config and RNG. + pub fn encrypt_with_rng( + &self, + passphrase: &str, + config: PpkEncryptionConfig, + mut rng: impl rand_core::Rng, + ) -> Result { + if self.encryption.is_some() { + return Err(PuttyError::AlreadyEncrypted); + } + + let ppk = match self.version { + PpkVersionKey::V2 => { + let key_material = kdf::derive_key_material_v2(passphrase)?; + + let mut private_key = ppk_aes::make_padding(self.private_key.clone(), rng); + let mac = self.calculate_mac_v2(passphrase, &private_key, PpkEncryptionValue::Aes256Cbc)?; + ppk_aes::encrypt(&mut private_key, key_material.key(), KeyMaterialV2::iv())?; + + Ppk { + version: self.version, + algorithm: self.algorithm, + encryption: Some(PpkEncryptionKind::Aes256CbcV2), + comment: self.comment.clone(), + public_key: self.public_key.clone(), + private_key, + mac, + } + } + PpkVersionKey::V3 => { + let mut argon2_salt = vec![0u8; config.argon2_salt_size as usize]; + rng.fill_bytes(&mut argon2_salt); + let argon2_params = Argon2Params { + flavor: config.argon2_flavour, + memory: config.argon2_memory, + passes: config.argon2_passes, + parallelism: config.argon2_parallelism, + salt: argon2_salt, + }; + + let key_material = kdf::derive_key_material_v3(&argon2_params, passphrase)?; + + let mut private_key = ppk_aes::make_padding(self.private_key.clone(), rng); + let mac = + self.calculate_mac_v3(key_material.hmac_key(), &private_key, PpkEncryptionValue::Aes256Cbc)?; + ppk_aes::encrypt(&mut private_key, key_material.key(), key_material.iv())?; + + Ppk { + version: self.version, + algorithm: self.algorithm, + encryption: Some(PpkEncryptionKind::Aes256CbcV3(argon2_params)), + comment: self.comment.clone(), + public_key: self.public_key.clone(), + private_key, + mac, + } + } + }; + + Ok(ppk) + } + + /// Returns PPK key decrypted with the specified passphrase + pub fn decrypt(&self, passphrase: &str) -> Result { + let encrytion = if let Some(encryption) = &self.encryption { + encryption + } else { + return Err(PuttyError::AlreadyDecrypted); + }; + + let (mut private_key, mac) = match encrytion { + PpkEncryptionKind::Aes256CbcV2 => { + let key_material = kdf::derive_key_material_v2(passphrase)?; + let mut private_key = self.private_key.clone(); + ppk_aes::decrypt(&mut private_key, key_material.key(), KeyMaterialV2::iv())?; + let mac = self.calculate_mac_v2(passphrase, &private_key, PpkEncryptionValue::Aes256Cbc)?; + (private_key, mac) + } + PpkEncryptionKind::Aes256CbcV3(argon2_params) => { + let key_material = kdf::derive_key_material_v3(argon2_params, passphrase)?; + let mut private_key = self.private_key.clone(); + ppk_aes::decrypt(&mut private_key, key_material.key(), key_material.iv())?; + let mac = + self.calculate_mac_v3(key_material.hmac_key(), &private_key, PpkEncryptionValue::Aes256Cbc)?; + (private_key, mac) + } + }; + + // Verify MAC + if mac.as_slice() != self.mac.as_slice() { + return Err(PuttyError::MacValidation); + } + + // Truncate private key padding if any + let truncated_size = { + let mut mpint_cursor = private_key.as_slice(); + for _ in 0..self.algorithm.key_mpint_values_count() { + // NOTE: Bytes and mpint stored the same way, therefore to avoid BigUint + // construction we can just read the bytes (we discard them either way) + let _value = mpint_cursor.read_ssh_bytes()?; + } + + private_key.len().wrapping_sub(mpint_cursor.len()) + }; + + private_key.truncate(truncated_size); + + let mac = self.calculate_unencrypted_mac(private_key.as_slice())?; + + let ppk = Ppk { + version: self.version, + algorithm: self.algorithm, + encryption: None, + comment: self.comment.clone(), + public_key: self.public_key.clone(), + private_key, + mac, + }; + + Ok(ppk) + } +} diff --git a/rust/vendor/picky/src/putty/ppk/kdf.rs b/rust/vendor/picky/src/putty/ppk/kdf.rs new file mode 100644 index 0000000..00affe8 --- /dev/null +++ b/rust/vendor/picky/src/putty/ppk/kdf.rs @@ -0,0 +1,120 @@ +//! Key derivation utilities for PPK files. + +use crate::putty::key_value::Argon2FlavourValue; +use crate::putty::ppk::{PuttyError, aes as ppk_aes}; + +use digest::Digest; +use zeroize::Zeroizing; + +const SHA256_DIGEST_SIZE: usize = 32; +const SHA1_DIGEST_SIZE: usize = 20; + +pub const MAC_SIZE_V3: usize = SHA256_DIGEST_SIZE; + +/// Argon2 key derivation function parameters. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Argon2Params { + pub flavor: Argon2FlavourValue, + pub memory: u32, + pub passes: u32, + pub parallelism: u32, + pub salt: Vec, +} + +const V3_KEY_MATERIAL_SIZE: usize = ppk_aes::KEY_SIZE // AES key + + ppk_aes::BLOCK_SIZE // AES IV + + MAC_SIZE_V3; // HMAC key + +pub(crate) struct KeyMaterialV3 { + key: Zeroizing<[u8; ppk_aes::KEY_SIZE]>, + iv: Zeroizing<[u8; ppk_aes::BLOCK_SIZE]>, + hmac_key: Zeroizing<[u8; MAC_SIZE_V3]>, +} + +impl KeyMaterialV3 { + pub fn key(&self) -> &[u8] { + self.key.as_ref() + } + + pub fn iv(&self) -> &[u8] { + self.iv.as_ref() + } + + pub fn hmac_key(&self) -> &[u8] { + self.hmac_key.as_ref() + } +} + +pub(crate) struct KeyMaterialV2 { + key: Zeroizing<[u8; ppk_aes::KEY_SIZE]>, +} + +impl KeyMaterialV2 { + pub fn key(&self) -> &[u8] { + self.key.as_ref() + } + + pub fn iv() -> &'static [u8; ppk_aes::BLOCK_SIZE] { + &[0u8; ppk_aes::BLOCK_SIZE] + } +} + +pub(crate) fn derive_key_material_v2(passphrase: &str) -> Result { + let tagged_hash = |tag: u32| -> [u8; SHA1_DIGEST_SIZE] { + let mut digest = sha1::Sha1::new(); + digest.update(tag.to_be_bytes()); + digest.update(passphrase.as_bytes()); + digest.finalize().into() + }; + + let hash1 = tagged_hash(0); + let hash2 = tagged_hash(1); + + let mut key = [0u8; ppk_aes::KEY_SIZE]; + key[..SHA1_DIGEST_SIZE].copy_from_slice(&hash1[..]); + key[SHA1_DIGEST_SIZE..].copy_from_slice(&hash2[..ppk_aes::KEY_SIZE - SHA1_DIGEST_SIZE]); + + Ok(KeyMaterialV2 { key: key.into() }) +} + +pub(crate) fn derive_key_material_v3( + argon2_params: &Argon2Params, + passphrase: &str, +) -> Result { + let mut key_material = [0u8; V3_KEY_MATERIAL_SIZE]; + + let kdf = argon2::Argon2::new( + argon2_params.flavor.into(), + argon2::Version::V0x13, + argon2::Params::new( + argon2_params.memory, + argon2_params.passes, + argon2_params.parallelism, + Some(V3_KEY_MATERIAL_SIZE), + ) + .map_err(|_| PuttyError::Argon2)?, + ); + + kdf.hash_password_into(passphrase.as_bytes(), &argon2_params.salt, &mut key_material) + .map_err(|_| PuttyError::Argon2)?; + + let key_material = key_material.as_ref(); + + const IV_OFFSET: usize = ppk_aes::KEY_SIZE; + const HMAC_KEY_OFFSET: usize = IV_OFFSET + ppk_aes::BLOCK_SIZE; + + let mut key = [0u8; ppk_aes::KEY_SIZE]; + key.copy_from_slice(&key_material[..ppk_aes::KEY_SIZE]); + + let mut iv = [0u8; ppk_aes::BLOCK_SIZE]; + iv.copy_from_slice(&key_material[IV_OFFSET..IV_OFFSET + ppk_aes::BLOCK_SIZE]); + + let mut hmac_key = [0u8; MAC_SIZE_V3]; + hmac_key.copy_from_slice(&key_material[HMAC_KEY_OFFSET..]); + + Ok(KeyMaterialV3 { + key: key.into(), + iv: iv.into(), + hmac_key: hmac_key.into(), + }) +} diff --git a/rust/vendor/picky/src/putty/ppk/mac.rs b/rust/vendor/picky/src/putty/ppk/mac.rs new file mode 100644 index 0000000..a7cb991 --- /dev/null +++ b/rust/vendor/picky/src/putty/ppk/mac.rs @@ -0,0 +1,79 @@ +//! PPK MAC calculation functions + +use crate::putty::key_value::{PpkEncryptionValue, PpkLiteral, PpkVersionKey}; +use crate::putty::{Ppk, PuttyError}; + +use digest::Digest; +use hmac::{KeyInit, Mac}; + +impl Ppk { + pub(super) fn calculate_mac_v3( + &self, + mac_key: &[u8], + private_key_data: &[u8], + encryption: PpkEncryptionValue, + ) -> Result, PuttyError> { + let mut hmac = hmac::Hmac::::new_from_slice(mac_key).map_err(|_| PuttyError::MacValidation)?; + + let mut hash_bytes = |data: &[u8]| -> Result<(), PuttyError> { + hmac.update( + &u32::try_from(data.len()) + .map_err(|_| PuttyError::MacValidation)? + .to_be_bytes(), + ); + hmac.update(data); + Ok(()) + }; + + hash_bytes(self.algorithm.as_static_str().as_bytes())?; + hash_bytes(encryption.as_static_str().as_bytes())?; + hash_bytes(self.comment.as_bytes())?; + hash_bytes(&self.public_key)?; + hash_bytes(private_key_data)?; + + let mac = hmac.finalize().into_bytes().to_vec(); + Ok(mac) + } + + pub(super) fn calculate_mac_v2( + &self, + passphrase: &str, + private_key_data: &[u8], + encryption: PpkEncryptionValue, + ) -> Result, PuttyError> { + let mac_key = { + let mut digest = sha1::Sha1::new(); + digest.update(b"putty-private-key-file-mac-key"); + digest.update(passphrase.as_bytes()); + digest.finalize() + }; + + let mut hmac = hmac::Hmac::::new_from_slice(&mac_key).map_err(|_| PuttyError::MacValidation)?; + + let mut hash_bytes = |data: &[u8]| -> Result<(), PuttyError> { + hmac.update( + &u32::try_from(data.len()) + .map_err(|_| PuttyError::MacValidation)? + .to_be_bytes(), + ); + hmac.update(data); + Ok(()) + }; + + hash_bytes(self.algorithm.as_static_str().as_bytes())?; + hash_bytes(encryption.as_static_str().as_bytes())?; + hash_bytes(self.comment.as_bytes())?; + hash_bytes(&self.public_key)?; + hash_bytes(private_key_data)?; + + let mac = hmac.finalize().into_bytes().to_vec(); + Ok(mac) + } + + pub(super) fn calculate_unencrypted_mac(&self, private_key_data: &[u8]) -> Result, PuttyError> { + match self.version { + PpkVersionKey::V2 => self.calculate_mac_v2("", private_key_data, PpkEncryptionValue::None), + PpkVersionKey::V3 => self.calculate_mac_v3(&[], private_key_data, PpkEncryptionValue::None), + } + } +} diff --git a/rust/vendor/picky/src/putty/ppk/mod.rs b/rust/vendor/picky/src/putty/ppk/mod.rs new file mode 100644 index 0000000..c367000 --- /dev/null +++ b/rust/vendor/picky/src/putty/ppk/mod.rs @@ -0,0 +1,258 @@ +mod aes; +mod encoding; +mod encryption; +mod kdf; +mod mac; + +use crate::key::{EcCurve, PrivateKey, PublicKey}; +use crate::putty::PuttyError; +use crate::putty::key_value::{PpkKeyAlgorithmValue, PpkVersionKey}; +use crate::putty::private_key::{PuttyBasePrivateKey, PuttyPrivateKey}; +use crate::putty::public_key::{PuttyBasePublicKey, PuttyPublicKey}; +use crate::ssh::SshPrivateKey; + +use self::encryption::PpkEncryptionKind; + +pub use encryption::{PpkEncryptionConfig, PpkEncryptionConfigBuilder}; +pub use kdf::Argon2Params; + +/// PuTTY Private Key (PPK) format. +/// +/// ### Functionality +/// - Generation of new keys. +/// - Conversion to/from OpenSSH format. +/// - Encoding/decoding to/from string. +/// - Version upgrade/downgrade. +/// +/// ### Usage notes +/// - Ppk structure is immutable. All operations that modify the key return a new instance. +/// - When input file is encrypted, all operations with the private key will be unavailable until +/// ppk is decrypted via [`Ppk::decrypt`]. +/// - Newly generated keys are always unencrypted. They should be encrypted via [`Ppk::encrypt`] +/// when required +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Ppk { + version: PpkVersionKey, + algorithm: PpkKeyAlgorithmValue, + encryption: Option, + comment: String, + public_key: Vec, + private_key: Vec, + mac: Vec, +} + +impl Ppk { + pub fn generate_rsa(bits: usize, comment: Option<&str>) -> Result { + let ssh_key = SshPrivateKey::generate_rsa(bits, None, comment.map(From::from))?; + Self::from_openssh_private_key(&ssh_key) + } + + pub fn generate_ec(curve: EcCurve, comment: Option<&str>) -> Result { + let ssh_key = SshPrivateKey::generate_ec(curve, None, comment.map(From::from))?; + Self::from_openssh_private_key(&ssh_key) + } + + pub fn generate_ed25519(comment: Option<&str>) -> Result { + let ssh_key = SshPrivateKey::generate_ed25519(None, comment.map(From::from))?; + Self::from_openssh_private_key(&ssh_key) + } + + /// Converts the OpenSSH private key to a PPK key. + pub fn from_openssh_private_key(key: &SshPrivateKey) -> Result { + let PuttyPrivateKey { base, comment }: PuttyPrivateKey = PuttyPrivateKey::from_openssh(key)?; + + let mut ppk = Ppk { + version: PpkVersionKey::V3, + algorithm: base.algorithm, + encryption: None, + comment, + public_key: base.public_key.data, + private_key: base.data, + mac: vec![], + }; + + ppk.mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?; + + Ok(ppk) + } + + /// Converts the PPK key to an OpenSSH private key (with or without encryption). + pub fn to_openssh_private_key(&self, passphrase: Option<&str>) -> Result { + if self.is_encrypted() { + return Err(PuttyError::Encrypted); + } + + let base = PuttyBasePrivateKey { + algorithm: self.algorithm, + public_key: PuttyBasePublicKey { + data: self.public_key.clone(), + }, + data: self.private_key.clone(), + }; + + let key = PuttyPrivateKey { + base, + comment: self.comment.clone(), + }; + + key.to_openssh(passphrase) + } + + /// Returns PPK public key. + pub fn public_key(&self) -> Result { + PuttyBasePublicKey { + data: self.public_key.clone(), + } + .to_inner_key() + } + + /// Returns PPK private key. + pub fn private_key(&self) -> Result { + if self.is_encrypted() { + return Err(PuttyError::Encrypted); + } + + PuttyBasePrivateKey { + algorithm: self.algorithm, + public_key: PuttyBasePublicKey { + data: self.public_key.clone(), + }, + data: self.private_key.clone(), + } + .to_inner_key() + } + + /// Returns extracted public key in PuTTY format. + pub fn extract_putty_public_key(&self) -> Result { + Ok(PuttyPublicKey { + base: PuttyBasePublicKey { + data: self.public_key.clone(), + }, + comment: self.comment.clone(), + }) + } + + /// Returns a new PPK key instance with a different comment. + pub fn with_comment(&self, comment: &str) -> Result { + if self.is_encrypted() { + // We need to decrypt the key to change the comment (MAC should be recalculated). + return Err(PuttyError::Encrypted); + } + + let mut ppk = Self { + comment: comment.to_string(), + ..self.clone() + }; + + ppk.mac = ppk.calculate_unencrypted_mac(ppk.private_key.as_slice())?; + + Ok(ppk) + } + + /// Returns the version of the PPK file format. + pub fn version(&self) -> PpkVersionKey { + self.version + } + + /// Returns the key algorithm. + pub fn algorithm(&self) -> PpkKeyAlgorithmValue { + self.algorithm + } + + /// Returns key comment. + pub fn comment(&self) -> &str { + &self.comment + } + + /// Returns new PPK kew with the specified format version. + /// + /// NOTE: `PpkVersionKey::V2` is considered insecure and should not be used for new keys in + /// normal circumstances. + pub fn to_version(&self, version: PpkVersionKey) -> Result { + if self.is_encrypted() { + return Err(PuttyError::Encrypted); + } + + let ppk = Ppk { + version, + algorithm: self.algorithm, + encryption: None, + comment: self.comment.clone(), + public_key: self.public_key.clone(), + private_key: self.private_key.clone(), + mac: self.calculate_unencrypted_mac(self.private_key.as_slice())?, + }; + + Ok(ppk) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + use picky_test_data::{ + PUTTY_KEY_ED25519, PUTTY_KEY_ED25519_ENCRYPTED, PUTTY_KEY_ED25519_V2, PUTTY_KEY_ED25519_V2_ENCRYPTED, + SSH_PRIVATE_KEY_EC_P256, SSH_PRIVATE_KEY_EC_P384, SSH_PRIVATE_KEY_EC_P521, SSH_PRIVATE_KEY_ED25519, + SSH_PRIVATE_KEY_RSA, + }; + + #[rstest] + #[case(PUTTY_KEY_ED25519)] + #[case(PUTTY_KEY_ED25519_ENCRYPTED)] + #[case(PUTTY_KEY_ED25519_V2)] + #[case(PUTTY_KEY_ED25519_V2_ENCRYPTED)] + fn ppk_encode_decode_roundtrip(#[case] input: &str) { + let key: Ppk = input.parse().unwrap(); + let encoded = key.to_string().unwrap(); + + assert_eq!(encoded, input); + } + + #[rstest] + #[case(PUTTY_KEY_ED25519_ENCRYPTED, PUTTY_KEY_ED25519)] + #[case(PUTTY_KEY_ED25519_V2_ENCRYPTED, PUTTY_KEY_ED25519_V2)] + fn decrypt_produces_same_key_as_puttygen(#[case] encrypted: &str, #[case] decrypted: &str) { + let mut key: Ppk = encrypted.parse().unwrap(); + assert!(key.is_encrypted()); + key = key.decrypt("test").unwrap(); + + let encoded = key.to_string().unwrap(); + assert_eq!(encoded, decrypted); + assert!(!key.is_encrypted()); + } + + #[rstest] + #[case(PUTTY_KEY_ED25519, PpkVersionKey::V3, "eddsa-key-20240414")] + #[case(PUTTY_KEY_ED25519_V2, PpkVersionKey::V2, "ed25519-key-20240418")] + fn encrypt_decrypt_roundtrip(#[case] input: &str, #[case] version: PpkVersionKey, #[case] comment: &str) { + let mut key: Ppk = input.parse().unwrap(); + key = key.encrypt("test", Default::default()).unwrap(); + assert!(key.is_encrypted()); + key = key.decrypt("test").unwrap(); + assert_eq!(key.to_string().unwrap(), input); + assert!(!key.is_encrypted()); + assert_eq!(key.version(), version); + assert_eq!(key.algorithm(), PpkKeyAlgorithmValue::Ed25519); + assert_eq!(key.comment(), comment); + } + + #[rstest] + #[case(SSH_PRIVATE_KEY_RSA)] + #[case(SSH_PRIVATE_KEY_EC_P256)] + #[case(SSH_PRIVATE_KEY_EC_P384)] + #[case(SSH_PRIVATE_KEY_EC_P521)] + #[case(SSH_PRIVATE_KEY_ED25519)] + fn test_openssh_roundtrip(#[case] input: &str) { + let ssh_key = SshPrivateKey::from_pem_str(input, None).unwrap(); + let key = Ppk::from_openssh_private_key(&ssh_key).unwrap(); + let mut ssh_key2 = key.to_openssh_private_key(None).unwrap(); + + // Check is re-generated when new ssh is created from scratch + ssh_key2.check = ssh_key.check; + + let ssh_key_str = ssh_key2.to_string().unwrap(); + assert_eq!(ssh_key_str, input); + } +} diff --git a/rust/vendor/picky/src/putty/private_key.rs b/rust/vendor/picky/src/putty/private_key.rs new file mode 100644 index 0000000..1ff8a1f --- /dev/null +++ b/rust/vendor/picky/src/putty/private_key.rs @@ -0,0 +1,235 @@ +use crate::key::ec::{EcdsaKeypair, EcdsaPublicKey, NamedEcCurve}; +use crate::key::ed::{EdKeypair, EdPublicKey, NamedEdAlgorithm}; +use crate::key::{EcCurve, EdAlgorithm, PrivateKey}; +use crate::putty::PuttyError; +use crate::putty::key_value::PpkKeyAlgorithmValue; +use crate::putty::public_key::PuttyBasePublicKey; +use crate::ssh::SshPrivateKey; +use crate::ssh::decode::SshReadExt; +use crate::ssh::encode::SshWriteExt; +use crate::ssh::private_key::SshBasePrivateKey; +use crate::ssh::public_key::SshBasePublicKey; +use crypto_bigint::BoxedUint; +use rsa::traits::{PrivateKeyParts, PublicKeyParts}; +use rsa::{RsaPrivateKey, RsaPublicKey}; + +/// PuTTY private key wrapper +pub(crate) struct PuttyPrivateKey { + pub(crate) base: PuttyBasePrivateKey, + pub(crate) comment: String, +} + +impl PuttyPrivateKey { + pub fn from_openssh(key: &SshPrivateKey) -> Result { + let base = PuttyBasePrivateKey::from_openssh(&key.base_key)?; + let comment = key.comment.clone(); + + Ok(Self { base, comment }) + } + + /// Converts the key to an OpenSSH key (with or without encryption) + pub fn to_openssh(&self, passphrase: Option<&str>) -> Result { + let base = self.base.to_openssh()?; + let comment = if self.comment.is_empty() { + None + } else { + Some(self.comment.clone()) + }; + + let key = match base { + SshBasePrivateKey::Rsa(key) => key, + SshBasePrivateKey::Ec(key) => key, + SshBasePrivateKey::Ed(key) => key, + SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => { + return Err(PuttyError::NotSupported { feature: "SK keys" }); + } + }; + + Ok(SshPrivateKey::h_picky_private_key_to_ssh_private_key( + key, + passphrase.map(From::from), + comment, + )?) + } +} + +pub(crate) struct PuttyBasePrivateKey { + pub(crate) algorithm: PpkKeyAlgorithmValue, + pub(crate) public_key: PuttyBasePublicKey, + pub(crate) data: Vec, +} + +impl PuttyBasePrivateKey { + pub fn from_openssh(key: &SshBasePrivateKey) -> Result { + let mut data = Vec::new(); + let cursor = &mut data; + + match key { + SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => { + // Putty does not support SK keys + Err(PuttyError::NotSupported { feature: "SK keys" }) + } + SshBasePrivateKey::Rsa(key) => { + let mut rsa_key = RsaPrivateKey::try_from(key)?; + + cursor.write_ssh_mpint(rsa_key.d())?; + if rsa_key.primes().len() != 2 { + return Err(PuttyError::RsaInvalidPrimesCount { + count: rsa_key.primes().len(), + }); + } + cursor.write_ssh_mpint(&rsa_key.primes()[0])?; + cursor.write_ssh_mpint(&rsa_key.primes()[1])?; + + rsa_key.precompute().map_err(|_| PuttyError::RsaPrecompute)?; + let qinv = rsa_key + .qinv() + .expect("BUG: should be precomuted above") + .retrieve() + .to_be_bytes_trimmed_vartime(); + cursor.write_ssh_bytes(&qinv)?; + + let ssh_public_key = SshBasePublicKey::Rsa(key.to_public_key()?); + let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?; + + Ok(Self { + algorithm: PpkKeyAlgorithmValue::Rsa, + data, + public_key, + }) + } + SshBasePrivateKey::Ec(key) => { + let ec_key = EcdsaKeypair::try_from(key)?; + + let secret = BoxedUint::from_be_slice_vartime(ec_key.secret()); + cursor.write_ssh_mpint(&secret)?; + + let algorithm = match ec_key.curve() { + NamedEcCurve::Known(EcCurve::NistP256) => PpkKeyAlgorithmValue::EcdsaSha2Nistp256, + NamedEcCurve::Known(EcCurve::NistP384) => PpkKeyAlgorithmValue::EcdsaSha2Nistp384, + NamedEcCurve::Known(EcCurve::NistP521) => PpkKeyAlgorithmValue::EcdsaSha2Nistp521, + _ => { + return Err(PuttyError::NotSupported { + feature: "unknown EC curve", + }); + } + }; + + let ssh_public_key = SshBasePublicKey::Ec(key.to_public_key()?); + let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?; + + Ok(Self { + algorithm, + data, + public_key, + }) + } + SshBasePrivateKey::Ed(key) => { + let ed_key = EdKeypair::try_from(key)?; + + cursor.write_ssh_mpint(&BoxedUint::from_be_slice_vartime(ed_key.secret()))?; + + let algorithm = match ed_key.algorithm() { + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => PpkKeyAlgorithmValue::Ed25519, + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => { + return Err(PuttyError::NotSupported { feature: "X25519 keys" }); + } + _ => { + return Err(PuttyError::NotSupported { + feature: "unknown EdDSA algorithm", + }); + } + }; + + let ssh_public_key = SshBasePublicKey::Ed(key.to_public_key()?); + let public_key = PuttyBasePublicKey::from_openssh(&ssh_public_key)?; + + Ok(Self { + algorithm, + data, + public_key, + }) + } + } + } + + pub fn to_openssh(&self) -> Result { + let ssh_public_key = self.public_key.to_openssh()?; + let mut data = self.data.as_slice(); + + match self.algorithm { + PpkKeyAlgorithmValue::Rsa => { + let public = match &ssh_public_key { + SshBasePublicKey::Rsa(rsa) => RsaPublicKey::try_from(rsa)?, + _ => return Err(PuttyError::PublicAndPrivateKeyMismatch), + }; + + let d = data.read_ssh_mpint()?; + let p1 = data.read_ssh_mpint()?; + let p2 = data.read_ssh_mpint()?; + let _qinv = data.read_ssh_mpint()?; + + let private_key = PrivateKey::from_rsa_components(public.n(), public.e(), &d, &[p1, p2])?; + + Ok(SshBasePrivateKey::Rsa(private_key)) + } + PpkKeyAlgorithmValue::EcdsaSha2Nistp256 + | PpkKeyAlgorithmValue::EcdsaSha2Nistp384 + | PpkKeyAlgorithmValue::EcdsaSha2Nistp521 => { + let public = match &ssh_public_key { + SshBasePublicKey::Ec(rsa) => EcdsaPublicKey::try_from(rsa)?, + _ => return Err(PuttyError::PublicAndPrivateKeyMismatch), + }; + + let secret = data.read_ssh_mpint()?; + + let curve = match self.algorithm { + PpkKeyAlgorithmValue::EcdsaSha2Nistp256 => NamedEcCurve::Known(EcCurve::NistP256), + PpkKeyAlgorithmValue::EcdsaSha2Nistp384 => NamedEcCurve::Known(EcCurve::NistP384), + PpkKeyAlgorithmValue::EcdsaSha2Nistp521 => NamedEcCurve::Known(EcCurve::NistP521), + _ => unreachable!("BUG: algorithm is checked above"), + }; + + let private_key = PrivateKey::from_ec_encoded_components( + curve.into(), + &secret.to_be_bytes_trimmed_vartime(), + Some(public.encoded_point()), + ); + + Ok(SshBasePrivateKey::Ec(private_key)) + } + PpkKeyAlgorithmValue::Ed25519 => { + let public = match &ssh_public_key { + SshBasePublicKey::Ed(rsa) => EdPublicKey::try_from(rsa)?, + _ => return Err(PuttyError::PublicAndPrivateKeyMismatch), + }; + + let secret = data.read_ssh_mpint()?; + + let private_key = PrivateKey::from_ed_encoded_components( + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(), + &secret.to_be_bytes_trimmed_vartime(), + Some(public.data()), + ); + + Ok(SshBasePrivateKey::Ed(private_key)) + } + _ => Err(PuttyError::NotSupported { + feature: "unsupported key algorithm", + }), + } + } + + pub fn to_inner_key(&self) -> Result { + let inner = match self.to_openssh()? { + SshBasePrivateKey::Rsa(key) => key, + SshBasePrivateKey::Ec(key) => key, + SshBasePrivateKey::Ed(key) => key, + SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => { + return Err(PuttyError::NotSupported { feature: "SK keys" }); + } + }; + + Ok(inner) + } +} diff --git a/rust/vendor/picky/src/putty/public_key.rs b/rust/vendor/picky/src/putty/public_key.rs new file mode 100644 index 0000000..bf7a345 --- /dev/null +++ b/rust/vendor/picky/src/putty/public_key.rs @@ -0,0 +1,224 @@ +use crate::key::PublicKey; +use crate::putty::PuttyError; +use crate::ssh::SshPublicKey; +use crate::ssh::decode::SshComplexTypeDecode; +use crate::ssh::encode::SshComplexTypeEncode; +use crate::ssh::public_key::SshBasePublicKey; +use std::str::FromStr; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD as BASE64_ENGINE; + +const PUTTY_PUBKEY_HEADER: &str = "---- BEGIN SSH2 PUBLIC KEY ----"; +const PUTTY_PUBKEY_FOOTER: &str = "---- END SSH2 PUBLIC KEY ----"; + +/// PuTTY public key format. +/// +/// ### Functionality: +/// - Conversion to/from OpenSSH format. +/// - Encoding/decoding to/from string. +/// - Could be extracted from [`crate::putty::Ppk`] private keys. +/// +/// ### Notes +/// - Although top-level containeris similar to PEM, it is not compatible with it because of +/// additional comment field after the header. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuttyPublicKey { + pub(crate) base: PuttyBasePublicKey, + pub(crate) comment: String, +} + +impl PuttyPublicKey { + /// Converts an OpenSSH public key to a PuTTY public key. + pub fn from_openssh(key: &SshPublicKey) -> Result { + let base = PuttyBasePublicKey::from_openssh(&key.inner_key)?; + + Ok(Self { + base, + comment: key.comment.clone(), + }) + } + + /// Converts the key to an OpenSSH public key. + pub fn to_openssh(&self) -> Result { + let base = self.base.to_openssh()?; + Ok(SshPublicKey { + inner_key: base, + comment: self.comment.clone(), + }) + } + + /// Returns key comment. + pub fn comment(&self) -> &str { + &self.comment + } + + /// Returns a new public key instance with a different comment. + pub fn with_comment(&self, comment: &str) -> Self { + Self { + comment: comment.to_string(), + ..self.clone() + } + } + + /// Parses and returns the inner key as standard picky key type. + pub fn to_inner_key(&self) -> Result { + self.base.to_inner_key() + } +} + +impl std::fmt::Display for PuttyPublicKey { + // False positive, clippy does not take into account that [`String::replace`] requires both + // arguments to be the same type, e.g. we can't use `&str` as a replacement for `char`. + #[allow(clippy::single_char_pattern)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + const MAX_CHARS_PER_LINE: usize = 64; + const LINE_END: &str = "\r\n"; + + let encoded_key = BASE64_ENGINE.encode(&self.base.data); + let escaped_comment = self.comment.replace("\\", "\\\\").replace("\"", "\\\""); + + let mut output = String::new(); + output.push_str(PUTTY_PUBKEY_HEADER); + output.push_str(LINE_END); + output.push_str("Comment: \""); + output.push_str(&escaped_comment); + output.push('"'); + output.push_str(LINE_END); + + let mut value_remaining = encoded_key.as_str(); + + while !value_remaining.is_empty() { + let line_len = value_remaining.len().min(MAX_CHARS_PER_LINE); + let (line, remaining) = value_remaining.split_at(line_len); + value_remaining = remaining; + + output.push_str(line); + output.push_str(LINE_END); + } + + output.push_str(PUTTY_PUBKEY_FOOTER); + output.push_str(LINE_END); + + f.write_str(&output) + } +} + +impl FromStr for PuttyPublicKey { + type Err = PuttyError; + + fn from_str(s: &str) -> Result { + let mut lines = s.lines(); + let header = lines.next().ok_or(PuttyError::EndOfInput)?; + if header != PUTTY_PUBKEY_HEADER { + return Err(PuttyError::InvalidPublicKeyContainer); + } + + let comment_line = lines.next().ok_or(PuttyError::EndOfInput)?; + if !comment_line.starts_with("Comment: ") { + return Err(PuttyError::InvalidPublicKeyComment); + } + + let unescaped_comment = comment_line + .split_once('"') + .and_then(|(_, remainder)| remainder.rsplit_once('"')) + .map(|(comment, _)| comment) + .ok_or(PuttyError::InvalidPublicKeyComment)?; + + let comment = unescaped_comment.replace("\\\"", "\"").replace("\\\\", "\\"); + + let mut encoded_key = String::new(); + for line in lines { + if line == PUTTY_PUBKEY_FOOTER { + let decoded = BASE64_ENGINE + .decode(encoded_key) + .map_err(|_| PuttyError::InvalidPublicKeyData)?; + + return Ok(PuttyPublicKey { + base: PuttyBasePublicKey { data: decoded }, + comment, + }); + } + + encoded_key.push_str(line); + } + + Err(PuttyError::EndOfInput) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PuttyBasePublicKey { + pub(crate) data: Vec, +} + +impl PuttyBasePublicKey { + pub fn from_openssh(key: &SshBasePublicKey) -> Result { + match key { + SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => { + // Putty does not support SK keys + return Err(PuttyError::NotSupported { feature: "SK keys" }); + } + _ => {} + }; + + let mut data = Vec::new(); + SshBasePublicKey::encode(key, &mut data)?; + + Ok(Self { data }) + } + + pub fn to_openssh(&self) -> Result { + let key = SshBasePublicKey::decode(self.data.as_slice())?; + Ok(key) + } + + pub fn to_inner_key(&self) -> Result { + let inner = match self.to_openssh()? { + SshBasePublicKey::Rsa(key) => key, + SshBasePublicKey::Ec(key) => key, + SshBasePublicKey::Ed(key) => key, + SshBasePublicKey::SkEcdsaSha2NistP256 { .. } | SshBasePublicKey::SkEd25519 { .. } => { + return Err(PuttyError::NotSupported { feature: "SK keys" }); + } + }; + + Ok(inner) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use picky_test_data::{ + PUTTY_KEY_ED25519_PUBLIC, PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT, PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT, + SSH_PUBLIC_KEY_EC_P256, SSH_PUBLIC_KEY_EC_P384, SSH_PUBLIC_KEY_EC_P521, SSH_PUBLIC_KEY_ED25519, + SSH_PUBLIC_KEY_RSA, + }; + use rstest::rstest; + + #[rstest] + #[case(PUTTY_KEY_ED25519_PUBLIC)] + #[case(PUTTY_KEY_RSA_PUBLIC_EMPTY_COMMENT)] + #[case(PUTTY_KEY_RSA_PUBLIC_ESCAPED_COMMENT)] + fn public_key_rountrip(#[case] input: &str) { + let key: PuttyPublicKey = input.parse().unwrap(); + let output = key.to_string(); + assert_eq!(input, output); + } + + #[rstest] + #[case(SSH_PUBLIC_KEY_RSA)] + #[case(SSH_PUBLIC_KEY_EC_P256)] + #[case(SSH_PUBLIC_KEY_EC_P384)] + #[case(SSH_PUBLIC_KEY_EC_P521)] + #[case(SSH_PUBLIC_KEY_ED25519)] + fn ssh_key_roundtrip(#[case] input: &str) { + let ssh_key: SshPublicKey = input.parse().unwrap(); + let key = PuttyPublicKey::from_openssh(&ssh_key).unwrap(); + let ssh_key2 = key.to_openssh().unwrap(); + + let ssh_key_str = ssh_key2.to_string().unwrap(); + assert_eq!(ssh_key_str, input); + } +} diff --git a/rust/vendor/picky/src/signature.rs b/rust/vendor/picky/src/signature.rs new file mode 100644 index 0000000..732cb50 --- /dev/null +++ b/rust/vendor/picky/src/signature.rs @@ -0,0 +1,633 @@ +//! Signature algorithms supported by picky + +use crate::hash::HashAlgorithm; +use crate::key::ec::{EcComponent, EcCurve, NamedEcCurve}; +use crate::key::{KeyError, PrivateKey, PublicKey}; + +use picky_asn1_x509::{AlgorithmIdentifier, oids}; +use rsa::signature::Signer; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum SignatureError { + /// Key error + #[error("Key error: {source}")] + Key { source: KeyError }, + + /// RSA error + #[error("RSA error: {context}")] + Rsa { context: String }, + + /// EC error + #[error("EC error: {context}")] + Ec { context: String }, + + /// ED error + #[error("ED error: {context}")] + Ed { context: String }, + + /// invalid signature + #[error("invalid signature")] + BadSignature, + + /// unsupported algorithm + #[error("unsupported algorithm: {algorithm}")] + UnsupportedAlgorithm { algorithm: String }, +} + +impl From for SignatureError { + fn from(e: rsa::errors::Error) -> Self { + SignatureError::Rsa { context: e.to_string() } + } +} + +impl From for SignatureError { + fn from(e: rsa::signature::Error) -> Self { + SignatureError::Rsa { context: e.to_string() } + } +} + +impl From for SignatureError { + fn from(e: KeyError) -> Self { + SignatureError::Key { source: e } + } +} + +/// Supported signature algorithms +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum SignatureAlgorithm { + RsaPkcs1v15(HashAlgorithm), + Ecdsa(HashAlgorithm), + Ed25519, +} + +impl TryFrom<&'_ AlgorithmIdentifier> for SignatureAlgorithm { + type Error = SignatureError; + + fn try_from(v: &AlgorithmIdentifier) -> Result { + let oid_string: String = v.oid().into(); + match oid_string.as_str() { + oids::MD5_WITH_RSA_ENCRYPTHION => Ok(Self::RsaPkcs1v15(HashAlgorithm::MD5)), + oids::SHA1_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA1)), + oids::SHA224_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_224)), + oids::SHA256_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_256)), + oids::SHA384_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_384)), + oids::SHA512_WITH_RSA_ENCRYPTION => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA2_512)), + oids::ID_RSASSA_PKCS1_V1_5_WITH_SHA3_384 => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA3_384)), + oids::ID_RSASSA_PKCS1_V1_5_WITH_SHA3_512 => Ok(Self::RsaPkcs1v15(HashAlgorithm::SHA3_512)), + oids::ECDSA_WITH_SHA256 => Ok(Self::Ecdsa(HashAlgorithm::SHA2_256)), + oids::ECDSA_WITH_SHA384 => Ok(Self::Ecdsa(HashAlgorithm::SHA2_384)), + oids::ED25519 => Ok(Self::Ed25519), + _ => Err(SignatureError::UnsupportedAlgorithm { algorithm: oid_string }), + } + } +} + +impl TryFrom for AlgorithmIdentifier { + type Error = SignatureError; + + fn try_from(ty: SignatureAlgorithm) -> Result { + match ty { + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => { + Ok(AlgorithmIdentifier::new_md5_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) => { + Ok(AlgorithmIdentifier::new_sha1_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) => { + Ok(AlgorithmIdentifier::new_sha224_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => { + Ok(AlgorithmIdentifier::new_sha256_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => { + Ok(AlgorithmIdentifier::new_sha384_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => { + Ok(AlgorithmIdentifier::new_sha512_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => { + Ok(AlgorithmIdentifier::new_sha3_384_with_rsa_encryption()) + } + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => { + Ok(AlgorithmIdentifier::new_sha3_512_with_rsa_encryption()) + } + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha256()), + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha384()), + SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512) => Ok(AlgorithmIdentifier::new_ecdsa_with_sha512()), + SignatureAlgorithm::Ecdsa(hash) => { + let msg = format!("ECDSA doesn't support {hash:?} hashing algorithm"); + Err(SignatureError::Ec { context: msg }) + } + SignatureAlgorithm::Ed25519 => Ok(AlgorithmIdentifier::new_ed25519()), + } + } +} + +impl SignatureAlgorithm { + pub fn from_algorithm_identifier(algorithm_identifier: &AlgorithmIdentifier) -> Result { + Self::try_from(algorithm_identifier) + } + + pub fn sign(self, msg: &[u8], private_key: &PrivateKey) -> Result, SignatureError> { + match self { + SignatureAlgorithm::RsaPkcs1v15(picky_hash_algo) => { + use rsa::signature::SignatureEncoding as _; + use rsa::{RsaPrivateKey, pkcs1v15}; + + let rsa_private_key = RsaPrivateKey::try_from(private_key)?; + + let signature = match picky_hash_algo { + HashAlgorithm::MD5 => pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)?, + HashAlgorithm::SHA1 => pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)?, + HashAlgorithm::SHA2_224 => { + pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)? + } + HashAlgorithm::SHA2_256 => { + pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)? + } + HashAlgorithm::SHA2_384 => { + pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)? + } + HashAlgorithm::SHA2_512 => { + pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)? + } + HashAlgorithm::SHA3_384 => { + pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)? + } + HashAlgorithm::SHA3_512 => { + pkcs1v15::SigningKey::::new(rsa_private_key).try_sign(msg)? + } + }; + + Ok(signature.to_vec()) + } + SignatureAlgorithm::Ecdsa(picky_hash_algo) => { + use crate::key::ec::EcdsaKeypair; + use p256::ecdsa::signature::Signer; + + let ec_keypair = EcdsaKeypair::try_from(private_key)?; + + match ec_keypair.curve() { + NamedEcCurve::Known(EcCurve::NistP256) => match picky_hash_algo { + HashAlgorithm::SHA2_256 => { + let secret_validated = + EcCurve::NistP256.validate_component(EcComponent::Secret(ec_keypair.secret()))?; + + let key_bytes = + p256::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| { + SignatureError::Ec { + context: format!( + "validated secret is not the right size(expected: {}, actual: {})", + EcCurve::NistP256.field_bytes_size(), + secret_validated.len(), + ), + } + })?; + let key = + p256::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec { + context: format!("Cannot decode p256 EC keypair: {e}"), + })?; + let sig: p256::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec { + context: format!("Cannot produce p256 signature: {e}"), + })?; + + Ok(sig.to_der().as_bytes().to_vec()) + } + _ => Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!( + "ECDSA P-256 curve with {picky_hash_algo:?} hash algorithm is not supported" + ), + }), + }, + NamedEcCurve::Known(EcCurve::NistP384) => match picky_hash_algo { + HashAlgorithm::SHA2_384 => { + let secret_validated = + EcCurve::NistP384.validate_component(EcComponent::Secret(ec_keypair.secret()))?; + + let key_bytes = + p384::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| { + SignatureError::Ec { + context: format!( + "validated secret is not the right size(expected: {}, actual: {})", + EcCurve::NistP384.field_bytes_size(), + secret_validated.len(), + ), + } + })?; + + let key = + p384::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec { + context: format!("Cannot decode p384 EC keypair: {e}"), + })?; + let sig: p384::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec { + context: format!("Cannot produce p384 signature: {e}"), + })?; + Ok(sig.to_der().as_bytes().to_vec()) + } + _ => Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!( + "ECDSA P-384 curve with {picky_hash_algo:?} hash algorithm is not supported" + ), + }), + }, + NamedEcCurve::Known(EcCurve::NistP521) => match picky_hash_algo { + HashAlgorithm::SHA2_512 => { + let secret_validated = + EcCurve::NistP521.validate_component(EcComponent::Secret(ec_keypair.secret()))?; + + let key_bytes = + p521::elliptic_curve::array::Array::try_from(secret_validated).map_err(|_| { + SignatureError::Ec { + context: format!( + "validated secret is not the right size(expected: {}, actual: {})", + EcCurve::NistP521.field_bytes_size(), + secret_validated.len(), + ), + } + })?; + + let key = + p521::ecdsa::SigningKey::from_bytes(&key_bytes).map_err(|e| SignatureError::Ec { + context: format!("Cannot decode p521 EC keypair: {e}"), + })?; + let sig: p521::ecdsa::Signature = key.try_sign(msg).map_err(|e| SignatureError::Ec { + context: format!("Cannot produce p521 signature: {e}"), + })?; + Ok(sig.to_der().as_bytes().to_vec()) + } + _ => Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!( + "ECDSA P-521 curve with {picky_hash_algo:?} hash algorithm is not supported" + ), + }), + }, + NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "signing").into()), + } + } + SignatureAlgorithm::Ed25519 => { + use crate::key::ed::{EdAlgorithm, EdKeypair, EdPublicKey, NamedEdAlgorithm}; + + let keypair = EdKeypair::try_from(private_key)?; + + let public_key = EdPublicKey::try_from(&keypair)?; + + match keypair.algorithm() { + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => { + let public_key = public_key.data().try_into().map_err(|e| SignatureError::Ed { + context: format!("invalid key size: {e}"), + })?; + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(public_key).map_err( + |e: ed25519_dalek::ed25519::Error| SignatureError::Ed { + context: format!("Cannot decode ed25519 public key: {e}"), + }, + )?; + + let secret_key: ed25519_dalek::SecretKey = + keypair.secret().try_into().map_err(|e| SignatureError::Ed { + context: format!("invalid secret key size: {e}"), + })?; + + let esk = ed25519_dalek::hazmat::ExpandedSecretKey::from(&secret_key); + + let signature = ed25519_dalek::hazmat::raw_sign::(&esk, msg, &verifying_key); + + Ok(signature.to_vec()) + } + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => Err(SignatureError::Ed { + context: "X25519 algorithm is not designed for signing".to_string(), + }), + NamedEdAlgorithm::Unsupported(oid) => { + Err(KeyError::unsupported_ed_algorithm(oid, "signing").into()) + } + } + } + } + } + + pub fn verify(self, public_key: &PublicKey, msg: &[u8], signature: &[u8]) -> Result<(), SignatureError> { + match self { + SignatureAlgorithm::RsaPkcs1v15(picky_hash_algo) => { + use rsa::signature::Verifier as _; + use rsa::{RsaPublicKey, pkcs1v15}; + + let rsa_public_key = RsaPublicKey::try_from(public_key)?; + let signature = pkcs1v15::Signature::try_from(signature)?; + + match picky_hash_algo { + HashAlgorithm::MD5 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA1 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA2_224 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA2_256 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA2_384 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA2_512 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA3_384 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + HashAlgorithm::SHA3_512 => { + pkcs1v15::VerifyingKey::::new(rsa_public_key).verify(msg, &signature) + } + } + .map_err(|_| SignatureError::BadSignature)?; + } + SignatureAlgorithm::Ecdsa(picky_hash_algo) => { + let ec_pub_key = crate::key::ec::EcdsaPublicKey::try_from(public_key)?; + + let curve = match ec_pub_key.curve() { + NamedEcCurve::Known(curve) => curve, + NamedEcCurve::Unsupported(oid) => return Err(KeyError::unsupported_curve(oid, "verifying").into()), + }; + + match picky_hash_algo { + HashAlgorithm::SHA2_256 => { + use p256::ecdsa::signature::Verifier; + + match curve { + EcCurve::NistP256 => {} + curve => { + return Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("SHA256 hash algorithm can't be used with `{curve}` curve"), + }); + } + }; + + let encoded_point = p256::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p256 public key from der bytes: {e}"), + } + })?; + + let vkey = p256::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p256 encoded point: {e}"), + } + })?; + + let signature = + p256::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec { + context: format!("Cannot parse p256 signature: {e}"), + })?; + vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)? + } + HashAlgorithm::SHA2_384 => { + use p384::ecdsa::signature::Verifier; + + match curve { + EcCurve::NistP384 => {} + curve => { + return Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("SHA384 hash algorithm can't be used with `{curve}` curve"), + }); + } + }; + + let encoded_point = p384::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p384 public key from der bytes: {e}"), + } + })?; + + let vkey = p384::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p384 encoded point: {e}"), + } + })?; + + let signature = + p384::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec { + context: format!("Cannot parse p384 signature: {e}"), + })?; + vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)? + } + HashAlgorithm::SHA2_512 => { + use p521::ecdsa::signature::Verifier; + + match curve { + EcCurve::NistP521 => {} + curve => { + return Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("SHA512 hash algorithm can't be used with `{curve}` curve"), + }); + } + }; + + let encoded_point = p521::Sec1Point::from_bytes(ec_pub_key.encoded_point()).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p521 public key from der bytes: {e}"), + } + })?; + + let vkey = p521::ecdsa::VerifyingKey::from_sec1_point(&encoded_point).map_err(|e| { + SignatureError::Ec { + context: format!("Cannot parse p521 encoded point: {e}"), + } + })?; + + let signature = + p521::ecdsa::Signature::from_der(signature).map_err(|e| SignatureError::Ec { + context: format!("Cannot parse p521 signature: {e}"), + })?; + vkey.verify(msg, &signature).map_err(|_| SignatureError::BadSignature)? + } + _ => { + return Err(SignatureError::UnsupportedAlgorithm { + algorithm: format!("ECDSA with {picky_hash_algo:?} hash algorithm is not supported"), + }); + } + } + } + SignatureAlgorithm::Ed25519 => { + use crate::key::ed::{EdAlgorithm, EdPublicKey, NamedEdAlgorithm}; + use ed25519_dalek::Verifier; + + let public_key = EdPublicKey::try_from(public_key)?; + + match public_key.algorithm() { + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => { + let public_key = public_key.data().try_into().map_err(|e| SignatureError::Ed { + context: format!("invalid key size: {e}"), + })?; + let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(public_key).map_err( + |e: ed25519_dalek::ed25519::Error| SignatureError::Ed { + context: format!("Cannot decode ed25519 public key: {e}"), + }, + )?; + + let signature = signature.try_into().map_err(|e| SignatureError::Ed { + context: format!("invalid signature size: {e}"), + })?; + let signature = ed25519_dalek::Signature::from_bytes(signature); + + verifying_key + .verify(msg, &signature) + .map_err(|_| SignatureError::BadSignature)?; + } + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => { + return Err(SignatureError::Ed { + context: "X25519 algorithm is not designed for signing".to_string(), + }); + } + NamedEdAlgorithm::Unsupported(oid) => { + return Err(KeyError::unsupported_ed_algorithm(oid, "verifying").into()); + } + } + } + } + + Ok(()) + } + + pub fn hash_algorithm(&self) -> HashAlgorithm { + match &self { + SignatureAlgorithm::RsaPkcs1v15(hash_algo) => *hash_algo, + SignatureAlgorithm::Ecdsa(hash_algo) => *hash_algo, + SignatureAlgorithm::Ed25519 => HashAlgorithm::SHA2_512, + } + } +} + +#[cfg(test)] +mod ec_tests { + use super::*; + use rstest::*; + + const EC_PRIVATE_KEY_NIST256_PEM: &str = r#"-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICHio5XUa+RbeFfGtGHfbPWehTFJJtCB4/izKHJ9Vm+goAoGCCqGSM49 +AwEHoUQDQgAEh7ZqcI6f0tgqq7nqdcxWM6P4GGCfkWc4q11uXFjtXOKHKCV3LzMY +g8/V1PD/YOh0HodRJAjkjXub8AmYxiTcXw== +-----END EC PRIVATE KEY-----"#; + + const EC_PRIVATE_KEY_NIST384_PEM: &str = r#"-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDDT8VOfdzHbIRaWOO1F0vgotY2qM2FfYS3zpdKE7Vqbh26hFsUw+iaG +GmGnT+29kg+gBwYFK4EEACKhZANiAAQFvVVUKRdN3/bqaEpDA1aHu8FEd3ujuyS0 +AadG6QAiZxH37BGumBcyTTeGHyArqb+GTpsHTUXASbP+P+p5JgkfF9wBMF1SVTvu +ACZOYcqzGbsAXXdMYqewckhc42ye0u0= +-----END EC PRIVATE KEY-----"#; + + #[rstest] + #[case(HashAlgorithm::MD5, false)] + #[case(HashAlgorithm::SHA1, false)] + #[case(HashAlgorithm::SHA2_224, false)] + #[case(HashAlgorithm::SHA2_256, true)] + #[case(HashAlgorithm::SHA2_384, true)] + #[case(HashAlgorithm::SHA2_512, true)] + #[case(HashAlgorithm::SHA3_384, false)] + #[case(HashAlgorithm::SHA3_512, false)] + fn algorithm_identifier_conversions(#[case] hash: HashAlgorithm, #[case] success: bool) { + let signature_algorithm = SignatureAlgorithm::Ecdsa(hash); + let algorithm_identifier = AlgorithmIdentifier::try_from(signature_algorithm); + if success { + assert!(algorithm_identifier.is_ok()); + } else { + assert!(matches!(algorithm_identifier, Err(SignatureError::Ec { context: _ }))); + } + } + + #[test] + fn verify_bad_signature() { + let private_key_signature = PrivateKey::from_pem_str(EC_PRIVATE_KEY_NIST256_PEM).unwrap(); + let signature_algorithm = SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256); + + let msg = b"hello world"; + let signature = signature_algorithm.sign(msg, &private_key_signature).unwrap(); + + let another_ec_private_key_nist256_pem = r#"-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIBVYtZ17YMj89Kuu47TOxJlLVlk7MDUuAlFrVXxexgkSoAoGCCqGSM49 +AwEHoUQDQgAE/irzdOJk28zjVv3sov15/NLIOxoIwL9kM2p/RfQAslATwHpD/T79 +csaQwO9jFvbQFIpCvcMRjaunLfhIWiYDdg== +-----END EC PRIVATE KEY-----"#; + + let another_private_key = PrivateKey::from_pem_str(another_ec_private_key_nist256_pem).unwrap(); + let wrong_public_key = PublicKey::try_from(another_private_key).unwrap(); + assert!(matches!( + signature_algorithm.verify(&wrong_public_key, msg, &signature), + Err(SignatureError::BadSignature) + )); + } + + #[rstest] + #[case(picky_test_data::EC_NIST256_PK_1, HashAlgorithm::SHA2_256, true)] + #[case(picky_test_data::EC_NIST384_PK_1, HashAlgorithm::SHA2_384, true)] + #[case(picky_test_data::EC_NIST521_PK_1, HashAlgorithm::SHA2_512, true)] + fn sign_and_verify(#[case] key_pem: &str, #[case] hash: HashAlgorithm, #[case] sign_successful: bool) { + let private_key = PrivateKey::from_pem_str(key_pem).unwrap(); + + let signature_algorithm = SignatureAlgorithm::Ecdsa(hash); + + let msg = b"hello world"; + let signature = signature_algorithm.sign(msg, &private_key); + assert_eq!(signature.is_ok(), sign_successful); + + if !sign_successful { + return; + } + + let public_key = PublicKey::try_from(private_key).unwrap(); + signature_algorithm + .verify(&public_key, msg, &signature.unwrap()) + .unwrap(); + } + + #[rstest] + #[case(EC_PRIVATE_KEY_NIST256_PEM, &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, HashAlgorithm::SHA2_256)] + #[case(EC_PRIVATE_KEY_NIST384_PEM, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, HashAlgorithm::SHA2_384)] + fn sign_and_verify_compatibility_with_ring( + #[case] key_pem: &str, + #[case] algorithm: &'static ring::signature::EcdsaSigningAlgorithm, + #[case] hash: HashAlgorithm, + ) { + // sign using ring + let private_key = PrivateKey::from_pem_str(key_pem).unwrap(); + let public_key = PublicKey::try_from(private_key).unwrap(); + let msg = b"hello world"; + + let (privk, pubk) = match hash { + HashAlgorithm::SHA2_256 => { + use p256::Sec1Point; + let k = p256::SecretKey::from_sec1_pem(key_pem).unwrap(); + ( + k.to_bytes().as_slice().to_vec(), + Into::::into(k.public_key()).as_bytes().to_vec(), + ) + } + HashAlgorithm::SHA2_384 => { + use p384::Sec1Point; + let k = p384::SecretKey::from_sec1_pem(key_pem).unwrap(); + ( + k.to_bytes().as_slice().to_vec(), + Into::::into(k.public_key()).as_bytes().to_vec(), + ) + } + _ => panic!("no this condition"), + }; + + let rng = ring::rand::SystemRandom::new(); + + let keypair = + ring::signature::EcdsaKeyPair::from_private_key_and_public_key(algorithm, &privk, &pubk, &rng).unwrap(); + + let rng = ring::rand::SystemRandom::new(); + let signature = keypair.sign(&rng, msg).unwrap(); + let sig = signature.as_ref().to_vec(); + + // verify using rust-crypto + let signature_algorithm = SignatureAlgorithm::Ecdsa(hash); + + signature_algorithm.verify(&public_key, msg, &sig).unwrap(); + } +} diff --git a/rust/vendor/picky/src/ssh/certificate.rs b/rust/vendor/picky/src/ssh/certificate.rs new file mode 100644 index 0000000..ce0251b --- /dev/null +++ b/rust/vendor/picky/src/ssh/certificate.rs @@ -0,0 +1,1070 @@ +use crate::hash::HashAlgorithm; +use crate::key::KeyError; +use crate::key::ec::EcdsaPublicKey; +use crate::key::ed::EdPublicKey; +use crate::signature::{SignatureAlgorithm, SignatureError}; +use crate::ssh::EcCurveSshExt as _; +use crate::ssh::decode::SshComplexTypeDecode; +use crate::ssh::encode::{SshComplexTypeEncode, SshWriteExt}; +use crate::ssh::private_key::{SshBasePrivateKey, SshPrivateKey, SshPrivateKeyError}; +use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError}; + +use byteorder::{BigEndian, WriteBytesExt}; +use rand::RngExt; +use rsa::RsaPublicKey; +use rsa::traits::PublicKeyParts as _; +use serde::Deserialize; +use std::cell::RefCell; +use std::convert::TryFrom; +use std::io; +use std::ops::DerefMut; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SshCertificateError { + #[error("Can not process the certificate: {0:?}")] + CertificateProcessingError(#[from] std::io::Error), + #[error("Unsupported certificate type: {0}")] + UnsupportedCertificateType(String), + #[error(transparent)] + SshCriticalOptionError(#[from] SshCriticalOptionError), + #[error(transparent)] + SshExtensionError(#[from] SshExtensionError), + #[error("invalid UTF-8")] + InvalidUtf8, + #[error("Invalid base64 string: {0:?}")] + Base64DecodeError(#[from] base64::DecodeError), + #[error(transparent)] + InvalidCertificateType(#[from] SshCertTypeError), + #[error("Invalid certificate key type: {0}")] + InvalidCertificateKeyType(String), + #[error("Certificate had invalid public key: {0:?}")] + InvalidPublicKey(#[from] SshPublicKeyError), + #[error(transparent)] + RsaError(#[from] rsa::errors::Error), + #[error(transparent)] + KeyError(#[from] KeyError), + #[error(transparent)] + SshSignatureError(#[from] SshSignatureError), +} + +impl From for SshCertificateError { + fn from(_: core::str::Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + +impl From for SshCertificateError { + fn from(_: std::string::FromUtf8Error) -> Self { + Self::InvalidUtf8 + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Deserialize)] +pub enum SshCertType { + Client, + Host, +} + +#[derive(Error, Debug)] +pub enum SshCertTypeError { + #[error("Invalid certificate type. Expected 1(Client) or 2(Host) but got: {0}")] + InvalidCertificateType(u32), + #[error(transparent)] + IoError(#[from] io::Error), +} + +impl TryFrom for SshCertType { + type Error = SshCertTypeError; + + fn try_from(value: u32) -> Result { + match value { + 1 => Ok(SshCertType::Client), + 2 => Ok(SshCertType::Host), + x => Err(SshCertTypeError::InvalidCertificateType(x)), + } + } +} + +impl From for u32 { + fn from(val: SshCertType) -> u32 { + match val { + SshCertType::Client => 1, + SshCertType::Host => 2, + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SshCertKeyType { + SshRsaV01, + SshDssV01, + RsaSha2_256V01, + RsaSha2_512v01, + EcdsaSha2Nistp256V01, + EcdsaSha2Nistp384V01, + EcdsaSha2Nistp521V01, + SshEd25519V01, + SkSshSha2Nistp256V01, + SkSshEd25519V01, +} + +impl SshCertKeyType { + pub fn as_str(&self) -> &str { + match self { + SshCertKeyType::SshRsaV01 => "ssh-rsa-cert-v01@openssh.com", + SshCertKeyType::SshDssV01 => "ssh-dss-cert-v01@openssh.com", + SshCertKeyType::RsaSha2_256V01 => "rsa-sha2-256-cert-v01@openssh.com", + SshCertKeyType::RsaSha2_512v01 => "rsa-sha2-512-cert-v01@openssh.com", + SshCertKeyType::EcdsaSha2Nistp256V01 => "ecdsa-sha2-nistp256-cert-v01@openssh.com", + SshCertKeyType::EcdsaSha2Nistp384V01 => "ecdsa-sha2-nistp384-cert-v01@openssh.com", + SshCertKeyType::EcdsaSha2Nistp521V01 => "ecdsa-sha2-nistp521-cert-v01@openssh.com", + SshCertKeyType::SshEd25519V01 => "ssh-ed25519-cert-v01@openssh.com", + SshCertKeyType::SkSshSha2Nistp256V01 => "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com", + SshCertKeyType::SkSshEd25519V01 => "sk-ssh-ed25519-cert-v01@openssh.com", + } + } +} + +impl TryFrom for SshCertKeyType { + type Error = SshCertificateError; + + fn try_from(value: String) -> Result { + match value.as_str() { + "ssh-rsa-cert-v01@openssh.com" => Ok(SshCertKeyType::SshRsaV01), + "ssh-dss-cert-v01@openssh.com" => Ok(SshCertKeyType::SshDssV01), + "rsa-sha2-256-cert-v01@openssh.com" => Ok(SshCertKeyType::RsaSha2_256V01), + "rsa-sha2-512-cert-v01@openssh.com" => Ok(SshCertKeyType::RsaSha2_512v01), + "ecdsa-sha2-nistp256-cert-v01@openssh.com" => Ok(SshCertKeyType::EcdsaSha2Nistp256V01), + "ecdsa-sha2-nistp384-cert-v01@openssh.com" => Ok(SshCertKeyType::EcdsaSha2Nistp384V01), + "ecdsa-sha2-nistp521-cert-v01@openssh.com" => Ok(SshCertKeyType::EcdsaSha2Nistp521V01), + "ssh-ed25519-cert-v01@openssh.com" => Ok(SshCertKeyType::SshEd25519V01), + "sk-ecdsa-sha2-nistp256-cert-v01@openssh.com" => Ok(SshCertKeyType::SkSshSha2Nistp256V01), + "sk-ssh-ed25519-cert-v01@openssh.com" => Ok(SshCertKeyType::SkSshEd25519V01), + _ => Err(SshCertificateError::InvalidCertificateKeyType(value)), + } + } +} + +#[derive(Error, Debug)] +pub enum SshCriticalOptionError { + #[error("Unsupported critical option type: {0}")] + UnsupportedCriticalOptionType(String), + #[error(transparent)] + IoError(#[from] io::Error), +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum SshCriticalOptionType { + ForceCommand, + SourceAddress, + VerifyRequired, +} + +impl SshCriticalOptionType { + pub fn as_str(&self) -> &str { + match self { + SshCriticalOptionType::ForceCommand => "force-command", + SshCriticalOptionType::SourceAddress => "source-address", + SshCriticalOptionType::VerifyRequired => "verify-required", + } + } +} + +impl TryFrom for SshCriticalOptionType { + type Error = SshCriticalOptionError; + + fn try_from(value: String) -> Result { + match value.as_str() { + "force-command" => Ok(SshCriticalOptionType::ForceCommand), + "source-address" => Ok(SshCriticalOptionType::SourceAddress), + "verify-required" => Ok(SshCriticalOptionType::VerifyRequired), + _ => Err(SshCriticalOptionError::UnsupportedCriticalOptionType(value)), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SshCriticalOption { + pub option_type: SshCriticalOptionType, + pub data: String, +} + +#[derive(Error, Debug)] +pub enum SshExtensionError { + #[error("Unsupported extension type: {0}")] + UnsupportedExtensionType(String), + #[error(transparent)] + IoError(#[from] io::Error), +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SshExtensionType { + NoTouchRequired, + PermitX11Forwarding, + PermitAgentForwarding, + PermitPortForwarding, + PermitPty, + PermitUserPc, +} + +impl SshExtensionType { + pub fn as_str(&self) -> &str { + match self { + SshExtensionType::NoTouchRequired => "no-touch-required", + SshExtensionType::PermitUserPc => "permit-user-rc", + SshExtensionType::PermitPty => "permit-pty", + SshExtensionType::PermitAgentForwarding => "permit-agent-forwarding", + SshExtensionType::PermitPortForwarding => "permit-port-forwarding", + SshExtensionType::PermitX11Forwarding => "permit-X11-forwarding", + } + } +} + +impl TryFrom for SshExtensionType { + type Error = SshExtensionError; + + fn try_from(value: String) -> Result { + match value.as_str() { + "no-touch-required" => Ok(SshExtensionType::NoTouchRequired), + "permit-X11-forwarding" => Ok(SshExtensionType::PermitX11Forwarding), + "permit-agent-forwarding" => Ok(SshExtensionType::PermitAgentForwarding), + "permit-port-forwarding" => Ok(SshExtensionType::PermitPortForwarding), + "permit-pty" => Ok(SshExtensionType::PermitPty), + "permit-user-rc" => Ok(SshExtensionType::PermitUserPc), + _ => Err(SshExtensionError::UnsupportedExtensionType(value)), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SshExtension { + pub extension_type: SshExtensionType, + pub data: String, +} + +impl SshExtension { + pub fn new(extension_type: SshExtensionType, data: String) -> Self { + Self { extension_type, data } + } +} + +#[derive(Error, Debug)] +pub enum SshSignatureError { + #[error("unsupported signature format {0}")] + UnsupportedSignatureFormat(String), + #[error(transparent)] + IoError(#[from] io::Error), +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SshSignatureFormat { + SshRsa, + RsaSha256, + RsaSha512, + EcdsaSha2Nistp256, + EcdsaSha2Nistp384, + EcdsaSha2Nistp521, + SshEd25519, + SkEcdsaSha2NistP256, + SkEd25519, +} + +impl SshSignatureFormat { + pub fn new>(format: T) -> Result { + match format.as_ref() { + "ssh-rsa" => Ok(SshSignatureFormat::SshRsa), + "rsa-sha2-256" => Ok(SshSignatureFormat::RsaSha256), + "rsa-sha2-512" => Ok(SshSignatureFormat::RsaSha512), + "ecdsa-sha2-nistp256" => Ok(SshSignatureFormat::EcdsaSha2Nistp256), + "ecdsa-sha2-nistp384" => Ok(SshSignatureFormat::EcdsaSha2Nistp384), + "ecdsa-sha2-nistp521" => Ok(SshSignatureFormat::EcdsaSha2Nistp521), + "ssh-ed25519" => Ok(SshSignatureFormat::SshEd25519), + "sk-ecdsa-sha2-nistp256@openssh.com" => Ok(SshSignatureFormat::SkEcdsaSha2NistP256), + "sk-ssh-ed25519@openssh.com" => Ok(SshSignatureFormat::SkEd25519), + _ => Err(SshSignatureError::UnsupportedSignatureFormat( + format.as_ref().to_owned(), + )), + } + } + + pub fn as_str(&self) -> &str { + match &self { + SshSignatureFormat::SshRsa => "ssh-rsa", + SshSignatureFormat::RsaSha256 => "rsa-sha2-256", + SshSignatureFormat::RsaSha512 => "rsa-sha2-512", + SshSignatureFormat::EcdsaSha2Nistp256 => "ecdsa-sha2-nistp256", + SshSignatureFormat::EcdsaSha2Nistp384 => "ecdsa-sha2-nistp384", + SshSignatureFormat::EcdsaSha2Nistp521 => "ecdsa-sha2-nistp521", + SshSignatureFormat::SshEd25519 => "ssh-ed25519", + SshSignatureFormat::SkEcdsaSha2NistP256 => "sk-ecdsa-sha2-nistp256@openssh.com", + SshSignatureFormat::SkEd25519 => "sk-ssh-ed25519@openssh.com", + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +enum EcCurveIdentifier { + #[serde(rename = "nistp256")] + Nistp256, + #[serde(rename = "nistp384")] + Nistp384, + #[serde(rename = "nistp521")] + Nistp521, +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum SshSignatureBlob { + Standard(Vec), + Sk { data: Vec, flags: u8, counter: u32 }, +} + +impl SshSignatureBlob { + pub fn size(&self) -> usize { + match self { + SshSignatureBlob::Standard(data) => data.len(), + SshSignatureBlob::Sk { data, .. } => data.len() + 5, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct SshSignature { + pub format: SshSignatureFormat, + pub blob: SshSignatureBlob, +} + +/// Elapsed seconds since UNIX epoch +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] +pub struct Timestamp(pub u64); + +impl Timestamp { + pub fn secs(self) -> u64 { + self.0 + } +} + +impl From for Timestamp { + fn from(v: u64) -> Self { + Self(v) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshCertificate { + pub cert_key_type: SshCertKeyType, + pub public_key: SshPublicKey, + pub nonce: Vec, + pub serial: u64, + pub cert_type: SshCertType, + pub key_id: String, + pub valid_principals: Vec, + pub valid_after: Timestamp, + pub valid_before: Timestamp, + pub critical_options: Vec, + pub extensions: Vec, + pub signature_key: SshPublicKey, + pub signature: SshSignature, + pub comment: String, +} + +impl SshCertificate { + pub fn to_string(&self) -> Result { + let mut buffer = Vec::with_capacity(2048); + self.encode(&mut buffer)?; + Ok(String::from_utf8(buffer)?) + } + + pub fn builder(&self) -> SshCertificateBuilder { + SshCertificateBuilder::init() + } +} + +impl FromStr for SshCertificate { + type Err = SshCertificateError; + + fn from_str(s: &str) -> Result { + SshComplexTypeDecode::decode(s.as_bytes()) + } +} + +#[derive(Debug, Error)] +pub enum SshCertificateGenerationError { + #[error("Unsupported certificate key type: {0}")] + UnsupportedCertificateKeyType(String), + #[error("{0}")] + IncorrectSignatureAlgorithm(String), + #[error("Missing Public key")] + MissingPublicKey, + #[error("Missing certificate type")] + MissingCertificateType, + #[error("Invalid time")] + InvalidTime, + #[error("Missing signature key")] + MissingSignatureKey, + #[error("No extensions are defined for host certificates at present")] + HostCertificateExtensions, + #[error("No critical options are defined for host certificates at present")] + HostCertificateCriticalOptions, + #[error("Key type is required, but it's missing")] + NoKeyType, + #[error(transparent)] + IoError(#[from] io::Error), + #[error(transparent)] + SshPublicKeyError(#[from] SshPublicKeyError), + #[error(transparent)] + SshPrivateKeyError(#[from] SshPrivateKeyError), + #[error(transparent)] + InvalidCertificateKeyType(#[from] SshCertTypeError), + #[error(transparent)] + SshCriticalOptionError(#[from] SshCriticalOptionError), + #[error(transparent)] + SshExtensionError(#[from] SshExtensionError), + #[error(transparent)] + SignatureError(#[from] SignatureError), +} + +#[derive(Debug, Clone, PartialEq, Default)] +struct SshCertificateBuilderInner { + cert_key_type: Option, + public_key: Option, + serial: Option, + cert_type: Option, + key_id: Option, + valid_principals: Option>, + valid_after: Option, + valid_before: Option, + critical_options: Option>, + extensions: Option>, + signature_algo: Option, + signature_key: Option, + comment: Option, +} + +pub struct SshCertificateBuilder { + inner: RefCell, +} + +impl SshCertificateBuilder { + pub fn init() -> Self { + Self { + inner: RefCell::new(SshCertificateBuilderInner::default()), + } + } + + /// Required + pub fn cert_key_type(&self, key_type: SshCertKeyType) -> &Self { + self.inner.borrow_mut().cert_key_type = Some(key_type); + self + } + + /// Required + pub fn key(&self, key: SshPublicKey) -> &Self { + self.inner.borrow_mut().public_key = Some(key); + self + } + + /// Optional (set to 0 by default) + pub fn serial(&self, serial: u64) -> &Self { + self.inner.borrow_mut().serial = Some(serial); + self + } + + /// Required + pub fn cert_type(&self, cert_type: SshCertType) -> &Self { + self.inner.borrow_mut().cert_type = Some(cert_type); + self + } + + /// Optional + pub fn key_id(&self, key_id: String) -> &Self { + self.inner.borrow_mut().key_id = Some(key_id); + self + } + + /// Optional. Zero by default means the certificate is valid for any principal of the specified type. + pub fn principals(&self, principals: Vec) -> &Self { + self.inner.borrow_mut().valid_principals = Some(principals); + self + } + + /// Required + pub fn valid_before(&self, valid_before: impl Into) -> &Self { + self.inner.borrow_mut().valid_before = Some(valid_before.into()); + self + } + + /// Required + pub fn valid_after(&self, valid_after: impl Into) -> &Self { + self.inner.borrow_mut().valid_after = Some(valid_after.into()); + self + } + + /// Optional + pub fn critical_options(&self, critical_options: Vec) -> &Self { + self.inner.borrow_mut().critical_options = Some(critical_options); + self + } + + /// Optional + pub fn extensions(&self, extensions: Vec) -> &Self { + self.inner.borrow_mut().extensions = Some(extensions); + self + } + + /// Required + pub fn signature_key(&self, signature_key: SshPrivateKey) -> &Self { + self.inner.borrow_mut().signature_key = Some(signature_key); + self + } + + /// Optional. RsaPkcs1v15 with SHA256 is used by default. + pub fn signature_algo(&self, signature_algo: SignatureAlgorithm) -> &Self { + self.inner.borrow_mut().signature_algo = Some(signature_algo); + self + } + + /// Optional + pub fn comment(&self, comment: String) -> &Self { + self.inner.borrow_mut().comment = Some(comment); + self + } + + pub fn build(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + + let SshCertificateBuilderInner { + cert_key_type, + public_key, + serial, + cert_type, + key_id, + valid_principals, + valid_after, + valid_before, + critical_options, + extensions, + signature_algo, + signature_key, + comment, + } = inner.deref_mut(); + + let cert_key_type = cert_key_type.ok_or(SshCertificateGenerationError::NoKeyType)?; + match cert_key_type { + SshCertKeyType::SshRsaV01 + | SshCertKeyType::RsaSha2_256V01 + | SshCertKeyType::RsaSha2_512v01 + | SshCertKeyType::EcdsaSha2Nistp256V01 + | SshCertKeyType::EcdsaSha2Nistp384V01 + | SshCertKeyType::SshEd25519V01 + | SshCertKeyType::SkSshSha2Nistp256V01 + | SshCertKeyType::SkSshEd25519V01 => {} + + SshCertKeyType::SshDssV01 | SshCertKeyType::EcdsaSha2Nistp521V01 => { + return Err(SshCertificateGenerationError::UnsupportedCertificateKeyType( + cert_key_type.as_str().to_owned(), + )); + } + } + + let public_key = public_key + .take() + .ok_or(SshCertificateGenerationError::MissingPublicKey)?; + let serial = serial.take().unwrap_or(0); + let cert_type = cert_type + .take() + .ok_or(SshCertificateGenerationError::MissingCertificateType)?; + let key_id = key_id.take().unwrap_or_default(); + + let mut nonce = Vec::new(); + let mut rnd = rand::rng(); + for _ in 0..32 { + nonce.push(rnd.random::()); + } + + let valid_after = valid_after.take().ok_or(SshCertificateGenerationError::InvalidTime)?; + let valid_before = valid_before.take().ok_or(SshCertificateGenerationError::InvalidTime)?; + + if valid_after.secs() > valid_before.secs() { + return Err(SshCertificateGenerationError::InvalidTime); + } + + let valid_principals = valid_principals.take().unwrap_or_default(); + + let mut critical_options = critical_options.take().unwrap_or_default(); + let mut extensions = extensions.take().unwrap_or_default(); + + if cert_type == SshCertType::Host { + if !extensions.is_empty() { + return Err(SshCertificateGenerationError::HostCertificateExtensions); + } + if !critical_options.is_empty() { + return Err(SshCertificateGenerationError::HostCertificateCriticalOptions); + } + } + + if cert_type == SshCertType::Client && extensions.is_empty() { + // set default extensions for user certificate as ssh-keygen does + extensions.extend_from_slice(&[ + SshExtension { + extension_type: SshExtensionType::PermitX11Forwarding, + data: String::new(), + }, + SshExtension { + extension_type: SshExtensionType::PermitAgentForwarding, + data: String::new(), + }, + SshExtension { + extension_type: SshExtensionType::PermitPortForwarding, + data: String::new(), + }, + SshExtension { + extension_type: SshExtensionType::PermitPty, + data: String::new(), + }, + SshExtension { + extension_type: SshExtensionType::PermitUserPc, + data: String::new(), + }, + ]) + } + + // Options and extensions must be lexically ordered by "name" if they appear in the sequence + critical_options + .sort_by(|lhs, rhs| lexical_sort::lexical_cmp(lhs.option_type.as_str(), rhs.option_type.as_str())); + extensions + .sort_by(|lhs, rhs| lexical_sort::lexical_cmp(lhs.extension_type.as_str(), rhs.extension_type.as_str())); + + let signature_algo = signature_algo.take().unwrap_or(match cert_key_type { + SshCertKeyType::EcdsaSha2Nistp256V01 => SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256), + SshCertKeyType::EcdsaSha2Nistp384V01 => SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384), + SshCertKeyType::EcdsaSha2Nistp521V01 => SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_512), + SshCertKeyType::SshEd25519V01 => SignatureAlgorithm::Ed25519, + SshCertKeyType::SkSshEd25519V01 => SignatureAlgorithm::Ed25519, + SshCertKeyType::SkSshSha2Nistp256V01 => SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256), + // Fallback default algorithm + _ => SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + }); + + let signature_key = signature_key + .take() + .ok_or(SshCertificateGenerationError::MissingSignatureKey)?; + let comment = comment.take().unwrap_or_default(); + + let raw_signature = { + let mut buff = Vec::with_capacity(1024); + + buff.write_ssh_string(cert_key_type.as_str()) + .map_err(SshCertificateGenerationError::IoError)?; + + buff.write_ssh_bytes(&nonce) + .map_err(SshCertificateGenerationError::IoError)?; + + match &public_key.inner_key { + SshBasePublicKey::Rsa(rsa) => { + let rsa = RsaPublicKey::try_from(rsa) + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + buff.write_ssh_mpint(rsa.e())?; + buff.write_ssh_mpint(rsa.n())?; + } + SshBasePublicKey::Ec(ec) => { + let ec = EcdsaPublicKey::try_from(ec) + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + let curve_identifier = ec + .curve() + .to_ecdsa_ssh_key_identifier() + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + buff.write_ssh_string(curve_identifier)?; + buff.write_ssh_bytes(ec.encoded_point())?; + } + SshBasePublicKey::Ed(ed) => { + let ed = EdPublicKey::try_from(ed) + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + buff.write_ssh_bytes(ed.data())?; + } + SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => { + let ec = EcdsaPublicKey::try_from(base_key) + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + let curve_identifier = ec + .curve() + .to_ecdsa_ssh_key_identifier() + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + buff.write_ssh_string(curve_identifier)?; + buff.write_ssh_bytes(ec.encoded_point())?; + buff.write_ssh_string(application)?; + } + SshBasePublicKey::SkEd25519 { base_key, application } => { + let ed = EdPublicKey::try_from(base_key) + .map_err(|err| SshCertificateGenerationError::SshPublicKeyError(err.into()))?; + buff.write_ssh_bytes(ed.data())?; + buff.write_ssh_string(application)?; + } + }; + + buff.write_u64::(serial) + .map_err(SshCertificateGenerationError::IoError)?; + + cert_type.encode(&mut buff)?; + + buff.write_ssh_string(&key_id)?; + valid_principals.encode(&mut buff)?; + + valid_after.encode(&mut buff)?; + valid_before.encode(&mut buff)?; + + critical_options.encode(&mut buff)?; + + extensions.encode(&mut buff)?; + + buff.write_ssh_bytes(&[])?; // reserved + + let mut buff2 = Vec::new(); + signature_key.public_key().inner_key.encode(&mut buff2)?; + buff.write_ssh_bytes(&buff2)?; + + buff + }; + + let (signature_blob, signature_format) = match signature_key.base_key() { + SshBasePrivateKey::Rsa(rsa) => { + let signature_format = match signature_algo { + SignatureAlgorithm::RsaPkcs1v15(hash_algo) => match hash_algo { + HashAlgorithm::SHA1 => SshSignatureFormat::SshRsa, + HashAlgorithm::SHA2_256 => SshSignatureFormat::RsaSha256, + HashAlgorithm::SHA2_512 => SshSignatureFormat::RsaSha512, + _ => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm(format!( + "Invalid signature format hash algorithm. Only sha1, sha2-256 and ssh2-521 are in use in OpenSSH for RSA keys, but got {hash_algo:?} hash" + ))); + } + }, + SignatureAlgorithm::Ecdsa(_) => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm( + "ECDSA signature algorithm can't be used with RSA keys".to_owned(), + )); + } + SignatureAlgorithm::Ed25519 => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm( + "Ed25519 signature algorithm can't be used with RSA keys".to_owned(), + )); + } + }; + + let signature = signature_algo.sign(&raw_signature, rsa)?; + (SshSignatureBlob::Standard(signature), signature_format) + } + SshBasePrivateKey::Ec(ec) => { + let signature_format = match signature_algo { + SignatureAlgorithm::Ecdsa(hash_algo) => match hash_algo { + HashAlgorithm::SHA2_256 => SshSignatureFormat::EcdsaSha2Nistp256, + HashAlgorithm::SHA2_384 => SshSignatureFormat::EcdsaSha2Nistp384, + HashAlgorithm::SHA2_512 => SshSignatureFormat::EcdsaSha2Nistp521, + _ => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm(format!( + "Invalid signature format hash algorithm. Only sha2-256, sha2-384 and ssh2-521 are in use in OpenSSH for ECDSA keys, but got {hash_algo:?} hash" + ))); + } + }, + SignatureAlgorithm::RsaPkcs1v15(_) => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm( + "RSA signature algorithm can't be used with ECDSA keys".to_owned(), + )); + } + SignatureAlgorithm::Ed25519 => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm( + "Ed25519 signature algorithm can't be used with ECDSA keys".to_owned(), + )); + } + }; + + let signature = signature_algo.sign(&raw_signature, ec)?; + (SshSignatureBlob::Standard(signature), signature_format) + } + SshBasePrivateKey::Ed(ed) => { + let signature_format = SshSignatureFormat::SshEd25519; + + let signature = signature_algo.sign(&raw_signature, ed)?; + (SshSignatureBlob::Standard(signature), signature_format) + } + SshBasePrivateKey::SkEd25519 { .. } => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm( + "Signing with sk-ed25519 keys is not supported".to_owned(), + )); + } + SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } => { + return Err(SshCertificateGenerationError::IncorrectSignatureAlgorithm( + "Signing with sk-ecdsa keys is not supported".to_owned(), + )); + } + }; + + let signature = SshSignature { + format: signature_format, + blob: signature_blob, + }; + + Ok(SshCertificate { + cert_key_type, + public_key, + nonce, + serial, + cert_type, + key_id, + valid_principals, + valid_after, + valid_before, + critical_options, + extensions, + signature_key: signature_key.public_key, + signature, + comment, + }) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::ssh::private_key::SshPrivateKey; + use rstest::rstest; + use std::time::{SystemTime, UNIX_EPOCH}; + + const PRIVATE_KEY_PEM: &str = "-----BEGIN OPENSSH PRIVATE KEY-----\n\ + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdz\n\ + c2gtcnNhAAAAAwEAAQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq\n\ + /A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx\n\ + 5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4V\n\ + CBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9j\n\ + h+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7\n\ + eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQF\n\ + hzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrh\n\ + peX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xe\n\ + iWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJo\n\ + GWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviK\n\ + H7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56L\n\ + YSsAAAdIA4ihRQOIoUUAAAAHc3NoLXJzYQAAAgEA21AiuHR9Z+HThQb/7I3zJmuK\n\ + uanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwh\n\ + k6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n\n\ + 9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789Lw\n\ + TQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Af\n\ + p0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJ\n\ + lLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3Wg\n\ + NjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDe\n\ + K9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+m\n\ + QMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPcl\n\ + ch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqy\n\ + HLDVL1YwDp8HN56LYSsAAAADAQABAAACAC7OXIqnefhIzx7uDoLLDODfRN05Mlo/\n\ + de/mR967zgo7mBwu2cuBz3e6U2oV9/IXZmHTHt1mkd1/uiQ0Efbkmq3S2FuumGiT\n\ + R2z/QXbUBw6eTntTPZEiTqxQYpRhuPuv/yX1cu7urP9PRLxT8OKIWLR0m0y6Qy7H\n\ + T2GDaqBgX3a4m3/SZumjch7GAYx0hRlkr2Wvxj/xYrM6UBKd0PBD8XxpQZX91ZjQ\n\ + BZ50HmdcVA61UKlZ6L6tdneEU3K0y/jpUKDXBfUOnoa3IR8iVwWPXhB1mBvX2IG2\n\ + FUsTJG9rDUQD6iLsfybWyJkLtrx2TIuQCPsBuep44Tz8SC7s2pLZs0HeihnrM5Ym\n\ + qprMggvZ1TkVFoR3bq/42XO6ULy5k8QPuP6t91UN5iVljgr8H/6Jo9MuCeRA45ZP\n\ + ZN94Cn1mKJWYamrqRuCqDR5za3A0oHPKYUAfzzD90BLL6Yaib75VpiEDTkOiBuW3\n\ + MJUcJsqZipDDl/6eas2Qyloplw60dx42FzcRIDXkXzRNn8hBSy7xmQ5MOKGBszCe\n\ + V/eTBtRITQN38yDVMerb8xDlwOsTtjo3PHCg4HEqqSzjv/B0op9aP7RJ8zp9xLOG\n\ + lxRZ9YhAlHctUOO6ATsv4uCFwCniZbVOdcUEYwNebYQ0x3IRGUF6RpqjOudUwgLl\n\ + o0Lq1KV05fM5AAABAC7fkAB4l5YMAseu+lcj+CwHySzcI+baRFCrMIKldNjEPvvZ\n\ + cCSOU/n5pgp2bw0ulw8c4mFQv0GsG//qQCBX1IrIWO0/nRBjEUTPIe2BUswoxm3+\n\ + F7pirphdIpABKMzV7ZvENn53p2ByrW9+uiwwXLo/z4tH18JW41Jyp5mXH2+1iWIY\n\ + zq5d4gVgMKLGnqWG3DisViHBGg/ExxQCayeXAhlcXVaWZiaVYsgyreaQg58S2RRU\n\ + IveWP+ZAeb8+ZJ72ZjIYLc0GIbP673GpcNWkRlCykTJXF9x+Ts0trffqvSxF+2YJ\n\ + naacLSWJmWFU1BsxUO2pIM4SI8VeHYBdEoAVqcQAAAEBAPUodhyNIr8dtcJona8L\n\ + kn+3BxLdvYAV1bnlWnUcG9m0RQ2L95kH6folOG00aWhRgJHFDoXcCaHND8Mg3PkA\n\ + XYUKCucipiIITyd8YeYnF0ckau5GmUEzwc6s4HcGyFilX1yBoyLE7hFMzOJ4+Rcq\n\ + +zpD2TfaWcuoo+njDWEHeTbzvGIDQoBYsPnGOtw57q9IA5oWYAG3LtwygazmNF2x\n\ + eEnMEtYPyPu7+W0teO0QIJiHWEuK/yLPOb+RHBfA6YJ1f9Jcgc614DxyW6qnB5Yu\n\ + zQBovLzgp/7j9J4Z9F8n8f9PAwYScf7IG8icVVhl5NwNgfNOpcjdg6+YB8Z0AXa4\n\ + dYcAAAEBAOUDEl6yS1nwZ0QsJwfHE232dpsOqxxqfV4ei4R8/obq+b5YPHiUgbt2\n\ + PlHyHtgfQr639BwMmIaAMSR9CLti44Mw6Z3k2DEz3Ef4+XilPeScNiZmWfYanWmV\n\ + wFEtb2c+YT3QweUH3DUAViHL+UdU7xp+zhkrd04daVPpYc9NNN9b9Gwmj6Pm0RP0\n\ + 5UJxsG1ipvN1rGpaCsJiLfS9IoSsKh0Vzdzdty1YvFhEErTl0WBVGGK6xaA5lfMt\n\ + aclWi2mGGNXfWflyQzkz87eYlPe2RhM7jW1Lo9h1BBYE6R+jKt3q0mHwRehj+upd\n\ + AAXJx0RWF7EDQVJtlTfSrUCm+SSFoD0AAAAOdGVzdEBwaWNreS5jb20BAgMEBQ==\n\ + -----END OPENSSH PRIVATE KEY-----"; + + #[test] + fn decode_host_cert() { + let cert = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgxrum49LfnPQE9T+xcClCKuEzSrwNh3M5P6f4uwda6CsAAAADAQABAAACAQCxxwZypEyoP3lq2HfeGiyO7fenoj1txaF4UodcPMMRAyatme6BRy3gobY59IStkhN/oA1QZPVb+uOBpgepZgNPDOMrsODgU0ZxbbYwH/cdGWRoXMYlRZhw1y4KJB5ZVg+pRwrkeNpgP5yrAYuAzjg3GGovEHRDhNGuvANgje/Mr+Ye/YGASUaUaXouPMn4BxoVHM5h7SpWQSXWvy7pszsYAMadGmSnik9Xilrio3I0Z4I51vyxkePwZhKrLUW7tlJES/r3Ezurjz1FW2CniivWtTHDsuM6hLeFPdLZ/Y7yeRpUwmS+21SH/abaxqKvU5dQr1rFs2anXBnPgH2RGXS7a3TznZe0BBccy2uRrvta4eN1pjIL7Olxe8yuea1rygjAn+wb6BFLekYu/GvIPzpf+bw9yVtE51eIkQy5QyqBNJTdRXdKSU5bm8Z4XZcgX5osDG+dpL2SewgLlrxXrAsrSjAeycLKwO+VOUFLMmFO040ZjuAs4Sbw8ptkePdCveU1BFHpWyvf/WG/BmdUzrSwjjVOJT2kguBLiOiH8YAOncCFMLDcHBfd5hFU6jQ5U7CU8HM2wYV8uq1kXtXqmfJ4QJV1D9he8MOJ+u3G4KZR0uNREe5gX7WjvQGT3kql5c8LanDb3rY0Auj9pJd639f7XGN+UYGROuycqvB7BvgQ1wAAAAAAAAAAAAAAAgAAAAVwaWNreQAAACsAAAARZmlyc3QuZXhhbXBsZS5jb20AAAASc2Vjb25kLmV4YW1wbGUuY29tAAAAAGFlVGwAAAAAY0U22QAAAAAAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAMwDtw6lA1R20MaWSHCB/23LYMQvKjiXv2mh3YjsHZZYj9mzoeWmhOF4jjDTB2r6//BuwPIyq+We4AQqbZladmXo1CVPZqtgCa2zCMRfWukj+OvluglSFqgc4fpFyEvbC1o7HA+OGzCcWS7fg2VKNyWnXuVxvPNJhgCo+fzXf3CQyWJ9rO5H6QGKaTtczW7IlZ7WfA1KP/NtCg57QWQzghH2hxTHK+DQN6uGzdIMmddJBklJXkialS+FhSJuWNKAkeN/gwfQ7qgItDUG9hRYvOO7aQbf1u/UQpXtV9jH+KAZrDlRS4/DdSta6G9bHjPfX/sqJYchIdbjLwPvu07Q2Gu6BRVj5qiKxH5VJ1eoHuw6PyV/EJP0nseUK8bspcxZ2ooIxmXbetpBdv5r4Piztw4CPZAap1ZXUhivc8hR/1Q5DhXAHKjtZVQ6nUTqALB27b6lkCUoaOgN/BW//O9Yh/g1uW8le8pzO7y8KsQL1pO9DkutJYQh9dEhVJvYkAHeQVWLTKOIUgGCzaVwh6i9VgwdVgibgqrJPxqJPhA1AEk2Wl+390cU/BfqyDM7/S0ezNoBKSY9dtAOBFE5uBd8PwwdhhnQKbHl+FVyco2A5ncN9bkpQgPlF1Cp+Pi/xQUyrJ3oOxuIszmN7Mhg+b2DiDygqbQ0U/IPpa3AY8QlMnL3AAACFAAAAAxyc2Etc2hhMi01MTIAAAIAaUKPXTKkIouWmHjfhSqV97D3Sh/airfktqVeZTAwjvVkwDcNSswJROfNr8r1Y3RlcFzGI/iFFBjfdoq4kdhMyh+wQs12lkqywj+S96Um9ox846OZwVa43eGuI+aH8D1jUiaFiLJG6+NK0yj4y/i+fHQpS9xveF1T+MsxCnhZ8AMLp0dkokfM1QowXpHHoTJeyg5g2GngxWYZcKogLYo/bVNcL5OoWQwrPDLQeJ+Oumv6HxNb1EOR6QpdQBvrw4mnpfyR1Z8pMNCACFHPCKimvEhfV5xlTtp6N1GH2rDyT8L1iuluMBMBVYmS9MLt2xbY4MJSf2wpvjgyQhhlOlMWjC1/dmaIri+V2qozG5S8Z/Yc0hgigJ8YQl747j7KDA6fSSYzSNogt7x1DLE8Vg6eSHEw05QDPZwBDh7sV+9MKgsZZX0Yb/dXGMEAttDs63YmLL2IqIRFgcJLlsD3fkNxnZvgkppKSw2KVic5PpONwD3DgvRyneVKLUICbh/WhOev90J+UKU/vyHEjrNX4XcJ9uhTc14sWxS5JyRRU48MjrLLQYK1ods6aAIqmOGc6YW3Q4pZFDuwO0dFpNnJPlzeytOObVSk+9ybFF45tJdViU1H7i832o4ifVFVV+jicLB8uy4ov6XG1h4kCeaUzIil90yosg9+qmBzDktkqbocPKc= with a trailing space \n"; + let cert = SshCertificate::from_str(cert).unwrap(); + + assert_eq!(SshCertType::Host, cert.cert_type); + assert_eq!("picky".to_owned(), cert.key_id); + assert_eq!( + vec!["first.example.com".to_owned(), "second.example.com".to_owned()], + cert.valid_principals + ); + assert_eq!("with a trailing space", cert.comment); + assert!(cert.critical_options.is_empty()); + assert!(cert.extensions.is_empty()); + } + + #[test] + fn decode_client_cert() { + let cert = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg0QJyixnKZv3MW8Kc0ny/3BeXWyqSeayV43TO/5jFqLsAAAADAQABAAACAQCv1ucpOue64v3ujEXUqjtgQdL4NBimmBv27qHgoodyODJrIx6OmLtHXBN39hRc5brPb2KYMXTWWHGjtyZ8nOVFc7TWo+M9esgyHerCKz45pjQLRFmmnD/pG28fRafQ3kneKN7aodQ8lti2cRrocNBdqt5TFxzCUV0McE7hNR+XxcAnSAov0P/OxHaUg3EdpKJ5bw3ck5FBY6iGDBfh/wsF+GXWdo9Ic4JfAO29ZhhswnYRgFHiE5AvoGQI3SPM3xof0Sr1F9vjlxYEc8IvYRFV64M/T1+b0Y20LiadPPES/2OcE9dQf3nwqU3lZ577Fkj+l5+NV2ScUSrKfS/2VHcgMz5PnEURHsIO2cjs+XW8je4pDbRi5XUEnHT27WWeADh90GcdRhDFaleK+Zv4JOVfjE3coJ+vJQTNcfHGCcEJ7jIP+5jDpX2haDSK6Y+wMyKLaMp6KSxqVgvCwB95uSgbEe6wnNAJ2y2sC9NkeKSjL3qJHWYmfv15+AOqUt6yzKHrI9TOCcfb2DjA0Vsj8J43CaPOVtfRC27ym4LNBl02mPzli3M7H3L0P36CoO6YFsRfUuY5YWjXbhBJZJXOQWncwrViPQ/9haN+SyO23a54KLIZyob/MbvlZFTZG3XTWMY9HeZGCh7Cmatnn1+4FMfU5/rjvRUr9NilZDwlgYrJwwAAAAAAAAAAAAAAAQAAABFwaWNreUBleGFtcGxlLmNvbQAAABYAAAAJdGVzdC11c2VyAAAABWd1ZXN0AAAAAGFlWZQAAAAAYWarZQAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQC9T+BcFV2flE0HzX00mAQHu4z0VbcnW8MY3JKjC3VjuyfZBYSDHwywgtsZewCA98BFwpZFjdxIv8JQtip+UTpSMHq2cpk1u++2sXxLcS5ySttWbeyXbSJ5dPCOpcZd2NfczxNdYASCK8quAipJpNSwjgnFkT3F3vqTIW8UR5WVOsH0oSewJ9VrIfgX32ZTHCjYMxKDvGENrF4PYfZhg8TIhtEp0LI/barKZepLHjqpN3aZaNTVXVIHd5kglH0OefgK7wbvbLQkZE0F/w2n8hZQ0jni3vBgcZD5yjFSqzTcSgDu4cw87rSNyfNCYyI3oh0JYO72fIGW3Gd63yh0c2XBGHP71vRYOWo597pWs9dp5f+Ii6v8zJAqYOVvM/EdqTplIMFGwYE1Sutb2u9zjNFp0VvBjsui9l5ypf4z4rfrxMU12q/sL8FuaIkrTivrpsNTo//g/maAx+/ivClnKgwP6k+kHRBCFO5Msf5IkVOOHkNqGUhPF2l567Gr0qXgOdtOzfaOHZOQW53KXJd94M21k32Tpaf9Bsg0vTeG1tnOOrl/ejQ2wV2T/ipmQ1oSSThEGh5u7iSWlPe+CXpBzTyyL2EUXYSBt6e29LzAXwQ+xYQih2Y4CEAvS+zWdWHZuxY1e/2m/AqFkZXJ2FO7yqtuGGJyltQPQNpvUbuO+N/YrwAAAhQAAAAMcnNhLXNoYTItNTEyAAACAKmWoCTYqsmWZAnXGyK8WaZZBPLFVvypnwGgKJls0hF6UhlP38XIEiSic4V+1MaD+AqKFd/mIqbzaxJX1PyNzlSqopi92KjPA1VUTHaE5rvsTCLQpkWuR9ys4BI6ku0AXB7V+/H+QAIqkvy0CUMEUbuZWHGUuBSqWQDoZTugzzUgPgeOCmQVRvEm67PW4MQABsJxzSvErz97g/oTJ5/4RC2Ctd3gZ4fhHQgRofW+89aKLf58tRKxtNkq/HMUjy3JJBukFw1QpbmFv/vYjf1MUTV8ESYA0ts+S75xYKFvUWcEa+ylLnMviuqJ4dvhKB6jA5Ircx2F0Ldlj8w3V1OVnYRTZvp98w1Je4MK+NwrqVxAS2F4bP/NkTArQOdiH9NkeF0DiVw85c2M7v6w5etYnG8t9ps8sBMY+nhDppB1Vl6oOok14kkMhfn68ahkBmeSoSjiQNtKBi8ajtOov0DUPYabuFSsqxnV8aj8jM2Aop1a3t5+ihvpmuPh3zjUJ6xY/mUlgnZqbtOOWNq8GqL/VI6YfHJcthmalAkaChEytjtGJutORkTMVmJxqxtHdmldFSzU1+N+/FuAe5AJApDBHcWxYfEjFdzSNSgiBW0b7hdpG7Mc9zIQeh4jpsq6XqgAk1omrKPCJXmQBVeUtPzdc/P4nwbEv/n5DfCzPsVdzNRy sasha@kubuntu \n"; + let cert = SshCertificate::from_str(cert).unwrap(); + + assert_eq!(SshCertType::Client, cert.cert_type); + assert_eq!("picky@example.com".to_owned(), cert.key_id); + assert_eq!(vec!["test-user".to_owned(), "guest".to_owned()], cert.valid_principals); + assert_eq!("sasha@kubuntu", cert.comment); + assert!(cert.critical_options.is_empty()); + assert_eq!( + vec![ + SshExtension::new(SshExtensionType::PermitX11Forwarding, "".to_owned()), + SshExtension::new(SshExtensionType::PermitAgentForwarding, "".to_owned()), + SshExtension::new(SshExtensionType::PermitPortForwarding, "".to_owned()), + SshExtension::new(SshExtensionType::PermitPty, "".to_owned()), + SshExtension::new(SshExtensionType::PermitUserPc, "".to_owned()), + ], + cert.extensions + ); + } + + #[test] + fn encode_host_cert() { + let cert_before = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgxrum49LfnPQE9T+xcClCKuEzSrwNh3M5P6f4uwda6CsAAAADAQABAAACAQCxxwZypEyoP3lq2HfeGiyO7fenoj1txaF4UodcPMMRAyatme6BRy3gobY59IStkhN/oA1QZPVb+uOBpgepZgNPDOMrsODgU0ZxbbYwH/cdGWRoXMYlRZhw1y4KJB5ZVg+pRwrkeNpgP5yrAYuAzjg3GGovEHRDhNGuvANgje/Mr+Ye/YGASUaUaXouPMn4BxoVHM5h7SpWQSXWvy7pszsYAMadGmSnik9Xilrio3I0Z4I51vyxkePwZhKrLUW7tlJES/r3Ezurjz1FW2CniivWtTHDsuM6hLeFPdLZ/Y7yeRpUwmS+21SH/abaxqKvU5dQr1rFs2anXBnPgH2RGXS7a3TznZe0BBccy2uRrvta4eN1pjIL7Olxe8yuea1rygjAn+wb6BFLekYu/GvIPzpf+bw9yVtE51eIkQy5QyqBNJTdRXdKSU5bm8Z4XZcgX5osDG+dpL2SewgLlrxXrAsrSjAeycLKwO+VOUFLMmFO040ZjuAs4Sbw8ptkePdCveU1BFHpWyvf/WG/BmdUzrSwjjVOJT2kguBLiOiH8YAOncCFMLDcHBfd5hFU6jQ5U7CU8HM2wYV8uq1kXtXqmfJ4QJV1D9he8MOJ+u3G4KZR0uNREe5gX7WjvQGT3kql5c8LanDb3rY0Auj9pJd639f7XGN+UYGROuycqvB7BvgQ1wAAAAAAAAAAAAAAAgAAAAVwaWNreQAAACsAAAARZmlyc3QuZXhhbXBsZS5jb20AAAASc2Vjb25kLmV4YW1wbGUuY29tAAAAAGFlVGwAAAAAY0U22QAAAAAAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAMwDtw6lA1R20MaWSHCB/23LYMQvKjiXv2mh3YjsHZZYj9mzoeWmhOF4jjDTB2r6//BuwPIyq+We4AQqbZladmXo1CVPZqtgCa2zCMRfWukj+OvluglSFqgc4fpFyEvbC1o7HA+OGzCcWS7fg2VKNyWnXuVxvPNJhgCo+fzXf3CQyWJ9rO5H6QGKaTtczW7IlZ7WfA1KP/NtCg57QWQzghH2hxTHK+DQN6uGzdIMmddJBklJXkialS+FhSJuWNKAkeN/gwfQ7qgItDUG9hRYvOO7aQbf1u/UQpXtV9jH+KAZrDlRS4/DdSta6G9bHjPfX/sqJYchIdbjLwPvu07Q2Gu6BRVj5qiKxH5VJ1eoHuw6PyV/EJP0nseUK8bspcxZ2ooIxmXbetpBdv5r4Piztw4CPZAap1ZXUhivc8hR/1Q5DhXAHKjtZVQ6nUTqALB27b6lkCUoaOgN/BW//O9Yh/g1uW8le8pzO7y8KsQL1pO9DkutJYQh9dEhVJvYkAHeQVWLTKOIUgGCzaVwh6i9VgwdVgibgqrJPxqJPhA1AEk2Wl+390cU/BfqyDM7/S0ezNoBKSY9dtAOBFE5uBd8PwwdhhnQKbHl+FVyco2A5ncN9bkpQgPlF1Cp+Pi/xQUyrJ3oOxuIszmN7Mhg+b2DiDygqbQ0U/IPpa3AY8QlMnL3AAACFAAAAAxyc2Etc2hhMi01MTIAAAIAaUKPXTKkIouWmHjfhSqV97D3Sh/airfktqVeZTAwjvVkwDcNSswJROfNr8r1Y3RlcFzGI/iFFBjfdoq4kdhMyh+wQs12lkqywj+S96Um9ox846OZwVa43eGuI+aH8D1jUiaFiLJG6+NK0yj4y/i+fHQpS9xveF1T+MsxCnhZ8AMLp0dkokfM1QowXpHHoTJeyg5g2GngxWYZcKogLYo/bVNcL5OoWQwrPDLQeJ+Oumv6HxNb1EOR6QpdQBvrw4mnpfyR1Z8pMNCACFHPCKimvEhfV5xlTtp6N1GH2rDyT8L1iuluMBMBVYmS9MLt2xbY4MJSf2wpvjgyQhhlOlMWjC1/dmaIri+V2qozG5S8Z/Yc0hgigJ8YQl747j7KDA6fSSYzSNogt7x1DLE8Vg6eSHEw05QDPZwBDh7sV+9MKgsZZX0Yb/dXGMEAttDs63YmLL2IqIRFgcJLlsD3fkNxnZvgkppKSw2KVic5PpONwD3DgvRyneVKLUICbh/WhOev90J+UKU/vyHEjrNX4XcJ9uhTc14sWxS5JyRRU48MjrLLQYK1ods6aAIqmOGc6YW3Q4pZFDuwO0dFpNnJPlzeytOObVSk+9ybFF45tJdViU1H7i832o4ifVFVV+jicLB8uy4ov6XG1h4kCeaUzIil90yosg9+qmBzDktkqbocPKc= sasha@kubuntu\r\n"; + let cert: SshCertificate = SshCertificate::from_str(cert_before).unwrap(); + + let cert_after = cert.to_string().unwrap(); + + pretty_assertions::assert_eq!(cert_after, cert_before); + } + + #[test] + fn encode_client_cert() { + let cert_before = "ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg0QJyixnKZv3MW8Kc0ny/3BeXWyqSeayV43TO/5jFqLsAAAADAQABAAACAQCv1ucpOue64v3ujEXUqjtgQdL4NBimmBv27qHgoodyODJrIx6OmLtHXBN39hRc5brPb2KYMXTWWHGjtyZ8nOVFc7TWo+M9esgyHerCKz45pjQLRFmmnD/pG28fRafQ3kneKN7aodQ8lti2cRrocNBdqt5TFxzCUV0McE7hNR+XxcAnSAov0P/OxHaUg3EdpKJ5bw3ck5FBY6iGDBfh/wsF+GXWdo9Ic4JfAO29ZhhswnYRgFHiE5AvoGQI3SPM3xof0Sr1F9vjlxYEc8IvYRFV64M/T1+b0Y20LiadPPES/2OcE9dQf3nwqU3lZ577Fkj+l5+NV2ScUSrKfS/2VHcgMz5PnEURHsIO2cjs+XW8je4pDbRi5XUEnHT27WWeADh90GcdRhDFaleK+Zv4JOVfjE3coJ+vJQTNcfHGCcEJ7jIP+5jDpX2haDSK6Y+wMyKLaMp6KSxqVgvCwB95uSgbEe6wnNAJ2y2sC9NkeKSjL3qJHWYmfv15+AOqUt6yzKHrI9TOCcfb2DjA0Vsj8J43CaPOVtfRC27ym4LNBl02mPzli3M7H3L0P36CoO6YFsRfUuY5YWjXbhBJZJXOQWncwrViPQ/9haN+SyO23a54KLIZyob/MbvlZFTZG3XTWMY9HeZGCh7Cmatnn1+4FMfU5/rjvRUr9NilZDwlgYrJwwAAAAAAAAAAAAAAAQAAABFwaWNreUBleGFtcGxlLmNvbQAAABYAAAAJdGVzdC11c2VyAAAABWd1ZXN0AAAAAGFlWZQAAAAAYWarZQAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQC9T+BcFV2flE0HzX00mAQHu4z0VbcnW8MY3JKjC3VjuyfZBYSDHwywgtsZewCA98BFwpZFjdxIv8JQtip+UTpSMHq2cpk1u++2sXxLcS5ySttWbeyXbSJ5dPCOpcZd2NfczxNdYASCK8quAipJpNSwjgnFkT3F3vqTIW8UR5WVOsH0oSewJ9VrIfgX32ZTHCjYMxKDvGENrF4PYfZhg8TIhtEp0LI/barKZepLHjqpN3aZaNTVXVIHd5kglH0OefgK7wbvbLQkZE0F/w2n8hZQ0jni3vBgcZD5yjFSqzTcSgDu4cw87rSNyfNCYyI3oh0JYO72fIGW3Gd63yh0c2XBGHP71vRYOWo597pWs9dp5f+Ii6v8zJAqYOVvM/EdqTplIMFGwYE1Sutb2u9zjNFp0VvBjsui9l5ypf4z4rfrxMU12q/sL8FuaIkrTivrpsNTo//g/maAx+/ivClnKgwP6k+kHRBCFO5Msf5IkVOOHkNqGUhPF2l567Gr0qXgOdtOzfaOHZOQW53KXJd94M21k32Tpaf9Bsg0vTeG1tnOOrl/ejQ2wV2T/ipmQ1oSSThEGh5u7iSWlPe+CXpBzTyyL2EUXYSBt6e29LzAXwQ+xYQih2Y4CEAvS+zWdWHZuxY1e/2m/AqFkZXJ2FO7yqtuGGJyltQPQNpvUbuO+N/YrwAAAhQAAAAMcnNhLXNoYTItNTEyAAACAKmWoCTYqsmWZAnXGyK8WaZZBPLFVvypnwGgKJls0hF6UhlP38XIEiSic4V+1MaD+AqKFd/mIqbzaxJX1PyNzlSqopi92KjPA1VUTHaE5rvsTCLQpkWuR9ys4BI6ku0AXB7V+/H+QAIqkvy0CUMEUbuZWHGUuBSqWQDoZTugzzUgPgeOCmQVRvEm67PW4MQABsJxzSvErz97g/oTJ5/4RC2Ctd3gZ4fhHQgRofW+89aKLf58tRKxtNkq/HMUjy3JJBukFw1QpbmFv/vYjf1MUTV8ESYA0ts+S75xYKFvUWcEa+ylLnMviuqJ4dvhKB6jA5Ircx2F0Ldlj8w3V1OVnYRTZvp98w1Je4MK+NwrqVxAS2F4bP/NkTArQOdiH9NkeF0DiVw85c2M7v6w5etYnG8t9ps8sBMY+nhDppB1Vl6oOok14kkMhfn68ahkBmeSoSjiQNtKBi8ajtOov0DUPYabuFSsqxnV8aj8jM2Aop1a3t5+ihvpmuPh3zjUJ6xY/mUlgnZqbtOOWNq8GqL/VI6YfHJcthmalAkaChEytjtGJutORkTMVmJxqxtHdmldFSzU1+N+/FuAe5AJApDBHcWxYfEjFdzSNSgiBW0b7hdpG7Mc9zIQeh4jpsq6XqgAk1omrKPCJXmQBVeUtPzdc/P4nwbEv/n5DfCzPsVdzNRy sasha@kubuntu\r\n"; + let cert: SshCertificate = SshCertificate::from_str(cert_before).unwrap(); + + let cert_after = cert.to_string().unwrap(); + + pretty_assertions::assert_eq!(cert_before, cert_after); + } + + #[rstest] + #[case(picky_test_data::SSH_CERT_EC_P256)] + #[case(picky_test_data::SSH_CERT_EC_P384)] + fn ecdsa_roundtrip(#[case] cert_before: &str) { + let cert: SshCertificate = SshCertificate::from_str(cert_before).unwrap(); + let cert_after = cert.to_string().unwrap(); + pretty_assertions::assert_eq!(cert_before, cert_after); + } + + #[test] + fn ed25519_roundtrip() { + let cert: SshCertificate = SshCertificate::from_str(picky_test_data::SSH_CERT_ED25519).unwrap(); + let cert_after = cert.to_string().unwrap(); + pretty_assertions::assert_eq!(picky_test_data::SSH_CERT_ED25519, cert_after); + } + + #[test] + fn sk_ed25519_signed_roundtrip() { + let cert: SshCertificate = SshCertificate::from_str(picky_test_data::SSH_CERT_SK_ED25519).unwrap(); + let cert_after = cert.to_string().unwrap(); + pretty_assertions::assert_eq!(picky_test_data::SSH_CERT_SK_ED25519, cert_after); + } + + #[test] + fn sk_ecdsa_signed_roundtrip() { + let cert: SshCertificate = SshCertificate::from_str(picky_test_data::SSH_CERT_SK_ECDSA).unwrap(); + let cert_after = cert.to_string().unwrap(); + pretty_assertions::assert_eq!(picky_test_data::SSH_CERT_SK_ECDSA, cert_after); + } + + #[test] + fn sk_ed25519_cert_roundtrip() { + let cert: SshCertificate = SshCertificate::from_str(picky_test_data::SSH_CERT_SK_ED25519_SIG_EC).unwrap(); + let cert_after = cert.to_string().unwrap(); + pretty_assertions::assert_eq!(picky_test_data::SSH_CERT_SK_ED25519_SIG_EC, cert_after); + } + + #[test] + fn sk_ecdsa_cert_roundtrip() { + let cert: SshCertificate = SshCertificate::from_str(picky_test_data::SSH_CERT_SK_ECDSA_SIG_EC).unwrap(); + let cert_after = cert.to_string().unwrap(); + pretty_assertions::assert_eq!(picky_test_data::SSH_CERT_SK_ECDSA_SIG_EC, cert_after); + } + + #[rstest] + #[case(SshCertKeyType::EcdsaSha2Nistp256V01, picky_test_data::SSH_PRIVATE_KEY_EC_P256)] + #[case(SshCertKeyType::RsaSha2_256V01, PRIVATE_KEY_PEM)] + #[case(SshCertKeyType::SshEd25519V01, picky_test_data::SSH_PRIVATE_KEY_ED25519)] + fn test_certificate_generation(#[case] key_type: SshCertKeyType, #[case] ssh_key_pem: &str) { + let certificate_builder = SshCertificateBuilder::init(); + certificate_builder.cert_key_type(key_type); + let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_key_pem, None).unwrap(); + certificate_builder.key(private_key.public_key().clone()); + certificate_builder.cert_type(SshCertType::Host); + let now_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + certificate_builder.valid_after(now_timestamp); + // 10 minutes = 600 seconds + let valid_before = now_timestamp + 600; + certificate_builder.valid_before(valid_before); + certificate_builder.signature_key(private_key); + let cert = certificate_builder.build().unwrap(); + // Check that we could parse this certificate after building it + let serialized = cert.to_string().unwrap(); + SshCertificate::from_str(&serialized).unwrap(); + } + + #[test] + fn test_time_validation_in_certificate_builder() { + let certificate_builder = SshCertificateBuilder::init(); + + certificate_builder.cert_key_type(SshCertKeyType::RsaSha2_256V01); + + let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(PRIVATE_KEY_PEM, None).unwrap(); + certificate_builder.key(private_key.public_key().clone()); + + certificate_builder.cert_type(SshCertType::Host); + + let now_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + // 10 minutes = 600 seconds + let after = now_timestamp + 600; + let before = now_timestamp - 600; + + certificate_builder.valid_after(after); + + certificate_builder.valid_before(before); + + certificate_builder.signature_key(private_key); + + let cert = certificate_builder.build(); + assert!(matches!(cert.unwrap_err(), SshCertificateGenerationError::InvalidTime)); + } + + #[test] + fn test_host_certificate_generation() { + let certificate_builder = SshCertificateBuilder::init(); + + certificate_builder.cert_key_type(SshCertKeyType::RsaSha2_256V01); + + let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(PRIVATE_KEY_PEM, None).unwrap(); + certificate_builder.key(private_key.public_key().clone()); + + certificate_builder.cert_type(SshCertType::Host); + + let now_timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + certificate_builder.valid_after(now_timestamp); + + // 10 minutes = 600 seconds + let valid_before = now_timestamp + 600; + certificate_builder.valid_before(valid_before); + + certificate_builder.signature_key(private_key); + + certificate_builder.principals(vec!["example".to_owned()]); + + certificate_builder.extensions(vec![SshExtension::new( + SshExtensionType::NoTouchRequired, + "".to_owned(), + )]); + + let cert = certificate_builder.build(); + assert!(matches!( + cert.unwrap_err(), + SshCertificateGenerationError::HostCertificateExtensions + )); + } +} diff --git a/rust/vendor/picky/src/ssh/decode.rs b/rust/vendor/picky/src/ssh/decode.rs new file mode 100644 index 0000000..7a46782 --- /dev/null +++ b/rust/vendor/picky/src/ssh/decode.rs @@ -0,0 +1,595 @@ +use crate::key::ec::{EcCurve, NamedEcCurve}; +use crate::key::ed::NamedEdAlgorithm; +use crate::key::{EdAlgorithm, PrivateKey, PublicKey}; +use crate::ssh::certificate::{ + SshCertKeyType, SshCertType, SshCertTypeError, SshCertificate, SshCertificateError, SshCriticalOption, + SshCriticalOptionError, SshCriticalOptionType, SshExtension, SshExtensionError, SshExtensionType, SshSignature, + SshSignatureError, SshSignatureFormat, Timestamp, +}; +use crate::ssh::private_key::{KdfOption, SshBasePrivateKey, SshPrivateKeyError}; +use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError}; +use crate::ssh::{Base64Reader, SSH_COMBO_ED25519_KEY_LENGTH, key_type, read_until_linebreak, read_until_whitespace}; + +use super::certificate::SshSignatureBlob; +use base64::engine::general_purpose; +use byteorder::{BigEndian, ReadBytesExt}; +use crypto_bigint::BoxedUint; +use picky_asn1_x509::oid::ObjectIdentifier; +use picky_asn1_x509::oids; +use std::io::{self, Cursor, Read}; + +pub trait SshReadExt { + type Error; + + fn read_ssh_string(&mut self) -> Result; + fn read_ssh_bytes(&mut self) -> Result, Self::Error>; + fn read_ssh_mpint(&mut self) -> Result; +} + +impl SshReadExt for T +where + T: Read, +{ + type Error = io::Error; + + fn read_ssh_string(&mut self) -> Result { + let size = self.read_u32::()? as usize; + let mut buffer = vec![0; size]; + self.read_exact(&mut buffer)?; + + Ok(String::from_utf8_lossy(&buffer).into_owned()) + } + + fn read_ssh_bytes(&mut self) -> Result, Self::Error> { + let size = self.read_u32::()? as usize; + let mut buffer = vec![0; size]; + self.read_exact(&mut buffer)?; + + Ok(buffer) + } + + fn read_ssh_mpint(&mut self) -> Result { + let size = self.read_u32::()? as usize; + let mut buffer = vec![0; size]; + self.read_exact(&mut buffer)?; + + if buffer[0] == 0 { + buffer.remove(0); + } + + Ok(BoxedUint::from_be_slice_vartime(&buffer)) + } +} + +pub trait SshComplexTypeDecode: Sized { + type Error; + + fn decode(stream: impl Read) -> Result; +} + +impl SshComplexTypeDecode for SshCertType { + type Error = SshCertTypeError; + + fn decode(mut stream: impl Read) -> Result { + SshCertType::try_from(stream.read_u32::()?) + } +} + +impl SshComplexTypeDecode for SshCriticalOption { + type Error = SshCriticalOptionError; + + fn decode(mut stream: impl Read) -> Result { + let option_type: String = stream.read_ssh_string()?; + let data: String = stream.read_ssh_string()?; + Ok(SshCriticalOption { + option_type: SshCriticalOptionType::try_from(option_type)?, + data, + }) + } +} + +impl SshComplexTypeDecode for Vec +where + T: SshComplexTypeDecode, + T::Error: From, +{ + type Error = T::Error; + + fn decode(mut stream: impl Read) -> Result { + let data = stream.read_ssh_bytes()?; + let len = data.len() as u64; + let mut cursor = Cursor::new(data); + let mut res = Vec::new(); + while cursor.position() < len { + let elem: Result = SshComplexTypeDecode::decode(&mut cursor); + res.push(elem?); + } + Ok(res) + } +} + +impl SshComplexTypeDecode for SshExtension { + type Error = SshExtensionError; + + fn decode(mut stream: impl Read) -> Result { + let extension_type = stream.read_ssh_string()?; + let data = stream.read_ssh_string()?; + Ok(SshExtension { + extension_type: SshExtensionType::try_from(extension_type)?, + data, + }) + } +} + +impl SshComplexTypeDecode for Vec { + type Error = io::Error; + + fn decode(mut stream: impl Read) -> Result { + let data = stream.read_ssh_bytes()?; + let len = data.len(); + let mut cursor = Cursor::new(data); + let mut res = Vec::new(); + while cursor.position() < len as u64 { + res.push(cursor.read_ssh_string()?); + } + Ok(res) + } +} + +impl SshComplexTypeDecode for SshSignature { + type Error = SshSignatureError; + + fn decode(mut stream: impl Read) -> Result { + let _overall_size = stream.read_u32::()?; + + let format = SshSignatureFormat::new(stream.read_ssh_string()?.as_str())?; + let data = stream.read_ssh_bytes()?; + + match format { + SshSignatureFormat::SkEd25519 | SshSignatureFormat::SkEcdsaSha2NistP256 => { + let flags = stream.read_u8()?; + let counter = stream.read_u32::()?; + + Ok(SshSignature { + format, + blob: SshSignatureBlob::Sk { data, flags, counter }, + }) + } + _ => Ok(SshSignature { + format, + blob: SshSignatureBlob::Standard(data), + }), + } + } +} + +impl SshComplexTypeDecode for KdfOption { + type Error = io::Error; + + fn decode(mut stream: impl Read) -> Result { + let data = stream.read_ssh_bytes()?; + if data.is_empty() { + return Ok(KdfOption::default()); + } + let mut data = data.as_slice(); + let salt = data.read_ssh_bytes()?; + let rounds = data.read_u32::()?; + Ok(KdfOption { salt, rounds }) + } +} + +impl SshComplexTypeDecode for Timestamp { + type Error = io::Error; + + fn decode(mut stream: impl Read) -> Result { + let timestamp = stream.read_u64::()?; + let time = Timestamp::from(timestamp); + Ok(time) + } +} + +impl SshComplexTypeDecode for SshBasePublicKey { + type Error = SshPublicKeyError; + + fn decode(mut stream: impl Read) -> Result { + let key_type = stream.read_ssh_string()?; + match key_type.as_str() { + key_type::RSA => { + let e = stream.read_ssh_mpint()?; + let n = stream.read_ssh_mpint()?; + Ok(SshBasePublicKey::Rsa(PublicKey::from_rsa_components(&n, &e))) + } + key_type::ECDSA_SHA2_NIST_P256 | key_type::ECDSA_SHA2_NIST_P384 | key_type::ECDSA_SHA2_NIST_P521 => { + let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?; + Ok(SshBasePublicKey::Ec(PublicKey::from_ec_encoded_components( + &curve.into(), + &point, + ))) + } + key_type::ED25519 => { + let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?; + + Ok(SshBasePublicKey::Ed(PublicKey::from_ed_encoded_components( + &algorithm.into(), + &public_key, + ))) + } + key_type::SK_ECDSA_SHA2_NIST_P256 => { + let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?; + let base_key = PublicKey::from_ec_encoded_components(&curve.into(), &point); + let application = stream.read_ssh_string()?; + + Ok(SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application }) + } + key_type::SK_ED25519 => { + let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?; + let base_key = PublicKey::from_ed_encoded_components(&algorithm.into(), &public_key); + let application = stream.read_ssh_string()?; + + Ok(SshBasePublicKey::SkEd25519 { base_key, application }) + } + _ => Err(SshPublicKeyError::UnknownKeyType), + } + } +} + +fn decode_ed25519_public_key_body_impl( + key_type: &str, + stream: &mut impl Read, +) -> Result<(NamedEdAlgorithm, Vec), SshPublicKeyError> { + let algorithm = match key_type { + key_type::ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519), + key_type::SK_ED25519 => NamedEdAlgorithm::Known(EdAlgorithm::Ed25519), + _ => { + return Err(SshPublicKeyError::UnknownKeyType); + } + }; + let public_key = stream.read_ssh_bytes()?; + Ok((algorithm, public_key)) +} + +fn decode_ec_public_key_body_impl( + key_type: &str, + stream: &mut impl Read, +) -> Result<(NamedEcCurve, Vec), SshPublicKeyError> { + let curve = match key_type { + key_type::ECDSA_SHA2_NIST_P256 => NamedEcCurve::Known(EcCurve::NistP256), + key_type::ECDSA_SHA2_NIST_P384 => NamedEcCurve::Known(EcCurve::NistP384), + key_type::ECDSA_SHA2_NIST_P521 => NamedEcCurve::Unsupported(oids::secp521r1()), + key_type::SK_ECDSA_SHA2_NIST_P256 => NamedEcCurve::Known(EcCurve::NistP256), + _ => { + return Err(SshPublicKeyError::UnknownKeyType); + } + }; + + // Duplicated information about key type + let _identifier = stream.read_ssh_string()?; + + // Public key encoded from an elliptic curve point into an + // octet string as per [RFC](https://datatracker.ietf.org/doc/html/rfc5656#section-3.1). + let point_data = stream.read_ssh_bytes()?; + + Ok((curve, point_data)) +} + +impl SshComplexTypeDecode for SshPublicKey { + type Error = SshPublicKeyError; + + fn decode(mut stream: impl Read) -> Result { + let mut buffer = Vec::with_capacity(1024); + + read_until_whitespace(&mut stream, &mut buffer)?; + + let header = String::from_utf8_lossy(&buffer).into_owned(); + buffer.clear(); + + let inner_key = match header.as_str() { + key_type::RSA + | key_type::ECDSA_SHA2_NIST_P256 + | key_type::ECDSA_SHA2_NIST_P384 + | key_type::ECDSA_SHA2_NIST_P521 + | key_type::ED25519 + | key_type::SK_ECDSA_SHA2_NIST_P256 + | key_type::SK_ED25519 => { + read_until_whitespace(&mut stream, &mut buffer)?; + let mut slice = buffer.as_slice(); + let decoder = Base64Reader::new(&mut slice, &general_purpose::STANDARD); + SshComplexTypeDecode::decode(decoder)? + } + _ => return Err(SshPublicKeyError::UnknownKeyType), + }; + + buffer.clear(); + read_until_linebreak(&mut stream, &mut buffer)?; + let comment = core::str::from_utf8(&buffer)?.trim_end().to_owned(); + + Ok(SshPublicKey { inner_key, comment }) + } +} + +impl SshComplexTypeDecode for SshBasePrivateKey { + type Error = SshPrivateKeyError; + + fn decode(mut stream: impl Read) -> Result { + let key_type = stream.read_ssh_string()?; + match key_type.as_str() { + key_type::RSA => { + let n_constant = stream.read_ssh_mpint()?; + let e_constant = stream.read_ssh_mpint()?; + let d_constant = stream.read_ssh_mpint()?; + let _iqmp = stream.read_ssh_mpint()?; + let p_constant = stream.read_ssh_mpint()?; + let q_constant = stream.read_ssh_mpint()?; + + Ok(SshBasePrivateKey::Rsa(PrivateKey::from_rsa_components( + &n_constant, + &e_constant, + &d_constant, + &[p_constant, q_constant], + )?)) + } + key_type::ECDSA_SHA2_NIST_P256 | key_type::ECDSA_SHA2_NIST_P384 | key_type::ECDSA_SHA2_NIST_P521 => { + let (curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?; + + let private_key_secret = stream.read_ssh_mpint()?.to_be_bytes_trimmed_vartime(); + + Ok(SshBasePrivateKey::Ec(PrivateKey::from_ec_encoded_components( + curve.into(), + &private_key_secret, + Some(point.as_slice()), + ))) + } + key_type::ED25519 => { + let (algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?; + + let private_key_secret = stream.read_ssh_mpint()?.to_be_bytes_trimmed_vartime(); + + // OpenSSH is really strange in regards to private ed25519 keys. It stores them as + // 64 byte-array, but actually only first 32 bytes are the private key, and the rest + // is public key copy + if private_key_secret.len() != SSH_COMBO_ED25519_KEY_LENGTH { + return Err(SshPrivateKeyError::InvalidKeyFormat); + } + + let private_key_secret = &private_key_secret[..ed25519_dalek::SECRET_KEY_LENGTH]; + + Ok(SshBasePrivateKey::Ed(PrivateKey::from_ed_encoded_components( + algorithm.into(), + private_key_secret, + Some(&public_key), + ))) + } + key_type::SK_ECDSA_SHA2_NIST_P256 => { + let (_curve, point) = decode_ec_public_key_body_impl(key_type.as_str(), &mut stream)?; + + let application = stream.read_ssh_string()?; + let flags = stream.read_u8()?; + let handle = stream.read_ssh_bytes()?; + let _reserved = stream.read_ssh_bytes()?; + + Ok(SshBasePrivateKey::SkEcdsaSha2NistP256 { + public_key: PublicKey::from_ec_encoded_components( + &ObjectIdentifier::from(NamedEcCurve::Known(EcCurve::NistP256)), + &point, + ), + application, + flags, + handle, + }) + } + key_type::SK_ED25519 => { + let (_algorithm, public_key) = decode_ed25519_public_key_body_impl(key_type.as_str(), &mut stream)?; + + let application = stream.read_ssh_string()?; + let flags = stream.read_u8()?; + let handle = stream.read_ssh_bytes()?; + let _reserved = stream.read_ssh_bytes()?; + + Ok(SshBasePrivateKey::SkEd25519 { + public_key: PublicKey::from_ed_encoded_components( + &ObjectIdentifier::from(EdAlgorithm::Ed25519), + &public_key, + ), + application, + flags, + handle, + }) + } + key_type => Err(SshPrivateKeyError::UnsupportedKeyType(key_type.to_owned())), + } + } +} + +impl SshComplexTypeDecode for SshCertificate { + type Error = SshCertificateError; + + fn decode(mut stream: impl Read) -> Result { + let mut cert_type = Vec::new(); + read_until_whitespace(&mut stream, &mut cert_type)?; + + let _ = SshCertKeyType::try_from(String::from_utf8(cert_type)?)?; + + let mut cert_data = Vec::new(); + read_until_whitespace(&mut stream, &mut cert_data)?; + + let mut cert_data = cert_data.as_slice(); + let mut cert_data = Base64Reader::new(&mut cert_data, &general_purpose::STANDARD); + + let cert_key_type = cert_data.read_ssh_string()?; + let cert_key_type = SshCertKeyType::try_from(cert_key_type)?; + + let nonce = cert_data.read_ssh_bytes()?; + + let inner_public_key = match &cert_key_type { + SshCertKeyType::SshRsaV01 | SshCertKeyType::RsaSha2_256V01 | SshCertKeyType::RsaSha2_512v01 => { + let e = cert_data.read_ssh_mpint()?; + let n = cert_data.read_ssh_mpint()?; + SshBasePublicKey::Rsa(PublicKey::from_rsa_components(&n, &e)) + } + SshCertKeyType::EcdsaSha2Nistp256V01 + | SshCertKeyType::EcdsaSha2Nistp384V01 + | SshCertKeyType::EcdsaSha2Nistp521V01 => { + let curve = match cert_key_type { + SshCertKeyType::EcdsaSha2Nistp256V01 => NamedEcCurve::Known(EcCurve::NistP256), + SshCertKeyType::EcdsaSha2Nistp384V01 => NamedEcCurve::Known(EcCurve::NistP384), + SshCertKeyType::EcdsaSha2Nistp521V01 => NamedEcCurve::Known(EcCurve::NistP521), + _ => unreachable!("Already validated in match above"), + }; + + let _curve_identifier = cert_data.read_ssh_string()?; + + let public_key_data = cert_data.read_ssh_bytes()?; + SshBasePublicKey::Ec(PublicKey::from_ec_encoded_components(&curve.into(), &public_key_data)) + } + SshCertKeyType::SshEd25519V01 => { + let algorithm = NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(); + + let public_key_data = cert_data.read_ssh_bytes()?; + SshBasePublicKey::Ed(PublicKey::from_ed_encoded_components(&algorithm, &public_key_data)) + } + SshCertKeyType::SshDssV01 => { + return Err(SshCertificateError::UnsupportedCertificateType( + cert_key_type.as_str().to_owned(), + )); + } + SshCertKeyType::SkSshSha2Nistp256V01 => { + let _curve_identifier = cert_data.read_ssh_string()?; + let public_key_data = cert_data.read_ssh_bytes()?; + let application = cert_data.read_ssh_string()?; + + SshBasePublicKey::SkEcdsaSha2NistP256 { + base_key: PublicKey::from_ec_encoded_components( + &NamedEcCurve::Known(EcCurve::NistP256).into(), + &public_key_data, + ), + application, + } + } + SshCertKeyType::SkSshEd25519V01 => { + let public_key_data = cert_data.read_ssh_bytes()?; + let application = cert_data.read_ssh_string()?; + + SshBasePublicKey::SkEd25519 { + base_key: PublicKey::from_ed_encoded_components( + &NamedEdAlgorithm::Known(EdAlgorithm::Ed25519).into(), + &public_key_data, + ), + application, + } + } + }; + + let serial = cert_data.read_u64::()?; + let cert_type: SshCertType = SshComplexTypeDecode::decode(&mut cert_data)?; + + let key_id = cert_data.read_ssh_string()?; + + let valid_principals: Vec = SshComplexTypeDecode::decode(&mut cert_data)?; + + let valid_after: Timestamp = SshComplexTypeDecode::decode(&mut cert_data)?; + let valid_before: Timestamp = SshComplexTypeDecode::decode(&mut cert_data)?; + + let critical_options: Vec = SshComplexTypeDecode::decode(&mut cert_data)?; + + let extensions: Vec = SshComplexTypeDecode::decode(&mut cert_data)?; + + let _ = cert_data.read_ssh_bytes()?; // reserved + + // here is public key + let signature_key = cert_data.read_ssh_bytes()?; + let signature_public_key: SshBasePublicKey = SshComplexTypeDecode::decode(signature_key.as_slice())?; + + let signature = SshSignature::decode(cert_data)?; + + let mut comment = Vec::new(); + read_until_linebreak(&mut stream, &mut comment)?; + let comment = core::str::from_utf8(&comment)?.trim_end().to_owned(); + + Ok(SshCertificate { + cert_key_type, + public_key: SshPublicKey { + inner_key: inner_public_key, + comment: String::new(), + }, + nonce, + serial, + cert_type, + key_id, + valid_principals, + valid_after, + valid_before, + critical_options, + extensions, + signature_key: SshPublicKey { + inner_key: signature_public_key, + comment: String::new(), + }, + signature, + comment, + }) + } +} + +#[cfg(test)] +mod test { + use super::SshReadExt; + use std::io::Cursor; + + #[test] + fn ssh_string_decode() { + let mut cursor = Cursor::new([0, 0, 0, 5, 112, 105, 99, 107, 121].to_vec()); + + let ssh_string = cursor.read_ssh_string().unwrap(); + + assert_eq!(5, ssh_string.len()); + assert_eq!("picky".to_owned(), ssh_string); + assert_eq!(9, cursor.position()); + + let mut cursor = Cursor::new([0, 0, 0, 0].to_vec()); + + let ssh_string = cursor.read_ssh_string().unwrap(); + + assert_eq!(0, ssh_string.len()); + assert_eq!("".to_owned(), ssh_string); + assert_eq!(4, cursor.position()); + } + + #[test] + fn byte_array_decode() { + let mut cursor = Cursor::new([0, 0, 0, 5, 1, 2, 3, 4, 5].to_vec()); + + let byte_array = cursor.read_ssh_bytes().unwrap(); + + assert_eq!(5, byte_array.len()); + assert_eq!([1, 2, 3, 4, 5].to_vec(), byte_array); + assert_eq!(9, cursor.position()); + + let mut cursor = Cursor::new([0, 0, 0, 0].to_vec()); + + let byte_array = cursor.read_ssh_bytes().unwrap(); + + assert_eq!(0, byte_array.len()); + assert_eq!(Vec::::new(), byte_array); + assert_eq!(4, cursor.position()); + } + + #[test] + fn mpint_decoding() { + let mut cursor = Cursor::new(vec![ + 0x00, 0x00, 0x00, 0x08, 0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7, + ]); + let mpint = cursor.read_ssh_mpint().unwrap(); + assert_eq!( + mpint.to_be_bytes_trimmed_vartime().as_ref(), + &[0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7] + ); + + let mut cursor = Cursor::new(vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x80]); + let mpint = cursor.read_ssh_mpint().unwrap(); + assert_eq!(mpint.to_be_bytes_trimmed_vartime().as_ref(), [0x80]); + + let mut cursor = Cursor::new(vec![0x00, 0x00, 0x00, 0x02, 0xed, 0xcc]); + let mpint = cursor.read_ssh_mpint().unwrap(); + assert_eq!(mpint.to_be_bytes_trimmed_vartime().as_ref(), &[0xed, 0xcc]); + } +} diff --git a/rust/vendor/picky/src/ssh/encode.rs b/rust/vendor/picky/src/ssh/encode.rs new file mode 100644 index 0000000..06cb077 --- /dev/null +++ b/rust/vendor/picky/src/ssh/encode.rs @@ -0,0 +1,574 @@ +use crate::key::ec::{EcdsaKeypair, EcdsaPublicKey}; +use crate::key::ed::{EdKeypair, EdPublicKey}; +use crate::ssh::certificate::{ + SshCertType, SshCertTypeError, SshCertificate, SshCertificateError, SshCriticalOption, SshCriticalOptionError, + SshExtension, SshExtensionError, SshSignature, SshSignatureError, Timestamp, +}; +use crate::ssh::private_key::{ + AES256_CTR, AUTH_MAGIC, Aes256Ctr, BCRYPT, KdfOption, NONE, SshBasePrivateKey, SshPrivateKey, SshPrivateKeyError, +}; +use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError}; +use crate::ssh::{Base64Writer, EcCurveSshExt as _, EdAlgorithmSshExt as _, SSH_COMBO_ED25519_KEY_LENGTH, key_type}; + +use super::certificate::SshSignatureBlob; +use super::key_identifier; +use aes::cipher::{KeyIvInit, StreamCipher}; +use base64::engine::general_purpose; +use byteorder::{BigEndian, WriteBytesExt}; +use crypto_bigint::NonZero; +use rsa::traits::{PrivateKeyParts as _, PublicKeyParts as _}; +use rsa::{BoxedUint, RsaPrivateKey, RsaPublicKey}; +use std::io::{self, Write}; + +pub trait SshWriteExt { + type Error; + + fn write_ssh_string(&mut self, data: &str) -> Result<(), Self::Error>; + fn write_ssh_bytes(&mut self, data: &[u8]) -> Result<(), Self::Error>; + fn write_ssh_mpint(&mut self, data: &BoxedUint) -> Result<(), Self::Error>; +} + +impl SshWriteExt for T +where + T: Write, +{ + type Error = io::Error; + + fn write_ssh_string(&mut self, data: &str) -> Result<(), Self::Error> { + self.write_u32::(data.len() as u32)?; + self.write_all(data.as_bytes()) + } + + fn write_ssh_bytes(&mut self, data: &[u8]) -> Result<(), Self::Error> { + self.write_u32::(data.len() as u32)?; + self.write_all(data) + } + + fn write_ssh_mpint(&mut self, data: &BoxedUint) -> Result<(), Self::Error> { + let data = data.to_be_bytes_trimmed_vartime(); + let size = data.len() as u32; + // If the most significant bit would be set for + // a positive number, the number MUST be preceded by a zero byte. + if size > 0 && data[0] & 0b10000000 != 0 { + self.write_u32::(size + 1)?; + self.write_u8(0)?; + } else { + self.write_u32::(size)?; + } + self.write_all(&data) + } +} + +pub trait SshComplexTypeEncode { + type Error; + + fn encode(&self, stream: impl Write) -> Result<(), Self::Error>; +} + +impl SshComplexTypeEncode for SshCertType { + type Error = SshCertTypeError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + stream.write_u32::((*self).into())?; + Ok(()) + } +} + +impl SshComplexTypeEncode for SshCriticalOption { + type Error = SshCriticalOptionError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + stream.write_ssh_string(self.option_type.as_str())?; + stream.write_ssh_string(self.data.as_str())?; + Ok(()) + } +} + +impl SshComplexTypeEncode for Vec +where + T: SshComplexTypeEncode, + T::Error: From, +{ + type Error = T::Error; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + let mut data = Vec::new(); + for elem in self.iter() { + elem.encode(&mut data)?; + } + stream.write_ssh_bytes(&data)?; + Ok(()) + } +} + +impl SshComplexTypeEncode for SshExtension { + type Error = SshExtensionError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + stream.write_ssh_string(self.extension_type.as_str())?; + stream.write_ssh_string(self.data.as_str())?; + Ok(()) + } +} + +impl SshComplexTypeEncode for Vec { + type Error = io::Error; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + let mut data = Vec::new(); + for s in self.iter() { + data.write_ssh_string(s)?; + } + stream.write_ssh_bytes(&data)?; + Ok(()) + } +} + +impl SshComplexTypeEncode for SshSignature { + type Error = SshSignatureError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + let overall_size = self.format.as_str().len() + self.blob.size() + 8; + stream.write_u32::(overall_size as u32)?; + stream.write_ssh_string(self.format.as_str())?; + + match &self.blob { + SshSignatureBlob::Standard(data) => { + stream.write_ssh_bytes(data)?; + } + SshSignatureBlob::Sk { data, flags, counter } => { + stream.write_ssh_bytes(data)?; + stream.write_u8(*flags)?; + stream.write_u32::(*counter)?; + } + }; + + Ok(()) + } +} + +impl SshComplexTypeEncode for KdfOption { + type Error = io::Error; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + if self.salt.is_empty() { + stream.write_u32::(0)?; + return Ok(()); + } + let mut data = Vec::new(); + data.write_ssh_bytes(&self.salt)?; + data.write_u32::(self.rounds)?; + stream.write_ssh_bytes(&data)?; + Ok(()) + } +} + +impl SshComplexTypeEncode for Timestamp { + type Error = io::Error; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + stream.write_u64::(self.0)?; + Ok(()) + } +} + +impl SshComplexTypeEncode for SshBasePublicKey { + type Error = SshPublicKeyError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + match self { + SshBasePublicKey::Rsa(rsa) => { + let rsa = RsaPublicKey::try_from(rsa)?; + stream.write_ssh_string(key_type::RSA)?; + stream.write_ssh_mpint(rsa.e())?; + stream.write_ssh_mpint(rsa.n())?; + Ok(()) + } + SshBasePublicKey::Ec(ec) => { + let key = EcdsaPublicKey::try_from(ec)?; + encode_ecdsa_public_key_body(&mut stream, &key)?; + Ok(()) + } + SshBasePublicKey::Ed(ed) => { + let key = EdPublicKey::try_from(ed)?; + encode_ed_public_key_body(&mut stream, &key) + } + SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => { + let key = EcdsaPublicKey::try_from(base_key)?; + + stream.write_ssh_string(key_type::SK_ECDSA_SHA2_NIST_P256)?; + stream.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?; + stream.write_ssh_bytes(key.encoded_point())?; + + stream.write_ssh_string(application.as_str())?; + + Ok(()) + } + SshBasePublicKey::SkEd25519 { base_key, application } => { + let key = EdPublicKey::try_from(base_key)?; + + stream.write_ssh_string(key_type::SK_ED25519)?; + stream.write_ssh_bytes(key.data())?; + + stream.write_ssh_string(application.as_str())?; + + Ok(()) + } + } + } +} + +impl SshComplexTypeEncode for SshPublicKey { + type Error = SshPublicKeyError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + // Write key type + match &self.inner_key { + SshBasePublicKey::Rsa(_) => { + stream.write_all(key_type::RSA.as_bytes())?; + } + SshBasePublicKey::Ec(key) => { + let key = EcdsaPublicKey::try_from(key)?; + stream.write_all(key.curve().to_ecdsa_ssh_key_type()?.as_bytes())?; + } + SshBasePublicKey::Ed(key) => { + let key = EdPublicKey::try_from(key)?; + stream.write_all(key.algorithm().to_ed_ssh_key_type()?.as_bytes())?; + } + SshBasePublicKey::SkEcdsaSha2NistP256 { .. } => { + stream.write_all(key_type::SK_ECDSA_SHA2_NIST_P256.as_bytes())?; + } + SshBasePublicKey::SkEd25519 { .. } => { + stream.write_all(key_type::SK_ED25519.as_bytes())?; + } + }; + + stream.write_u8(b' ')?; + + { + let mut base64_write = Base64Writer::new(&mut stream, &general_purpose::STANDARD); + self.inner_key.encode(&mut base64_write)?; + base64_write.finish()?; + } + + stream.write_u8(b' ')?; + stream.write_all(self.comment.as_bytes())?; + stream.write_all("\r\n".as_bytes())?; + + Ok(()) + } +} + +impl SshComplexTypeEncode for SshBasePrivateKey { + type Error = SshPrivateKeyError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + match self { + SshBasePrivateKey::Rsa(rsa) => { + let rsa = RsaPrivateKey::try_from(rsa)?; + stream.write_ssh_string(key_type::RSA)?; + stream.write_ssh_mpint(rsa.n())?; + stream.write_ssh_mpint(rsa.e())?; + stream.write_ssh_mpint(rsa.d())?; + + let prime = NonZero::new(rsa.primes()[0].clone()) + .into_option() + .ok_or(SshPrivateKeyError::RsaPrimeIsZero)?; + let iqmp = rsa.primes()[1] + .invert_mod(&prime) + .into_option() + .ok_or(SshPrivateKeyError::RsaSecondPrimeInvertModFirstPrimeFailed)?; + stream.write_ssh_mpint(&iqmp)?; + + for prime in rsa.primes().iter() { + stream.write_ssh_mpint(prime)?; + } + } + SshBasePrivateKey::Ec(key) => { + let keypair = EcdsaKeypair::try_from(key)?; + + let public_key = EcdsaPublicKey::try_from(&keypair)?; + + // Encode the public key part + encode_ecdsa_public_key_body(&mut stream, &public_key)?; + + // Ecnode encoded secret + let secret = BoxedUint::from_be_slice_vartime(keypair.secret()); + stream.write_ssh_mpint(&secret)?; + } + SshBasePrivateKey::Ed(key) => { + let keypair = EdKeypair::try_from(key)?; + let public_key = EdPublicKey::try_from(&keypair)?; + encode_ed_public_key_body(&mut stream, &public_key)?; + + // SSH Ed25519 key private kye field contains secret in first 32 bytes and the + // public key copy in the last 32 bytes. + let mut secret = Vec::with_capacity(SSH_COMBO_ED25519_KEY_LENGTH); + secret.extend_from_slice(keypair.secret()); + secret.extend_from_slice(public_key.data()); + + stream.write_ssh_bytes(&secret)?; + } + SshBasePrivateKey::SkEcdsaSha2NistP256 { + public_key, + application, + flags, + handle, + } => { + let ec_key = EcdsaPublicKey::try_from(public_key)?; + + // Encode the public key part + stream.write_ssh_string(key_type::SK_ECDSA_SHA2_NIST_P256)?; + stream.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?; + stream.write_ssh_bytes(ec_key.encoded_point())?; + + stream.write_ssh_string(application.as_str())?; + stream.write_u8(*flags)?; + stream.write_ssh_bytes(handle)?; + // Reserved + stream.write_ssh_bytes(&[])?; + } + SshBasePrivateKey::SkEd25519 { + public_key, + application, + flags, + handle, + } => { + let ed_key = EdPublicKey::try_from(public_key)?; + + stream.write_ssh_string(key_type::SK_ED25519)?; + stream.write_ssh_bytes(ed_key.data())?; + + stream.write_ssh_string(application.as_str())?; + stream.write_u8(*flags)?; + stream.write_ssh_bytes(handle)?; + // Reserved + stream.write_ssh_bytes(&[])?; + } + }; + + Ok(()) + } +} + +fn encode_ed_public_key_body(mut stream: impl Write, key: &EdPublicKey<'_>) -> Result<(), SshPublicKeyError> { + stream.write_ssh_string(key.algorithm().to_ed_ssh_key_type()?)?; + stream.write_ssh_bytes(key.data())?; + Ok(()) +} + +fn encode_ecdsa_public_key_body(mut stream: impl Write, key: &EcdsaPublicKey<'_>) -> Result<(), SshPublicKeyError> { + stream.write_ssh_string(key.curve().to_ecdsa_ssh_key_type()?)?; + stream.write_ssh_string(key.curve().to_ecdsa_ssh_key_identifier()?)?; + + // So called "Q" value from RFC5656. In fact - standard SEC1 encoded public key representation + stream.write_ssh_bytes(key.encoded_point())?; + Ok(()) +} + +impl SshComplexTypeEncode for SshPrivateKey { + type Error = SshPrivateKeyError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + const AES256_CTR_BLOCK_SIZE: usize = 16; + const UNENCRYPTED_PADDING_SIZE: usize = 8; + + stream.write_all(AUTH_MAGIC.as_bytes())?; + stream.write_u8(b'\0')?; + + if self.passphrase.is_some() { + stream.write_ssh_string(AES256_CTR)?; + stream.write_ssh_string(BCRYPT)?; + + let salt = &self.kdf.option.salt; + let rounds = self.kdf.option.rounds; + + let mut kdf_options = Vec::new(); + kdf_options.write_ssh_bytes(salt)?; + kdf_options.write_u32::(rounds)?; + + stream.write_ssh_bytes(&kdf_options)?; + } else { + stream.write_ssh_string(NONE)?; + stream.write_ssh_string(NONE)?; + stream.write_ssh_string("")?; + } + + stream.write_u32::(1)?; // keys amount + + let mut public_key = Vec::new(); + self.public_key().inner_key.encode(&mut public_key)?; + stream.write_ssh_bytes(&public_key)?; + + public_key.clear(); + let mut private_key = public_key; + + private_key.write_u32::(self.check)?; + private_key.write_u32::(self.check)?; + self.base_key.encode(&mut private_key)?; + + private_key.write_ssh_string(&self.comment)?; + + let padding_size = if self.passphrase.is_some() { + AES256_CTR_BLOCK_SIZE + } else { + UNENCRYPTED_PADDING_SIZE + }; + + // add padding + for i in 1..=(padding_size - (private_key.len() % padding_size)) { + private_key.push(i as u8); + } + + if let Some(passphrase) = &self.passphrase { + // encrypt private_key + let n = 48; + let mut hash = [0; 48]; + + let salt = &self.kdf.option.salt; + let rounds = self.kdf.option.rounds; + + bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, rounds, &mut hash)?; + + let (key, iv) = hash.split_at(n - 16); + let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap(); + + let private_key_len = private_key.len(); + private_key.resize(private_key_len + 32, 0u8); + cipher.apply_keystream(&mut private_key); + private_key.truncate(private_key_len); + } + + stream.write_ssh_bytes(&private_key)?; + + Ok(()) + } +} + +impl SshComplexTypeEncode for SshCertificate { + type Error = SshCertificateError; + + fn encode(&self, mut stream: impl Write) -> Result<(), Self::Error> { + stream.write_all(self.cert_key_type.as_str().as_bytes())?; + stream.write_u8(b' ')?; + + let mut cert_data = Base64Writer::new(stream, &general_purpose::STANDARD); + + cert_data.write_ssh_string(self.cert_key_type.as_str())?; + cert_data.write_ssh_bytes(&self.nonce)?; + match &self.public_key.inner_key { + SshBasePublicKey::Rsa(rsa) => { + let rsa = RsaPublicKey::try_from(rsa)?; + cert_data.write_ssh_mpint(rsa.e())?; + cert_data.write_ssh_mpint(rsa.n())?; + } + SshBasePublicKey::Ec(ec) => { + let ec = EcdsaPublicKey::try_from(ec)?; + cert_data.write_ssh_string(ec.curve().to_ecdsa_ssh_key_identifier()?)?; + cert_data.write_ssh_bytes(ec.encoded_point())?; + } + SshBasePublicKey::Ed(ed) => { + let ed = EdPublicKey::try_from(ed)?; + cert_data.write_ssh_bytes(ed.data())?; + } + SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, application } => { + let ec = EcdsaPublicKey::try_from(base_key)?; + cert_data.write_ssh_string(key_identifier::ECDSA_SHA2_NIST_P256)?; + cert_data.write_ssh_bytes(ec.encoded_point())?; + cert_data.write_ssh_string(application.as_str())?; + } + SshBasePublicKey::SkEd25519 { base_key, application } => { + let ed = EdPublicKey::try_from(base_key)?; + cert_data.write_ssh_bytes(ed.data())?; + cert_data.write_ssh_string(application.as_str())?; + } + }; + + cert_data.write_u64::(self.serial)?; + + self.cert_type.encode(&mut cert_data)?; + + cert_data.write_ssh_string(self.key_id.as_str())?; + + self.valid_principals.encode(&mut cert_data)?; + self.valid_after.encode(&mut cert_data)?; + self.valid_before.encode(&mut cert_data)?; + self.critical_options.encode(&mut cert_data)?; + self.extensions.encode(&mut cert_data)?; + + cert_data.write_ssh_bytes(&[])?; // reserved + + let mut rsa_key = Vec::new(); + self.signature_key.inner_key.encode(&mut rsa_key)?; + + cert_data.write_ssh_bytes(&rsa_key)?; + self.signature.encode(&mut cert_data)?; + + // stream.write_all(cert_data.finish()?.as_slice())?; + let mut stream = cert_data.finish().unwrap(); + stream.write_u8(b' ')?; + + stream.write_all(self.comment.as_bytes())?; + stream.write_all("\r\n".as_bytes())?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::SshWriteExt; + use rsa::BoxedUint; + + #[test] + fn ssh_string_encode() { + let mut res = Vec::new(); + let ssh_string = "picky"; + + res.write_ssh_string(ssh_string).unwrap(); + + assert_eq!(vec![0, 0, 0, 5, 112, 105, 99, 107, 121], res); + + res.clear(); + let ssh_string = ""; + + res.write_ssh_string(ssh_string).unwrap(); + + assert_eq!(vec![0, 0, 0, 0], res); + } + + #[test] + fn byte_array_encode() { + let mut res = Vec::new(); + let byte_array = [1, 2, 3, 4, 5, 6]; + + res.write_ssh_bytes(&byte_array).unwrap(); + + assert_eq!(vec![0, 0, 0, 6, 1, 2, 3, 4, 5, 6], res); + + res.clear(); + let byte_array = []; + + res.write_ssh_bytes(&byte_array).unwrap(); + + assert_eq!(vec![0, 0, 0, 0], res); + } + + #[test] + fn mpint_encoding() { + let mpint = BoxedUint::from_be_slice_vartime(&[0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7]); + let mut res = Vec::new(); + res.write_ssh_mpint(&mpint).unwrap(); + + assert_eq!( + res, + vec![0x00, 0x00, 0x00, 0x08, 0x09, 0xa3, 0x78, 0xf9, 0xb2, 0xe3, 0x32, 0xa7], + ); + + let mpint = BoxedUint::from_be_slice_vartime(&[0x80]); + let mut res = Vec::new(); + res.write_ssh_mpint(&mpint).unwrap(); + + assert_eq!(res, vec![0x00, 0x00, 0x00, 0x02, 0x00, 0x80]); + } +} diff --git a/rust/vendor/picky/src/ssh/mod.rs b/rust/vendor/picky/src/ssh/mod.rs new file mode 100644 index 0000000..07aa3dc --- /dev/null +++ b/rust/vendor/picky/src/ssh/mod.rs @@ -0,0 +1,115 @@ +pub mod certificate; +pub mod decode; +pub mod encode; +pub mod private_key; +pub mod public_key; + +use crate::key::ec::NamedEcCurve; +use crate::key::ed::NamedEdAlgorithm; +use crate::key::{EcCurve, EdAlgorithm, KeyError}; + +use byteorder::ReadBytesExt; +use std::io::{self, Read}; + +pub use certificate::{SshCertKeyType, SshCertType, SshCertificate, SshCertificateBuilder}; +pub use private_key::SshPrivateKey; +pub use public_key::SshPublicKey; + +pub(crate) type Base64Writer<'a, T, E> = base64::write::EncoderWriter<'a, T, E>; +pub(crate) type Base64Reader<'a, T, E> = base64::read::DecoderReader<'a, T, E>; + +const SSH_COMBO_ED25519_KEY_LENGTH: usize = ed25519_dalek::SECRET_KEY_LENGTH + ed25519_dalek::PUBLIC_KEY_LENGTH; + +mod key_type { + pub const RSA: &str = "ssh-rsa"; + pub const ECDSA_SHA2_NIST_P256: &str = "ecdsa-sha2-nistp256"; + pub const ECDSA_SHA2_NIST_P384: &str = "ecdsa-sha2-nistp384"; + pub const ECDSA_SHA2_NIST_P521: &str = "ecdsa-sha2-nistp521"; + pub const ED25519: &str = "ssh-ed25519"; + pub const SK_ECDSA_SHA2_NIST_P256: &str = "sk-ecdsa-sha2-nistp256@openssh.com"; + pub const SK_ED25519: &str = "sk-ssh-ed25519@openssh.com"; +} + +mod key_identifier { + pub const ECDSA_SHA2_NIST_P256: &str = "nistp256"; + pub const ECDSA_SHA2_NIST_P384: &str = "nistp384"; + pub const ECDSA_SHA2_NIST_P521: &str = "nistp521"; +} + +trait EcCurveSshExt { + fn to_ecdsa_ssh_key_type(&self) -> Result<&'static str, KeyError>; + fn to_ecdsa_ssh_key_identifier(&self) -> Result<&'static str, KeyError>; +} + +impl EcCurveSshExt for NamedEcCurve { + fn to_ecdsa_ssh_key_type(&self) -> Result<&'static str, KeyError> { + match self { + NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_type::ECDSA_SHA2_NIST_P256), + NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_type::ECDSA_SHA2_NIST_P384), + NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_type::ECDSA_SHA2_NIST_P521), + NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key type serialization")), + } + } + + fn to_ecdsa_ssh_key_identifier(&self) -> Result<&'static str, KeyError> { + match self { + NamedEcCurve::Known(EcCurve::NistP256) => Ok(key_identifier::ECDSA_SHA2_NIST_P256), + NamedEcCurve::Known(EcCurve::NistP384) => Ok(key_identifier::ECDSA_SHA2_NIST_P384), + NamedEcCurve::Known(EcCurve::NistP521) => Ok(key_identifier::ECDSA_SHA2_NIST_P521), + NamedEcCurve::Unsupported(oid) => Err(KeyError::unsupported_curve(oid, "ssh key identifier serialization")), + } + } +} + +trait EdAlgorithmSshExt { + fn to_ed_ssh_key_type(&self) -> Result<&'static str, KeyError>; +} + +impl EdAlgorithmSshExt for NamedEdAlgorithm { + fn to_ed_ssh_key_type(&self) -> Result<&'static str, KeyError> { + match self { + NamedEdAlgorithm::Known(EdAlgorithm::Ed25519) => Ok(key_type::ED25519), + NamedEdAlgorithm::Known(EdAlgorithm::X25519) => Err(KeyError::UnsupportedAlgorithm { + algorithm: "X25519 can't be use for SSH EdDSA keys", + }), + NamedEdAlgorithm::Unsupported(oid) => { + Err(KeyError::unsupported_ed_algorithm(oid, "ssh key type serialization")) + } + } + } +} + +fn read_until_whitespace(stream: &mut dyn Read, buffer: &mut Vec) -> io::Result<()> { + loop { + match stream.read_u8() { + Ok(symbol) => { + if symbol as char == ' ' { + break; + } else { + buffer.push(symbol); + } + } + Err(ref e) if e.kind() == io::ErrorKind::UnexpectedEof => { + break; + } + Err(e) => return Err(e), + }; + } + + Ok(()) +} + +fn read_until_linebreak(stream: &mut dyn Read, buffer: &mut Vec) -> io::Result<()> { + loop { + match stream.read_u8() { + Ok(b'\r') | Ok(b'\n') => break, + Ok(c) => buffer.push(c), + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { + break; + } + Err(e) => return Err(e), + } + } + + Ok(()) +} diff --git a/rust/vendor/picky/src/ssh/private_key.rs b/rust/vendor/picky/src/ssh/private_key.rs new file mode 100644 index 0000000..13922e6 --- /dev/null +++ b/rust/vendor/picky/src/ssh/private_key.rs @@ -0,0 +1,780 @@ +use crate::key::{EcCurve, EdAlgorithm, KeyError, PrivateKey, PrivateKeyKind, PublicKey}; +use crate::pem::{Pem, PemError, parse_pem}; +use crate::ssh::decode::{SshComplexTypeDecode, SshReadExt}; +use crate::ssh::encode::SshComplexTypeEncode; +use crate::ssh::public_key::{SshBasePublicKey, SshPublicKey, SshPublicKeyError}; + +use aes::cipher::block_padding::NoPadding; +use aes::cipher::{KeyIvInit, StreamCipher}; +use byteorder::{BigEndian, ReadBytesExt}; +use cbc::cipher::BlockModeDecrypt; +use rand::RngExt; +use std::io::{Cursor, Read}; +use std::string; +use thiserror::Error; + +pub type Aes128CbcDec = cbc::Decryptor; +pub type Aes256CbcDec = cbc::Decryptor; +pub type Aes128Ctr = ctr::Ctr32BE; +pub type Aes256Ctr = ctr::Ctr32BE; + +const SSH_PRIVATE_KEY_LABEL: &str = "OPENSSH PRIVATE KEY"; +pub(crate) const AUTH_MAGIC: &str = "openssh-key-v1"; + +const AES128_CTR: &str = "aes128-ctr"; +pub(crate) const AES256_CTR: &str = "aes256-ctr"; + +const AES128_CBC: &str = "aes128-cbc"; +const AES256_CBC: &str = "aes256-cbc"; + +pub(crate) const BCRYPT: &str = "bcrypt"; +pub(crate) const NONE: &str = "none"; + +#[derive(Debug, Error)] +pub enum SshPrivateKeyError { + #[error(transparent)] + FromUtf8Error(#[from] string::FromUtf8Error), + #[error(transparent)] + Base64DecodeError(#[from] base64::DecodeError), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error("Unsupported key type: {0}")] + UnsupportedKeyType(String), + #[error("Unsupported cipher: {0}")] + UnsupportedCipher(String), + #[error("Unsupported kdf: {0}")] + UnsupportedKdf(String), + #[error("Invalid auth magic header")] + InvalidAuthMagicHeader, + #[error("Invalid keys amount. Expected 1 but got {0}")] + InvalidKeysAmount(u32), + #[error("Check numbers are not equal: {0} {1}. Wrong passphrase or key is corrupted")] + InvalidCheckNumbers(u32, u32), + #[error("Invalid public key: {0:?}")] + InvalidPublicKey(#[from] SshPublicKeyError), + #[error("Invalid key format")] + InvalidKeyFormat, + #[error("Can not decrypt private key: {0}")] + DecryptionError(String), + #[error("Can not hash the passphrase: {0:?}")] + HashingError(#[from] bcrypt_pbkdf::Error), + #[error("Passphrase required for encrypted private key")] + MissingPassphrase, + #[error(transparent)] + KeyError(#[from] KeyError), + #[error(transparent)] + PemError(#[from] PemError), + #[error("RSA prime is zero")] + RsaPrimeIsZero, + #[error("RSA second prime invert mod first prime failed")] + RsaSecondPrimeInvertModFirstPrimeFailed, +} + +#[derive(Debug, Eq, PartialEq, Clone, Default)] +pub struct KdfOption { + pub salt: Vec, + pub rounds: u32, +} + +#[derive(Debug, Eq, PartialEq, Clone)] +pub struct Kdf { + pub name: String, + pub option: KdfOption, +} + +impl Default for Kdf { + fn default() -> Self { + Self { + name: NONE.to_owned(), + option: Default::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SshBasePrivateKey { + Rsa(PrivateKey), + Ec(PrivateKey), + Ed(PrivateKey), + /// U2F ecdsa SSH key. Note that this key does not contain the private key data, only handle + /// is stored which could be used to sign data via hardware U2F key. + SkEcdsaSha2NistP256 { + public_key: PublicKey, + application: String, + flags: u8, + handle: Vec, + }, + /// U2F ed25519 SSH key. Note that this key does not contain the private key data, only handle + /// is stored which could be used to sign data via hardware U2F key. + SkEd25519 { + public_key: PublicKey, + application: String, + flags: u8, + handle: Vec, + }, +} + +impl SshBasePrivateKey { + pub fn base_public_key(&self) -> Result { + let key = match self { + SshBasePrivateKey::Rsa(rsa) => SshBasePublicKey::Rsa(rsa.to_public_key()?), + SshBasePrivateKey::Ec(ec) => SshBasePublicKey::Ec(ec.to_public_key()?), + SshBasePrivateKey::Ed(ed) => SshBasePublicKey::Ed(ed.to_public_key()?), + SshBasePrivateKey::SkEcdsaSha2NistP256 { + public_key, + application, + .. + } => SshBasePublicKey::SkEcdsaSha2NistP256 { + base_key: public_key.clone(), + application: application.clone(), + }, + SshBasePrivateKey::SkEd25519 { + public_key, + application, + .. + } => SshBasePublicKey::SkEd25519 { + base_key: public_key.clone(), + application: application.clone(), + }, + }; + + Ok(key) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshPrivateKey { + pub cipher_name: String, + pub kdf: Kdf, + pub base_key: SshBasePrivateKey, + pub public_key: SshPublicKey, + pub check: u32, + pub comment: String, + pub passphrase: Option, +} + +impl SshPrivateKey { + pub fn generate_rsa( + bits: usize, + passphrase: Option, + comment: Option, + ) -> Result { + SshPrivateKey::h_picky_private_key_to_ssh_private_key(PrivateKey::generate_rsa(bits)?, passphrase, comment) + } + + pub fn generate_ec( + curve: EcCurve, + passphrase: Option, + comment: Option, + ) -> Result { + SshPrivateKey::h_picky_private_key_to_ssh_private_key(PrivateKey::generate_ec(curve)?, passphrase, comment) + } + + pub fn generate_ed25519(passphrase: Option, comment: Option) -> Result { + SshPrivateKey::h_picky_private_key_to_ssh_private_key( + PrivateKey::generate_ed(EdAlgorithm::Ed25519, true)?, + passphrase, + comment, + ) + } + + pub fn from_pem(pem: &Pem, passphrase: Option) -> Result { + SshPrivateKey::decode(&mut pem.data(), passphrase) + } + + pub fn from_pem_str(pem: &str, passphrase: Option) -> Result { + let pem = parse_pem(pem)?; + SshPrivateKey::decode(&mut pem.data(), passphrase) + } + + pub fn to_pem(&self) -> Result, SshPrivateKeyError> { + let mut buffer = Vec::with_capacity(2048); + self.encode(&mut buffer)?; + Ok(Pem::new(SSH_PRIVATE_KEY_LABEL, buffer)) + } + + pub fn to_string(&self) -> Result { + let mut buffer = Vec::with_capacity(2048); + self.encode(&mut buffer)?; + let mut result = Pem::new(SSH_PRIVATE_KEY_LABEL, buffer).to_string(); + // ssh private key must contain \x0A (\n) character at the end + result.push('\x0A'); + Ok(result) + } + + pub fn public_key(&self) -> &SshPublicKey { + &self.public_key + } + + pub fn base_key(&self) -> &SshBasePrivateKey { + &self.base_key + } + + pub fn inner_key(&self) -> Option<&PrivateKey> { + match self.base_key() { + SshBasePrivateKey::Rsa(key) => Some(key), + SshBasePrivateKey::Ec(key) => Some(key), + SshBasePrivateKey::Ed(key) => Some(key), + SshBasePrivateKey::SkEcdsaSha2NistP256 { .. } | SshBasePrivateKey::SkEd25519 { .. } => None, + } + } + + pub(crate) fn h_picky_private_key_to_ssh_private_key( + private_key: PrivateKey, + passphrase: Option, + comment: Option, + ) -> Result { + let (kdf, cipher_name) = match &passphrase { + Some(_) => { + let mut salt: Vec = Vec::new(); + let rounds = 16; + let mut rnd = rand::rng(); + for _ in 0..rounds { + salt.push(rnd.random::()); + } + + let kdf = Kdf { + name: BCRYPT.to_owned(), + option: KdfOption { salt, rounds }, + }; + + (kdf, String::new()) + } + None => (Kdf::default(), NONE.to_owned()), + }; + + let public_key = private_key.to_public_key()?; + + let (public, private) = match private_key.as_kind() { + PrivateKeyKind::Rsa => { + let private = SshBasePrivateKey::Rsa(private_key); + let public = SshBasePublicKey::Rsa(public_key); + (public, private) + } + PrivateKeyKind::Ec { .. } => { + let private = SshBasePrivateKey::Ec(private_key); + let public = SshBasePublicKey::Ec(public_key); + (public, private) + } + PrivateKeyKind::Ed { .. } => { + let private = SshBasePrivateKey::Ed(private_key); + let public = SshBasePublicKey::Ed(public_key); + (public, private) + } + }; + + let public_key = SshPublicKey { + inner_key: public, + comment: String::new(), + }; + + Ok(SshPrivateKey { + cipher_name, + kdf, + base_key: private, + public_key, + check: 0, + comment: comment.unwrap_or_default(), + passphrase, + }) + } + + fn decode(mut stream: impl Read, passphrase: Option) -> Result + where + Self: Sized, + { + let mut auth_magic = [0; AUTH_MAGIC.len()]; + stream.read_exact(&mut auth_magic)?; + if auth_magic != AUTH_MAGIC.as_bytes() { + return Err(SshPrivateKeyError::InvalidAuthMagicHeader); + } + stream.read_u8()?; // skip 1 byte (null-byte) + + let cipher_name = stream.read_ssh_string()?; + let kdf_name = stream.read_ssh_string()?; + let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut stream)?; + let keys_amount = stream.read_u32::()?; + + if keys_amount != 1 { + return Err(SshPrivateKeyError::InvalidKeysAmount(keys_amount)); + } + + // read public key + let _ = stream.read_ssh_bytes()?; + + // read private key + let private_key = stream.read_ssh_bytes()?; + + let data = decrypt(&cipher_name, &kdf_name, &kdf_option, passphrase.as_deref(), private_key)?; + + let mut cursor = Cursor::new(data); + + let check0 = cursor.read_u32::()?; + let check1 = cursor.read_u32::()?; + if check0 != check1 { + return Err(SshPrivateKeyError::InvalidCheckNumbers(check0, check1)); + } + + let base_key: SshBasePrivateKey = SshComplexTypeDecode::decode(&mut cursor)?; + let base_public_key = base_key.base_public_key()?; + + let comment = cursor.read_ssh_string()?.trim_end().to_owned(); + + Ok(SshPrivateKey { + base_key, + public_key: SshPublicKey { + inner_key: base_public_key, + comment: String::new(), + }, + passphrase, + kdf: Kdf { + name: kdf_name, + option: kdf_option, + }, + cipher_name, + check: check0, + comment, + }) + } +} + +impl TryFrom for SshPrivateKey { + type Error = SshPrivateKeyError; + + fn try_from(private_key: PrivateKey) -> Result { + SshPrivateKey::h_picky_private_key_to_ssh_private_key(private_key, None, None) + } +} + +pub(crate) fn decrypt( + cipher_name: &str, + kdf_name: &str, + kdf_options: &KdfOption, + passphrase: Option<&str>, + mut data: Vec, +) -> Result, SshPrivateKeyError> { + if kdf_name == NONE { + Ok(data) + } else { + let n = match cipher_name { + AES128_CBC | AES128_CTR => 32, + AES256_CBC | AES256_CTR => 48, + name => return Err(SshPrivateKeyError::UnsupportedCipher(name.to_owned())), + }; + + let mut key = [0; 48]; + match kdf_name { + BCRYPT => { + let salt = &kdf_options.salt; + let rounds = kdf_options.rounds; + let passphrase = passphrase.ok_or(SshPrivateKeyError::MissingPassphrase)?; + + bcrypt_pbkdf::bcrypt_pbkdf(passphrase, salt, rounds, &mut key[..n])?; + } + name => return Err(SshPrivateKeyError::UnsupportedKdf(name.to_owned())), + }; + + let (key, iv) = key.split_at(n - 16); + + let start_len = data.len(); + data.resize(data.len() + 32, 0u8); + match cipher_name { + AES128_CBC => { + let cipher = Aes128CbcDec::new_from_slices(key, iv).unwrap(); + let n = cipher + .decrypt_padded_inout::(data.as_mut_slice().into()) + .map_err(|e| SshPrivateKeyError::DecryptionError(e.to_string()))? + .len(); + data.truncate(n); + Ok(data) + } + AES256_CBC => { + let cipher = Aes256CbcDec::new_from_slices(key, iv).unwrap(); + let n = cipher + .decrypt_padded_inout::(data.as_mut_slice().into()) + .map_err(|e| SshPrivateKeyError::DecryptionError(e.to_string()))? + .len(); + data.truncate(n); + Ok(data) + } + AES128_CTR => { + let mut cipher = Aes128Ctr::new_from_slices(key, iv).unwrap(); + cipher.apply_keystream(&mut data); + data.truncate(start_len); + Ok(data) + } + AES256_CTR => { + let mut cipher = Aes256Ctr::new_from_slices(key, iv).unwrap(); + cipher.apply_keystream(&mut data); + data.truncate(start_len); + Ok(data) + } + name => Err(SshPrivateKeyError::UnsupportedCipher(name.to_owned())), + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::key::ec::EcdsaKeypair; + use crate::key::ed::EdKeypair; + use crate::ssh::private_key::SshPrivateKey; + use rsa::RsaPrivateKey; + use rstest::rstest; + + #[test] + fn decode_without_passphrase_2048() { + // ssh-keygen -t rsa -b 2048 -C "test2@picky.com" (without the passphrase) + let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\ + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\n\ + c2gtcnNhAAAAAwEAAQAAAQEAyPYbdoNqjj4EhuYblWIxVKLsmsOff+kLkKlFRsIJ\n\ + yE5YUWzPm5LyUH3LoqnL/rw/f/Og37oJO/bEn4P2lSvlf6ZagAGaLo8/8ACw4xKY\n\ + UsQFHAEfreIthd/T2u9TEnN+yPS99M99bXG2tV+6He4c61TJfYrq5DsgQuMXCFmt\n\ + R/IdJg8qF8lj06qEzjQ1HvXQdXruhm4sQn1HMb3VbdKQFSU3TpmzVysEaOVl3zK7\n\ + KirBU9gHIOFZuE3y0oUklFuK6jOhjgQnxeo58Rb00g3p7R+YcpI1i95TAoIQ/tYS\n\ + cjnZzByQv+ak1BjgfOjMbEeEQl6kvi2axqTEnFcg0IHu6wAAA8iqDGUDqgxlAwAA\n\ + AAdzc2gtcnNhAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhR\n\ + bM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUc\n\ + AR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0m\n\ + DyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT\n\ + 2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnM\n\ + HJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7rAAAAAwEAAQAAAQATZEw6H2xE\n\ + 1Y8yRTocLCF+fUo/lOjrOt22096veUHgZk73bHyMEp33Tmw8Ag6BQkEOY7/+VsFV\n\ + W/aVPfKpalb2/mJ1P7JVE9Wjny1ye/Te57NmhGU+LjkeVf7nfXiSqzpswdEisnL0\n\ + AKkUz2vyP2vi+YeH6cPIyjvOuIMcdyrVakejnGbss19ZoXw660X/7TRqG/41KhTm\n\ + lkN610JBKI2Rozecx9l3LZ3CTRpOOJ2sfssegvL+qxvvH1YVkRat4dwNZxsi+cho\n\ + zqWOciXrbzifBghBp0Upe5fgR2JRpyB6sMVXIHKkeP9YBQUARm1ECdbdJmPSiNYP\n\ + gMKpTaEObMahAAAAgCtugmDSAwIPibrD9MAbJB6KbN15heA6vTtCLOvFe1Hikw94\n\ + DYAJz+vlKadbOZW5SfGAOuIe7IynafthWm4RcbXEXxhnVtqHxzMHOZo/Mnoh+bUO\n\ + esDSoERyNHokpNK6m1NKbmQeFj4n7rkcrR8hrwX8+Ng8CsBEglDi+ULtVivbAAAA\n\ + gQD1vEPRUu9aD7CjkYgDyD2vNRRevARf01ImgT1tpiEA+GLHJ0xMetd7OH0wutAZ\n\ + uH26V19Kt4sWpsTwfdl2fIw7XHPc+G1OSqiOk6AS9qT/sy/VL1Wn7CqyAN2jikzn\n\ + quE6MbebTUJQSNHK9vQhn+u4hUDdEoMOLTYdWxxcjdJirQAAAIEA0VsOxBRDSTLc\n\ + Ar0Y97oCmb/6tU9XGAZwL2E14GVK85PnJNwHrx4aqb0qATE4iPLfE7ms+eBtT8Uj\n\ + HF0fxM3KDQiFSrvtgM4JjGTDS4dTYIBD/eQ0/aTaRgLOQqplyBgYVr3x7ATfcIP5\n\ + 961TfdiJ/QESutdb1KQquFXIMRII4vcAAAAPdGVzdDJAcGlja3kuY29tAQIDBA==\n\ + -----END OPENSSH PRIVATE KEY-----"; + + let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap(); + + let kdf = Kdf::default(); + + assert_eq!("test2@picky.com".to_owned(), private_key.comment); + assert_eq!(kdf, private_key.kdf); + assert_eq!("none", private_key.cipher_name); + } + + #[test] + fn decode_without_passphrase_4096() { + // ssh-keygen -t rsa -b 4096 -C "test@picky.com" (without the passphrase) + let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcnNhAAAAAwEAAQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56LYSsAAAdIA4ihRQOIoUUAAAAHc3NoLXJzYQAAAgEA21AiuHR9Z+HThQb/7I3zJmuKuanu0mePY9hjgxiq/A7nmTFmC03JOtblDDJVQU918l+pnul+FrAaIo80Fr4MKSwhk6pYUE57ZuRaYVxx5CsRb4zIT8wpxzUvi9Hm83sHHnLGOa7YMPugYRcHWRoRQX4n9f+rPau8u/vBnt4VCBKi3YjAw88XOusyGltuo2cTuATB7iqe15Z9iXg47ER789LwTQHXTn5L7afoDO9jh+LZvcEv1fG1TmevKFNKLPA7ohBp8AOUZ4zo2hXR1rdZg/Afp0SDcSPM2MkHKqd7eKeedj9Ba4b44IsYuu0cmsdA1DbszdjKUNDkVIEZH8v8VryJlLHj/wX6rzYlpBQFhzQw0rHOdFpq/oNCYnBtoKMBy2D8SkYyyGzqviYMR6xOE3WgNjSaHlKaSYFlOMrhpeX8dRvgXHa9AvpbDI9eB6fmhmoxDi0OzKtx81hKMfRtSoDeK9uujKH3fE+L64xeiWvRPqadKV4BL9nL7WCSz9Knax1mn295VrD+ISVp7/zWlz+mQMYhHh7IoK2PfJJoGWx5v+gJogSe2ykP0vz3pWI95ky9GmJBhe/albQM0pe8iPclch7Je3beY3ZqeviKH7hLTX5wHH6Gki7tDo6LafVQTL4peqI0nGyTSwS/LRjePrqyHLDVL1YwDp8HN56LYSsAAAADAQABAAACAC7OXIqnefhIzx7uDoLLDODfRN05Mlo/de/mR967zgo7mBwu2cuBz3e6U2oV9/IXZmHTHt1mkd1/uiQ0Efbkmq3S2FuumGiTR2z/QXbUBw6eTntTPZEiTqxQYpRhuPuv/yX1cu7urP9PRLxT8OKIWLR0m0y6Qy7HT2GDaqBgX3a4m3/SZumjch7GAYx0hRlkr2Wvxj/xYrM6UBKd0PBD8XxpQZX91ZjQBZ50HmdcVA61UKlZ6L6tdneEU3K0y/jpUKDXBfUOnoa3IR8iVwWPXhB1mBvX2IG2FUsTJG9rDUQD6iLsfybWyJkLtrx2TIuQCPsBuep44Tz8SC7s2pLZs0HeihnrM5YmqprMggvZ1TkVFoR3bq/42XO6ULy5k8QPuP6t91UN5iVljgr8H/6Jo9MuCeRA45ZPZN94Cn1mKJWYamrqRuCqDR5za3A0oHPKYUAfzzD90BLL6Yaib75VpiEDTkOiBuW3MJUcJsqZipDDl/6eas2Qyloplw60dx42FzcRIDXkXzRNn8hBSy7xmQ5MOKGBszCeV/eTBtRITQN38yDVMerb8xDlwOsTtjo3PHCg4HEqqSzjv/B0op9aP7RJ8zp9xLOGlxRZ9YhAlHctUOO6ATsv4uCFwCniZbVOdcUEYwNebYQ0x3IRGUF6RpqjOudUwgLlo0Lq1KV05fM5AAABAC7fkAB4l5YMAseu+lcj+CwHySzcI+baRFCrMIKldNjEPvvZcCSOU/n5pgp2bw0ulw8c4mFQv0GsG//qQCBX1IrIWO0/nRBjEUTPIe2BUswoxm3+F7pirphdIpABKMzV7ZvENn53p2ByrW9+uiwwXLo/z4tH18JW41Jyp5mXH2+1iWIYzq5d4gVgMKLGnqWG3DisViHBGg/ExxQCayeXAhlcXVaWZiaVYsgyreaQg58S2RRUIveWP+ZAeb8+ZJ72ZjIYLc0GIbP673GpcNWkRlCykTJXF9x+Ts0trffqvSxF+2YJnaacLSWJmWFU1BsxUO2pIM4SI8VeHYBdEoAVqcQAAAEBAPUodhyNIr8dtcJona8Lkn+3BxLdvYAV1bnlWnUcG9m0RQ2L95kH6folOG00aWhRgJHFDoXcCaHND8Mg3PkAXYUKCucipiIITyd8YeYnF0ckau5GmUEzwc6s4HcGyFilX1yBoyLE7hFMzOJ4+Rcq+zpD2TfaWcuoo+njDWEHeTbzvGIDQoBYsPnGOtw57q9IA5oWYAG3LtwygazmNF2xeEnMEtYPyPu7+W0teO0QIJiHWEuK/yLPOb+RHBfA6YJ1f9Jcgc614DxyW6qnB5YuzQBovLzgp/7j9J4Z9F8n8f9PAwYScf7IG8icVVhl5NwNgfNOpcjdg6+YB8Z0AXa4dYcAAAEBAOUDEl6yS1nwZ0QsJwfHE232dpsOqxxqfV4ei4R8/obq+b5YPHiUgbt2PlHyHtgfQr639BwMmIaAMSR9CLti44Mw6Z3k2DEz3Ef4+XilPeScNiZmWfYanWmVwFEtb2c+YT3QweUH3DUAViHL+UdU7xp+zhkrd04daVPpYc9NNN9b9Gwmj6Pm0RP05UJxsG1ipvN1rGpaCsJiLfS9IoSsKh0Vzdzdty1YvFhEErTl0WBVGGK6xaA5lfMtaclWi2mGGNXfWflyQzkz87eYlPe2RhM7jW1Lo9h1BBYE6R+jKt3q0mHwRehj+updAAXJx0RWF7EDQVJtlTfSrUCm+SSFoD0AAAAOdGVzdEBwaWNreS5jb20BAgMEBQ==-----END OPENSSH PRIVATE KEY-----"; + + let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap(); + let kdf = Kdf::default(); + assert_eq!("test@picky.com".to_owned(), private_key.comment); + assert_eq!(kdf, private_key.kdf); + assert_eq!("none", private_key.cipher_name); + } + + #[test] + fn decode_with_passphrase_2048() { + // ssh-keygen -t rsa -b 2048 -C "test_with_pass2@picky.com" + let passphrase = Some("123123".to_string()); + let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\ + b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBI\n\ + MsVovOqXSrZa+iEvQwXzAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAAB\n\ + AQCkR5WaC3NTPZdj9X/bX88YYbR2k5r3aE+I/ezxzbG6xIJi+So9AohypAhReyW9\n\ + 7XSGut5n6a9O+n/c9nCiXFVoyXbMSdM90Av5bu799+V4w3kBlRzN5D3A6uIZRjgl\n\ + wc3Xso9kthneNByB7OjZuSDdmuWE3YOgmW0TirP3dztbtVScLPZUSsEveIMt90aw\n\ + uOFWaEUshqb7l713bdEB0Tb77Z1wZpt6UmIgpraV58kN/ahepbY8lirMS4ym75wt\n\ + Be6PgyKGKIR3aNQdbfHYMHCgxNQMAFUt2yD9f+JE5HWG7kyKDcLTHCY60dtKTNfp\n\ + cByi4Bwm3209V4rGYSKAzFXvAAAD0CdYEpFE8Dda2GBNy1l5vDNdbyJvx7SSP49l\n\ + 4OmHsgRE2WneNC9CfO2IxPRNXsPEmXimeubqm6alsmJ1Ch+KsdjvyU7WIEnjuonC\n\ + lLWx6rhsuppJqZSICbikMUXjhlHpLirGnL0WoaBnLYEVYgu8cMbIgE9BNho+bS+1\n\ + qvyIrIdIblJwwc66CJKUYPz1yRA84WIMZOWlsYfeHnCvTGjiYUG2YFayVAuXvAz/\n\ + ND3bQYUlO34XOOsJvZxfQNEg1/tzhB7RvcGOG1InoQxT6dZtTp85CkTU/QQ6w2eY\n\ + j4qDDmsFm/eSDgEFfOJDLrfHsB4+G2aBZLmgk2bn7vo3JBkcPAETX6kKd7bkyEfh\n\ + LVph9i48vbmNJ8mXWiXMoRXqRgkKqBAMVnuXtbKVDVzzlZXIFu1cbuKyt0zUg7jB\n\ + IeIdG+5U0L6qygTjOKU6aP+dK1wRc0XyC8jxTJupt2eTEKBLzy4TwlLH5QhEcj1c\n\ + coV97PyslJ/NnQx8IKflHxxxQF4CbYgyXt9fWZpBfaD9TVWgsFoKrlZ9HOb6s5WJ\n\ + MwijgNLfllKNkJB/KpQUIwMAqEjkfk4HyKeC9sfCHkjkXoZO28GypRR8Bd5M+/Qf\n\ + otFvcdRHqbvv+mj1y6nBIv0hv5eqJEil5s/dwGI7cexMGBjPVOPK63kbh6JlMcrb\n\ + 58jKid1VTzUbxxKm6YfL2aQpGp/veGPZRkm+x3DHoANYLYJ64WRQgBOGcf4QSqiT\n\ + xP9Y5ZxfQuheDzOkiQCt3ToTWwguXtVLm3AAUKxUhHVgMy2PQNFXcNsPWGCzhOW1\n\ + FzC82iZhuQi7SlTX7iA40np23nMkHu37hkHpfpipySxEIIjv1T0UglqN25hPlHDI\n\ + jrTRwcBVxikVhP0IFbDUtlmqSP5MkDEE2ZKTeD0ivd8c2WLO5RUoEICaTVHOx+Mx\n\ + OJ9L07ZhA2NMKiMMqhe0bXwZoFFHMUxXh8+iTTy89oE1PQ7xz/d6hJUtbqJ/N2xp\n\ + cWMNtnvbjWpxzwhjPGiqKx8GCtpGoAjpUeNqWL9V0a20rJBYqzJGLYfKDd+PW2XT\n\ + tOHbQwl0DFNq41jP4nYnaFo2YCjWb3mleRUWkU5SoUHq+vUvs4dxqKjlzvKnK5pc\n\ + yH9bnpKPaBI28QHtye7o25AfkOj7eHVSe5CV4u8okVaBEq1OFhBeWm+jx1fBrk82\n\ + hEGamuq1GZsZre2y9jauusOFcMXrV5oxJjBLLbGCi0i5ES0O+kBOlB/kY3hdkReC\n\ + HCJlMN7v92mkSsadahzwx3fTQWCwgVDg6LLN+xCPGFTMts4XDwg=\n\ + -----END OPENSSH PRIVATE KEY-----"; + + let private_key: SshPrivateKey = SshPrivateKey::from_pem_str(ssh_private_key_pem, passphrase).unwrap(); + + assert_eq!("test_with_pass2@picky.com".to_owned(), private_key.comment); + assert_eq!( + Kdf { + name: "bcrypt".to_owned(), + option: KdfOption { + salt: vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243], + rounds: 16, + } + }, + private_key.kdf + ); + assert_eq!("aes256-ctr", private_key.cipher_name); + } + + #[test] + fn encode_without_passphrase_2048() { + // ssh-keygen -t rsa -b 2048 -C "test2@picky.com" (without the passphrase) + let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\ + b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz\n\ + c2gtcnNhAAAAAwEAAQAAAQEAyPYbdoNqjj4EhuYblWIxVKLsmsOff+kLkKlFRsIJ\n\ + yE5YUWzPm5LyUH3LoqnL/rw/f/Og37oJO/bEn4P2lSvlf6ZagAGaLo8/8ACw4xKY\n\ + UsQFHAEfreIthd/T2u9TEnN+yPS99M99bXG2tV+6He4c61TJfYrq5DsgQuMXCFmt\n\ + R/IdJg8qF8lj06qEzjQ1HvXQdXruhm4sQn1HMb3VbdKQFSU3TpmzVysEaOVl3zK7\n\ + KirBU9gHIOFZuE3y0oUklFuK6jOhjgQnxeo58Rb00g3p7R+YcpI1i95TAoIQ/tYS\n\ + cjnZzByQv+ak1BjgfOjMbEeEQl6kvi2axqTEnFcg0IHu6wAAA8iqDGUDqgxlAwAA\n\ + AAdzc2gtcnNhAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhR\n\ + bM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUc\n\ + AR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0m\n\ + DyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT\n\ + 2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnM\n\ + HJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7rAAAAAwEAAQAAAQATZEw6H2xE\n\ + 1Y8yRTocLCF+fUo/lOjrOt22096veUHgZk73bHyMEp33Tmw8Ag6BQkEOY7/+VsFV\n\ + W/aVPfKpalb2/mJ1P7JVE9Wjny1ye/Te57NmhGU+LjkeVf7nfXiSqzpswdEisnL0\n\ + AKkUz2vyP2vi+YeH6cPIyjvOuIMcdyrVakejnGbss19ZoXw660X/7TRqG/41KhTm\n\ + lkN610JBKI2Rozecx9l3LZ3CTRpOOJ2sfssegvL+qxvvH1YVkRat4dwNZxsi+cho\n\ + zqWOciXrbzifBghBp0Upe5fgR2JRpyB6sMVXIHKkeP9YBQUARm1ECdbdJmPSiNYP\n\ + gMKpTaEObMahAAAAgCtugmDSAwIPibrD9MAbJB6KbN15heA6vTtCLOvFe1Hikw94\n\ + DYAJz+vlKadbOZW5SfGAOuIe7IynafthWm4RcbXEXxhnVtqHxzMHOZo/Mnoh+bUO\n\ + esDSoERyNHokpNK6m1NKbmQeFj4n7rkcrR8hrwX8+Ng8CsBEglDi+ULtVivbAAAA\n\ + gQD1vEPRUu9aD7CjkYgDyD2vNRRevARf01ImgT1tpiEA+GLHJ0xMetd7OH0wutAZ\n\ + uH26V19Kt4sWpsTwfdl2fIw7XHPc+G1OSqiOk6AS9qT/sy/VL1Wn7CqyAN2jikzn\n\ + quE6MbebTUJQSNHK9vQhn+u4hUDdEoMOLTYdWxxcjdJirQAAAIEA0VsOxBRDSTLc\n\ + Ar0Y97oCmb/6tU9XGAZwL2E14GVK85PnJNwHrx4aqb0qATE4iPLfE7ms+eBtT8Uj\n\ + HF0fxM3KDQiFSrvtgM4JjGTDS4dTYIBD/eQ0/aTaRgLOQqplyBgYVr3x7ATfcIP5\n\ + 961TfdiJ/QESutdb1KQquFXIMRII4vcAAAAPdGVzdDJAcGlja3kuY29tAQIDBA==\n\ + -----END OPENSSH PRIVATE KEY-----\x0A"; + + let private_key = SshPrivateKey::from_pem_str(ssh_private_key_pem, None).unwrap(); + let ssh_private_key_after = private_key.to_string().unwrap(); + + pretty_assertions::assert_eq!(ssh_private_key_pem, ssh_private_key_after.as_str()); + } + + #[test] + fn encode_with_passphrase_2048() { + // ssh-keygen -t rsa -b 2048 -C "test_with_pass2@picky.com" + let passphrase = Some("123123".to_string()); + let ssh_private_key_pem = "-----BEGIN OPENSSH PRIVATE KEY-----\n\ + b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABBI\n\ + MsVovOqXSrZa+iEvQwXzAAAAEAAAAAEAAAEXAAAAB3NzaC1yc2EAAAADAQABAAAB\n\ + AQCkR5WaC3NTPZdj9X/bX88YYbR2k5r3aE+I/ezxzbG6xIJi+So9AohypAhReyW9\n\ + 7XSGut5n6a9O+n/c9nCiXFVoyXbMSdM90Av5bu799+V4w3kBlRzN5D3A6uIZRjgl\n\ + wc3Xso9kthneNByB7OjZuSDdmuWE3YOgmW0TirP3dztbtVScLPZUSsEveIMt90aw\n\ + uOFWaEUshqb7l713bdEB0Tb77Z1wZpt6UmIgpraV58kN/ahepbY8lirMS4ym75wt\n\ + Be6PgyKGKIR3aNQdbfHYMHCgxNQMAFUt2yD9f+JE5HWG7kyKDcLTHCY60dtKTNfp\n\ + cByi4Bwm3209V4rGYSKAzFXvAAAD0CdYEpFE8Dda2GBNy1l5vDNdbyJvx7SSP49l\n\ + 4OmHsgRE2WneNC9CfO2IxPRNXsPEmXimeubqm6alsmJ1Ch+KsdjvyU7WIEnjuonC\n\ + lLWx6rhsuppJqZSICbikMUXjhlHpLirGnL0WoaBnLYEVYgu8cMbIgE9BNho+bS+1\n\ + qvyIrIdIblJwwc66CJKUYPz1yRA84WIMZOWlsYfeHnCvTGjiYUG2YFayVAuXvAz/\n\ + ND3bQYUlO34XOOsJvZxfQNEg1/tzhB7RvcGOG1InoQxT6dZtTp85CkTU/QQ6w2eY\n\ + j4qDDmsFm/eSDgEFfOJDLrfHsB4+G2aBZLmgk2bn7vo3JBkcPAETX6kKd7bkyEfh\n\ + LVph9i48vbmNJ8mXWiXMoRXqRgkKqBAMVnuXtbKVDVzzlZXIFu1cbuKyt0zUg7jB\n\ + IeIdG+5U0L6qygTjOKU6aP+dK1wRc0XyC8jxTJupt2eTEKBLzy4TwlLH5QhEcj1c\n\ + coV97PyslJ/NnQx8IKflHxxxQF4CbYgyXt9fWZpBfaD9TVWgsFoKrlZ9HOb6s5WJ\n\ + MwijgNLfllKNkJB/KpQUIwMAqEjkfk4HyKeC9sfCHkjkXoZO28GypRR8Bd5M+/Qf\n\ + otFvcdRHqbvv+mj1y6nBIv0hv5eqJEil5s/dwGI7cexMGBjPVOPK63kbh6JlMcrb\n\ + 58jKid1VTzUbxxKm6YfL2aQpGp/veGPZRkm+x3DHoANYLYJ64WRQgBOGcf4QSqiT\n\ + xP9Y5ZxfQuheDzOkiQCt3ToTWwguXtVLm3AAUKxUhHVgMy2PQNFXcNsPWGCzhOW1\n\ + FzC82iZhuQi7SlTX7iA40np23nMkHu37hkHpfpipySxEIIjv1T0UglqN25hPlHDI\n\ + jrTRwcBVxikVhP0IFbDUtlmqSP5MkDEE2ZKTeD0ivd8c2WLO5RUoEICaTVHOx+Mx\n\ + OJ9L07ZhA2NMKiMMqhe0bXwZoFFHMUxXh8+iTTy89oE1PQ7xz/d6hJUtbqJ/N2xp\n\ + cWMNtnvbjWpxzwhjPGiqKx8GCtpGoAjpUeNqWL9V0a20rJBYqzJGLYfKDd+PW2XT\n\ + tOHbQwl0DFNq41jP4nYnaFo2YCjWb3mleRUWkU5SoUHq+vUvs4dxqKjlzvKnK5pc\n\ + yH9bnpKPaBI28QHtye7o25AfkOj7eHVSe5CV4u8okVaBEq1OFhBeWm+jx1fBrk82\n\ + hEGamuq1GZsZre2y9jauusOFcMXrV5oxJjBLLbGCi0i5ES0O+kBOlB/kY3hdkReC\n\ + HCJlMN7v92mkSsadahzwx3fTQWCwgVDg6LLN+xCPGFTMts4XDwg=\n\ + -----END OPENSSH PRIVATE KEY-----\x0A"; + + let private_key = SshPrivateKey::from_pem_str(ssh_private_key_pem, passphrase).unwrap(); + let ssh_private_key_after = private_key.to_string().unwrap(); + + pretty_assertions::assert_eq!(ssh_private_key_pem, ssh_private_key_after.as_str()); + } + + #[rstest] + #[case(picky_test_data::SSH_PRIVATE_KEY_EC_P256)] + #[case(picky_test_data::SSH_PRIVATE_KEY_EC_P384)] + #[case(picky_test_data::SSH_PRIVATE_KEY_EC_P521)] + fn ecdsa_keys_unencrypted(#[case] pem: &str) { + let key = SshPrivateKey::from_pem_str(pem, None).unwrap(); + let encoded = key.to_string().unwrap(); + pretty_assertions::assert_eq!(encoded.as_str(), pem); + } + + #[test] + fn ecdsa_keys_encrypted() { + let passphrase = Some("test".to_string()); + let key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_EC_P256_ENCRYPTED, passphrase).unwrap(); + let encoded = key.to_string().unwrap(); + pretty_assertions::assert_eq!(encoded.as_str(), picky_test_data::SSH_PRIVATE_KEY_EC_P256_ENCRYPTED); + } + + #[test] + fn rsa_rounttrip() { + let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_RSA, None).unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_RSA, encoded.as_str()); + } + + #[test] + fn ed25519_roundtrip() { + let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_ED25519, None).unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_ED25519, encoded.as_str()); + } + + #[test] + fn ed25519_roundtrip_encrypted() { + let passphrase = Some("test".to_string()); + let private_key = + SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_ED25519_ENCRYPTED, passphrase).unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_ED25519_ENCRYPTED, encoded.as_str()); + } + + #[test] + fn sk_ed25519_roundtrip() { + let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519, None).unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519, encoded.as_str()); + } + + #[test] + fn sk_ed25519_roundtrip_encrypted() { + let private_key = SshPrivateKey::from_pem_str( + picky_test_data::SSH_PRIVATE_KEY_SK_ED25519_ENCRYPTED, + Some("test".to_string()), + ) + .unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ED25519_ENCRYPTED, encoded.as_str()); + } + + #[test] + fn sk_ecdsa_roundtrip() { + let private_key = SshPrivateKey::from_pem_str(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA, None).unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA, encoded.as_str()); + } + + #[test] + fn sk_ecdsa_roundtrip_encrypted() { + let private_key = SshPrivateKey::from_pem_str( + picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA_ENCRYPTED, + Some("test".to_string()), + ) + .unwrap(); + let encoded = private_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PRIVATE_KEY_SK_ECDSA_ENCRYPTED, encoded.as_str()); + } + + #[test] + fn test_rsa_private_key_generation() { + let private_key = SshPrivateKey::generate_rsa(2048, Option::Some("123".to_string()), None).unwrap(); + let data = private_key.to_pem().unwrap(); + let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap(); + + match parsed.base_key() { + SshBasePrivateKey::Rsa(key) => { + let _rsa: RsaPrivateKey = key.try_into().unwrap(); + } + _ => panic!("Invalid key type"), + } + } + + #[rstest] + #[case(EcCurve::NistP256)] + #[case(EcCurve::NistP384)] + #[case(EcCurve::NistP521)] + fn test_ec_private_key_generation(#[case] curve: EcCurve) { + let private_key = SshPrivateKey::generate_ec(curve, Option::Some("123".to_string()), None).unwrap(); + let data = private_key.to_pem().unwrap(); + let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap(); + + match parsed.base_key() { + SshBasePrivateKey::Ec(ec) => { + let _ec: EcdsaKeypair = ec.try_into().unwrap(); + } + _ => panic!("Invalid key type"), + } + } + + #[test] + fn test_ed_private_key_generation() { + let private_key = SshPrivateKey::generate_ed25519(Option::Some("123".to_string()), None).unwrap(); + let data = private_key.to_pem().unwrap(); + let parsed = SshPrivateKey::from_pem(&data, Option::Some("123".to_string())).unwrap(); + + match parsed.base_key() { + SshBasePrivateKey::Ed(ed) => { + let _ed: EdKeypair = ed.try_into().unwrap(); + } + _ => panic!("Invalid key type"), + } + } + + #[test] + fn kdf_option_decode() { + let mut cursor = Cursor::new(vec![ + 0, 0, 0, 24, 0, 0, 0, 16, 72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243, 0, 0, 0, + 16, + ]); + let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut cursor).unwrap(); + let KdfOption { salt, rounds } = kdf_option; + + assert_eq!( + vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243], + salt + ); + assert_eq!(16, rounds); + + let mut cursor = Cursor::new(vec![0, 0, 0, 0]); + let kdf_option: KdfOption = SshComplexTypeDecode::decode(&mut cursor).unwrap(); + let KdfOption { salt, rounds } = kdf_option; + + assert!(salt.is_empty()); + assert_eq!(0, rounds); + } + + #[test] + fn kdf_option_encode() { + let mut res: Vec = Vec::new(); + let kdf_option = KdfOption { + salt: vec![72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243], + rounds: 16, + }; + + kdf_option.encode(&mut res).unwrap(); + + assert_eq!( + vec![ + 0, 0, 0, 24, 0, 0, 0, 16, 72, 50, 197, 104, 188, 234, 151, 74, 182, 90, 250, 33, 47, 67, 5, 243, 0, 0, + 0, 16 + ], + res + ); + + res.clear(); + let kdf_option = KdfOption::default(); + kdf_option.encode(&mut res).unwrap(); + + assert_eq!(vec![0, 0, 0, 0], res); + } +} diff --git a/rust/vendor/picky/src/ssh/public_key.rs b/rust/vendor/picky/src/ssh/public_key.rs new file mode 100644 index 0000000..ce984ec --- /dev/null +++ b/rust/vendor/picky/src/ssh/public_key.rs @@ -0,0 +1,309 @@ +use crate::key::{KeyError, PublicKey}; +use crate::ssh::decode::SshComplexTypeDecode; +use crate::ssh::encode::SshComplexTypeEncode; + +use std::io; +use std::str::FromStr; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SshPublicKeyError { + #[error(transparent)] + IoError(#[from] io::Error), + #[error("invalid UTF-8")] + InvalidUtf8, + #[error(transparent)] + RsaError(#[from] rsa::errors::Error), + #[error(transparent)] + Base64DecodeError(#[from] base64::DecodeError), + #[error("Unknown key type. We only support RSA")] + UnknownKeyType, + #[error(transparent)] + KeyError(#[from] KeyError), +} + +impl From for SshPublicKeyError { + fn from(_: core::str::Utf8Error) -> Self { + Self::InvalidUtf8 + } +} + +impl From for SshPublicKeyError { + fn from(_: std::string::FromUtf8Error) -> Self { + Self::InvalidUtf8 + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SshBasePublicKey { + Rsa(PublicKey), + Ec(PublicKey), + Ed(PublicKey), + /// U2F ecdsa SSH key + SkEcdsaSha2NistP256 { + base_key: PublicKey, + application: String, + }, + /// U2F ed25519 SSH key + SkEd25519 { + base_key: PublicKey, + application: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SshPublicKey { + pub inner_key: SshBasePublicKey, + pub comment: String, +} + +impl SshPublicKey { + pub fn to_string(&self) -> Result { + let mut buffer = Vec::with_capacity(1024); + self.encode(&mut buffer)?; + Ok(String::from_utf8(buffer)?) + } + + pub fn inner_key(&self) -> &PublicKey { + match &self.inner_key { + SshBasePublicKey::Rsa(key) => key, + SshBasePublicKey::Ec(key) => key, + SshBasePublicKey::Ed(key) => key, + SshBasePublicKey::SkEcdsaSha2NistP256 { base_key, .. } => base_key, + SshBasePublicKey::SkEd25519 { base_key, .. } => base_key, + } + } + + pub fn fingerprint_md5(&self) -> Result<[u8; 16], SshPublicKeyError> { + use md5::{Digest, Md5}; + + let mut encoded = Vec::new(); + self.inner_key.encode(&mut encoded)?; + + let mut hasher = Md5::new(); + hasher.update(&encoded); + let fingerprint = hasher.finalize(); + + Ok(fingerprint.into()) + } + + pub fn fingerprint_sha1(&self) -> Result<[u8; 20], SshPublicKeyError> { + use sha1::{Digest, Sha1}; + + let mut encoded = Vec::new(); + self.inner_key.encode(&mut encoded)?; + + let mut hasher = Sha1::new(); + hasher.update(&encoded); + let fingerprint = hasher.finalize(); + + Ok(fingerprint.into()) + } + + pub fn fingerprint_sha256(&self) -> Result<[u8; 32], SshPublicKeyError> { + use sha2::{Digest, Sha256}; + + let mut encoded = Vec::new(); + self.inner_key.encode(&mut encoded)?; + + let mut hasher = Sha256::new(); + hasher.update(&encoded); + let fingerprint = hasher.finalize(); + + Ok(fingerprint.into()) + } +} + +impl FromStr for SshPublicKey { + type Err = SshPublicKeyError; + + fn from_str(s: &str) -> Result { + SshComplexTypeDecode::decode(s.as_bytes()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use base64::Engine; + use base64::engine::general_purpose::STANDARD_NO_PAD; + use crypto_bigint::BoxedUint; + use rstest::rstest; + + #[test] + fn decode_ssh_rsa_4096_public_key() { + // ssh-keygen -t rsa -b 4096 -C "test@picky.com" + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbUCK4dH1n4dOFBv/sjfMma4q5qe7SZ49j2GODGKr8DueZMWYLTck61uUMMlVBT3XyX6me6X4WsBoijzQWvgwpLCGTqlhQTntm5FphXHHkKxFvjMhPzCnHNS+L0ebzewcecsY5rtgw+6BhFwdZGhFBfif1/6s9q7y7+8Ge3hUIEqLdiMDDzxc66zIaW26jZxO4BMHuKp7Xln2JeDjsRHvz0vBNAddOfkvtp+gM72OH4tm9wS/V8bVOZ68oU0os8DuiEGnwA5RnjOjaFdHWt1mD8B+nRINxI8zYyQcqp3t4p552P0Frhvjgixi67Ryax0DUNuzN2MpQ0ORUgRkfy/xWvImUseP/BfqvNiWkFAWHNDDSsc50Wmr+g0JicG2gowHLYPxKRjLIbOq+JgxHrE4TdaA2NJoeUppJgWU4yuGl5fx1G+Bcdr0C+lsMj14Hp+aGajEOLQ7Mq3HzWEox9G1KgN4r266Mofd8T4vrjF6Ja9E+pp0pXgEv2cvtYJLP0qdrHWafb3lWsP4hJWnv/NaXP6ZAxiEeHsigrY98kmgZbHm/6AmiBJ7bKQ/S/PelYj3mTL0aYkGF79qVtAzSl7yI9yVyHsl7dt5jdmp6+IofuEtNfnAcfoaSLu0Ojotp9VBMvil6ojScbJNLBL8tGN4+urIcsNUvVjAOnwc3nothKw== test@picky.com\r\n"; + + let public_key = SshPublicKey::from_str(ssh_public_key).unwrap(); + + assert_eq!("test@picky.com".to_owned(), public_key.comment); + assert_eq!( + SshBasePublicKey::Rsa(PublicKey::from_rsa_components( + &BoxedUint::from_be_slice_vartime(&[ + 219, 80, 34, 184, 116, 125, 103, 225, 211, 133, 6, 255, 236, 141, 243, 38, 107, 138, 185, 169, 238, + 210, 103, 143, 99, 216, 99, 131, 24, 170, 252, 14, 231, 153, 49, 102, 11, 77, 201, 58, 214, 229, + 12, 50, 85, 65, 79, 117, 242, 95, 169, 158, 233, 126, 22, 176, 26, 34, 143, 52, 22, 190, 12, 41, + 44, 33, 147, 170, 88, 80, 78, 123, 102, 228, 90, 97, 92, 113, 228, 43, 17, 111, 140, 200, 79, 204, + 41, 199, 53, 47, 139, 209, 230, 243, 123, 7, 30, 114, 198, 57, 174, 216, 48, 251, 160, 97, 23, 7, + 89, 26, 17, 65, 126, 39, 245, 255, 171, 61, 171, 188, 187, 251, 193, 158, 222, 21, 8, 18, 162, 221, + 136, 192, 195, 207, 23, 58, 235, 50, 26, 91, 110, 163, 103, 19, 184, 4, 193, 238, 42, 158, 215, + 150, 125, 137, 120, 56, 236, 68, 123, 243, 210, 240, 77, 1, 215, 78, 126, 75, 237, 167, 232, 12, + 239, 99, 135, 226, 217, 189, 193, 47, 213, 241, 181, 78, 103, 175, 40, 83, 74, 44, 240, 59, 162, + 16, 105, 240, 3, 148, 103, 140, 232, 218, 21, 209, 214, 183, 89, 131, 240, 31, 167, 68, 131, 113, + 35, 204, 216, 201, 7, 42, 167, 123, 120, 167, 158, 118, 63, 65, 107, 134, 248, 224, 139, 24, 186, + 237, 28, 154, 199, 64, 212, 54, 236, 205, 216, 202, 80, 208, 228, 84, 129, 25, 31, 203, 252, 86, + 188, 137, 148, 177, 227, 255, 5, 250, 175, 54, 37, 164, 20, 5, 135, 52, 48, 210, 177, 206, 116, 90, + 106, 254, 131, 66, 98, 112, 109, 160, 163, 1, 203, 96, 252, 74, 70, 50, 200, 108, 234, 190, 38, 12, + 71, 172, 78, 19, 117, 160, 54, 52, 154, 30, 82, 154, 73, 129, 101, 56, 202, 225, 165, 229, 252, + 117, 27, 224, 92, 118, 189, 2, 250, 91, 12, 143, 94, 7, 167, 230, 134, 106, 49, 14, 45, 14, 204, + 171, 113, 243, 88, 74, 49, 244, 109, 74, 128, 222, 43, 219, 174, 140, 161, 247, 124, 79, 139, 235, + 140, 94, 137, 107, 209, 62, 166, 157, 41, 94, 1, 47, 217, 203, 237, 96, 146, 207, 210, 167, 107, + 29, 102, 159, 111, 121, 86, 176, 254, 33, 37, 105, 239, 252, 214, 151, 63, 166, 64, 198, 33, 30, + 30, 200, 160, 173, 143, 124, 146, 104, 25, 108, 121, 191, 232, 9, 162, 4, 158, 219, 41, 15, 210, + 252, 247, 165, 98, 61, 230, 76, 189, 26, 98, 65, 133, 239, 218, 149, 180, 12, 210, 151, 188, 136, + 247, 37, 114, 30, 201, 123, 118, 222, 99, 118, 106, 122, 248, 138, 31, 184, 75, 77, 126, 112, 28, + 126, 134, 146, 46, 237, 14, 142, 139, 105, 245, 80, 76, 190, 41, 122, 162, 52, 156, 108, 147, 75, + 4, 191, 45, 24, 222, 62, 186, 178, 28, 176, 213, 47, 86, 48, 14, 159, 7, 55, 158, 139, 97, 43 + ]), + &BoxedUint::from_be_slice_vartime(&[1, 0, 1]) + )), + public_key.inner_key + ); + } + + #[test] + fn decode_ssh_rsa_2048_public_key() { + // ssh-keygen -t rsa -b 2048 -C "test2@picky.com" + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n"; + + let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap(); + + assert_eq!("test2@picky.com".to_owned(), public_key.comment); + assert_eq!( + SshBasePublicKey::Rsa(PublicKey::from_rsa_components( + &BoxedUint::from_be_slice_vartime(&[ + 200, 246, 27, 118, 131, 106, 142, 62, 4, 134, 230, 27, 149, 98, 49, 84, 162, 236, 154, 195, 159, + 127, 233, 11, 144, 169, 69, 70, 194, 9, 200, 78, 88, 81, 108, 207, 155, 146, 242, 80, 125, 203, + 162, 169, 203, 254, 188, 63, 127, 243, 160, 223, 186, 9, 59, 246, 196, 159, 131, 246, 149, 43, 229, + 127, 166, 90, 128, 1, 154, 46, 143, 63, 240, 0, 176, 227, 18, 152, 82, 196, 5, 28, 1, 31, 173, 226, + 45, 133, 223, 211, 218, 239, 83, 18, 115, 126, 200, 244, 189, 244, 207, 125, 109, 113, 182, 181, + 95, 186, 29, 238, 28, 235, 84, 201, 125, 138, 234, 228, 59, 32, 66, 227, 23, 8, 89, 173, 71, 242, + 29, 38, 15, 42, 23, 201, 99, 211, 170, 132, 206, 52, 53, 30, 245, 208, 117, 122, 238, 134, 110, 44, + 66, 125, 71, 49, 189, 213, 109, 210, 144, 21, 37, 55, 78, 153, 179, 87, 43, 4, 104, 229, 101, 223, + 50, 187, 42, 42, 193, 83, 216, 7, 32, 225, 89, 184, 77, 242, 210, 133, 36, 148, 91, 138, 234, 51, + 161, 142, 4, 39, 197, 234, 57, 241, 22, 244, 210, 13, 233, 237, 31, 152, 114, 146, 53, 139, 222, + 83, 2, 130, 16, 254, 214, 18, 114, 57, 217, 204, 28, 144, 191, 230, 164, 212, 24, 224, 124, 232, + 204, 108, 71, 132, 66, 94, 164, 190, 45, 154, 198, 164, 196, 156, 87, 32, 208, 129, 238, 235 + ]), + &BoxedUint::from_be_slice_vartime(&[1, 0, 1]) + )), + public_key.inner_key + ); + } + + #[test] + fn encode_ssh_rsa_4096_public_key() { + // ssh-keygen -t rsa -b 4096 -C "test@picky.com" + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDbUCK4dH1n4dOFBv/sjfMma4q5qe7SZ49j2GODGKr8DueZMWYLTck61uUMMlVBT3XyX6me6X4WsBoijzQWvgwpLCGTqlhQTntm5FphXHHkKxFvjMhPzCnHNS+L0ebzewcecsY5rtgw+6BhFwdZGhFBfif1/6s9q7y7+8Ge3hUIEqLdiMDDzxc66zIaW26jZxO4BMHuKp7Xln2JeDjsRHvz0vBNAddOfkvtp+gM72OH4tm9wS/V8bVOZ68oU0os8DuiEGnwA5RnjOjaFdHWt1mD8B+nRINxI8zYyQcqp3t4p552P0Frhvjgixi67Ryax0DUNuzN2MpQ0ORUgRkfy/xWvImUseP/BfqvNiWkFAWHNDDSsc50Wmr+g0JicG2gowHLYPxKRjLIbOq+JgxHrE4TdaA2NJoeUppJgWU4yuGl5fx1G+Bcdr0C+lsMj14Hp+aGajEOLQ7Mq3HzWEox9G1KgN4r266Mofd8T4vrjF6Ja9E+pp0pXgEv2cvtYJLP0qdrHWafb3lWsP4hJWnv/NaXP6ZAxiEeHsigrY98kmgZbHm/6AmiBJ7bKQ/S/PelYj3mTL0aYkGF79qVtAzSl7yI9yVyHsl7dt5jdmp6+IofuEtNfnAcfoaSLu0Ojotp9VBMvil6ojScbJNLBL8tGN4+urIcsNUvVjAOnwc3nothKw== test@picky.com\r\n"; + let public_key = SshPublicKey::from_str(ssh_public_key).unwrap(); + + let ssh_public_key_after = public_key.to_string().unwrap(); + + assert_eq!(ssh_public_key, ssh_public_key_after.as_str()); + } + + #[test] + fn encode_ssh_rsa_2048_public_key() { + // ssh-keygen -t rsa -b 4096 -C "test@picky.com" + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n"; + let public_key = SshPublicKey::from_str(ssh_public_key).unwrap(); + + let ssh_public_key_after = public_key.to_string().unwrap(); + + assert_eq!(ssh_public_key, ssh_public_key_after.as_str()); + } + + #[test] + fn rsa_roundtrip() { + let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_RSA).unwrap(); + let ssh_public_key_after = public_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PUBLIC_KEY_RSA, ssh_public_key_after.as_str()); + } + + #[rstest] + #[case(picky_test_data::SSH_PUBLIC_KEY_EC_P256)] + #[case(picky_test_data::SSH_PUBLIC_KEY_EC_P384)] + #[case(picky_test_data::SSH_PUBLIC_KEY_EC_P521)] + fn ecdsa_roundtrip(#[case] key_str: &str) { + let public_key = SshPublicKey::from_str(key_str).unwrap(); + let ssh_public_key_after = public_key.to_string().unwrap(); + assert_eq!(key_str, ssh_public_key_after.as_str()); + } + + #[test] + fn ed25519_roundtrip() { + let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_ED25519).unwrap(); + let ssh_public_key_after = public_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PUBLIC_KEY_ED25519, ssh_public_key_after.as_str()); + } + + #[test] + fn sk_ed25519_roundtrip() { + let public_key: SshPublicKey = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_SK_ED25519).unwrap(); + let ssh_public_key_after = public_key.to_string().unwrap(); + assert_eq!( + picky_test_data::SSH_PUBLIC_KEY_SK_ED25519, + ssh_public_key_after.as_str() + ); + } + + #[test] + fn sk_ecdsa_roundtrip() { + let public_key = SshPublicKey::from_str(picky_test_data::SSH_PUBLIC_KEY_SK_ECDSA).unwrap(); + let ssh_public_key_after = public_key.to_string().unwrap(); + assert_eq!(picky_test_data::SSH_PUBLIC_KEY_SK_ECDSA, ssh_public_key_after.as_str()); + } + + #[test] + fn fingerprint_md5_ssh_rsa_2048_public_key() { + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n"; + + let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap(); + let md5 = hex::encode(public_key.fingerprint_md5().unwrap()); + + assert_eq!(md5, "7b6b9cc2e44452aec58c3a0a31d6258d"); + } + + #[test] + fn fingerprint_sha1_ssh_rsa_2048_public_key() { + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n"; + + let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap(); + let sha1 = STANDARD_NO_PAD.encode(public_key.fingerprint_sha1().unwrap()); + + assert_eq!(sha1, "ezHoULh4V/R9NybfxCW2pL9ADcU"); + } + + #[test] + fn fingerprint_sha256_ssh_rsa_2048_public_key() { + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDI9ht2g2qOPgSG5huVYjFUouyaw59/6QuQqUVGwgnITlhRbM+bkvJQfcuiqcv+vD9/86Dfugk79sSfg/aVK+V/plqAAZoujz/wALDjEphSxAUcAR+t4i2F39Pa71MSc37I9L30z31tcba1X7od7hzrVMl9iurkOyBC4xcIWa1H8h0mDyoXyWPTqoTONDUe9dB1eu6GbixCfUcxvdVt0pAVJTdOmbNXKwRo5WXfMrsqKsFT2Acg4Vm4TfLShSSUW4rqM6GOBCfF6jnxFvTSDentH5hykjWL3lMCghD+1hJyOdnMHJC/5qTUGOB86MxsR4RCXqS+LZrGpMScVyDQge7r test2@picky.com\r\n"; + + let public_key: SshPublicKey = SshPublicKey::from_str(ssh_public_key).unwrap(); + + let sha256 = STANDARD_NO_PAD.encode(public_key.fingerprint_sha256().unwrap()); + + assert_eq!(sha256, "cTXkM4frGl07u46Bhzy+YMOS01lX51oE2j6STi7g568"); + } + + #[test] + fn decode_ssh_rsa_2024_public_key_with_multiwords_comment() { + // ssh-keygen -t rsa -b 2048 -C "test using several words" + let ssh_public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+76yL1+ocu4iuVam4VO5YlODohmHbIuhjyQgiMGS8ZtdFKcltzAH0ot4zJDH/Z3ja6xO2IOc/4+UMABgPOgFwmzyl414/Zo42CYqk9OB5GJylFYI99HrATqH03Wz2qJ3dzP6QJVf8g05hY27RKaU5H+0fo471SACeHet9uqstRecsUcauPS91xwpPhrcpRXjGH1yLBdWTpDq5R6c1Wgh9SVuzY/ITMB3pq8rzwal8e2rR4T+wHc48l61LGwmuOTkhAo5/0sn72CzKWQZVd0CarfCr3biCW7cUai0FvH79aAfIBV/FIMXgtgqdpY/Qg7v+JWIyJk/OB8Be1ix8YVRV test using several words\r\n"; + + let public_key = SshPublicKey::from_str(ssh_public_key).unwrap(); + + assert_eq!(public_key.comment, "test using several words"); + } +} diff --git a/rust/vendor/picky/src/ssh/sshtime.rs b/rust/vendor/picky/src/ssh/sshtime.rs new file mode 100644 index 0000000..418a916 --- /dev/null +++ b/rust/vendor/picky/src/ssh/sshtime.rs @@ -0,0 +1,131 @@ +// TODO: support `SshTime` without `chrono` nor `time` +#[cfg(not(any(feature = "chrono_conversion", feature = "time_conversion")))] +compile_error!( + "Either feature \"chrono_conversion\" or \"time_conversion\" must be enabled when the feature \"ssh\" is set." +); + +pub use time_impl::SshTime; + +#[cfg(feature = "time_conversion")] +mod time_impl { + use time::OffsetDateTime; + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub struct SshTime(pub(crate) OffsetDateTime); + + impl SshTime { + pub fn now() -> Self { + Self(OffsetDateTime::now_utc()) + } + + pub fn from_timestamp(timestamp: u64) -> Self { + Self(OffsetDateTime::from_unix_timestamp(timestamp as i64).unwrap()) + } + + pub fn timestamp(&self) -> u64 { + self.0.unix_timestamp() as u64 + } + + pub fn month(&self) -> u8 { + u8::from(self.0.month()) + } + + pub fn day(&self) -> u8 { + self.0.day() + } + + pub fn hour(&self) -> u8 { + self.0.hour() + } + + pub fn minute(&self) -> u8 { + self.0.minute() + } + + pub fn second(&self) -> u8 { + self.0.second() + } + + pub fn year(&self) -> u16 { + self.0.year().try_into().unwrap() + } + } + + impl From for OffsetDateTime { + fn from(time: SshTime) -> Self { + time.0 + } + } + + impl From for SshTime { + fn from(time: OffsetDateTime) -> Self { + Self::from_timestamp(time.unix_timestamp() as u64) + } + } + + impl From for u64 { + fn from(time: SshTime) -> u64 { + time.0.unix_timestamp() as u64 + } + } +} + +#[cfg(all(feature = "chrono_conversion", not(feature = "time_conversion")))] +mod time_impl { + use chrono::{DateTime, Utc}; + pub use chrono::{Datelike, Timelike}; + use std::time::{Duration, SystemTime, UNIX_EPOCH}; + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub struct SshTime(pub(crate) DateTime); + + impl SshTime { + pub fn now() -> Self { + SshTime(DateTime::::from(SystemTime::now())) + } + + pub fn from_timestamp(timestamp: u64) -> Self { + Self(DateTime::::from(UNIX_EPOCH + Duration::from_secs(timestamp))) + } + + pub fn timestamp(&self) -> u64 { + self.0.timestamp() as u64 + } + + pub fn month(&self) -> u8 { + self.0.month().try_into().unwrap() + } + + pub fn day(&self) -> u8 { + self.0.day().try_into().unwrap() + } + + pub fn hour(&self) -> u8 { + self.0.hour().try_into().unwrap() + } + + pub fn minute(&self) -> u8 { + self.0.minute().try_into().unwrap() + } + + pub fn second(&self) -> u8 { + self.0.second().try_into().unwrap() + } + + pub fn year(&self) -> u16 { + self.0.year().try_into().unwrap() + } + } + + impl From> for SshTime { + fn from(date: DateTime) -> Self { + Self(date) + } + } + + impl From for DateTime { + fn from(time: SshTime) -> Self { + time.0 + } + } +} diff --git a/rust/vendor/picky/src/x509/certificate.rs b/rust/vendor/picky/src/x509/certificate.rs new file mode 100644 index 0000000..a3d64f9 --- /dev/null +++ b/rust/vendor/picky/src/x509/certificate.rs @@ -0,0 +1,1677 @@ +use super::utils::{from_der, from_pem, from_pem_str, to_der, to_pem}; +use crate::hash::HashAlgorithm; +use crate::key::{KeyError, PrivateKey, PublicKey}; +use crate::pem::{Pem, PemError}; +use crate::signature::{SignatureAlgorithm, SignatureError}; +use crate::x509::csr::{Csr, CsrError}; +use crate::x509::date::UtcDate; +use crate::x509::key_id_gen_method::{KeyIdGenError, KeyIdGenMethod}; +use crate::x509::name::{DirectoryName, GeneralNames}; +use picky_asn1::bit_string::BitString; +use picky_asn1::wrapper::{ExplicitContextTag0, ExplicitContextTag3, IntegerAsn1}; +use picky_asn1_der::{Asn1DerError, Asn1RawDer}; +use picky_asn1_x509::{ + AlgorithmIdentifier, AuthorityKeyIdentifier, BasicConstraints, Certificate, ExtendedKeyUsage, Extension, + ExtensionView, Extensions, KeyIdentifier, KeyUsage, Name, SubjectPublicKeyInfo, TbsCertificate, Validity, Version, + oids, +}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use thiserror::Error; + +const ELEMENT_NAME: &str = "x509 certificate"; + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum CertError { + /// couldn't generate certificate + #[error("couldn't generate certificate: {source}")] + CertGeneration { source: Box }, + + /// invalid certificate + #[error("invalid certificate '{id}': {source}")] + InvalidCertificate { id: String, source: Box }, + + /// ASN1 serialization error + #[error("(ASN1) couldn't serialize {element}: {source}")] + Asn1Serialization { + element: &'static str, + source: Asn1DerError, + }, + + /// ASN1 deserialization error + #[error("(ASN1) couldn't deserialize {element}: {source}")] + Asn1Deserialization { + element: &'static str, + source: Asn1DerError, + }, + + /// signature error + #[error("signature error: {source}")] + Signature { source: SignatureError }, + + /// key id generation error + #[error("key id generation error: {source}")] + KeyIdGen { source: KeyIdGenError }, + + /// CA chain error + #[error("CA chain error: {source}")] + InvalidChain { source: CaChainError }, + + /// CSR error + #[error("CSR error: {source}")] + InvalidCsr { source: CsrError }, + + /// extension not found + #[error("extension not found: {name}")] + ExtensionNotFound { name: &'static str }, + + /// missing required builder argument + #[error("missing required builder argument `{arg}`")] + MissingBuilderArgument { arg: &'static str }, + + /// certificate is not yet valid + #[error("certificate is not yet valid (not before: {not_before}, now: {now})")] + CertificateNotYetValid { not_before: UtcDate, now: UtcDate }, + + /// certificate expired + #[error("certificate expired (not after: {not_after}, now: {now})")] + CertificateExpired { not_after: UtcDate, now: UtcDate }, + + /// invalid PEM label error + #[error("invalid PEM label: {label}")] + InvalidPemLabel { label: String }, + + /// invalid PEM provided + #[error("invalid PEM provided: {source}")] + Pem { source: PemError }, + + #[error("failed to get public key from private key: {source}")] + PrivateKeyToPublicKey { source: KeyError }, +} + +impl From for CertError { + fn from(e: PemError) -> Self { + Self::Pem { source: e } + } +} + +#[derive(Debug, Error)] +pub enum CaChainError { + /// chain depth does't satisfy basic constraints extension + #[error( + "chain depth doesn't satisfy basic constraints extension: certificate '{cert_id}' has pathlen of {pathlen}" + )] + TooDeep { cert_id: String, pathlen: u8 }, + + /// chain is missing a root certificate + #[error("chain is missing a root certificate")] + NoRoot, + + /// issuer certificate is not a CA + #[error("issuer certificate '{issuer_id}' is not a CA")] + IssuerIsNotCA { issuer_id: String }, + + /// authority key id doesn't match + #[error( + "authority key id doesn't match (expected: {}, got: {})", + base64::encode(expected), + base64::encode(actual) + )] + AuthorityKeyIdMismatch { expected: Vec, actual: Vec }, + + /// issuer name doesn't match + #[error("issuer name doesn't match (expected: {expected}, got: {actual})")] + IssuerNameMismatch { expected: String, actual: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CertType { + Root, + Intermediate, + Leaf, + Unknown, +} + +const CERT_PEM_LABELS: &[&str] = &["CERTIFICATE", "TRUSTED CERTIFICATE", "X509 CERTIFICATE"]; + +/// CertificateOverview is used to validate signatures (using tbs_certificate der encoding) and encode back original certificate as is. +/// Refer PSDiagnostics PowerShell module authenticode test for details as to why this is useful. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +struct CertificateOverview { + tbs_certificate: Asn1RawDer, + signature_algorithm: Asn1RawDer, + signature_value: Asn1RawDer, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Cert { + details: Certificate, + overview: CertificateOverview, +} + +impl TryFrom for Cert { + type Error = CertError; + + fn try_from(certificate: Certificate) -> Result { + let der = picky_asn1_der::to_vec(&certificate).map_err(|source| CertError::Asn1Serialization { + element: "certificate", + source, + })?; + let overview = picky_asn1_der::from_bytes(&der).map_err(|source| CertError::Asn1Deserialization { + element: "certificate", + source, + })?; + Ok(Self { + details: certificate, + overview, + }) + } +} + +impl From for Certificate { + fn from(certificate: Cert) -> Self { + certificate.details + } +} + +macro_rules! find_ext { + ($oid:expr, $certificate:ident, $ext_name:literal) => {{ + let key_identifier_oid = $oid; + ($certificate.tbs_certificate.extensions.0) + .0 + .iter() + .find(|ext| ext.extn_id() == &key_identifier_oid) + .ok_or(CertError::ExtensionNotFound { name: $ext_name }) + }}; +} + +impl Cert { + pub fn from_der>(der: &T) -> Result { + Ok(Self { + details: from_der(der, ELEMENT_NAME)?, + overview: from_der(der, ELEMENT_NAME)?, + }) + } + + pub fn from_pem(pem: &Pem) -> Result { + Ok(Self { + details: from_pem(pem, CERT_PEM_LABELS, ELEMENT_NAME)?, + overview: from_pem(pem, CERT_PEM_LABELS, ELEMENT_NAME)?, + }) + } + + pub fn from_pem_str(pem_str: &str) -> Result { + Ok(Self { + details: from_pem_str(pem_str, CERT_PEM_LABELS, ELEMENT_NAME)?, + overview: from_pem_str(pem_str, CERT_PEM_LABELS, ELEMENT_NAME)?, + }) + } + + pub fn to_der(&self) -> Result, CertError> { + to_der(&self.overview, ELEMENT_NAME) + } + + pub fn to_pem(&self) -> Result, CertError> { + to_pem(&self.overview, CERT_PEM_LABELS[0], ELEMENT_NAME) + } + + pub fn ty(&self) -> CertType { + if let Some(ca) = self.basic_constraints().map(|bc| bc.ca()).unwrap_or(None) { + if ca { + if self.subject_name() == self.issuer_name() { + CertType::Root + } else { + CertType::Intermediate + } + } else { + CertType::Leaf + } + } else { + CertType::Unknown + } + } + + pub fn serial_number(&self) -> &IntegerAsn1 { + &self.details.tbs_certificate.serial_number + } + + pub fn signature_algorithm(&self) -> &AlgorithmIdentifier { + &self.details.tbs_certificate.signature + } + + pub fn valid_not_before(&self) -> UtcDate { + self.details.tbs_certificate.validity.not_before.clone().into() + } + + pub fn valid_not_after(&self) -> UtcDate { + self.details.tbs_certificate.validity.not_after.clone().into() + } + + pub fn subject_key_identifier(&self) -> Result<&[u8], CertError> { + let certificate = &self.details; + + let ext = find_ext!(oids::subject_key_identifier(), certificate, "subject key identifier")?; + match ext.extn_value() { + ExtensionView::SubjectKeyIdentifier(ski) => Ok(&ski.0), + _ => unreachable!("invalid extension (expected subject key identifier)"), + } + } + + pub fn authority_key_identifier(&self) -> Result<&AuthorityKeyIdentifier, CertError> { + let certificate = &self.details; + + let ext = find_ext!( + oids::authority_key_identifier(), + certificate, + "authority key identifier" + )?; + match ext.extn_value() { + ExtensionView::AuthorityKeyIdentifier(aki) => Ok(aki), + _ => unreachable!("invalid extension (expected authority key identifier)"), + } + } + + pub fn basic_constraints(&self) -> Result<&BasicConstraints, CertError> { + let certificate = &self.details; + let ext = find_ext!(oids::basic_constraints(), certificate, "basic constraints")?; + match ext.extn_value() { + ExtensionView::BasicConstraints(bc) => Ok(bc), + _ => unreachable!("invalid extension (expected basic constraints)"), + } + } + + pub fn subject_name(&self) -> DirectoryName { + self.details.tbs_certificate.subject.clone().into() + } + + pub fn issuer_name(&self) -> DirectoryName { + self.details.tbs_certificate.issuer.clone().into() + } + + pub fn extensions(&self) -> &[Extension] { + (self.details.tbs_certificate.extensions.0).0.as_slice() + } + + pub fn public_key(&self) -> &PublicKey { + (&self.details.tbs_certificate.subject_public_key_info).into() + } + + pub fn into_public_key(self) -> PublicKey { + self.details.tbs_certificate.subject_public_key_info.into() + } + + pub fn is_parent_of(&self, other: &Cert) -> Result<(), CertError> { + if let Ok(other_aki) = other.authority_key_identifier() { + if let Some(other_aki) = other_aki.key_identifier() { + let parent_ski = self + .subject_key_identifier() + .map_err(|e| CertError::InvalidCertificate { + source: Box::new(e), + id: self.subject_name().to_string(), + })?; + + if parent_ski != other_aki { + return Err(CaChainError::AuthorityKeyIdMismatch { + expected: other_aki.to_vec(), + actual: parent_ski.to_vec(), + }) + .map_err(|e| CertError::InvalidChain { source: e }) + .map_err(|e| CertError::InvalidCertificate { + source: Box::new(e), + id: other.subject_name().to_string(), + }); + } + } + } + + let other_issuer_name = other.issuer_name(); + let self_subject_name = self.subject_name(); + if other_issuer_name != self_subject_name { + return Err(CaChainError::IssuerNameMismatch { + expected: other_issuer_name.to_string(), + actual: self_subject_name.to_string(), + }) + .map_err(|e| CertError::InvalidChain { source: e }) + .map_err(|e| CertError::InvalidCertificate { + source: Box::new(e), + id: other.subject_name().to_string(), + }); + } + + Ok(()) + } + + pub fn verifier<'a, 'b, Chain: Iterator>(&'a self) -> CertValidator<'a, 'b, Chain> { + CertValidator { + cert: self, + inner: RefCell::new(CertValidatorInner { + strictness: Default::default(), + now: None, + chain: None, + }), + } + } +} + +// === certificate verifier === / + +#[derive(Debug, Clone)] +pub(super) enum ValidityCheck<'a> { + Interval { lower: &'a UtcDate, upper: &'a UtcDate }, + Exact(&'a UtcDate), +} + +#[derive(Debug, Clone)] +struct CheckStrictness { + require_not_before_check: bool, + require_not_after_check: bool, + require_chain_check: bool, + chain_should_contains_root_certificate: bool, +} + +impl Default for CheckStrictness { + fn default() -> Self { + Self { + require_not_before_check: true, + require_not_after_check: true, + require_chain_check: true, + chain_should_contains_root_certificate: true, + } + } +} + +#[derive(Clone, Debug)] +struct CertValidatorInner<'a, 'b, Chain: Iterator> { + strictness: CheckStrictness, + now: Option>, + chain: Option, +} + +/// Utility to verify x509 `Cert`s +#[derive(Clone, Debug)] +pub struct CertValidator<'a, 'b, Chain: Iterator> { + cert: &'a Cert, + inner: RefCell>, +} + +impl<'a, 'b, Chain: Iterator> CertValidator<'a, 'b, Chain> { + #[inline] + pub fn exact_date(&self, exact: &'a UtcDate) -> &Self { + self.inner.borrow_mut().now = Some(ValidityCheck::Exact(exact)); + self + } + + #[inline] + pub fn interval_date(&self, lower: &'a UtcDate, upper: &'a UtcDate) -> &Self { + self.inner.borrow_mut().now = Some(ValidityCheck::Interval { lower, upper }); + self + } + + #[inline] + pub fn chain(&self, chain: Chain) -> &Self { + self.inner.borrow_mut().chain = Some(chain); + self + } + + #[inline] + pub fn require_not_before_check(&self) -> &Self { + self.inner.borrow_mut().strictness.require_not_before_check = true; + self + } + + #[inline] + pub fn require_not_after_check(&self) -> &Self { + self.inner.borrow_mut().strictness.require_not_after_check = true; + self + } + + #[inline] + pub fn require_chain_check(&self) -> &Self { + self.inner.borrow_mut().strictness.require_chain_check = true; + self + } + + #[inline] + pub fn ignore_not_before_check(&self) -> &Self { + self.inner.borrow_mut().strictness.require_not_before_check = false; + self + } + + #[inline] + pub fn ignore_not_after_check(&self) -> &Self { + self.inner.borrow_mut().strictness.require_not_after_check = false; + self + } + + #[inline] + pub fn ignore_chain_check(&self) -> &Self { + self.inner.borrow_mut().strictness.require_chain_check = false; + self + } + + #[inline] + #[allow(dead_code)] + pub(super) fn chain_should_contains_root_certificate(&self, should_contains: bool) -> &Self { + self.inner + .borrow_mut() + .strictness + .chain_should_contains_root_certificate = should_contains; + self + } + + pub fn verify(&self) -> Result<(), CertError> { + let mut inner = self.inner.borrow_mut(); + + if (inner.strictness.require_not_after_check || inner.strictness.require_not_before_check) + && inner.now.is_none() + { + return Err(CertError::MissingBuilderArgument { arg: "now" }); + } + + if let Some(now) = &inner.now { + verify_cert_validity(self.cert, &inner.strictness, now.clone()).map_err(|e| { + CertError::InvalidCertificate { + source: Box::new(e), + id: self.cert.subject_name().to_string(), + } + })?; + } + + if !inner.strictness.require_chain_check { + return Ok(()); + } + + let chain = if let Some(chain) = inner.chain.take() { + chain + } else { + return Err(CertError::MissingBuilderArgument { arg: "chain" }); + }; + + let mut current_cert = self.cert; + + for (number_certs, parent_cert) in chain.enumerate() { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 + // The cA boolean indicates whether the certified public key may be used + // to verify certificate signatures. If the cA boolean is not asserted, + // then the keyCertSign bit in the key usage extension MUST NOT be + // asserted. If the basic constraints extension is not present in a + // version 3 certificate, or the extension is present but the cA boolean + // is not asserted, then the certified public key MUST NOT be used to + // verify certificate signatures. + match parent_cert + .basic_constraints() + .map(|bc| (bc.ca(), bc.pathlen())) + .unwrap_or((None, None)) + { + (None | Some(false), _) => { + return Err(CaChainError::IssuerIsNotCA { + issuer_id: parent_cert.subject_name().to_string(), + }) + .map_err(|e| CertError::InvalidChain { source: e }); + } + (_, Some(pathlen)) if usize::from(pathlen) < number_certs => { + return Err(CaChainError::TooDeep { + cert_id: parent_cert.subject_name().to_string(), + pathlen, + }) + .map_err(|e| CertError::InvalidChain { source: e }); + } + _ => {} + } + + // verify parent validity + if let Some(now) = &inner.now { + verify_cert_validity(parent_cert, &inner.strictness, now.clone()).map_err(|e| { + CertError::InvalidCertificate { + source: Box::new(e), + id: parent_cert.subject_name().to_string(), + } + })?; + } + + // check parent_cert is the parent of current_cert + parent_cert.is_parent_of(current_cert)?; + + // validate current cert signature using parent public key + let hash_type = SignatureAlgorithm::from_algorithm_identifier(¤t_cert.details.signature_algorithm) + .map_err(|e| CertError::Signature { source: e })?; + let public_key = &parent_cert.details.tbs_certificate.subject_public_key_info; + hash_type + .verify( + &public_key.clone().into(), + ¤t_cert.overview.tbs_certificate.0, + current_cert.details.signature_value.0.payload_view(), + ) + .map_err(|e| CertError::Signature { source: e }) + .map_err(|e| CertError::InvalidCertificate { + source: Box::new(e), + id: current_cert.subject_name().to_string(), + })?; + + current_cert = parent_cert; + } + + // make sure `current_cert` (the last certificate of the chain) is a root CA + if inner.strictness.chain_should_contains_root_certificate && current_cert.ty() != CertType::Root { + return Err(CaChainError::NoRoot).map_err(|e| CertError::InvalidChain { source: e }); + } + + Ok(()) + } +} + +fn verify_cert_validity(cert: &Cert, strictness: &CheckStrictness, now: ValidityCheck<'_>) -> Result<(), CertError> { + let validity = &cert.details.tbs_certificate.validity; + let not_before: UtcDate = validity.not_before.clone().into(); + let not_after: UtcDate = validity.not_after.clone().into(); + + match now { + ValidityCheck::Interval { lower, upper } => { + if not_before.gt(upper) && strictness.require_not_before_check { + return Err(CertError::CertificateNotYetValid { + not_before, + now: upper.clone(), + }); + } + + if not_after.lt(lower) && strictness.require_not_after_check { + return Err(CertError::CertificateExpired { + not_after, + now: lower.clone(), + }); + } + } + ValidityCheck::Exact(now) => { + if not_before.gt(now) && strictness.require_not_before_check { + return Err(CertError::CertificateNotYetValid { + not_before, + now: now.clone(), + }); + } + + if not_after.lt(now) && strictness.require_not_after_check { + return Err(CertError::CertificateExpired { + not_after, + now: now.clone(), + }); + } + } + } + + Ok(()) +} + +// === builder === // + +#[derive(Clone, Debug)] +enum SubjectInfos { + Csr(Csr), + NameAndPublicKey { name: DirectoryName, public_key: PublicKey }, +} + +#[derive(Clone, Debug)] +struct IssuerInfos<'a> { + name: DirectoryName, + key: &'a PrivateKey, + self_signed: bool, +} + +// Statically checks the field actually exists and returns a &'static str of the field name +macro_rules! field_str { + ($field:ident) => {{ + const _: fn() = || { + let CertificateBuilderInner { $field: _, .. }; + }; + stringify!($field) + }}; +} + +#[derive(Default, Clone, Debug)] +struct CertificateBuilderInner<'a> { + valid_from: Option, + valid_to: Option, + subject_infos: Option, + issuer_infos: Option>, + authority_key_identifier: Option>, + ca: Option, + pathlen: Option, + signature_hash_type: Option, + key_id_gen_method: Option, + key_usage: Option, + extended_key_usage: Option, + subject_alt_name: Option, + issuer_alt_name: Option, + serial_number: Option>, + inherit_extensions_from_csr_attributes: bool, +} + +#[derive(Default, Clone, Debug)] +pub struct CertificateBuilder<'a> { + inner: RefCell>, +} + +impl<'a> CertificateBuilder<'a> { + pub fn new() -> Self { + Self::default() + } + + /// Required + #[inline] + pub fn validity(&self, valid_from: UtcDate, valid_to: UtcDate) -> &Self { + let mut inner_mut = self.inner.borrow_mut(); + inner_mut.valid_from = Some(valid_from); + inner_mut.valid_to = Some(valid_to); + drop(inner_mut); + self + } + + /// Required (alternatives: `subject_from_csr`, `self_signed`) + #[inline] + pub fn subject(&self, subject_name: DirectoryName, public_key: PublicKey) -> &Self { + self.inner.borrow_mut().subject_infos = Some(SubjectInfos::NameAndPublicKey { + name: subject_name, + public_key, + }); + self + } + + /// Required (alternatives: `subject`, `self_signed`) + #[inline] + pub fn subject_from_csr(&self, csr: Csr) -> &Self { + self.inner.borrow_mut().subject_infos = Some(SubjectInfos::Csr(csr)); + self + } + + /// Required (alternative: `self_signed`, `issuer_cert`) + #[inline] + pub fn issuer(&self, issuer_name: DirectoryName, issuer_key: &'a PrivateKey) -> &Self { + self.inner.borrow_mut().issuer_infos = Some(IssuerInfos { + name: issuer_name, + key: issuer_key, + self_signed: false, + }); + self + } + + /// Required (alternative: `issuer`, `issuer_cert`) + #[inline] + pub fn self_signed(&self, name: DirectoryName, key: &'a PrivateKey) -> &Self { + self.inner.borrow_mut().issuer_infos = Some(IssuerInfos { + name, + key, + self_signed: true, + }); + self + } + + /// Required (alternative: `issuer`, `self_signed`) + #[inline] + pub fn issuer_cert(&self, issuer_cert: &Cert, issuer_key: &'a PrivateKey) -> &Self { + let builder = self.issuer(issuer_cert.subject_name(), issuer_key); + + if let Ok(issuer_ski) = issuer_cert.subject_key_identifier() { + self.authority_key_identifier(issuer_ski.to_vec()) + } else { + builder + } + } + + /// Optional (alternative: `issuer_cert`, `self_signed`) + #[inline] + pub fn authority_key_identifier(&self, aki: Vec) -> &Self { + self.inner.borrow_mut().authority_key_identifier = Some(aki); + self + } + + /// Optional + #[inline] + pub fn ca(&self, ca: bool) -> &Self { + self.inner.borrow_mut().ca = Some(ca); + self + } + + /// Optional + #[inline] + pub fn pathlen(&self, pathlen: u8) -> &Self { + self.inner.borrow_mut().pathlen = Some(pathlen); + self + } + + /// Optional + #[inline] + pub fn signature_hash_type(&self, signature_hash_type: SignatureAlgorithm) -> &Self { + self.inner.borrow_mut().signature_hash_type = Some(signature_hash_type); + self + } + + /// Optional + #[inline] + pub fn key_id_gen_method(&self, key_id_gen_method: KeyIdGenMethod) -> &Self { + self.inner.borrow_mut().key_id_gen_method = Some(key_id_gen_method); + self + } + + /// Optional + #[inline] + pub fn key_usage(&self, key_usage: KeyUsage) -> &Self { + self.inner.borrow_mut().key_usage = Some(key_usage); + self + } + + /// Optional + #[inline] + pub fn extended_key_usage(&self, extended_key_usage: ExtendedKeyUsage) -> &Self { + self.inner.borrow_mut().extended_key_usage = Some(extended_key_usage); + self + } + + /// Optional + #[inline] + pub fn subject_alt_name(&self, subject_alt_name: GeneralNames) -> &Self { + self.inner.borrow_mut().subject_alt_name = Some(subject_alt_name); + self + } + + /// Optional + #[inline] + pub fn issuer_alt_name(&self, issuer_alt_name: GeneralNames) -> &Self { + self.inner.borrow_mut().issuer_alt_name = Some(issuer_alt_name); + self + } + + /// Optional + /// + /// Bypass picky serial number generator by providing your own. + #[inline] + pub fn serial_number(&self, unsigned_integer_bytes: Vec) -> &Self { + self.inner.borrow_mut().serial_number = Some(unsigned_integer_bytes); + self + } + + /// Optional + /// + /// Inherit extensions from the "extension request" attribute of the provided CSR if applicable + /// Extensions already present will be ignored. + #[inline] + pub fn inherit_extensions_from_csr_attributes(&self, inherit: bool) -> &Self { + self.inner.borrow_mut().inherit_extensions_from_csr_attributes = inherit; + self + } + + pub fn build(&self) -> Result { + let mut inner = self.inner.borrow_mut(); + + let valid_from = inner.valid_from.take().ok_or(CertError::MissingBuilderArgument { + arg: field_str!(valid_from), + })?; + let valid_to = inner.valid_to.take().ok_or(CertError::MissingBuilderArgument { + arg: field_str!(valid_to), + })?; + + let signature_hash_type = inner + .signature_hash_type + .take() + .unwrap_or(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)); + + let key_id_gen_method = inner + .key_id_gen_method + .take() + .unwrap_or(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_256)); + + let issuer_infos = inner.issuer_infos.take().ok_or(CertError::MissingBuilderArgument { + arg: field_str!(issuer_infos), + })?; + let (issuer_name, issuer_key, aki, subject_infos) = { + let (aki, subject_infos) = if issuer_infos.self_signed { + let public_key = issuer_infos + .key + .to_public_key() + .map_err(|source| CertError::PrivateKeyToPublicKey { source })?; + let aki = key_id_gen_method + .generate_from(&public_key) + .map_err(|e| CertError::KeyIdGen { source: e }) + .map_err(|e| CertError::CertGeneration { source: Box::new(e) })?; + let subject_infos = SubjectInfos::NameAndPublicKey { + name: issuer_infos.name.clone(), + public_key, + }; + (aki, subject_infos) + } else { + let aki = inner + .authority_key_identifier + .take() + .ok_or(CertError::MissingBuilderArgument { + arg: field_str!(authority_key_identifier), + })?; + let subject_infos = inner.subject_infos.take().ok_or(CertError::MissingBuilderArgument { + arg: field_str!(subject_infos), + })?; + (aki, subject_infos) + }; + + (issuer_infos.name, issuer_infos.key, aki, subject_infos) + }; + let (subject_name, subject_public_key, ext_req) = match subject_infos { + SubjectInfos::Csr(csr) => { + csr.verify().map_err(|e| CertError::InvalidCsr { source: e })?; + + let ext_req = ((csr.0.certification_request_info.attributes.0).0) + .into_iter() + .find_map(|attr| match attr.value { + picky_asn1_x509::AttributeValues::Extensions(set_of_extensions) => { + set_of_extensions.0.into_iter().next() + } + _ => None, + }); + + let subject_name = csr.0.certification_request_info.subject.into(); + let subject_public_key = csr.0.certification_request_info.subject_public_key_info.into(); + + (subject_name, subject_public_key, ext_req) + } + SubjectInfos::NameAndPublicKey { name, public_key } => (name, public_key, None), + }; + + let ca = inner.ca.take().unwrap_or(false); + let pathlen = inner.pathlen.take(); + let key_usage_opt = inner.key_usage.take(); + let extended_key_usage_opt = inner.extended_key_usage.take(); + let subject_alt_name_opt = inner.subject_alt_name.take(); + let issuer_alt_name_opt = inner.issuer_alt_name.take(); + + let serial_number = if let Some(unsigned_integer_bytes) = inner.serial_number.take() { + IntegerAsn1::from_bytes_be_unsigned(unsigned_integer_bytes) + } else { + generate_serial_number() + }; + + let inherit_extensions_from_csr_attributes = inner.inherit_extensions_from_csr_attributes; + + drop(inner); + + let validity = Validity { + not_before: valid_from.into(), + not_after: valid_to.into(), + }; + + let extensions = { + let mut extensions = Vec::new(); + + // key usage + basic constraints + if let Some(key_usage) = key_usage_opt { + if key_usage.digital_signature() { + extensions.push(Extension::new_basic_constraints(ca, pathlen).into_critical()); + } else { + extensions.push(Extension::new_basic_constraints(ca, pathlen).into_non_critical()); + } + extensions.push(Extension::new_key_usage(key_usage)); + } else { + extensions.push(Extension::new_basic_constraints(ca, pathlen).into_non_critical()); + } + + // eku + if let Some(extended_key_usage) = extended_key_usage_opt { + extensions.push(Extension::new_extended_key_usage(extended_key_usage)); + } + + // san + if let Some(san) = subject_alt_name_opt { + extensions.push(Extension::new_subject_alt_name(san)); + } + + // ian + if let Some(ian) = issuer_alt_name_opt { + extensions.push(Extension::new_issuer_alt_name(ian)); + } + + // ski + let ski = key_id_gen_method + .generate_from(&subject_public_key) + .map_err(|e| CertError::KeyIdGen { source: e }) + .map_err(|e| CertError::CertGeneration { source: Box::new(e) })?; + extensions.push(Extension::new_subject_key_identifier(ski)); + + // aki + extensions.push(Extension::new_authority_key_identifier( + KeyIdentifier::from(aki), + None, + None, + )); + + // inherit extensions from csr "request extension" attribute if allowed to + match ext_req { + Some(requested_exts) if inherit_extensions_from_csr_attributes => { + for requested_ext in requested_exts.0 { + if !extensions.iter().any(|o| requested_ext.extn_id() == o.extn_id()) { + extensions.push(requested_ext); + } + } + } + _ => {} + } + + Extensions(extensions) + }; + + let signature = + AlgorithmIdentifier::try_from(signature_hash_type).map_err(|e| CertError::Signature { source: e })?; + + let tbs_certificate = TbsCertificate { + version: ExplicitContextTag0(Version::V3), + serial_number, + signature, + issuer: Name::from(issuer_name), + validity, + subject: Name::from(subject_name), + subject_public_key_info: SubjectPublicKeyInfo::from(subject_public_key), + extensions: ExplicitContextTag3(extensions), + }; + + let signature_algorithm = signature_hash_type + .try_into() + .map_err(|e| CertError::Signature { source: e })?; + + let tbs_der = picky_asn1_der::to_vec(&tbs_certificate) + .map_err(|e| CertError::Asn1Serialization { + source: e, + element: "tbs certificate", + }) + .map_err(|e| CertError::CertGeneration { source: Box::new(e) })?; + + let signature_value = BitString::with_bytes( + signature_hash_type + .sign(&tbs_der, issuer_key) + .map_err(|e| CertError::Signature { source: e }) + .map_err(|e| CertError::CertGeneration { source: Box::new(e) })?, + ) + .into(); + + let signature_algorithm_der = + picky_asn1_der::to_vec(&signature_algorithm).map_err(|source| CertError::Asn1Serialization { + element: "signature_algorithm", + source, + })?; + + let signature_value_der = + picky_asn1_der::to_vec(&signature_value).map_err(|source| CertError::Asn1Serialization { + element: "signature_value", + source, + })?; + + Ok(Cert { + details: Certificate { + tbs_certificate, + signature_algorithm, + signature_value, + }, + overview: CertificateOverview { + tbs_certificate: Asn1RawDer(tbs_der), + signature_algorithm: Asn1RawDer(signature_algorithm_der), + signature_value: Asn1RawDer(signature_value_der), + }, + }) + } +} + +fn generate_serial_number() -> IntegerAsn1 { + let x = rand::random::(); + let b1 = ((x >> 24) & 0xff) as u8; + let b2 = ((x >> 16) & 0xff) as u8; + let b3 = ((x >> 8) & 0xff) as u8; + let b4 = (x & 0xff) as u8; + // serial number MUST be a positive integer + IntegerAsn1::from_bytes_be_unsigned(vec![b1, b2, b3, b4]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pem::{Pem, parse_pem}; + use crate::x509::csr::Attribute; + + #[test] + fn read_pem_and_parse_certificate() { + let pem = parse_pem(picky_test_data::INTERMEDIATE_CA.as_bytes()).unwrap(); + let cert = Cert::from_der(pem.data()).unwrap(); + + assert_eq!(cert.serial_number(), &vec![1]); + assert_eq!( + Into::::into(cert.signature_algorithm().oid()).as_str(), + oids::SHA1_WITH_RSA_ENCRYPTION + ); + assert_eq!(cert.valid_not_before(), UtcDate::new(2011, 2, 12, 14, 44, 6).unwrap()); + assert_eq!(cert.valid_not_after(), UtcDate::new(2021, 2, 12, 14, 44, 6).unwrap()); + + assert_eq!(cert.issuer_name().to_string(), "C=NL,O=PolarSSL,CN=PolarSSL Test CA"); + } + + #[test] + fn get_identifier() { + let pem = picky_test_data::RSA_2048_PK_1 + .parse::() + .expect("couldn't parse pem"); + let private_key = PrivateKey::from_pkcs8(pem.data()).expect("couldn't extract private key from pkcs8"); + + // validity + let valid_from = UtcDate::ymd(2019, 10, 10).unwrap(); + let valid_to = UtcDate::ymd(2019, 10, 11).unwrap(); + + let root = CertificateBuilder::new() + .validity(valid_from, valid_to) + .self_signed(DirectoryName::new_common_name("test"), &private_key) + .ca(true) + .build() + .expect("couldn't generate root ca"); + + root.subject_key_identifier() + .expect("couldn't get subject key identifier"); + root.authority_key_identifier() + .expect("couldn't get authority key identifier"); + + assert_eq!(root.ty(), CertType::Root); + } + + #[test] + fn key_id_and_cert() { + let kid = "c4a7b1a47b2c71fadbe14b9075ffc41560858910"; + let pem = picky_test_data::ROOT_CA.parse::().expect("couldn't parse PEM"); + let cert = Cert::from_der(pem.data()).expect("couldn't deserialize certificate"); + assert_eq!(cert.ty(), CertType::Root); + let key_id = cert + .subject_key_identifier() + .expect("couldn't get subject key identifier"); + pretty_assertions::assert_eq!(hex::encode(key_id), kid); + } + + fn parse_key(pem_str: &str) -> PrivateKey { + let pem = pem_str.parse::().unwrap(); + PrivateKey::from_pkcs8(pem.data()).unwrap() + } + + #[test] + fn valid_ca_chain() { + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + let intermediate_key = parse_key(picky_test_data::RSA_2048_PK_2); + let leaf_key = parse_key(picky_test_data::RSA_2048_PK_3); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("TheFuture.usodakedo Root CA"), &root_key) + .ca(true) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384)) + .build() + .expect("couldn't build root ca"); + assert_eq!(root.ty(), CertType::Root); + + let intermediate = CertificateBuilder::new() + .validity(UtcDate::ymd(2068, 1, 1).unwrap(), UtcDate::ymd(2071, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("TheFuture.usodakedo Authority"), + intermediate_key.to_public_key().unwrap(), + ) + .issuer_cert(&root, &root_key) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) + .key_id_gen_method(KeyIdGenMethod::SPKValueHashedLeftmost160(HashAlgorithm::SHA1)) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build intermediate ca"); + assert_eq!(intermediate.ty(), CertType::Intermediate); + + let csr = Csr::generate( + DirectoryName::new_common_name("ChillingInTheFuture.usobakkari"), + &leaf_key, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), + ) + .unwrap(); + + let signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject_from_csr(csr) + .issuer_cert(&intermediate, &intermediate_key) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512)) + .pathlen(0) // not meaningful in non-CA certificates + .build() + .expect("couldn't build signed leaf"); + assert_eq!(signed_leaf.ty(), CertType::Leaf); + + let chain = [intermediate, root]; + + // check with exact date + signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .expect("couldn't verify chain"); + + // check with interval date + signed_leaf + .verifier() + .chain(chain.iter()) + .interval_date( + &UtcDate::new(2068, 12, 31, 23, 59, 59).unwrap(), + &UtcDate::ymd(2069, 1, 1).unwrap(), + ) + .verify() + .expect("couldn't verify chain with interval date"); + + // check with ignore not before + signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::new(2068, 12, 31, 23, 59, 59).unwrap()) + .ignore_not_before_check() + .verify() + .expect("couldn't verify chain with interval date"); + + // check with no date validity check + signed_leaf + .verifier() + .chain(chain.iter()) + .ignore_not_after_check() + .ignore_not_before_check() + .verify() + .expect("couldn't verify chain with no date validity check"); + + let expired_err = signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2080, 10, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + expired_err.to_string(), + "invalid certificate \'CN=ChillingInTheFuture.usobakkari\': \ + certificate expired (not after: 2072-01-01 00:00:00, now: 2080-10-01 00:00:00)" + ); + + let intermediate_expired_err = signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2071, 6, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + intermediate_expired_err.to_string(), + "invalid certificate \'CN=TheFuture.usodakedo Authority\': \ + certificate expired (not after: 2071-01-01 00:00:00, now: 2071-06-01 00:00:00)" + ); + + let root_expired_err = signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2070, 6, 16).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + root_expired_err.to_string(), + "invalid certificate \'CN=TheFuture.usodakedo Root CA\': \ + certificate expired (not after: 2070-06-15 00:00:00, now: 2070-06-16 00:00:00)" + ); + + let still_in_2019_err = signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2019, 11, 14).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + still_in_2019_err.to_string(), + "invalid certificate \'CN=ChillingInTheFuture.usobakkari\': \ + certificate is not yet valid (not before: 2069-01-01 00:00:00, now: 2019-11-14 00:00:00)" + ); + + let not_yet_valid_with_interval_err = signed_leaf + .verifier() + .chain(chain.iter()) + .interval_date( + &UtcDate::ymd(2068, 12, 30).unwrap(), + &UtcDate::ymd(2068, 12, 31).unwrap(), + ) + .verify() + .unwrap_err(); + assert_eq!( + not_yet_valid_with_interval_err.to_string(), + "invalid certificate \'CN=ChillingInTheFuture.usobakkari\': \ + certificate is not yet valid (not before: 2069-01-01 00:00:00, now: 2068-12-31 00:00:00)" + ); + + let date_is_missing_err = signed_leaf.verifier().chain(chain.iter()).verify().unwrap_err(); + assert_eq!( + date_is_missing_err.to_string(), + "missing required builder argument `now`" + ); + } + + #[test] + fn ec_signing() { + let root_key = parse_key(picky_test_data::EC_NIST256_PK_1); + let intermediate_key = parse_key(picky_test_data::EC_NIST384_PK_1); + let leaf_key = parse_key(picky_test_data::EC_NIST256_PK_2); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("TheFuture.usodakedo Root CA"), &root_key) + .ca(true) + .signature_hash_type(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384)) + .build() + .expect("couldn't build root ca"); + + let intermediate = CertificateBuilder::new() + .validity(UtcDate::ymd(2068, 1, 1).unwrap(), UtcDate::ymd(2071, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("TheFuture.usodakedo Authority"), + intermediate_key.to_public_key().unwrap(), + ) + .issuer_cert(&root, &root_key) + .signature_hash_type(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)) + .key_id_gen_method(KeyIdGenMethod::SPKValueHashedLeftmost160(HashAlgorithm::SHA1)) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build intermediate ca"); + + let signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("ChillingInTheFuture.usobakkari"), + leaf_key.to_public_key().unwrap(), + ) + .issuer_cert(&intermediate, &intermediate_key) + .signature_hash_type(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512)) + .pathlen(0) // not meaningful in non-CA certificates + .build() + .expect("couldn't build signed leaf"); + + let chain = [intermediate, root]; + + signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .expect("couldn't verify chain"); + } + + #[test] + fn ed25519_signing() { + let root_key = parse_key(picky_test_data::ED25519_PEM_PK_1); + let intermediate_key = parse_key(picky_test_data::ED25519_PEM_PK_2); + let leaf_key = parse_key(picky_test_data::ED25519_PEM_PK_3); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("TheFuture.usodakedo Root CA"), &root_key) + .ca(true) + .signature_hash_type(SignatureAlgorithm::Ed25519) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384)) + .build() + .expect("couldn't build root ca"); + + let intermediate = CertificateBuilder::new() + .validity(UtcDate::ymd(2068, 1, 1).unwrap(), UtcDate::ymd(2071, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("TheFuture.usodakedo Authority"), + intermediate_key.to_public_key().unwrap(), + ) + .issuer_cert(&root, &root_key) + .signature_hash_type(SignatureAlgorithm::Ed25519) + .key_id_gen_method(KeyIdGenMethod::SPKValueHashedLeftmost160(HashAlgorithm::SHA1)) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build intermediate ca"); + + let signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("ChillingInTheFuture.usobakkari"), + leaf_key.to_public_key().unwrap(), + ) + .issuer_cert(&intermediate, &intermediate_key) + .signature_hash_type(SignatureAlgorithm::Ed25519) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_512)) + .pathlen(0) // not meaningful in non-CA certificates + .build() + .expect("couldn't build signed leaf"); + + let chain = [intermediate, root]; + + signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .expect("couldn't verify chain"); + } + + #[test] + fn malicious_ca_chain() { + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + let intermediate_key = parse_key(picky_test_data::RSA_2048_PK_2); + let leaf_key = parse_key(picky_test_data::RSA_2048_PK_3); + let malicious_root_key = parse_key(picky_test_data::RSA_2048_PK_4); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("VerySafe Root CA"), &root_key) + .ca(true) + .pathlen(1) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_224)) + .build() + .expect("couldn't build root ca"); + + let intermediate = CertificateBuilder::new() + .validity(UtcDate::ymd(2068, 1, 1).unwrap(), UtcDate::ymd(2071, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("V.E.R.Y Legitimate VerySafe Authority"), + intermediate_key.to_public_key().unwrap(), + ) + .issuer_cert(&root, &malicious_root_key) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) + .key_id_gen_method(KeyIdGenMethod::SPKValueHashedLeftmost160(HashAlgorithm::SHA2_384)) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build intermediate ca"); + + let csr = Csr::generate( + DirectoryName::new_common_name("I Trust This V.E.R.Y Legitimate Intermediate Certificate"), + &leaf_key, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), + ) + .unwrap(); + + let signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject_from_csr(csr) + .issuer_cert(&intermediate, &intermediate_key) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384)) + .build() + .expect("couldn't build signed leaf"); + + let chain = [intermediate, root]; + + let root_missing_err = signed_leaf + .verifier() + .chain(chain[..1].iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + root_missing_err.to_string(), + "CA chain error: chain is missing a root certificate" + ); + + let invalid_sig_err = signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + invalid_sig_err.to_string(), + "invalid certificate \'CN=V.E.R.Y Legitimate VerySafe Authority\': signature error: invalid signature" + ); + } + + #[test] + fn invalid_basic_constraints_chain() { + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + let intermediate_key = parse_key(picky_test_data::RSA_2048_PK_2); + let leaf_key = parse_key(picky_test_data::RSA_2048_PK_3); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("VerySafe Root CA"), &root_key) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build root ca"); + + let intermediate = CertificateBuilder::new() + .validity(UtcDate::ymd(2068, 1, 1).unwrap(), UtcDate::ymd(2071, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("V.E.R.Y Legitimate VerySafe Authority"), + intermediate_key.to_public_key().unwrap(), + ) + .issuer_cert(&root, &root_key) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build intermediate ca"); + + let csr = Csr::generate( + DirectoryName::new_common_name("I Trust This V.E.R.Y Legitimate Intermediate Certificate"), + &leaf_key, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), + ) + .unwrap(); + + let signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject_from_csr(csr.clone()) + .issuer_cert(&intermediate, &intermediate_key) + .build() + .expect("couldn't build signed leaf"); + + let chain = [intermediate.clone(), root.clone()]; + + let invalid_pathlen_err = signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + invalid_pathlen_err.to_string(), + "CA chain error: chain depth doesn\'t satisfy basic constraints extension: \ + certificate \'CN=VerySafe Root CA\' has pathlen of 0" + ); + + let invalid_issuer_signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject_from_csr(csr) + .issuer_cert(&signed_leaf, &leaf_key) + .build() + .expect("couldn't build invalid issuer signed leaf"); + + let chain = [signed_leaf, intermediate, root]; + + let invalid_issuer_err = invalid_issuer_signed_leaf + .verifier() + .chain(chain.iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + invalid_issuer_err.to_string(), + "CA chain error: issuer certificate \'CN=I Trust This V.E.R.Y Legitimate Intermediate Certificate\' is not a CA" + ); + } + + #[test] + fn issuer_missing_ca_basic_constraints() { + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + let intermediate = parse_key(picky_test_data::RSA_2048_PK_2); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("VerySafe Root CA"), &root_key) + .pathlen(0) + .build() + .expect("couldn't build root ca"); + + let intermediate = CertificateBuilder::new() + .validity(UtcDate::ymd(2068, 1, 1).unwrap(), UtcDate::ymd(2071, 1, 1).unwrap()) + .subject( + DirectoryName::new_common_name("V.E.R.Y Legitimate VerySafe Authority"), + intermediate.to_public_key().unwrap(), + ) + .issuer_cert(&root, &root_key) + .ca(true) + .pathlen(1) + .build() + .expect("couldn't build intermediate ca"); + + let invalid_pathlen_err = intermediate + .verifier() + .chain([&root].into_iter()) + .exact_date(&UtcDate::ymd(2069, 10, 1).unwrap()) + .verify() + .unwrap_err(); + assert_eq!( + invalid_pathlen_err.to_string(), + "CA chain error: issuer certificate 'CN=VerySafe Root CA' is not a CA" + ); + } + + #[test] + fn bypass_serial_number_generator() { + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + + let unsigned_integer_bytes = [21, 84, 58, 122]; + + let cert = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("TheFuture.usodakedo Root CA"), &root_key) + .ca(true) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_384)) + .serial_number(unsigned_integer_bytes.to_vec()) + .build() + .expect("couldn't build root ca"); + + assert_eq!(cert.serial_number().as_unsigned_bytes_be(), unsigned_integer_bytes); + } + + #[test] + fn validity_encoding() { + use picky_asn1_x509::validity::Time; + + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + + let cert = CertificateBuilder::new() + .validity(UtcDate::ymd(2045, 6, 15).unwrap(), UtcDate::ymd(2055, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("Am I valid"), &root_key) + .ca(true) + .signature_hash_type(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1)) + .key_id_gen_method(KeyIdGenMethod::SPKFullDER(HashAlgorithm::SHA2_224)) + .build() + .expect("couldn't build root ca"); + + let validity = &cert.details.tbs_certificate.validity; + + assert!(matches!(validity.not_before, Time::Utc(_))); + assert!(matches!(validity.not_after, Time::Generalized(_))); + } + + #[test] + fn inherit_requested_extensions_by_csr() { + use crate::x509::name::GeneralName; + + let root_key = parse_key(picky_test_data::RSA_2048_PK_1); + let leaf_key = parse_key(picky_test_data::RSA_2048_PK_3); + + let root = CertificateBuilder::new() + .validity(UtcDate::ymd(2065, 6, 15).unwrap(), UtcDate::ymd(2070, 6, 15).unwrap()) + .self_signed(DirectoryName::new_common_name("VerySafe Root CA"), &root_key) + .ca(true) + .pathlen(0) + .build() + .expect("couldn't build root ca"); + + let extensions = + vec![Extension::new_subject_alt_name(GeneralName::new_dns_name("localhost").unwrap()).into_non_critical()]; + let attr = Attribute::new_extension_request(extensions); + let csr = Csr::generate_with_attributes( + DirectoryName::new_common_name("I want more extensions"), + &leaf_key, + SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), + vec![attr], + ) + .unwrap(); + + let signed_leaf = CertificateBuilder::new() + .validity(UtcDate::ymd(2069, 1, 1).unwrap(), UtcDate::ymd(2072, 1, 1).unwrap()) + .subject_from_csr(csr) + .issuer_cert(&root, &root_key) + .inherit_extensions_from_csr_attributes(true) + .build() + .expect("couldn't build signed leaf"); + + let subject_alt_name = signed_leaf + .extensions() + .iter() + .find_map(|ext| match ext.extn_value() { + ExtensionView::SubjectAltName(gn) => match gn.0.first().unwrap() { + picky_asn1_x509::GeneralName::DnsName(name) => Some(name.to_string()), + _ => None, + }, + _ => None, + }) + .unwrap(); + + assert_eq!(subject_alt_name, "localhost"); + } + + /// We noticed a few Authenticode certificates where encoded using a constructed (explicit) + /// context tag instead of a primitive (implicit) context tag for the subject alternative name + /// extension (notably PSDiagnostics PowerShell module). + /// + /// Relevant documentation from RFC5280 Appendix A.2: + /// ```not_rust + /// DEFINITIONS IMPLICIT TAGS ::= + /// + /// […] + /// + /// GeneralName ::= CHOICE { + /// otherName [0] AnotherName, + /// rfc822Name [1] IA5String, + /// dNSName [2] IA5String, + /// x400Address [3] ORAddress, + /// directoryName [4] Name, + /// ediPartyName [5] EDIPartyName, + /// uniformResourceIdentifier [6] IA5String, + /// iPAddress [7] OCTET STRING, + /// registeredID [8] OBJECT IDENTIFIER } + /// ``` + /// [Link](https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.2) + /// + /// `DEFINITIONS IMPLICIT TAGS ::=` is used to specify that except stated otherwise, tags are + /// implicits (also said primitives). + /// + /// Picky is encoding this using an implicit context tag as specified by the RFC, and this is a + /// problem when validating some Windows certificates because picky 6.3.0 (and prior) is fully + /// parsing the certificate and encode back into der when validating the signature causing + /// signature validation to fail. + /// + /// To improve validation robustness, it was decided starting picky 6.4.0 to use the originally + /// parsed DER representation internally instead of re-encoding on demand. + /// + /// The aforementioned PSDiagnostics module certificate chain is used as test case to validate + /// this behavior. + #[test] + fn psdiag_constructed_context_tag_in_subject_alt_name_ext() { + let leaf = Cert::from_pem_str(picky_test_data::PSDIAG_LEAF).unwrap(); + let inter = Cert::from_pem_str(picky_test_data::PSDIAG_INTER).unwrap(); + let root = Cert::from_pem_str(picky_test_data::PSDIAG_ROOT).unwrap(); + + let chain = [inter, root]; + let unexpired_date = UtcDate::new(2021, 11, 21, 1, 0, 0).unwrap(); + + leaf.verifier() + .exact_date(&unexpired_date) + .chain(chain.iter()) + .verify() + .unwrap(); + } +} diff --git a/rust/vendor/picky/src/x509/csr.rs b/rust/vendor/picky/src/x509/csr.rs new file mode 100644 index 0000000..4885c5b --- /dev/null +++ b/rust/vendor/picky/src/x509/csr.rs @@ -0,0 +1,182 @@ +use super::utils::{from_der, from_pem, from_pem_str, to_der, to_pem}; +use crate::key::{KeyError, PrivateKey, PublicKey}; +use crate::pem::{Pem, PemError}; +use crate::signature::{SignatureAlgorithm, SignatureError}; +use crate::x509::certificate::CertError; +use crate::x509::name::DirectoryName; +use picky_asn1::bit_string::BitString; +use picky_asn1_der::Asn1DerError; +use picky_asn1_x509::{CertificationRequest, CertificationRequestInfo}; +use thiserror::Error; + +pub use picky_asn1_x509::Attribute; + +const ELEMENT_NAME: &str = "certification request"; + +#[derive(Debug, Error)] +pub enum CsrError { + /// ASN1 serialization error + #[error("(ASN1) couldn't serialize {element}: {source}")] + Asn1Serialization { + element: &'static str, + source: Asn1DerError, + }, + + /// ASN1 deserialization error + #[error("(ASN1) couldn't deserialize {}: {}", element, source)] + Asn1Deserialization { + element: &'static str, + source: Asn1DerError, + }, + + /// signature error + #[error("signature error: {}", source)] + Signature { source: SignatureError }, + + /// invalid PEM label error + #[error("invalid PEM label: {}", label)] + InvalidPemLabel { label: String }, + + /// invalid PEM provided + #[error("invalid PEM provided: {source}")] + Pem { source: PemError }, + + #[error("failed to get public key from private key: {source}")] + PrivateKeyToPublicKey { source: KeyError }, +} + +impl From for CsrError { + fn from(e: CertError) -> Self { + match e { + CertError::Asn1Deserialization { element, source } => CsrError::Asn1Deserialization { element, source }, + CertError::Asn1Serialization { element, source } => CsrError::Asn1Serialization { element, source }, + CertError::Pem { source } => CsrError::Pem { source }, + CertError::InvalidPemLabel { label } => CsrError::InvalidPemLabel { label }, + _ => unreachable!(), + } + } +} + +const CSR_PEM_LABEL: &str = "CERTIFICATE REQUEST"; + +/// Certificate Signing Request +#[derive(Clone, Debug, PartialEq)] +pub struct Csr(pub(crate) CertificationRequest); + +impl From for Csr { + fn from(certification_request: CertificationRequest) -> Self { + Self(certification_request) + } +} + +impl Csr { + pub fn from_der>(der: &T) -> Result { + Ok(from_der(der, ELEMENT_NAME).map(Self)?) + } + + pub fn from_pem_str(pem_str: &str) -> Result { + Ok(from_pem_str(pem_str, &[CSR_PEM_LABEL], ELEMENT_NAME).map(Self)?) + } + + pub fn from_pem(pem: &Pem) -> Result { + Ok(from_pem(pem, &[CSR_PEM_LABEL], ELEMENT_NAME).map(Self)?) + } + + pub fn to_der(&self) -> Result, CsrError> { + Ok(to_der(&self.0, ELEMENT_NAME)?) + } + + pub fn to_pem(&self) -> Result, CsrError> { + Ok(to_pem(&self.0, CSR_PEM_LABEL, ELEMENT_NAME)?) + } + + pub fn generate( + subject: DirectoryName, + private_key: &PrivateKey, + signature_hash_type: SignatureAlgorithm, + ) -> Result { + let public_key = private_key + .to_public_key() + .map_err(|source| CsrError::PrivateKeyToPublicKey { source })?; + + let cri = CertificationRequestInfo::new(subject.into(), public_key.into()); + h_generate_from_cri(cri, private_key, signature_hash_type) + } + + pub fn generate_with_attributes( + subject: DirectoryName, + private_key: &PrivateKey, + signature_hash_type: SignatureAlgorithm, + attributes: Vec, + ) -> Result { + let public_key = private_key + .to_public_key() + .map_err(|source| CsrError::PrivateKeyToPublicKey { source })?; + + let mut cri = CertificationRequestInfo::new(subject.into(), public_key.into()); + for attr in attributes { + cri.add_attribute(attr); + } + h_generate_from_cri(cri, private_key, signature_hash_type) + } + + pub fn subject_name(&self) -> DirectoryName { + self.0.certification_request_info.subject.clone().into() + } + + pub fn public_key(&self) -> &PublicKey { + (&self.0.certification_request_info.subject_public_key_info).into() + } + + pub fn into_subject_infos(self) -> (DirectoryName, PublicKey) { + ( + self.0.certification_request_info.subject.into(), + self.0.certification_request_info.subject_public_key_info.into(), + ) + } + + pub fn verify(&self) -> Result<(), CsrError> { + let hash_type = SignatureAlgorithm::from_algorithm_identifier(&self.0.signature_algorithm) + .map_err(|e| CsrError::Signature { source: e })?; + + let public_key = &self.0.certification_request_info.subject_public_key_info; + + let msg = + picky_asn1_der::to_vec(&self.0.certification_request_info).map_err(|e| CsrError::Asn1Serialization { + source: e, + element: "certification request info", + })?; + + hash_type + .verify(&public_key.clone().into(), &msg, self.0.signature.0.payload_view()) + .map_err(|e| CsrError::Signature { source: e })?; + + Ok(()) + } +} + +fn h_generate_from_cri( + cri: CertificationRequestInfo, + private_key: &PrivateKey, + signature_hash_type: SignatureAlgorithm, +) -> Result { + let cri_der = picky_asn1_der::to_vec(&cri).map_err(|e| CsrError::Asn1Serialization { + source: e, + element: "certification request cri", + })?; + let signature = BitString::with_bytes( + signature_hash_type + .sign(&cri_der, private_key) + .map_err(|e| CsrError::Signature { source: e })?, + ); + + let signature_algorithm = signature_hash_type + .try_into() + .map_err(|e| CsrError::Signature { source: e })?; + + Ok(Csr(CertificationRequest { + certification_request_info: cri, + signature_algorithm, + signature: signature.into(), + })) +} diff --git a/rust/vendor/picky/src/x509/date.rs b/rust/vendor/picky/src/x509/date.rs new file mode 100644 index 0000000..129de7b --- /dev/null +++ b/rust/vendor/picky/src/x509/date.rs @@ -0,0 +1,182 @@ +use picky_asn1::date::{Date, GeneralizedTime, UTCTime, UTCTimeRepr}; +use picky_asn1_x509::validity::Time; +use std::fmt; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct UtcDate(GeneralizedTime); + +impl UtcDate { + #[inline] + pub fn new(year: u16, month: u8, day: u8, hour: u8, minute: u8, second: u8) -> Option { + Some(Self(GeneralizedTime::new(year, month, day, hour, minute, second)?)) + } + + #[inline] + pub fn ymd(year: u16, month: u8, day: u8) -> Option { + Some(Self(GeneralizedTime::new(year, month, day, 0, 0, 0)?)) + } + + #[cfg(any(feature = "time_conversion", feature = "chrono_conversion"))] + #[inline] + pub fn now() -> Self { + #[cfg(feature = "time_conversion")] + { + Self(time::OffsetDateTime::now_utc().into()) + } + #[cfg(all(feature = "chrono_conversion", not(feature = "time_conversion")))] + { + Self(chrono::offset::Utc::now().into()) + } + } + + #[inline] + pub fn year(&self) -> u16 { + self.0.year() + } + + #[inline] + pub fn month(&self) -> u8 { + self.0.month() + } + + #[inline] + pub fn day(&self) -> u8 { + self.0.day() + } + + #[inline] + pub fn hour(&self) -> u8 { + self.0.hour() + } + + #[inline] + pub fn minute(&self) -> u8 { + self.0.minute() + } + + #[inline] + pub fn second(&self) -> u8 { + self.0.second() + } +} + +impl From for UTCTime { + fn from(date: UtcDate) -> Self { + unsafe { + UTCTime::new_unchecked( + date.0.year(), + date.0.month(), + date.0.day(), + date.0.hour(), + date.0.minute(), + date.0.second(), + ) + } + } +} + +impl From for UtcDate { + fn from(date: Date) -> Self { + Self(unsafe { + GeneralizedTime::new_unchecked( + date.year(), + date.month(), + date.day(), + date.hour(), + date.minute(), + date.second(), + ) + }) + } +} + +impl From for GeneralizedTime { + fn from(date: UtcDate) -> GeneralizedTime { + date.0 + } +} + +impl From for UtcDate { + fn from(date: GeneralizedTime) -> Self { + Self(date) + } +} + +impl From for Time { + fn from(date: UtcDate) -> Self { + // Time is used to encode validity period. + // As per RFC 5280, + // > CAs conforming to this profile MUST always encode certificate + // > validity dates through the year 2049 as UTCTime; certificate validity + // > dates in 2050 or later MUST be encoded as GeneralizedTime. + // > Conforming applications MUST be able to process validity dates that + // > are encoded in either UTCTime or GeneralizedTime. + if date.year() >= 2050 { + Self::Generalized(Into::::into(date).into()) + } else { + Self::Utc(Into::::into(date).into()) + } + } +} + +impl From