Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion contract-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 9 additions & 4 deletions contract-tests/src/client_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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(&params.context)
.map_err(|e| e.to_string())?;
Ok(Some(CommandResponse::SecureModeHash(
SecureModeHashResponse {
result: self.client.secure_mode_hash(&params.context),
},
SecureModeHashResponse { result: hash },
)))
}
"migrationVariation" => {
Expand Down
4 changes: 4 additions & 0 deletions contract-tests/src/command_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -25,6 +26,7 @@ pub struct CommandParams {
pub identify_event: Option<IdentifyEventParams>,
pub context_build: Option<ContextBuildParams>,
pub context_convert: Option<ContextConvertParams>,
#[cfg(any(feature = "crypto-aws-lc-rs", feature = "crypto-openssl"))]
pub secure_mode_hash: Option<SecureModeHashParams>,
pub migration_variation: Option<MigrationVariationParams>,
pub migration_operation: Option<MigrationOperationParams>,
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions contract-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
8 changes: 6 additions & 2 deletions launchdarkly-server-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
65 changes: 58 additions & 7 deletions launchdarkly-server-sdk/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Runtime>>,
Expand Down Expand Up @@ -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:
/// <https://docs.launchdarkly.com/sdk/features/secure-mode#rust>.
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<String, String> {
#[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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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"
);
}
Expand Down Expand Up @@ -2740,4 +2774,21 @@ mod tests {
fn make_mocked_client() -> (Client, Receiver<OutputEvent>) {
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");
}
}