├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── auth_keys.rs ├── error.rs ├── lib.rs ├── logger.rs ├── pam_items.rs ├── sign_verify.rs └── ssh_agent_auth.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | env: 17 | SSH_AUTH_SOCK: /tmp/ssh-agent.sock 18 | steps: 19 | - name: Install Packages 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install libpam0g-dev libssl-dev 23 | - uses: actions/checkout@v2 24 | with: 25 | submodules: recursive 26 | - name: Build 27 | run: cargo build --verbose 28 | - name: Run tests 29 | run: | 30 | ssh-agent -a $SSH_AUTH_SOCK 31 | ssh-keygen -t ecdsa -b 521 -f $HOME/.ssh/id_ecdsa521 32 | ssh-keygen -t ecdsa -b 384 -f $HOME/.ssh/id_ecdsa384 33 | ssh-keygen -t ecdsa -b 256 -f $HOME/.ssh/id_ecdsa256 34 | ssh-keygen -t ed25519 -f $HOME/.ssh/id_ed25519 35 | ssh-keygen -t rsa -f $HOME/.ssh/id_rsa 36 | ssh-keygen -t dsa -f $HOME/.ssh/id_dsa 37 | ssh-add $HOME/.ssh/id_ecdsa521 38 | ssh-add $HOME/.ssh/id_ecdsa384 39 | ssh-add $HOME/.ssh/id_ecdsa256 40 | ssh-add $HOME/.ssh/id_ed25519 41 | ssh-add $HOME/.ssh/id_rsa 42 | ssh-add $HOME/.ssh/id_dsa 43 | cp $HOME/.ssh/id_rsa.pub $HOME/.ssh/authorized_keys 44 | 45 | cargo test --verbose 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dep/ssh-agent.rs"] 2 | path = dep/ssh-agent.rs 3 | url = https://github.com/z4yx/ssh-agent.rs.git 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "base64" 7 | version = "0.22.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.6.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 16 | 17 | [[package]] 18 | name = "byteorder" 19 | version = "1.5.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 22 | 23 | [[package]] 24 | name = "cc" 25 | version = "1.1.15" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" 28 | dependencies = [ 29 | "shlex", 30 | ] 31 | 32 | [[package]] 33 | name = "cfg-if" 34 | version = "1.0.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 37 | 38 | [[package]] 39 | name = "deranged" 40 | version = "0.3.11" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 43 | dependencies = [ 44 | "powerfmt", 45 | ] 46 | 47 | [[package]] 48 | name = "foreign-types" 49 | version = "0.3.2" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 52 | dependencies = [ 53 | "foreign-types-shared", 54 | ] 55 | 56 | [[package]] 57 | name = "foreign-types-shared" 58 | version = "0.1.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 61 | 62 | [[package]] 63 | name = "futures" 64 | version = "0.1.31" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" 67 | 68 | [[package]] 69 | name = "hostname" 70 | version = "0.4.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" 73 | dependencies = [ 74 | "cfg-if", 75 | "libc", 76 | "windows", 77 | ] 78 | 79 | [[package]] 80 | name = "itoa" 81 | version = "1.0.11" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 84 | 85 | [[package]] 86 | name = "libc" 87 | version = "0.2.158" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" 90 | 91 | [[package]] 92 | name = "log" 93 | version = "0.4.22" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 96 | dependencies = [ 97 | "serde", 98 | ] 99 | 100 | [[package]] 101 | name = "memchr" 102 | version = "2.7.4" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 105 | 106 | [[package]] 107 | name = "multisock" 108 | version = "1.0.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "09b00b95a51f8573ee359668dfbfed424212dd0fc74df2333816fddff856f342" 111 | 112 | [[package]] 113 | name = "num-conv" 114 | version = "0.1.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 117 | 118 | [[package]] 119 | name = "num_threads" 120 | version = "0.1.7" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 123 | dependencies = [ 124 | "libc", 125 | ] 126 | 127 | [[package]] 128 | name = "once_cell" 129 | version = "1.19.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 132 | 133 | [[package]] 134 | name = "openssl" 135 | version = "0.10.66" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" 138 | dependencies = [ 139 | "bitflags", 140 | "cfg-if", 141 | "foreign-types", 142 | "libc", 143 | "once_cell", 144 | "openssl-macros", 145 | "openssl-sys", 146 | ] 147 | 148 | [[package]] 149 | name = "openssl-macros" 150 | version = "0.1.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 153 | dependencies = [ 154 | "proc-macro2", 155 | "quote", 156 | "syn", 157 | ] 158 | 159 | [[package]] 160 | name = "openssl-sys" 161 | version = "0.9.103" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 164 | dependencies = [ 165 | "cc", 166 | "libc", 167 | "pkg-config", 168 | "vcpkg", 169 | ] 170 | 171 | [[package]] 172 | name = "pam-bindings" 173 | version = "0.1.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "95c337e922acb6ab9c3ddd1016fed13957a5bf14f51b6caa293ddc8dd47660ca" 176 | dependencies = [ 177 | "libc", 178 | ] 179 | 180 | [[package]] 181 | name = "pam_rssh" 182 | version = "1.2.0" 183 | dependencies = [ 184 | "base64", 185 | "byteorder", 186 | "log", 187 | "multisock", 188 | "openssl", 189 | "openssl-sys", 190 | "pam-bindings", 191 | "pwd", 192 | "ssh-agent", 193 | "subst", 194 | "syslog", 195 | ] 196 | 197 | [[package]] 198 | name = "pkg-config" 199 | version = "0.3.30" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 202 | 203 | [[package]] 204 | name = "powerfmt" 205 | version = "0.2.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 208 | 209 | [[package]] 210 | name = "proc-macro2" 211 | version = "1.0.86" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 214 | dependencies = [ 215 | "unicode-ident", 216 | ] 217 | 218 | [[package]] 219 | name = "pwd" 220 | version = "1.4.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "72c71c0c79b9701efe4e1e4b563b2016dd4ee789eb99badcb09d61ac4b92e4a2" 223 | dependencies = [ 224 | "libc", 225 | "thiserror", 226 | ] 227 | 228 | [[package]] 229 | name = "quote" 230 | version = "1.0.37" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 233 | dependencies = [ 234 | "proc-macro2", 235 | ] 236 | 237 | [[package]] 238 | name = "serde" 239 | version = "1.0.209" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" 242 | dependencies = [ 243 | "serde_derive", 244 | ] 245 | 246 | [[package]] 247 | name = "serde_derive" 248 | version = "1.0.209" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" 251 | dependencies = [ 252 | "proc-macro2", 253 | "quote", 254 | "syn", 255 | ] 256 | 257 | [[package]] 258 | name = "shlex" 259 | version = "1.3.0" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 262 | 263 | [[package]] 264 | name = "ssh-agent" 265 | version = "0.2.3" 266 | dependencies = [ 267 | "byteorder", 268 | "futures", 269 | "log", 270 | "serde", 271 | ] 272 | 273 | [[package]] 274 | name = "subst" 275 | version = "0.3.3" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "266d3fe7ffc582b3a0c3fe36cdc88d5635a1c2d53e7c3f813c901d7bd1d34ba0" 278 | dependencies = [ 279 | "memchr", 280 | "unicode-width", 281 | ] 282 | 283 | [[package]] 284 | name = "syn" 285 | version = "2.0.77" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" 288 | dependencies = [ 289 | "proc-macro2", 290 | "quote", 291 | "unicode-ident", 292 | ] 293 | 294 | [[package]] 295 | name = "syslog" 296 | version = "7.0.0" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "019f1500a13379b7d051455df397c75770de6311a7a188a699499502704d9f10" 299 | dependencies = [ 300 | "hostname", 301 | "libc", 302 | "log", 303 | "time", 304 | ] 305 | 306 | [[package]] 307 | name = "thiserror" 308 | version = "1.0.63" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 311 | dependencies = [ 312 | "thiserror-impl", 313 | ] 314 | 315 | [[package]] 316 | name = "thiserror-impl" 317 | version = "1.0.63" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 320 | dependencies = [ 321 | "proc-macro2", 322 | "quote", 323 | "syn", 324 | ] 325 | 326 | [[package]] 327 | name = "time" 328 | version = "0.3.36" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 331 | dependencies = [ 332 | "deranged", 333 | "itoa", 334 | "libc", 335 | "num-conv", 336 | "num_threads", 337 | "powerfmt", 338 | "serde", 339 | "time-core", 340 | "time-macros", 341 | ] 342 | 343 | [[package]] 344 | name = "time-core" 345 | version = "0.1.2" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 348 | 349 | [[package]] 350 | name = "time-macros" 351 | version = "0.2.18" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 354 | dependencies = [ 355 | "num-conv", 356 | "time-core", 357 | ] 358 | 359 | [[package]] 360 | name = "unicode-ident" 361 | version = "1.0.12" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 364 | 365 | [[package]] 366 | name = "unicode-width" 367 | version = "0.1.13" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" 370 | 371 | [[package]] 372 | name = "vcpkg" 373 | version = "0.2.15" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 376 | 377 | [[package]] 378 | name = "windows" 379 | version = "0.52.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 382 | dependencies = [ 383 | "windows-core", 384 | "windows-targets", 385 | ] 386 | 387 | [[package]] 388 | name = "windows-core" 389 | version = "0.52.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 392 | dependencies = [ 393 | "windows-targets", 394 | ] 395 | 396 | [[package]] 397 | name = "windows-targets" 398 | version = "0.52.6" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 401 | dependencies = [ 402 | "windows_aarch64_gnullvm", 403 | "windows_aarch64_msvc", 404 | "windows_i686_gnu", 405 | "windows_i686_gnullvm", 406 | "windows_i686_msvc", 407 | "windows_x86_64_gnu", 408 | "windows_x86_64_gnullvm", 409 | "windows_x86_64_msvc", 410 | ] 411 | 412 | [[package]] 413 | name = "windows_aarch64_gnullvm" 414 | version = "0.52.6" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 417 | 418 | [[package]] 419 | name = "windows_aarch64_msvc" 420 | version = "0.52.6" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 423 | 424 | [[package]] 425 | name = "windows_i686_gnu" 426 | version = "0.52.6" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 429 | 430 | [[package]] 431 | name = "windows_i686_gnullvm" 432 | version = "0.52.6" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 435 | 436 | [[package]] 437 | name = "windows_i686_msvc" 438 | version = "0.52.6" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 441 | 442 | [[package]] 443 | name = "windows_x86_64_gnu" 444 | version = "0.52.6" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 447 | 448 | [[package]] 449 | name = "windows_x86_64_gnullvm" 450 | version = "0.52.6" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 453 | 454 | [[package]] 455 | name = "windows_x86_64_msvc" 456 | version = "0.52.6" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 459 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pam_rssh" 3 | version = "1.2.0" 4 | authors = ["Yuxiang Zhang"] 5 | edition = "2018" 6 | 7 | [lib] 8 | name = "pam_rssh" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | pam-bindings = "0.1.1" 13 | ssh-agent = { path = "./dep/ssh-agent.rs" } 14 | multisock = "^1.0.0" 15 | byteorder = "1.5.0" 16 | base64 = "^0.22.1" 17 | openssl-sys = "^0.9" 18 | openssl = "^0.10" 19 | log = { version = "^0.4", features = ["std", "serde"] } 20 | subst = "^0.3.0" 21 | syslog = "^7.0" 22 | pwd = "1" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Yuxiang Zhang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAM-RSSH 2 | 3 | [![Rust](https://github.com/z4yx/pam_rssh/actions/workflows/rust.yml/badge.svg)](https://github.com/z4yx/pam_rssh/actions/workflows/rust.yml) 4 | 5 | This PAM module provides ssh-agent based authentication. The primary design goal is to avoid typing password when you `sudo` on remote servers. Instead, you can simply touch your hardware security key (e.g. Yubikey/Canokey) to fulfill user verification. The process is done by forwarding the remote authentication request to client-side ssh-agent as a signature request. 6 | 7 | This project is developed in Rust language to minimize security flaws. 8 | 9 | ## Development Status 10 | 11 | It's ready for production use, and has been tested on production servers for over a year. More tests and feedback are welcome. 12 | 13 | Currently supported SSH public key types: 14 | - RSA (with SHA256 digest) 15 | - DSA 16 | - ECDSA 256/384/521 17 | - ECDSA-SK (FIDO2/U2F) 18 | - ED25519 19 | - ED25519-SK (FIDO2) 20 | 21 | ## Build and Install 22 | 23 | Prerequisites: 24 | 25 | - OpenSSL (>=1.1.1) 26 | - libpam 27 | - Rust (with Cargo) 28 | 29 | Clone this repo with **a submodule**. 30 | 31 | ``` 32 | git clone --recurse-submodule https://github.com/z4yx/pam_rssh.git 33 | cd pam_rssh 34 | ``` 35 | 36 | Then build it using Cargo. 37 | 38 | ``` 39 | cargo build --release 40 | cp target/release/libpam_rssh.so 41 | ``` 42 | 43 | ## `pam module path` 44 | 45 | The module path is specific to certain distributions 46 | 47 | | OS | Destination | 48 | | ------------ | ----------------------------------- | 49 | | Arch Linux | `/usr/lib/security/` | 50 | | Debian | `/lib/x86_64-linux-gnu/security/` | 51 | | openSUSE | `/lib/security/` | 52 | 53 | ## Config 54 | 55 | Add the following line to `/etc/pam.d/sudo` (place it before existing rules): 56 | 57 | ``` 58 | auth sufficient libpam_rssh.so 59 | ``` 60 | 61 | Then edit sudoers with `visudo` command. Add the following line: (It makes `sudo` keep the environment variable, so this module can communicate with ssh-agent) 62 | ``` 63 | Defaults env_keep += "SSH_AUTH_SOCK" 64 | ``` 65 | 66 | 67 | Start a ssh-agent on your client, then add your keys with `ssh-add`. 68 | 69 | Try to ssh to your server with forwarded agent (-A option), and make a `sudo` there. 70 | 71 | ## Optional Arguments 72 | 73 | The following arguments are supported: 74 | 75 | - `loglevel=` Select the level of messages logged to syslog. Defaults to `warn`. 76 | - `debug` Equivalent to `loglevel=debug`. 77 | - `ssh_agent_addr=` The address of ssh-agent. Defaults to the value of `SSH_AUTH_SOCK` environment variable, which is set by ssh automatically. 78 | - `auth_key_file=` Public keys allowed for user authentication. Defaults to `/.ssh/authorized_keys`. `` is read from system configuration, usually it expands to `/home/`. 79 | - `authorized_keys_command=` A command to generate the authorized_keys. It takes a single argument, the username of the user being authenticated. The standard output of this command will be parsed as authorized_keys. The `auth_key_file` will be ignored if you specify this argument. 80 | - `authorized_keys_command_user=` The `authorized_keys_command` will be run as the user specified here. If this argument is omitted, the `authorized_keys_command` will be run as the user being authenticated. 81 | - `cue` Enable device interaction prompt. When enabled, displays a message reminding the user to touch their device during authentication. 82 | - `[cue_prompt=]` Set custom prompt message for device interaction. Default: "Please touch the device". Use square brackets to include spaces in the message. 83 | 84 | Arguments should be appended to the PAM rule. For example: 85 | 86 | ``` 87 | auth sufficient libpam_rssh.so debug authorized_keys_command=/usr/bin/sss_ssh_authorizedkeys authorized_keys_command_user=nobody cue [cue_prompt=long long prompt] 88 | ``` 89 | 90 | ## Use Variables in Arguments 91 | 92 | Certain variables can be used in arguments. Supported formats are `$var`, `${var}` and `${var:default value}`. For example: 93 | 94 | ``` 95 | auth sufficient libpam_rssh.so auth_key_file=/data/${user}.keys 96 | ``` 97 | 98 | Variables are mapped to PAM items. Currently the following variables are available: 99 | 100 | - service: PAM_SERVICE. The service name (which identifies the PAM stack that will be used). 101 | - user: PAM_USER. The username of the entity under whose identity service will be given. 102 | - tty: PAM_TTY. The terminal name. 103 | - rhost: PAM_RHOST. The requesting hostname. 104 | - ruser: PAM_RUSER. The requesting entity. 105 | 106 | For detailed description on PAM items, read man page pam_get_item(3). 107 | -------------------------------------------------------------------------------- /src/auth_keys.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | #[link(name = "c")] 3 | extern "C" { 4 | fn geteuid() -> u32; 5 | fn getegid() -> u32; 6 | } 7 | use pwd::Passwd; 8 | use ssh_agent::proto::from_bytes; 9 | use ssh_agent::proto::public_key::PublicKey; 10 | 11 | use std::fs; 12 | use std::os::unix::process::CommandExt; 13 | use std::path::PathBuf; 14 | use std::process::Command; 15 | 16 | use super::error::RsshErr; 17 | 18 | type ErrType = Box; 19 | 20 | fn parse_pubkey_fields(line: &str) -> Result { 21 | let mut fields = line.split_whitespace(); 22 | if let Some(algo) = fields.next() { 23 | if let Some(b64key) = fields.next() { 24 | let b64trimmed = b64key.trim_end(); 25 | debug!("parse_pubkey_fields: {} {}", algo, b64trimmed); 26 | let key = base64::decode(b64trimmed) 27 | .map_err(|_| RsshErr::ParsePubkeyErr) 28 | .and_then(|blob| from_bytes(&blob).map_err(|_| RsshErr::ParsePubkeyErr))?; 29 | return Ok(key); 30 | } 31 | } 32 | warn!("At least two fields are required: `{}`", line); 33 | return Err(RsshErr::ParsePubkeyErr.into_ptr()); 34 | } 35 | 36 | // Based on sshkey_advance_past_options() in https://github.com/openssh/openssh-portable/blob/master/authfile.c 37 | fn skip_options(line: &str) -> Result { 38 | let mut it = line.chars(); 39 | let mut quote = false; 40 | while let Some(ch) = it.next() { 41 | if (ch == ' ' || ch == '\t') && !quote { 42 | break; 43 | } 44 | if ch == '\\' { 45 | // skip one character after escape symbol 46 | it.next(); 47 | } else if ch == '"' { 48 | quote = !quote; 49 | } 50 | } 51 | if quote { 52 | Err(RsshErr::ParsePubkeyErr.into_ptr()) 53 | } else { 54 | Ok(it.collect::()) 55 | } 56 | } 57 | 58 | pub fn parse_content_of_authorized_keys(content: &String) -> Result, ErrType> { 59 | let mut lines = content.lines(); 60 | let mut res: Vec = vec![]; 61 | while let Some(line) = lines.next() { 62 | let trimed = line.trim_start(); 63 | if trimed.is_empty() || trimed.starts_with("#") { 64 | continue; 65 | } 66 | if let Ok(pubkey) = parse_pubkey_fields(trimed) { 67 | res.push(pubkey); 68 | continue; 69 | } 70 | // If there are options before public key, skip them 71 | let skipped = skip_options(trimed); 72 | if let Err(e) = skipped { 73 | debug!("skip_options() returns: {}", e.as_ref()); 74 | continue; 75 | } 76 | debug!("pubkey line after options skipped: {:?}", skipped); 77 | match skipped.and_then(|s| parse_pubkey_fields(s.trim_start())) { 78 | Ok(pubkey) => res.push(pubkey), 79 | Err(e) => debug!("parse_pubkey_fields() returns: {}", e.as_ref()) 80 | } 81 | } 82 | Ok(res) 83 | } 84 | 85 | pub fn parse_authorized_keys(filename: &str) -> Result, ErrType> { 86 | let content = 87 | fs::read_to_string(filename).map_err(|_| RsshErr::FileReadErr(filename.to_string()))?; 88 | parse_content_of_authorized_keys(&content) 89 | } 90 | 91 | pub fn parse_user_authorized_keys(username: &str) -> Result, ErrType> { 92 | let mut prefix = format!("/home/{}",username); 93 | // Gets user's $HOME to search for authorized_keys 94 | let _ = Passwd::from_name(username).map(|opt_passwd| { 95 | opt_passwd.map(|passwd| { 96 | prefix = passwd.dir; 97 | }) 98 | }); 99 | let path: PathBuf = [prefix.as_str(), ".ssh", "authorized_keys"] 100 | .iter() 101 | .collect(); 102 | parse_authorized_keys(path.to_str().ok_or(RsshErr::GetHomeErr)?) 103 | } 104 | 105 | pub fn run_authorized_keys_cmd(auth_key_cmd: &str, auth_user: &str, run_user: &str) -> Result { 106 | let uid; 107 | let opt_p = Passwd::from_name(run_user)?; 108 | if let Some(p) = opt_p { 109 | uid = p.uid; 110 | } else { 111 | warn!("Failed to get the uid of `{}`", run_user); 112 | return Err(RsshErr::GetUidErr.into_ptr()); 113 | }; 114 | let euid: u32; 115 | unsafe { 116 | euid = geteuid(); 117 | } 118 | let mut cmd = Command::new(auth_key_cmd); 119 | let cmd_with_arg; 120 | if euid == 0 { // current user is root, setuid() is allowed 121 | debug!("Current user is root, set uid to {}", uid); 122 | cmd_with_arg = cmd.arg(auth_user).uid(uid); 123 | } else { 124 | cmd_with_arg = cmd.arg(auth_user); 125 | } 126 | let result = cmd_with_arg.output()?; 127 | let status = result.status; 128 | if !status.success() { 129 | return Err(RsshErr::CmdExitErr(status.code()).into_ptr()) 130 | } 131 | if let Ok(t) = String::from_utf8(result.stdout) { 132 | debug!("Got authorized keys from command output, len={}", t.len()); 133 | Ok(t) 134 | } else { 135 | Err(RsshErr::CmdOutputDecodeErr.into_ptr()) 136 | } 137 | } 138 | 139 | #[test] 140 | fn test_skip_options() { 141 | assert_eq!(skip_options("opt key comment").unwrap(), "key comment"); 142 | assert_eq!(skip_options("a=b,c=d key comment").unwrap(), "key comment"); 143 | assert_eq!( 144 | skip_options("a=b,x=\"\t\",c=\"d key\"\t key2 comment").unwrap(), 145 | " key2 comment" 146 | ); 147 | assert_eq!(skip_options("ee").unwrap(), ""); 148 | assert_eq!(skip_options("").unwrap(), ""); 149 | assert_eq!(skip_options("a=\"").is_err(), true); 150 | assert_eq!(skip_options("a=\" k").is_err(), true); 151 | assert_eq!(skip_options("a=\\\"").unwrap(), ""); 152 | assert_eq!(skip_options("a=\"\\\"\" k").unwrap(), "k"); 153 | assert_eq!(skip_options("a=\"\\\\\" k").unwrap(), "k"); 154 | } 155 | 156 | #[test] 157 | fn test_parse_authorized_keys() { 158 | let _ = log::set_boxed_logger(Box::new(super::logger::ConsoleLogger)) 159 | .map(|()| log::set_max_level(log::LevelFilter::Debug)); 160 | let mut content = concat!( 161 | "ssh-ed25519 InvalidBase64\n", 162 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILNwZPJqdxsO6ahniFpVqNbT9ACXHSDpF5XLkrRU9dUV \n", 163 | " some_opt ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILNwZPJqdxsO6ahniFpVqNbT9ACXHSDpF5XLkrRU9dUV my key\n", 164 | "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILNwZPJqdxsO6ahniFpVqNbT9ACXHSDpF5XLkrRU9dUV\tmy key\n", 165 | " ssh-ed25519\tAAAAC3NzaC1lZDI1NTE5AAAAILNwZPJqdxsO6ahniFpVqNbT9ACXHSDpF5XLkrRU9dUV\tmy key\n", 166 | " #ssh-ed25519\tAAAAC3NzaC1lZDI1NTE5AAAAILNwZPJqdxsO6ahniFpVqNbT9ACXHSDpF5XLkrRU9dUV\tmy key\n", 167 | "#comment\n", 168 | ); 169 | let path = "/tmp/pam_rssh.test"; 170 | fs::write(path, content).unwrap(); 171 | assert_eq!(parse_authorized_keys(path).unwrap().len(), 4); 172 | 173 | content = concat!( 174 | "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBOs1kRdss9EmY9MPMA/e5HC10mtxnkXbU6wSdlIMugAweQc7ckFPvY+y1F86Z2eR4X42Qo/85bMDjlM/2BAJqbYAAAAEc3NoOg== test@n\n", 175 | "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIBRogHNAvK4P9f8EKulnxrF8SfKIdqNrIleHdj0wvAU5AAAADXNzaDpUb3VjaE9ubHk= \n", 176 | "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCx3nZIDXpjn68tl0McPq8WCoTio1iVaAlAE7NnQRn9js8+T/dhiE1s4T7qMqf3iSNDzfq/qY8paAWips5z8t/Soy2x5xTcSWBX2zoCcM1H4R/jYcnfUTvfsTfjCVkUiQxX3wkyThe5biU/NjrB92NbQH6ZFf53SjXL6ax/9Q1e5938uKQnE+bBBHDBPBixRQzGT6NbTiegjDf6tyQaKNzPhATTlAqDaQzUIVHvoVPuJ2hT7OiDr72wwdrnIkobKtykbrGITobd4XujhIMje024gqlTucWUzA91m2LgBx4pfOzYlVUZcXorVAL0wpTPN4ursiEEtU/kYZN5xUDX1anp0GeNK74SBvUfq8mm9nx7evPMEH9Cdl3SFa9oQsqOSOHJNICW6svfaDweGTsI51KJptSuF1jq/V9VlLZmIOFezPEXiM13Q6ZqAigzPBggzNI4KgtSzztFnEKwvV1RO/GfaMn5xp2Cdovpfb0FhIsZ7i6wTXCk4ZqfvUDuKRoDE+k=\n", 177 | "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBF279yp1qnjADnYs+muktz26HuNwznkDf6vQT6WKJ42GwD4GHhFLCj2CIL26qkroEynLAqt19XUMBnb/JGPCThw= test\n", 178 | "ssh-dss AAAAB3NzaC1kc3MAAACBAOPA1udikQuPhCemarDkDj5ZIinN3quZty5Y0/APTdhOVQ8Ad5aJ7Bz0cCNzjeaTYrZh92nT8PJzIFnn/bT0sPcao8ApLu3OgeYdXx9tofw53o5bcUEkcVaKiSgVTYXIYTJmDFTTgTaQWNAjag+zcfZGRXVyuHfhOLYzIUuJEZmjAAAAFQDq6w2ThuInZSY3FEAynryDfNjAuwAAAIALvNCORC5qigYfNpqJ68P21GDQ3II9FeOa3zo4vV2h0htJb9OY8MpUEAMFyOuhtJ97OvbjjTBQY6WLycOwoJnsf8wZcVJFR5/R0DrVzil9sPLduAVkzrHGbH0ESAxWsymlLDWYywycCtCEFEGouyBNgsgzMgRycDPH2akQeX+vGAAAAIEAurSBK5tyWFvdGQUlhPQwmGnT4iGaYsElDLjRSFe4qNFJ1vJ+m8FEaoNudR0JLx/AtlyP2GNdDddnsxNvusr2x+uXzyc5PRKhZ82xSlGDSkrPPxDl4s24KE3c6jY8Lu5JZ/dEjjW2SsydY3nrlY12toLCtFAMMojcf0pNQMqnh6s= \n", 179 | ); 180 | fs::write(path, content).unwrap(); 181 | assert_eq!(parse_authorized_keys(path).unwrap().len(), 5); 182 | } 183 | 184 | #[test] 185 | fn test_run_authorized_keys_cmd() { 186 | let _ = log::set_boxed_logger(Box::new(super::logger::ConsoleLogger)) 187 | .map(|()| log::set_max_level(log::LevelFilter::Debug)); 188 | // let whoami = Passwd::current_user().unwrap().name; 189 | let test_user = String::from("nobody"); 190 | 191 | let mut ret = run_authorized_keys_cmd("/bin/non-exist", "root", "root"); 192 | assert!(ret.is_err()); 193 | info!("err message: {}", ret.unwrap_err()); 194 | 195 | ret = run_authorized_keys_cmd("/bin/echo", "sb", "root"); 196 | assert!(ret.is_ok()); 197 | assert_eq!(ret.unwrap(), "sb\n"); 198 | 199 | ret = run_authorized_keys_cmd("/bin/echo", test_user.as_str(), test_user.as_str()); 200 | assert!(ret.is_ok()); 201 | assert_eq!(ret.unwrap(), test_user.clone() + "\n"); 202 | 203 | ret = run_authorized_keys_cmd("/bin/echo", "sb", "no_such_user"); 204 | assert!(ret.is_err()); 205 | let mut err = ret.unwrap_err(); 206 | info!("err message: {}", err); 207 | assert!(matches!(err.downcast::().unwrap().as_ref(), RsshErr::GetUidErr)); 208 | 209 | ret = run_authorized_keys_cmd("/bin/false", "sb", test_user.as_str()); 210 | assert!(ret.is_err()); 211 | err = ret.unwrap_err(); 212 | info!("err message: {}", err); 213 | assert!(matches!(err.downcast::().unwrap().as_ref(), RsshErr::CmdExitErr(Some(n)) if *n==1)); 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::Formatter; 3 | use std::fmt::{Display, Result}; 4 | 5 | #[derive(Debug)] 6 | pub enum RsshErr { 7 | FileReadErr(String), 8 | ParsePubkeyErr, 9 | AgentFailureErr, 10 | SignVerifyErr, 11 | InvalidSigErr, 12 | RetryLT1Err, 13 | InvalidRspErr, 14 | GetUserErr, 15 | UsernameDecodeErr, 16 | GetHomeErr, 17 | GetUidErr, 18 | CmdExitErr(Option), 19 | CmdOutputDecodeErr, 20 | InvalidLogLvlErr, 21 | OptNameErr(String), 22 | OptValEmptyErr(String), 23 | OptVarErr(String), 24 | SendPromptErr, 25 | } 26 | 27 | impl RsshErr { 28 | pub fn into_ptr(self) -> Box { 29 | Box::new(self) 30 | } 31 | } 32 | 33 | macro_rules! S { 34 | ($s:expr) => { 35 | String::from($s) 36 | }; 37 | } 38 | 39 | impl Display for RsshErr { 40 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 41 | let msg = match self { 42 | RsshErr::FileReadErr(name) => format!("Failed to read `{}`", name), 43 | RsshErr::ParsePubkeyErr => S!("Failed to parse the public key"), 44 | RsshErr::AgentFailureErr => S!("SSH-Agent reports failure"), 45 | RsshErr::SignVerifyErr => S!("Signature verification failed"), 46 | RsshErr::InvalidSigErr => S!("Invalid signature format"), 47 | RsshErr::RetryLT1Err => S!("Number of retry is less than one"), 48 | RsshErr::InvalidRspErr => S!("Invalid type of response"), 49 | RsshErr::GetUserErr => S!("Failed to get user name"), 50 | RsshErr::UsernameDecodeErr => S!("Failed to decode the user name"), 51 | RsshErr::GetHomeErr => S!("Cannot get user's home directory"), 52 | RsshErr::GetUidErr => S!("Cannot get uid of specified user"), 53 | RsshErr::CmdExitErr(code) => format!("Command exit code is {}", code.unwrap_or(-1)), 54 | RsshErr::CmdOutputDecodeErr => S!("Failed to decode the output of command"), 55 | RsshErr::InvalidLogLvlErr => S!("Invalid log level"), 56 | RsshErr::OptNameErr(name) => format!("Unknown option name `{}`", name), 57 | RsshErr::OptValEmptyErr(name) => format!("Value of option `{}` is empty", name), 58 | RsshErr::OptVarErr(name) => format!("Failed to evaluate variables in option `{}`", name), 59 | RsshErr::SendPromptErr => S!("Failed to send prompt message"), 60 | }; 61 | f.write_str(&msg) 62 | } 63 | } 64 | 65 | impl Error for RsshErr {} 66 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate pam; 3 | 4 | mod auth_keys; 5 | mod error; 6 | mod logger; 7 | mod sign_verify; 8 | mod ssh_agent_auth; 9 | mod pam_items; 10 | 11 | use log::*; 12 | use pam::constants::{PamFlag, PamResultCode, PAM_TEXT_INFO}; 13 | use pam::items::User; 14 | use pam::module::{PamHandle, PamHooks, PamResult}; 15 | use ssh_agent::proto::KeyTypeEnum; 16 | use ssh_agent::proto::public_key::PublicKey; 17 | use syslog::{BasicLogger, Facility, Formatter3164}; 18 | 19 | use std::ffi::CStr; 20 | use std::str::FromStr; 21 | use pam::conv::Conv; 22 | use self::error::RsshErr; 23 | use self::logger::ConsoleLogger; 24 | 25 | type ErrType = Box; 26 | 27 | struct PamRssh; 28 | pam_hooks!(PamRssh); 29 | 30 | fn is_key_authorized(key: &PublicKey, authorized_keys: &Vec) -> bool { 31 | for item in authorized_keys { 32 | if item == key { 33 | return true; 34 | } 35 | } 36 | false 37 | } 38 | 39 | fn get_username_from_pam(pamh: &PamHandle) -> Result<&str, ErrType> { 40 | let user_item: User = pamh.get_item().map_err(|_| RsshErr::GetUserErr)?.ok_or(RsshErr::GetUserErr)?; 41 | let user = user_item.to_str().map_err(|_| RsshErr::UsernameDecodeErr)?; 42 | Ok(user) 43 | } 44 | 45 | fn read_authorized_keys(pamh: &PamHandle, auth_key_file: &str) -> Result, ErrType> { 46 | if auth_key_file.is_empty() { 47 | let user = get_username_from_pam(pamh)?; 48 | info!("Reading authorized_keys of user `{}`", user); 49 | auth_keys::parse_user_authorized_keys(&user) 50 | } else { 51 | info!("Reading configured authorized_keys file: {}", auth_key_file); 52 | auth_keys::parse_authorized_keys(auth_key_file) 53 | } 54 | } 55 | 56 | fn retrieve_authorized_keys_from_cmd(pamh: &PamHandle, auth_key_cmd: &str, run_as_user: &str) -> Result, ErrType> { 57 | let auth_user = get_username_from_pam(pamh)?; 58 | debug!("Run command `{}` as user `{}`", auth_key_cmd, run_as_user); 59 | let content = auth_keys::run_authorized_keys_cmd(&auth_key_cmd, 60 | auth_user, 61 | if run_as_user.is_empty() { auth_user } else { run_as_user })?; 62 | auth_keys::parse_content_of_authorized_keys(&content) 63 | } 64 | 65 | fn authenticate_via_agent( 66 | agent: &mut ssh_agent_auth::AgentClient, 67 | pubkey: &PublicKey, 68 | ) -> Result<(), ErrType> { 69 | let challenge = sign_verify::gen_challenge()?; 70 | let sig = agent.sign_data(&challenge, pubkey)?; 71 | let verified = sign_verify::verify_signature(&challenge, pubkey, &sig)?; 72 | if verified { 73 | Ok(()) 74 | } else { 75 | Err(RsshErr::SignVerifyErr.into_ptr()) 76 | } 77 | } 78 | 79 | fn setup_logger() { 80 | let formatter = Formatter3164 { 81 | facility: Facility::LOG_AUTH, 82 | hostname: None, 83 | process: "pam_rssh".into(), 84 | pid: std::process::id() as u32, 85 | }; 86 | syslog::unix(formatter) 87 | .ok() 88 | .and_then(|logger| log::set_boxed_logger(Box::new(BasicLogger::new(logger))).ok()) 89 | .or_else(|| log::set_boxed_logger(Box::new(ConsoleLogger)).ok()) 90 | .map(|()| log::set_max_level(log::LevelFilter::Warn)); 91 | } 92 | 93 | fn substitute_variables(kv: &Vec<&str>, variables: &pam_items::PamItemsMap) -> Result { 94 | subst::substitute(kv[1], variables) 95 | .or(Err(RsshErr::OptVarErr(kv[0].to_string()).into_ptr())) 96 | } 97 | 98 | fn non_empty_option_check(kv: &Vec<&str>) -> Result<(), ErrType> { 99 | if kv.len() == 1 || kv[1].is_empty() { 100 | return Err(RsshErr::OptValEmptyErr(kv[0].to_string()).into_ptr()) 101 | } 102 | Ok(()) 103 | } 104 | 105 | impl PamHooks for PamRssh { 106 | fn sm_authenticate(pamh: &mut PamHandle, args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { 107 | /* if (flags & pam::constants::PAM_SILENT) == 0 */ 108 | { 109 | setup_logger(); 110 | } 111 | 112 | let pam_vars = pam_items::PamItemsMap::new(&pamh); 113 | let mut ssh_agent_addr = String::new(); 114 | let mut auth_key_file = String::new(); 115 | let mut authorized_keys_command = String::new(); 116 | let mut authorized_keys_command_user = String::new(); 117 | let mut cue = false; 118 | let mut cue_prompt = "Please touch the device.".to_string(); 119 | for carg in args { 120 | let kv: Vec<&str> = carg.to_str().unwrap_or("").splitn(2, '=').collect(); 121 | if kv.len() == 0 { 122 | continue; 123 | } 124 | trace!("Parsing option {:?}", kv); 125 | let mut parse_options = || -> Result<(), ErrType> { 126 | match kv[0] { 127 | "loglevel" => { 128 | non_empty_option_check(&kv)?; 129 | match log::Level::from_str(kv[1]) { 130 | Ok(level) => log::set_max_level(level.to_level_filter()), 131 | Err(_) => { return Err(RsshErr::InvalidLogLvlErr.into_ptr()) } 132 | } 133 | } 134 | "debug" => log::set_max_level(log::LevelFilter::Debug), 135 | "ssh_agent_addr" => { 136 | non_empty_option_check(&kv)?; 137 | ssh_agent_addr = substitute_variables(&kv, &pam_vars)?; 138 | } 139 | "auth_key_file" => { 140 | non_empty_option_check(&kv)?; 141 | auth_key_file = substitute_variables(&kv, &pam_vars)?; 142 | } 143 | "authorized_keys_command" => { 144 | non_empty_option_check(&kv)?; 145 | authorized_keys_command = substitute_variables(&kv, &pam_vars)?; 146 | } 147 | "authorized_keys_command_user" => { 148 | non_empty_option_check(&kv)?; 149 | authorized_keys_command_user = substitute_variables(&kv, &pam_vars)?; 150 | } 151 | "cue" => cue = true, 152 | "cue_prompt" => { 153 | non_empty_option_check(&kv)?; 154 | cue_prompt = substitute_variables(&kv, &pam_vars)?; 155 | } 156 | _ => { 157 | return Err(RsshErr::OptNameErr(kv[0].to_string()).into_ptr()); 158 | } 159 | } 160 | Ok(()) 161 | }; 162 | if let Err(opt_err) = parse_options() { 163 | error!("{}", opt_err); 164 | return PamResultCode::PAM_SYSTEM_ERR; 165 | } 166 | } 167 | 168 | let addr_from_env; 169 | if ssh_agent_addr.is_empty() { 170 | let agent_addr_os = std::env::var_os("SSH_AUTH_SOCK"); 171 | if let Some(a) = agent_addr_os { 172 | addr_from_env = a; 173 | ssh_agent_addr = addr_from_env.to_str().unwrap_or("").to_string(); 174 | } 175 | debug!("SSH-Agent address: {}", ssh_agent_addr); 176 | if ssh_agent_addr.is_empty() { 177 | error!("SSH agent socket address not configured"); 178 | return PamResultCode::PAM_AUTHINFO_UNAVAIL; 179 | } 180 | } 181 | 182 | let authorized_keys_result = if authorized_keys_command.is_empty() { 183 | read_authorized_keys(pamh, &auth_key_file) 184 | } else { 185 | retrieve_authorized_keys_from_cmd(pamh, &authorized_keys_command, &authorized_keys_command_user) 186 | }; 187 | let authorized_keys = match authorized_keys_result { 188 | Ok(u) => { 189 | info!("Got {} entries from authorized_keys", u.len()); 190 | u 191 | }, 192 | Err(e) => { 193 | error!("read_authorized_keys: {}", e); 194 | return PamResultCode::PAM_CRED_INSUFFICIENT; 195 | } 196 | }; 197 | 198 | let send_prompt = || -> PamResult<()> { 199 | if cue { 200 | pamh.get_item::()? 201 | .ok_or(PamResultCode::PAM_BUF_ERR)? 202 | .send(PAM_TEXT_INFO, &cue_prompt)?; 203 | } 204 | Ok(()) 205 | }; 206 | 207 | let mut agent = ssh_agent_auth::AgentClient::new(ssh_agent_addr.as_str()); 208 | let result = agent.list_identities().map(|client_keys| { 209 | debug!("SSH-Agent reports {} keys", client_keys.len()); 210 | for (i, key) in client_keys.iter().enumerate() { 211 | if !is_key_authorized(&key, &authorized_keys) { 212 | debug!("Key[{}] {} is not authorized", i, key.key_type()); 213 | continue; 214 | } 215 | debug!("Key[{}] is authorized", i); 216 | if let Err(_) = send_prompt() { 217 | warn!("{}", RsshErr::SendPromptErr); 218 | } 219 | match authenticate_via_agent(&mut agent, &key) { 220 | Ok(_) => { 221 | info!("Successful authentication"); 222 | return true; 223 | } 224 | Err(e) => { 225 | warn!("Failed to authenticate key {}: {}", i, e); 226 | continue; // try next key 227 | } 228 | } 229 | } 230 | warn!("None of these keys passed authentication"); 231 | false 232 | }); 233 | match result { 234 | Ok(true) => PamResultCode::PAM_SUCCESS, 235 | Ok(false) => PamResultCode::PAM_AUTH_ERR, 236 | Err(e) => { 237 | error!("{}", e); 238 | PamResultCode::PAM_AUTH_ERR 239 | } 240 | } 241 | } 242 | 243 | // Always return PAM_SUCCESS for sm_setcred, just like pam-u2f 244 | fn sm_setcred(_pamh: &mut PamHandle, _args: Vec<&CStr>, _flags: PamFlag) -> PamResultCode { 245 | info!("set-credentials is not implemented"); 246 | PamResultCode::PAM_SUCCESS 247 | } 248 | } 249 | 250 | #[cfg(test)] 251 | mod tests { 252 | use super::ssh_agent_auth::AgentClient; 253 | use log::debug; 254 | 255 | fn init_log() { 256 | let _ = log::set_boxed_logger(Box::new(super::logger::ConsoleLogger)) 257 | .map(|()| log::set_max_level(log::LevelFilter::Info)); 258 | } 259 | 260 | fn enable_debug_log() { 261 | log::set_max_level(log::LevelFilter::Debug) 262 | } 263 | 264 | #[test] 265 | fn sshagent_list_identities() { 266 | init_log(); 267 | enable_debug_log(); 268 | let mut agent = AgentClient::new(env!("SSH_AUTH_SOCK")); 269 | let result = agent.list_identities(); 270 | debug!("result={:?}", result); 271 | assert!(result.is_ok()); 272 | let keys = result.unwrap(); 273 | assert!(keys.len() > 0); 274 | for item in keys { 275 | debug!("key: {:?}", item); 276 | } 277 | } 278 | 279 | #[test] 280 | fn sshagent_auth() { 281 | init_log(); 282 | enable_debug_log(); 283 | let mut agent = AgentClient::new(env!("SSH_AUTH_SOCK")); 284 | let result = agent.list_identities(); 285 | debug!("result={:?}", result); 286 | assert!(result.is_ok()); 287 | let keys = result.unwrap(); 288 | assert!(keys.len() > 0); 289 | for item in keys { 290 | let data: &[u8] = &[3, 5, 6, 7]; 291 | let sig_ret = agent.sign_data(data, &item); 292 | debug!("sig_ret={:?}", sig_ret); 293 | assert!(sig_ret.is_ok()); 294 | let sig = sig_ret.unwrap(); 295 | let verify_ret = super::sign_verify::verify_signature(&data, &item, &sig); 296 | debug!("verify_ret={:?}", verify_ret); 297 | assert!(verify_ret.is_ok()); 298 | assert!(verify_ret.unwrap()); 299 | } 300 | } 301 | 302 | #[test] 303 | fn sshagent_more_auth() { 304 | init_log(); 305 | let mut agent = AgentClient::new(env!("SSH_AUTH_SOCK")); 306 | let result = agent.list_identities(); 307 | assert!(result.is_ok()); 308 | let keys = result.unwrap(); 309 | assert!(keys.len() > 0); 310 | for _ in 0..1000 { 311 | for item in &keys { 312 | let auth_ret = super::authenticate_via_agent(&mut agent, &item); 313 | assert!(auth_ret.is_ok()); 314 | } 315 | } 316 | } 317 | 318 | #[test] 319 | fn parse_user_authorized_keys() { 320 | init_log(); 321 | enable_debug_log(); 322 | let username = env!("USER"); 323 | let result = super::auth_keys::parse_user_authorized_keys(&username); 324 | assert!(result.is_ok()); 325 | let keys = result.unwrap(); 326 | assert!(keys.len() > 0); 327 | for item in keys { 328 | debug!("key: {:?}", item); 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use log::{Metadata, Record}; 2 | 3 | pub struct ConsoleLogger; 4 | 5 | impl log::Log for ConsoleLogger { 6 | fn enabled(&self, metadata: &Metadata) -> bool { 7 | metadata.level() <= log::max_level() 8 | } 9 | 10 | fn log(&self, record: &Record) { 11 | if self.enabled(record.metadata()) { 12 | println!("{} - {}", record.level(), record.args()); 13 | } 14 | } 15 | 16 | fn flush(&self) {} 17 | } 18 | -------------------------------------------------------------------------------- /src/pam_items.rs: -------------------------------------------------------------------------------- 1 | 2 | use pam::constants::PamResultCode; 3 | use pam::module::{PamHandle, PamResult}; 4 | use pam::items; 5 | use subst::VariableMap; 6 | 7 | pub struct PamItemsMap<'a> { 8 | pam: &'a PamHandle, 9 | } 10 | 11 | impl<'a> VariableMap<'a> for PamItemsMap<'a> { 12 | type Value = String; 13 | fn get(&'a self, key: &str) -> Option { 14 | self.get_str_val(key).ok() 15 | } 16 | } 17 | 18 | impl<'a> PamItemsMap<'a> { 19 | pub fn new(pam_handle: &PamHandle) ->PamItemsMap { 20 | PamItemsMap { pam: pam_handle } 21 | } 22 | fn get_str_val(&'a self, key: &str) -> PamResult { 23 | let str_val = match key { 24 | "service" => { 25 | let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; 26 | v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? 27 | } 28 | "user" => { 29 | let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; 30 | v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? 31 | } 32 | "tty" => { 33 | let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; 34 | v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? 35 | } 36 | "rhost" => { 37 | let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; 38 | v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? 39 | } 40 | "ruser" => { 41 | let v = self.pam.get_item::()?.ok_or(PamResultCode::PAM_BUF_ERR)?; 42 | v.to_str().or(Err(PamResultCode::PAM_BUF_ERR))? 43 | } 44 | _ => { 45 | return Err(PamResultCode::PAM_BAD_ITEM) 46 | } 47 | }; 48 | Ok(str_val.to_string()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/sign_verify.rs: -------------------------------------------------------------------------------- 1 | use log::*; 2 | use openssl::ec::{EcGroup, EcKey, EcPoint}; 3 | use openssl::hash::MessageDigest; 4 | use openssl::nid::Nid; 5 | use openssl::pkey::{PKey, Public}; 6 | use ssh_agent::proto; 7 | use ssh_agent::proto::public_key::PublicKey; 8 | use ssh_agent::proto::from_bytes; 9 | 10 | use super::error::RsshErr; 11 | 12 | type ErrType = Box; 13 | 14 | trait ToOpensslKey { 15 | type OpensslKeyResult; 16 | fn to_pkey(&self) -> Self::OpensslKeyResult; 17 | } 18 | impl ToOpensslKey for PublicKey { 19 | type OpensslKeyResult = Result<(PKey, Option), ErrType>; 20 | 21 | fn to_pkey(&self) -> Self::OpensslKeyResult { 22 | let parse_ecdsa = |identifier: &String, q: &Vec| -> Self::OpensslKeyResult { 23 | let (nid, digest) = match identifier.as_str() { 24 | "nistp256" => (Nid::X9_62_PRIME256V1, Some(MessageDigest::sha256())), 25 | "nistp384" => (Nid::SECP384R1, Some(MessageDigest::sha384())), 26 | "nistp521" => (Nid::SECP521R1, Some(MessageDigest::sha512())), 27 | _ => return Err(RsshErr::ParsePubkeyErr.into_ptr()), 28 | }; 29 | let group = EcGroup::from_curve_name(nid)?; 30 | debug!(" Curve group: {}", nid.long_name()?); 31 | let mut ctx = openssl::bn::BigNumContext::new()?; 32 | let pt = EcPoint::from_bytes(&group, &q, &mut ctx)?; 33 | let eckey = EcKey::from_public_key(&group, &pt)?; 34 | eckey.check_key()?; 35 | Ok((PKey::from_ec_key(eckey)?, digest)) 36 | }; 37 | debug!("SSH public key to OpenSSL format:"); 38 | match self { 39 | PublicKey::EcDsa(input) => { 40 | debug!(" ECDSA key identifier={}", input.identifier); 41 | parse_ecdsa(&input.identifier, &input.q) 42 | } 43 | PublicKey::SkEcDsa(input) => { 44 | debug!(" ECDSA security key identifier={}", input.identifier); 45 | parse_ecdsa(&input.identifier, &input.q) 46 | } 47 | PublicKey::Ed25519(input) => { 48 | debug!(" ED25519 key"); 49 | Ok(( 50 | PKey::public_key_from_raw_bytes(&input.enc_a, openssl::pkey::Id::ED25519)?, 51 | None, 52 | )) 53 | } 54 | PublicKey::SkEd25519(input) => { 55 | debug!(" ED25519 security key"); 56 | Ok(( 57 | PKey::public_key_from_raw_bytes(&input.enc_a, openssl::pkey::Id::ED25519)?, 58 | None, 59 | )) 60 | } 61 | PublicKey::Rsa(input) => { 62 | debug!(" RSA key"); 63 | use openssl::bn::BigNum; 64 | use openssl::rsa::Rsa; 65 | let e = BigNum::from_slice(&input.e)?; 66 | let n = BigNum::from_slice(&input.n)?; 67 | let rsa = Rsa::from_public_components(n, e)?; 68 | Ok((PKey::from_rsa(rsa)?, Some(MessageDigest::sha256()))) 69 | } 70 | PublicKey::Dss(input) => { 71 | debug!(" DSA key"); 72 | use openssl::bn::BigNum; 73 | use openssl::dsa::Dsa; 74 | let p = BigNum::from_slice(&input.p)?; 75 | let q = BigNum::from_slice(&input.q)?; 76 | let g = BigNum::from_slice(&input.g)?; 77 | let y = BigNum::from_slice(&input.y)?; 78 | let dsa = Dsa::from_public_components(p, q, g, y)?; 79 | Ok((PKey::from_dsa(dsa)?, Some(MessageDigest::sha1()))) 80 | } 81 | } 82 | } 83 | } 84 | 85 | fn build_asn1_integer(input: &[u8]) -> Vec { 86 | let mut bn = input; 87 | while bn.len() > 1 && bn[0] == 0 { 88 | bn = &bn[1..]; 89 | } 90 | let mut header = if bn[0] & 0x80 == 0 { 91 | vec![0x02, bn.len() as u8] 92 | } else { 93 | vec![0x02, (bn.len() + 1) as u8, 0] 94 | }; 95 | header.extend_from_slice(bn); 96 | header 97 | } 98 | 99 | fn decode_signature_blob(blob: &[u8], pubkey: &PublicKey) -> Result, ErrType> { 100 | match pubkey { 101 | PublicKey::SkEcDsa(_) | PublicKey::EcDsa(_) => { 102 | use openssl::bn::BigNum; 103 | use openssl::ecdsa::EcdsaSig; 104 | 105 | let data: proto::EcDsaSignatureData = from_bytes(&blob)?; 106 | trace!("ECDSA signature: r={:02X?} s={:02X?}", data.r, data.s); 107 | let r = BigNum::from_slice(&data.r)?; 108 | let s = BigNum::from_slice(&data.s)?; 109 | 110 | Ok(EcdsaSig::from_private_components(r, s)?.to_der()?) 111 | } 112 | PublicKey::Dss(_) => { 113 | if blob.len() != 40 { 114 | return Err(RsshErr::InvalidSigErr.into_ptr()); 115 | } 116 | trace!( 117 | "DSA signature: r={:02X?} s={:02X?}", 118 | &blob[..20], 119 | &blob[20..] 120 | ); 121 | // Blob to ASN.1 SEQUENCE(INTEGER,INTEGER) 122 | let mut r = build_asn1_integer(&blob[..20]); 123 | let mut s = build_asn1_integer(&blob[20..]); 124 | let mut seq = vec![0x30, (r.len() + s.len()) as u8]; 125 | seq.append(&mut r); 126 | seq.append(&mut s); 127 | Ok(seq) 128 | } 129 | _ => { 130 | trace!("signature: blob={:02X?}", blob); 131 | Ok(blob.to_vec()) 132 | } 133 | } 134 | } 135 | 136 | fn preprocess_content_and_sig( 137 | msg: &[u8], 138 | ssh_sig: &[u8], 139 | pubkey: &PublicKey, 140 | ) -> Result<(Vec, Vec), ErrType> { 141 | use byteorder::{BigEndian, WriteBytesExt}; 142 | use openssl::sha::sha256; 143 | match pubkey { 144 | // regenerate the message as per U2F spec 145 | PublicKey::SkEcDsa(_) | PublicKey::SkEd25519(_) => { 146 | let sig: proto::SkSignature = from_bytes(ssh_sig)?; 147 | let app = match pubkey { 148 | PublicKey::SkEcDsa(k) => &k.application, 149 | PublicKey::SkEd25519(k) => &k.application, 150 | _ => panic!() 151 | }; 152 | let mut new_msg = Vec::with_capacity(32 + 1 + 4 + 32); 153 | new_msg.extend_from_slice(&sha256(app.as_bytes())); 154 | new_msg.push(sig.flags); 155 | new_msg.write_u32::(sig.counter)?; 156 | new_msg.extend_from_slice(&sha256(msg)); 157 | Ok((new_msg, decode_signature_blob(&sig.blob, pubkey)?)) 158 | } 159 | // "msg" is untouched otherwise 160 | _ => { 161 | let sig: proto::Signature = from_bytes(ssh_sig)?; 162 | Ok((msg.to_vec(), decode_signature_blob(&sig.blob, pubkey)?)) 163 | } 164 | } 165 | } 166 | 167 | pub fn verify_signature( 168 | raw_content: &[u8], 169 | pubkey: &PublicKey, 170 | ssh_signature: &[u8], 171 | ) -> Result { 172 | use openssl::sign::Verifier; 173 | 174 | let (content, signature) = preprocess_content_and_sig(raw_content, ssh_signature, pubkey)?; 175 | let (pkey, digest) = pubkey.to_pkey()?; 176 | let mut verifier = digest.map_or(Verifier::new_without_digest(&pkey), |d| { 177 | Verifier::new(d, &pkey) 178 | })?; 179 | let ret = verifier.verify_oneshot(&signature, &content)?; 180 | 181 | Ok(ret) 182 | } 183 | 184 | pub fn gen_challenge() -> Result, ErrType> { 185 | let mut buffer: Vec = vec![0; 32]; 186 | let buf_ref = buffer.as_mut_slice(); 187 | openssl::rand::rand_bytes(buf_ref)?; 188 | Ok(buffer) 189 | } 190 | 191 | #[test] 192 | fn test_dsa_sign_verify() { 193 | use openssl::dsa::Dsa; 194 | use openssl::hash::MessageDigest; 195 | use openssl::pkey::PKey; 196 | use openssl::sign::{Signer, Verifier}; 197 | 198 | // Generate a keypair 199 | let keypair = Dsa::generate(1024).unwrap(); 200 | let keypair = PKey::from_dsa(keypair).unwrap(); 201 | 202 | let data = b"hello, world!"; 203 | let data2 = b"hola, mundo!"; 204 | 205 | // Sign the data 206 | let mut signer = Signer::new(MessageDigest::sha1(), &keypair).unwrap(); 207 | signer.update(data).unwrap(); 208 | signer.update(data2).unwrap(); 209 | let signature = signer.sign_to_vec().unwrap(); 210 | 211 | println!( 212 | "signature={} len={}", 213 | base64::encode(&signature), 214 | signature.len() 215 | ); 216 | 217 | // Verify the data 218 | let mut verifier = Verifier::new(MessageDigest::sha1(), &keypair).unwrap(); 219 | verifier.update(data).unwrap(); 220 | verifier.update(data2).unwrap(); 221 | assert!(verifier.verify(&signature).unwrap()); 222 | } 223 | -------------------------------------------------------------------------------- /src/ssh_agent_auth.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt}; 2 | use log::*; 3 | use multisock::{SocketAddr, Stream}; 4 | use ssh_agent::proto; 5 | use ssh_agent::proto::public_key::PublicKey; 6 | use ssh_agent::proto::signature; 7 | use ssh_agent::proto::{from_bytes, to_bytes, Message}; 8 | 9 | use std::io::{Read, Write}; 10 | // use std::mem::size_of; 11 | use std::net::Shutdown; 12 | 13 | use super::error::RsshErr; 14 | 15 | type ErrType = Box; 16 | 17 | pub struct AgentClient<'a> { 18 | addr: &'a str, 19 | stream: Option, 20 | } 21 | 22 | static NET_RETRY_CNT: u32 = 3; 23 | 24 | impl<'a> AgentClient<'a> { 25 | pub fn new(addr: &str) -> AgentClient { 26 | AgentClient { addr, stream: None } 27 | } 28 | 29 | fn read_message(stream: &mut Stream) -> Result { 30 | let length = stream.read_u32::()? as usize; 31 | debug!("read_message len={}", length); 32 | let mut buffer: Vec = vec![0; length as usize]; 33 | stream.read_exact(buffer.as_mut_slice())?; 34 | trace!("Read {} bytes: {:02X?}", buffer.len(), buffer); 35 | let msg: Message = from_bytes(buffer.as_slice())?; 36 | Ok(msg) 37 | } 38 | 39 | fn write_message(stream: &mut Stream, msg: &Message) -> Result<(), ErrType> { 40 | let mut bytes = to_bytes(&to_bytes(msg)?)?; 41 | stream.write_all(&mut bytes)?; 42 | trace!("Written {} bytes: {:02X?}", bytes.len(), bytes); 43 | Ok(()) 44 | } 45 | 46 | fn connect(&mut self) -> Result<(), ErrType> { 47 | let addr = if self.addr.starts_with('/') { 48 | String::from("unix:") + self.addr 49 | } else { 50 | String::from(self.addr) 51 | }; 52 | let sockaddr: SocketAddr = addr.parse()?; 53 | if let Some(ref mut s) = self.stream { 54 | let _ = s.shutdown(Shutdown::Both); 55 | self.stream = None; 56 | } 57 | self.stream = Some(Stream::connect(&sockaddr)?); 58 | info!("Connected to {:?}", sockaddr); 59 | Ok(()) 60 | } 61 | 62 | fn call_agent_once(&mut self, cmd: &Message) -> Result { 63 | if self.stream.is_none() { 64 | self.connect()?; 65 | } 66 | let sock = self.stream.as_mut().unwrap(); 67 | Self::write_message(sock, cmd)?; 68 | Self::read_message(sock) 69 | } 70 | 71 | fn call_agent(&mut self, cmd: &Message, retry: u32) -> Result { 72 | let mut ret: Result = Err(RsshErr::RetryLT1Err.into_ptr()); 73 | for _i in 0..retry { 74 | ret = self.call_agent_once(cmd); 75 | if let Ok(val) = ret { 76 | return Ok(val); 77 | } 78 | } 79 | ret 80 | } 81 | 82 | pub fn list_identities(&mut self) -> Result, ErrType> { 83 | let msg = self.call_agent(&Message::RequestIdentities, NET_RETRY_CNT)?; 84 | if let Message::IdentitiesAnswer(keys) = msg { 85 | if keys.len() == 0 { 86 | info!("Please add keys to ssh-agent") 87 | } 88 | let mut result = vec![]; 89 | for item in keys { 90 | debug!( 91 | "list_identities: {:02X?} ({})", 92 | item.pubkey_blob, item.comment 93 | ); 94 | if let Ok(pubkey) = from_bytes(&item.pubkey_blob) { 95 | result.push(pubkey); 96 | } 97 | } 98 | Ok(result) 99 | } else { 100 | Err(RsshErr::InvalidRspErr.into_ptr()) 101 | } 102 | } 103 | 104 | pub fn sign_data<'b>( 105 | &mut self, 106 | data: &'b [u8], 107 | pubkey: &'b PublicKey, 108 | ) -> Result, ErrType> { 109 | let mut flags = 0u32; 110 | match pubkey { 111 | PublicKey::Rsa(_) => flags = signature::RSA_SHA2_256, 112 | _ => {} 113 | } 114 | let args = proto::SignRequest { 115 | pubkey_blob: to_bytes(pubkey)?, 116 | data: data.to_vec(), 117 | flags, 118 | }; 119 | let msg = self.call_agent(&Message::SignRequest(args), NET_RETRY_CNT)?; 120 | if let Message::Failure = msg { 121 | return Err(RsshErr::AgentFailureErr.into_ptr()); 122 | } 123 | if let Message::SignResponse(val) = msg { 124 | // println!("signature payload: {:?}", val); 125 | // if let Ok(mut file) = std::fs::File::create("sign.bin") { 126 | // file.write_all(&val); 127 | // } 128 | Ok(val) 129 | } else { 130 | Err(RsshErr::InvalidRspErr.into_ptr()) 131 | } 132 | } 133 | } 134 | --------------------------------------------------------------------------------