Wire native Dosh auth runtime
This commit is contained in:
Generated
+318
@@ -80,6 +80,12 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
@@ -255,6 +261,18 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-bigint"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -310,6 +328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
@@ -354,10 +373,12 @@ dependencies = [
|
|||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"ssh-key",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
"vt100",
|
"vt100",
|
||||||
|
"x25519-dalek",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -366,6 +387,20 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.16.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"digest",
|
||||||
|
"elliptic-curve",
|
||||||
|
"rfc6979",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ed25519"
|
name = "ed25519"
|
||||||
version = "2.2.3"
|
version = "2.2.3"
|
||||||
@@ -390,6 +425,25 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elliptic-curve"
|
||||||
|
version = "0.13.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"crypto-bigint",
|
||||||
|
"digest",
|
||||||
|
"ff",
|
||||||
|
"generic-array",
|
||||||
|
"group",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core",
|
||||||
|
"sec1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -412,6 +466,16 @@ version = "2.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ff"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fiat-crypto"
|
name = "fiat-crypto"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
@@ -443,6 +507,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -469,6 +534,17 @@ dependencies = [
|
|||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "group"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||||
|
dependencies = [
|
||||||
|
"ff",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
@@ -561,6 +637,9 @@ name = "lazy_static"
|
|||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
dependencies = [
|
||||||
|
"spin",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
@@ -574,6 +653,12 @@ version = "0.2.186"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.17"
|
version = "0.1.17"
|
||||||
@@ -651,6 +736,52 @@ dependencies = [
|
|||||||
"pin-utils",
|
"pin-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint-dig"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"libm",
|
||||||
|
"num-integer",
|
||||||
|
"num-iter",
|
||||||
|
"num-traits",
|
||||||
|
"rand",
|
||||||
|
"smallvec",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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-iter"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -675,6 +806,44 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p256"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||||
|
dependencies = [
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p384"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
|
||||||
|
dependencies = [
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p521"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"rand_core",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -698,6 +867,15 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -710,6 +888,17 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs1"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"pkcs8",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -771,6 +960,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "primeorder"
|
||||||
|
version = "0.13.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||||
|
dependencies = [
|
||||||
|
"elliptic-curve",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -845,6 +1043,37 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfc6979"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "0.9.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"digest",
|
||||||
|
"num-bigint-dig",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"pkcs1",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core",
|
||||||
|
"sha2",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc_version"
|
name = "rustc_version"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -886,6 +1115,20 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sec1"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"der",
|
||||||
|
"generic-array",
|
||||||
|
"pkcs8",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.28"
|
version = "1.0.28"
|
||||||
@@ -1050,6 +1293,7 @@ version = "2.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"digest",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1069,6 +1313,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spin"
|
||||||
|
version = "0.9.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spki"
|
name = "spki"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@@ -1079,6 +1329,48 @@ dependencies = [
|
|||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh-cipher"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
"ssh-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh-encoding"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh-key"
|
||||||
|
version = "0.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
|
||||||
|
dependencies = [
|
||||||
|
"ed25519-dalek",
|
||||||
|
"p256",
|
||||||
|
"p384",
|
||||||
|
"p521",
|
||||||
|
"rand_core",
|
||||||
|
"rsa",
|
||||||
|
"sec1",
|
||||||
|
"sha2",
|
||||||
|
"signature",
|
||||||
|
"ssh-cipher",
|
||||||
|
"ssh-encoding",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -1638,6 +1930,18 @@ dependencies = [
|
|||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "x25519-dalek"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||||
|
dependencies = [
|
||||||
|
"curve25519-dalek",
|
||||||
|
"rand_core",
|
||||||
|
"serde",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.52"
|
version = "0.8.52"
|
||||||
@@ -1663,6 +1967,20 @@ name = "zeroize"
|
|||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ portable-pty = "0.8"
|
|||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
ssh-key = { version = "0.6.7", features = ["ed25519"] }
|
||||||
tokio = { version = "1.41", features = ["full"] }
|
tokio = { version = "1.41", features = ["full"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
vt100 = "0.15"
|
vt100 = "0.15"
|
||||||
|
x25519-dalek = { version = "2.0.1", features = ["static_secrets", "getrandom"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.14"
|
tempfile = "3.14"
|
||||||
|
|||||||
+1
-1
@@ -202,7 +202,7 @@ pub fn verify_bootstrap(resp: &BootstrapResponse, secret: &[u8; 32]) -> Result<b
|
|||||||
Ok(expected == resp.attach_token)
|
Ok(expected == resp.attach_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_attach_ticket(
|
pub fn build_attach_ticket(
|
||||||
secret: &[u8; 32],
|
secret: &[u8; 32],
|
||||||
server_id: [u8; 32],
|
server_id: [u8; 32],
|
||||||
server_key_epoch: u64,
|
server_key_epoch: u64,
|
||||||
|
|||||||
+302
-4
@@ -9,11 +9,14 @@ use dosh::auth::{
|
|||||||
use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_server_config};
|
use dosh::config::{expand_tilde, load_client_config, load_hosts_config, load_server_config};
|
||||||
use dosh::crypto;
|
use dosh::crypto;
|
||||||
use dosh::native::{
|
use dosh::native::{
|
||||||
TrustResult, host_fingerprint, parse_host_public_key_line, remove_trusted_host, trust_host,
|
KnownHostStatus, TrustResult, derive_native_session_key, generate_native_ephemeral,
|
||||||
|
host_fingerprint, load_ed25519_identity, parse_host_public_key_line, remove_trusted_host,
|
||||||
|
sign_user_auth, trust_host, verify_known_host, verify_server_hello,
|
||||||
};
|
};
|
||||||
use dosh::protocol::{
|
use dosh::protocol::{
|
||||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||||
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
|
NativeAuthOkBody, NativeClientHelloBody, NativeServerHelloBody, NativeUserAuthBody, PacketKind,
|
||||||
|
Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
|
||||||
TicketAttachOkEnvelope,
|
TicketAttachOkEnvelope,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -43,6 +46,8 @@ struct Args {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
local_auth: bool,
|
local_auth: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
auth: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
no_cache: bool,
|
no_cache: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
remove: bool,
|
remove: bool,
|
||||||
@@ -237,6 +242,66 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let auth_preference = args
|
||||||
|
.auth
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| config.auth_preference.clone());
|
||||||
|
if !args.local_auth && auth_allows(&auth_preference, "native") {
|
||||||
|
let native_start = Instant::now();
|
||||||
|
match try_native_auth(
|
||||||
|
&socket,
|
||||||
|
&config,
|
||||||
|
&requested_server,
|
||||||
|
&server,
|
||||||
|
ssh_port,
|
||||||
|
&target_udp_host,
|
||||||
|
dosh_port,
|
||||||
|
args.ssh_key.as_deref(),
|
||||||
|
&session,
|
||||||
|
&mode,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((frame, cred)) => {
|
||||||
|
log_timing(args.verbose, "native_auth", native_start.elapsed());
|
||||||
|
if !args.no_cache {
|
||||||
|
save_cache(&cache_path, &cred)?;
|
||||||
|
}
|
||||||
|
log_timing(args.verbose, "terminal_ready", started.elapsed());
|
||||||
|
if args.attach_only {
|
||||||
|
render_frame(&frame)?;
|
||||||
|
detach_once(&socket, &cred, 2).await?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
return run_terminal(
|
||||||
|
socket,
|
||||||
|
cred,
|
||||||
|
Some(frame),
|
||||||
|
predict,
|
||||||
|
startup_command,
|
||||||
|
config.reconnect_timeout_secs,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(err) if auth_allows(&auth_preference, "ssh") => {
|
||||||
|
log_debug(
|
||||||
|
args.verbose,
|
||||||
|
2,
|
||||||
|
&format!("dosh native auth failed, falling back to SSH bootstrap: {err:#}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.context("native auth failed")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.local_auth && !auth_allows(&auth_preference, "ssh") {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"auth preference {auth_preference:?} does not allow SSH fallback"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let bootstrap_start = Instant::now();
|
let bootstrap_start = Instant::now();
|
||||||
let bootstrap = if args.local_auth {
|
let bootstrap = if args.local_auth {
|
||||||
local_bootstrap(&session, &mode, cols, rows, dosh_port, target_udp_host)?
|
local_bootstrap(&session, &mode, cols, rows, dosh_port, target_udp_host)?
|
||||||
@@ -565,6 +630,13 @@ fn local_bootstrap(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn auth_allows(preference: &str, method: &str) -> bool {
|
||||||
|
preference
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.any(|entry| entry == "auto" || entry == method)
|
||||||
|
}
|
||||||
|
|
||||||
fn log_timing(verbose: u8, name: &str, elapsed: Duration) {
|
fn log_timing(verbose: u8, name: &str, elapsed: Duration) {
|
||||||
if verbose >= 1 {
|
if verbose >= 1 {
|
||||||
eprintln!("dosh timing {name}={}ms", elapsed.as_millis());
|
eprintln!("dosh timing {name}={}ms", elapsed.as_millis());
|
||||||
@@ -663,6 +735,15 @@ fn ssh_destination_host(server: &str) -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ssh_username(server: &str) -> Option<String> {
|
||||||
|
let (user, _) = server.rsplit_once('@')?;
|
||||||
|
if user.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(user.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn ssh_config_hostname(server: &str, ssh_port: Option<u16>) -> Result<String> {
|
fn ssh_config_hostname(server: &str, ssh_port: Option<u16>) -> Result<String> {
|
||||||
ssh_config(server, ssh_port)?
|
ssh_config(server, ssh_port)?
|
||||||
.hostname
|
.hostname
|
||||||
@@ -673,6 +754,8 @@ fn ssh_config_hostname(server: &str, ssh_port: Option<u16>) -> Result<String> {
|
|||||||
struct SshConfig {
|
struct SshConfig {
|
||||||
hostname: Option<String>,
|
hostname: Option<String>,
|
||||||
port: Option<u16>,
|
port: Option<u16>,
|
||||||
|
user: Option<String>,
|
||||||
|
identity_files: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ssh_config(server: &str, ssh_port: Option<u16>) -> Result<SshConfig> {
|
fn ssh_config(server: &str, ssh_port: Option<u16>) -> Result<SshConfig> {
|
||||||
@@ -707,6 +790,10 @@ fn parse_ssh_config(raw: &str) -> SshConfig {
|
|||||||
config.hostname = Some(value.to_string());
|
config.hostname = Some(value.to_string());
|
||||||
} else if key.eq_ignore_ascii_case("port") {
|
} else if key.eq_ignore_ascii_case("port") {
|
||||||
config.port = value.parse().ok();
|
config.port = value.parse().ok();
|
||||||
|
} else if key.eq_ignore_ascii_case("user") && !value.is_empty() {
|
||||||
|
config.user = Some(value.to_string());
|
||||||
|
} else if key.eq_ignore_ascii_case("identityfile") && !value.is_empty() {
|
||||||
|
config.identity_files.push(value.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config
|
config
|
||||||
@@ -720,6 +807,191 @@ fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr> {
|
|||||||
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
|
.ok_or_else(|| anyhow!("no UDP address resolved for {host}:{port}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn try_native_auth(
|
||||||
|
socket: &UdpSocket,
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
requested_server: &str,
|
||||||
|
server: &str,
|
||||||
|
ssh_port: Option<u16>,
|
||||||
|
udp_host: &str,
|
||||||
|
udp_port: u16,
|
||||||
|
cli_identity: Option<&Path>,
|
||||||
|
session: &str,
|
||||||
|
mode: &str,
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
) -> Result<(Frame, CachedCredential)> {
|
||||||
|
let addr = resolve_addr(udp_host, udp_port)?;
|
||||||
|
let ssh_config = ssh_config(server, ssh_port).unwrap_or_default();
|
||||||
|
let requested_user = ssh_username(server)
|
||||||
|
.or(ssh_config.user.clone())
|
||||||
|
.unwrap_or_else(|| std::env::var("USER").unwrap_or_else(|_| "unknown".to_string()));
|
||||||
|
let (client_secret, client_public) = generate_native_ephemeral();
|
||||||
|
let hello = dosh::native::NativeClientHello {
|
||||||
|
protocol_version: dosh::native::NATIVE_PROTOCOL_VERSION,
|
||||||
|
client_random: crypto::random_32(),
|
||||||
|
client_ephemeral_public: client_public,
|
||||||
|
requested_host: requested_server.to_string(),
|
||||||
|
requested_user,
|
||||||
|
requested_session: session.to_string(),
|
||||||
|
requested_mode: mode.to_string(),
|
||||||
|
terminal_size: (cols, rows),
|
||||||
|
supported_aead: vec!["chacha20poly1305".to_string()],
|
||||||
|
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||||
|
cached_host_key_fingerprint: None,
|
||||||
|
attach_ticket_envelope: None,
|
||||||
|
};
|
||||||
|
let packet = protocol::encode_plain(
|
||||||
|
PacketKind::NativeClientHello,
|
||||||
|
[0u8; 16],
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
&protocol::to_body(&NativeClientHelloBody {
|
||||||
|
hello: hello.clone(),
|
||||||
|
})?,
|
||||||
|
)?;
|
||||||
|
socket.send_to(&packet, addr).await?;
|
||||||
|
|
||||||
|
let mut buf = vec![0u8; 65535];
|
||||||
|
let (n, _) = tokio::time::timeout(
|
||||||
|
Duration::from_millis(config.native_auth_timeout_ms.max(1)),
|
||||||
|
socket.recv_from(&mut buf),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let packet = protocol::decode(&buf[..n])?;
|
||||||
|
if packet.header.kind != PacketKind::NativeServerHello {
|
||||||
|
if packet.header.kind == PacketKind::AttachReject {
|
||||||
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
|
return Err(anyhow!("native auth rejected: {}", reject.reason));
|
||||||
|
}
|
||||||
|
return Err(anyhow!("native auth received unexpected server response"));
|
||||||
|
}
|
||||||
|
let server_hello: NativeServerHelloBody = protocol::from_body(&packet.body)?;
|
||||||
|
verify_server_hello(&hello, &server_hello.hello)?;
|
||||||
|
|
||||||
|
let known_hosts = expand_tilde(&config.known_hosts);
|
||||||
|
match verify_known_host(&known_hosts, requested_server, &server_hello.hello.host_key)? {
|
||||||
|
KnownHostStatus::Trusted => {}
|
||||||
|
KnownHostStatus::Unknown if config.trust_on_first_use => {
|
||||||
|
trust_host(
|
||||||
|
&known_hosts,
|
||||||
|
requested_server,
|
||||||
|
&server_hello.hello.host_key,
|
||||||
|
"tofu",
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
KnownHostStatus::Unknown => {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Dosh host key for {requested_server} is not trusted; run `dosh trust {requested_server}` first"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
KnownHostStatus::Mismatch { expected, actual } => {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Dosh host key mismatch for {requested_server}: expected {expected}, got {actual}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let session_key = derive_native_session_key(
|
||||||
|
&client_secret,
|
||||||
|
server_hello.hello.server_ephemeral_public,
|
||||||
|
&hello,
|
||||||
|
&server_hello.hello,
|
||||||
|
)?;
|
||||||
|
let identity = load_first_native_identity(config, cli_identity, &ssh_config)?;
|
||||||
|
let auth = sign_user_auth(&identity, &hello, &server_hello.hello, Vec::new())?;
|
||||||
|
let mut pending_id = [0u8; 16];
|
||||||
|
pending_id.copy_from_slice(&server_hello.hello.auth_challenge[..16]);
|
||||||
|
let auth_body = protocol::to_body(&NativeUserAuthBody { auth })?;
|
||||||
|
let packet = protocol::encode_encrypted(
|
||||||
|
PacketKind::NativeUserAuth,
|
||||||
|
pending_id,
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
&session_key,
|
||||||
|
CLIENT_TO_SERVER,
|
||||||
|
&auth_body,
|
||||||
|
)?;
|
||||||
|
socket.send_to(&packet, addr).await?;
|
||||||
|
|
||||||
|
let (n, _) = tokio::time::timeout(
|
||||||
|
Duration::from_millis(config.native_auth_timeout_ms.max(1)),
|
||||||
|
socket.recv_from(&mut buf),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let packet = protocol::decode(&buf[..n])?;
|
||||||
|
if packet.header.kind != PacketKind::NativeAuthOk {
|
||||||
|
if packet.header.kind == PacketKind::AttachReject {
|
||||||
|
let reject: AttachReject = protocol::from_body(&packet.body)?;
|
||||||
|
return Err(anyhow!("native auth rejected: {}", reject.reason));
|
||||||
|
}
|
||||||
|
return Err(anyhow!("native auth received unexpected auth response"));
|
||||||
|
}
|
||||||
|
let plain = protocol::decrypt_body(&packet, &session_key, SERVER_TO_CLIENT)?;
|
||||||
|
let ok: NativeAuthOkBody = protocol::from_body(&plain)?;
|
||||||
|
let frame = Frame {
|
||||||
|
session: ok.ok.session.clone(),
|
||||||
|
output_seq: ok.ok.initial_seq,
|
||||||
|
bytes: ok.ok.snapshot.clone(),
|
||||||
|
snapshot: true,
|
||||||
|
closed: false,
|
||||||
|
};
|
||||||
|
let cred = CachedCredential {
|
||||||
|
server: requested_server.to_string(),
|
||||||
|
session: ok.ok.session,
|
||||||
|
mode: ok.ok.mode,
|
||||||
|
udp_host: udp_host.to_string(),
|
||||||
|
udp_port,
|
||||||
|
client_id: ok.ok.client_id,
|
||||||
|
session_key: ok.ok.session_key,
|
||||||
|
session_key_id: ok.ok.session_key_id,
|
||||||
|
attach_ticket: ok.ok.attach_ticket,
|
||||||
|
attach_ticket_psk: ok.ok.attach_ticket_psk,
|
||||||
|
last_rendered_seq: ok.ok.initial_seq,
|
||||||
|
};
|
||||||
|
Ok((frame, cred))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_first_native_identity(
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
cli_identity: Option<&Path>,
|
||||||
|
ssh_config: &SshConfig,
|
||||||
|
) -> Result<ed25519_dalek::SigningKey> {
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
if let Some(path) = cli_identity {
|
||||||
|
paths.push(path.to_path_buf());
|
||||||
|
}
|
||||||
|
paths.extend(
|
||||||
|
ssh_config
|
||||||
|
.identity_files
|
||||||
|
.iter()
|
||||||
|
.map(|path| expand_tilde(path)),
|
||||||
|
);
|
||||||
|
paths.extend(config.identity_files.iter().map(|path| expand_tilde(path)));
|
||||||
|
paths.sort();
|
||||||
|
paths.dedup();
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for path in paths {
|
||||||
|
if !path.exists() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match load_ed25519_identity(&path) {
|
||||||
|
Ok(identity) => return Ok(identity),
|
||||||
|
Err(err) => errors.push(format!("{}: {err:#}", path.display())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errors.is_empty() {
|
||||||
|
Err(anyhow!("no usable ssh-ed25519 identity found"))
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"no usable ssh-ed25519 identity found: {}",
|
||||||
|
errors.join("; ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn bootstrap_attach(
|
async fn bootstrap_attach(
|
||||||
socket: &UdpSocket,
|
socket: &UdpSocket,
|
||||||
server_name: &str,
|
server_name: &str,
|
||||||
@@ -1424,8 +1696,8 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
FrameBuffer, Predictor, parse_ssh_config, raw_contains_host_table, ssh_destination_host,
|
FrameBuffer, Predictor, auth_allows, parse_ssh_config, raw_contains_host_table,
|
||||||
startup_command, toml_bare_key_or_quoted,
|
ssh_destination_host, ssh_username, startup_command, toml_bare_key_or_quoted,
|
||||||
};
|
};
|
||||||
use dosh::protocol::Frame;
|
use dosh::protocol::Frame;
|
||||||
|
|
||||||
@@ -1454,6 +1726,18 @@ mod tests {
|
|||||||
assert_eq!(parsed.port, Some(2222));
|
assert_eq!(parsed.port, Some(2222));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_ssh_config_user_and_identity_files() {
|
||||||
|
let parsed = parse_ssh_config(
|
||||||
|
"user palav\nidentityfile ~/.ssh/id_ed25519\nidentityfile ~/.ssh/work\n",
|
||||||
|
);
|
||||||
|
assert_eq!(parsed.user, Some("palav".to_string()));
|
||||||
|
assert_eq!(
|
||||||
|
parsed.identity_files,
|
||||||
|
vec!["~/.ssh/id_ed25519".to_string(), "~/.ssh/work".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn detects_existing_imported_host_tables() {
|
fn detects_existing_imported_host_tables() {
|
||||||
assert!(raw_contains_host_table(
|
assert!(raw_contains_host_table(
|
||||||
@@ -1470,6 +1754,20 @@ mod tests {
|
|||||||
assert_eq!(ssh_destination_host("palav@example.com"), "example.com");
|
assert_eq!(ssh_destination_host("palav@example.com"), "example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_user_from_ssh_destination() {
|
||||||
|
assert_eq!(ssh_username("palav@example.com"), Some("palav".to_string()));
|
||||||
|
assert_eq!(ssh_username("example.com"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_preference_accepts_auto_or_named_method() {
|
||||||
|
assert!(auth_allows("native,ssh", "native"));
|
||||||
|
assert!(auth_allows("native,ssh", "ssh"));
|
||||||
|
assert!(auth_allows("auto", "native"));
|
||||||
|
assert!(!auth_allows("native", "ssh"));
|
||||||
|
}
|
||||||
|
|
||||||
fn test_frame(seq: u64, snapshot: bool) -> Frame {
|
fn test_frame(seq: u64, snapshot: bool) -> Frame {
|
||||||
Frame {
|
Frame {
|
||||||
session: "test".to_string(),
|
session: "test".to_string(),
|
||||||
|
|||||||
+264
-5
@@ -1,16 +1,21 @@
|
|||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use dosh::auth::{
|
use dosh::auth::{
|
||||||
build_bootstrap, encode_bootstrap, load_or_create_server_secret, open_attach_ticket,
|
build_attach_ticket, build_bootstrap, encode_bootstrap, load_or_create_server_secret, now_secs,
|
||||||
verify_bootstrap,
|
open_attach_ticket, verify_bootstrap,
|
||||||
};
|
};
|
||||||
use dosh::config::{ServerConfig, load_server_config};
|
use dosh::config::{ServerConfig, load_server_config};
|
||||||
use dosh::crypto;
|
use dosh::crypto;
|
||||||
use dosh::native::{host_public_key, host_public_key_line, load_or_create_host_key};
|
use dosh::native::{
|
||||||
|
NativeAuthOk, NativeServerHello, derive_native_session_key, generate_native_ephemeral,
|
||||||
|
host_public_key, host_public_key_line, load_or_create_host_key, sign_server_hello,
|
||||||
|
verify_native_user_auth_from_config,
|
||||||
|
};
|
||||||
use dosh::protocol::{
|
use dosh::protocol::{
|
||||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||||
PacketKind, ReplayWindow, Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody,
|
NativeAuthOkBody, NativeClientHelloBody, NativeServerHelloBody, NativeUserAuthBody, PacketKind,
|
||||||
TicketAttachEnvelope, TicketAttachOkEnvelope,
|
ReplayWindow, Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
|
||||||
|
TicketAttachOkEnvelope,
|
||||||
};
|
};
|
||||||
use dosh::pty::{PtyHandle, PtyOutput, spawn_pty_session};
|
use dosh::pty::{PtyHandle, PtyOutput, spawn_pty_session};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
@@ -157,6 +162,7 @@ struct ServerState {
|
|||||||
secret: [u8; 32],
|
secret: [u8; 32],
|
||||||
pty_tx: mpsc::UnboundedSender<PtyOutput>,
|
pty_tx: mpsc::UnboundedSender<PtyOutput>,
|
||||||
sessions: HashMap<String, Session>,
|
sessions: HashMap<String, Session>,
|
||||||
|
pending_native: HashMap<[u8; 16], PendingNativeAuth>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Session {
|
struct Session {
|
||||||
@@ -190,6 +196,14 @@ struct PendingFrame {
|
|||||||
attempts: u8,
|
attempts: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PendingNativeAuth {
|
||||||
|
client: dosh::native::NativeClientHello,
|
||||||
|
server: NativeServerHello,
|
||||||
|
session_key: [u8; 32],
|
||||||
|
peer: SocketAddr,
|
||||||
|
created_at: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
impl ServerState {
|
impl ServerState {
|
||||||
fn new(
|
fn new(
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
@@ -201,6 +215,7 @@ impl ServerState {
|
|||||||
secret,
|
secret,
|
||||||
pty_tx,
|
pty_tx,
|
||||||
sessions: HashMap::new(),
|
sessions: HashMap::new(),
|
||||||
|
pending_native: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +252,10 @@ async fn handle_packet(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let packet = protocol::decode(raw)?;
|
let packet = protocol::decode(raw)?;
|
||||||
match packet.header.kind {
|
match packet.header.kind {
|
||||||
|
PacketKind::NativeClientHello => {
|
||||||
|
handle_native_client_hello(state, socket, peer, packet.body).await
|
||||||
|
}
|
||||||
|
PacketKind::NativeUserAuth => handle_native_user_auth(state, socket, peer, &packet).await,
|
||||||
PacketKind::BootstrapAttachRequest => {
|
PacketKind::BootstrapAttachRequest => {
|
||||||
handle_bootstrap_attach(state, socket, peer, packet.body).await
|
handle_bootstrap_attach(state, socket, peer, packet.body).await
|
||||||
}
|
}
|
||||||
@@ -258,6 +277,246 @@ async fn handle_packet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_native_client_hello(
|
||||||
|
state: &Arc<Mutex<ServerState>>,
|
||||||
|
socket: &Arc<UdpSocket>,
|
||||||
|
peer: SocketAddr,
|
||||||
|
body: Vec<u8>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let req: NativeClientHelloBody = protocol::from_body(&body)?;
|
||||||
|
let built: Result<([u8; 16], NativeServerHello)> = {
|
||||||
|
let mut locked = state.lock().expect("server state poisoned");
|
||||||
|
if !locked.config.native_auth {
|
||||||
|
Err(anyhow!("native auth disabled"))
|
||||||
|
} else if !req
|
||||||
|
.hello
|
||||||
|
.supported_aead
|
||||||
|
.iter()
|
||||||
|
.any(|algorithm| algorithm == "chacha20poly1305")
|
||||||
|
{
|
||||||
|
Err(anyhow!("native auth requires chacha20poly1305"))
|
||||||
|
} else if !req
|
||||||
|
.hello
|
||||||
|
.supported_user_key_algorithms
|
||||||
|
.iter()
|
||||||
|
.any(|algorithm| algorithm == "ssh-ed25519")
|
||||||
|
{
|
||||||
|
Err(anyhow!("native auth requires ssh-ed25519"))
|
||||||
|
} else {
|
||||||
|
let current_user = std::env::var("USER").unwrap_or_else(|_| "unknown".to_string());
|
||||||
|
if req.hello.requested_user != current_user {
|
||||||
|
Err(anyhow!("native auth user mismatch"))
|
||||||
|
} else {
|
||||||
|
let host_signing = load_or_create_host_key(&locked.config)?;
|
||||||
|
let (server_secret, server_public) = generate_native_ephemeral();
|
||||||
|
let mut hello = NativeServerHello {
|
||||||
|
protocol_version: dosh::native::NATIVE_PROTOCOL_VERSION,
|
||||||
|
server_random: crypto::random_32(),
|
||||||
|
server_ephemeral_public: server_public,
|
||||||
|
host_key: host_public_key(&host_signing),
|
||||||
|
chosen_aead: "chacha20poly1305".to_string(),
|
||||||
|
server_key_epoch: 1,
|
||||||
|
auth_challenge: crypto::random_32(),
|
||||||
|
rate_limit_remaining: Some(locked.config.native_auth_rate_limit_per_minute),
|
||||||
|
host_signature: Vec::new(),
|
||||||
|
};
|
||||||
|
sign_server_hello(&host_signing, &req.hello, &mut hello)?;
|
||||||
|
let session_key = derive_native_session_key(
|
||||||
|
&server_secret,
|
||||||
|
req.hello.client_ephemeral_public,
|
||||||
|
&req.hello,
|
||||||
|
&hello,
|
||||||
|
)?;
|
||||||
|
let mut pending_id = [0u8; 16];
|
||||||
|
pending_id.copy_from_slice(&hello.auth_challenge[..16]);
|
||||||
|
locked.pending_native.insert(
|
||||||
|
pending_id,
|
||||||
|
PendingNativeAuth {
|
||||||
|
client: req.hello,
|
||||||
|
server: hello.clone(),
|
||||||
|
session_key,
|
||||||
|
peer,
|
||||||
|
created_at: Instant::now(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok((pending_id, hello))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (pending_id, hello) = match built {
|
||||||
|
Ok(built) => built,
|
||||||
|
Err(err) => return send_reject(socket, peer, &err.to_string()).await,
|
||||||
|
};
|
||||||
|
let body = protocol::to_body(&NativeServerHelloBody { hello })?;
|
||||||
|
let out = protocol::encode_plain(PacketKind::NativeServerHello, pending_id, 1, 0, &body)?;
|
||||||
|
socket.send_to(&out, peer).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_native_user_auth(
|
||||||
|
state: &Arc<Mutex<ServerState>>,
|
||||||
|
socket: &Arc<UdpSocket>,
|
||||||
|
peer: SocketAddr,
|
||||||
|
packet: &protocol::Packet,
|
||||||
|
) -> Result<()> {
|
||||||
|
let pending = {
|
||||||
|
let mut locked = state.lock().expect("server state poisoned");
|
||||||
|
locked.pending_native.remove(&packet.header.conn_id)
|
||||||
|
};
|
||||||
|
let Some(pending) = pending else {
|
||||||
|
return send_reject_to_client(socket, peer, packet.header.conn_id, "unknown native auth")
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
if pending.peer != peer {
|
||||||
|
return send_reject_to_client(
|
||||||
|
socket,
|
||||||
|
peer,
|
||||||
|
packet.header.conn_id,
|
||||||
|
"native auth peer changed",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
if pending.created_at.elapsed() > Duration::from_secs(30) {
|
||||||
|
return send_reject_to_client(socket, peer, packet.header.conn_id, "native auth expired")
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
let body = protocol::decrypt_body(packet, &pending.session_key, CLIENT_TO_SERVER)?;
|
||||||
|
let req: NativeUserAuthBody = protocol::from_body(&body)?;
|
||||||
|
|
||||||
|
let attached: Result<(
|
||||||
|
[u8; 16],
|
||||||
|
[u8; 16],
|
||||||
|
Vec<u8>,
|
||||||
|
[u8; 32],
|
||||||
|
String,
|
||||||
|
String,
|
||||||
|
u64,
|
||||||
|
Vec<u8>,
|
||||||
|
)> = {
|
||||||
|
let mut locked = state.lock().expect("server state poisoned");
|
||||||
|
verify_native_user_auth_from_config(
|
||||||
|
&locked.config,
|
||||||
|
&pending.client,
|
||||||
|
&pending.server,
|
||||||
|
&req.auth,
|
||||||
|
)
|
||||||
|
.context("verify native user auth")
|
||||||
|
.and_then(|_| {
|
||||||
|
let session_name = pending.client.requested_session.clone();
|
||||||
|
let mode = pending.client.requested_mode.clone();
|
||||||
|
let cols = pending.client.terminal_size.0;
|
||||||
|
let rows = pending.client.terminal_size.1;
|
||||||
|
if !locked.sessions.contains_key(&session_name) {
|
||||||
|
if locked.config.create_on_attach {
|
||||||
|
locked.ensure_session(&session_name, cols, rows)?;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("session does not exist"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let session_key = pending.session_key;
|
||||||
|
let key_id = {
|
||||||
|
let digest = crypto::sha256(&session_key);
|
||||||
|
let mut out = [0u8; 16];
|
||||||
|
out.copy_from_slice(&digest[..16]);
|
||||||
|
out
|
||||||
|
};
|
||||||
|
let attach_ticket_psk = crypto::random_32();
|
||||||
|
let issued_at = now_secs()?;
|
||||||
|
let attach_ticket = build_attach_ticket(
|
||||||
|
&locked.secret,
|
||||||
|
crypto::sha256(&locked.secret),
|
||||||
|
1,
|
||||||
|
pending.client.requested_user.clone(),
|
||||||
|
session_name.clone(),
|
||||||
|
mode.clone(),
|
||||||
|
issued_at,
|
||||||
|
issued_at + locked.config.attach_ticket_ttl_secs,
|
||||||
|
&attach_ticket_psk,
|
||||||
|
)?;
|
||||||
|
let session = locked
|
||||||
|
.sessions
|
||||||
|
.get_mut(&session_name)
|
||||||
|
.expect("session exists");
|
||||||
|
if mode != "view-only" {
|
||||||
|
session.pty.resize(cols, rows)?;
|
||||||
|
session.parser.set_size(rows, cols);
|
||||||
|
}
|
||||||
|
let client_id = crypto::random_16();
|
||||||
|
let snapshot = screen_snapshot(session.parser.screen());
|
||||||
|
let screen = session.parser.screen().clone();
|
||||||
|
let output_seq = session.output_seq;
|
||||||
|
session.clients.insert(
|
||||||
|
client_id,
|
||||||
|
ClientState {
|
||||||
|
endpoint: peer,
|
||||||
|
mode: mode.clone(),
|
||||||
|
session_key,
|
||||||
|
last_acked: output_seq,
|
||||||
|
replay: ReplayWindow::default(),
|
||||||
|
send_seq: 1,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
last_seen: Instant::now(),
|
||||||
|
pending: VecDeque::new(),
|
||||||
|
last_screen: Some(screen),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok((
|
||||||
|
client_id,
|
||||||
|
key_id,
|
||||||
|
attach_ticket,
|
||||||
|
attach_ticket_psk,
|
||||||
|
session_name,
|
||||||
|
mode,
|
||||||
|
output_seq,
|
||||||
|
snapshot,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let (
|
||||||
|
client_id,
|
||||||
|
key_id,
|
||||||
|
attach_ticket,
|
||||||
|
attach_ticket_psk,
|
||||||
|
session_name,
|
||||||
|
mode,
|
||||||
|
output_seq,
|
||||||
|
snapshot,
|
||||||
|
) = match attached {
|
||||||
|
Ok(attached) => attached,
|
||||||
|
Err(err) => {
|
||||||
|
return send_reject_to_client(socket, peer, packet.header.conn_id, &err.to_string())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ok = NativeAuthOk {
|
||||||
|
client_id,
|
||||||
|
session: session_name,
|
||||||
|
mode,
|
||||||
|
session_key: pending.session_key,
|
||||||
|
session_key_id: key_id,
|
||||||
|
attach_ticket,
|
||||||
|
attach_ticket_psk,
|
||||||
|
initial_seq: output_seq,
|
||||||
|
snapshot,
|
||||||
|
policy_flags: Vec::new(),
|
||||||
|
};
|
||||||
|
let body = protocol::to_body(&NativeAuthOkBody { ok })?;
|
||||||
|
let out = protocol::encode_encrypted(
|
||||||
|
PacketKind::NativeAuthOk,
|
||||||
|
client_id,
|
||||||
|
1,
|
||||||
|
packet.header.seq,
|
||||||
|
&pending.session_key,
|
||||||
|
SERVER_TO_CLIENT,
|
||||||
|
&body,
|
||||||
|
)?;
|
||||||
|
socket.send_to(&out, peer).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_bootstrap_attach(
|
async fn handle_bootstrap_attach(
|
||||||
state: &Arc<Mutex<ServerState>>,
|
state: &Arc<Mutex<ServerState>>,
|
||||||
socket: &Arc<UdpSocket>,
|
socket: &Arc<UdpSocket>,
|
||||||
|
|||||||
+101
-1
@@ -10,6 +10,7 @@ use std::fs;
|
|||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
|
||||||
|
|
||||||
pub const HOST_KEY_ALGORITHM: &str = "dosh-ed25519";
|
pub const HOST_KEY_ALGORITHM: &str = "dosh-ed25519";
|
||||||
pub const NATIVE_PROTOCOL_VERSION: u8 = 1;
|
pub const NATIVE_PROTOCOL_VERSION: u8 = 1;
|
||||||
@@ -87,9 +88,10 @@ pub struct NativeAuthOk {
|
|||||||
pub client_id: [u8; 16],
|
pub client_id: [u8; 16],
|
||||||
pub session: String,
|
pub session: String,
|
||||||
pub mode: String,
|
pub mode: String,
|
||||||
|
pub session_key: [u8; 32],
|
||||||
pub session_key_id: [u8; 16],
|
pub session_key_id: [u8; 16],
|
||||||
pub attach_ticket: Vec<u8>,
|
pub attach_ticket: Vec<u8>,
|
||||||
pub encrypted_attach_ticket_psk: Vec<u8>,
|
pub attach_ticket_psk: [u8; 32],
|
||||||
pub initial_seq: u64,
|
pub initial_seq: u64,
|
||||||
pub snapshot: Vec<u8>,
|
pub snapshot: Vec<u8>,
|
||||||
pub policy_flags: Vec<String>,
|
pub policy_flags: Vec<String>,
|
||||||
@@ -331,6 +333,46 @@ pub fn verify_native_user_auth_from_config(
|
|||||||
verify_native_user_auth(client, server, auth, &authorized_keys)
|
verify_native_user_auth(client, server, auth, &authorized_keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_native_ephemeral() -> (StaticSecret, [u8; 32]) {
|
||||||
|
let secret = StaticSecret::random();
|
||||||
|
let public = X25519PublicKey::from(&secret).to_bytes();
|
||||||
|
(secret, public)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derive_native_session_key(
|
||||||
|
local_secret: &StaticSecret,
|
||||||
|
remote_public: [u8; 32],
|
||||||
|
client: &NativeClientHello,
|
||||||
|
server: &NativeServerHello,
|
||||||
|
) -> Result<[u8; 32]> {
|
||||||
|
let remote_public = X25519PublicKey::from(remote_public);
|
||||||
|
let shared = local_secret.diffie_hellman(&remote_public);
|
||||||
|
anyhow::ensure!(
|
||||||
|
shared.was_contributory(),
|
||||||
|
"native key exchange produced non-contributory shared secret"
|
||||||
|
);
|
||||||
|
let mut salt = b"dosh/native/session-key/v1".to_vec();
|
||||||
|
salt.extend(crypto::sha256(&bincode::serialize(client)?));
|
||||||
|
salt.extend(crypto::sha256(&bincode::serialize(server)?));
|
||||||
|
crypto::hkdf32(shared.as_bytes(), &salt, b"dosh/native/chacha20poly1305/v1")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_ed25519_identity(path: &Path) -> Result<SigningKey> {
|
||||||
|
let private_key = ssh_key::PrivateKey::read_openssh_file(path)
|
||||||
|
.with_context(|| format!("read OpenSSH identity {}", path.display()))?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
!private_key.is_encrypted(),
|
||||||
|
"OpenSSH identity {} is encrypted; encrypted-key prompts are not wired yet",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
let ed25519 = private_key
|
||||||
|
.key_data()
|
||||||
|
.ed25519()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("OpenSSH identity {} is not ssh-ed25519", path.display()))?;
|
||||||
|
ed25519_dalek::SigningKey::try_from(ed25519)
|
||||||
|
.with_context(|| format!("parse ssh-ed25519 identity {}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
impl AuthorizedKey {
|
impl AuthorizedKey {
|
||||||
fn ensure_native_allowed(&self, auth: &NativeUserAuth) -> Result<()> {
|
fn ensure_native_allowed(&self, auth: &NativeUserAuth) -> Result<()> {
|
||||||
if !self.options.unsupported.is_empty() {
|
if !self.options.unsupported.is_empty() {
|
||||||
@@ -864,6 +906,64 @@ mod tests {
|
|||||||
assert!(verify_native_user_auth(&client, &server, &auth, &authorized).is_err());
|
assert!(verify_native_user_auth(&client, &server, &auth, &authorized).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_ed25519_identity_reads_openssh_private_key() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("id_ed25519");
|
||||||
|
let signing = SigningKey::from_bytes(&[11u8; 32]);
|
||||||
|
let keypair = ssh_key::private::Ed25519Keypair::from(&signing);
|
||||||
|
let private =
|
||||||
|
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::from(keypair), "").unwrap();
|
||||||
|
private
|
||||||
|
.write_openssh_file(&path, ssh_key::LineEnding::LF)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let loaded = load_ed25519_identity(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
VerifyingKey::from(&loaded).to_bytes(),
|
||||||
|
VerifyingKey::from(&signing).to_bytes()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_session_key_derivation_matches_and_binds_transcript() {
|
||||||
|
let host_signing = SigningKey::from_bytes(&[3u8; 32]);
|
||||||
|
let (client_secret, client_public) = generate_native_ephemeral();
|
||||||
|
let (server_secret, server_public) = generate_native_ephemeral();
|
||||||
|
let mut client = test_client_hello();
|
||||||
|
client.client_ephemeral_public = client_public;
|
||||||
|
let mut server = test_server_hello(&host_signing);
|
||||||
|
server.server_ephemeral_public = server_public;
|
||||||
|
sign_server_hello(&host_signing, &client, &mut server).unwrap();
|
||||||
|
|
||||||
|
let client_key = derive_native_session_key(
|
||||||
|
&client_secret,
|
||||||
|
server.server_ephemeral_public,
|
||||||
|
&client,
|
||||||
|
&server,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let server_key = derive_native_session_key(
|
||||||
|
&server_secret,
|
||||||
|
client.client_ephemeral_public,
|
||||||
|
&client,
|
||||||
|
&server,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(client_key, server_key);
|
||||||
|
|
||||||
|
let mut tampered = server.clone();
|
||||||
|
tampered.auth_challenge = [99u8; 32];
|
||||||
|
let tampered_key = derive_native_session_key(
|
||||||
|
&client_secret,
|
||||||
|
tampered.server_ephemeral_public,
|
||||||
|
&client,
|
||||||
|
&tampered,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(client_key, tampered_key);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_user_auth_enforces_no_port_forwarding_and_permitopen() {
|
fn native_user_auth_enforces_no_port_forwarding_and_permitopen() {
|
||||||
let host_signing = SigningKey::from_bytes(&[3u8; 32]);
|
let host_signing = SigningKey::from_bytes(&[3u8; 32]);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::auth::BootstrapResponse;
|
use crate::auth::BootstrapResponse;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
|
use crate::native::{NativeAuthOk, NativeClientHello, NativeServerHello, NativeUserAuth};
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ pub enum PacketKind {
|
|||||||
Ping = 11,
|
Ping = 11,
|
||||||
Pong = 12,
|
Pong = 12,
|
||||||
Detach = 13,
|
Detach = 13,
|
||||||
|
NativeClientHello = 14,
|
||||||
|
NativeServerHello = 15,
|
||||||
|
NativeUserAuth = 16,
|
||||||
|
NativeAuthOk = 17,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for PacketKind {
|
impl TryFrom<u8> for PacketKind {
|
||||||
@@ -45,6 +50,10 @@ impl TryFrom<u8> for PacketKind {
|
|||||||
11 => Self::Ping,
|
11 => Self::Ping,
|
||||||
12 => Self::Pong,
|
12 => Self::Pong,
|
||||||
13 => Self::Detach,
|
13 => Self::Detach,
|
||||||
|
14 => Self::NativeClientHello,
|
||||||
|
15 => Self::NativeServerHello,
|
||||||
|
16 => Self::NativeUserAuth,
|
||||||
|
17 => Self::NativeAuthOk,
|
||||||
_ => bail!("unknown packet kind {value}"),
|
_ => bail!("unknown packet kind {value}"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -214,6 +223,26 @@ pub struct TicketAttachOkEnvelope {
|
|||||||
pub ciphertext: Vec<u8>,
|
pub ciphertext: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NativeClientHelloBody {
|
||||||
|
pub hello: NativeClientHello,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NativeServerHelloBody {
|
||||||
|
pub hello: NativeServerHello,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NativeUserAuthBody {
|
||||||
|
pub auth: NativeUserAuth,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NativeAuthOkBody {
|
||||||
|
pub ok: NativeAuthOk,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AttachOk {
|
pub struct AttachOk {
|
||||||
pub client_id: [u8; 16],
|
pub client_id: [u8; 16],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::time::Duration;
|
|||||||
use dosh::auth::{build_bootstrap, load_or_create_server_secret};
|
use dosh::auth::{build_bootstrap, load_or_create_server_secret};
|
||||||
use dosh::config::load_server_config;
|
use dosh::config::load_server_config;
|
||||||
use dosh::crypto;
|
use dosh::crypto;
|
||||||
|
use dosh::native::{host_public_key, load_or_create_host_key, trust_host};
|
||||||
use dosh::protocol::{
|
use dosh::protocol::{
|
||||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||||
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT,
|
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT,
|
||||||
@@ -47,9 +48,13 @@ create_on_attach = true
|
|||||||
shell = "/bin/sh"
|
shell = "/bin/sh"
|
||||||
sessions_dir = "{sessions}"
|
sessions_dir = "{sessions}"
|
||||||
secret_path = "{secret}"
|
secret_path = "{secret}"
|
||||||
|
host_key = "{host_key}"
|
||||||
|
authorized_keys = ["{authorized_keys}"]
|
||||||
"#,
|
"#,
|
||||||
sessions = dir.path().join("sessions").display(),
|
sessions = dir.path().join("sessions").display(),
|
||||||
secret = dir.path().join("secret").display(),
|
secret = dir.path().join("secret").display(),
|
||||||
|
host_key = dir.path().join("host_key").display(),
|
||||||
|
authorized_keys = dir.path().join("authorized_keys").display(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -89,6 +94,54 @@ fn attach_once(dir: &tempfile::TempDir, port: u16, cache: bool) -> std::process:
|
|||||||
cmd.output().unwrap()
|
cmd.output().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn native_attach_once(dir: &tempfile::TempDir, port: u16) -> std::process::Output {
|
||||||
|
let client = env!("CARGO_BIN_EXE_dosh-client");
|
||||||
|
Command::new(client)
|
||||||
|
.arg("--auth")
|
||||||
|
.arg("native")
|
||||||
|
.arg("--attach-only")
|
||||||
|
.arg("-v")
|
||||||
|
.arg("--session")
|
||||||
|
.arg("default")
|
||||||
|
.arg("--dosh-host")
|
||||||
|
.arg("127.0.0.1")
|
||||||
|
.arg("--dosh-port")
|
||||||
|
.arg(port.to_string())
|
||||||
|
.arg("local")
|
||||||
|
.env("HOME", dir.path())
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Path) {
|
||||||
|
let ssh_dir = dir.path().join(".ssh");
|
||||||
|
fs::create_dir_all(&ssh_dir).unwrap();
|
||||||
|
let signing = ed25519_dalek::SigningKey::from_bytes(&[42u8; 32]);
|
||||||
|
let keypair = ssh_key::private::Ed25519Keypair::from(&signing);
|
||||||
|
let private =
|
||||||
|
ssh_key::PrivateKey::new(ssh_key::private::KeypairData::from(keypair), "test").unwrap();
|
||||||
|
private
|
||||||
|
.write_openssh_file(&ssh_dir.join("id_ed25519"), ssh_key::LineEnding::LF)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("authorized_keys"),
|
||||||
|
format!("{}\n", private.public_key().to_openssh().unwrap()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = load_server_config(Some(config_path.to_path_buf())).unwrap();
|
||||||
|
let host_signing = load_or_create_host_key(&config).unwrap();
|
||||||
|
let known_hosts = dir.path().join(".config/dosh/known_hosts");
|
||||||
|
trust_host(
|
||||||
|
&known_hosts,
|
||||||
|
"local",
|
||||||
|
&host_public_key(&host_signing),
|
||||||
|
"test",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
fn direct_attach(
|
fn direct_attach(
|
||||||
config: &std::path::Path,
|
config: &std::path::Path,
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -275,6 +328,22 @@ fn local_attach_only_smoke() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_attach_only_smoke() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let port = free_udp_port();
|
||||||
|
let config = write_server_config(&dir, port);
|
||||||
|
write_native_client_auth(&dir, &config);
|
||||||
|
let mut server = start_server(&dir, &config);
|
||||||
|
let output = native_attach_once(&dir, port);
|
||||||
|
let _ = server.kill();
|
||||||
|
let _ = server.wait();
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(output.status.success(), "stderr={stderr}");
|
||||||
|
assert!(stderr.contains("native_auth"), "stderr={stderr}");
|
||||||
|
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_attach_after_server_restart_smoke() {
|
fn ticket_attach_after_server_restart_smoke() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user