Skip to content

Commit 920a22a

Browse files
authored
fix(kms): key usage/state/spec validation + real ECDH DeriveSharedSecret (bug-hunt) (#2280)
2 parents a466d1f + 54ecdb2 commit 920a22a

6 files changed

Lines changed: 375 additions & 19 deletions

File tree

crates/fakecloud-e2e/tests/kms.rs

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -390,12 +390,31 @@ async fn kms_derive_shared_secret() {
390390
.unwrap();
391391
let key_id = resp.key_metadata().unwrap().key_id().to_string();
392392

393-
let fake_pub = Blob::new(vec![0x04; 65]); // Fake uncompressed EC point
393+
// Real ECDH requires a valid SubjectPublicKeyInfo for the counterparty;
394+
// use a second KEY_AGREEMENT key's public key.
395+
let peer = client
396+
.create_key()
397+
.key_usage(aws_sdk_kms::types::KeyUsageType::KeyAgreement)
398+
.key_spec(aws_sdk_kms::types::KeySpec::EccNistP256)
399+
.send()
400+
.await
401+
.unwrap();
402+
let peer_id = peer.key_metadata().unwrap().key_id().to_string();
403+
let peer_pub = client
404+
.get_public_key()
405+
.key_id(&peer_id)
406+
.send()
407+
.await
408+
.unwrap()
409+
.public_key()
410+
.unwrap()
411+
.clone();
412+
394413
let result = client
395414
.derive_shared_secret()
396415
.key_id(&key_id)
397416
.key_agreement_algorithm(aws_sdk_kms::types::KeyAgreementAlgorithmSpec::Ecdh)
398-
.public_key(fake_pub)
417+
.public_key(peer_pub)
399418
.send()
400419
.await
401420
.unwrap();
@@ -1151,3 +1170,124 @@ async fn kms_generate_data_key_binds_encryption_context() {
11511170
"decrypt with a different encryption context must fail"
11521171
);
11531172
}
1173+
1174+
#[tokio::test]
1175+
async fn kms_generate_data_key_rejects_non_encrypt_key() {
1176+
// Bug-hunt 1.18: GenerateDataKey must reject a key whose usage isn't
1177+
// ENCRYPT_DECRYPT (InvalidKeyUsageException), not silently succeed.
1178+
let server = helpers::TestServer::start().await;
1179+
let client = server.kms_client().await;
1180+
1181+
let key = client
1182+
.create_key()
1183+
.key_usage(aws_sdk_kms::types::KeyUsageType::SignVerify)
1184+
.key_spec(aws_sdk_kms::types::KeySpec::Rsa2048)
1185+
.send()
1186+
.await
1187+
.unwrap();
1188+
let key_id = key.key_metadata().unwrap().key_id().to_string();
1189+
1190+
let result = client
1191+
.generate_data_key()
1192+
.key_id(&key_id)
1193+
.key_spec(aws_sdk_kms::types::DataKeySpec::Aes256)
1194+
.send()
1195+
.await;
1196+
assert!(
1197+
result.is_err(),
1198+
"GenerateDataKey on a SIGN_VERIFY key must fail"
1199+
);
1200+
}
1201+
1202+
#[tokio::test]
1203+
async fn kms_create_key_rejects_incompatible_spec_usage() {
1204+
// Bug-hunt 1.20: an HMAC spec with ENCRYPT_DECRYPT usage is unusable and
1205+
// must be rejected at CreateKey.
1206+
let server = helpers::TestServer::start().await;
1207+
let client = server.kms_client().await;
1208+
1209+
let result = client
1210+
.create_key()
1211+
.key_spec(aws_sdk_kms::types::KeySpec::Hmac256)
1212+
.key_usage(aws_sdk_kms::types::KeyUsageType::EncryptDecrypt)
1213+
.send()
1214+
.await;
1215+
assert!(
1216+
result.is_err(),
1217+
"HMAC + ENCRYPT_DECRYPT is incompatible and must be rejected"
1218+
);
1219+
}
1220+
1221+
#[tokio::test]
1222+
async fn kms_enable_key_rejects_pending_deletion() {
1223+
// Bug-hunt 1.19: EnableKey must not resurrect a key scheduled for deletion.
1224+
let server = helpers::TestServer::start().await;
1225+
let client = server.kms_client().await;
1226+
1227+
let key = client.create_key().send().await.unwrap();
1228+
let key_id = key.key_metadata().unwrap().key_id().to_string();
1229+
1230+
client
1231+
.schedule_key_deletion()
1232+
.key_id(&key_id)
1233+
.pending_window_in_days(7)
1234+
.send()
1235+
.await
1236+
.unwrap();
1237+
1238+
let result = client.enable_key().key_id(&key_id).send().await;
1239+
assert!(
1240+
result.is_err(),
1241+
"EnableKey on a PendingDeletion key must fail (KMSInvalidStateException)"
1242+
);
1243+
}
1244+
1245+
#[tokio::test]
1246+
async fn kms_derive_shared_secret_converges() {
1247+
// Bug-hunt 1.21: two parties running DeriveSharedSecret with each other's
1248+
// public key must derive the SAME secret (real ECDH), which the prior
1249+
// SHA-256(seed || peer) construction did not.
1250+
let server = helpers::TestServer::start().await;
1251+
let client = server.kms_client().await;
1252+
1253+
async fn make_agreement_key(
1254+
c: &aws_sdk_kms::Client,
1255+
) -> (String, aws_sdk_kms::primitives::Blob) {
1256+
let k = c
1257+
.create_key()
1258+
.key_spec(aws_sdk_kms::types::KeySpec::EccNistP256)
1259+
.key_usage(aws_sdk_kms::types::KeyUsageType::KeyAgreement)
1260+
.send()
1261+
.await
1262+
.unwrap();
1263+
let id = k.key_metadata().unwrap().key_id().to_string();
1264+
let pk = c.get_public_key().key_id(&id).send().await.unwrap();
1265+
let spki = pk.public_key().unwrap().clone();
1266+
(id, spki)
1267+
}
1268+
let (id_a, pub_a) = make_agreement_key(&client).await;
1269+
let (id_b, pub_b) = make_agreement_key(&client).await;
1270+
1271+
let secret_a = client
1272+
.derive_shared_secret()
1273+
.key_id(&id_a)
1274+
.key_agreement_algorithm(aws_sdk_kms::types::KeyAgreementAlgorithmSpec::Ecdh)
1275+
.public_key(pub_b)
1276+
.send()
1277+
.await
1278+
.unwrap();
1279+
let secret_b = client
1280+
.derive_shared_secret()
1281+
.key_id(&id_b)
1282+
.key_agreement_algorithm(aws_sdk_kms::types::KeyAgreementAlgorithmSpec::Ecdh)
1283+
.public_key(pub_a)
1284+
.send()
1285+
.await
1286+
.unwrap();
1287+
1288+
assert_eq!(
1289+
secret_a.shared_secret().unwrap().as_ref(),
1290+
secret_b.shared_secret().unwrap().as_ref(),
1291+
"both parties must derive identical shared secrets"
1292+
);
1293+
}

crates/fakecloud-kms/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ aes-gcm = "0.10"
2929
rsa = { workspace = true }
3030
rand = "0.8"
3131
signature = "2"
32-
p256 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
33-
p384 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
32+
p256 = { version = "0.13", features = ["ecdsa", "ecdh", "pkcs8"] }
33+
p384 = { version = "0.13", features = ["ecdsa", "ecdh", "pkcs8"] }
3434
k256 = { version = "0.13", features = ["ecdsa", "pkcs8"] }
3535
p521 = { version = "0.13", features = ["ecdsa", "pkcs8"] }

crates/fakecloud-kms/src/helpers.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,78 @@ pub(crate) fn require_usable_key_state(key: &KmsKey) -> Result<(), AwsServiceErr
540540
))
541541
}
542542

