fabrica

hanna/fabrica

feat(ssh): russh server — publickey auth and exec pack dispatch

79ec208 · hanna committed on 2026-07-25

Add the SSH front-end. A russh 0.62 server accepts only publickey auth
(identity by the offered key's SHA256 fingerprint, matched against the
keys table; last_used_at bumped), handles only the exec channel, and
streams git-upload-pack/receive-pack over git::pack via the async Handle.
Read needs Read access, push needs Write. A shell request (or any other
command) gets the friendly banner and exit 1; an unregistered key is
rejected.

The host key (ed25519) is generated at ssh.host_key_path on first start
(mode 0600) and bridged to russh's ssh-key version via the OpenSSH PEM.
The root binary runs the SSH server alongside web. Pushes set FABRICA_*
identity + FABRICA_CONFIG so post-receive updates pushed_at.

Verified end-to-end: SSH clone, SSH push (hook fires), the shell banner,
and unregistered-key rejection. russh's aws-lc-rs backend requires the
OpenSSL license and cmake — admitted for the mandatory SSH crypto only
(see docs/decisions.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
14 files changed · +2046 −152UnifiedSplit
Cargo.lock +1336 −146
@@ -16,7 +16,17 @@ checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
1616 dependencies = [
1717 "bytes",
1818 "crypto-common 0.1.6",
19- "generic-array",
19+ "generic-array 0.14.9",
20+]
21+
22+[[package]]
23+name = "aead"
24+version = "0.6.1"
25+source = "registry+https://github.com/rust-lang/crates.io-index"
26+checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99"
27+dependencies = [
28+ "crypto-common 0.2.2",
29+ "inout 0.2.2",
2030 ]
2131
2232 [[package]]
@@ -26,22 +36,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2636 checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
2737 dependencies = [
2838 "cfg-if",
29- "cipher",
39+ "cipher 0.4.4",
3040 "cpufeatures 0.2.17",
3141 ]
3242
3343 [[package]]
44+name = "aes"
45+version = "0.9.1"
46+source = "registry+https://github.com/rust-lang/crates.io-index"
47+checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138"
48+dependencies = [
49+ "cipher 0.5.2",
50+ "cpubits",
51+ "cpufeatures 0.3.0",
52+ "zeroize",
53+]
54+
55+[[package]]
3456 name = "aes-gcm"
3557 version = "0.10.3"
3658 source = "registry+https://github.com/rust-lang/crates.io-index"
3759 checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
3860 dependencies = [
39- "aead",
40- "aes",
41- "cipher",
42- "ctr",
43- "ghash",
61+ "aead 0.5.2",
62+ "aes 0.8.4",
63+ "cipher 0.4.4",
64+ "ctr 0.9.2",
65+ "ghash 0.5.1",
66+ "subtle",
67+]
68+
69+[[package]]
70+name = "aes-gcm"
71+version = "0.11.0"
72+source = "registry+https://github.com/rust-lang/crates.io-index"
73+checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028"
74+dependencies = [
75+ "aead 0.6.1",
76+ "aes 0.9.1",
77+ "cipher 0.5.2",
78+ "ctr 0.10.1",
79+ "ghash 0.6.0",
4480 "subtle",
81+ "zeroize",
4582 ]
4683
4784 [[package]]
@@ -50,7 +87,7 @@ version = "0.2.1"
5087 source = "registry+https://github.com/rust-lang/crates.io-index"
5188 checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c"
5289 dependencies = [
53- "aes",
90+ "aes 0.8.4",
5491 ]
5592
5693 [[package]]
@@ -94,6 +131,15 @@ dependencies = [
94131 ]
95132
96133 [[package]]
134+name = "android_system_properties"
135+version = "0.1.5"
136+source = "registry+https://github.com/rust-lang/crates.io-index"
137+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
138+dependencies = [
139+ "libc",
140+]
141+
142+[[package]]
97143 name = "anstream"
98144 version = "1.0.0"
99145 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -160,13 +206,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
160206 checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
161207 dependencies = [
162208 "base64ct",
163- "blake2",
209+ "blake2 0.10.6",
164210 "cpufeatures 0.2.17",
165- "password-hash",
211+ "password-hash 0.5.0",
166212 "zeroize",
167213 ]
168214
169215 [[package]]
216+name = "argon2"
217+version = "0.6.0-rc.8"
218+source = "registry+https://github.com/rust-lang/crates.io-index"
219+checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5"
220+dependencies = [
221+ "base64ct",
222+ "blake2 0.11.0-rc.6",
223+ "cpufeatures 0.3.0",
224+ "password-hash 0.6.1",
225+]
226+
227+[[package]]
170228 name = "async-compression"
171229 version = "0.4.42"
172230 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -217,7 +275,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
217275 name = "auth"
218276 version = "0.1.0"
219277 dependencies = [
220- "argon2",
278+ "argon2 0.5.3",
221279 "base64",
222280 "hmac 0.12.1",
223281 "model",
@@ -235,6 +293,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
235293 checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
236294
237295 [[package]]
296+name = "aws-lc-rs"
297+version = "1.17.3"
298+source = "registry+https://github.com/rust-lang/crates.io-index"
299+checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
300+dependencies = [
301+ "aws-lc-sys",
302+ "untrusted",
303+ "zeroize",
304+]
305+
306+[[package]]
307+name = "aws-lc-sys"
308+version = "0.43.0"
309+source = "registry+https://github.com/rust-lang/crates.io-index"
310+checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
311+dependencies = [
312+ "cc",
313+ "cmake",
314+ "dunce",
315+ "fs_extra",
316+ "pkg-config",
317+]
318+
319+[[package]]
238320 name = "axum"
239321 version = "0.8.9"
240322 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -315,6 +397,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
315397 checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
316398
317399 [[package]]
400+name = "base16ct"
401+version = "1.0.0"
402+source = "registry+https://github.com/rust-lang/crates.io-index"
403+checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6"
404+
405+[[package]]
318406 name = "base64"
319407 version = "0.22.1"
320408 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -327,6 +415,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
327415 checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
328416
329417 [[package]]
418+name = "bcrypt-pbkdf"
419+version = "0.11.0"
420+source = "registry+https://github.com/rust-lang/crates.io-index"
421+checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0"
422+dependencies = [
423+ "blowfish 0.10.0",
424+ "pbkdf2",
425+ "sha2 0.11.0",
426+]
427+
428+[[package]]
330429 name = "bitfields"
331430 version = "1.0.3"
332431 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -378,12 +477,21 @@ dependencies = [
378477 ]
379478
380479 [[package]]
480+name = "blake2"
481+version = "0.11.0-rc.6"
482+source = "registry+https://github.com/rust-lang/crates.io-index"
483+checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690"
484+dependencies = [
485+ "digest 0.11.3",
486+]
487+
488+[[package]]
381489 name = "block-buffer"
382490 version = "0.10.4"
383491 source = "registry+https://github.com/rust-lang/crates.io-index"
384492 checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
385493 dependencies = [
386- "generic-array",
494+ "generic-array 0.14.9",
387495 ]
388496
389497 [[package]]
@@ -393,6 +501,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
393501 checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
394502 dependencies = [
395503 "hybrid-array",
504+ "zeroize",
396505 ]
397506
398507 [[package]]
@@ -401,7 +510,16 @@ version = "0.3.3"
401510 source = "registry+https://github.com/rust-lang/crates.io-index"
402511 checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
403512 dependencies = [
404- "generic-array",
513+ "generic-array 0.14.9",
514+]
515+
516+[[package]]
517+name = "block-padding"
518+version = "0.4.2"
519+source = "registry+https://github.com/rust-lang/crates.io-index"
520+checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
521+dependencies = [
522+ "hybrid-array",
405523 ]
406524
407525 [[package]]
@@ -411,7 +529,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
411529 checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
412530 dependencies = [
413531 "byteorder",
414- "cipher",
532+ "cipher 0.4.4",
533+]
534+
535+[[package]]
536+name = "blowfish"
537+version = "0.10.0"
538+source = "registry+https://github.com/rust-lang/crates.io-index"
539+checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298"
540+dependencies = [
541+ "byteorder",
542+ "cipher 0.5.2",
415543 ]
416544
417545 [[package]]
@@ -473,7 +601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
473601 checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30"
474602 dependencies = [
475603 "byteorder",
476- "cipher",
604+ "cipher 0.4.4",
477605 ]
478606
479607 [[package]]
@@ -491,7 +619,16 @@ version = "0.11.1"
491619 source = "registry+https://github.com/rust-lang/crates.io-index"
492620 checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911"
493621 dependencies = [
494- "cipher",
622+ "cipher 0.4.4",
623+]
624+
625+[[package]]
626+name = "cbc"
627+version = "0.2.1"
628+source = "registry+https://github.com/rust-lang/crates.io-index"
629+checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
630+dependencies = [
631+ "cipher 0.5.2",
495632 ]
496633
497634 [[package]]
@@ -512,7 +649,7 @@ version = "0.8.2"
512649 source = "registry+https://github.com/rust-lang/crates.io-index"
513650 checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330"
514651 dependencies = [
515- "cipher",
652+ "cipher 0.4.4",
516653 ]
517654
518655 [[package]]
@@ -522,14 +659,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
522659 checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
523660
524661 [[package]]
662+name = "cfg_aliases"
663+version = "0.2.2"
664+source = "registry+https://github.com/rust-lang/crates.io-index"
665+checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
666+
667+[[package]]
525668 name = "chacha20"
526669 version = "0.10.1"
527670 source = "registry+https://github.com/rust-lang/crates.io-index"
528671 checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
529672 dependencies = [
530673 "cfg-if",
674+ "cipher 0.5.2",
531675 "cpufeatures 0.3.0",
532676 "rand_core 0.10.1",
677+ "zeroize",
678+]
679+
680+[[package]]
681+name = "chrono"
682+version = "0.4.45"
683+source = "registry+https://github.com/rust-lang/crates.io-index"
684+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
685+dependencies = [
686+ "iana-time-zone",
687+ "js-sys",
688+ "num-traits",
689+ "wasm-bindgen",
690+ "windows-link",
533691 ]
534692
535693 [[package]]
@@ -539,7 +697,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
539697 checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
540698 dependencies = [
541699 "crypto-common 0.1.6",
542- "inout",
700+ "inout 0.1.4",
701+]
702+
703+[[package]]
704+name = "cipher"
705+version = "0.5.2"
706+source = "registry+https://github.com/rust-lang/crates.io-index"
707+checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
708+dependencies = [
709+ "block-buffer 0.12.1",
710+ "crypto-common 0.2.2",
711+ "inout 0.2.2",
712+ "zeroize",
543713 ]
544714
545715 [[package]]
@@ -607,12 +777,21 @@ version = "0.7.2"
607777 source = "registry+https://github.com/rust-lang/crates.io-index"
608778 checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa"
609779 dependencies = [
610- "cipher",
780+ "cipher 0.4.4",
611781 "dbl",
612782 "digest 0.10.7",
613783 ]
614784
615785 [[package]]
786+name = "cmake"
787+version = "0.1.58"
788+source = "registry+https://github.com/rust-lang/crates.io-index"
789+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
790+dependencies = [
791+ "cc",
792+]
793+
794+[[package]]
616795 name = "cmov"
617796 version = "0.5.4"
618797 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -738,6 +917,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
738917 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
739918
740919 [[package]]
920+name = "cpubits"
921+version = "0.1.1"
922+source = "registry+https://github.com/rust-lang/crates.io-index"
923+checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
924+
925+[[package]]
741926 name = "cpufeatures"
742927 version = "0.2.17"
743928 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -824,19 +1009,36 @@ version = "0.5.5"
8241009 source = "registry+https://github.com/rust-lang/crates.io-index"
8251010 checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
8261011 dependencies = [
827- "generic-array",
1012+ "generic-array 0.14.9",
8281013 "rand_core 0.6.4",
8291014 "subtle",
8301015 "zeroize",
8311016 ]
8321017
8331018 [[package]]
1019+name = "crypto-bigint"
1020+version = "0.7.5"
1021+source = "registry+https://github.com/rust-lang/crates.io-index"
1022+checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271"
1023+dependencies = [
1024+ "cpubits",
1025+ "ctutils",
1026+ "getrandom 0.4.3",
1027+ "hybrid-array",
1028+ "num-traits",
1029+ "rand_core 0.10.1",
1030+ "serdect 0.4.3",
1031+ "subtle",
1032+ "zeroize",
1033+]
1034+
1035+[[package]]
8341036 name = "crypto-common"
8351037 version = "0.1.6"
8361038 source = "registry+https://github.com/rust-lang/crates.io-index"
8371039 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
8381040 dependencies = [
839- "generic-array",
1041+ "generic-array 0.14.9",
8401042 "rand_core 0.6.4",
8411043 "typenum",
8421044 ]
@@ -847,7 +1049,19 @@ version = "0.2.2"
8471049 source = "registry+https://github.com/rust-lang/crates.io-index"
8481050 checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
8491051 dependencies = [
1052+ "getrandom 0.4.3",
8501053 "hybrid-array",
1054+ "rand_core 0.10.1",
1055+]
1056+
1057+[[package]]
1058+name = "crypto-primes"
1059+version = "0.7.2"
1060+source = "registry+https://github.com/rust-lang/crates.io-index"
1061+checksum = "3633a51a39c69ebbaa4feaa694bd83d241e4093901c84a0963b19d9bb3f0cf8f"
1062+dependencies = [
1063+ "crypto-bigint 0.7.5",
1064+ "rand_core 0.10.1",
8511065 ]
8521066
8531067 [[package]]
@@ -867,7 +1081,16 @@ version = "0.9.2"
8671081 source = "registry+https://github.com/rust-lang/crates.io-index"
8681082 checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
8691083 dependencies = [
870- "cipher",
1084+ "cipher 0.4.4",
1085+]
1086+
1087+[[package]]
1088+name = "ctr"
1089+version = "0.10.1"
1090+source = "registry+https://github.com/rust-lang/crates.io-index"
1091+checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21"
1092+dependencies = [
1093+ "cipher 0.5.2",
8711094 ]
8721095
8731096 [[package]]
@@ -877,6 +1100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
8771100 checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
8781101 dependencies = [
8791102 "cmov",
1103+ "subtle",
8801104 ]
8811105
8821106 [[package]]
@@ -889,7 +1113,24 @@ dependencies = [
8891113 "cpufeatures 0.2.17",
8901114 "curve25519-dalek-derive",
8911115 "digest 0.10.7",
892- "fiat-crypto",
1116+ "fiat-crypto 0.2.9",
1117+ "rustc_version",
1118+ "subtle",
1119+ "zeroize",
1120+]
1121+
1122+[[package]]
1123+name = "curve25519-dalek"
1124+version = "5.0.0"
1125+source = "registry+https://github.com/rust-lang/crates.io-index"
1126+checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
1127+dependencies = [
1128+ "cfg-if",
1129+ "cpufeatures 0.3.0",
1130+ "curve25519-dalek-derive",
1131+ "digest 0.11.3",
1132+ "fiat-crypto 0.3.0",
1133+ "rand_core 0.10.1",
8931134 "rustc_version",
8941135 "subtle",
8951136 "zeroize",
@@ -912,13 +1153,13 @@ version = "0.1.1"
9121153 source = "registry+https://github.com/rust-lang/crates.io-index"
9131154 checksum = "b4c0cf476284b03eb6c10e78787b21c7abb7d7d43cb2f02532ba6b831ed892fa"
9141155 dependencies = [
915- "crypto-bigint",
916- "elliptic-curve",
917- "pkcs8",
1156+ "crypto-bigint 0.5.5",
1157+ "elliptic-curve 0.13.8",
1158+ "pkcs8 0.10.2",
9181159 "rand_core 0.6.4",
9191160 "serdect 0.3.0",
920- "sha3",
921- "signature",
1161+ "sha3 0.10.9",
1162+ "signature 2.2.0",
9221163 "subtle",
9231164 "zeroize",
9241165 ]
@@ -959,12 +1200,29 @@ dependencies = [
9591200 ]
9601201
9611202 [[package]]
1203+name = "data-encoding"
1204+version = "2.11.0"
1205+source = "registry+https://github.com/rust-lang/crates.io-index"
1206+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
1207+
1208+[[package]]
9621209 name = "dbl"
9631210 version = "0.3.2"
9641211 source = "registry+https://github.com/rust-lang/crates.io-index"
9651212 checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9"
9661213 dependencies = [
967- "generic-array",
1214+ "generic-array 0.14.9",
1215+]
1216+
1217+[[package]]
1218+name = "delegate"
1219+version = "0.13.5"
1220+source = "registry+https://github.com/rust-lang/crates.io-index"
1221+checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370"
1222+dependencies = [
1223+ "proc-macro2",
1224+ "quote",
1225+ "syn 2.0.119",
9681226 ]
9691227
9701228 [[package]]
@@ -974,7 +1232,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
9741232 checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
9751233 dependencies = [
9761234 "const-oid 0.9.6",
977- "pem-rfc7468",
1235+ "pem-rfc7468 0.7.0",
1236+ "zeroize",
1237+]
1238+
1239+[[package]]
1240+name = "der"
1241+version = "0.8.1"
1242+source = "registry+https://github.com/rust-lang/crates.io-index"
1243+checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d"
1244+dependencies = [
1245+ "const-oid 0.10.2",
1246+ "pem-rfc7468 1.0.0",
9781247 "zeroize",
9791248 ]
9801249
@@ -1047,7 +1316,16 @@ version = "0.8.1"
10471316 source = "registry+https://github.com/rust-lang/crates.io-index"
10481317 checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e"
10491318 dependencies = [
1050- "cipher",
1319+ "cipher 0.4.4",
1320+]
1321+
1322+[[package]]
1323+name = "des"
1324+version = "0.9.0"
1325+source = "registry+https://github.com/rust-lang/crates.io-index"
1326+checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a"
1327+dependencies = [
1328+ "cipher 0.5.2",
10511329 ]
10521330
10531331 [[package]]
@@ -1100,10 +1378,10 @@ dependencies = [
11001378 "digest 0.10.7",
11011379 "num-bigint-dig",
11021380 "num-traits",
1103- "pkcs8",
1104- "rfc6979",
1381+ "pkcs8 0.10.2",
1382+ "rfc6979 0.4.0",
11051383 "sha2 0.10.9",
1106- "signature",
1384+ "signature 2.2.0",
11071385 "zeroize",
11081386 ]
11091387
@@ -1123,15 +1401,21 @@ dependencies = [
11231401 ]
11241402
11251403 [[package]]
1404+name = "dunce"
1405+version = "1.0.5"
1406+source = "registry+https://github.com/rust-lang/crates.io-index"
1407+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
1408+
1409+[[package]]
11261410 name = "eax"
11271411 version = "0.5.0"
11281412 source = "registry+https://github.com/rust-lang/crates.io-index"
11291413 checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28"
11301414 dependencies = [
1131- "aead",
1132- "cipher",
1415+ "aead 0.5.2",
1416+ "cipher 0.4.4",
11331417 "cmac",
1134- "ctr",
1418+ "ctr 0.9.2",
11351419 "subtle",
11361420 ]
11371421
@@ -1141,12 +1425,27 @@ version = "0.16.9"
11411425 source = "registry+https://github.com/rust-lang/crates.io-index"
11421426 checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
11431427 dependencies = [
1144- "der",
1428+ "der 0.7.10",
11451429 "digest 0.10.7",
1146- "elliptic-curve",
1147- "rfc6979",
1148- "signature",
1149- "spki",
1430+ "elliptic-curve 0.13.8",
1431+ "rfc6979 0.4.0",
1432+ "signature 2.2.0",
1433+ "spki 0.7.3",
1434+]
1435+
1436+[[package]]
1437+name = "ecdsa"
1438+version = "0.17.0"
1439+source = "registry+https://github.com/rust-lang/crates.io-index"
1440+checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0"
1441+dependencies = [
1442+ "der 0.8.1",
1443+ "digest 0.11.3",
1444+ "elliptic-curve 0.14.1",
1445+ "rfc6979 0.6.0",
1446+ "signature 3.0.0",
1447+ "spki 0.8.0",
1448+ "zeroize",
11501449 ]
11511450
11521451 [[package]]
@@ -1155,8 +1454,18 @@ version = "2.2.3"
11551454 source = "registry+https://github.com/rust-lang/crates.io-index"
11561455 checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
11571456 dependencies = [
1158- "pkcs8",
1159- "signature",
1457+ "pkcs8 0.10.2",
1458+ "signature 2.2.0",
1459+]
1460+
1461+[[package]]
1462+name = "ed25519"
1463+version = "3.0.0"
1464+source = "registry+https://github.com/rust-lang/crates.io-index"
1465+checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
1466+dependencies = [
1467+ "pkcs8 0.11.0",
1468+ "signature 3.0.0",
11601469 ]
11611470
11621471 [[package]]
@@ -1165,8 +1474,8 @@ version = "2.2.0"
11651474 source = "registry+https://github.com/rust-lang/crates.io-index"
11661475 checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
11671476 dependencies = [
1168- "curve25519-dalek",
1169- "ed25519",
1477+ "curve25519-dalek 4.1.3",
1478+ "ed25519 2.2.3",
11701479 "rand_core 0.6.4",
11711480 "serde",
11721481 "sha2 0.10.9",
@@ -1175,6 +1484,22 @@ dependencies = [
11751484 ]
11761485
11771486 [[package]]
1487+name = "ed25519-dalek"
1488+version = "3.0.0"
1489+source = "registry+https://github.com/rust-lang/crates.io-index"
1490+checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
1491+dependencies = [
1492+ "curve25519-dalek 5.0.0",
1493+ "ed25519 3.0.0",
1494+ "rand_core 0.10.1",
1495+ "serde",
1496+ "sha2 0.11.0",
1497+ "signature 3.0.0",
1498+ "subtle",
1499+ "zeroize",
1500+]
1501+
1502+[[package]]
11781503 name = "either"
11791504 version = "1.17.0"
11801505 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1189,18 +1514,18 @@ version = "0.13.8"
11891514 source = "registry+https://github.com/rust-lang/crates.io-index"
11901515 checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
11911516 dependencies = [
1192- "base16ct",
1517+ "base16ct 0.2.0",
11931518 "base64ct",
1194- "crypto-bigint",
1519+ "crypto-bigint 0.5.5",
11951520 "digest 0.10.7",
1196- "ff",
1197- "generic-array",
1198- "group",
1521+ "ff 0.13.1",
1522+ "generic-array 0.14.9",
1523+ "group 0.13.0",
11991524 "hkdf 0.12.4",
1200- "pem-rfc7468",
1201- "pkcs8",
1525+ "pem-rfc7468 0.7.0",
1526+ "pkcs8 0.10.2",
12021527 "rand_core 0.6.4",
1203- "sec1",
1528+ "sec1 0.7.3",
12041529 "serde_json",
12051530 "serdect 0.2.0",
12061531 "subtle",
@@ -1209,6 +1534,28 @@ dependencies = [
12091534 ]
12101535
12111536 [[package]]
1537+name = "elliptic-curve"
1538+version = "0.14.1"
1539+source = "registry+https://github.com/rust-lang/crates.io-index"
1540+checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65"
1541+dependencies = [
1542+ "base16ct 1.0.0",
1543+ "crypto-bigint 0.7.5",
1544+ "crypto-common 0.2.2",
1545+ "digest 0.11.3",
1546+ "ff 0.14.0",
1547+ "group 0.14.0",
1548+ "hkdf 0.13.0",
1549+ "hybrid-array",
1550+ "pem-rfc7468 1.0.0",
1551+ "pkcs8 0.11.0",
1552+ "rand_core 0.10.1",
1553+ "sec1 0.8.1",
1554+ "subtle",
1555+ "zeroize",
1556+]
1557+
1558+[[package]]
12121559 name = "email-encoding"
12131560 version = "0.4.1"
12141561 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1237,6 +1584,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
12371584 checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
12381585
12391586 [[package]]
1587+name = "enum_dispatch"
1588+version = "0.3.13"
1589+source = "registry+https://github.com/rust-lang/crates.io-index"
1590+checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
1591+dependencies = [
1592+ "once_cell",
1593+ "proc-macro2",
1594+ "quote",
1595+ "syn 2.0.119",
1596+]
1597+
1598+[[package]]
12401599 name = "equivalent"
12411600 version = "1.0.2"
12421601 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1280,6 +1639,7 @@ dependencies = [
12801639 "anyhow",
12811640 "cli",
12821641 "config",
1642+ "ssh",
12831643 "store",
12841644 "tokio",
12851645 "web",
@@ -1303,12 +1663,28 @@ dependencies = [
13031663 ]
13041664
13051665 [[package]]
1666+name = "ff"
1667+version = "0.14.0"
1668+source = "registry+https://github.com/rust-lang/crates.io-index"
1669+checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f"
1670+dependencies = [
1671+ "rand_core 0.10.1",
1672+ "subtle",
1673+]
1674+
1675+[[package]]
13061676 name = "fiat-crypto"
13071677 version = "0.2.9"
13081678 source = "registry+https://github.com/rust-lang/crates.io-index"
13091679 checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
13101680
13111681 [[package]]
1682+name = "fiat-crypto"
1683+version = "0.3.0"
1684+source = "registry+https://github.com/rust-lang/crates.io-index"
1685+checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
1686+
1687+[[package]]
13121688 name = "figment"
13131689 version = "0.10.19"
13141690 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1395,12 +1771,33 @@ dependencies = [
13951771 ]
13961772
13971773 [[package]]
1774+name = "fs_extra"
1775+version = "1.3.0"
1776+source = "registry+https://github.com/rust-lang/crates.io-index"
1777+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
1778+
1779+[[package]]
13981780 name = "funty"
13991781 version = "2.0.0"
14001782 source = "registry+https://github.com/rust-lang/crates.io-index"
14011783 checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
14021784
14031785 [[package]]
1786+name = "futures"
1787+version = "0.3.33"
1788+source = "registry+https://github.com/rust-lang/crates.io-index"
1789+checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
1790+dependencies = [
1791+ "futures-channel",
1792+ "futures-core",
1793+ "futures-executor",
1794+ "futures-io",
1795+ "futures-sink",
1796+ "futures-task",
1797+ "futures-util",
1798+]
1799+
1800+[[package]]
14041801 name = "futures-channel"
14051802 version = "0.3.33"
14061803 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1445,6 +1842,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
14451842 checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
14461843
14471844 [[package]]
1845+name = "futures-macro"
1846+version = "0.3.33"
1847+source = "registry+https://github.com/rust-lang/crates.io-index"
1848+checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
1849+dependencies = [
1850+ "proc-macro2",
1851+ "quote",
1852+ "syn 2.0.119",
1853+]
1854+
1855+[[package]]
14481856 name = "futures-sink"
14491857 version = "0.3.33"
14501858 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1462,8 +1870,10 @@ version = "0.3.33"
14621870 source = "registry+https://github.com/rust-lang/crates.io-index"
14631871 checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
14641872 dependencies = [
1873+ "futures-channel",
14651874 "futures-core",
14661875 "futures-io",
1876+ "futures-macro",
14671877 "futures-sink",
14681878 "futures-task",
14691879 "memchr",
@@ -1483,6 +1893,17 @@ dependencies = [
14831893 ]
14841894
14851895 [[package]]
1896+name = "generic-array"
1897+version = "1.4.4"
1898+source = "registry+https://github.com/rust-lang/crates.io-index"
1899+checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b"
1900+dependencies = [
1901+ "generic-array 0.14.9",
1902+ "rustversion",
1903+ "typenum",
1904+]
1905+
1906+[[package]]
14861907 name = "getrandom"
14871908 version = "0.2.17"
14881909 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1512,9 +1933,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
15121933 checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
15131934 dependencies = [
15141935 "cfg-if",
1936+ "js-sys",
15151937 "libc",
15161938 "r-efi 6.0.0",
15171939 "rand_core 0.10.1",
1940+ "wasm-bindgen",
15181941 ]
15191942
15201943 [[package]]
@@ -1524,7 +1947,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
15241947 checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
15251948 dependencies = [
15261949 "opaque-debug",
1527- "polyval",
1950+ "polyval 0.6.2",
1951+]
1952+
1953+[[package]]
1954+name = "ghash"
1955+version = "0.6.0"
1956+source = "registry+https://github.com/rust-lang/crates.io-index"
1957+checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
1958+dependencies = [
1959+ "polyval 0.7.3",
15281960 ]
15291961
15301962 [[package]]
@@ -1538,7 +1970,7 @@ dependencies = [
15381970 "moka",
15391971 "pgp",
15401972 "rand_core 0.6.4",
1541- "ssh-key",
1973+ "ssh-key 0.6.7",
15421974 "tempfile",
15431975 "thiserror 2.0.19",
15441976 "tokio",
@@ -1576,12 +2008,23 @@ version = "0.13.0"
15762008 source = "registry+https://github.com/rust-lang/crates.io-index"
15772009 checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
15782010 dependencies = [
1579- "ff",
2011+ "ff 0.13.1",
15802012 "rand_core 0.6.4",
15812013 "subtle",
15822014 ]
15832015
15842016 [[package]]
2017+name = "group"
2018+version = "0.14.0"
2019+source = "registry+https://github.com/rust-lang/crates.io-index"
2020+checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4"
2021+dependencies = [
2022+ "ff 0.14.0",
2023+ "rand_core 0.10.1",
2024+ "subtle",
2025+]
2026+
2027+[[package]]
15852028 name = "hashbrown"
15862029 version = "0.16.1"
15872030 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1620,6 +2063,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
16202063 checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
16212064
16222065 [[package]]
2066+name = "hex-literal"
2067+version = "1.1.0"
2068+source = "registry+https://github.com/rust-lang/crates.io-index"
2069+checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1"
2070+
2071+[[package]]
16232072 name = "highlight"
16242073 version = "0.1.0"
16252074 dependencies = [
@@ -1735,7 +2184,10 @@ version = "0.4.13"
17352184 source = "registry+https://github.com/rust-lang/crates.io-index"
17362185 checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
17372186 dependencies = [
2187+ "ctutils",
2188+ "subtle",
17382189 "typenum",
2190+ "zeroize",
17392191 ]
17402192
17412193 [[package]]
@@ -1774,6 +2226,30 @@ dependencies = [
17742226 ]
17752227
17762228 [[package]]
2229+name = "iana-time-zone"
2230+version = "0.1.65"
2231+source = "registry+https://github.com/rust-lang/crates.io-index"
2232+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
2233+dependencies = [
2234+ "android_system_properties",
2235+ "core-foundation-sys",
2236+ "iana-time-zone-haiku",
2237+ "js-sys",
2238+ "log",
2239+ "wasm-bindgen",
2240+ "windows-core",
2241+]
2242+
2243+[[package]]
2244+name = "iana-time-zone-haiku"
2245+version = "0.1.2"
2246+source = "registry+https://github.com/rust-lang/crates.io-index"
2247+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
2248+dependencies = [
2249+ "cc",
2250+]
2251+
2252+[[package]]
17772253 name = "icu_collections"
17782254 version = "2.1.1"
17792255 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1860,7 +2336,7 @@ version = "0.5.1"
18602336 source = "registry+https://github.com/rust-lang/crates.io-index"
18612337 checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477"
18622338 dependencies = [
1863- "cipher",
2339+ "cipher 0.4.4",
18642340 ]
18652341
18662342 [[package]]
@@ -1929,7 +2405,17 @@ version = "0.1.4"
19292405 source = "registry+https://github.com/rust-lang/crates.io-index"
19302406 checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
19312407 dependencies = [
1932- "generic-array",
2408+ "generic-array 0.14.9",
2409+]
2410+
2411+[[package]]
2412+name = "inout"
2413+version = "0.2.2"
2414+source = "registry+https://github.com/rust-lang/crates.io-index"
2415+checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
2416+dependencies = [
2417+ "block-padding 0.4.2",
2418+ "hybrid-array",
19332419 ]
19342420
19352421 [[package]]
@@ -1945,6 +2431,18 @@ dependencies = [
19452431 ]
19462432
19472433 [[package]]
2434+name = "internal-russh-num-bigint"
2435+version = "0.5.0"
2436+source = "registry+https://github.com/rust-lang/crates.io-index"
2437+checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8"
2438+dependencies = [
2439+ "num-integer",
2440+ "num-traits",
2441+ "rand 0.10.2",
2442+ "rand_core 0.10.1",
2443+]
2444+
2445+[[package]]
19482446 name = "is_terminal_polyfill"
19492447 version = "1.70.2"
19502448 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1990,11 +2488,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19902488 checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b"
19912489 dependencies = [
19922490 "cfg-if",
1993- "ecdsa",
1994- "elliptic-curve",
2491+ "ecdsa 0.16.9",
2492+ "elliptic-curve 0.13.8",
19952493 "once_cell",
19962494 "sha2 0.10.9",
1997- "signature",
2495+ "signature 2.2.0",
19982496 ]
19992497
20002498 [[package]]
@@ -2007,6 +2505,26 @@ dependencies = [
20072505 ]
20082506
20092507 [[package]]
2508+name = "keccak"
2509+version = "0.2.0"
2510+source = "registry+https://github.com/rust-lang/crates.io-index"
2511+checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa"
2512+dependencies = [
2513+ "cfg-if",
2514+ "cpufeatures 0.3.0",
2515+]
2516+
2517+[[package]]
2518+name = "kem"
2519+version = "0.3.0"
2520+source = "registry+https://github.com/rust-lang/crates.io-index"
2521+checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd"
2522+dependencies = [
2523+ "crypto-common 0.2.2",
2524+ "rand_core 0.10.1",
2525+]
2526+
2527+[[package]]
20102528 name = "lazy_static"
20112529 version = "1.5.0"
20122530 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2209,6 +2727,12 @@ dependencies = [
22092727 ]
22102728
22112729 [[package]]
2730+name = "md5"
2731+version = "0.8.1"
2732+source = "registry+https://github.com/rust-lang/crates.io-index"
2733+checksum = "7ebb8d8732c6a6df3d8f032a82911cfc747e00efb95cc46e8d0acd5b5b88570c"
2734+
2735+[[package]]
22122736 name = "memchr"
22132737 version = "2.8.3"
22142738 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2252,6 +2776,20 @@ dependencies = [
22522776 ]
22532777
22542778 [[package]]
2779+name = "ml-kem"
2780+version = "0.3.2"
2781+source = "registry+https://github.com/rust-lang/crates.io-index"
2782+checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66"
2783+dependencies = [
2784+ "hybrid-array",
2785+ "kem",
2786+ "module-lattice",
2787+ "pkcs8 0.11.0",
2788+ "rand_core 0.10.1",
2789+ "sha3 0.11.0",
2790+]
2791+
2792+[[package]]
22552793 name = "model"
22562794 version = "0.1.0"
22572795 dependencies = [
@@ -2259,6 +2797,17 @@ dependencies = [
22592797 ]
22602798
22612799 [[package]]
2800+name = "module-lattice"
2801+version = "0.2.3"
2802+source = "registry+https://github.com/rust-lang/crates.io-index"
2803+checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe"
2804+dependencies = [
2805+ "ctutils",
2806+ "hybrid-array",
2807+ "num-traits",
2808+]
2809+
2810+[[package]]
22622811 name = "moka"
22632812 version = "0.12.15"
22642813 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2299,6 +2848,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
22992848 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
23002849
23012850 [[package]]
2851+name = "nix"
2852+version = "0.31.3"
2853+source = "registry+https://github.com/rust-lang/crates.io-index"
2854+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
2855+dependencies = [
2856+ "bitflags",
2857+ "cfg-if",
2858+ "cfg_aliases",
2859+ "libc",
2860+]
2861+
2862+[[package]]
23022863 name = "nom"
23032864 version = "8.0.0"
23042865 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2317,6 +2878,16 @@ dependencies = [
23172878 ]
23182879
23192880 [[package]]
2881+name = "num-bigint"
2882+version = "0.4.8"
2883+source = "registry+https://github.com/rust-lang/crates.io-index"
2884+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
2885+dependencies = [
2886+ "num-integer",
2887+ "num-traits",
2888+]
2889+
2890+[[package]]
23202891 name = "num-bigint-dig"
23212892 version = "0.8.6"
23222893 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2395,9 +2966,9 @@ version = "0.1.0"
23952966 source = "registry+https://github.com/rust-lang/crates.io-index"
23962967 checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb"
23972968 dependencies = [
2398- "aead",
2399- "cipher",
2400- "ctr",
2969+ "aead 0.5.2",
2970+ "cipher 0.4.4",
2971+ "ctr 0.9.2",
24012972 "subtle",
24022973 ]
24032974
@@ -2468,39 +3039,100 @@ version = "0.13.2"
24683039 source = "registry+https://github.com/rust-lang/crates.io-index"
24693040 checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
24703041 dependencies = [
2471- "ecdsa",
2472- "elliptic-curve",
2473- "primeorder",
3042+ "ecdsa 0.16.9",
3043+ "elliptic-curve 0.13.8",
3044+ "primeorder 0.13.6",
24743045 "sha2 0.10.9",
24753046 ]
24763047
24773048 [[package]]
3049+name = "p256"
3050+version = "0.14.0"
3051+source = "registry+https://github.com/rust-lang/crates.io-index"
3052+checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a"
3053+dependencies = [
3054+ "ecdsa 0.17.0",
3055+ "elliptic-curve 0.14.1",
3056+ "primefield",
3057+ "primeorder 0.14.0",
3058+ "sha2 0.11.0",
3059+]
3060+
3061+[[package]]
24783062 name = "p384"
24793063 version = "0.13.1"
24803064 source = "registry+https://github.com/rust-lang/crates.io-index"
24813065 checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
24823066 dependencies = [
2483- "ecdsa",
2484- "elliptic-curve",
2485- "primeorder",
3067+ "ecdsa 0.16.9",
3068+ "elliptic-curve 0.13.8",
3069+ "primeorder 0.13.6",
24863070 "sha2 0.10.9",
24873071 ]
24883072
24893073 [[package]]
3074+name = "p384"
3075+version = "0.14.0"
3076+source = "registry+https://github.com/rust-lang/crates.io-index"
3077+checksum = "d17b851e6b3e378ab4ecb07fa2ed23f4d15f075735f8fec9fa1e7bdce5f8301f"
3078+dependencies = [
3079+ "ecdsa 0.17.0",
3080+ "elliptic-curve 0.14.1",
3081+ "fiat-crypto 0.3.0",
3082+ "primefield",
3083+ "primeorder 0.14.0",
3084+ "sha2 0.11.0",
3085+]
3086+
3087+[[package]]
24903088 name = "p521"
24913089 version = "0.13.3"
24923090 source = "registry+https://github.com/rust-lang/crates.io-index"
24933091 checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
24943092 dependencies = [
2495- "base16ct",
2496- "ecdsa",
2497- "elliptic-curve",
2498- "primeorder",
3093+ "base16ct 0.2.0",
3094+ "ecdsa 0.16.9",
3095+ "elliptic-curve 0.13.8",
3096+ "primeorder 0.13.6",
24993097 "rand_core 0.6.4",
25003098 "sha2 0.10.9",
25013099 ]
25023100
25033101 [[package]]
3102+name = "p521"
3103+version = "0.14.0"
3104+source = "registry+https://github.com/rust-lang/crates.io-index"
3105+checksum = "4ad64cc32c2dc466317c12ee5853e61f159f9eab1fe7efade0395dc2e7b43449"
3106+dependencies = [
3107+ "base16ct 1.0.0",
3108+ "ecdsa 0.17.0",
3109+ "elliptic-curve 0.14.1",
3110+ "primefield",
3111+ "primeorder 0.14.0",
3112+ "sha2 0.11.0",
3113+]
3114+
3115+[[package]]
3116+name = "pageant"
3117+version = "0.2.1"
3118+source = "registry+https://github.com/rust-lang/crates.io-index"
3119+checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5"
3120+dependencies = [
3121+ "base16ct 1.0.0",
3122+ "byteorder",
3123+ "bytes",
3124+ "delegate",
3125+ "futures",
3126+ "log",
3127+ "rand 0.10.2",
3128+ "sha2 0.11.0",
3129+ "thiserror 2.0.19",
3130+ "tokio",
3131+ "windows",
3132+ "windows-strings",
3133+]
3134+
3135+[[package]]
25043136 name = "parking"
25053137 version = "2.2.1"
25063138 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2541,6 +3173,25 @@ dependencies = [
25413173 ]
25423174
25433175 [[package]]
3176+name = "password-hash"
3177+version = "0.6.1"
3178+source = "registry+https://github.com/rust-lang/crates.io-index"
3179+checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b"
3180+dependencies = [
3181+ "phc",
3182+]
3183+
3184+[[package]]
3185+name = "pbkdf2"
3186+version = "0.13.0"
3187+source = "registry+https://github.com/rust-lang/crates.io-index"
3188+checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
3189+dependencies = [
3190+ "digest 0.11.3",
3191+ "hmac 0.13.0",
3192+]
3193+
3194+[[package]]
25443195 name = "pear"
25453196 version = "0.2.9"
25463197 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2573,6 +3224,15 @@ dependencies = [
25733224 ]
25743225
25753226 [[package]]
3227+name = "pem-rfc7468"
3228+version = "1.0.0"
3229+source = "registry+https://github.com/rust-lang/crates.io-index"
3230+checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
3231+dependencies = [
3232+ "base64ct",
3233+]
3234+
3235+[[package]]
25763236 name = "percent-encoding"
25773237 version = "2.3.2"
25783238 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2584,15 +3244,15 @@ version = "0.20.0"
25843244 source = "registry+https://github.com/rust-lang/crates.io-index"
25853245 checksum = "1cfa4743b28656065ff4c0ba09e46b357a65e8c00fc2341e89084b82f87cbdf1"
25863246 dependencies = [
2587- "aead",
2588- "aes",
2589- "aes-gcm",
3247+ "aead 0.5.2",
3248+ "aes 0.8.4",
3249+ "aes-gcm 0.10.3",
25903250 "aes-kw",
2591- "argon2",
3251+ "argon2 0.5.3",
25923252 "base64",
25933253 "bitfields",
2594- "block-padding",
2595- "blowfish",
3254+ "block-padding 0.3.3",
3255+ "blowfish 0.9.1",
25963256 "buffer-redux",
25973257 "byteorder",
25983258 "bytes",
@@ -2600,22 +3260,22 @@ dependencies = [
26003260 "camellia",
26013261 "cast5",
26023262 "cfb-mode",
2603- "cipher",
3263+ "cipher 0.4.4",
26043264 "const-oid 0.9.6",
26053265 "crc24",
2606- "curve25519-dalek",
3266+ "curve25519-dalek 4.1.3",
26073267 "cx448",
26083268 "derive_builder",
26093269 "derive_more",
2610- "des",
3270+ "des 0.8.1",
26113271 "digest 0.10.7",
26123272 "dsa",
26133273 "eax",
2614- "ecdsa",
2615- "ed25519-dalek",
2616- "elliptic-curve",
3274+ "ecdsa 0.16.9",
3275+ "ed25519-dalek 2.2.0",
3276+ "elliptic-curve 0.13.8",
26173277 "flate2",
2618- "generic-array",
3278+ "generic-array 0.14.9",
26193279 "hex",
26203280 "hkdf 0.12.4",
26213281 "idea",
@@ -2628,18 +3288,18 @@ dependencies = [
26283288 "num-traits",
26293289 "num_enum",
26303290 "ocb3",
2631- "p256",
2632- "p384",
2633- "p521",
3291+ "p256 0.13.2",
3292+ "p384 0.13.1",
3293+ "p521 0.13.3",
26343294 "rand 0.8.7",
26353295 "replace_with",
26363296 "ripemd",
2637- "rsa",
3297+ "rsa 0.9.10",
26383298 "sha1 0.10.7",
26393299 "sha1-checked",
26403300 "sha2 0.10.9",
2641- "sha3",
2642- "signature",
3301+ "sha3 0.10.9",
3302+ "signature 2.2.0",
26433303 "smallvec",
26443304 "snafu",
26453305 "subtle",
@@ -2649,6 +3309,16 @@ dependencies = [
26493309 ]
26503310
26513311 [[package]]
3312+name = "phc"
3313+version = "0.6.1"
3314+source = "registry+https://github.com/rust-lang/crates.io-index"
3315+checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892"
3316+dependencies = [
3317+ "base64ct",
3318+ "ctutils",
3319+]
3320+
3321+[[package]]
26523322 name = "phf"
26533323 version = "0.13.1"
26543324 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2699,9 +3369,36 @@ version = "0.7.5"
26993369 source = "registry+https://github.com/rust-lang/crates.io-index"
27003370 checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
27013371 dependencies = [
2702- "der",
2703- "pkcs8",
2704- "spki",
3372+ "der 0.7.10",
3373+ "pkcs8 0.10.2",
3374+ "spki 0.7.3",
3375+]
3376+
3377+[[package]]
3378+name = "pkcs1"
3379+version = "0.8.0-rc.4"
3380+source = "registry+https://github.com/rust-lang/crates.io-index"
3381+checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e"
3382+dependencies = [
3383+ "der 0.8.1",
3384+ "spki 0.8.0",
3385+]
3386+
3387+[[package]]
3388+name = "pkcs5"
3389+version = "0.8.1"
3390+source = "registry+https://github.com/rust-lang/crates.io-index"
3391+checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca"
3392+dependencies = [
3393+ "aes 0.9.1",
3394+ "aes-gcm 0.11.0",
3395+ "cbc",
3396+ "der 0.8.1",
3397+ "pbkdf2",
3398+ "rand_core 0.10.1",
3399+ "scrypt",
3400+ "sha2 0.11.0",
3401+ "spki 0.8.0",
27053402 ]
27063403
27073404 [[package]]
@@ -2710,8 +3407,20 @@ version = "0.10.2"
27103407 source = "registry+https://github.com/rust-lang/crates.io-index"
27113408 checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
27123409 dependencies = [
2713- "der",
2714- "spki",
3410+ "der 0.7.10",
3411+ "spki 0.7.3",
3412+]
3413+
3414+[[package]]
3415+name = "pkcs8"
3416+version = "0.11.0"
3417+source = "registry+https://github.com/rust-lang/crates.io-index"
3418+checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
3419+dependencies = [
3420+ "der 0.8.1",
3421+ "pkcs5",
3422+ "rand_core 0.10.1",
3423+ "spki 0.8.0",
27153424 ]
27163425
27173426 [[package]]
@@ -2721,6 +3430,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
27213430 checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
27223431
27233432 [[package]]
3433+name = "poly1305"
3434+version = "0.9.1"
3435+source = "registry+https://github.com/rust-lang/crates.io-index"
3436+checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c"
3437+dependencies = [
3438+ "cpufeatures 0.3.0",
3439+ "universal-hash 0.6.1",
3440+ "zeroize",
3441+]
3442+
3443+[[package]]
27243444 name = "polyval"
27253445 version = "0.6.2"
27263446 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2729,7 +3449,18 @@ dependencies = [
27293449 "cfg-if",
27303450 "cpufeatures 0.2.17",
27313451 "opaque-debug",
2732- "universal-hash",
3452+ "universal-hash 0.5.1",
3453+]
3454+
3455+[[package]]
3456+name = "polyval"
3457+version = "0.7.3"
3458+source = "registry+https://github.com/rust-lang/crates.io-index"
3459+checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd"
3460+dependencies = [
3461+ "cpubits",
3462+ "cpufeatures 0.3.0",
3463+ "universal-hash 0.6.1",
27333464 ]
27343465
27353466 [[package]]
@@ -2769,12 +3500,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
27693500 checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
27703501
27713502 [[package]]
3503+name = "primefield"
3504+version = "0.14.0"
3505+source = "registry+https://github.com/rust-lang/crates.io-index"
3506+checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4"
3507+dependencies = [
3508+ "crypto-bigint 0.7.5",
3509+ "crypto-common 0.2.2",
3510+ "ff 0.14.0",
3511+ "rand_core 0.10.1",
3512+ "subtle",
3513+ "zeroize",
3514+]
3515+
3516+[[package]]
27723517 name = "primeorder"
27733518 version = "0.13.6"
27743519 source = "registry+https://github.com/rust-lang/crates.io-index"
27753520 checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
27763521 dependencies = [
2777- "elliptic-curve",
3522+ "elliptic-curve 0.13.8",
3523+]
3524+
3525+[[package]]
3526+name = "primeorder"
3527+version = "0.14.0"
3528+source = "registry+https://github.com/rust-lang/crates.io-index"
3529+checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06"
3530+dependencies = [
3531+ "elliptic-curve 0.14.1",
3532+ "once_cell",
3533+ "primefield",
3534+ "serdect 0.4.3",
3535+ "wnaf",
27783536 ]
27793537
27803538 [[package]]
@@ -2963,6 +3721,16 @@ dependencies = [
29633721 ]
29643722
29653723 [[package]]
3724+name = "rfc6979"
3725+version = "0.6.0"
3726+source = "registry+https://github.com/rust-lang/crates.io-index"
3727+checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b"
3728+dependencies = [
3729+ "crypto-bigint 0.7.5",
3730+ "hmac 0.13.0",
3731+]
3732+
3733+[[package]]
29663734 name = "ripemd"
29673735 version = "0.1.3"
29683736 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2975,42 +3743,157 @@ dependencies = [
29753743 name = "rpassword"
29763744 version = "7.5.4"
29773745 source = "registry+https://github.com/rust-lang/crates.io-index"
2978-checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
3746+checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
3747+dependencies = [
3748+ "libc",
3749+ "rtoolbox",
3750+ "windows-sys 0.61.2",
3751+]
3752+
3753+[[package]]
3754+name = "rsa"
3755+version = "0.9.10"
3756+source = "registry+https://github.com/rust-lang/crates.io-index"
3757+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
3758+dependencies = [
3759+ "const-oid 0.9.6",
3760+ "digest 0.10.7",
3761+ "num-bigint-dig",
3762+ "num-integer",
3763+ "num-traits",
3764+ "pkcs1 0.7.5",
3765+ "pkcs8 0.10.2",
3766+ "rand_core 0.6.4",
3767+ "sha2 0.10.9",
3768+ "signature 2.2.0",
3769+ "spki 0.7.3",
3770+ "subtle",
3771+ "zeroize",
3772+]
3773+
3774+[[package]]
3775+name = "rsa"
3776+version = "0.10.0-rc.18"
3777+source = "registry+https://github.com/rust-lang/crates.io-index"
3778+checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf"
3779+dependencies = [
3780+ "const-oid 0.10.2",
3781+ "crypto-bigint 0.7.5",
3782+ "crypto-primes",
3783+ "digest 0.11.3",
3784+ "pkcs1 0.8.0-rc.4",
3785+ "pkcs8 0.11.0",
3786+ "rand_core 0.10.1",
3787+ "sha2 0.11.0",
3788+ "signature 3.0.0",
3789+ "spki 0.8.0",
3790+ "zeroize",
3791+]
3792+
3793+[[package]]
3794+name = "rtoolbox"
3795+version = "0.0.5"
3796+source = "registry+https://github.com/rust-lang/crates.io-index"
3797+checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
29793798 dependencies = [
29803799 "libc",
2981- "rtoolbox",
2982- "windows-sys 0.61.2",
3800+ "windows-sys 0.59.0",
29833801 ]
29843802
29853803 [[package]]
2986-name = "rsa"
2987-version = "0.9.10"
3804+name = "russh"
3805+version = "0.62.4"
29883806 source = "registry+https://github.com/rust-lang/crates.io-index"
2989-checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
3807+checksum = "b8b67b5a0d8068c89dcbe9d95df986af7a851d1f3c604525274c37468e60464f"
29903808 dependencies = [
2991- "const-oid 0.9.6",
2992- "digest 0.10.7",
2993- "num-bigint-dig",
2994- "num-integer",
2995- "num-traits",
2996- "pkcs1",
2997- "pkcs8",
2998- "rand_core 0.6.4",
2999- "sha2 0.10.9",
3000- "signature",
3001- "spki",
3809+ "aes 0.9.1",
3810+ "aws-lc-rs",
3811+ "bitflags",
3812+ "block-padding 0.4.2",
3813+ "byteorder",
3814+ "bytes",
3815+ "cbc",
3816+ "cipher 0.5.2",
3817+ "crypto-bigint 0.7.5",
3818+ "ctr 0.10.1",
3819+ "curve25519-dalek 5.0.0",
3820+ "data-encoding",
3821+ "delegate",
3822+ "der 0.8.1",
3823+ "digest 0.11.3",
3824+ "ecdsa 0.17.0",
3825+ "ed25519-dalek 3.0.0",
3826+ "elliptic-curve 0.14.1",
3827+ "enum_dispatch",
3828+ "flate2",
3829+ "futures",
3830+ "generic-array 1.4.4",
3831+ "getrandom 0.4.3",
3832+ "ghash 0.6.0",
3833+ "hex-literal",
3834+ "hmac 0.13.0",
3835+ "inout 0.2.2",
3836+ "internal-russh-num-bigint",
3837+ "keccak 0.2.0",
3838+ "log",
3839+ "md5",
3840+ "ml-kem",
3841+ "module-lattice",
3842+ "num-bigint",
3843+ "p256 0.14.0",
3844+ "p384 0.14.0",
3845+ "p521 0.14.0",
3846+ "pageant",
3847+ "pbkdf2",
3848+ "pkcs1 0.8.0-rc.4",
3849+ "pkcs5",
3850+ "pkcs8 0.11.0",
3851+ "polyval 0.7.3",
3852+ "rand 0.10.2",
3853+ "rand_core 0.10.1",
3854+ "rsa 0.10.0-rc.18",
3855+ "russh-cryptovec",
3856+ "russh-util",
3857+ "salsa20",
3858+ "scrypt",
3859+ "sec1 0.8.1",
3860+ "sha1 0.11.0",
3861+ "sha2 0.11.0",
3862+ "sha3 0.12.0",
3863+ "signature 3.0.0",
3864+ "spki 0.8.0",
3865+ "ssh-encoding 0.3.0",
3866+ "ssh-key 0.7.0-rc.11",
30023867 "subtle",
3868+ "thiserror 2.0.19",
3869+ "tokio",
3870+ "typenum",
3871+ "universal-hash 0.6.1",
30033872 "zeroize",
30043873 ]
30053874
30063875 [[package]]
3007-name = "rtoolbox"
3008-version = "0.0.5"
3876+name = "russh-cryptovec"
3877+version = "0.62.0"
30093878 source = "registry+https://github.com/rust-lang/crates.io-index"
3010-checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
3879+checksum = "3aec6cb630dbe85d72ffd7bcd95f07e1bd69f9f270ee8adfa1afe443a6331438"
30113880 dependencies = [
3012- "libc",
3013- "windows-sys 0.59.0",
3881+ "log",
3882+ "nix",
3883+ "ssh-encoding 0.3.0",
3884+ "windows-sys 0.61.2",
3885+]
3886+
3887+[[package]]
3888+name = "russh-util"
3889+version = "0.52.0"
3890+source = "registry+https://github.com/rust-lang/crates.io-index"
3891+checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6"
3892+dependencies = [
3893+ "chrono",
3894+ "tokio",
3895+ "wasm-bindgen",
3896+ "wasm-bindgen-futures",
30143897 ]
30153898
30163899 [[package]]
@@ -3089,6 +3972,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
30893972 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
30903973
30913974 [[package]]
3975+name = "salsa20"
3976+version = "0.11.0"
3977+source = "registry+https://github.com/rust-lang/crates.io-index"
3978+checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c"
3979+dependencies = [
3980+ "cfg-if",
3981+ "cipher 0.5.2",
3982+]
3983+
3984+[[package]]
30923985 name = "same-file"
30933986 version = "1.0.6"
30943987 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3113,21 +4006,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
31134006 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
31144007
31154008 [[package]]
4009+name = "scrypt"
4010+version = "0.12.0"
4011+source = "registry+https://github.com/rust-lang/crates.io-index"
4012+checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0"
4013+dependencies = [
4014+ "cfg-if",
4015+ "pbkdf2",
4016+ "salsa20",
4017+ "sha2 0.11.0",
4018+]
4019+
4020+[[package]]
31164021 name = "sec1"
31174022 version = "0.7.3"
31184023 source = "registry+https://github.com/rust-lang/crates.io-index"
31194024 checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
31204025 dependencies = [
3121- "base16ct",
3122- "der",
3123- "generic-array",
3124- "pkcs8",
4026+ "base16ct 0.2.0",
4027+ "der 0.7.10",
4028+ "generic-array 0.14.9",
4029+ "pkcs8 0.10.2",
31254030 "serdect 0.2.0",
31264031 "subtle",
31274032 "zeroize",
31284033 ]
31294034
31304035 [[package]]
4036+name = "sec1"
4037+version = "0.8.1"
4038+source = "registry+https://github.com/rust-lang/crates.io-index"
4039+checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d"
4040+dependencies = [
4041+ "base16ct 1.0.0",
4042+ "ctutils",
4043+ "der 0.8.1",
4044+ "hybrid-array",
4045+ "subtle",
4046+ "zeroize",
4047+]
4048+
4049+[[package]]
31314050 name = "security-framework"
31324051 version = "3.7.0"
31334052 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3237,7 +4156,7 @@ version = "0.2.0"
32374156 source = "registry+https://github.com/rust-lang/crates.io-index"
32384157 checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
32394158 dependencies = [
3240- "base16ct",
4159+ "base16ct 0.2.0",
32414160 "serde",
32424161 ]
32434162
@@ -3247,7 +4166,17 @@ version = "0.3.0"
32474166 source = "registry+https://github.com/rust-lang/crates.io-index"
32484167 checksum = "f42f67da2385b51a5f9652db9c93d78aeaf7610bf5ec366080b6de810604af53"
32494168 dependencies = [
3250- "base16ct",
4169+ "base16ct 0.2.0",
4170+ "serde",
4171+]
4172+
4173+[[package]]
4174+name = "serdect"
4175+version = "0.4.3"
4176+source = "registry+https://github.com/rust-lang/crates.io-index"
4177+checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e"
4178+dependencies = [
4179+ "base16ct 1.0.0",
32514180 "serde",
32524181 ]
32534182
@@ -3313,7 +4242,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
33134242 checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874"
33144243 dependencies = [
33154244 "digest 0.10.7",
3316- "keccak",
4245+ "keccak 0.1.6",
4246+]
4247+
4248+[[package]]
4249+name = "sha3"
4250+version = "0.11.0"
4251+source = "registry+https://github.com/rust-lang/crates.io-index"
4252+checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1"
4253+dependencies = [
4254+ "digest 0.11.3",
4255+ "keccak 0.2.0",
4256+]
4257+
4258+[[package]]
4259+name = "sha3"
4260+version = "0.12.0"
4261+source = "registry+https://github.com/rust-lang/crates.io-index"
4262+checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759"
4263+dependencies = [
4264+ "digest 0.11.3",
4265+ "keccak 0.2.0",
4266+ "sponge-cursor",
33174267 ]
33184268
33194269 [[package]]
@@ -3352,6 +4302,16 @@ dependencies = [
33524302 ]
33534303
33544304 [[package]]
4305+name = "signature"
4306+version = "3.0.0"
4307+source = "registry+https://github.com/rust-lang/crates.io-index"
4308+checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5"
4309+dependencies = [
4310+ "digest 0.11.3",
4311+ "rand_core 0.10.1",
4312+]
4313+
4314+[[package]]
33554315 name = "simd-adler32"
33564316 version = "0.3.10"
33574317 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3431,10 +4391,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
34314391 checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
34324392 dependencies = [
34334393 "base64ct",
3434- "der",
4394+ "der 0.7.10",
34354395 ]
34364396
34374397 [[package]]
4398+name = "spki"
4399+version = "0.8.0"
4400+source = "registry+https://github.com/rust-lang/crates.io-index"
4401+checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
4402+dependencies = [
4403+ "base64ct",
4404+ "der 0.8.1",
4405+]
4406+
4407+[[package]]
4408+name = "sponge-cursor"
4409+version = "0.1.0"
4410+source = "registry+https://github.com/rust-lang/crates.io-index"
4411+checksum = "3a0219bd7d979d58245a4f41f695e1ac9f8befdffadd7f61f1bae9e39abc6620"
4412+
4413+[[package]]
34384414 name = "sqlx"
34394415 version = "0.9.0"
34404416 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3534,7 +4510,7 @@ dependencies = [
35344510 "either",
35354511 "futures-core",
35364512 "futures-util",
3537- "generic-array",
4513+ "generic-array 0.14.9",
35384514 "log",
35394515 "percent-encoding",
35404516 "serde",
@@ -3607,6 +4583,20 @@ dependencies = [
36074583 [[package]]
36084584 name = "ssh"
36094585 version = "0.1.0"
4586+dependencies = [
4587+ "auth",
4588+ "config",
4589+ "git",
4590+ "model",
4591+ "rand_core 0.6.4",
4592+ "russh",
4593+ "ssh-key 0.6.7",
4594+ "store",
4595+ "tempfile",
4596+ "thiserror 2.0.19",
4597+ "tokio",
4598+ "tracing",
4599+]
36104600
36114601 [[package]]
36124602 name = "ssh-cipher"
@@ -3614,8 +4604,26 @@ version = "0.2.0"
36144604 source = "registry+https://github.com/rust-lang/crates.io-index"
36154605 checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
36164606 dependencies = [
3617- "cipher",
3618- "ssh-encoding",
4607+ "cipher 0.4.4",
4608+ "ssh-encoding 0.2.0",
4609+]
4610+
4611+[[package]]
4612+name = "ssh-cipher"
4613+version = "0.3.0"
4614+source = "registry+https://github.com/rust-lang/crates.io-index"
4615+checksum = "d801accda99469cde6d73da741422610fdf6508a72d9a69d1b55cb241c720597"
4616+dependencies = [
4617+ "aead 0.6.1",
4618+ "aes 0.9.1",
4619+ "aes-gcm 0.11.0",
4620+ "chacha20",
4621+ "cipher 0.5.2",
4622+ "ctutils",
4623+ "des 0.9.0",
4624+ "poly1305",
4625+ "ssh-encoding 0.3.0",
4626+ "zeroize",
36194627 ]
36204628
36214629 [[package]]
@@ -3625,33 +4633,74 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
36254633 checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
36264634 dependencies = [
36274635 "base64ct",
3628- "pem-rfc7468",
4636+ "pem-rfc7468 0.7.0",
36294637 "sha2 0.10.9",
36304638 ]
36314639
36324640 [[package]]
4641+name = "ssh-encoding"
4642+version = "0.3.0"
4643+source = "registry+https://github.com/rust-lang/crates.io-index"
4644+checksum = "7b54d0ed0498daf3f78d82e00e28c8eec9d75a067c4cfbcc7a0f7d0f4077749e"
4645+dependencies = [
4646+ "base64ct",
4647+ "bytes",
4648+ "crypto-bigint 0.7.5",
4649+ "ctutils",
4650+ "digest 0.11.3",
4651+ "pem-rfc7468 1.0.0",
4652+ "zeroize",
4653+]
4654+
4655+[[package]]
36334656 name = "ssh-key"
36344657 version = "0.6.7"
36354658 source = "registry+https://github.com/rust-lang/crates.io-index"
36364659 checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
36374660 dependencies = [
3638- "ed25519-dalek",
4661+ "ed25519-dalek 2.2.0",
36394662 "num-bigint-dig",
3640- "p256",
3641- "p384",
3642- "p521",
4663+ "p256 0.13.2",
4664+ "p384 0.13.1",
4665+ "p521 0.13.3",
36434666 "rand_core 0.6.4",
3644- "rsa",
3645- "sec1",
4667+ "rsa 0.9.10",
4668+ "sec1 0.7.3",
36464669 "sha2 0.10.9",
3647- "signature",
3648- "ssh-cipher",
3649- "ssh-encoding",
4670+ "signature 2.2.0",
4671+ "ssh-cipher 0.2.0",
4672+ "ssh-encoding 0.2.0",
36504673 "subtle",
36514674 "zeroize",
36524675 ]
36534676
36544677 [[package]]
4678+name = "ssh-key"
4679+version = "0.7.0-rc.11"
4680+source = "registry+https://github.com/rust-lang/crates.io-index"
4681+checksum = "f9a32fae177b74a22aa9c5b01bf7e68b33545be32d9e381e248058d2adc15ce3"
4682+dependencies = [
4683+ "argon2 0.6.0-rc.8",
4684+ "bcrypt-pbkdf",
4685+ "ctutils",
4686+ "ed25519-dalek 3.0.0",
4687+ "hex",
4688+ "hmac 0.13.0",
4689+ "p256 0.14.0",
4690+ "p384 0.14.0",
4691+ "p521 0.14.0",
4692+ "rand_core 0.10.1",
4693+ "rsa 0.10.0-rc.18",
4694+ "sec1 0.8.1",
4695+ "sha1 0.11.0",
4696+ "sha2 0.11.0",
4697+ "signature 3.0.0",
4698+ "ssh-cipher 0.3.0",
4699+ "ssh-encoding 0.3.0",
4700+ "zeroize",
4701+]
4702+
4703+[[package]]
36554704 name = "stable_deref_trait"
36564705 version = "1.2.1"
36574706 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4156,7 +5205,7 @@ version = "0.7.1"
41565205 source = "registry+https://github.com/rust-lang/crates.io-index"
41575206 checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013"
41585207 dependencies = [
4159- "cipher",
5208+ "cipher 0.4.4",
41605209 ]
41615210
41625211 [[package]]
@@ -4246,6 +5295,22 @@ dependencies = [
42465295 ]
42475296
42485297 [[package]]
5298+name = "universal-hash"
5299+version = "0.6.1"
5300+source = "registry+https://github.com/rust-lang/crates.io-index"
5301+checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96"
5302+dependencies = [
5303+ "crypto-common 0.2.2",
5304+ "ctutils",
5305+]
5306+
5307+[[package]]
5308+name = "untrusted"
5309+version = "0.7.1"
5310+source = "registry+https://github.com/rust-lang/crates.io-index"
5311+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
5312+
5313+[[package]]
42495314 name = "url"
42505315 version = "2.5.8"
42515316 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4343,6 +5408,16 @@ dependencies = [
43435408 ]
43445409
43455410 [[package]]
5411+name = "wasm-bindgen-futures"
5412+version = "0.4.76"
5413+source = "registry+https://github.com/rust-lang/crates.io-index"
5414+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
5415+dependencies = [
5416+ "js-sys",
5417+ "wasm-bindgen",
5418+]
5419+
5420+[[package]]
43465421 name = "wasm-bindgen-macro"
43475422 version = "0.2.126"
43485423 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4444,12 +5519,107 @@ dependencies = [
44445519 ]
44455520
44465521 [[package]]
5522+name = "windows"
5523+version = "0.62.2"
5524+source = "registry+https://github.com/rust-lang/crates.io-index"
5525+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
5526+dependencies = [
5527+ "windows-collections",
5528+ "windows-core",
5529+ "windows-future",
5530+ "windows-numerics",
5531+]
5532+
5533+[[package]]
5534+name = "windows-collections"
5535+version = "0.3.2"
5536+source = "registry+https://github.com/rust-lang/crates.io-index"
5537+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
5538+dependencies = [
5539+ "windows-core",
5540+]
5541+
5542+[[package]]
5543+name = "windows-core"
5544+version = "0.62.2"
5545+source = "registry+https://github.com/rust-lang/crates.io-index"
5546+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
5547+dependencies = [
5548+ "windows-implement",
5549+ "windows-interface",
5550+ "windows-link",
5551+ "windows-result",
5552+ "windows-strings",
5553+]
5554+
5555+[[package]]
5556+name = "windows-future"
5557+version = "0.3.2"
5558+source = "registry+https://github.com/rust-lang/crates.io-index"
5559+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
5560+dependencies = [
5561+ "windows-core",
5562+ "windows-link",
5563+ "windows-threading",
5564+]
5565+
5566+[[package]]
5567+name = "windows-implement"
5568+version = "0.60.2"
5569+source = "registry+https://github.com/rust-lang/crates.io-index"
5570+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
5571+dependencies = [
5572+ "proc-macro2",
5573+ "quote",
5574+ "syn 2.0.119",
5575+]
5576+
5577+[[package]]
5578+name = "windows-interface"
5579+version = "0.59.3"
5580+source = "registry+https://github.com/rust-lang/crates.io-index"
5581+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
5582+dependencies = [
5583+ "proc-macro2",
5584+ "quote",
5585+ "syn 2.0.119",
5586+]
5587+
5588+[[package]]
44475589 name = "windows-link"
44485590 version = "0.2.1"
44495591 source = "registry+https://github.com/rust-lang/crates.io-index"
44505592 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
44515593
44525594 [[package]]
5595+name = "windows-numerics"
5596+version = "0.3.1"
5597+source = "registry+https://github.com/rust-lang/crates.io-index"
5598+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
5599+dependencies = [
5600+ "windows-core",
5601+ "windows-link",
5602+]
5603+
5604+[[package]]
5605+name = "windows-result"
5606+version = "0.4.1"
5607+source = "registry+https://github.com/rust-lang/crates.io-index"
5608+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
5609+dependencies = [
5610+ "windows-link",
5611+]
5612+
5613+[[package]]
5614+name = "windows-strings"
5615+version = "0.5.1"
5616+source = "registry+https://github.com/rust-lang/crates.io-index"
5617+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
5618+dependencies = [
5619+ "windows-link",
5620+]
5621+
5622+[[package]]
44535623 name = "windows-sys"
44545624 version = "0.59.0"
44555625 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4484,6 +5654,15 @@ dependencies = [
44845654 ]
44855655
44865656 [[package]]
5657+name = "windows-threading"
5658+version = "0.2.1"
5659+source = "registry+https://github.com/rust-lang/crates.io-index"
5660+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
5661+dependencies = [
5662+ "windows-link",
5663+]
5664+
5665+[[package]]
44875666 name = "windows_aarch64_gnullvm"
44885667 version = "0.52.6"
44895668 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4547,6 +5726,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
45475726 checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
45485727
45495728 [[package]]
5729+name = "wnaf"
5730+version = "0.14.0"
5731+source = "registry+https://github.com/rust-lang/crates.io-index"
5732+checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1"
5733+dependencies = [
5734+ "ff 0.14.0",
5735+ "group 0.14.0",
5736+ "hybrid-array",
5737+]
5738+
5739+[[package]]
45505740 name = "writeable"
45515741 version = "0.6.3"
45525742 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4567,7 +5757,7 @@ version = "2.0.1"
45675757 source = "registry+https://github.com/rust-lang/crates.io-index"
45685758 checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
45695759 dependencies = [
4570- "curve25519-dalek",
5760+ "curve25519-dalek 4.1.3",
45715761 "rand_core 0.6.4",
45725762 "serde",
45735763 "zeroize",
Cargo.toml +3 −0
@@ -18,6 +18,7 @@ path = "src/main.rs"
1818 # `serve` back to us so that no library crate has to depend on `web`.
1919 cli = { path = "crates/cli" }
2020 web = { path = "crates/web" }
21+ssh = { path = "crates/ssh" }
2122 config = { path = "crates/config" }
2223 store = { path = "crates/store" }
2324 anyhow = { workspace = true }
@@ -150,6 +151,8 @@ ammonia = "4"
150151 # gzip-encoded upload-pack requests.
151152 tokio-util = { version = "0.7", features = ["io"] }
152153 flate2 = "1"
154+# SSH server (default features: aws-lc-rs backend, no ring; re-exports ssh-key).
155+russh = "0.62"
153156 tracing = "0.1"
154157 tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] }
155158
crates/cli/src/hook_cmd.rs +4 −4
@@ -44,10 +44,10 @@ fn post_receive() -> anyhow::Result<()> {
4444 let mut input = String::new();
4545 std::io::stdin().read_to_string(&mut input)?;
4646
47- let git_dir = std::env::var_os("GIT_DIR")
48- .map(PathBuf::from)
49- .or_else(|| std::env::current_dir().ok())
50- .ok_or_else(|| anyhow::anyhow!("cannot determine GIT_DIR"))?;
47+ // git runs post-receive with cwd = the repo dir and often GIT_DIR=".";
48+ // canonicalize so we get the absolute `{id}.git` directory either way.
49+ let git_dir = std::env::var_os("GIT_DIR").map_or_else(|| PathBuf::from("."), PathBuf::from);
50+ let git_dir = std::fs::canonicalize(&git_dir).unwrap_or(git_dir);
5151
5252 // The bare repo dir is `{id}.git`; the stem is the repo's ULID.
5353 let Some(repo_id) = git_dir
crates/ssh/Cargo.toml +16 −0
@@ -10,6 +10,22 @@ repository.workspace = true
1010 publish.workspace = true
1111
1212 [dependencies]
13+config = { workspace = true }
14+store = { workspace = true }
15+git = { workspace = true }
16+auth = { workspace = true }
17+model = { workspace = true }
18+ssh-key = { workspace = true }
19+rand_core = { workspace = true }
20+# SSH server. Default features use the aws-lc-rs crypto backend (no `ring`); it
21+# re-exports ssh-key as russh::keys. See docs/decisions.md for the license note.
22+russh = { workspace = true }
23+tokio = { workspace = true }
24+tracing = { workspace = true }
25+thiserror = { workspace = true }
26+
27+[dev-dependencies]
28+tempfile = { workspace = true }
1329
1430 [lints]
1531 workspace = true
crates/ssh/src/command.rs +114 −0
@@ -0,0 +1,114 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Parsing the SSH `exec` command line.
6+//!
7+//! Only the three git pack commands are accepted, each with exactly one quoted
8+//! path argument. Anything else — including an interactive shell — is refused with
9+//! a friendly banner by the caller.
10+
11+use git::pack::Service;
12+
13+/// A parsed, accepted git command: the service and its (raw, un-normalized) path.
14+#[derive(Debug, Clone, PartialEq, Eq)]
15+pub struct GitCommand {
16+ /// The git service requested.
17+ pub service: Service,
18+ /// The repository path argument, as given (still needs normalizing).
19+ pub path: String,
20+}
21+
22+/// Parse an exec command line into an accepted [`GitCommand`], or `None` if it is
23+/// not one of the three git pack commands with a single path argument.
24+#[must_use]
25+pub fn parse(command: &str) -> Option<GitCommand> {
26+ let words = shell_split(command)?;
27+ let (name, rest) = words.split_first()?;
28+ if rest.len() != 1 {
29+ return None;
30+ }
31+ let service = match name.as_str() {
32+ "git-upload-pack" => Service::UploadPack,
33+ "git-receive-pack" => Service::ReceivePack,
34+ "git-upload-archive" => Service::UploadArchive,
35+ _ => return None,
36+ };
37+ Some(GitCommand {
38+ service,
39+ path: rest[0].clone(),
40+ })
41+}
42+
43+/// Split a command line into words, honouring single and double quotes (git sends
44+/// the path single-quoted). Returns `None` on an unterminated quote.
45+fn shell_split(input: &str) -> Option<Vec<String>> {
46+ let mut words = Vec::new();
47+ let mut current = String::new();
48+ let mut has_word = false;
49+ let mut chars = input.chars().peekable();
50+
51+ while let Some(c) = chars.next() {
52+ match c {
53+ ' ' | '\t' => {
54+ if has_word {
55+ words.push(std::mem::take(&mut current));
56+ has_word = false;
57+ }
58+ }
59+ '\'' | '"' => {
60+ has_word = true;
61+ let quote = c;
62+ loop {
63+ match chars.next() {
64+ Some(q) if q == quote => break,
65+ Some(other) => current.push(other),
66+ None => return None, // unterminated quote
67+ }
68+ }
69+ }
70+ other => {
71+ has_word = true;
72+ current.push(other);
73+ }
74+ }
75+ }
76+ if has_word {
77+ words.push(current);
78+ }
79+ Some(words)
80+}
81+
82+#[cfg(test)]
83+mod tests {
84+ use super::*;
85+
86+ #[test]
87+ fn parses_the_three_git_commands() {
88+ assert_eq!(
89+ parse("git-upload-pack 'owner/repo.git'"),
90+ Some(GitCommand {
91+ service: Service::UploadPack,
92+ path: "owner/repo.git".to_string()
93+ })
94+ );
95+ assert_eq!(
96+ parse("git-receive-pack '/owner/repo'").map(|c| c.service),
97+ Some(Service::ReceivePack)
98+ );
99+ assert_eq!(
100+ parse("git-upload-archive 'x'").map(|c| c.service),
101+ Some(Service::UploadArchive)
102+ );
103+ }
104+
105+ #[test]
106+ fn rejects_shells_and_extra_args() {
107+ assert!(parse("bash").is_none());
108+ assert!(parse("").is_none());
109+ assert!(parse("git-upload-pack").is_none());
110+ assert!(parse("git-upload-pack a b").is_none());
111+ assert!(parse("scp -t /tmp").is_none());
112+ assert!(parse("git-upload-pack 'unterminated").is_none());
113+ }
114+}
crates/ssh/src/hostkey.rs +70 −0
@@ -0,0 +1,70 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The server host key: generated on first start, loaded thereafter.
6+//!
7+//! Returned as the OpenSSH-format PEM string rather than a typed key so the caller
8+//! (`server`) parses it with russh's own re-exported `ssh-key` version — the
9+//! workspace pins a different `ssh-key` patch, and the two key types are distinct,
10+//! but the OpenSSH text format bridges them.
11+
12+use std::fs;
13+use std::os::unix::fs::PermissionsExt;
14+use std::path::Path;
15+
16+use rand_core::OsRng;
17+use ssh_key::{Algorithm, LineEnding, PrivateKey};
18+
19+use crate::SshError;
20+
21+/// Load the ed25519 host key from `path` as an OpenSSH PEM, generating and
22+/// persisting one (mode `0600`) if it does not yet exist.
23+///
24+/// # Errors
25+///
26+/// Returns [`SshError`] if the key file cannot be read, generated, or written.
27+pub fn load_or_generate(path: &Path) -> Result<String, SshError> {
28+ if path.exists() {
29+ return Ok(fs::read_to_string(path)?);
30+ }
31+
32+ let key = PrivateKey::random(&mut OsRng, Algorithm::Ed25519)
33+ .map_err(|e| SshError::HostKey(format!("generating host key: {e}")))?;
34+ if let Some(parent) = path.parent() {
35+ fs::create_dir_all(parent)?;
36+ }
37+ let pem = key
38+ .to_openssh(LineEnding::LF)
39+ .map_err(|e| SshError::HostKey(format!("encoding host key: {e}")))?;
40+ fs::write(path, pem.as_bytes())?;
41+ fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
42+ Ok(pem.to_string())
43+}
44+
45+#[cfg(test)]
46+mod tests {
47+ #![allow(clippy::unwrap_used)]
48+
49+ use super::*;
50+
51+ #[test]
52+ fn generates_then_loads_a_stable_host_key() {
53+ let dir = tempfile::tempdir().unwrap();
54+ let path = dir.path().join("ssh").join("host_ed25519");
55+
56+ let first_pem = load_or_generate(&path).unwrap();
57+ let first = PrivateKey::from_openssh(first_pem.as_str()).unwrap();
58+ assert_eq!(first.algorithm(), Algorithm::Ed25519);
59+ // The file is created with owner-only permissions.
60+ let mode = fs::metadata(&path).unwrap().permissions().mode();
61+ assert_eq!(mode & 0o777, 0o600);
62+
63+ // A second load returns the same key (same public fingerprint).
64+ let second = PrivateKey::from_openssh(load_or_generate(&path).unwrap().as_str()).unwrap();
65+ assert_eq!(
66+ first.public_key().fingerprint(ssh_key::HashAlg::Sha256),
67+ second.public_key().fingerprint(ssh_key::HashAlg::Sha256)
68+ );
69+ }
70+}
crates/ssh/src/lib.rs +27 −0
@@ -3,3 +3,30 @@
33 // file, You can obtain one at https://mozilla.org/MPL/2.0/.
44
55 //! The SSH front-end: publickey auth and `exec` dispatch into pack processes.
6+//!
7+//! A `russh` server accepts only `publickey` auth (matching the offered key's
8+//! SHA256 fingerprint against the `keys` table), handles only the `exec` channel
9+//! request, and streams the channel data to and from a spawned `git`
10+//! upload-pack/receive-pack process via [`git::pack`]. Anything else — a shell,
11+//! a PTY, an unrecognized command — gets a friendly banner and a non-zero exit.
12+
13+mod command;
14+mod hostkey;
15+mod resolve;
16+mod server;
17+
18+pub use crate::server::serve;
19+
20+/// An error from the SSH layer.
21+#[derive(Debug, thiserror::Error)]
22+pub enum SshError {
23+ /// A filesystem or socket I/O error.
24+ #[error(transparent)]
25+ Io(#[from] std::io::Error),
26+ /// The host key could not be generated, read, or written.
27+ #[error("host key error: {0}")]
28+ HostKey(String),
29+ /// The russh server failed to start or run.
30+ #[error("ssh server error: {0}")]
31+ Server(String),
32+}
crates/ssh/src/resolve.rs +56 −0
@@ -0,0 +1,56 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! Normalizing and resolving the repository path from an SSH command.
6+
7+/// Normalize an SSH repo path into `(owner, repo_path)`.
8+///
9+/// Strips a leading `/` and a trailing `.git`, and rejects `..`, absolute, or
10+/// empty paths. The remainder is `owner/group…/repo`; the owner is the first
11+/// segment and the repo path is everything after it.
12+#[must_use]
13+pub fn normalize(raw: &str) -> Option<(String, String)> {
14+ let trimmed = raw.trim().trim_matches('\'');
15+ let without_lead = trimmed.strip_prefix('/').unwrap_or(trimmed);
16+ let path = without_lead.strip_suffix(".git").unwrap_or(without_lead);
17+
18+ if path.is_empty() || path.starts_with('/') {
19+ return None;
20+ }
21+ // Reject traversal in any segment.
22+ if path
23+ .split('/')
24+ .any(|seg| seg == ".." || seg == "." || seg.is_empty())
25+ {
26+ return None;
27+ }
28+ let (owner, repo_path) = path.split_once('/')?;
29+ Some((owner.to_string(), repo_path.to_string()))
30+}
31+
32+#[cfg(test)]
33+mod tests {
34+ use super::*;
35+
36+ #[test]
37+ fn strips_lead_and_git_suffix() {
38+ assert_eq!(
39+ normalize("/hanna/backend/api.git"),
40+ Some(("hanna".to_string(), "backend/api".to_string()))
41+ );
42+ assert_eq!(
43+ normalize("hanna/proj"),
44+ Some(("hanna".to_string(), "proj".to_string()))
45+ );
46+ }
47+
48+ #[test]
49+ fn rejects_traversal_and_malformed() {
50+ assert!(normalize("../etc/passwd").is_none());
51+ assert!(normalize("hanna/../secret").is_none());
52+ assert!(normalize("/absolute//path").is_none());
53+ assert!(normalize("").is_none());
54+ assert!(normalize("noowner").is_none()); // no repo path
55+ }
56+}
crates/ssh/src/server.rs +355 −0
@@ -0,0 +1,355 @@
1+// This Source Code Form is subject to the terms of the Mozilla Public
2+// License, v. 2.0. If a copy of the MPL was not distributed with this
3+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+//! The russh server: publickey auth, exec dispatch into pack processes.
6+
7+use std::sync::Arc;
8+use std::time::Duration;
9+
10+use config::Config;
11+use model::KeyKind;
12+use russh::keys::{HashAlg, PublicKey};
13+use russh::server::{Auth, ChannelOpenHandle, Handler, Msg, Server as _, Session};
14+use russh::{Channel, ChannelId, MethodKind, MethodSet};
15+use store::Store;
16+use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
17+use tokio::process::ChildStdin;
18+use tokio::sync::Mutex;
19+
20+use crate::SshError;
21+use crate::{command, resolve};
22+
23+/// State shared across every connection.
24+struct Shared {
25+ store: Store,
26+ config: Arc<Config>,
27+ /// The `--config` path to hand hooks via `FABRICA_CONFIG`.
28+ config_path: Option<String>,
29+}
30+
31+/// The russh `Server` factory.
32+struct GitServer {
33+ shared: Arc<Shared>,
34+}
35+
36+impl russh::server::Server for GitServer {
37+ type Handler = GitHandler;
38+
39+ fn new_client(&mut self, _peer: Option<std::net::SocketAddr>) -> GitHandler {
40+ GitHandler {
41+ shared: Arc::clone(&self.shared),
42+ identity: None,
43+ stdin: Arc::new(Mutex::new(None)),
44+ }
45+ }
46+}
47+
48+/// The authenticated identity, bound after publickey auth.
49+#[derive(Clone)]
50+struct Identity {
51+ user_id: String,
52+ username: String,
53+ is_admin: bool,
54+ key_id: String,
55+}
56+
57+/// One connection's handler.
58+struct GitHandler {
59+ shared: Arc<Shared>,
60+ identity: Option<Identity>,
61+ /// The running pack subprocess's stdin, written to as client data arrives.
62+ stdin: Arc<Mutex<Option<ChildStdin>>>,
63+}
64+
65+impl Handler for GitHandler {
66+ type Error = russh::Error;
67+
68+ async fn auth_publickey(
69+ &mut self,
70+ _user: &str,
71+ public_key: &PublicKey,
72+ ) -> Result<Auth, Self::Error> {
73+ // Identify by key fingerprint, not the SSH username (everyone is `git@`).
74+ let fingerprint = public_key.fingerprint(HashAlg::Sha256).to_string();
75+ let Ok(Some(key)) = self
76+ .shared
77+ .store
78+ .key_by_fingerprint(KeyKind::Ssh, &fingerprint)
79+ .await
80+ else {
81+ return Ok(Auth::reject());
82+ };
83+ let Ok(Some(user)) = self.shared.store.user_by_id(&key.user_id).await else {
84+ return Ok(Auth::reject());
85+ };
86+ if user.disabled_at.is_some() {
87+ return Ok(Auth::reject());
88+ }
89+ let _ = self.shared.store.touch_key_used(&key.id).await;
90+ self.identity = Some(Identity {
91+ user_id: user.id,
92+ username: user.username,
93+ is_admin: user.is_admin,
94+ key_id: key.id,
95+ });
96+ Ok(Auth::Accept)
97+ }
98+
99+ async fn channel_open_session(
100+ &mut self,
101+ _channel: Channel<Msg>,
102+ reply: ChannelOpenHandle,
103+ _session: &mut Session,
104+ ) -> Result<(), Self::Error> {
105+ // Dropping the handle rejects the channel, so accept it explicitly.
106+ reply.accept().await;
107+ Ok(())
108+ }
109+
110+ async fn exec_request(
111+ &mut self,
112+ channel: ChannelId,
113+ data: &[u8],
114+ session: &mut Session,
115+ ) -> Result<(), Self::Error> {
116+ session.channel_success(channel)?;
117+ let command = String::from_utf8_lossy(data).into_owned();
118+ if let Err(banner) = self.spawn_exec(channel, &command, session).await {
119+ let _ = session.extended_data(channel, 1, banner.into_bytes());
120+ let _ = session.exit_status_request(channel, 1);
121+ let _ = session.eof(channel);
122+ let _ = session.close(channel);
123+ }
124+ Ok(())
125+ }
126+
127+ async fn shell_request(
128+ &mut self,
129+ channel: ChannelId,
130+ session: &mut Session,
131+ ) -> Result<(), Self::Error> {
132+ session.channel_success(channel)?;
133+ // Use the async handle so the banner/close actually flush before return.
134+ let handle = session.handle();
135+ let _ = handle
136+ .extended_data(channel, 1, self.no_shell_banner().into_bytes())
137+ .await;
138+ let _ = handle.exit_status_request(channel, 1).await;
139+ let _ = handle.eof(channel).await;
140+ let _ = handle.close(channel).await;
141+ Ok(())
142+ }
143+
144+ async fn data(
145+ &mut self,
146+ _channel: ChannelId,
147+ data: &[u8],
148+ _session: &mut Session,
149+ ) -> Result<(), Self::Error> {
150+ if let Some(stdin) = self.stdin.lock().await.as_mut() {
151+ let _ = stdin.write_all(data).await;
152+ }
153+ Ok(())
154+ }
155+
156+ async fn channel_eof(
157+ &mut self,
158+ _channel: ChannelId,
159+ _session: &mut Session,
160+ ) -> Result<(), Self::Error> {
161+ // Close the child's stdin so upload-pack/receive-pack sees EOF.
162+ *self.stdin.lock().await = None;
163+ Ok(())
164+ }
165+}
166+
167+impl GitHandler {
168+ /// The friendly shell-access banner.
169+ fn no_shell_banner(&self) -> String {
170+ let who = self
171+ .identity
172+ .as_ref()
173+ .map_or("there", |i| i.username.as_str());
174+ format!("Hi {who}! fabrica does not provide shell access.\n")
175+ }
176+
177+ /// Authorize the exec command and, on success, spawn the pack process and start
178+ /// streaming. On failure returns the banner to write to stderr.
179+ async fn spawn_exec(
180+ &mut self,
181+ channel: ChannelId,
182+ command: &str,
183+ session: &mut Session,
184+ ) -> Result<(), String> {
185+ let Some(identity) = self.identity.clone() else {
186+ return Err("Authentication required.".to_string());
187+ };
188+ let parsed = command::parse(command).ok_or_else(|| self.no_shell_banner())?;
189+ let (owner, repo_path) = resolve::normalize(&parsed.path)
190+ .ok_or_else(|| "Invalid repository path.".to_string())?;
191+
192+ let repo = self
193+ .resolve_repo(&owner, &repo_path, &identity, parsed.service)
194+ .await?;
195+
196+ let repo_dir = git::repo_path(&self.shared.config.storage.repo_dir, &repo.id)
197+ .map_err(|_| "Repository is unavailable.".to_string())?;
198+
199+ let env = git::pack::PackEnv {
200+ home: self.shared.config.storage.data_dir.join("tmp"),
201+ config_path: self.shared.config_path.clone(),
202+ user_id: Some(identity.user_id),
203+ user_name: Some(identity.username),
204+ repo_id: Some(repo.id),
205+ key_id: Some(identity.key_id),
206+ protocol: None,
207+ };
208+ let mut child = git::pack::spawn(
209+ &self.shared.config.git.binary,
210+ parsed.service,
211+ &[],
212+ &repo_dir,
213+ &env,
214+ )
215+ .map_err(|e| format!("Failed to start git: {e}"))?;
216+
217+ let (Some(stdin), Some(mut stdout), Some(mut stderr)) =
218+ (child.stdin.take(), child.stdout.take(), child.stderr.take())
219+ else {
220+ return Err("Internal error.".to_string());
221+ };
222+ *self.stdin.lock().await = Some(stdin);
223+
224+ let handle = session.handle();
225+ tokio::spawn(async move {
226+ let mut obuf = vec![0u8; 32 * 1024];
227+ let mut ebuf = vec![0u8; 32 * 1024];
228+ let (mut out_done, mut err_done) = (false, false);
229+ while !(out_done && err_done) {
230+ tokio::select! {
231+ r = stdout.read(&mut obuf), if !out_done => match r {
232+ Ok(0) | Err(_) => out_done = true,
233+ Ok(n) => { let _ = handle.data(channel, obuf[..n].to_vec()).await; }
234+ },
235+ r = stderr.read(&mut ebuf), if !err_done => match r {
236+ Ok(0) | Err(_) => err_done = true,
237+ Ok(n) => { let _ = handle.extended_data(channel, 1, ebuf[..n].to_vec()).await; }
238+ },
239+ }
240+ }
241+ let code = child
242+ .wait()
243+ .await
244+ .ok()
245+ .and_then(|s| s.code())
246+ .and_then(|c| u32::try_from(c).ok())
247+ .unwrap_or(0);
248+ let _ = handle.exit_status_request(channel, code).await;
249+ let _ = handle.eof(channel).await;
250+ let _ = handle.close(channel).await;
251+ });
252+ Ok(())
253+ }
254+
255+ /// Resolve and authorize the repo for the requested service.
256+ async fn resolve_repo(
257+ &self,
258+ owner: &str,
259+ repo_path: &str,
260+ identity: &Identity,
261+ service: git::pack::Service,
262+ ) -> Result<model::Repo, String> {
263+ let not_found = || "Repository not found.".to_string();
264+ let owner_user = self
265+ .shared
266+ .store
267+ .user_by_username(owner)
268+ .await
269+ .ok()
270+ .flatten()
271+ .ok_or_else(not_found)?;
272+ let repo = self
273+ .shared
274+ .store
275+ .repo_by_owner_path(&owner_user.id, repo_path)
276+ .await
277+ .ok()
278+ .flatten()
279+ .ok_or_else(not_found)?;
280+
281+ let collaborator = self
282+ .shared
283+ .store
284+ .collaborator_permission(&repo.id, &identity.user_id)
285+ .await
286+ .ok()
287+ .flatten()
288+ .and_then(|p| auth::Permission::parse(&p));
289+ let viewer = auth::Viewer {
290+ id: identity.user_id.clone(),
291+ is_admin: identity.is_admin,
292+ };
293+ let access = auth::access(
294+ Some(&viewer),
295+ &repo,
296+ collaborator,
297+ self.shared.config.instance.allow_anonymous,
298+ );
299+
300+ let required = match service {
301+ git::pack::Service::ReceivePack => auth::Access::Write,
302+ _ => auth::Access::Read,
303+ };
304+ if access >= required {
305+ Ok(repo)
306+ } else if access >= auth::Access::Read {
307+ Err("You do not have write access to this repository.".to_string())
308+ } else {
309+ // No read access at all: do not confirm the repo exists.
310+ Err(not_found())
311+ }
312+ }
313+}
314+
315+/// Run the SSH server until the process exits. A no-op when `ssh.enabled` is false.
316+///
317+/// # Errors
318+///
319+/// Returns [`SshError`] if the host key cannot be prepared or the server fails to
320+/// bind or run.
321+pub async fn serve(
322+ config: Arc<Config>,
323+ store: Store,
324+ config_path: Option<String>,
325+) -> Result<(), SshError> {
326+ if !config.ssh.enabled {
327+ tracing::info!("ssh disabled (ssh.enabled = false)");
328+ return Ok(());
329+ }
330+
331+ let host_pem = crate::hostkey::load_or_generate(&config.ssh.host_key_path)?;
332+ let host_key = russh::keys::PrivateKey::from_openssh(host_pem.as_str())
333+ .map_err(|e| SshError::HostKey(format!("parsing host key: {e}")))?;
334+ let russh_config = Arc::new(russh::server::Config {
335+ methods: MethodSet::from(&[MethodKind::PublicKey][..]),
336+ keys: vec![host_key],
337+ inactivity_timeout: Some(Duration::from_secs(3600)),
338+ auth_rejection_time: Duration::from_secs(2),
339+ ..russh::server::Config::default()
340+ });
341+
342+ let shared = Arc::new(Shared {
343+ store,
344+ config: Arc::clone(&config),
345+ config_path,
346+ });
347+ let addr = (config.ssh.address.clone(), config.ssh.port);
348+ tracing::info!(address = %config.ssh.address, port = config.ssh.port, "ssh server listening");
349+
350+ let mut server = GitServer { shared };
351+ server
352+ .run_on_address(russh_config, addr)
353+ .await
354+ .map_err(|e| SshError::Server(e.to_string()))
355+}
crates/store/src/keys.rs +14 −0
@@ -156,6 +156,20 @@ impl Store {
156156 Ok(row.as_ref().map(map_key).transpose()?)
157157 }
158158
159+ /// Stamp a key's `last_used_at` with the current time (SSH auth binding).
160+ ///
161+ /// # Errors
162+ ///
163+ /// Returns [`StoreError::Query`] on a database failure.
164+ pub async fn touch_key_used(&self, key_id: &str) -> Result<(), StoreError> {
165+ sqlx::query("UPDATE keys SET last_used_at = $1 WHERE id = $2")
166+ .bind(now_ms())
167+ .bind(key_id)
168+ .execute(&self.pool)
169+ .await?;
170+ Ok(())
171+ }
172+
159173 /// Delete a key by id. Returns `true` if a row was removed.
160174 ///
161175 /// # Errors
deny.toml +5 −0
@@ -22,6 +22,11 @@ allow = [
2222 "Unicode-3.0",
2323 "Zlib",
2424 "CC0-1.0",
25+ # SSH transport requires a crypto backend (russh -> aws-lc-rs). aws-lc-rs and
26+ # aws-lc-sys carry the OpenSSL license in their SPDX expression; SSH is not
27+ # optional for a git server, so this one addition is accepted. See
28+ # docs/decisions.md.
29+ "OpenSSL",
2530 ]
2631 confidence-threshold = 0.9
2732
docs/decisions.md +26 −0
@@ -473,3 +473,29 @@ lands with the API phase. `fabrica hook post-receive` derives the repo id from t
473473 `GIT_DIR` basename and loads config from `FABRICA_CONFIG` (set by the SSH/HTTP
474474 server on the child) or the default location; a local push with neither set no-ops
475475 cleanly, and it always exits 0 so a hook error never rejects an accepted push.
476+
477+## 2026-07-25 — SSH via russh; the OpenSSL license is admitted for the crypto backend
478+
479+**Decision:** `crates/ssh` uses `russh` 0.62 with its **default** crypto backend
480+`aws-lc-rs` (explicitly **not** `ring`). `deny.toml` gains the **`OpenSSL`** license,
481+and `flake.nix` gains `cmake` (aws-lc-sys builds AWS-LC via cmake).
482+
483+**Alternatives:** the `ring` backend (also carries OpenSSL-family terms and is not
484+allowlisted); not shipping SSH at all.
485+
486+**Rationale:** SSH is not optional for a git server (the DoD requires push over
487+SSH), the spec names russh, and russh cannot function without a crypto backend.
488+Its only backends are `aws-lc-rs` (default) and `ring`; both pull the OpenSSL
489+license into the SPDX graph, so *some* license admission is unavoidable to have SSH
490+at all. `aws-lc-rs` is the default and keeps `ring` out. Admitting `OpenSSL` — a
491+permissive license — for the crypto backend is the minimal, contained concession,
492+justified by SSH being mandatory. This is the one place the otherwise-strict
493+allowlist is widened, and it is scoped to the transport crypto.
494+
495+**Implementation notes:** russh merged `russh-keys` into `russh::keys` and
496+re-exports `ssh-key` at a patch level distinct from the workspace's, so the two
497+`PrivateKey` types differ; the host key bridges them via the OpenSSH PEM text
498+(generate/load with the workspace `ssh-key`, parse with `russh::keys`). Identity is
499+by key fingerprint, not the SSH username (everyone connects as `git@`);
500+`channel_open_session` must explicitly `reply.accept()` in 0.62 or the channel is
501+rejected; exec output streams through the `Clone + Send` `Handle` from a task.
flake.nix +3 −0
@@ -53,6 +53,8 @@
5353 nativeBuildInputs = [
5454 pkgs.pkg-config
5555 pkgs.makeWrapper
56+ # aws-lc-sys (russh's crypto backend) builds AWS-LC via cmake.
57+ pkgs.cmake
5658 ];
5759 buildInputs = [
5860 pkgs.libgit2
@@ -161,6 +163,7 @@
161163 pkgs.cargo-deny
162164 pkgs.just
163165 pkgs.pkg-config
166+ pkgs.cmake
164167 ];
165168 buildInputs = commonArgs.buildInputs;
166169 LIBGIT2_NO_VENDOR = "1";
src/serve.rs +17 −2
@@ -54,8 +54,23 @@ pub fn run(req: ServeRequest) -> anyhow::Result<ExitCode> {
5454 )
5555 .parse()?;
5656
57- let state = web::AppState::build(Arc::new(loaded.config), store, secret)?;
58- web::serve(state, addr).await?;
57+ let config = Arc::new(loaded.config);
58+
59+ // Start the SSH server alongside the web server. Its lifetime is tied to
60+ // the process: when web's graceful shutdown returns, we abort it.
61+ let ssh_config = Arc::clone(&config);
62+ let ssh_store = store.clone();
63+ let ssh_config_path = req.config_path.as_ref().map(|p| p.display().to_string());
64+ let ssh_task = tokio::spawn(async move {
65+ if let Err(err) = ssh::serve(ssh_config, ssh_store, ssh_config_path).await {
66+ eprintln!("ssh server error: {err}");
67+ }
68+ });
69+
70+ let state = web::AppState::build(config, store, secret)?;
71+ let result = web::serve(state, addr).await;
72+ ssh_task.abort();
73+ result?;
5974 Ok(ExitCode::SUCCESS)
6075 })
6176 }