Compare commits

..

1 Commits

Author SHA1 Message Date
Dustin 04e4a6991a ci: Import ci pipeline from original repo
dustin/sshca/pipeline/head There was a failure building this commit Details
When this repository was split from the original *dustin/sshca*
repository, the CI pipeline was not imported.  It wouldn't have mattered
if it had been, since it wouldn't have worked, anyway, given the path
changes.
2023-11-13 20:07:12 -06:00
19 changed files with 163 additions and 1370 deletions

View File

@ -8,8 +8,3 @@ trim_trailing_whitespace = true
[**.rs] [**.rs]
max_line_length = 79 max_line_length = 79
[Jenkinsfile]
max_line_length = 79
indent_style = space
indent_size = 4

677
Cargo.lock generated
View File

@ -61,21 +61,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "argh" name = "argh"
version = "0.1.12" version = "0.1.12"
@ -339,21 +324,6 @@ dependencies = [
"cpufeatures", "cpufeatures",
] ]
[[package]]
name = "chrono"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -370,22 +340,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.11" version = "0.2.11"
@ -440,7 +394,6 @@ dependencies = [
"platforms", "platforms",
"rustc_version", "rustc_version",
"subtle", "subtle",
"zeroize",
] ]
[[package]] [[package]]
@ -454,41 +407,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "darling"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.5.3" version = "5.5.3"
@ -496,7 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"hashbrown 0.14.2", "hashbrown",
"lock_api", "lock_api",
"once_cell", "once_cell",
"parking_lot_core", "parking_lot_core",
@ -509,20 +427,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
dependencies = [ dependencies = [
"const-oid", "const-oid",
"pem-rfc7468",
"zeroize", "zeroize",
] ]
[[package]]
name = "deranged"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
"serde",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@ -556,12 +463,6 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "dyn-clone"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.16.8" version = "0.16.8"
@ -582,7 +483,6 @@ version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [ dependencies = [
"pkcs8",
"signature", "signature",
] ]
@ -594,17 +494,9 @@ checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
dependencies = [ dependencies = [
"curve25519-dalek", "curve25519-dalek",
"ed25519", "ed25519",
"serde",
"sha2", "sha2",
"zeroize",
] ]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "elliptic-curve" name = "elliptic-curve"
version = "0.13.6" version = "0.13.6"
@ -617,8 +509,6 @@ dependencies = [
"ff", "ff",
"generic-array", "generic-array",
"group", "group",
"hkdf",
"pem-rfc7468",
"pkcs8", "pkcs8",
"rand_core", "rand_core",
"sec1", "sec1",
@ -679,21 +569,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form-data-builder" name = "form-data-builder"
version = "1.0.1" version = "1.0.1"
@ -808,10 +683,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -841,31 +714,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "h2"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap 2.1.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.14.2" version = "0.14.2"
@ -896,27 +744,6 @@ dependencies = [
"http", "http",
] ]
[[package]]
name = "hermit-abi"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
dependencies = [
"hmac",
]
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.12.1" version = "0.12.1"
@ -970,7 +797,6 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2",
"http", "http",
"http-body", "http-body",
"httparse", "httparse",
@ -984,69 +810,6 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [
"autocfg",
"hashbrown 0.12.3",
"serde",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.1.0" version = "2.1.0"
@ -1054,8 +817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.14.2", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -1068,21 +830,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "ipnet"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.9"
@ -1229,24 +976,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@ -1305,36 +1034,6 @@ dependencies = [
"libm", "libm",
] ]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "oauth2"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f"
dependencies = [
"base64 0.13.1",
"chrono",
"getrandom",
"http",
"rand",
"reqwest",
"serde",
"serde_json",
"serde_path_to_error",
"sha2",
"thiserror",
"url",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.32.1" version = "0.32.1"
@ -1356,97 +1055,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openidconnect"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62d6050f6a84b81f23c569f5607ad883293e57491036e318fafe6fc4895fadb1"
dependencies = [
"base64 0.13.1",
"chrono",
"dyn-clone",
"ed25519-dalek",
"hmac",
"http",
"itertools",
"log",
"oauth2",
"p256",
"p384",
"rand",
"rsa",
"serde",
"serde-value",
"serde_derive",
"serde_json",
"serde_path_to_error",
"serde_plain",
"serde_with",
"sha2",
"subtle",
"thiserror",
"url",
]
[[package]]
name = "openssl"
version = "0.10.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33"
dependencies = [
"bitflags 2.4.1",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "option-ext" name = "option-ext"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1623,12 +1237,6 @@ dependencies = [
"universal-hash", "universal-hash",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -1756,44 +1364,6 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "reqwest"
version = "0.11.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
dependencies = [
"base64 0.21.5",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"system-configuration",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]] [[package]]
name = "rfc6979" name = "rfc6979"
version = "0.4.0" version = "0.4.0"
@ -1880,15 +1450,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "schannel"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -1909,29 +1470,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.20" version = "1.0.20"
@ -1947,16 +1485,6 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.190" version = "1.0.190"
@ -1989,15 +1517,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_plain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.4" version = "0.6.4"
@ -2019,35 +1538,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
dependencies = [
"base64 0.21.5",
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.1.0",
"serde",
"serde_json",
"serde_with_macros",
"time",
]
[[package]]
name = "serde_with_macros"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "serial_test" name = "serial_test"
version = "2.0.0" version = "2.0.0"
@ -2241,7 +1731,6 @@ dependencies = [
"form-data-builder", "form-data-builder",
"hyper", "hyper",
"jsonwebtoken", "jsonwebtoken",
"openidconnect",
"rand_core", "rand_core",
"serde", "serde",
"serde_json", "serde_json",
@ -2257,12 +1746,6 @@ dependencies = [
"virt", "virt",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"
@ -2286,27 +1769,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.8.1" version = "3.8.1"
@ -2350,50 +1812,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "time"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"itoa",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20"
dependencies = [
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.33.0" version = "1.33.0"
@ -2404,7 +1822,6 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"num_cpus",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2 0.5.5", "socket2 0.5.5",
@ -2423,30 +1840,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.6" version = "0.8.6"
@ -2474,7 +1867,7 @@ version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [ dependencies = [
"indexmap 2.1.0", "indexmap",
"serde", "serde",
"serde_spanned", "serde_spanned",
"toml_datetime", "toml_datetime",
@ -2583,27 +1976,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-bidi"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.12" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]] [[package]]
name = "universal-hash" name = "universal-hash"
version = "0.5.1" version = "0.5.1"
@ -2620,18 +1998,6 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "url"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.5.0" version = "1.5.0"
@ -2644,12 +2010,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.4"
@ -2716,18 +2076,6 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.88" version = "0.2.88"
@ -2789,15 +2137,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.51.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -2873,16 +2212,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys",
]
[[package]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.6.0" version = "1.6.0"

View File

@ -9,7 +9,6 @@ argon2 = { version = "0.5.2", default-features = false, features = ["alloc"] }
axum = { version = "0.6.20", features = ["multipart", "headers", "json"] } axum = { version = "0.6.20", features = ["multipart", "headers", "json"] }
dirs = "5.0.1" dirs = "5.0.1"
jsonwebtoken = { version = "8.3.0", default-features = false } jsonwebtoken = { version = "8.3.0", default-features = false }
openidconnect = { version = "3.4.0", default-features = false, features = ["native-tls", "reqwest"] }
rand_core = { version = "0.6.4", features = ["getrandom"] } rand_core = { version = "0.6.4", features = ["getrandom"] }
serde = { version = "1.0.190", features = ["derive"] } serde = { version = "1.0.190", features = ["derive"] }
serde_json = "1.0.108" serde_json = "1.0.108"

View File

@ -6,7 +6,6 @@ RUN --mount=type=cache,target=/var/cache \
cargo \ cargo \
libvirt-devel \ libvirt-devel \
rust \ rust \
openssl-devel \
&& : && :
COPY . /build COPY . /build

9
ci/Jenkinsfile vendored
View File

@ -40,15 +40,6 @@ pipeline {
} }
} }
} }
stage('Deploy') {
when {
branch 'master'
}
steps {
sh '. ci/deploy.sh'
}
}
} }
} }
} }

