Self-hosted fleet management platform for Linux
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: jwks setup

+661 -8
+289 -1
Cargo.lock
··· 156 156 ] 157 157 158 158 [[package]] 159 + name = "base64" 160 + version = "0.22.1" 161 + source = "registry+https://github.com/rust-lang/crates.io-index" 162 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 163 + 164 + [[package]] 165 + name = "base64ct" 166 + version = "1.8.3" 167 + source = "registry+https://github.com/rust-lang/crates.io-index" 168 + checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" 169 + 170 + [[package]] 171 + name = "block-buffer" 172 + version = "0.10.4" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 175 + dependencies = [ 176 + "generic-array", 177 + ] 178 + 179 + [[package]] 159 180 name = "bumpalo" 160 181 version = "3.20.2" 161 182 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 264 285 ] 265 286 266 287 [[package]] 288 + name = "const-oid" 289 + version = "0.9.6" 290 + source = "registry+https://github.com/rust-lang/crates.io-index" 291 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 292 + 293 + [[package]] 267 294 name = "core-foundation-sys" 268 295 version = "0.8.7" 269 296 source = "registry+https://github.com/rust-lang/crates.io-index" 270 297 checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 271 298 272 299 [[package]] 300 + name = "cpufeatures" 301 + version = "0.2.17" 302 + source = "registry+https://github.com/rust-lang/crates.io-index" 303 + checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 304 + dependencies = [ 305 + "libc", 306 + ] 307 + 308 + [[package]] 309 + name = "crypto-common" 310 + version = "0.1.7" 311 + source = "registry+https://github.com/rust-lang/crates.io-index" 312 + checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" 313 + dependencies = [ 314 + "generic-array", 315 + "typenum", 316 + ] 317 + 318 + [[package]] 319 + name = "curve25519-dalek" 320 + version = "4.1.3" 321 + source = "registry+https://github.com/rust-lang/crates.io-index" 322 + checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" 323 + dependencies = [ 324 + "cfg-if", 325 + "cpufeatures", 326 + "curve25519-dalek-derive", 327 + "digest", 328 + "fiat-crypto", 329 + "rustc_version", 330 + "subtle", 331 + "zeroize", 332 + ] 333 + 334 + [[package]] 335 + name = "curve25519-dalek-derive" 336 + version = "0.1.1" 337 + source = "registry+https://github.com/rust-lang/crates.io-index" 338 + checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 339 + dependencies = [ 340 + "proc-macro2", 341 + "quote", 342 + "syn", 343 + ] 344 + 345 + [[package]] 346 + name = "der" 347 + version = "0.7.10" 348 + source = "registry+https://github.com/rust-lang/crates.io-index" 349 + checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" 350 + dependencies = [ 351 + "const-oid", 352 + "pem-rfc7468", 353 + "zeroize", 354 + ] 355 + 356 + [[package]] 357 + name = "digest" 358 + version = "0.10.7" 359 + source = "registry+https://github.com/rust-lang/crates.io-index" 360 + checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 361 + dependencies = [ 362 + "block-buffer", 363 + "crypto-common", 364 + ] 365 + 366 + [[package]] 367 + name = "ed25519" 368 + version = "2.2.3" 369 + source = "registry+https://github.com/rust-lang/crates.io-index" 370 + checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" 371 + dependencies = [ 372 + "pkcs8", 373 + "signature", 374 + ] 375 + 376 + [[package]] 377 + name = "ed25519-dalek" 378 + version = "2.2.0" 379 + source = "registry+https://github.com/rust-lang/crates.io-index" 380 + checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" 381 + dependencies = [ 382 + "curve25519-dalek", 383 + "ed25519", 384 + "rand_core", 385 + "serde", 386 + "sha2", 387 + "subtle", 388 + "zeroize", 389 + ] 390 + 391 + [[package]] 273 392 name = "equivalent" 274 393 version = "1.0.2" 275 394 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 284 403 "libc", 285 404 "windows-sys", 286 405 ] 406 + 407 + [[package]] 408 + name = "fiat-crypto" 409 + version = "0.2.9" 410 + source = "registry+https://github.com/rust-lang/crates.io-index" 411 + checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" 287 412 288 413 [[package]] 289 414 name = "find-msvc-tools" ··· 340 465 ] 341 466 342 467 [[package]] 468 + name = "generic-array" 469 + version = "0.14.7" 470 + source = "registry+https://github.com/rust-lang/crates.io-index" 471 + checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 472 + dependencies = [ 473 + "typenum", 474 + "version_check", 475 + ] 476 + 477 + [[package]] 478 + name = "getrandom" 479 + version = "0.2.17" 480 + source = "registry+https://github.com/rust-lang/crates.io-index" 481 + checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" 482 + dependencies = [ 483 + "cfg-if", 484 + "libc", 485 + "wasi", 486 + ] 487 + 488 + [[package]] 343 489 name = "hashbrown" 344 490 version = "0.17.0" 345 491 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 350 496 version = "0.5.0" 351 497 source = "registry+https://github.com/rust-lang/crates.io-index" 352 498 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 499 + 500 + [[package]] 501 + name = "hex" 502 + version = "0.4.3" 503 + source = "registry+https://github.com/rust-lang/crates.io-index" 504 + checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 353 505 354 506 [[package]] 355 507 name = "http" ··· 510 662 version = "0.1.0-alpha.0" 511 663 dependencies = [ 512 664 "axum", 665 + "http", 666 + "lucid-crypto", 667 + "lucid-server-config", 668 + "serde", 669 + "serde_json", 513 670 "tokio-util", 671 + "tracing", 514 672 ] 515 673 516 674 [[package]] 517 675 name = "lucid-auth" 518 - version = "0.1.0" 676 + version = "0.1.0-alpha.0" 519 677 520 678 [[package]] 521 679 name = "lucid-core" ··· 525 683 ] 526 684 527 685 [[package]] 686 + name = "lucid-crypto" 687 + version = "0.1.0-alpha.0" 688 + dependencies = [ 689 + "base64", 690 + "ed25519-dalek", 691 + "hex", 692 + "serde", 693 + "thiserror", 694 + ] 695 + 696 + [[package]] 528 697 name = "lucid-database" 529 698 version = "0.1.0-alpha.0" 530 699 ··· 536 705 "clap", 537 706 "lucid-api", 538 707 "lucid-core", 708 + "lucid-crypto", 539 709 "lucid-server-config", 540 710 "tokio", 541 711 "tokio-util", ··· 548 718 version = "0.1.0-alpha.0" 549 719 dependencies = [ 550 720 "confique", 721 + "serde", 551 722 ] 552 723 553 724 [[package]] ··· 608 779 version = "1.70.2" 609 780 source = "registry+https://github.com/rust-lang/crates.io-index" 610 781 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 782 + 783 + [[package]] 784 + name = "pem-rfc7468" 785 + version = "0.7.0" 786 + source = "registry+https://github.com/rust-lang/crates.io-index" 787 + checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" 788 + dependencies = [ 789 + "base64ct", 790 + ] 611 791 612 792 [[package]] 613 793 name = "percent-encoding" ··· 622 802 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" 623 803 624 804 [[package]] 805 + name = "pkcs8" 806 + version = "0.10.2" 807 + source = "registry+https://github.com/rust-lang/crates.io-index" 808 + checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 809 + dependencies = [ 810 + "der", 811 + "spki", 812 + ] 813 + 814 + [[package]] 625 815 name = "proc-macro2" 626 816 version = "1.0.106" 627 817 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 640 830 ] 641 831 642 832 [[package]] 833 + name = "rand_core" 834 + version = "0.6.4" 835 + source = "registry+https://github.com/rust-lang/crates.io-index" 836 + checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 837 + dependencies = [ 838 + "getrandom", 839 + ] 840 + 841 + [[package]] 643 842 name = "regex" 644 843 version = "1.12.3" 645 844 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 684 883 checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 685 884 686 885 [[package]] 886 + name = "rustc_version" 887 + version = "0.4.1" 888 + source = "registry+https://github.com/rust-lang/crates.io-index" 889 + checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 890 + dependencies = [ 891 + "semver", 892 + ] 893 + 894 + [[package]] 687 895 name = "rustversion" 688 896 version = "1.0.22" 689 897 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 694 902 version = "1.0.23" 695 903 source = "registry+https://github.com/rust-lang/crates.io-index" 696 904 checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" 905 + 906 + [[package]] 907 + name = "semver" 908 + version = "1.0.28" 909 + source = "registry+https://github.com/rust-lang/crates.io-index" 910 + checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" 697 911 698 912 [[package]] 699 913 name = "serde" ··· 771 985 ] 772 986 773 987 [[package]] 988 + name = "sha2" 989 + version = "0.10.9" 990 + source = "registry+https://github.com/rust-lang/crates.io-index" 991 + checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 992 + dependencies = [ 993 + "cfg-if", 994 + "cpufeatures", 995 + "digest", 996 + ] 997 + 998 + [[package]] 774 999 name = "sharded-slab" 775 1000 version = "0.1.7" 776 1001 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 793 1018 dependencies = [ 794 1019 "errno", 795 1020 "libc", 1021 + ] 1022 + 1023 + [[package]] 1024 + name = "signature" 1025 + version = "2.2.0" 1026 + source = "registry+https://github.com/rust-lang/crates.io-index" 1027 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1028 + dependencies = [ 1029 + "rand_core", 796 1030 ] 797 1031 798 1032 [[package]] ··· 818 1052 ] 819 1053 820 1054 [[package]] 1055 + name = "spki" 1056 + version = "0.7.3" 1057 + source = "registry+https://github.com/rust-lang/crates.io-index" 1058 + checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1059 + dependencies = [ 1060 + "base64ct", 1061 + "der", 1062 + ] 1063 + 1064 + [[package]] 821 1065 name = "strsim" 822 1066 version = "0.11.1" 823 1067 source = "registry+https://github.com/rust-lang/crates.io-index" 824 1068 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1069 + 1070 + [[package]] 1071 + name = "subtle" 1072 + version = "2.6.1" 1073 + source = "registry+https://github.com/rust-lang/crates.io-index" 1074 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 825 1075 826 1076 [[package]] 827 1077 name = "syn" ··· 841 1091 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 842 1092 843 1093 [[package]] 1094 + name = "thiserror" 1095 + version = "2.0.18" 1096 + source = "registry+https://github.com/rust-lang/crates.io-index" 1097 + checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" 1098 + dependencies = [ 1099 + "thiserror-impl", 1100 + ] 1101 + 1102 + [[package]] 1103 + name = "thiserror-impl" 1104 + version = "2.0.18" 1105 + source = "registry+https://github.com/rust-lang/crates.io-index" 1106 + checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" 1107 + dependencies = [ 1108 + "proc-macro2", 1109 + "quote", 1110 + "syn", 1111 + ] 1112 + 1113 + [[package]] 844 1114 name = "thread_local" 845 1115 version = "1.1.9" 846 1116 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1032 1302 ] 1033 1303 1034 1304 [[package]] 1305 + name = "typenum" 1306 + version = "1.19.0" 1307 + source = "registry+https://github.com/rust-lang/crates.io-index" 1308 + checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 1309 + 1310 + [[package]] 1035 1311 name = "unicode-ident" 1036 1312 version = "1.0.24" 1037 1313 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1048 1324 version = "0.1.1" 1049 1325 source = "registry+https://github.com/rust-lang/crates.io-index" 1050 1326 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 1327 + 1328 + [[package]] 1329 + name = "version_check" 1330 + version = "0.9.5" 1331 + source = "registry+https://github.com/rust-lang/crates.io-index" 1332 + checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1051 1333 1052 1334 [[package]] 1053 1335 name = "wasi" ··· 1201 1483 version = "1.0.1" 1202 1484 source = "registry+https://github.com/rust-lang/crates.io-index" 1203 1485 checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" 1486 + 1487 + [[package]] 1488 + name = "zeroize" 1489 + version = "1.8.2" 1490 + source = "registry+https://github.com/rust-lang/crates.io-index" 1491 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 1204 1492 1205 1493 [[package]] 1206 1494 name = "zmij"
+5
Cargo.toml
··· 4 4 "crates/api", 5 5 "crates/core", 6 6 "crates/core/auth", 7 + "crates/core/crypto", 7 8 "crates/database", 8 9 "crates/server", 9 10 "crates/server/config", ··· 19 20 20 21 lucid-core = { path = "crates/core" } 21 22 lucid-auth = { path = "crates/core/auth" } 23 + lucid-crypto = { path = "crates/core/crypto" } 22 24 23 25 lucid-database = { path = "crates/database" } 24 26 ··· 27 29 28 30 anyhow = "1.0" 29 31 axum = { version = "0.8", features = ["macros"] } 32 + base64 = "0.22" 30 33 clap = { version = "4", features = ["derive", "env"] } 31 34 confique = { version = "0.4", features = ["toml"] } 35 + ed25519-dalek = { version = "2.2", features = ["pem", "pkcs8", "rand_core"] } 36 + hex = "0.4" 32 37 http = "1.4" 33 38 serde = { version = "1.0", features = ["derive"] } 34 39 serde_json = "1.0"
+20
config.toml
··· 1 + [server] 2 + host = "127.0.0.1" 3 + port = 8080 4 + 5 + [secrets] 6 + [[secrets.signing_keys]] 7 + key_id = "default" 8 + private_key_pem = """ 9 + -----BEGIN PRIVATE KEY----- 10 + MC4CAQAwBQYDK2VwBCIEIMhfJfvb/KX+7UCtK/Ua0HRETLLnTndvC0qTpXcGcwC9 11 + -----END PRIVATE KEY----- 12 + """ 13 + 14 + [[secrets.encryption_keys]] 15 + key_id = "default" 16 + private_key_pem = """ 17 + -----BEGIN PRIVATE KEY----- 18 + MC4CAQAwBQYDK2VwBCIEIAipyj2hLo5wYWNnjPub5dzQKhSuyCP92me/SDGtYBJ1 19 + -----END PRIVATE KEY----- 20 + """
+7
crates/api/Cargo.toml
··· 5 5 license.workspace = true 6 6 7 7 [dependencies] 8 + lucid-server-config.workspace = true 9 + lucid-crypto.workspace = true 10 + 8 11 axum.workspace = true 12 + http.workspace = true 13 + serde.workspace = true 14 + serde_json.workspace = true 15 + tracing.workspace = true 9 16 tokio-util.workspace = true
+1
crates/api/src/endpoints/mod.rs
··· 1 + pub mod well_known;
+17
crates/api/src/endpoints/well_known/mod.rs
··· 1 + use axum::{Json, extract::State}; 2 + use lucid_crypto::jwk::{Jwk, JwkSet, create_jwk_set}; 3 + 4 + use crate::{error::ApiError, state::AppState}; 5 + 6 + pub fn well_known_routes() -> axum::Router<AppState> { 7 + axum::Router::new() 8 + .route("/jwks.json", axum::routing::get(jwks)) 9 + } 10 + 11 + async fn jwks( 12 + State(state): State<AppState>, 13 + ) -> Result<Json<JwkSet>, ApiError> { 14 + let jwks = state.get_signing_keys().iter().map(Into::into).collect::<Vec<Jwk>>(); 15 + 16 + Ok(Json(create_jwk_set(jwks))) 17 + }
+71
crates/api/src/error.rs
··· 1 + use std::borrow::Cow; 2 + 3 + use axum::{Json, response::IntoResponse}; 4 + use http::StatusCode; 5 + use serde::Serialize; 6 + 7 + #[derive(Debug, Serialize)] 8 + struct ErrorBody<'a> { 9 + error: Cow<'a, str>, 10 + message: String, 11 + } 12 + 13 + pub enum ApiError { 14 + InternalError(Option<String>), 15 + DatabaseError, 16 + Forbidden, 17 + } 18 + 19 + impl ApiError { 20 + fn status_code(&self) -> StatusCode { 21 + match self { 22 + Self::InternalError(_) | Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR, 23 + Self::Forbidden => StatusCode::FORBIDDEN, 24 + } 25 + } 26 + 27 + fn error_name(&self) -> Cow<'static, str> { 28 + match self { 29 + Self::InternalError(_) | Self::DatabaseError => Cow::Borrowed("InternalError"), 30 + Self::Forbidden => Cow::Borrowed("Forbidden"), 31 + } 32 + } 33 + 34 + fn message(&self) -> String { 35 + match self { 36 + Self::InternalError(msg) => msg.clone().unwrap_or_else(|| "Internal Service Error".into()), 37 + Self::DatabaseError => "Internal Service Error".into(), 38 + Self::Forbidden => "Forbidden".into(), 39 + } 40 + } 41 + } 42 + 43 + impl IntoResponse for ApiError { 44 + fn into_response(self) -> axum::response::Response { 45 + let error_name = self.error_name(); 46 + let message = self.message(); 47 + 48 + let body = ErrorBody { 49 + error: error_name, 50 + message, 51 + }; 52 + 53 + let response = (self.status_code(), Json(body)).into_response(); 54 + 55 + response 56 + } 57 + } 58 + 59 + 60 + pub trait DbResultExt<T> { 61 + fn log_db_err(self, ctx: &str) -> Result<T, ApiError>; 62 + } 63 + 64 + impl<T, E: std::fmt::Debug> DbResultExt<T> for Result<T, E> { 65 + fn log_db_err(self, ctx: &str) -> Result<T, ApiError> { 66 + self.map_err(|e| { 67 + tracing::error!("DB error {}: {:?}", ctx, e); 68 + ApiError::DatabaseError 69 + }) 70 + } 71 + }
+5 -1
crates/api/src/lib.rs
··· 1 + pub mod endpoints; 2 + pub mod error; 1 3 pub mod state; 2 4 3 5 use axum::Router; ··· 5 7 use crate::state::AppState; 6 8 7 9 pub fn app(state: AppState) -> Router { 8 - let router = Router::new().with_state(state); 10 + let router = Router::new() 11 + .nest("/.well-known", endpoints::well_known::well_known_routes()) 12 + .with_state(state); 9 13 10 14 router 11 15 }
+11 -1
crates/api/src/state.rs
··· 1 1 use std::error::Error; 2 2 3 + use lucid_crypto::signing::SigningKeyPair; 3 4 use tokio_util::sync::CancellationToken; 4 5 5 6 #[derive(Clone)] 6 7 pub struct AppState { 8 + signing_keys: Vec<SigningKeyPair>, 7 9 pub shutdown: CancellationToken, 8 10 } 9 11 10 12 impl AppState { 11 13 pub async fn new(shutdown: CancellationToken) -> Result<Self, Box<dyn Error>> { 12 - Ok(Self { shutdown }) 14 + Ok(Self { signing_keys: Vec::new(), shutdown }) 15 + } 16 + 17 + pub fn add_signing_key(&mut self, key: SigningKeyPair) { 18 + self.signing_keys.push(key); 19 + } 20 + 21 + pub fn get_signing_keys(&self) -> &[SigningKeyPair] { 22 + &self.signing_keys 13 23 } 14 24 }
+3 -2
crates/core/auth/Cargo.toml
··· 1 1 [package] 2 2 name = "lucid-auth" 3 - version = "0.1.0" 4 - edition = "2024" 3 + version.workspace = true 4 + edition.workspace = true 5 + license.workspace = true 5 6 6 7 [dependencies]
+12
crates/core/crypto/Cargo.toml
··· 1 + [package] 2 + name = "lucid-crypto" 3 + version.workspace = true 4 + edition.workspace = true 5 + license.workspace = true 6 + 7 + [dependencies] 8 + base64.workspace = true 9 + ed25519-dalek.workspace = true 10 + hex.workspace = true 11 + serde.workspace = true 12 + thiserror.workspace = true
+25
crates/core/crypto/src/jwk.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Debug, Clone, Serialize, Deserialize)] 4 + pub struct JwkSet { 5 + pub keys: Vec<Jwk>, 6 + } 7 + 8 + #[derive(Debug, Clone, Serialize, Deserialize)] 9 + pub struct Jwk { 10 + pub kty: String, 11 + #[serde(rename = "use", skip_serializing_if = "Option::is_none")] 12 + pub key_use: Option<String>, 13 + #[serde(skip_serializing_if = "Option::is_none")] 14 + pub kid: Option<String>, 15 + #[serde(skip_serializing_if = "Option::is_none")] 16 + pub alg: Option<String>, 17 + #[serde(skip_serializing_if = "Option::is_none")] 18 + pub crv: Option<String>, 19 + #[serde(skip_serializing_if = "Option::is_none")] 20 + pub x: Option<String>, 21 + } 22 + 23 + pub fn create_jwk_set(keys: Vec<Jwk>) -> JwkSet { 24 + JwkSet { keys } 25 + }
+16
crates/core/crypto/src/lib.rs
··· 1 + pub mod jwk; 2 + pub mod signing; 3 + 4 + use thiserror::Error; 5 + 6 + #[derive(Debug, Clone, Error)] 7 + pub enum CryptoError { 8 + #[error("Encryption failed: {0}")] 9 + EncryptionFailed(String), 10 + #[error("Decryption failed: {0}")] 11 + DecryptionFailed(String), 12 + #[error("Invalid key: {0}")] 13 + InvalidKey(String), 14 + #[error("Key derivation failed: {0}")] 15 + KeyDerivationFailed(String), 16 + }
+60
crates/core/crypto/src/signing.rs
··· 1 + use std::fmt; 2 + 3 + use base64::Engine; 4 + use base64::prelude::BASE64_URL_SAFE_NO_PAD; 5 + use ed25519_dalek::SigningKey; 6 + use ed25519_dalek::pkcs8::DecodePrivateKey; 7 + 8 + use crate::CryptoError; 9 + use crate::jwk::Jwk; 10 + 11 + #[derive(Clone)] 12 + pub struct SigningKeyPair { 13 + #[allow(dead_code)] 14 + signing_key: SigningKey, 15 + kid: String, 16 + x: String, 17 + } 18 + 19 + impl fmt::Display for SigningKeyPair { 20 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 + write!(f, "SigningKeyPair(kid: {})", self.kid) 22 + } 23 + } 24 + 25 + impl SigningKeyPair { 26 + pub fn from_pem(kid: &str, priv_pem: &str) -> Result<Self, CryptoError> { 27 + let signing_key = SigningKey::from_pkcs8_pem(priv_pem) 28 + .map_err(|e| CryptoError::InvalidKey(format!("Failed to create signing key: {e}")))?; 29 + 30 + let public_key = signing_key.verifying_key(); 31 + let kid = kid.to_string(); 32 + 33 + let x = BASE64_URL_SAFE_NO_PAD.encode(public_key.to_bytes()); 34 + 35 + Ok(Self { 36 + kid, 37 + signing_key, 38 + x, 39 + }) 40 + } 41 + } 42 + 43 + impl From<SigningKeyPair> for Jwk { 44 + fn from(value: SigningKeyPair) -> Jwk { 45 + Jwk { 46 + kid: Some(value.kid), 47 + kty: "OKP".to_string(), 48 + key_use: Some("sig".to_string()), 49 + alg: Some("EdDSA".to_string()), 50 + crv: Some("Ed25519".to_string()), 51 + x: Some(value.x), 52 + } 53 + } 54 + } 55 + 56 + impl From<&SigningKeyPair> for Jwk { 57 + fn from(value: &SigningKeyPair) -> Jwk { 58 + From::from(value.clone()) 59 + } 60 + }
+1
crates/server/Cargo.toml
··· 11 11 [dependencies] 12 12 lucid-api.workspace = true 13 13 lucid-core.workspace = true 14 + lucid-crypto.workspace = true 14 15 lucid-server-config.workspace = true 15 16 16 17 axum.workspace = true
+1
crates/server/config/Cargo.toml
··· 6 6 7 7 [dependencies] 8 8 confique.workspace = true 9 + serde.workspace = true
+96
crates/server/config/src/lib.rs
··· 1 1 use std::{fmt, path::PathBuf, sync::OnceLock}; 2 2 3 3 use confique::Config; 4 + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; 4 5 5 6 static CONFIG: OnceLock<LucidConfig> = OnceLock::new(); 6 7 ··· 64 65 pub struct LucidConfig { 65 66 #[config(nested)] 66 67 pub server: ServerConfig, 68 + #[config(nested)] 69 + pub secrets: SecretsConfig, 67 70 } 68 71 69 72 #[derive(Debug, Config)] ··· 75 78 /// Port to bind the HTTP server to. 76 79 #[config(env = "LUCID_SERVER_PORT", default = 8080)] 77 80 pub port: u16, 81 + } 82 + 83 + #[derive(Debug, Config)] 84 + pub struct SecretsConfig { 85 + /// Signing keys used for signing tokens. Each key must have a unique `key_id`. 86 + pub signing_keys: Vec<CryptoKeyConfig>, 87 + 88 + /// Encryption keys used for encrypting values. Each key must have a unique `key_id`. 89 + pub encryption_keys: Vec<CryptoKeyConfig>, 90 + } 91 + 92 + #[derive(Debug, Config, Deserialize)] 93 + pub struct CryptoKeyConfig { 94 + /// The ID of the key, used to identify it. 95 + pub key_id: String, 96 + // /// The public key in PEM format, or a path to a file containing the public key PEM. 97 + // pub public_key_pem: StringOrPath, 98 + /// The private key in PEM format, or a path to a file containing the private key PEM. 99 + pub private_key_pem: StringOrPath, 100 + } 101 + 102 + #[derive(Clone)] 103 + pub enum StringOrPath { 104 + String(String), 105 + Path { path: PathBuf }, 106 + } 107 + 108 + impl Serialize for StringOrPath { 109 + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 110 + where 111 + S: Serializer, 112 + { 113 + match self { 114 + StringOrPath::String(s) => s.serialize(serializer), 115 + StringOrPath::Path { path } => { 116 + use serde::ser::SerializeMap; 117 + let mut map = serializer.serialize_map(Some(1))?; 118 + map.serialize_entry("path", path)?; 119 + map.end() 120 + } 121 + } 122 + } 123 + } 124 + 125 + impl<'de> Deserialize<'de> for StringOrPath { 126 + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 127 + where 128 + D: Deserializer<'de>, 129 + { 130 + struct StringOrPathVisitor; 131 + 132 + impl<'de> de::Visitor<'de> for StringOrPathVisitor { 133 + type Value = StringOrPath; 134 + 135 + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 136 + formatter.write_str("a string or a table with a 'path' key") 137 + } 138 + 139 + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> 140 + where 141 + E: de::Error, 142 + { 143 + Ok(StringOrPath::String(v.to_owned())) 144 + } 145 + 146 + fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error> 147 + where 148 + M: de::MapAccess<'de>, 149 + { 150 + let mut path: Option<PathBuf> = None; 151 + while let Some(key) = map.next_key::<String>()? { 152 + if key == "path" { 153 + path = Some(map.next_value()?); 154 + } else { 155 + map.next_value::<de::IgnoredAny>()?; 156 + } 157 + } 158 + let path = path.ok_or_else(|| de::Error::missing_field("path"))?; 159 + Ok(StringOrPath::Path { path }) 160 + } 161 + } 162 + 163 + deserializer.deserialize_any(StringOrPathVisitor) 164 + } 165 + } 166 + 167 + impl fmt::Debug for StringOrPath { 168 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 169 + match self { 170 + StringOrPath::String(_) => write!(f, "String(**REDACTED**)"), 171 + StringOrPath::Path { path } => write!(f, "Path({path:?})"), 172 + } 173 + } 78 174 } 79 175 80 176 /// Generate a TOML configuration template with all available options,
+21 -3
crates/server/src/main.rs
··· 3 3 use clap::{Parser, Subcommand}; 4 4 use lucid_api::state::AppState; 5 5 use lucid_core::BUILD_VERSION; 6 + use lucid_crypto::signing::SigningKeyPair; 7 + use lucid_server_config::StringOrPath; 6 8 use tokio_util::sync::CancellationToken; 7 9 use tracing::{error, info}; 8 10 ··· 69 71 70 72 spawn_signal_handler(shutdown.clone()); 71 73 72 - let state = AppState::new(shutdown.clone()).await?; 74 + let mut state = AppState::new(shutdown.clone()).await?; 73 75 74 - let app = lucid_api::app(state); 76 + let cfg = lucid_server_config::get(); 75 77 76 - let cfg = lucid_server_config::get(); 78 + for key in cfg.secrets.signing_keys.iter() { 79 + let pkey = match key.private_key_pem { 80 + StringOrPath::String(ref s) => s.clone(), 81 + StringOrPath::Path { ref path } => { 82 + std::fs::read_to_string(path).map_err(|e| { 83 + format!("Failed to read signing key {} from {}: {}", key.key_id, path.display(), e) 84 + })? 85 + } 86 + }; 87 + 88 + let signing_key = SigningKeyPair::from_pem(&key.key_id, &pkey) 89 + .map_err(|e| format!("Failed to load signing key {}: {}", key.key_id, e))?; 90 + 91 + state.add_signing_key(signing_key); 92 + } 93 + 94 + let app = lucid_api::app(state); 77 95 78 96 let host = &cfg.server.host; 79 97 let port = cfg.server.port;