Compare commits
No commits in common. "0.1.0" and "master" have entirely different histories.
|
@ -8,3 +8,8 @@ trim_trailing_whitespace = true
|
|||
|
||||
[**.rs]
|
||||
max_line_length = 79
|
||||
|
||||
[Jenkinsfile]
|
||||
max_line_length = 79
|
||||
indent_style = space
|
||||
indent_size = 4
|
|
@ -61,6 +61,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "argh"
|
||||
version = "0.1.12"
|
||||
|
@ -324,6 +339,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
|
@ -340,6 +370,22 @@ version = "0.9.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.11"
|
||||
|
@ -394,6 +440,7 @@ dependencies = [
|
|||
"platforms",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -407,6 +454,41 @@ dependencies = [
|
|||
"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]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
|
@ -414,7 +496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.2",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
|
@ -427,9 +509,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
|
@ -463,6 +556,12 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.8"
|
||||
|
@ -483,6 +582,7 @@ version = "2.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
|
@ -494,9 +594,17 @@ checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
|
|||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.6"
|
||||
|
@ -509,6 +617,8 @@ dependencies = [
|
|||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"hkdf",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
|
@ -569,6 +679,21 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "form-data-builder"
|
||||
version = "1.0.1"
|
||||
|
@ -683,8 +808,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -714,6 +841,31 @@ dependencies = [
|
|||
"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]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.2"
|
||||
|
@ -744,6 +896,27 @@ dependencies = [
|
|||
"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]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
|
@ -797,6 +970,7 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
|
@ -810,6 +984,69 @@ dependencies = [
|
|||
"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]]
|
||||
name = "indexmap"
|
||||
version = "2.1.0"
|
||||
|
@ -817,7 +1054,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -830,6 +1068,21 @@ dependencies = [
|
|||
"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]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
|
@ -976,6 +1229,24 @@ dependencies = [
|
|||
"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]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
|
@ -1034,6 +1305,36 @@ dependencies = [
|
|||
"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]]
|
||||
name = "object"
|
||||
version = "0.32.1"
|
||||
|
@ -1055,12 +1356,97 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
|
@ -1237,6 +1623,12 @@ dependencies = [
|
|||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
|
@ -1364,6 +1756,44 @@ version = "0.8.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
|
@ -1450,6 +1880,15 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
|
@ -1470,6 +1909,29 @@ dependencies = [
|
|||
"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]]
|
||||
name = "semver"
|
||||
version = "1.0.20"
|
||||
|
@ -1485,6 +1947,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.190"
|
||||
|
@ -1517,6 +1989,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_plain"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.4"
|
||||
|
@ -1538,6 +2019,35 @@ dependencies = [
|
|||
"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]]
|
||||
name = "serial_test"
|
||||
version = "2.0.0"
|
||||
|
@ -1731,6 +2241,7 @@ dependencies = [
|
|||
"form-data-builder",
|
||||
"hyper",
|
||||
"jsonwebtoken",
|
||||
"openidconnect",
|
||||
"rand_core",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -1746,6 +2257,12 @@ dependencies = [
|
|||
"virt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.5.0"
|
||||
|
@ -1769,6 +2286,27 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "tempfile"
|
||||
version = "3.8.1"
|
||||
|
@ -1812,6 +2350,50 @@ dependencies = [
|
|||
"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]]
|
||||
name = "tokio"
|
||||
version = "1.33.0"
|
||||
|
@ -1822,6 +2404,7 @@ dependencies = [
|
|||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.5",
|
||||
|
@ -1840,6 +2423,30 @@ dependencies = [
|
|||
"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]]
|
||||
name = "toml"
|
||||
version = "0.8.6"
|
||||
|
@ -1867,7 +2474,7 @@ version = "0.20.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"indexmap 2.1.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
|
@ -1976,12 +2583,27 @@ version = "1.17.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
|
@ -1998,6 +2620,18 @@ version = "0.7.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "uuid"
|
||||
version = "1.5.0"
|
||||
|
@ -2010,6 +2644,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
|
@ -2076,6 +2716,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.88"
|
||||
|
@ -2137,6 +2789,15 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
|
@ -2212,6 +2873,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
|
@ -9,6 +9,7 @@ argon2 = { version = "0.5.2", default-features = false, features = ["alloc"] }
|
|||
axum = { version = "0.6.20", features = ["multipart", "headers", "json"] }
|
||||
dirs = "5.0.1"
|
||||
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"] }
|
||||
serde = { version = "1.0.190", features = ["derive"] }
|
||||
serde_json = "1.0.108"
|
|
@ -6,6 +6,7 @@ RUN --mount=type=cache,target=/var/cache \
|
|||
cargo \
|
||||
libvirt-devel \
|
||||
rust \
|
||||
openssl-devel \
|
||||
&& :
|
||||
|
||||
COPY . /build
|
|
@ -3,143 +3,54 @@ pipeline {
|
|||
|
||||
stages {
|
||||
stage('SSHCA') {
|
||||
matrix {
|
||||
axes {
|
||||
axis {
|
||||
name 'COMPONENT'
|
||||
values 'client', 'server'
|
||||
}
|
||||
axis {
|
||||
name 'ARCH'
|
||||
values 'amd64', 'arm64'
|
||||
}
|
||||
}
|
||||
|
||||
excludes {
|
||||
exclude {
|
||||
axis {
|
||||
name 'COMPONENT'
|
||||
values 'server'
|
||||
}
|
||||
axis {
|
||||
name 'ARCH'
|
||||
values 'arm64'
|
||||
stages {
|
||||
stage('Server') {
|
||||
agent {
|
||||
kubernetes {
|
||||
yamlFile 'ci/podTemplate.yaml'
|
||||
yamlMergeStrategy merge()
|
||||
defaultContainer 'buildah'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Server') {
|
||||
when {
|
||||
expression {
|
||||
env.COMPONENT == 'server'
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh '. ci/build.sh'
|
||||
}
|
||||
}
|
||||
agent {
|
||||
kubernetes {
|
||||
yamlFile 'ci/serverPodTemplate.yaml'
|
||||
yamlMergeStrategy merge()
|
||||
defaultContainer 'buildah'
|
||||
}
|
||||
}
|
||||
stages {
|
||||
stage('Build - Server') {
|
||||
steps {
|
||||
sh '. ci/build-server.sh'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish - Server') {
|
||||
steps {
|
||||
withEnv([
|
||||
"REGISTRY_AUTH_FILE=${env.WORKSPACE_TMP}/auth.json"
|
||||
]) {
|
||||
withCredentials([usernamePassword(
|
||||
credentialsId: 'jenkins-packages',
|
||||
usernameVariable: 'BUILDAH_USERNAME',
|
||||
passwordVariable: 'BUILDAH_PASSWORD',
|
||||
)]) {
|
||||
sh """
|
||||
buildah login \
|
||||
--username \${BUILDAH_USERNAME} \
|
||||
--password \${BUILDAH_PASSWORD} \
|
||||
git.pyrocufflink.net
|
||||
"""
|
||||
}
|
||||
sh '. ci/publish-server.sh'
|
||||
stage('Publish') {
|
||||
steps {
|
||||
withEnv([
|
||||
"REGISTRY_AUTH_FILE=${env.WORKSPACE_TMP}/auth.json"
|
||||
]) {
|
||||
withCredentials([usernamePassword(
|
||||
credentialsId: 'jenkins-packages',
|
||||
usernameVariable: 'BUILDAH_USERNAME',
|
||||
passwordVariable: 'BUILDAH_PASSWORD',
|
||||
)]) {
|
||||
sh """
|
||||
buildah login \
|
||||
--username \${BUILDAH_USERNAME} \
|
||||
--password \${BUILDAH_PASSWORD} \
|
||||
git.pyrocufflink.net
|
||||
"""
|
||||
}
|
||||
sh '. ci/publish.sh'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('CLI') {
|
||||
when {
|
||||
expression {
|
||||
env.COMPONENT = 'client'
|
||||
stage('Deploy') {
|
||||
when {
|
||||
branch 'master'
|
||||
}
|
||||
}
|
||||
agent {
|
||||
kubernetes {
|
||||
yamlFile 'ci/clientPodTemplate.yaml'
|
||||
yamlMergeStrategy merge()
|
||||
defaultContainer 'fedora'
|
||||
nodeSelector "kubernetes.io/arch=${ARCH}"
|
||||
}
|
||||
}
|
||||
environment {
|
||||
GNUPGHOME = "${env.WORKSPACE_TMP}/gnupg"
|
||||
}
|
||||
stages {
|
||||
stage('Prepare - CLI') {
|
||||
steps {
|
||||
sh '. ci/prepare-client.sh'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build - CLI') {
|
||||
steps {
|
||||
sh '. ci/build-client.sh'
|
||||
script {
|
||||
if (env.BRANCH_NAME == 'master') {
|
||||
withCredentials([
|
||||
file(
|
||||
credentialsId: 'rpm-gpg-key',
|
||||
variable: 'RPM_GPG_PRIVATE_KEY',
|
||||
),
|
||||
file(
|
||||
credentialsId: 'rpm-gpg-key-passphrase',
|
||||
variable: 'RPM_GPG_KEY_PASSPHRASE',
|
||||
),
|
||||
]) {
|
||||
sh '. ci/sign-rpms.sh'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
success {
|
||||
dir('cli') {
|
||||
archiveArtifacts '*.rpm'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish - CLI') {
|
||||
when {
|
||||
branch 'master'
|
||||
}
|
||||
steps {
|
||||
sshagent(['jenkins-repohost']) {
|
||||
sh '. ci/publish-client.sh'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh '. ci/deploy.sh'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
cd cli
|
||||
make rpm
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
. ci/common.sh
|
||||
|
||||
buildah build -t "${IMAGE_NAME}:${TAG}" server
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
|
||||
. ci/common.sh
|
||||
|
||||
buildah build -t "${IMAGE_NAME}:${TAG}" .
|
|
@ -1,17 +0,0 @@
|
|||
spec:
|
||||
containers:
|
||||
- name: fedora
|
||||
image: registry.fedoraproject.org/fedora:38
|
||||
command:
|
||||
- cat
|
||||
stdin: true
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- mountPath: /etc/ssh/ssh_known_hosts
|
||||
name: ssh-known-hosts
|
||||
subPath: ssh_known_hosts
|
||||
hostUsers: false
|
||||
volumes:
|
||||
- name: ssh-known-hosts
|
||||
configMap:
|
||||
name: ssh-known-hosts
|
|
@ -5,7 +5,7 @@ escape_name() {
|
|||
}
|
||||
|
||||
REGISTRY_URL=git.pyrocufflink.net
|
||||
NAMESPACE=containerimages
|
||||
NAMESPACE=packages
|
||||
NAME="${JOB_NAME#*/}"
|
||||
NAME=$(escape_name "${NAME%/*}")
|
||||
TAG=$(escape_name "${BRANCH_NAME}")
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
#!/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}"'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
dnf install -y \
|
||||
--setopt install_weak_deps=0 \
|
||||
cargo \
|
||||
cargo-rpm-macros \
|
||||
make \
|
||||
openssh-clients \
|
||||
openssl-devel \
|
||||
rpm-build \
|
||||
rpm-sign \
|
||||
rsync \
|
||||
rust \
|
||||
systemd-rpm-macros \
|
||||
tar \
|
||||
--
|
||||
|
||||
install -m u=rwx,go= -d "${GNUPGHOME}"
|
||||
cat > "${GNUPGHOME}"/gpg-agent.conf <<EOF
|
||||
allow-loopback-pinentry
|
||||
EOF
|
|
@ -1,25 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
REPO_HOST=jenkins@files.pyrocufflink.blue
|
||||
REPO_PATH=/srv/www/repohost/repos/dch/fedora/$(rpm --eval %fedora)
|
||||
|
||||
ssh-add -l
|
||||
ssh-add -L
|
||||
|
||||
case "${ARCH}" in
|
||||
x86_64)
|
||||
# only include the SRPM once
|
||||
include='*.rpm'
|
||||
;;
|
||||
*)
|
||||
include="*.${ARCH}.rpm"
|
||||
;;
|
||||
esac
|
||||
|
||||
rsync -rtiO \
|
||||
--chmod=ugo=rwX \
|
||||
--include "${include}" \
|
||||
--exclude '*' \
|
||||
cli/ \
|
||||
"${REPO_HOST}:${REPO_PATH}/"
|
|
@ -1,12 +0,0 @@
|
|||
#!/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}"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
/target
|
||||
/vendor
|
||||
/*.log
|
||||
/*.tar.xz
|
||||
/*.rpm
|
File diff suppressed because it is too large
Load Diff
|
@ -1,20 +0,0 @@
|
|||
[package]
|
||||
name = "sshca-cli"
|
||||
version = "0.1.0"
|
||||
description = "CLI client for SSHCA"
|
||||
homepage = "https://git.pyrocufflink.net/dustin/sshca"
|
||||
authors = ["Dustin C. Hatch <dustin@hatch.name>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
argh = "0.1.12"
|
||||
argon2 = { version = "0.5.2", default-features = false, features = ["alloc"] }
|
||||
gethostname = "0.4.3"
|
||||
jsonwebtoken = { version = "9.1.0", default-features = false }
|
||||
reqwest = { version = "0.11.22", features = ["multipart"] }
|
||||
serde = { version = "1.0.190", features = ["derive"] }
|
||||
tokio = { version = "1.33.0", features = ["rt", "macros"] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||
uuid = "1.5.0"
|
|
@ -1,201 +0,0 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
79
cli/Makefile
79
cli/Makefile
|
@ -1,79 +0,0 @@
|
|||
NAME := sshca-cli
|
||||
VERSION := $(shell sed -nr '/^version =/s/version = "(.*)"/\1/p' Cargo.toml)
|
||||
RELEASE := $(shell rpm --eval "$$(sed -nr '/^Release:/s/Release:\s*(.*)/\1/p' sshca-cli.spec)")
|
||||
ARCH := $(shell uname -m)
|
||||
|
||||
MOCK_CONFIG = fedora-$(shell rpm --eval %fedora)-$(ARCH)
|
||||
|
||||
debug: target/debug/$(NAME)
|
||||
|
||||
release: target/release/$(NAME)
|
||||
|
||||
target/debug/$(NAME): $(find src) $(wildcard Cargo.*)
|
||||
cargo build
|
||||
|
||||
target/release/$(NAME): $(find src) $(wildcard Cargo.*)
|
||||
cargo build --release
|
||||
|
||||
rpm: $(NAME)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||
|
||||
$(NAME)-$(VERSION)-$(RELEASE).$(ARCH).rpm: \
|
||||
$(NAME)-$(VERSION)-$(RELEASE).src.rpm
|
||||
mkdir -p rpmbuild
|
||||
rpmbuild --rebuild \
|
||||
-D "_topdir ${PWD}/rpmbuild" \
|
||||
$<
|
||||
mv rpmbuild/RPMS/$(ARCH)/*.rpm .
|
||||
rm -rf rpmbuild
|
||||
|
||||
srpm: $(NAME)-$(VERSION)-$(RELEASE).src.rpm
|
||||
|
||||
$(NAME)-$(VERSION)-$(RELEASE).src.rpm: \
|
||||
$(NAME).spec \
|
||||
$(NAME)-$(VERSION).tar.xz \
|
||||
$(NAME)-$(VERSION)-vendor.tar.xz
|
||||
rpmbuild -bs \
|
||||
-D "_topdir ${PWD}" \
|
||||
-D '_builddir %{_topdir}' \
|
||||
-D '_buildrootdir %{_topdir}' \
|
||||
-D '_rpmdir %{_topdir}' \
|
||||
-D '_sourcedir %{_topdir}' \
|
||||
-D '_specdir %{_topdir}' \
|
||||
-D '_srcrpmdir %{_topdir}' \
|
||||
$<
|
||||
|
||||
$(NAME)-$(VERSION).tar.xz: \
|
||||
$(shell find src -type f) \
|
||||
Cargo.toml \
|
||||
Cargo.lock \
|
||||
$(wildcard LICENSE-*.txt)
|
||||
tar -czf $@ \
|
||||
--transform 's/^/$(NAME)-$(VERSION)\//' \
|
||||
src \
|
||||
Cargo.lock \
|
||||
Cargo.toml \
|
||||
LICENSE-*.txt
|
||||
|
||||
$(NAME)-$(VERSION)-vendor.tar.xz: Cargo.lock
|
||||
cargo vendor
|
||||
tar -czf $@ vendor
|
||||
|
||||
mockbuild: $(NAME)-$(VERSION)-$(RELEASE).src.rpm
|
||||
mock -r $(MOCK_CONFIG) --resultdir $(PWD) $<
|
||||
|
||||
clean:
|
||||
rm -f $(NAME)*-$(VERSION)-$(RELEASE).*.rpm
|
||||
rm -f $(NAME)-$(VERSION).tar.xz
|
||||
rm -f $(NAME)-$(VERSION)-vendor.tar.xz
|
||||
rm -f build.log hw_info.log installed_pkgs.log root.log state.log
|
||||
|
||||
fullclean: clean
|
||||
rm -rf target
|
||||
|
||||
.PHONY: \
|
||||
clean \
|
||||
debug \
|
||||
mockbuild \
|
||||
release \
|
||||
rpm \
|
||||
srpm
|
234
cli/src/main.rs
234
cli/src/main.rs
|
@ -1,234 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
use std::time;
|
||||
|
||||
use argh::FromArgs;
|
||||
use argon2::Argon2;
|
||||
use jsonwebtoken::{encode, EncodingKey};
|
||||
use reqwest::multipart::{Form, Part};
|
||||
use reqwest::{StatusCode, Url};
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use uuid::Uuid;
|
||||
|
||||
static RPI_SERIAL_PATH: &str = "/sys/firmware/devicetree/base/serial-number";
|
||||
static DMI_UUID_PATH: &str = "/sys/class/dmi/id/product_uuid";
|
||||
|
||||
/// SSH CA client CLI
|
||||
#[derive(FromArgs)]
|
||||
struct Args {
|
||||
#[argh(subcommand)]
|
||||
command: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand)]
|
||||
enum Subcommand {
|
||||
Host(HostArgs),
|
||||
}
|
||||
|
||||
/// Manage host keys and certificates
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand, name = "host")]
|
||||
struct HostArgs {
|
||||
#[argh(subcommand)]
|
||||
command: HostSubcommand,
|
||||
}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand)]
|
||||
enum HostSubcommand {
|
||||
Sign(SignArgs),
|
||||
}
|
||||
|
||||
/// Request a signed certificate for an SSH public key
|
||||
#[derive(FromArgs)]
|
||||
#[argh(subcommand, name = "sign")]
|
||||
struct SignArgs {
|
||||
/// path to destination SSH host certificate file
|
||||
#[argh(option, short = 'c')]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// path to SSH host public key file to sign
|
||||
#[argh(positional)]
|
||||
pubkey: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Claims {
|
||||
sub: String,
|
||||
iss: String,
|
||||
aud: String,
|
||||
iat: u64,
|
||||
nbf: u64,
|
||||
exp: u64,
|
||||
}
|
||||
|
||||
type MainResult = Result<(), Box<dyn std::error::Error>>;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
|
||||
match inner_main().await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn inner_main() -> MainResult {
|
||||
let args: Args = argh::from_env();
|
||||
match args.command {
|
||||
Subcommand::Host(args) => host_cmd(args).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn host_cmd(args: HostArgs) -> MainResult {
|
||||
match args.command {
|
||||
HostSubcommand::Sign(args) => sign_key(args).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn sign_key(args: SignArgs) -> MainResult {
|
||||
let url = match std::env::var("SSHCA_SERVER") {
|
||||
Ok(v) => v,
|
||||
Err(std::env::VarError::NotPresent) => {
|
||||
return Err("SSHCA_SERVER environment variable is not set".into());
|
||||
}
|
||||
Err(std::env::VarError::NotUnicode(_)) => {
|
||||
return Err("SSHCA_SERVER environment variable is invalid".into());
|
||||
}
|
||||
};
|
||||
let Some(hostname) = get_hostname() else {
|
||||
return Err("Hostname must be valid UTF-8".into());
|
||||
};
|
||||
let Some(machine_id) = get_machine_id() else {
|
||||
return Err("Could not determine machine ID".into());
|
||||
};
|
||||
|
||||
let pubkey = std::fs::read_to_string(&args.pubkey)?;
|
||||
|
||||
let mut url =
|
||||
Url::parse(&url).map_err(|e| format!("Invalid URL: {}", e))?;
|
||||
url.path_segments_mut()
|
||||
.map_err(|_| "Invalid URL: missing host")?
|
||||
.pop_if_empty()
|
||||
.push("host")
|
||||
.push("sign");
|
||||
|
||||
let form = Form::new().text("hostname", hostname.clone()).part(
|
||||
"pubkey",
|
||||
Part::bytes(pubkey.into_bytes()).file_name(
|
||||
args.pubkey
|
||||
.file_name()
|
||||
.ok_or("Invalid public key file path")?
|
||||
.to_str()
|
||||
.ok_or("Invalid public key file path")?
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
|
||||
let token = get_token(
|
||||
&hostname,
|
||||
&machine_id,
|
||||
url.host_str().ok_or("Invalid URL: missing host")?,
|
||||
)?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
info!(
|
||||
"Requesting SSH host certificate for {} with key {}",
|
||||
hostname,
|
||||
args.pubkey.display()
|
||||
);
|
||||
debug!("Request: POST {}", url);
|
||||
let res = client
|
||||
.post(url)
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.await?;
|
||||
debug!("Response: {:?} {}", &res.version(), &res.status());
|
||||
match res.error_for_status_ref() {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.status() == Some(StatusCode::BAD_REQUEST) => {
|
||||
let msg = res.text().await.unwrap_or_else(|e| e.to_string());
|
||||
error!("{}: {}", e, msg);
|
||||
return Err(format!("{}\n{}", e, msg).into());
|
||||
}
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
let cert = res.text().await?;
|
||||
if let Some(path) = args.output {
|
||||
std::fs::write(path, cert)?;
|
||||
} else {
|
||||
println!("{}", cert);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_hostname() -> Option<String> {
|
||||
gethostname::gethostname().into_string().ok()
|
||||
}
|
||||
|
||||
fn get_machine_id() -> Option<Uuid> {
|
||||
match std::fs::read_to_string(RPI_SERIAL_PATH) {
|
||||
Ok(s) => match Uuid::parse_str(&format!("{:0>32}", s)) {
|
||||
Ok(u) => return Some(u),
|
||||
Err(e) => {
|
||||
debug!("Invalid UUID: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
debug!("Could not read Raspberry Pi serial number: {}", e)
|
||||
}
|
||||
};
|
||||
match std::fs::read_to_string(DMI_UUID_PATH) {
|
||||
Ok(s) => match Uuid::parse_str(s.trim_end()) {
|
||||
Ok(u) => return Some(u),
|
||||
Err(e) => {
|
||||
debug!("Invalid UUID: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
debug!("Could not read DMI product UUID from sysfs: {}", e);
|
||||
}
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
fn get_token(
|
||||
hostname: &str,
|
||||
machine_id: &Uuid,
|
||||
server: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let now = time::SystemTime::now()
|
||||
.duration_since(time::UNIX_EPOCH)?
|
||||
.as_secs();
|
||||
let claims = Claims {
|
||||
sub: hostname.into(),
|
||||
iss: hostname.into(),
|
||||
aud: server.split(':').next().unwrap().into(),
|
||||
nbf: now - 60,
|
||||
iat: now,
|
||||
exp: now + 60,
|
||||
};
|
||||
let mut secret = [0u8; 32];
|
||||
Argon2::default()
|
||||
.hash_password_into(
|
||||
machine_id.as_bytes(),
|
||||
hostname.as_bytes(),
|
||||
&mut secret,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let key = EncodingKey::from_secret(&secret);
|
||||
Ok(encode(&Default::default(), &claims, &key)?)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
[Unit]
|
||||
Description=Request %I SSH Host Certificate
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
EnvironmentFile=-/etc/sysconfig/ssh-host-cert-sign
|
||||
ExecStart=/usr/bin/sshca-cli host sign --output /etc/ssh/ssh_host_%I_key-cert.pub /etc/ssh/ssh_host_%I_key.pub
|
||||
|
||||
CapabilityBoundingSet=CAP_CHOWN
|
||||
DeviceAllow=
|
||||
DevicePolicy=closed
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
NoNewPrivileges=yes
|
||||
PrivateDevices=yes
|
||||
PrivateUsers=yes
|
||||
PrivateTmp=yes
|
||||
ProcSubset=pid
|
||||
ProtectClock=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectHome=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectProc=invisible
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/etc/ssh
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
|
@ -1,7 +0,0 @@
|
|||
# vim: set ft=systemd :
|
||||
[Unit]
|
||||
Description=Request SSH Host Certificates
|
||||
StopWhenUnneeded=yes
|
||||
Wants=ssh-host-cert-sign@ed25519.service
|
||||
Wants=ssh-host-cert-sign@rsa.service
|
||||
Wants=ssh-host-cert-sign@ecdsa.service
|
|
@ -1,12 +0,0 @@
|
|||
# vim: set ft=systemd :
|
||||
[Unit]
|
||||
Description=Periodically renew SSH host certificates
|
||||
|
||||
[Timer]
|
||||
Unit=%N.target
|
||||
OnCalendar=Tue *-*-* 00:00:00
|
||||
RandomizedDelaySec=48h
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
|
@ -1,10 +0,0 @@
|
|||
# vim: set ft=systemd :
|
||||
[Unit]
|
||||
Description=Request SSH Host Certificates
|
||||
ConditionFirstBoot=yes
|
||||
Wants=ssh-host-cert-sign@ed25519.service
|
||||
Wants=ssh-host-cert-sign@rsa.service
|
||||
Wants=ssh-host-cert-sign@ecdsa.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -1,93 +0,0 @@
|
|||
# Generated by rust2rpm 25
|
||||
%bcond_without check
|
||||
|
||||
# prevent library files from being installed
|
||||
%global __cargo_is_lib() 0
|
||||
|
||||
%global crate sshca-cli
|
||||
|
||||
Name: sshca-cli
|
||||
Version: 0.1.0
|
||||
Release: 1%{?dist}
|
||||
Summary: CLI client for SSHCA
|
||||
|
||||
SourceLicense: MIT OR Apache-2.0
|
||||
License: MIT OR Apache-2.0
|
||||
# LICENSE.dependencies contains a full license breakdown
|
||||
|
||||
URL: https://git.pyrocufflink.net/dustin/sshca
|
||||
Source: sshca-cli-%{version}.tar.xz
|
||||
Source: sshca-cli-%{version}-vendor.tar.xz
|
||||
Source: ssh-host-cert-sign@.service
|
||||
Source: ssh-host-certs.target
|
||||
Source: ssh-host-certs-renew.target
|
||||
Source: ssh-host-certs-renew.timer
|
||||
|
||||
ExclusiveArch: %{rust_arches}
|
||||
|
||||
BuildRequires: cargo-rpm-macros >= 25
|
||||
BuildRequires: openssl-devel
|
||||
BuildRequires: systemd-rpm-macros
|
||||
|
||||
%global _description %{expand:
|
||||
CLI client for SSHCA.}
|
||||
|
||||
%description %{_description}
|
||||
|
||||
%package systemd
|
||||
Summary: systemd units for managing SSH host certificates with SSHCA
|
||||
Requires: %{name} = %{version}
|
||||
|
||||
%description systemd
|
||||
A collection of systemd service, timer, and target units that automatically
|
||||
request and renew SSH host certificates from an SSHCA server.
|
||||
|
||||
%prep
|
||||
%autosetup -n %{crate}-%{version} -p1 -a1
|
||||
%cargo_prep -v vendor
|
||||
|
||||
|
||||
%build
|
||||
%cargo_build
|
||||
%{cargo_license_summary}
|
||||
%{cargo_license} > LICENSE.dependencies
|
||||
%{cargo_vendor_manifest}
|
||||
|
||||
%install
|
||||
%cargo_install
|
||||
|
||||
mkdir -p $RPM_BUILD_ROOT%{_unitdir}
|
||||
install -m u=rw,go=r \
|
||||
%{SOURCE2} \
|
||||
%{SOURCE3} \
|
||||
%{SOURCE4} \
|
||||
%{SOURCE5} \
|
||||
$RPM_BUILD_ROOT%{_unitdir}
|
||||
|
||||
%if %{with check}
|
||||
%check
|
||||
%cargo_test
|
||||
%endif
|
||||
|
||||
%post systemd
|
||||
%systemd_post ssh-host-certs.target ssh-host-certs-renew.timer
|
||||
|
||||
%preun systemd
|
||||
%systemd_preun ssh-host-certs.target ssh-host-certs-renew.timer
|
||||
|
||||
%postun systemd
|
||||
%systemd_postun ssh-host-certs.target ssh-host-certs-renew.timer
|
||||
|
||||
%files
|
||||
%license LICENSE-Apache-2.0.txt
|
||||
%license LICENSE-MIT.txt
|
||||
%license LICENSE.dependencies
|
||||
%license cargo-vendor.txt
|
||||
%{_bindir}/sshca-cli
|
||||
|
||||
%files systemd
|
||||
%{_unitdir}/*
|
||||
|
||||
%changelog
|
||||
* Sun Nov 05 2023 Dustin C. Hatch <dustin@hatch.name> - 0.1.0-1
|
||||
- Initial package
|
|
@ -1 +0,0 @@
|
|||
Cargo.lock -diff
|
|
@ -1 +0,0 @@
|
|||
max_width = 79
|
|
@ -1,147 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
use axum::extract::multipart::{Multipart, MultipartError};
|
||||
use axum::extract::State;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::Serialize;
|
||||
use ssh_key::Algorithm;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::auth::Claims;
|
||||
use crate::ca;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SignKeyResponse {
|
||||
success: bool,
|
||||
errors: Vec<String>,
|
||||
certificates: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub enum SignKeyError {
|
||||
Multipart(MultipartError),
|
||||
Cert(ca::CertError),
|
||||
LoadPrivateKey(ca::LoadKeyError),
|
||||
ParsePublicKey(ca::LoadKeyError),
|
||||
UnsupportedAlgorithm(String),
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SignKeyRequest {
|
||||
hostname: String,
|
||||
pubkey: Vec<u8>,
|
||||
}
|
||||
|
||||
pub(super) async fn sign_host_cert(
|
||||
claims: Claims,
|
||||
State(ctx): State<super::State>,
|
||||
mut form: Multipart,
|
||||
) -> Result<String, SignKeyError> {
|
||||
let hostname = claims.sub;
|
||||
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("hostname") => body.hostname = field.text().await?,
|
||||
Some(n) => {
|
||||
warn!("Client request included unsupported field {:?}", n);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if body.pubkey.is_empty() {
|
||||
return Err(SignKeyError::NoKey);
|
||||
}
|
||||
|
||||
let config = &ctx.config;
|
||||
let duration = Duration::from_secs(config.ca.host.cert_duration);
|
||||
let privkey = ca::load_private_key(
|
||||
&config.ca.host.private_key_file,
|
||||
config.ca.host.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(),
|
||||
hostname
|
||||
);
|
||||
let cert =
|
||||
ca::sign_cert(&hostname, &pubkey, duration, &privkey, &[])?;
|
||||
info!(
|
||||
"Signed {} key for {}",
|
||||
pubkey.algorithm().as_str(),
|
||||
hostname
|
||||
);
|
||||
Ok(cert.to_openssh().map_err(ca::CertError::from)?)
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
mod host;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
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::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{RequestPartsExt, Router, TypedHeader};
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::debug;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::{self, Claims};
|
||||
use crate::config::Configuration;
|
||||
use crate::machine_id;
|
||||
|
||||
struct Context {
|
||||
config: Arc<Configuration>,
|
||||
cache: RwLock<HashMap<String, (Instant, Uuid)>>,
|
||||
}
|
||||
|
||||
type State = Arc<Context>;
|
||||
|
||||
pub struct AuthError;
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let ctx = Arc::new(Context {
|
||||
config: config.into(),
|
||||
cache: RwLock::new(Default::default()),
|
||||
});
|
||||
Router::new()
|
||||
.route("/", get(|| async { "UP" }))
|
||||
.route("/host/sign", post(host::sign_host_cert))
|
||||
.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)]
|
||||
mod test {
|
||||
use axum::body::Body;
|
||||
use axum::http::Request;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_up() {
|
||||
let app = make_app(Configuration::default());
|
||||
let response = app
|
||||
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
|
||||
assert_eq!(&body[..], b"UP");
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ use uuid::Uuid;
|
|||
|
||||
/// JWT Token Claims
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Claims {
|
||||
pub struct HostClaims {
|
||||
/// Token subject (machine hostname)
|
||||
pub sub: String,
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ pub fn get_token_subject(token: &str) -> Result<String> {
|
|||
v.insecure_disable_signature_validation();
|
||||
v.set_required_spec_claims(&["sub"]);
|
||||
let k = DecodingKey::from_secret(b"");
|
||||
let data: TokenData<Claims> = decode(token, &k, &v)?;
|
||||
let data: TokenData<HostClaims> = decode(token, &k, &v)?;
|
||||
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).
|
||||
/// The token must be signed with HMAC-SHA256 using the host's machine ID as
|
||||
/// the secret key.
|
||||
pub fn validate_token(
|
||||
pub fn validate_host_token(
|
||||
token: &str,
|
||||
hostname: &str,
|
||||
machine_id: &Uuid,
|
||||
service: &str,
|
||||
) -> Result<Claims> {
|
||||
) -> Result<HostClaims> {
|
||||
let mut v = Validation::new(Algorithm::HS256);
|
||||
v.validate_nbf = true;
|
||||
v.set_issuer(&[hostname]);
|
||||
|
@ -66,7 +66,7 @@ pub fn validate_token(
|
|||
OsRng.fill_bytes(&mut secret);
|
||||
}
|
||||
let k = DecodingKey::from_secret(&secret);
|
||||
let data: TokenData<Claims> = decode(token, &k, &v)?;
|
||||
let data: TokenData<HostClaims> = decode(token, &k, &v)?;
|
||||
Ok(data.claims)
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,12 @@ pub(crate) mod test {
|
|||
|
||||
let machine_id = uuid!("9afd42e5-4ac3-4530-90c4-191869063ef9");
|
||||
let token = make_token(hostname, machine_id);
|
||||
validate_token(&token, hostname, &machine_id, "sshca.example.org")
|
||||
.unwrap();
|
||||
validate_host_token(
|
||||
&token,
|
||||
hostname,
|
||||
&machine_id,
|
||||
"sshca.example.org",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
/// key. The certificate will be valid for the specified hostname and
|
||||
/// any alias names provided.
|
||||
pub fn sign_cert(
|
||||
pub fn sign_host_cert(
|
||||
hostname: &str,
|
||||
pubkey: &PublicKey,
|
||||
duration: Duration,
|
||||
|
@ -160,6 +160,38 @@ pub fn sign_cert(
|
|||
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)]
|
||||
mod test {
|
||||
use ssh_key::Algorithm;
|
||||
|
@ -175,9 +207,9 @@ mod test {
|
|||
let host_pub_key = host_key.public_key();
|
||||
let duration = Duration::from_secs(86400 * 30);
|
||||
let hostname = "cloud0.example.org";
|
||||
let cert = sign_cert(
|
||||
let cert = sign_host_cert(
|
||||
hostname,
|
||||
&host_pub_key,
|
||||
host_pub_key,
|
||||
duration,
|
||||
&ca_key,
|
||||
&["nextcloud.example.org"],
|
|
@ -1,4 +1,5 @@
|
|||
//! Application configuration
|
||||
use std::collections::HashMap;
|
||||
use std::io::ErrorKind;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -65,11 +66,57 @@ 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
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct CaConfig {
|
||||
/// Host CA configuration
|
||||
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
|
||||
|
@ -91,6 +138,9 @@ pub struct Configuration {
|
|||
/// CA configuration
|
||||
#[serde(default)]
|
||||
pub ca: CaConfig,
|
||||
/// OpenID Connect configuration for user authorization
|
||||
#[serde(default)]
|
||||
pub oidc: Option<OidcConfig>,
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
|
@ -99,6 +149,7 @@ impl Default for Configuration {
|
|||
libvirt: vec![],
|
||||
machine_ids: default_machine_ids(),
|
||||
ca: Default::default(),
|
||||
oidc: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +174,24 @@ fn default_host_cert_duration() -> u64 {
|
|||
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
|
||||
///
|
||||
/// If `path` is provided, the configuration will be loaded from the
|
|
@ -0,0 +1,75 @@
|
|||
//! 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
use std::collections::HashMap;
|
||||
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, Host};
|
||||
use axum::http::request::Parts;
|
||||
use axum::{RequestPartsExt, TypedHeader};
|
||||
use serde::Serialize;
|
||||
use ssh_key::Algorithm;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::error::SignKeyError;
|
||||
use super::{AuthError, Context};
|
||||
use crate::auth::{self, HostClaims};
|
||||
use crate::ca;
|
||||
use crate::machine_id;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SignKeyResponse {
|
||||
success: bool,
|
||||
errors: Vec<String>,
|
||||
certificates: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FromRequestParts<Arc<Context>> for HostClaims {
|
||||
type Rejection = AuthError;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut Parts,
|
||||
ctx: &super::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_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> {
|
||||
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)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SignKeyRequest {
|
||||
hostname: String,
|
||||
pubkey: Vec<u8>,
|
||||
}
|
||||
|
||||
pub(super) async fn sign_host_cert(
|
||||
claims: HostClaims,
|
||||
State(ctx): State<super::State>,
|
||||
mut form: Multipart,
|
||||
) -> Result<String, SignKeyError> {
|
||||
let hostname = claims.sub;
|
||||
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("hostname") => body.hostname = field.text().await?,
|
||||
Some(n) => {
|
||||
warn!("Client request included unsupported field {:?}", n);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if body.pubkey.is_empty() {
|
||||
return Err(SignKeyError::NoKey);
|
||||
}
|
||||
|
||||
let config = &ctx.config;
|
||||
let duration = Duration::from_secs(config.ca.host.cert_duration);
|
||||
let privkey = ca::load_private_key(
|
||||
&config.ca.host.private_key_file,
|
||||
config.ca.host.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(),
|
||||
hostname
|
||||
);
|
||||
let cert =
|
||||
ca::sign_host_cert(&hostname, &pubkey, duration, &privkey, &[])?;
|
||||
info!(
|
||||
"Signed {} key for {}",
|
||||
pubkey.algorithm().as_str(),
|
||||
hostname
|
||||
);
|
||||
Ok(cert.to_openssh().map_err(ca::CertError::from)?)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
mod error;
|
||||
mod host;
|
||||
mod oidc;
|
||||
mod user;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::Router;
|
||||
use openidconnect::core::CoreProviderMetadata;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::config::Configuration;
|
||||
|
||||
struct Context {
|
||||
config: Arc<Configuration>,
|
||||
cache: RwLock<HashMap<String, (Instant, Uuid)>>,
|
||||
oidc: RwLock<Option<(Instant, CoreProviderMetadata)>>,
|
||||
}
|
||||
|
||||
type State = Arc<Context>;
|
||||
|
||||
pub struct AuthError;
|
||||
|
||||
impl IntoResponse for AuthError {
|
||||
fn into_response(self) -> Response {
|
||||
(StatusCode::UNAUTHORIZED, "Unauthorized").into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn make_app(config: Configuration) -> Router {
|
||||
let ctx = Arc::new(Context {
|
||||
config: config.into(),
|
||||
cache: RwLock::new(Default::default()),
|
||||
oidc: Default::default(),
|
||||
});
|
||||
Router::new()
|
||||
.route("/", get(|| async { "UP" }))
|
||||
.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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use axum::body::Body;
|
||||
use axum::http::Request;
|
||||
use tower::ServiceExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_up() {
|
||||
let app = make_app(Configuration::default());
|
||||
let response = app
|
||||
.oneshot(Request::builder().uri("/").body(Body::empty()).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = hyper::body::to_bytes(response.into_body()).await.unwrap();
|
||||
assert_eq!(&body[..], b"UP");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
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 {}
|
|
@ -0,0 +1,284 @@
|
|||
//! 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ use ssh_key::{Algorithm, Fingerprint, PrivateKey, PublicKey};
|
|||
use tempfile::NamedTempFile;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use sshca::config::Configuration;
|
||||
use sshca::config::{Configuration, OidcConfig};
|
||||
|
||||
static INIT: Once = Once::new();
|
||||
|
||||
|
@ -24,6 +24,11 @@ fn gen_machine_ids() -> Result<NamedTempFile, Box<dyn Error>> {
|
|||
fn gen_config(machine_ids: &Path, host_key: &Path) -> Configuration {
|
||||
let mut config = Configuration {
|
||||
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()
|
||||
};
|
||||
config.ca.host.private_key_file = host_key.to_str().unwrap().into();
|
|
@ -0,0 +1,34 @@
|
|||
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);
|
||||
}
|
Loading…
Reference in New Issue