View File

@ -2,4 +2,4 @@
. ci/common.sh . ci/common.sh
buildah build -t "${IMAGE_NAME}:${TAG}" . buildah build -t "${IMAGE_NAME}:${TAG}" server

View File

@ -5,7 +5,7 @@ escape_name() {
} }
REGISTRY_URL=git.pyrocufflink.net REGISTRY_URL=git.pyrocufflink.net
NAMESPACE=packages NAMESPACE=containerimages
NAME="${JOB_NAME#*/}" NAME="${JOB_NAME#*/}"
NAME=$(escape_name "${NAME%/*}") NAME=$(escape_name "${NAME%/*}")
TAG=$(escape_name "${BRANCH_NAME}") TAG=$(escape_name "${BRANCH_NAME}")

View File

@ -1,24 +0,0 @@
#!/bin/sh
namespace=sshca
name=sshca
now=$(date +%Y-%m-%dT%H:%M:%S%:z)
curl https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT}/apis/apps/v1/namespaces/${namespace}/deployments/${name} \
--cacert /run/secrets/kubernetes.io/serviceaccount/ca.crt \
-X PATCH \
-H "Authorization: Bearer $(cat /run/secrets/kubernetes.io/serviceaccount/token)" \
-H 'Content-Type: application/merge-patch+json' \
-H 'Accept: application/json' \
-d '{
"spec": {
"template": {
"metadata": {
"annotations": {
"kubectl.kubernetes.io/restartedAt": "'"${now}"'"
}
}
}
}
}'

12
ci/sign-rpms.sh Normal file
View File