543+
/// The KeyUsage values compatible with a given KeySpec, per AWS KMS. An
544+
/// empty slice means the spec is unknown (leave validation to later paths).
545+
fn allowed_usages_for_key_spec(key_spec: &str) -> &'static [&'static str] {
546+
match key_spec {
547+
"SYMMETRIC_DEFAULT" => &["ENCRYPT_DECRYPT"],
548+
s if s.starts_with("HMAC_") => &["GENERATE_VERIFY_MAC"],
549+
s if s.starts_with("RSA_") => &["ENCRYPT_DECRYPT", "SIGN_VERIFY"],
550+
// NIST curves support signing and key agreement.
551+
"ECC_NIST_P256" | "ECC_NIST_P384" | "ECC_NIST_P521" => &["SIGN_VERIFY", "KEY_AGREEMENT"],
552+
// secp256k1 supports signing only.
553+
"ECC_SECG_P256K1" => &["SIGN_VERIFY"],
554+
// SM2 (China regions) supports all three.
555+
"SM2" => &["SIGN_VERIFY", "ENCRYPT_DECRYPT", "KEY_AGREEMENT"],
556+
_ => &[],
557+
}
558+
}
559+
560+
/// CreateKey must reject a KeyUsage that is incompatible with the KeySpec
561+
/// (e.g. HMAC_256 + ENCRYPT_DECRYPT), which would otherwise create a key that
562+
/// no crypto operation can use. AWS returns ValidationException.
563+
pub(crate) fn validate_key_spec_usage(
564+
key_spec: &str,
565+
key_usage: &str,
566+
) -> Result<(), AwsServiceError> {
567+
let allowed = allowed_usages_for_key_spec(key_spec);
568+
if allowed.is_empty() || allowed.contains(&key_usage) {
569+
return Ok(());
570+
}
571+
Err(AwsServiceError::aws_error(
572+
StatusCode::BAD_REQUEST,
573+
"ValidationException",
574+
format!("KeyUsage {key_usage} is not compatible with KeySpec {key_spec}"),
575+
))
576+
}
577+
578+
/// EnableKey / DisableKey may only transition a key that is currently
579+
/// `Enabled` or `Disabled`. Any other lifecycle state
580+
/// (`PendingDeletion`, `PendingImport`, `Unavailable`, ...) is a
581+
/// `KMSInvalidStateException`; in particular a scheduled-for-deletion key must
582+
/// not be resurrected without CancelKeyDeletion.
583+
pub(crate) fn require_enable_disable_transition(key: &KmsKey) -> Result<(), AwsServiceError> {
584+
if key.key_state == "Enabled" || key.key_state == "Disabled" {
585+
return Ok(());
586+
}
587+
Err(AwsServiceError::aws_error(
588+
StatusCode::BAD_REQUEST,
589+
"KMSInvalidStateException",
590+
format!(
591+
"Key '{}' is not in a state that allows this operation (current state: {})",
592+
key.arn, key.key_state
593+
),
594+
))
595+
}
596+
597+
/// GenerateDataKey / GenerateDataKeyPair (and their `WithoutPlaintext`
598+
/// variants) require an `ENCRYPT_DECRYPT` key. A SIGN_VERIFY /
599+
/// GENERATE_VERIFY_MAC / KEY_AGREEMENT key must be rejected with
600+
/// `InvalidKeyUsageException`, matching the Encrypt/ReEncrypt paths.
601+
pub(crate) fn require_key_usage_encrypt_decrypt(key: &KmsKey) -> Result<(), AwsServiceError> {
602+
if key.key_usage != "ENCRYPT_DECRYPT" {
603+
return Err(AwsServiceError::aws_error(
604+
StatusCode::BAD_REQUEST,
605+
"InvalidKeyUsageException",
606+
format!(
607+
"The operation failed because the KMS key {} is not enabled for the requested operation. The key usage must be ENCRYPT_DECRYPT but is {}.",
608+
key.arn, key.key_usage
609+
),
610+
));
611+
}
612+
Ok(())
613+
}
614+
543615
pub(crate) fn validate_key_usage_signing(
544616
key: &KmsKey,
545617
resolved: &str,
@@ -923,3 +995,28 @@ pub(crate) fn action_matches(policy_action: &str, requested_action: &str) -> boo
923995
}
924996
false
925997
}
998+
999+
#[cfg(test)]
1000+
mod key_spec_usage_tests {
1001+
use super::validate_key_spec_usage;
1002+
1003+
#[test]
1004+
fn compatible_combos_pass() {
1005+
assert!(validate_key_spec_usage("SYMMETRIC_DEFAULT", "ENCRYPT_DECRYPT").is_ok());
1006+
assert!(validate_key_spec_usage("HMAC_256", "GENERATE_VERIFY_MAC").is_ok());
1007+
assert!(validate_key_spec_usage("RSA_2048", "SIGN_VERIFY").is_ok());
1008+
assert!(validate_key_spec_usage("RSA_2048", "ENCRYPT_DECRYPT").is_ok());
1009+
assert!(validate_key_spec_usage("ECC_NIST_P256", "KEY_AGREEMENT").is_ok());
1010+
assert!(validate_key_spec_usage("ECC_NIST_P256", "SIGN_VERIFY").is_ok());
1011+
}
1012+
1013+
#[test]
1014+
fn incompatible_combos_rejected() {
1015+
// HMAC key can't encrypt/decrypt.
1016+
assert!(validate_key_spec_usage("HMAC_256", "ENCRYPT_DECRYPT").is_err());
1017+
// Symmetric key can't sign.
1018+
assert!(validate_key_spec_usage("SYMMETRIC_DEFAULT", "SIGN_VERIFY").is_err());
1019+
// secp256k1 supports signing only.
1020+
assert!(validate_key_spec_usage("ECC_SECG_P256K1", "KEY_AGREEMENT").is_err());
1021+
}
1022+
}

