From 7ca4a0f8b8e0600fa022023185185a13a9170e83 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Fri, 20 Feb 2026 11:55:52 -0500 Subject: [PATCH 1/7] feat: Choose crypto library with crypto-(aws-lc-rs|openssl) features fixes #126 --- .github/workflows/ci.yml | 5 ++- contract-tests/Cargo.toml | 5 ++- contract-tests/src/client_entity.rs | 9 ++++-- contract-tests/src/command_params.rs | 1 + contract-tests/src/main.rs | 1 + launchdarkly-server-sdk/Cargo.toml | 8 +++-- launchdarkly-server-sdk/src/client.rs | 44 ++++++++++++++++++++++----- 7 files changed, 59 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6be473..7e1ce67 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,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..146abd1 100644 --- a/contract-tests/src/client_entity.rs +++ b/contract-tests/src/client_entity.rs @@ -221,14 +221,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..eea2996 100644 --- a/contract-tests/src/command_params.rs +++ b/contract-tests/src/command_params.rs @@ -25,6 +25,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, 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..39bef73 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -585,15 +585,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()); + + return 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}"))?; + + return Ok(data_encoding::HEXLOWER.encode(&hmac)); + } } /// Returns an object that encapsulates the state of all feature flags for a given context. This @@ -1830,6 +1854,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 +1866,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 +1897,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" ); } From 252dd3d2f85e81e2fd20650ee9243f70e370805d Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 23 Feb 2026 13:53:40 -0500 Subject: [PATCH 2/7] fix feature name --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e1ce67..d6df4fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: cargo-flags: "--no-default-features --features native-tls" - name: "openssl crypto" - cargo-flags: "--no-default-features --features hyper-rustls-native-roots,openssl" + cargo-flags: "--no-default-features --features hyper-rustls-native-roots,crypto-openssl" name: CI (${{ matrix.features.name }}) From 8afcc7eafd6bd233bb89cdf84402ef248d727e3c Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 23 Feb 2026 14:02:40 -0500 Subject: [PATCH 3/7] return not necessary --- launchdarkly-server-sdk/src/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 39bef73..2d5f17a 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -597,7 +597,7 @@ impl Client { 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()); - return Ok(data_encoding::HEXLOWER.encode(tag.as_ref())); + Ok(data_encoding::HEXLOWER.encode(tag.as_ref())) } #[cfg(feature = "crypto-openssl")] { @@ -616,7 +616,7 @@ impl Client { .sign_to_vec() .map_err(|e| format!("Failed to sign: {e}"))?; - return Ok(data_encoding::HEXLOWER.encode(&hmac)); + Ok(data_encoding::HEXLOWER.encode(&hmac)) } } From 93b4f59c174ffa843d1817f24cf3454d72c4ec88 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 23 Feb 2026 14:06:17 -0500 Subject: [PATCH 4/7] use sdk key --- launchdarkly-server-sdk/src/client.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 2d5f17a..83b592e 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -2770,4 +2770,18 @@ 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"); + } } From d3ca5ddde684ad658bc48df270b960ab2d9922ae Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 23 Feb 2026 14:08:24 -0500 Subject: [PATCH 5/7] cargo fmt --- launchdarkly-server-sdk/src/client.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 83b592e..e1caabd 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -2780,7 +2780,10 @@ mod tests { let client = Client::build(config).expect("client should build successfully"); - assert!(!client.started.load(Ordering::SeqCst), "client should not be started yet"); + 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"); } From 3c85ffc2bc641f7151a8079d4a58a99c90d64ec2 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 23 Feb 2026 14:26:24 -0500 Subject: [PATCH 6/7] clippy --- contract-tests/src/client_entity.rs | 4 +++- contract-tests/src/command_params.rs | 3 +++ launchdarkly-server-sdk/src/client.rs | 6 ++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/contract-tests/src/client_entity.rs b/contract-tests/src/client_entity.rs index 146abd1..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::{ diff --git a/contract-tests/src/command_params.rs b/contract-tests/src/command_params.rs index eea2996..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), @@ -127,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/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index e1caabd..3cf5170 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -156,6 +156,7 @@ 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>, @@ -2780,10 +2781,7 @@ mod tests { let client = Client::build(config).expect("client should build successfully"); - assert!( - !client.started.load(Ordering::SeqCst), - "client should not be started yet" - ); + 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"); } From b6507ae1be758930a22a35d067ab7c2fbace3c28 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 23 Feb 2026 14:27:33 -0500 Subject: [PATCH 7/7] cargo fmt again --- launchdarkly-server-sdk/src/client.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/launchdarkly-server-sdk/src/client.rs b/launchdarkly-server-sdk/src/client.rs index 3cf5170..c9b5a3d 100644 --- a/launchdarkly-server-sdk/src/client.rs +++ b/launchdarkly-server-sdk/src/client.rs @@ -156,7 +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))] + #[cfg_attr( + not(any(feature = "crypto-openssl", feature = "crypto-aws-lc-rs")), + allow(dead_code) + )] sdk_key: String, shutdown_broadcast: broadcast::Sender<()>, runtime: RwLock>, @@ -2781,7 +2784,10 @@ mod tests { let client = Client::build(config).expect("client should build successfully"); - assert!(!client.started.load(Ordering::SeqCst), "client should not be started yet"); + 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"); }