1use serde_json::Value;
58
59use crate::attestation::Pcrs;
60
61const EQ_OPERATORS: [&str; 2] = ["StringEquals", "StringEqualsIgnoreCase"];
65
66const FORBIDDEN_ACTIONS: [&str; 3] = [
69 "kms:putkeypolicy",
70 "kms:creategrant",
71 "kms:replicatekey",
72];
73
74#[derive(Debug, thiserror::Error, PartialEq, Eq)]
76pub enum PolicyError {
77 #[error("key policy is not valid JSON: {0}")]
79 Json(String),
80 #[error("key policy has no Statement element")]
82 NoStatements,
83 #[error("an Allow statement uses NotAction, which is too broad to bound")]
85 NotAction,
86 #[error("key policy grants wildcard action {0:?}; only explicit actions are allowed")]
88 WildcardAction(String),
89 #[error("key policy grants gate-loosening action {0:?} (could change or bypass the decrypt gate)")]
91 ForbiddenAction(String),
92 #[error("a statement allows kms:Decrypt without binding it to this enclave's PCR0/1/2")]
95 UngatedDecrypt,
96 #[error("kms:Decrypt is bound to PCR{index} {found:?}, not this enclave's {expected:?}")]
98 PcrMismatch {
99 index: u8,
101 found: String,
103 expected: String,
105 },
106}
107
108pub fn verify_decrypt_policy(policy_json: &str, own: &Pcrs) -> Result<(), PolicyError> {
113 let doc: Value =
114 serde_json::from_str(policy_json).map_err(|e| PolicyError::Json(e.to_string()))?;
115
116 let statements = match doc.get("Statement") {
117 Some(Value::Array(arr)) => arr.clone(),
118 Some(other) => vec![other.clone()],
119 None => return Err(PolicyError::NoStatements),
120 };
121
122 let want = [
123 ("0", 0u8, hex::encode(&own.pcr0)),
124 ("1", 1u8, hex::encode(&own.pcr1)),
125 ("2", 2u8, hex::encode(&own.pcr2)),
126 ];
127
128 for stmt in &statements {
129 if stmt.get("Effect").and_then(Value::as_str) != Some("Allow") {
133 continue;
134 }
135
136 if stmt.get("NotAction").is_some() {
139 return Err(PolicyError::NotAction);
140 }
141
142 let actions = normalize_actions(stmt.get("Action"));
143
144 for action in &actions {
145 if action.contains('*') {
146 return Err(PolicyError::WildcardAction(action.clone()));
147 }
148 if FORBIDDEN_ACTIONS.contains(&action.as_str())
149 || action.starts_with("kms:reencrypt")
150 {
151 return Err(PolicyError::ForbiddenAction(action.clone()));
152 }
153 }
154
155 if actions.iter().any(|a| a == "kms:decrypt") {
157 let bound = collect_pcr_bindings(stmt);
158 for (suffix, index, expected) in &want {
159 match bound.get(*suffix) {
160 None => return Err(PolicyError::UngatedDecrypt),
161 Some(found) => {
162 if !found.eq_ignore_ascii_case(expected) {
163 return Err(PolicyError::PcrMismatch {
164 index: *index,
165 found: found.clone(),
166 expected: expected.clone(),
167 });
168 }
169 }
170 }
171 }
172 }
173 }
174
175 Ok(())
176}
177
178fn normalize_actions(action: Option<&Value>) -> Vec<String> {
182 match action {
183 Some(Value::String(s)) => vec![s.to_ascii_lowercase()],
184 Some(Value::Array(arr)) => arr
185 .iter()
186 .filter_map(Value::as_str)
187 .map(|s| s.to_ascii_lowercase())
188 .collect(),
189 _ => Vec::new(),
190 }
191}
192
193fn collect_pcr_bindings(stmt: &Value) -> std::collections::BTreeMap<String, String> {
200 let mut out = std::collections::BTreeMap::new();
201 let Some(cond) = stmt.get("Condition").and_then(Value::as_object) else {
202 return out;
203 };
204 for (op, kv) in cond {
205 if !EQ_OPERATORS.contains(&op.as_str()) {
206 continue;
207 }
208 let Some(kv_map) = kv.as_object() else { continue };
209 for (key, val) in kv_map {
210 let suffix = key
211 .strip_prefix("kms:RecipientAttestation:PCR")
212 .or_else(|| key.strip_prefix("kms:RecipientAttestation:pcr"));
213 let Some(suffix) = suffix else { continue };
214 if let Value::String(s) = val {
217 out.insert(suffix.to_string(), s.clone());
218 }
219 }
220 }
221 out
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn pcrs(a: &[u8], b: &[u8], c: &[u8]) -> Pcrs {
229 Pcrs {
230 pcr0: a.to_vec(),
231 pcr1: b.to_vec(),
232 pcr2: c.to_vec(),
233 }
234 }
235
236 fn own() -> Pcrs {
238 pcrs(&[0xaa; 48], &[0xbb; 48], &[0xcc; 48])
239 }
240
241 fn hex_of(b: &[u8]) -> String {
242 hex::encode(b)
243 }
244
245 fn good_policy(own: &Pcrs) -> String {
248 serde_json::json!({
249 "Version": "2012-10-17",
250 "Statement": [
251 {
252 "Sid": "AccountKeyLifecycle",
253 "Effect": "Allow",
254 "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
255 "Action": [
256 "kms:DescribeKey",
257 "kms:GetKeyPolicy",
258 "kms:ScheduleKeyDeletion"
259 ],
260 "Resource": "*"
261 },
262 {
263 "Sid": "EnclaveGetPublicKey",
264 "Effect": "Allow",
265 "Principal": { "AWS": "arn:aws:iam::111122223333:role/enc" },
266 "Action": "kms:GetPublicKey",
267 "Resource": "*"
268 },
269 {
270 "Sid": "EnclaveAttestedDecrypt",
271 "Effect": "Allow",
272 "Principal": { "AWS": "arn:aws:iam::111122223333:role/enc" },
273 "Action": "kms:Decrypt",
274 "Resource": "*",
275 "Condition": {
276 "StringEqualsIgnoreCase": {
277 "kms:RecipientAttestation:PCR0": hex_of(&own.pcr0),
278 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
279 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2),
280 }
281 }
282 }
283 ]
284 })
285 .to_string()
286 }
287
288 #[test]
289 fn accepts_correct_policy() {
290 let own = own();
291 verify_decrypt_policy(&good_policy(&own), &own).expect("good policy accepted");
292 }
293
294 #[test]
295 fn accepts_correct_policy_case_insensitive_pcr_hex() {
296 let own = own();
299 let doc = serde_json::json!({
300 "Statement": [{
301 "Effect": "Allow",
302 "Action": "kms:Decrypt",
303 "Condition": { "StringEqualsIgnoreCase": {
304 "kms:RecipientAttestation:PCR0": hex_of(&own.pcr0).to_uppercase(),
305 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1).to_uppercase(),
306 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2).to_uppercase(),
307 }}
308 }]
309 })
310 .to_string();
311 verify_decrypt_policy(&doc, &own).expect("case-insensitive hex accepted");
312 }
313
314 #[test]
315 fn empty_statements_pass_vacuously() {
316 let doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
317 verify_decrypt_policy(doc, &own()).expect("empty policy is vacuously safe");
318 }
319
320 #[test]
321 fn rejects_account_kms_star() {
322 let own = own();
323 let doc = serde_json::json!({
324 "Statement": [{
325 "Sid": "AccountAdmin",
326 "Effect": "Allow",
327 "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
328 "Action": "kms:*",
329 "Resource": "*"
330 }]
331 })
332 .to_string();
333 assert_eq!(
334 verify_decrypt_policy(&doc, &own),
335 Err(PolicyError::WildcardAction("kms:*".into()))
336 );
337 }
338
339 #[test]
340 fn rejects_put_key_policy() {
341 let doc = serde_json::json!({
342 "Statement": [{
343 "Effect": "Allow",
344 "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
345 "Action": ["kms:DescribeKey", "kms:PutKeyPolicy"],
346 "Resource": "*"
347 }]
348 })
349 .to_string();
350 assert_eq!(
351 verify_decrypt_policy(&doc, &own()),
352 Err(PolicyError::ForbiddenAction("kms:putkeypolicy".into()))
353 );
354 }
355
356 #[test]
357 fn rejects_create_grant() {
358 let doc = serde_json::json!({
359 "Statement": [{
360 "Effect": "Allow",
361 "Action": "kms:CreateGrant",
362 "Resource": "*"
363 }]
364 })
365 .to_string();
366 assert_eq!(
367 verify_decrypt_policy(&doc, &own()),
368 Err(PolicyError::ForbiddenAction("kms:creategrant".into()))
369 );
370 }
371
372 #[test]
373 fn rejects_reencrypt() {
374 let doc = serde_json::json!({
375 "Statement": [{
376 "Effect": "Allow",
377 "Action": "kms:ReEncryptFrom",
378 "Resource": "*"
379 }]
380 })
381 .to_string();
382 assert_eq!(
383 verify_decrypt_policy(&doc, &own()),
384 Err(PolicyError::ForbiddenAction("kms:reencryptfrom".into()))
385 );
386 }
387
388 #[test]
389 fn rejects_ungated_decrypt() {
390 let doc = serde_json::json!({
391 "Statement": [{
392 "Effect": "Allow",
393 "Principal": "*",
394 "Action": "kms:Decrypt",
395 "Resource": "*"
396 }]
397 })
398 .to_string();
399 assert_eq!(
400 verify_decrypt_policy(&doc, &own()),
401 Err(PolicyError::UngatedDecrypt)
402 );
403 }
404
405 #[test]
406 fn rejects_decrypt_gated_to_wrong_pcr() {
407 let own = own();
408 let doc = serde_json::json!({
409 "Statement": [{
410 "Effect": "Allow",
411 "Action": "kms:Decrypt",
412 "Condition": { "StringEqualsIgnoreCase": {
413 "kms:RecipientAttestation:PCR0": hex_of(&[0xde; 48]),
414 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
415 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2),
416 }}
417 }]
418 })
419 .to_string();
420 match verify_decrypt_policy(&doc, &own) {
421 Err(PolicyError::PcrMismatch { index: 0, .. }) => {}
422 other => panic!("expected PCR0 mismatch, got {other:?}"),
423 }
424 }
425
426 #[test]
427 fn rejects_decrypt_missing_one_pcr() {
428 let own = own();
429 let doc = serde_json::json!({
430 "Statement": [{
431 "Effect": "Allow",
432 "Action": "kms:Decrypt",
433 "Condition": { "StringEqualsIgnoreCase": {
434 "kms:RecipientAttestation:PCR0": hex_of(&own.pcr0),
435 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
436 }}
437 }]
438 })
439 .to_string();
440 assert_eq!(
441 verify_decrypt_policy(&doc, &own),
442 Err(PolicyError::UngatedDecrypt)
443 );
444 }
445
446 #[test]
447 fn rejects_decrypt_gated_by_set_of_pcrs() {
448 let own = own();
452 let doc = serde_json::json!({
453 "Statement": [{
454 "Effect": "Allow",
455 "Action": "kms:Decrypt",
456 "Condition": { "StringEqualsIgnoreCase": {
457 "kms:RecipientAttestation:PCR0": [hex_of(&own.pcr0), hex_of(&[0xff; 48])],
458 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
459 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2),
460 }}
461 }]
462 })
463 .to_string();
464 assert_eq!(
465 verify_decrypt_policy(&doc, &own),
466 Err(PolicyError::UngatedDecrypt)
467 );
468 }
469
470 #[test]
471 fn rejects_decrypt_gated_by_wrong_operator() {
472 let own = own();
475 let doc = serde_json::json!({
476 "Statement": [{
477 "Effect": "Allow",
478 "Action": "kms:Decrypt",
479 "Condition": { "StringLike": {
480 "kms:RecipientAttestation:PCR0": hex_of(&own.pcr0),
481 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
482 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2),
483 }}
484 }]
485 })
486 .to_string();
487 assert_eq!(
488 verify_decrypt_policy(&doc, &own),
489 Err(PolicyError::UngatedDecrypt)
490 );
491 }
492
493 #[test]
494 fn rejects_not_action_allow() {
495 let doc = serde_json::json!({
496 "Statement": [{
497 "Effect": "Allow",
498 "Principal": { "AWS": "arn:aws:iam::111122223333:root" },
499 "NotAction": "kms:CreateKey",
500 "Resource": "*"
501 }]
502 })
503 .to_string();
504 assert_eq!(verify_decrypt_policy(&doc, &own()), Err(PolicyError::NotAction));
505 }
506
507 #[test]
508 fn ignores_deny_statements() {
509 let own = own();
512 let doc = serde_json::json!({
513 "Statement": [
514 {
515 "Effect": "Deny",
516 "Principal": "*",
517 "Action": "kms:*",
518 "Resource": "*",
519 "Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } }
520 },
521 {
522 "Effect": "Allow",
523 "Action": "kms:Decrypt",
524 "Condition": { "StringEqualsIgnoreCase": {
525 "kms:RecipientAttestation:PCR0": hex_of(&own.pcr0),
526 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
527 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2),
528 }}
529 }
530 ]
531 })
532 .to_string();
533 verify_decrypt_policy(&doc, &own).expect("deny statements are ignored");
534 }
535
536 #[test]
537 fn rejects_non_json() {
538 match verify_decrypt_policy("not json", &own()) {
539 Err(PolicyError::Json(_)) => {}
540 other => panic!("expected Json error, got {other:?}"),
541 }
542 }
543
544 #[test]
545 fn rejects_missing_statement() {
546 assert_eq!(
547 verify_decrypt_policy(r#"{"Version":"2012-10-17"}"#, &own()),
548 Err(PolicyError::NoStatements)
549 );
550 }
551
552 #[test]
553 fn accepts_single_statement_object() {
554 let own = own();
556 let doc = serde_json::json!({
557 "Statement": {
558 "Effect": "Allow",
559 "Action": "kms:Decrypt",
560 "Condition": { "StringEquals": {
561 "kms:RecipientAttestation:PCR0": hex_of(&own.pcr0),
562 "kms:RecipientAttestation:PCR1": hex_of(&own.pcr1),
563 "kms:RecipientAttestation:PCR2": hex_of(&own.pcr2),
564 }}
565 }
566 })
567 .to_string();
568 verify_decrypt_policy(&doc, &own).expect("single-object statement accepted");
569 }
570}