crates/fakecloud-kms/src/service.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,9 @@ impl KmsService {
546546

547547
fn create_key(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
548548
let input = CreateKeyInput::from_body(&req.json_body())?;
549+
// Reject incompatible KeySpec/KeyUsage combinations (e.g. an HMAC spec
550+
// with ENCRYPT_DECRYPT) before minting a key no operation can use.
551+
validate_key_spec_usage(&input.key_spec, &input.key_usage)?;
549552

550553
let mut accounts = self.state.write();
551554
let state = accounts.get_or_create(&req.account_id);
@@ -825,6 +828,11 @@ impl KmsService {
825828
"Key state became inconsistent",
826829
)
827830
})?;
831+
// EnableKey is only valid from Enabled/Disabled. A key that is
832+
// PendingDeletion / PendingImport / Unavailable must not be silently
833+
// resurrected to Enabled — AWS returns KMSInvalidStateException (you
834+
// must CancelKeyDeletion / import material first).
835+
require_enable_disable_transition(key)?;
828836
key.enabled = true;
829837
key.key_state = "Enabled".to_string();
830838

@@ -844,6 +852,7 @@ impl KmsService {
844852
"Key state became inconsistent",
845853
)
846854
})?;
855+
require_enable_disable_transition(key)?;
847856
key.enabled = false;
848857
key.key_state = "Disabled".to_string();
849858

@@ -1209,6 +1218,9 @@ impl KmsService {
12091218
"Key state became inconsistent",
12101219
)
12111220
})?;
1221+
// Rotation cannot be toggled on a key pending deletion / import;
1222+
// AWS rejects with KMSInvalidStateException (Disabled -> DisabledException).
1223+
require_usable_key_state(key)?;
12121224
// RotationPeriodInDays is optional; AWS validates 90..=2560 and
12131225
// defaults to 365. Persist it so GetKeyRotationStatus echoes it back
12141226
// instead of dropping it (bug-audit 2026-06-20, 1.24).
@@ -1251,6 +1263,7 @@ impl KmsService {
12511263
"Key state became inconsistent",
12521264
)
12531265
})?;
1266+
require_usable_key_state(key)?;
12541267
key.key_rotation_enabled = false;
12551268

12561269
Ok(AwsResponse::json(StatusCode::OK, "{}"))
@@ -1269,6 +1282,7 @@ impl KmsService {
12691282
"Key state became inconsistent",
12701283
)
12711284
})?;
1285+
require_usable_key_state(key)?;
12721286

12731287
let rotation = KeyRotation {
12741288
key_id: key.key_id.clone(),

0 commit comments

Comments
 (0)