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
100 changes: 100 additions & 0 deletions ddi/tbor/types/src/aes_encrypt_decrypt.rs
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",
);
}
}
93 changes: 93 additions & 0 deletions ddi/tbor/types/src/aes_generate_key.rs
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",
);
}
}
4 changes: 4 additions & 0 deletions ddi/tbor/types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ impl From<SessionControlKind> for u8 {
}
}

mod aes_encrypt_decrypt;
mod aes_generate_key;
mod api_rev;
mod ecc_generate_key;
mod ecc_sign;
Expand Down Expand Up @@ -106,6 +108,8 @@ mod session_open_init;
mod status;
mod unwrap_key;

pub use aes_encrypt_decrypt::*;
pub use aes_generate_key::*;
pub use api_rev::*;
pub use ecc_generate_key::*;
pub use ecc_sign::*;
Expand Down
185 changes: 185 additions & 0 deletions ddi/tbor/types/tests/commands/aes_encrypt_decrypt.rs
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];
Comment thread
vsonims marked this conversation as resolved.
Dismissed

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];
Comment thread
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];
Comment thread
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");
}
Loading
Loading