diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6be473..d6df4fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: features: - - name: "default" + - name: "default (aws-lc-rs for crypto)" - name: "no-features" cargo-flags: "--no-default-features" @@ -36,6 +36,9 @@ jobs: - name: "native-tls" cargo-flags: "--no-default-features --features native-tls" + - name: "openssl crypto" + cargo-flags: "--no-default-features --features hyper-rustls-native-roots,crypto-openssl" + name: CI (${{ matrix.features.name }}) steps: diff --git a/contract-tests/Cargo.toml b/contract-tests/Cargo.toml index e7dec85..ba07039 100644 --- a/contract-tests/Cargo.toml +++ b/contract-tests/Cargo.toml @@ -23,7 +23,10 @@ reqwest = { version = "0.12.4", features = ["default", "blocking", "json"] } async-mutex = "1.4.0" [features] -default = ["hyper"] +default = ["hyper", "crypto-aws-lc-rs"] + +crypto-aws-lc-rs = ["launchdarkly-server-sdk/crypto-aws-lc-rs"] +crypto-openssl = ["launchdarkly-server-sdk/crypto-openssl"] hyper = [ "launchdarkly-sdk-transport/hyper", diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index 14dffed..ee9036a 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -15,9 +15,11 @@ use launchdarkly_server_sdk::{ ServiceEndpointsBuilder, StreamingDataSourceBuilder, }; +#[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] +use crate::command_params::SecureModeHashResponse; use crate::command_params::{ ContextBuildParams, ContextConvertParams, ContextParam, ContextResponse, - MigrationOperationResponse, MigrationVariationResponse, SecureModeHashResponse, + MigrationOperationResponse, MigrationVariationResponse, }; use crate::HttpsConnector; use crate::{ @@ -221,14 +223,17 @@ impl ClientEntity { ContextResponse::from(Self::context_convert(params)), ))) } + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] "secureModeHash" => { let params = command .secure_mode_hash .ok_or("secureModeHash params should be set")?; + let hash = self + .client + .secure_mode_hash(¶ms.context) + .map_err(|e| e.to_string())?; Ok(Some(CommandResponse::SecureModeHash( - SecureModeHashResponse { - result: self.client.secure_mode_hash(¶ms.context), - }, + SecureModeHashResponse { result: hash }, ))) } "migrationVariation" => { diff --git a/contract-tests/src/command_params.rs b/contract-tests/src/command_params.rs index 5bbf495..2d19593 100644 --- a/contract-tests/src/command_params.rs +++ b/contract-tests/src/command_params.rs @@ -10,6 +10,7 @@ pub enum CommandResponse { EvaluateFlag(EvaluateFlagResponse), EvaluateAll(EvaluateAllFlagsResponse), ContextBuildOrConvert(ContextResponse), + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] SecureModeHash(SecureModeHashResponse), MigrationVariation(MigrationVariationResponse), MigrationOperation(MigrationOperationResponse), @@ -25,6 +26,7 @@ pub struct CommandParams { pub identify_event: Option, pub context_build: Option, pub context_convert: Option, + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] pub secure_mode_hash: Option, pub migration_variation: Option, pub migration_operation: Option, @@ -126,12 +128,14 @@ pub struct ContextConvertParams { pub input: String, } +#[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SecureModeHashParams { pub context: Context, } +#[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] #[derive(Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SecureModeHashResponse { diff --git a/contract-tests/src/main.rs b/contract-tests/src/main.rs index 2d00e03..480b2ad 100644 --- a/contract-tests/src/main.rs +++ b/contract-tests/src/main.rs @@ -103,6 +103,7 @@ async fn status() -> impl Responder { "tags".to_string(), "service-endpoints".to_string(), "context-type".to_string(), + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] "secure-mode-hash".to_string(), "inline-context-all".to_string(), "anonymous-redaction".to_string(), diff --git a/launchdarkly-server-sdk/Cargo.toml b/launchdarkly-server-sdk/Cargo.toml index 89e8996..e21934c 100644 --- a/launchdarkly-server-sdk/Cargo.toml +++ b/launchdarkly-server-sdk/Cargo.toml @@ -39,7 +39,8 @@ bytes = "1.11" bitflags = "2.4" rand = "0.9" flate2 = { version = "1.0.35", optional = true } -aws-lc-rs = "1.14.1" +aws-lc-rs = { version = "1.14.1", optional = true } +openssl = { version = "0.10.75", optional = true } [dev-dependencies] maplit = "1.0.1" @@ -54,7 +55,10 @@ reqwest = { version = "0.12.4", features = ["json"] } testing_logger = "0.1.1" [features] -default = ["hyper-rustls-native-roots"] +default = ["hyper-rustls-native-roots", "crypto-aws-lc-rs"] + +crypto-aws-lc-rs = ["dep:aws-lc-rs"] +crypto-openssl = ["dep:openssl"] hyper = [ "launchdarkly-sdk-transport/hyper", diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 39af0d8..c9b5a3d 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -156,6 +156,10 @@ pub struct Client { started: AtomicBool, offline: bool, daemon_mode: bool, + #[cfg_attr( + not(any(feature = "crypto-openssl", feature = "crypto-aws-lc-rs")), + allow(dead_code) + )] sdk_key: String, shutdown_broadcast: broadcast::Sender<()>, runtime: RwLock>, @@ -585,15 +589,39 @@ impl Client { .try_map(|val| val.as_json(), default, eval::Error::WrongType) } + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] /// Generates the secure mode hash value for a context. /// /// For more information, see the Reference Guide: /// . - pub fn secure_mode_hash(&self, context: &Context) -> String { - let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, self.sdk_key.as_bytes()); - let tag = aws_lc_rs::hmac::sign(&key, context.canonical_key().as_bytes()); - - data_encoding::HEXLOWER.encode(tag.as_ref()) + pub fn secure_mode_hash(&self, context: &Context) -> Result { + #[cfg(feature = "crypto-aws-lc-rs")] + { + let key = + aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, self.sdk_key.as_bytes()); + let tag = aws_lc_rs::hmac::sign(&key, context.canonical_key().as_bytes()); + + Ok(data_encoding::HEXLOWER.encode(tag.as_ref())) + } + #[cfg(feature = "crypto-openssl")] + { + use openssl::hash::MessageDigest; + use openssl::pkey::PKey; + use openssl::sign::Signer; + + let key = PKey::hmac(self.sdk_key.as_bytes()) + .map_err(|e| format!("Failed to create HMAC key: {e}"))?; + let mut signer = Signer::new(MessageDigest::sha256(), &key) + .map_err(|e| format!("Failed to create signer: {e}"))?; + signer + .update(context.canonical_key().as_bytes()) + .map_err(|e| format!("Failed to update signer: {e}"))?; + let hmac = signer + .sign_to_vec() + .map_err(|e| format!("Failed to sign: {e}"))?; + + Ok(data_encoding::HEXLOWER.encode(&hmac)) + } } /// Returns an object that encapsulates the state of all feature flags for a given context. This @@ -1830,6 +1858,7 @@ mod tests { } #[test] + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] fn secure_mode_hash() { let config = ConfigBuilder::new("secret") .offline(true) @@ -1841,12 +1870,15 @@ mod tests { .expect("Failed to create context"); assert_eq!( - client.secure_mode_hash(&context), + client + .secure_mode_hash(&context) + .expect("Hash should be computed"), "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597" ); } #[test] + #[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))] fn secure_mode_hash_with_multi_kind() { let config = ConfigBuilder::new("secret") .offline(true) @@ -1869,7 +1901,9 @@ mod tests { .expect("failed to build multi-context"); assert_eq!( - client.secure_mode_hash(&context), + client + .secure_mode_hash(&context) + .expect("Hash should be computed"), "5687e6383b920582ed50c2a96c98a115f1b6aad85a60579d761d9b8797415163" ); } @@ -2740,4 +2774,21 @@ mod tests { fn make_mocked_client() -> (Client, Receiver) { make_mocked_client_with_delay(0, false, false) } + + #[test] + fn client_builds_successfully() { + let config = ConfigBuilder::new("sdk-key") + .offline(true) + .build() + .expect("config should build"); + + let client = Client::build(config).expect("client should build successfully"); + + assert!( + !client.started.load(Ordering::SeqCst), + "client should not be started yet" + ); + assert!(client.offline, "client should be in offline mode"); + assert_eq!(client.sdk_key, "sdk-key", "sdk_key should match"); + } }