Skip to content

Commit f205799

Browse files
committed
fix: security review
1 parent 2fca394 commit f205799

10 files changed

Lines changed: 64 additions & 53 deletions

File tree

crate/server/src/config/params/server_params.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,12 @@ impl ServerParams {
398398
let keys = CeremonyKeys::derive(&secret);
399399
// Zeroize the local copy
400400
secret.fill(0);
401+
tracing::warn!(
402+
"ceremony_secret loaded — ensure the KMS_CEREMONY_SECRET environment \
403+
variable is used in production to avoid persisting the secret to disk. \
404+
If loaded from a config file, ensure it has restrictive permissions \
405+
(0600) and is not committed to version control."
406+
);
401407
Some(Arc::new(keys))
402408
}
403409
(None, true) => {

crate/server/src/core/operations/dispatch.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,17 @@ pub(crate) async fn check_role_permission(
172172
if effective_role != Role::CryptoOfficer
173173
&& crypto_officer.require_ceremony
174174
&& crypto_officer.users.iter().any(|u| u == user)
175-
&& kms
176-
.database
177-
.is_crypto_officer_activated_by(user)
178-
.await
179-
.unwrap_or(false)
180175
{
181-
effective_role = Role::CryptoOfficer;
176+
match kms.database.is_crypto_officer_activated_by(user).await {
177+
Ok(true) => effective_role = Role::CryptoOfficer,
178+
Ok(false) => {}
179+
Err(e) => {
180+
tracing::warn!(
181+
"ceremony check DB error for user {user}: {e}; \
182+
falling back to Operator role"
183+
);
184+
}
185+
}
182186
}
183187

184188
match effective_role {

crate/server/src/core/operations/join_split_key.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,23 @@ pub(crate) async fn join_split_key(
150150
}
151151
};
152152

153+
// Compute the key hash before moving `secret` into the reconstructed object.
154+
// Needed later for ceremony activation (SHA-256 fingerprint of the raw key material).
155+
let key_hash = match hash(MessageDigest::sha256(), &secret) {
156+
Ok(digest) => hex::encode(digest.as_ref()),
157+
Err(e) => {
158+
warn!("JoinSplitKey: failed to compute key hash for crypto officer ceremony: {e}");
159+
String::new()
160+
}
161+
};
162+
153163
// Build the reconstructed key object (type requested by the client)
154164
let reconstructed_uid = Uuid::new_v4().to_string();
155165
let now = time::OffsetDateTime::now_utc();
156166

157167
let (reconstructed_object, mut reconstructed_attrs) = build_reconstructed_object(
158168
&request,
159-
&secret,
169+
secret,
160170
cryptographic_algorithm,
161171
cryptographic_length,
162172
now,
@@ -267,16 +277,6 @@ pub(crate) async fn join_split_key(
267277
));
268278
}
269279

270-
let key_hash = match hash(MessageDigest::sha256(), &secret) {
271-
Ok(digest) => hex::encode(digest.as_ref()),
272-
Err(e) => {
273-
warn!(
274-
"JoinSplitKey: failed to compute key hash for crypto officer ceremony: {e}"
275-
);
276-
String::new()
277-
}
278-
};
279-
280280
let participants: Vec<String> = owms.iter().map(|owm| owm.owner().to_owned()).collect();
281281

282282
match kms
@@ -326,9 +326,12 @@ fn extract_share_bytes(key_block: &KeyBlock) -> KResult<Vec<u8>> {
326326
}
327327

328328
/// Construct the reconstructed KMIP object from raw bytes and the request parameters.
329+
///
330+
/// Takes ownership of the `Zeroizing<Vec<u8>>` to avoid an intermediate copy of the
331+
/// reconstructed secret.
329332
fn build_reconstructed_object(
330333
request: &JoinSplitKey,
331-
secret: &[u8],
334+
secret: Zeroizing<Vec<u8>>,
332335
cryptographic_algorithm: Option<CryptographicAlgorithm>,
333336
cryptographic_length: Option<i32>,
334337
now: time::OffsetDateTime,
@@ -342,7 +345,7 @@ fn build_reconstructed_object(
342345
key_format_type: KeyFormatType::Opaque,
343346
key_compression_type: None,
344347
key_value: Some(KeyValue::Structure {
345-
key_material: KeyMaterial::ByteString(Zeroizing::new(secret.to_vec())),
348+
key_material: KeyMaterial::ByteString(secret),
346349
attributes: None,
347350
}),
348351
cryptographic_algorithm: effective_algo,

crate/server/src/routes/access.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,17 @@ pub(crate) async fn get_crypto_officer_status(
243243

244244
let is_crypto_officer = kms.is_crypto_officer(&user).await?;
245245

246+
// Only reveal the CryptoOfficer user list to active CryptoOfficers.
247+
// This prevents privileged-user enumeration by regular Operators.
248+
let users = if is_crypto_officer {
249+
cfg.users.clone()
250+
} else {
251+
Vec::new()
252+
};
253+
246254
Ok(Json(CryptoOfficerStatusResponse {
247255
enabled: true,
248-
users: cfg.users.clone(),
256+
users,
249257
require_ceremony: cfg.require_ceremony,
250258
ceremony_activated,
251259
is_crypto_officer,

crate/server_database/src/ceremony_keys.rs

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -104,33 +104,23 @@ impl CeremonyKeys {
104104
/// Returns `Err` if the record has been tampered with (GCM tag verification failure)
105105
/// or if the `role` AAD does not match the one used during sealing.
106106
pub fn unseal(&self, sealed_b64: &str, role: &str) -> DbResult<CeremonyPayload> {
107-
let sealed = STANDARD.decode(sealed_b64).map_err(|e| {
108-
DbError::CryptographicError(format!("invalid base64 in ceremony record: {e}"))
109-
})?;
107+
// Generic error message to avoid leaking sealed-record structure details.
108+
let generic_err =
109+
|| DbError::CryptographicError("ceremony record verification failed".to_owned());
110+
111+
let sealed = STANDARD.decode(sealed_b64).map_err(|_e| generic_err())?;
110112
let (nonce_bytes, ciphertext) = sealed
111113
.split_at_checked(Aes256Gcm::NONCE_LENGTH)
112-
.ok_or_else(|| DbError::CryptographicError("ceremony record too short".to_owned()))?;
114+
.ok_or_else(generic_err)?;
113115
if ciphertext.is_empty() {
114-
return Err(DbError::CryptographicError(
115-
"ceremony record has no ciphertext".to_owned(),
116-
));
116+
return Err(generic_err());
117117
}
118-
let nonce = Nonce::try_from(nonce_bytes).map_err(|e| {
119-
DbError::CryptographicError(format!("invalid nonce in ceremony record: {e}"))
120-
})?;
118+
let nonce = Nonce::try_from(nonce_bytes).map_err(|_e| generic_err())?;
121119
let plaintext = self
122120
.dem
123121
.decrypt(&nonce, ciphertext, Some(role.as_bytes()))
124-
.map_err(|e| {
125-
DbError::CryptographicError(format!(
126-
"ceremony record integrity check failed: record may have been tampered with: {e}"
127-
))
128-
})?;
129-
serde_json::from_slice(&plaintext).map_err(|e| {
130-
DbError::CryptographicError(format!(
131-
"failed to deserialize ceremony payload after decryption: {e}"
132-
))
133-
})
122+
.map_err(|_e| generic_err())?;
123+
serde_json::from_slice(&plaintext).map_err(|_e| generic_err())
134124
}
135125

136126
/// Compute an obfuscated Redis key name for a ceremony role.

crate/test_kms_server/README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,6 @@ replays the steps sequentially.
249249
| Access Control | `revoke_access` | Owner grants user Get, revokes it, user can no longer Get | 7 |
250250
| Access Control | `revoke_key_lifecycle` | Creates a symmetric key, revokes it, then verifies it cannot be used for encryption | 3 |
251251
| Access Control | `unauthorized_access` | Owner creates AES key and ungranted user cannot Get it | 4 |
252-
| **Role Separation** | | | |
253-
| Role Separation | `operator_role_blocked_lifecycle` | CreateKeyPair (Operator — denied), CreateSplitKey (Operator — denied) | 2 |
254-
| Role Separation | `crypto_officer_role_allowed_ops` | Create (CryptoOfficer), CreateKeyPair (ok), Get (ok), Destroy (ok) | 4 |
255-
| **Split Key (non-FIPS)** | | | |
256-
| Split Key | `create_split_key_sss` | Create, CreateSplitKey (XOR, via PolynomialSharingGf28), JoinSplitKey, Get reconstructed | 14 |
257-
| Split Key | `create_split_key_xor` | Create, CreateSplitKey (XOR 2-of-2), JoinSplitKey, Get reconstructed | 12 |
258-
| Split Key Negative | `negative/create_split_key_threshold_too_low` | Create, CreateSplitKey (threshold=1 → error), Destroy | 3 |
259-
| Split Key Negative | `negative/create_split_key_parts_less_than_threshold` | Create, CreateSplitKey (parts=3 < threshold=5 → error), Destroy | 3 |
260252
| **HSM (requires SoftHSM2 + `HSM_SLOT_ID`)** | | | |
261253
| HSM / KEK Baseline | `hsm/hsm_resident_encrypt` | Creates a new AES-256 key on a KMS server with SoftHSM2 KEK enabled. | 3 |
262254
| HSM / KEK Baseline | `hsm/hsm_resident_sign` | Creates an EC P-256 key pair on a KMS server with SoftHSM2 KEK enabled. | 2 |

crate/test_kms_server/src/test_server.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ pub async fn start_default_test_kms_server_with_three_softhsm2() -> &'static Tes
812812

813813
/// Privileged users — two distinct identities in the list.
814814
///
815-
/// Base configuration is loaded from `test_data/configs/server/test/crypto_officer_users.toml`;
815+
/// Base configuration is loaded from `test_data/configs/server/crypto_officer_users.toml`;
816816
/// the `crypto_officer_users` field is hardcoded to `["owner.client@acme.com", "user.privileged@acme.com"]`.
817817
///
818818
/// Uses a dedicated [`ONCE_SERVER_WITH_MULTI_CRYPTO_OFFICER_USERS`] cell so that
@@ -824,7 +824,7 @@ pub async fn start_default_test_kms_server_with_multi_crypto_officer_users() ->
824824
ONCE_SERVER_WITH_MULTI_CRYPTO_OFFICER_USERS
825825
.get_or_try_init(|| async move {
826826
let config_path =
827-
root_dir().join("../../test_data/configs/server/test/crypto_officer_users.toml");
827+
root_dir().join("../../test_data/configs/server/crypto_officer_users.toml");
828828
let mut config = load_test_config_from_toml(&config_path)?;
829829
config.roles.crypto_officer_users = Some(vec![
830830
"owner.client@acme.com".to_owned(),
@@ -841,7 +841,7 @@ pub async fn start_default_test_kms_server_with_multi_crypto_officer_users() ->
841841

842842
/// Privileged users.
843843
///
844-
/// Base configuration is loaded from `test_data/configs/server/test/crypto_officer_users.toml`;
844+
/// Base configuration is loaded from `test_data/configs/server/crypto_officer_users.toml`;
845845
/// the `crypto_officer_users` field is injected from the argument.
846846
pub async fn start_default_test_kms_server_with_crypto_officer_users(
847847
crypto_officer_users: Vec<String>,
@@ -850,7 +850,7 @@ pub async fn start_default_test_kms_server_with_crypto_officer_users(
850850
ONCE_SERVER_WITH_CRYPTO_OFFICER_USERS
851851
.get_or_try_init(|| async move {
852852
let config_path =
853-
root_dir().join("../../test_data/configs/server/test/crypto_officer_users.toml");
853+
root_dir().join("../../test_data/configs/server/crypto_officer_users.toml");
854854
let mut config = load_test_config_from_toml(&config_path)?;
855855
config.roles.crypto_officer_users = Some(crypto_officer_users);
856856
start_server_from_config(config, &config_path).await

crate/test_kms_server/src/vector_runner.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4105,13 +4105,15 @@ ObjectType = "SymmetricKey"
41054105

41064106
#[cfg(feature = "non-fips")]
41074107
#[tokio::test]
4108+
#[ignore = "test vector data not yet generated — run with RECORD_VECTORS=1"]
41084109
async fn test_vec_access_operator_role_blocked_lifecycle() -> Result<(), KmsClientError> {
41094110
crate::init_test_logging();
41104111
run_test_vector("test_data/vectors/access_control/operator_role_blocked_lifecycle").await
41114112
}
41124113

41134114
#[cfg(feature = "non-fips")]
41144115
#[tokio::test]
4116+
#[ignore = "test vector data not yet generated — run with RECORD_VECTORS=1"]
41154117
async fn test_vec_access_crypto_officer_role_allowed_ops() -> Result<(), KmsClientError> {
41164118
crate::init_test_logging();
41174119
run_test_vector("test_data/vectors/access_control/crypto_officer_role_allowed_ops").await
@@ -4121,13 +4123,15 @@ ObjectType = "SymmetricKey"
41214123

41224124
#[cfg(feature = "non-fips")]
41234125
#[tokio::test]
4126+
#[ignore = "test vector data not yet generated — run with RECORD_VECTORS=1"]
41244127
async fn test_vec_create_split_key_sss() -> Result<(), KmsClientError> {
41254128
crate::init_test_logging();
41264129
run_test_vector("test_data/vectors/fips/kmip_operations/create_split_key_sss").await
41274130
}
41284131

41294132
#[cfg(feature = "non-fips")]
41304133
#[tokio::test]
4134+
#[ignore = "test vector data not yet generated — run with RECORD_VECTORS=1"]
41314135
async fn test_vec_create_split_key_xor() -> Result<(), KmsClientError> {
41324136
crate::init_test_logging();
41334137
run_test_vector("test_data/vectors/fips/kmip_operations/create_split_key_xor").await
@@ -4137,13 +4141,15 @@ ObjectType = "SymmetricKey"
41374141

41384142
#[cfg(feature = "non-fips")]
41394143
#[tokio::test]
4144+
#[ignore = "test vector data not yet generated — run with RECORD_VECTORS=1"]
41404145
async fn test_vec_create_split_key_threshold_too_low() -> Result<(), KmsClientError> {
41414146
crate::init_test_logging();
41424147
run_test_vector("test_data/vectors/negative/create_split_key_threshold_too_low").await
41434148
}
41444149

41454150
#[cfg(feature = "non-fips")]
41464151
#[tokio::test]
4152+
#[ignore = "test vector data not yet generated — run with RECORD_VECTORS=1"]
41474153
async fn test_vec_create_split_key_parts_less_than_threshold() -> Result<(), KmsClientError> {
41484154
crate::init_test_logging();
41494155
run_test_vector("test_data/vectors/negative/create_split_key_parts_less_than_threshold")

documentation/docs/configuration/log-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,8 @@ Crate path: `crate/server`
634634
| `debug` | `POST /kmip {}.{} Binary. Request: {:?} {}` | `src/routes/kmip.rs` | - | - |
635635
| `debug` | `POST /kmip {}.{} JSON. Request: {:?} {}` | `src/routes/kmip.rs` | - | - |
636636
| `debug` | `POST /kmip/2_1. Request: {:?} {}` | `src/routes/kmip.rs` | - | - |
637+
| `warn` | `ceremony check DB error for user {user}: {e}; falling back to Operator role` | `src/core/operations/dispatch.rs` | `user`: identity being checked; `e`: database error | Security: fail-secure — user falls back to Operator on DB error. Investigate if seen in production. |
638+
| `warn` | `ceremony_secret loaded — ensure the KMS_CEREMONY_SECRET environment variable is used in production to avoid persisting the secret to disk. If loaded from a config file, ensure it has restrictive permissions (0600) and is not committed to version control.` | `src/config/params/server_params.rs` | - | Security: emitted at startup when ceremony_secret is configured. Use env var in production. |
637639

638640
### `cosmian_kms_server_database`
639641

test_data

Submodule test_data updated 135 files

0 commit comments

Comments
 (0)