-
Notifications
You must be signed in to change notification settings - Fork 12
feat(tbor): implement AesGenerateKey + AesEncryptDecrypt (masked-key AES) #584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! Host-side wrapper for the TBOR `AesEncryptDecrypt` command. | ||
| //! | ||
| //! `AesEncryptDecrypt` is an **in-session** command (Crypto-Officer or | ||
| //! Crypto-User) that AES-CBC encrypts or decrypts `msg` using a | ||
| //! caller-held **masked** AES key (the `masked_key` from | ||
| //! [`AesGenerateKey`](crate::aes_generate_key) or imported via | ||
| //! [`UnwrapKey`](crate::unwrap_key)). The device unmasks the key | ||
| //! on-device, runs the AES-CBC transform, and returns the transformed | ||
| //! message plus the updated chaining IV; nothing is persisted. | ||
|
|
||
| use alloc::vec::Vec; | ||
|
|
||
| pub use crate::aes_generate_key::MASKED_AES_KEY_MAX_LEN; | ||
| pub use crate::aes_generate_key::MASKED_AES_KEY_MIN_LEN; | ||
| use crate::tbor; | ||
|
|
||
| /// TBOR opcode for `AesEncryptDecrypt`. | ||
| pub const TBOR_OP_AES_ENCRYPT_DECRYPT: u8 = 0x16; | ||
|
|
||
| /// AES-CBC block size in bytes — also the required IV length. | ||
| pub const AES_IV_LEN: usize = 16; | ||
|
|
||
| /// Maximum message length (bytes) accepted by `AesEncryptDecrypt`. | ||
| pub const AES_MSG_MAX_LEN: usize = 1024; | ||
|
|
||
| /// `AesOp` discriminant selecting encryption. | ||
| pub const AES_OP_ENCRYPT: u8 = 1; | ||
| /// `AesOp` discriminant selecting decryption. | ||
| pub const AES_OP_DECRYPT: u8 = 2; | ||
|
|
||
| /// Host-facing TBOR `AesEncryptDecrypt` request. | ||
| #[tbor(opcode = TBOR_OP_AES_ENCRYPT_DECRYPT, session_ctrl = in_session)] | ||
| #[derive(Debug, Default, Clone, PartialEq, Eq)] | ||
| pub struct TborAesEncryptDecryptReq { | ||
| /// Session id this request is bound to. Cross-checked against the | ||
| /// SQE-carried session id by the dispatcher. | ||
| #[tbor(session_id)] | ||
| pub session_id: u16, | ||
|
|
||
| /// The masked AES key (from `AesGenerateKey` / `UnwrapKey`), an | ||
| /// AEAD-GCM-256 envelope of 148..=164 B. | ||
| #[tbor(min_len = 148, max_len = 164)] | ||
| pub masked_key: Vec<u8>, | ||
|
|
||
| /// The direction as the 1-byte `AesOp` discriminant (see | ||
| /// [`AES_OP_ENCRYPT`] / [`AES_OP_DECRYPT`]). | ||
| pub op: u8, | ||
|
|
||
| /// The message to transform: a non-empty multiple of the 16-byte AES | ||
| /// block, up to [`AES_MSG_MAX_LEN`] (1024) bytes. | ||
| #[tbor(max_len = 1024)] | ||
| pub msg: Vec<u8>, | ||
|
|
||
| /// The 16-byte CBC initialization vector. A fixed-size array encodes | ||
| /// the exact length (the FW codec is the length authority). | ||
| pub iv: [u8; 16], | ||
| } | ||
|
|
||
| /// Host-facing TBOR `AesEncryptDecrypt` response. | ||
| #[tbor(response)] | ||
| #[derive(Debug, Default, Clone, PartialEq, Eq)] | ||
| pub struct TborAesEncryptDecryptResp { | ||
| /// The transformed message (same length as the input `msg`). | ||
| #[tbor(max_len = 1024)] | ||
| pub msg: Vec<u8>, | ||
|
|
||
| /// The updated chaining IV (the last ciphertext block). A fixed-size | ||
| /// array encodes the exact length. | ||
| pub iv: [u8; 16], | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use azihsm_ddi_tbor_types::TborOpReq; | ||
|
|
||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn request_encodes_op_and_iv() { | ||
| let req = TborAesEncryptDecryptReq { | ||
| session_id: 7, | ||
| masked_key: alloc::vec![0x11u8; MASKED_AES_KEY_MIN_LEN], | ||
| op: AES_OP_ENCRYPT, | ||
| msg: alloc::vec![0x22u8; 32], | ||
| iv: [0x33u8; AES_IV_LEN], | ||
| }; | ||
|
|
||
| let mut buf = [0u8; 1536]; | ||
| let frame = req.encode_request(&mut buf).expect("encode"); | ||
|
|
||
| // The message bytes must appear in the encoded frame. | ||
| assert!( | ||
| frame.windows(4).any(|w| w == [0x22u8; 4]), | ||
| "encoded frame must carry the message bytes", | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! Host-side wrapper for the TBOR `AesGenerateKey` command. | ||
| //! | ||
| //! `AesGenerateKey` is an **in-session** command (Crypto-Officer or | ||
| //! Crypto-User) that generates a fresh random AES key of the requested | ||
| //! size and returns it as a **masked** blob. The key is not stored | ||
| //! on-device; the caller holds the masked blob and passes it back to | ||
| //! [`AesEncryptDecrypt`](crate::aes_encrypt_decrypt) to transform data | ||
| //! (unmask-on-use). | ||
| //! | ||
| //! The request carries the requested key `scope` (lifecycle / visibility | ||
| //! domain) and `key_size` as raw 1-byte discriminants — the firmware types | ||
| //! them as the `KeyScope` / `AesKeySize` open-enums, but this host crate is | ||
| //! firewalled from the firmware PAL types so it carries the same bytes as | ||
| //! raw `u8`. | ||
|
|
||
| use alloc::vec::Vec; | ||
|
|
||
| use crate::tbor; | ||
|
|
||
| /// TBOR opcode for `AesGenerateKey`. | ||
| pub const TBOR_OP_AES_GENERATE_KEY: u8 = 0x15; | ||
|
|
||
| /// Minimum masked AES-key envelope length (AES-128, 16-byte key): an | ||
| /// AEAD-GCM-256 masked-key envelope `header(8) ‖ iv(12) ‖ aad(96) ‖ | ||
| /// pt(16) ‖ tag(16)`. | ||
| pub const MASKED_AES_KEY_MIN_LEN: usize = 8 + 12 + 96 + 16 + 16; | ||
|
|
||
| /// Maximum masked AES-key envelope length (AES-256, 32-byte key). | ||
| pub const MASKED_AES_KEY_MAX_LEN: usize = 8 + 12 + 96 + 32 + 16; | ||
|
|
||
| /// `AesKeySize` discriminant for AES-128 (16-byte key). | ||
| pub const AES_KEY_SIZE_128: u8 = 1; | ||
| /// `AesKeySize` discriminant for AES-192 (24-byte key). | ||
| pub const AES_KEY_SIZE_192: u8 = 2; | ||
| /// `AesKeySize` discriminant for AES-256 (32-byte key). | ||
| pub const AES_KEY_SIZE_256: u8 = 3; | ||
|
|
||
| /// Host-facing TBOR `AesGenerateKey` request. | ||
| #[tbor(opcode = TBOR_OP_AES_GENERATE_KEY, session_ctrl = in_session)] | ||
| #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] | ||
| pub struct TborAesGenerateKeyReq { | ||
| /// Session id this request is bound to. Cross-checked against the | ||
| /// SQE-carried session id by the dispatcher. | ||
| #[tbor(session_id)] | ||
| pub session_id: u16, | ||
|
|
||
| /// Requested key scope as the 1-byte `KeyScope` discriminant. | ||
| pub scope: u8, | ||
|
|
||
| /// AES key size as the 1-byte `AesKeySize` discriminant (see | ||
| /// [`AES_KEY_SIZE_128`] / [`AES_KEY_SIZE_192`] / [`AES_KEY_SIZE_256`]). | ||
| pub key_size: u8, | ||
| } | ||
|
|
||
| /// Host-facing TBOR `AesGenerateKey` response. | ||
| #[tbor(response)] | ||
| #[derive(Debug, Default, Clone, PartialEq, Eq)] | ||
| pub struct TborAesGenerateKeyResp { | ||
| /// The freshly generated AES key, masked (AEAD-GCM-256) under the | ||
| /// requested scope's masking key. 148 / 156 / 164 B for | ||
| /// AES-128 / 192 / 256; not stored on-device. | ||
| #[tbor(max_len = 164)] | ||
| pub masked_key: Vec<u8>, | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use azihsm_ddi_tbor_types::TborOpReq; | ||
|
|
||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn request_encodes_scope_and_size() { | ||
| let req = TborAesGenerateKeyReq { | ||
| session_id: 5, | ||
| // KeyScope::Local discriminant (0b011). | ||
| scope: 0b011, | ||
| key_size: AES_KEY_SIZE_256, | ||
| }; | ||
|
|
||
| let mut buf = [0u8; 256]; | ||
| let frame = req.encode_request(&mut buf).expect("encode"); | ||
|
|
||
| // The 1-byte key-size discriminant must appear in the encoded frame. | ||
| assert!( | ||
| frame.contains(&AES_KEY_SIZE_256), | ||
| "encoded frame must carry the key-size discriminant", | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| //! Integration tests for the TBOR `AesEncryptDecrypt` command. | ||
| //! | ||
| //! `AesEncryptDecrypt` AES-CBC transforms a host message using a | ||
| //! caller-held **masked** AES key (from `AesGenerateKey` or imported via | ||
| //! `UnwrapKey`). The device unmasks the key on-device, runs the | ||
| //! transform, and returns the result plus the updated chaining IV. | ||
| //! | ||
| //! Coverage: | ||
| //! * Encrypt → decrypt round-trip per key size (128/192/256): the | ||
| //! ciphertext differs from the plaintext and decrypt recovers it. | ||
| //! * The returned IV is the last ciphertext block (CBC chaining). | ||
| //! * Tampered masked key → `AesGcmDecryptTagDoesNotMatch`. | ||
| //! * A message that is not a whole number of AES blocks → `InvalidArg`. | ||
| //! * An AES key imported via `UnwrapKey` encrypts/decrypts (cross-command). | ||
|
|
||
| #![cfg(any(feature = "emu", feature = "mock", feature = "sock"))] | ||
| // The `aes_op` transform helper stays available under any backend (so the | ||
| // module isn't emu-limited); only the emu-gated tests exercise it today. | ||
| #![cfg_attr(not(feature = "emu"), allow(dead_code))] | ||
|
|
||
| use azihsm_ddi_tbor_types::TborAesEncryptDecryptReq; | ||
| use azihsm_ddi_tbor_types::TborAesEncryptDecryptResp; | ||
| // Test-only imports: the round-trip / reject tests need the emu FW handler | ||
| // plus the masked-key + unwrap helpers, so keep them emu-gated. | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::TborStatus; | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::AES_KEY_SIZE_128; | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::AES_KEY_SIZE_192; | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::AES_KEY_SIZE_256; | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::AES_OP_DECRYPT; | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::AES_OP_ENCRYPT; | ||
| #[cfg(feature = "emu")] | ||
| use azihsm_ddi_tbor_types::KEY_CLASS_AES; | ||
|
|
||
| #[cfg(feature = "emu")] | ||
| use crate::commands::aes_generate_key::generate_key; | ||
| #[cfg(feature = "emu")] | ||
| use crate::commands::aes_generate_key::SCOPE_LOCAL; | ||
| #[cfg(feature = "emu")] | ||
| use crate::commands::sd_sealing_key_gen::finalized_co_session; | ||
| #[cfg(feature = "emu")] | ||
| use crate::commands::unwrap_key::unwrap; | ||
| use crate::harness::TestCtx; | ||
|
|
||
| /// AES block / IV length. | ||
| const IV_LEN: usize = 16; | ||
|
|
||
| /// AES-CBC transform `msg` under the masked key + `iv`. | ||
| fn aes_op( | ||
| ctx: &TestCtx, | ||
| session_id: u16, | ||
| masked_key: &[u8], | ||
| op: u8, | ||
| msg: &[u8], | ||
| iv: &[u8], | ||
| ) -> TborAesEncryptDecryptResp { | ||
| let req = TborAesEncryptDecryptReq { | ||
| session_id, | ||
| masked_key: masked_key.to_vec(), | ||
| op, | ||
| msg: msg.to_vec(), | ||
| iv: iv.try_into().expect("IV must be 16 bytes"), | ||
| }; | ||
| ctx.tbor(&req).expect("AesEncryptDecrypt") | ||
| } | ||
|
|
||
| #[cfg(feature = "emu")] | ||
| #[test] | ||
| fn aes_encrypt_decrypt_roundtrip_all_sizes_emu() { | ||
| let ctx = TestCtx::new(); | ||
| let session = finalized_co_session(&ctx); | ||
| let msg = [0xA5u8; 32]; // two AES blocks | ||
| let iv = [0x11u8; IV_LEN]; | ||
|
|
||
| for size in [AES_KEY_SIZE_128, AES_KEY_SIZE_192, AES_KEY_SIZE_256] { | ||
| let key = generate_key(&ctx, session.session_id, SCOPE_LOCAL, size); | ||
|
|
||
| let enc = aes_op(&ctx, session.session_id, &key, AES_OP_ENCRYPT, &msg, &iv); | ||
| assert_eq!(enc.msg.len(), msg.len(), "ciphertext length matches input"); | ||
| assert_ne!(enc.msg, msg, "ciphertext must differ from plaintext"); | ||
|
|
||
| // Decrypt the ciphertext with the *original* IV to recover the | ||
| // plaintext (the returned chaining IV is for the next block). | ||
| let dec = aes_op( | ||
| &ctx, | ||
| session.session_id, | ||
| &key, | ||
| AES_OP_DECRYPT, | ||
| &enc.msg, | ||
| &iv, | ||
| ); | ||
| assert_eq!(dec.msg, msg, "decrypt must recover the plaintext"); | ||
| } | ||
| } | ||
|
|
||
| #[cfg(feature = "emu")] | ||
| #[test] | ||
| fn aes_encrypt_decrypt_chaining_iv_emu() { | ||
| let ctx = TestCtx::new(); | ||
| let session = finalized_co_session(&ctx); | ||
| let key = generate_key(&ctx, session.session_id, SCOPE_LOCAL, AES_KEY_SIZE_256); | ||
| let msg = [0x5Au8; 48]; // three AES blocks | ||
| let iv = [0x22u8; IV_LEN]; | ||
|
vsonims marked this conversation as resolved.
Dismissed
|
||
|
|
||
| let enc = aes_op(&ctx, session.session_id, &key, AES_OP_ENCRYPT, &msg, &iv); | ||
| // For CBC, the updated chaining IV is the last ciphertext block. | ||
| assert_eq!( | ||
| enc.iv, | ||
| enc.msg[enc.msg.len() - IV_LEN..], | ||
| "chaining IV must equal the last ciphertext block", | ||
| ); | ||
| } | ||
|
|
||
| #[cfg(feature = "emu")] | ||
| #[test] | ||
| fn aes_encrypt_decrypt_rejects_tampered_key_emu() { | ||
| let ctx = TestCtx::new(); | ||
| let session = finalized_co_session(&ctx); | ||
| let mut key = generate_key(&ctx, session.session_id, SCOPE_LOCAL, AES_KEY_SIZE_256); | ||
|
|
||
| // Flip a byte in the AEAD tag region (last 16 bytes) so the unmask tag | ||
| // check fails without disturbing the cleartext scope metadata. | ||
| let last = key.len() - 1; | ||
| key[last] ^= 0x01; | ||
|
|
||
| let req = TborAesEncryptDecryptReq { | ||
| session_id: session.session_id, | ||
| masked_key: key, | ||
| op: AES_OP_ENCRYPT, | ||
| msg: vec![0u8; 16], | ||
| iv: [0u8; IV_LEN], | ||
| }; | ||
| ctx.expect_fw_reject(&req, TborStatus::AesGcmDecryptTagDoesNotMatch); | ||
| } | ||
|
|
||
| #[cfg(feature = "emu")] | ||
| #[test] | ||
| fn aes_encrypt_decrypt_rejects_bad_msg_len_emu() { | ||
| let ctx = TestCtx::new(); | ||
| let session = finalized_co_session(&ctx); | ||
| let key = generate_key(&ctx, session.session_id, SCOPE_LOCAL, AES_KEY_SIZE_256); | ||
|
|
||
| let req = TborAesEncryptDecryptReq { | ||
| session_id: session.session_id, | ||
| masked_key: key, | ||
| op: AES_OP_ENCRYPT, | ||
| // 20 bytes is not a whole number of 16-byte AES blocks. | ||
| msg: vec![0u8; 20], | ||
| iv: [0u8; IV_LEN], | ||
| }; | ||
| ctx.expect_fw_reject(&req, TborStatus::InvalidArg); | ||
| } | ||
|
|
||
| #[cfg(feature = "emu")] | ||
| #[test] | ||
| fn aes_encrypt_decrypt_unwrapped_key_roundtrip_emu() { | ||
| let ctx = TestCtx::new(); | ||
| let session = finalized_co_session(&ctx); | ||
|
|
||
| // Import an AES-256 key via UnwrapKey (RSA-AES key import), then use | ||
| // the recovered masked key to encrypt and decrypt. | ||
| let aes_key = [0x42u8; 32]; | ||
| let masked = unwrap(&ctx, session.session_id, KEY_CLASS_AES, &aes_key).masked_key; | ||
|
|
||
| let msg = [0x37u8; 16]; | ||
| let iv = [0x88u8; IV_LEN]; | ||
|
vsonims marked this conversation as resolved.
Dismissed
|
||
| let enc = aes_op(&ctx, session.session_id, &masked, AES_OP_ENCRYPT, &msg, &iv); | ||
| let dec = aes_op( | ||
| &ctx, | ||
| session.session_id, | ||
| &masked, | ||
| AES_OP_DECRYPT, | ||
| &enc.msg, | ||
| &iv, | ||
| ); | ||
| assert_eq!(dec.msg, msg, "unwrapped AES key must round-trip"); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.