@ -0,0 +1,12 @@
#!/bin/sh
gpg2 --pinentry-mode loopback --passphrase-fd 0 \
--import "${RPM_GPG_PRIVATE_KEY}" \
< "${RPM_GPG_KEY_PASSPHRASE}"
rpmsign --addsign \
-D '_gpg_name jenkins@pyrocufflink.net' \
-D '_gpg_sign_cmd_extra_args --pinentry-mode loopback --passphrase-fd 3' \
cli/*.rpm \
3< "${RPM_GPG_KEY_PASSPHRASE}"

View File

@ -19,7 +19,7 @@ use uuid::Uuid;
/// JWT Token Claims /// JWT Token Claims
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct HostClaims { pub struct Claims {
/// Token subject (machine hostname) /// Token subject (machine hostname)
pub sub: String, pub sub: String,
} }
@ -34,7 +34,7 @@ pub fn get_token_subject(token: &str) -> Result<String> {
v.insecure_disable_signature_validation(); v.insecure_disable_signature_validation();
v.set_required_spec_claims(&["sub"]); v.set_required_spec_claims(&["sub"]);
let k = DecodingKey::from_secret(b""); let k = DecodingKey::from_secret(b"");
let data: TokenData<HostClaims> = decode(token, &k, &v)?; let data: TokenData<Claims> = decode(token, &k, &v)?;
Ok(data.claims.sub) Ok(data.claims.sub)
} }
@ -46,12 +46,12 @@ pub fn get_token_subject(token: &str) -> Result<String> {
/// `service` argument, and is within its validity period (not before/expires). /// `service` argument, and is within its validity period (not before/expires).
/// The token must be signed with HMAC-SHA256 using the host's machine ID as /// The token must be signed with HMAC-SHA256 using the host's machine ID as
/// the secret key. /// the secret key.
pub fn validate_host_token( pub fn validate_token(
token: &str, token: &str,
hostname: &str, hostname: &str,
machine_id: &Uuid, machine_id: &Uuid,
service: &str, service: &str,
) -> Result<HostClaims> { ) -> Result<Claims> {
let mut v = Validation::new(Algorithm::HS256); let mut v = Validation::new(Algorithm::HS256);
v.validate_nbf = true; v.validate_nbf = true;
v.set_issuer(&[hostname]); v.set_issuer(&[hostname]);
@ -66,7 +66,7 @@ pub fn validate_host_token(
OsRng.fill_bytes(&mut secret); OsRng.fill_bytes(&mut secret);
} }
let k = DecodingKey::from_secret(&secret); let k = DecodingKey::from_secret(&secret);
let data: TokenData<HostClaims> = decode(token, &k, &v)?; let data: TokenData<Claims> = decode(token, &k, &v)?;
Ok(data.claims) Ok(data.claims)
} }
@ -130,12 +130,7 @@ pub(crate) mod test {
let machine_id = uuid!("9afd42e5-4ac3-4530-90c4-191869063ef9"); let machine_id = uuid!("9afd42e5-4ac3-4530-90c4-191869063ef9");
let token = make_token(hostname, machine_id); let token = make_token(hostname, machine_id);
validate_host_token( validate_token(&token, hostname, &machine_id, "sshca.example.org")
&token,
hostname,
&machine_id,
"sshca.example.org",
)
.unwrap(); .unwrap();
} }
} }

View File

@ -137,7 +137,7 @@ pub fn parse_public_key(data: &[u8]) -> Result<PublicKey, LoadKeyError> {
/// This function creates a signed certificate for an SSH host public /// This function creates a signed certificate for an SSH host public
/// key. The certificate will be valid for the specified hostname and /// key. The certificate will be valid for the specified hostname and
/// any alias names provided. /// any alias names provided.
pub fn sign_host_cert( pub fn sign_cert(
hostname: &str, hostname: &str,
pubkey: &PublicKey, pubkey: &PublicKey,
duration: Duration, duration: Duration,
@ -160,38 +160,6 @@ pub fn sign_host_cert(
Ok(builder.sign(privkey)?) Ok(builder.sign(privkey)?)
} }
/// Create a signed SSH certificate for a user public key
///
/// This function creates a signed certificate for an SSH user public
/// key. The certificate will be valid for the specified username and
/// any alias names provided.
pub fn sign_user_cert(
username: &str,
pubkey: &PublicKey,
duration: Duration,
privkey: &PrivateKey,
alias: &[&str],
extensions: &[impl AsRef<str>],
) -> Result<Certificate, CertError> {
let now = SystemTime::now();
let not_before = now.duration_since(UNIX_EPOCH)?.as_secs();
let not_after = not_before + duration.as_secs();
let mut builder = Builder::new_with_random_nonce(
&mut OsRng, pubkey, not_before, not_after,
)?;
builder.cert_type(CertType::User)?;
builder.valid_principal(username)?;
for a in alias {
builder.valid_principal(*a)?;
}
for e in extensions {
builder.extension(e.as_ref(), "")?;
}
Ok(builder.sign(privkey)?)
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use ssh_key::Algorithm; use ssh_key::Algorithm;
@ -207,9 +175,9 @@ mod test {
let host_pub_key = host_key.public_key(); let host_pub_key = host_key.public_key();
let duration = Duration::from_secs(86400 * 30); let duration = Duration::from_secs(86400 * 30);
let hostname = "cloud0.example.org"; let hostname = "cloud0.example.org";
let cert = sign_host_cert( let cert = sign_cert(
hostname, hostname,
host_pub_key, &host_pub_key,
duration, duration,
&ca_key, &ca_key,
&["nextcloud.example.org"], &["nextcloud.example.org"],

View File

@ -1,5 +1,4 @@
//! Application configuration //! Application configuration
use std::collections::HashMap;
use std::io::ErrorKind; use std::io::ErrorKind;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -66,57 +65,11 @@ impl Default for HostCaConfig {
} }
} }
#[derive(Debug, Deserialize)]
pub struct UserCaConfig {
/// Path to the User CA private key file
#[serde(default = "default_user_ca_key")]
pub private_key_file: PathBuf,
pub private_key_passphrase_file: Option<PathBuf>,
/// Duration of issued user certificates
#[serde(default = "default_user_cert_duration")]
pub cert_duration: u64,
/// Certificate extensions
#[serde(default = "default_user_cert_extensions")]
pub extensions: Vec<String>,
/// Additional principals to add based on user's group membership
#[serde(default)]
pub group_principals: HashMap<String, Vec<String>>
}
impl Default for UserCaConfig {
fn default() -> Self {
Self {
private_key_file: default_user_ca_key(),
private_key_passphrase_file: None,
cert_duration: default_user_cert_duration(),
extensions: default_user_cert_extensions(),
group_principals: Default::default(),
}
}
}
/// CA configuration /// CA configuration
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Deserialize)]
pub struct CaConfig { pub struct CaConfig {
/// Host CA configuration /// Host CA configuration
pub host: HostCaConfig, pub host: HostCaConfig,
/// User CA configuration
pub user: UserCaConfig,
}
/// OpenID Connect configuration
#[derive(Debug, Deserialize)]
pub struct OidcConfig {
/// OIDC Discovery base URL (without /.well-known/...)
pub discovery_url: String,
/// OAuth2 client ID
pub client_id: String,
/// OAuth2 client secret
#[serde(default)]
pub client_secret: Option<String>,
} }
/// Defines a connection to a libvirt VM host /// Defines a connection to a libvirt VM host
@ -138,9 +91,6 @@ pub struct Configuration {
/// CA configuration /// CA configuration
#[serde(default)] #[serde(default)]
pub ca: CaConfig, pub ca: CaConfig,
/// OpenID Connect configuration for user authorization
#[serde(default)]
pub oidc: Option<OidcConfig>,
} }
impl Default for Configuration { impl Default for Configuration {
@ -149,7 +99,6 @@ impl Default for Configuration {
libvirt: vec![], libvirt: vec![],
machine_ids: default_machine_ids(), machine_ids: default_machine_ids(),
ca: Default::default(), ca: Default::default(),
oidc: Default::default(),
} }
} }
} }
@ -174,24 +123,6 @@ fn default_host_cert_duration() -> u64 {
86400 * 30 86400 * 30
} }
fn default_user_ca_key() -> PathBuf {
default_config_path("user-ca.key")
}
fn default_user_cert_duration() -> u64 {
3600
}
fn default_user_cert_extensions() -> Vec<String> {
vec![
"permit-X11-forwarding".into(),
"permit-agent-forwarding".into(),
"permit-port-forwarding".into(),
"permit-pty".into(),
"permit-user-rc".into(),
]
}
/// Load configuration from a TOML file /// Load configuration from a TOML file
/// ///
/// If `path` is provided, the configuration will be loaded from the /// If `path` is provided, the configuration will be loaded from the

View File

@ -1,75 +0,0 @@
//! SSHCA HTTP server errors
use axum::extract::multipart::MultipartError;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use tracing::{debug, error};
use crate::ca;
/// Error encountered while signing a key
pub enum SignKeyError {
/// Failed to parse HTTP multipart form request
Multipart(MultipartError),
/// Error handing SSH certificate
Cert(ca::CertError),
/// Error loading SSH private key file
LoadPrivateKey(ca::LoadKeyError),
/// Error parsing SSH public key
ParsePublicKey(ca::LoadKeyError),
/// Unsupported SSH key algorithm
UnsupportedAlgorithm(String),
/// No SSH public key included in request
NoKey,
}
impl From<MultipartError> for SignKeyError {
fn from(e: MultipartError) -> Self {
Self::Multipart(e)
}
}
impl From<ca::CertError> for SignKeyError {
fn from(e: ca::CertError) -> Self {
Self::Cert(e)
}
}
impl IntoResponse for SignKeyError {
fn into_response(self) -> Response {
match self {
Self::Multipart(e) => {
debug!("Error reading request: {}", e);
let body = e.to_string();
(StatusCode::BAD_REQUEST, body).into_response()
}
Self::Cert(e) => {
error!("Failed to sign certificate: {}", e);
let body = "Service Unavailable";
(StatusCode::SERVICE_UNAVAILABLE, body).into_response()
}
Self::LoadPrivateKey(e) => {
error!("Error loading CA private key: {}", e);
let body = "Service Unavailable";
(StatusCode::SERVICE_UNAVAILABLE, body).into_response()
}
Self::ParsePublicKey(e) => {
error!("Error parsing public keykey: {}", e);
let body = e.to_string();
(StatusCode::BAD_REQUEST, body).into_response()
}
Self::UnsupportedAlgorithm(a) => {
debug!("Requested certificate for unsupported key algorithm \"{}\"", a);
let body = format!("Unsupported key algorithm: {}", a);
(StatusCode::BAD_REQUEST, body).into_response()
}
Self::NoKey => {
debug!("No SSH public key provided in request");
(
StatusCode::BAD_REQUEST,
"No SSH public key provided in request",
)
.into_response()
}
}
}
}

View File

@ -1,25 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::time::Duration;
use std::time::{Duration, Instant};
use axum::async_trait; use axum::extract::multipart::{Multipart, MultipartError};
use axum::extract::multipart::Multipart;
use axum::extract::FromRequestParts;
use axum::extract::State; use axum::extract::State;
use axum::headers::authorization::Bearer; use axum::http::StatusCode;
use axum::headers::{Authorization, Host}; use axum::response::{IntoResponse, Response};
use axum::http::request::Parts;
use axum::{RequestPartsExt, TypedHeader};
use serde::Serialize; use serde::Serialize;
use ssh_key::Algorithm; use ssh_key::Algorithm;
use tracing::{debug, info, warn}; use tracing::{debug, error, info, warn};
use uuid::Uuid;
use super::error::SignKeyError; use crate::auth::Claims;
use super::{AuthError, Context};
use crate::auth::{self, HostClaims};
use crate::ca; use crate::ca;
use crate::machine_id;
#[derive(Serialize)] #[derive(Serialize)]
pub struct SignKeyResponse { pub struct SignKeyResponse {
@ -28,68 +19,65 @@ pub struct SignKeyResponse {
certificates: HashMap<String, String>, certificates: HashMap<String, String>,
} }
#[async_trait] pub enum SignKeyError {
impl FromRequestParts<Arc<Context>> for HostClaims { Multipart(MultipartError),
type Rejection = AuthError; Cert(ca::CertError),
LoadPrivateKey(ca::LoadKeyError),
ParsePublicKey(ca::LoadKeyError),
UnsupportedAlgorithm(String),
NoKey,
}
async fn from_request_parts( impl From<MultipartError> for SignKeyError {
parts: &mut Parts, fn from(e: MultipartError) -> Self {
ctx: &super::State, Self::Multipart(e)
) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|e| {
debug!("Failed to extract token from HTTP request: {}", e);
AuthError
})?;
let host = parts.extract::<TypedHeader<Host>>().await.map_or_else(
|_| "localhost".to_owned(),
|v| v.hostname().to_owned(),
);
let hostname =
auth::get_token_subject(bearer.token()).map_err(|e| {
debug!("Could not get token subject: {}", e);
AuthError
})?;
let machine_id =
get_machine_id(&hostname, ctx).await.ok_or_else(|| {
debug!("No machine ID found for host {}", hostname);
AuthError
})?;
let claims = auth::validate_host_token(
bearer.token(),
&hostname,
&machine_id,
&host,
)
.map_err(|e| {
debug!("Invalid auth token: {}", e);
AuthError
})?;
debug!("Successfully authenticated request from host {}", hostname);
Ok(claims)
} }
} }
async fn get_machine_id(hostname: &str, ctx: &super::State) -> Option<Uuid> { impl From<ca::CertError> for SignKeyError {
let cache = ctx.cache.read().await; fn from(e: ca::CertError) -> Self {
if let Some((ts, m)) = cache.get(hostname) { Self::Cert(e)
if ts.elapsed() < Duration::from_secs(60) { }
debug!("Found cached machine ID for {}", hostname); }
return Some(*m);
} else { impl IntoResponse for SignKeyError {
debug!("Cached machine ID for {} has expired", hostname); fn into_response(self) -> Response {
match self {
Self::Multipart(e) => {
debug!("Error reading request: {}", e);
let body = e.to_string();
(StatusCode::BAD_REQUEST, body).into_response()
}
Self::Cert(e) => {
error!("Failed to sign certificate: {}", e);
let body = "Service Unavailable";
(StatusCode::SERVICE_UNAVAILABLE, body).into_response()
}
Self::LoadPrivateKey(e) => {
error!("Error loading CA private key: {}", e);
let body = "Service Unavailable";
(StatusCode::SERVICE_UNAVAILABLE, body).into_response()
}
Self::ParsePublicKey(e) => {
error!("Error parsing public keykey: {}", e);
let body = e.to_string();
(StatusCode::BAD_REQUEST, body).into_response()
}
Self::UnsupportedAlgorithm(a) => {
debug!("Requested certificate for unsupported key algorithm \"{}\"", a);
let body = format!("Unsupported key algorithm: {}", a);
(StatusCode::BAD_REQUEST, body).into_response()
}
Self::NoKey => {
debug!("No SSH public key provided in request");
(
StatusCode::BAD_REQUEST,
"No SSH public key provided in request",
)
.into_response()
}
} }
} }
drop(cache);
let machine_id =
machine_id::get_machine_id(hostname, ctx.config.clone()).await?;
let mut cache = ctx.cache.write().await;
debug!("Caching machine ID for {}", hostname);
cache.insert(hostname.into(), (Instant::now(), machine_id));
Some(machine_id)
} }
#[derive(Default)] #[derive(Default)]
@ -99,7 +87,7 @@ struct SignKeyRequest {
} }
pub(super) async fn sign_host_cert( pub(super) async fn sign_host_cert(
claims: HostClaims, claims: Claims,
State(ctx): State<super::State>, State(ctx): State<super::State>,
mut form: Multipart, mut form: Multipart,
) -> Result<String, SignKeyError> { ) -> Result<String, SignKeyError> {
@ -149,7 +137,7 @@ pub(super) async fn sign_host_cert(
hostname hostname
); );
let cert = let cert =
ca::sign_host_cert(&hostname, &pubkey, duration, &privkey, &[])?; ca::sign_cert(&hostname, &pubkey, duration, &privkey, &[])?;
info!( info!(
"Signed {} key for {}", "Signed {} key for {}",
pubkey.algorithm().as_str(), pubkey.algorithm().as_str(),

View File

@ -1,26 +1,29 @@
mod error;
mod host; mod host;
mod oidc;
mod user;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::{Duration, Instant};
use axum::async_trait;
use axum::extract::FromRequestParts;
use axum::headers::authorization::Bearer;
use axum::headers::{Authorization, Host};
use axum::http::request::Parts;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::Router; use axum::{RequestPartsExt, Router, TypedHeader};
use openidconnect::core::CoreProviderMetadata;
use tokio::sync::RwLock; use tokio::sync::RwLock;
use tracing::debug;
use uuid::Uuid; use uuid::Uuid;
use crate::auth::{self, Claims};
use crate::config::Configuration; use crate::config::Configuration;
use crate::machine_id;
struct Context { struct Context {
config: Arc<Configuration>, config: Arc<Configuration>,
cache: RwLock<HashMap<String, (Instant, Uuid)>>, cache: RwLock<HashMap<String, (Instant, Uuid)>>,
oidc: RwLock<Option<(Instant, CoreProviderMetadata)>>,
} }
type State = Arc<Context>; type State = Arc<Context>;
@ -33,21 +36,81 @@ impl IntoResponse for AuthError {
} }
} }
#[async_trait]
impl FromRequestParts<Arc<Context>> for Claims {
type Rejection = AuthError;
async fn from_request_parts(
parts: &mut Parts,
ctx: &State,
) -> Result<Self, Self::Rejection> {
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|e| {
debug!("Failed to extract token from HTTP request: {}", e);
AuthError
})?;
let host = parts.extract::<TypedHeader<Host>>().await.map_or_else(
|_| "localhost".to_owned(),
|v| v.hostname().to_owned(),
);
let hostname =
auth::get_token_subject(bearer.token()).map_err(|e| {
debug!("Could not get token subject: {}", e);
AuthError
})?;
let machine_id =
get_machine_id(&hostname, ctx).await.ok_or_else(|| {
debug!("No machine ID found for host {}", hostname);
AuthError
})?;
let claims = auth::validate_token(
bearer.token(),
&hostname,
&machine_id,
&host,
)
.map_err(|e| {
debug!("Invalid auth token: {}", e);
AuthError
})?;
debug!("Successfully authenticated request from host {}", hostname);
Ok(claims)
}
}
pub fn make_app(config: Configuration) -> Router { pub fn make_app(config: Configuration) -> Router {
let ctx = Arc::new(Context { let ctx = Arc::new(Context {
config: config.into(), config: config.into(),
cache: RwLock::new(Default::default()), cache: RwLock::new(Default::default()),
oidc: Default::default(),
}); });
Router::new() Router::new()
.route("/", get(|| async { "UP" })) .route("/", get(|| async { "UP" }))
.route("/host/sign", post(host::sign_host_cert)) .route("/host/sign", post(host::sign_host_cert))
.route("/user/oidc-config", get(user::get_oidc_config))
.route("/user/sign", post(user::sign_user_cert))
.route("/user/ca", get(user::get_ca_pubkey))
.with_state(ctx) .with_state(ctx)
} }
async fn get_machine_id(hostname: &str, ctx: &State) -> Option<Uuid> {
let cache = ctx.cache.read().await;
if let Some((ts, m)) = cache.get(hostname) {
if ts.elapsed() < Duration::from_secs(60) {
debug!("Found cached machine ID for {}", hostname);
return Some(*m);
} else {
debug!("Cached machine ID for {} has expired", hostname);
}
}
drop(cache);
let machine_id =
machine_id::get_machine_id(hostname, ctx.config.clone()).await?;
let mut cache = ctx.cache.write().await;
debug!("Caching machine ID for {}", hostname);
cache.insert(hostname.into(), (Instant::now(), machine_id));
Some(machine_id)
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use axum::body::Body; use axum::body::Body;

View File

@ -1,55 +0,0 @@
use openidconnect::core::*;
use openidconnect::*;
use serde::{Deserialize, Serialize};
pub type IdTokenFields = openidconnect::IdTokenFields<
AdditionalClaims,
EmptyExtraTokenFields,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
>;
pub type IdToken = openidconnect::IdToken<
AdditionalClaims,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
>;
pub type IdTokenClaims =
openidconnect::IdTokenClaims<AdditionalClaims, CoreGenderClaim>;
pub type TokenResponse = StandardTokenResponse<IdTokenFields, CoreTokenType>;
pub type Client = openidconnect::Client<
AdditionalClaims,
CoreAuthDisplay,
CoreGenderClaim,
CoreJweContentEncryptionAlgorithm,
CoreJwsSigningAlgorithm,
CoreJsonWebKeyType,
CoreJsonWebKeyUse,
CoreJsonWebKey,
CoreAuthPrompt,
StandardErrorResponse<CoreErrorResponseType>,
TokenResponse,
CoreTokenType,
CoreTokenIntrospectionResponse,
CoreRevocableToken,
CoreRevocationErrorResponse,
>;
#[derive(Serialize, Deserialize, Debug)]
pub struct AdditionalClaims {
groups: Vec<String>,
}
impl AdditionalClaims {
pub fn groups(&self) -> &Vec<String> {
&self.groups
}
}
impl openidconnect::AdditionalClaims for AdditionalClaims {}

View File

@ -1,284 +0,0 @@
//! User CA operations
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::async_trait;
use axum::extract::multipart::Multipart;
use axum::extract::FromRequestParts;
use axum::extract::State;
use axum::headers::authorization::Bearer;
use axum::headers::Authorization;
use axum::http::request::Parts;
use axum::Json;
use axum::{RequestPartsExt, TypedHeader};
use openidconnect::core::CoreProviderMetadata;
use openidconnect::reqwest::async_http_client;
use openidconnect::IssuerUrl;
use openidconnect::Nonce;
use openidconnect::{ClientId, ClientSecret};
use serde::Serialize;
use ssh_key::Algorithm;
use tracing::{debug, error, info, trace, warn};
use super::error::SignKeyError;
use super::oidc;
use super::{AuthError, Context};
use crate::ca;
/// Response type for GET /user/openid-config
///
/// This structure contains OpenID configuration information for
/// client utilities.
#[derive(Debug, Default, Serialize)]
pub struct OidcConfigResponse {
url: Option<String>,
client_id: Option<String>,
client_secret: Option<String>,
}
/// OpenID Connect ID token claims
pub struct Claims(oidc::IdTokenClaims);
/// Axum request extractor for OIDC ID tokens in Authorization headers
///
/// This extractor parses an OpenID Connect identity token (as a JWT)
/// from the `Authorization` HTTP request header. If the token is
/// valid, a [`Claims`] structure is returned. If the token is not
/// valid (e.g. signed by an untrusted key, expired, for a different
/// audience, etc), [`AuthError`] is returned.
#[async_trait]
impl FromRequestParts<Arc<Context>> for Claims {
type Rejection = AuthError;
async fn from_request_parts(
parts: &mut Parts,
ctx: &super::State,
) -> Result<Self, Self::Rejection> {
let config = &ctx.config;
let oidc_config = &config.oidc.as_ref().ok_or_else(|| {
warn!("OpenID Connect not configured");
AuthError
})?;
let TypedHeader(Authorization(bearer)) = parts
.extract::<TypedHeader<Authorization<Bearer>>>()
.await
.map_err(|e| {
debug!("Failed to extract token from HTTP request: {}", e);
AuthError
})?;
let token = oidc::IdToken::from_str(bearer.token()).map_err(|e| {
debug!("Failed to parse OIDC ID token: {}", e);
AuthError
})?;
let client_id = &oidc_config.client_id;
let client_secret = &oidc_config.client_secret;
let provider_metadata = get_metadata(ctx).await.ok_or(AuthError)?;
let client = oidc::Client::from_provider_metadata(
provider_metadata,
ClientId::new(client_id.into()),
client_secret.as_ref().map(|s| ClientSecret::new(s.into())),
);
let verifier = client.id_token_verifier();
let claims = token
// Ignore the token nonce, as we have no way of validating it.
.into_claims(&verifier, |_: Option<&Nonce>| Ok(()))
.map_err(|e| {
debug!("Invalid ID token: {}", e);
AuthError
})?;
trace!("Token Claims: {:?}", claims);
debug!(
"Successfully authorized user {} (issuer {})",
claims.subject().as_str(),
claims.issuer().as_str()
);
Ok(Claims(claims))
}
}
/// Retrieve OpenID Connect client configuration
///
/// Clients can access this resource to determine the appropriate
/// configuration for the OpenID Connect identity provider where they
/// can obtain identity tokens.
pub(super) async fn get_oidc_config(
State(ctx): State<super::State>,
) -> Json<OidcConfigResponse> {
let config = &ctx.config;
let res = if let Some(oidc) = &config.oidc {
OidcConfigResponse {
url: Some(oidc.discovery_url.clone()),
client_id: Some(oidc.client_id.clone()),
client_secret: oidc.client_secret.clone(),
}
} else {
Default::default()
};
Json(res)
}
/// User SSH key signing request payload
#[derive(Default)]
struct SignKeyRequest {
/// Public keys to sign
pubkey: Vec<u8>,
}
/// Handler for user certificate signing requests
///
/// An SSH user certificate will be signed for each submitted public
/// key. The valid principals on the certificates will be taken from
/// the OpenID Connect Identity Token in the Authorization header, via
/// the `sub`, `perferred_username`, and `email` claims (if present).
pub(super) async fn sign_user_cert(
Claims(claims): Claims,
State(ctx): State<super::State>,
mut form: Multipart,
) -> Result<String, SignKeyError> {
let username = claims.subject().as_str();
let mut body = SignKeyRequest::default();
while let Some(field) = form.next_field().await? {
match field.name() {
Some("pubkey") => {
body.pubkey = field.bytes().await?.into();
}
Some(n) => {
warn!("Client request included unsupported field {:?}", n);
}
None => {}
}
}
if body.pubkey.is_empty() {
return Err(SignKeyError::NoKey);
}
let mut alias = vec![];
if let Some(username) = claims.preferred_username() {
alias.push(username.as_str());
}
if let Some(email) = claims.email() {
if claims.email_verified() == Some(true) {
alias.push(email.as_str());
}
}
let config = &ctx.config;
let duration = Duration::from_secs(config.ca.user.cert_duration);
let extensions = &config.ca.user.extensions;
for group in claims.additional_claims().groups() {
if let Some(principals) = config.ca.user.group_principals.get(group) {
debug!("Adding principals from group {}", group);
for p in principals {
alias.push(p.as_str())
}
}
}
let privkey = ca::load_private_key(
&config.ca.user.private_key_file,
config.ca.user.private_key_passphrase_file.as_ref(),
)
.await
.map_err(SignKeyError::LoadPrivateKey)?;
let pubkey = ca::parse_public_key(&body.pubkey)
.map_err(SignKeyError::ParsePublicKey)?;
match pubkey.algorithm() {
Algorithm::Ecdsa { .. } => (),
Algorithm::Ed25519 => (),
Algorithm::Rsa { .. } => (),
_ => {
return Err(SignKeyError::UnsupportedAlgorithm(
pubkey.algorithm().as_str().into(),
));
}
};
debug!(
"Signing {} key for {}",
pubkey.algorithm().as_str(),
username
);
let cert = ca::sign_user_cert(
username,
&pubkey,
duration,
&privkey,
&alias,
&extensions[..],
)?;
info!(
"Signed {} key for {}",
pubkey.algorithm().as_str(),
username
);
Ok(cert.to_openssh().map_err(ca::CertError::from)?)
}
/// Get the public key of the user CA
///
/// Returns a string representation of the CA public key. This can be
/// used by hosts to find the current public key to trust for
/// authenticating users.
pub(super) async fn get_ca_pubkey(
State(ctx): State<super::State>,
) -> Result<String, SignKeyError> {
let config = &ctx.config;
let privkey = ca::load_private_key(
&config.ca.user.private_key_file,
config.ca.user.private_key_passphrase_file.as_ref(),
)
.await
.map_err(SignKeyError::LoadPrivateKey)?;
let pubkey = privkey.public_key()
.to_openssh()
.map_err(ca::LoadKeyError::SshKey)
.map_err(SignKeyError::LoadPrivateKey)?;
Ok(format!("{}\n", pubkey))
}
/// Get OIDC provider metadata (possibly from cache)
///
/// This function will return metadata for the configured OIDC identity
/// provider. When called for the first time, it will initiate an
/// HTTP request to the provider's OpenID Provider Configuration
/// Document (i.e. `/.well-known/openid-configuration`). The result is
/// cached for 1 hour, so subsequent calls to this function will not
/// initiate another HTTP request, unless more than 1 hour has passed
/// since the first request.
///
/// If an error occurs while attempting to fetch the metadata, `None`
/// is returned.
async fn get_metadata(ctx: &super::State) -> Option<CoreProviderMetadata> {
let cache = ctx.oidc.read().await;
if let Some((ts, m)) = &*cache {
if ts.elapsed() < Duration::from_secs(3600) {
debug!("Using cached OIDC provider metadata");
return Some(m.clone());
}
};
let oidc_url = &ctx.config.oidc.as_ref()?.discovery_url;
debug!("Fetching OIDC provider metadata");
let metadata = CoreProviderMetadata::discover_async(
IssuerUrl::new(oidc_url.into()).unwrap(),
async_http_client,
)
.await;
match metadata {
Ok(m) => {
drop(cache);
debug!("Caching OIDC provider metadata");
let mut cache = ctx.oidc.write().await;
(*cache).replace((Instant::now(), m.clone()));
Some(m)
}
Err(e) => {
error!("Failed to get OIDC provider metadata: {}", e);
None
}
}
}

View File

@ -8,7 +8,7 @@ use ssh_key::{Algorithm, Fingerprint, PrivateKey, PublicKey};
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use sshca::config::{Configuration, OidcConfig}; use sshca::config::Configuration;
static INIT: Once = Once::new(); static INIT: Once = Once::new();
@ -24,11 +24,6 @@ fn gen_machine_ids() -> Result<NamedTempFile, Box<dyn Error>> {
fn gen_config(machine_ids: &Path, host_key: &Path) -> Configuration { fn gen_config(machine_ids: &Path, host_key: &Path) -> Configuration {
let mut config = Configuration { let mut config = Configuration {
machine_ids: machine_ids.to_str().unwrap().into(), machine_ids: machine_ids.to_str().unwrap().into(),
oidc: Some(OidcConfig {
discovery_url: "https://auth.example.org".into(),
client_id: "sshca".into(),
client_secret: None,
}),
..Default::default() ..Default::default()
}; };
config.ca.host.private_key_file = host_key.to_str().unwrap().into(); config.ca.host.private_key_file = host_key.to_str().unwrap().into();

View File

@ -1,34 +0,0 @@
mod common;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;
use sshca::server::make_app;
use common::setup;
#[tokio::test]
async fn test_oidc_config() {
let (_ctx, config) = setup::setup().await.unwrap();
let app = make_app(config);
let res = app
.oneshot(
Request::builder()
.uri("/user/oidc-config")
.method("GET")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let body = hyper::body::to_bytes(res.into_body()).await.unwrap();
let result: serde_json::Value =
serde_json::from_str(std::str::from_utf8(&body).unwrap()).unwrap();
assert_eq!(result["url"].as_str(), Some("https://auth.example.org"));
assert_eq!(result["client_id"].as_str(), Some("sshca"));
assert_eq!(result["client_secret"].as_str(), None);
}