diff --git a/Cargo.lock b/Cargo.lock index 2ea45e1..6584473 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,12 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -602,6 +608,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -625,6 +643,7 @@ dependencies = [ "fiat-crypto", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -702,7 +721,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -728,12 +749,27 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ + "pkcs8", "signature", ] @@ -745,9 +781,11 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ "curve25519-dalek", "ed25519", + "serde", "sha2", "signature", "subtle", + "zeroize", ] [[package]] @@ -756,6 +794,27 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email_address" version = "0.2.9" @@ -821,6 +880,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -996,6 +1065,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1035,6 +1105,17 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.4.15" @@ -1102,6 +1183,24 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "1.5.0" @@ -1546,11 +1645,38 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "jsonwebtoken" +version = "11.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881733cbc631fc9e472e24447ce32a64bedf2da498d6d8570b08edc87de71f65" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.7", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", + "zeroize", +] + [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1558,6 +1684,12 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1722,6 +1854,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.7", + "smallvec", + "zeroize", +] + [[package]] name = "num-cmp" version = "0.1.0" @@ -1780,6 +1928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1901,6 +2050,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "page_size" version = "0.6.0" @@ -1977,6 +2150,16 @@ dependencies = [ "serde", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2029,6 +2212,17 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -2122,6 +2316,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -2435,6 +2638,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -2449,6 +2662,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -2577,6 +2810,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2749,6 +2996,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.12" @@ -2771,6 +3030,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spki" version = "0.7.3" @@ -2850,6 +3115,7 @@ dependencies = [ "code0-flow", "env_logger", "futures-lite", + "jsonwebtoken", "log", "opentelemetry", "prost", @@ -3899,6 +4165,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 2305a3c..49e69db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ lru = "0.18.2" syn = { version = "3", features = ["full", "extra-traits"] } quote = "1" proc-macro2 = "1" +jsonwebtoken = { version = "11.0.0", features = ["rust_crypto"] } [workspace.dependencies.taurus-core] path = "./crates/taurus-core" diff --git a/crates/taurus/Cargo.toml b/crates/taurus/Cargo.toml index 422424e..7d99694 100644 --- a/crates/taurus/Cargo.toml +++ b/crates/taurus/Cargo.toml @@ -20,7 +20,8 @@ taurus-core = { workspace = true } taurus-provider = { workspace = true } uuid = { workspace = true } env_logger = { workspace = true } +jsonwebtoken = { workspace = true } +serde = { workspace = true } [dev-dependencies] -serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/taurus/src/app/definitions.rs b/crates/taurus/src/app/definitions.rs index 9f970d4..c6d50aa 100644 --- a/crates/taurus/src/app/definitions.rs +++ b/crates/taurus/src/app/definitions.rs @@ -14,6 +14,7 @@ use tonic::{Extensions, Request}; use tucana::aquila::{ModuleUpdateRequest, module_service_client::ModuleServiceClient}; use tucana::shared::Module; +use crate::auth::aquila_jwt; use crate::config::Config; use crate::telemetry::errors; @@ -71,7 +72,9 @@ pub async fn push_definitions_until_success(config: &Config) { let mut retry_count = 1; loop { let request = Request::from_parts( - code0_flow::flow_service::auth::get_authorization_metadata(&config.aquila_token), + code0_flow::flow_service::auth::get_authorization_metadata(&aquila_jwt( + &config.aquila_token, + )), Extensions::new(), ModuleUpdateRequest { modules: modules.clone(), diff --git a/crates/taurus/src/auth.rs b/crates/taurus/src/auth.rs new file mode 100644 index 0000000..0c402ad --- /dev/null +++ b/crates/taurus/src/auth.rs @@ -0,0 +1,91 @@ +//! Mints the short-lived JWT Taurus presents to Aquila's gRPC services. +//! +//! Aquila's service configuration pre-provisions a secret per identity +//! (still called `token` on both sides for historical reasons); Taurus signs +//! an HS256 JWT with that secret instead of sending it as a plain bearer +//! credential, so Aquila can verify the request without the secret itself +//! ever crossing the wire. + +use std::time::{SystemTime, UNIX_EPOCH}; + +use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; +use serde::Serialize; + +/// The identity every `taurus-*` instance authenticates as - matches the +/// `taurus` family identifier Aquila's service configuration registers a +/// runtime secret under (see `ServiceConfiguration::extract_service_name`). +const SUBJECT: &str = "taurus"; + +/// How long a minted JWT stays valid. Short-lived since a fresh one is +/// minted for every outgoing request rather than reused across a +/// long-lived connection. +const TTL_SECONDS: u64 = 60; + +#[derive(Serialize)] +struct Claims { + sub: String, + exp: u64, +} + +/// Encodes a fresh JWT authenticating Taurus to Aquila, signed with +/// `aquila_token` (the shared secret from `Config::aquila_token`). +pub fn aquila_jwt(aquila_token: &str) -> String { + let claims = Claims { + sub: SUBJECT.to_string(), + exp: now_unix_seconds().saturating_add(TTL_SECONDS), + }; + + encode( + &Header::new(Algorithm::HS256), + &claims, + &EncodingKey::from_secret(aquila_token.as_bytes()), + ) + .unwrap_or_else(|error| panic!("failed to encode Aquila authentication JWT: {error}")) +} + +fn now_unix_seconds() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use jsonwebtoken::{DecodingKey, Validation, decode}; + + #[derive(serde::Deserialize)] + struct DecodedClaims { + sub: String, + exp: u64, + } + + #[test] + fn aquila_jwt_is_signed_with_the_given_secret_and_carries_the_taurus_subject() { + let token = aquila_jwt("shared-secret"); + + let data = decode::( + &token, + &DecodingKey::from_secret(b"shared-secret"), + &Validation::new(Algorithm::HS256), + ) + .unwrap(); + + assert_eq!(data.claims.sub, "taurus"); + assert!(data.claims.exp > now_unix_seconds()); + } + + #[test] + fn aquila_jwt_does_not_verify_against_a_different_secret() { + let token = aquila_jwt("shared-secret"); + + let result = decode::( + &token, + &DecodingKey::from_secret(b"wrong-secret"), + &Validation::new(Algorithm::HS256), + ); + + assert!(result.is_err()); + } +} diff --git a/crates/taurus/src/client/runtime_execution.rs b/crates/taurus/src/client/runtime_execution.rs index d86d74e..e45485a 100644 --- a/crates/taurus/src/client/runtime_execution.rs +++ b/crates/taurus/src/client/runtime_execution.rs @@ -15,6 +15,7 @@ use tucana::{ shared::{ExecutionResult, execution_result}, }; +use crate::auth::aquila_jwt; use crate::telemetry::errors; // tonic clients over `Channel` are cheap to clone (the channel itself is a @@ -55,7 +56,7 @@ impl TaurusRuntimeExecutionService { normalize_execution_result(&mut runtime_execution); let request = Request::from_parts( - get_authorization_metadata(&self.aquila_token), + get_authorization_metadata(&aquila_jwt(&self.aquila_token)), Extensions::new(), ExecutionRequest { execution_result: Some(runtime_execution), diff --git a/crates/taurus/src/client/runtime_status.rs b/crates/taurus/src/client/runtime_status.rs index 6b017c3..7cdfa26 100644 --- a/crates/taurus/src/client/runtime_status.rs +++ b/crates/taurus/src/client/runtime_status.rs @@ -15,6 +15,7 @@ use tucana::{ shared::{ModuleStatus, module_status::StatusVariant}, }; +use crate::auth::aquila_jwt; use crate::telemetry::errors; pub struct TaurusRuntimeStatusService { @@ -51,7 +52,7 @@ impl TaurusRuntimeStatusService { for request in build_runtime_status_requests(&self.identifiers, status, timestamp) { let request = Request::from_parts( - get_authorization_metadata(&self.aquila_token), + get_authorization_metadata(&aquila_jwt(&self.aquila_token)), Extensions::new(), request, ); diff --git a/crates/taurus/src/main.rs b/crates/taurus/src/main.rs index 29ca32c..96d1c5d 100644 --- a/crates/taurus/src/main.rs +++ b/crates/taurus/src/main.rs @@ -7,6 +7,7 @@ //! for the environment-driven `Config` every other module reads from. mod app; +mod auth; mod client; mod config; mod telemetry;