Skip to content
Open
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
20 changes: 16 additions & 4 deletions SE050Sim/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ A software simulator for the NXP SE050 secure element, implementing the full I2C
- Persistent object store (JSON file on disk)
- WriteBinary, ReadObject, CheckObjectExists, DeleteSecureObject
- ReadIDList, ReadType, ReadSize
- Per-object access policies, including read, write, delete, use, and secure-channel requirements
- ReadObjectAttributes policy and origin reporting
- UserID, Counter objects
- Crypto object lifecycle (Create, List, Delete)
- EC public key import and verification
Expand Down Expand Up @@ -46,7 +48,7 @@ docker build -f Dockerfile.sdk-test -t se050-sim-sdk-test .
docker run se050-sim-sdk-test
```

This tests the simulator through the NXP Plug&Trust SDK's SSS API, with independent verification using OpenSSL. **All 18 tests pass.** See [SDK Test Suite](#sdk-test-suite) for details.
This tests the simulator through the NXP Plug&Trust SDK's SSS API, with independent verification using OpenSSL. **All 32 tests pass.** See [SDK Test Suite](#sdk-test-suite) for details.

### Run the wolfCrypt test suite

Expand Down Expand Up @@ -133,6 +135,7 @@ SE050Sim/
├── i2c_a7.c Custom PAL: TCP socket transport
├── se05x_reset.c No-op reset stub for Docker
├── main.c wolfCrypt test wrapper with SE050 init
├── test_api_improvements.c Focused policy/session/SCP03 API smoke test
├── CMakeLists.txt SDK library build
├── patch_ftr.py Enable EC curve features in SDK
└── run_test.sh Test runner script
Expand All @@ -144,7 +147,7 @@ The simulator has an independent test suite that uses the NXP Plug&Trust SDK's S

### Test results

All 30 tests pass:
All 32 tests pass:

| Test | Description |
|------|-------------|
Expand Down Expand Up @@ -329,9 +332,15 @@ over the secure channel against the simulator in CI (`sdk-test-scp03` and
`--build-arg SE05X_AUTH=PlatfSCP03` (default `None` keeps plain mode).
- `SetPlatformSCPRequest` is modelled: setting SCP_REQUIRED inside a session
makes plain commands fail 0x6985 (persisted).
- GlobalPlatform PUT KEY is modelled for platform SCP03 key rotation. It must
be sent through an active secure channel targeting the NXP Supplementary
Security Domain; an applet-targeted PUT KEY returns `0x6A80`, matching the
hardware. The simulator unwraps the ENC/MAC/DEK values with the current DEK,
checks each supplied KCV, and persists the new key set for subsequent
connections. The wolfSSL API-improvement smoke test exercises both
explicit-key and HKDF-seed rotation, then reconnects with the new keys.

Not modelled: SCP02, ECKey / AppletSCP03 authenticated sessions, and PUT KEY
(key rotation).
Not modelled: SCP02 and ECKey / AppletSCP03 authenticated sessions.

### Implementation notes (worth knowing before you change this code)

Expand Down Expand Up @@ -376,6 +385,9 @@ these; treat them as required coverage, not a nice-to-have.

- The `SE050_SIM_SCP03_ENC/_MAC` static keys default to well-known NXP
development keys; do not treat a simulated SCP03 channel as confidential.
- The persisted simulator state contains the active platform SCP03 keys in
plaintext. This is intentional for test reproducibility and is not a model
for production key storage.

## License

Expand Down
70 changes: 70 additions & 0 deletions SE050Sim/sdk-test/test_se050.c
Original file line number Diff line number Diff line change
Expand Up @@ -1764,6 +1764,75 @@ static void test_object_delete(void)
TEST_PASS();
}

/* ======================================================================
* Test: immutable object policy, attributes, no-write and no-delete
* ====================================================================== */
static void test_object_policy(void)
{
TEST_BEGIN("Object-policy-no-write-no-delete");
sss_status_t status;
sss_se05x_object_t obj;
sss_policy_u common;
sss_policy_u file;
sss_policy_t policy;
uint32_t obj_id = OBJ_ID_BASE + 202;
uint8_t data[] = "protected";
uint8_t replacement[] = "replaced!";
SE05x_Result_t exists = kSE05x_Result_FAILURE;
#if SSS_HAVE_SE05X_VER_GTE_07_02
uint8_t attributes[MAX_POLICY_BUFFER_SIZE + 32] = {0};
size_t attributes_len = sizeof(attributes);
#endif

memset(&common, 0, sizeof(common));
memset(&file, 0, sizeof(file));
memset(&policy, 0, sizeof(policy));
common.type = KPolicy_Common;
common.auth_obj_id = 0;
common.policy.common.can_Read = 1;
file.type = KPolicy_File;
file.auth_obj_id = 0;
file.policy.file.can_Read = 1;
policy.policies[0] = &common;
policy.policies[1] = &file;
policy.nPolicies = 2;

cleanup_object(obj_id);
sss_key_object_init(&obj, &g_ks);
status = sss_key_object_allocate_handle(&obj, obj_id,
kSSS_KeyPart_Default, kSSS_CipherType_Binary, sizeof(data),
kKeyObject_Mode_Persistent);
ASSERT_OK(status, "policy object allocate");

status = sss_key_store_set_key(&g_ks, &obj, data, sizeof(data),
sizeof(data) * 8, &policy, 0);
ASSERT_OK(status, "policy object create");

#if SSS_HAVE_SE05X_VER_GTE_07_02
status = (sss_status_t)Se05x_API_ReadObjectAttributes(&g_session->s_ctx,
obj_id, attributes, &attributes_len);
ASSERT_EQ(status, SM_OK, "ReadObjectAttributes");
ASSERT_EQ(attributes[14], 8, "policy entry length");
ASSERT_EQ(attributes[19], 0x00, "policy header byte 1");
ASSERT_EQ(attributes[20], 0x20, "policy read permission");
ASSERT_EQ(attributes[21], 0x00, "policy header byte 3");
ASSERT_EQ(attributes[22], 0x00, "policy header byte 4");
ASSERT_EQ(attributes[23], kSE05x_Origin_EXTERNAL, "object origin");
#endif

status = sss_key_store_set_key(&g_ks, &obj, replacement,
sizeof(replacement), sizeof(replacement) * 8, NULL, 0);
ASSERT_EQ(status, kStatus_SSS_Fail, "no-write object was overwritten");

status = sss_key_store_erase_key(&g_ks, &obj);
ASSERT_EQ(status, kStatus_SSS_Fail, "no-delete object was erased");
Se05x_API_CheckObjectExists(&g_session->s_ctx, obj_id, &exists);
ASSERT_EQ(exists, kSE05x_Result_SUCCESS, "protected object disappeared");

sss_key_object_free(&obj);
TEST_PASS();
}

/* ======================================================================
* Main
* ====================================================================== */
Expand Down Expand Up @@ -1845,6 +1914,7 @@ int main(void)
/* Object management */
test_object_write_read();
test_object_delete();
test_object_policy();

/* Summary */
test_summary();
Expand Down
1 change: 1 addition & 0 deletions SE050Sim/se050-sim/src/apdu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ pub const P2_DELETE_ALL: u8 = 0x2A;
pub const P2_ID: u8 = 0x36;
pub const P2_ENCRYPT_ONESHOT: u8 = 0x37;
pub const P2_DECRYPT_ONESHOT: u8 = 0x38;
pub const P2_ATTRIBUTES: u8 = 0x3B;
pub const P2_PARAM: u8 = 0x40;
pub const P2_ENCRYPT_INIT: u8 = 0x42;
pub const P2_DECRYPT_INIT: u8 = 0x43;
Expand Down
5 changes: 5 additions & 0 deletions SE050Sim/se050-sim/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) ->
let component = crate::tlv::find_tlv(&tlvs, crate::tlv::TAG_4)
.and_then(|t| t.value.first().copied())
.unwrap_or(0);
if let Some(id) = obj_id {
if !store.policy_allows(&id, crate::policy::POLICY_OBJ_ALLOW_READ) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}
}
match obj_id.and_then(|id| store.get(&id)) {
Some(crate::object_store::types::SecureObject::RSAKeyPair { private_key_der, .. }) => {
use rsa::pkcs1::DecodeRsaPrivateKey;
Expand Down
59 changes: 46 additions & 13 deletions SE050Sim/se050-sim/src/handlers/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
use crate::apdu::*;
use crate::object_store::types::SecureObject;
use crate::object_store::{CryptoObjectState, ObjectStore};
use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_POLICY};
use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4};

use aes::cipher::generic_array::GenericArray;
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
Expand Down Expand Up @@ -73,7 +73,7 @@ impl AnyAes {
}
}

fn decrypt_block(&self, block: &mut [u8; 16]) {
pub(crate) fn decrypt_block(&self, block: &mut [u8; 16]) {
let ga = GenericArray::from_mut_slice(block);
match self {
AnyAes::A128(c) => c.decrypt_block(ga),
Expand Down Expand Up @@ -199,12 +199,24 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR
}
_ => return ApduResponse::error(SW_WRONG_DATA),
};
let creation_policy = match crate::policy::creation_policy(&tlvs) {
Ok(policy) => policy,
Err(_) => return ApduResponse::error(SW_WRONG_DATA),
};
let object_existed = store.exists(&obj_id);
if object_existed
&& !store.policy_allows(&obj_id, crate::policy::POLICY_OBJ_ALLOW_WRITE)
{
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}

// Check if key data is provided in Tag3
let key_data = tlv::find_tlv(&tlvs, TAG_3).map(|t| t.value.clone());

// Check if this is key generation (P2=Generate) or has a key size tag
if apdu.p2 == P2_GENERATE || key_data.as_ref().map_or(false, |d| d.len() <= 2) {
let generated = apdu.p2 == P2_GENERATE
|| key_data.as_ref().is_some_and(|d| d.len() <= 2);
let response = if generated {
// Key generation: Tag3 contains 2-byte key size
let key_len = key_data
.as_ref()
Expand Down Expand Up @@ -233,7 +245,15 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR
ApduResponse::success()
} else {
ApduResponse::error(SW_WRONG_DATA)
}
};
if response.sw == SW_NO_ERROR && !object_existed {
store.set_creation_metadata(
obj_id,
creation_policy,
if generated { 0x02 } else { 0x01 },
);
}
response
}

/// Handle WRITE HMAC key command (WriteSymmKey with P1=HMAC).
Expand All @@ -257,20 +277,33 @@ pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apdu
_ => return ApduResponse::error(SW_WRONG_DATA),
};

// A present but malformed (or empty) policy TLV is rejected up front,
// like the applet would, rather than being recorded as "no policy" and
// surfacing later as a strict-mode read denial.
let policy = match tlv::find_tlv(&tlvs, TAG_POLICY) {
Some(t) => match crate::policy::ar_header_union(&t.value) {
Some(header) => Some(header),
None => return ApduResponse::error(SW_WRONG_DATA),
},
None => None,
// An empty policy TLV is not a valid HMAC derive-target policy on the
// applet, even though other SDK wrappers use it to mean "not supplied".
if tlv::find_tlv(&tlvs, crate::tlv::TAG_POLICY)
.is_some_and(|tlv| tlv.value.is_empty())
{
return ApduResponse::error(SW_WRONG_DATA);
}
let creation_policy = match crate::policy::creation_policy(&tlvs) {
Ok(policy) => policy,
Err(_) => return ApduResponse::error(SW_WRONG_DATA),
};
let policy = creation_policy
.as_deref()
.and_then(crate::policy::ar_header_union);
let object_existed = store.exists(&obj_id);
if object_existed
&& !store.policy_allows(&obj_id, crate::policy::POLICY_OBJ_ALLOW_WRITE)
{
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}

match tlv::find_tlv(&tlvs, TAG_3) {
Some(t) if !t.value.is_empty() => {
store.insert(obj_id, SecureObject::HMACKey { key: t.value.clone(), policy });
if !object_existed {
store.set_creation_metadata(obj_id, creation_policy, 0x01);
}
ApduResponse::success()
}
_ => ApduResponse::error(SW_WRONG_DATA),
Expand Down
21 changes: 20 additions & 1 deletion SE050Sim/se050-sim/src/handlers/ec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,16 @@ pub fn handle_write_ec_key(
}
_ => return ApduResponse::error(SW_WRONG_DATA),
};
let creation_policy = match crate::policy::creation_policy(&tlvs) {
Ok(policy) => policy,
Err(_) => return ApduResponse::error(SW_WRONG_DATA),
};
let object_existed = store.exists(&obj_id);
if object_existed
&& !store.policy_allows(&obj_id, crate::policy::POLICY_OBJ_ALLOW_WRITE)
{
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}

// Extract curve from Tag2
let (curve_byte, curve) = match tlv::find_tlv(&tlvs, TAG_2) {
Expand Down Expand Up @@ -102,7 +112,8 @@ pub fn handle_write_ec_key(
.or_else(|| tlv::find_tlv(&tlvs, TAG_2).filter(|t| t.value.len() > 4))
.map(|t| t.value.clone());

if apdu.key_type() == P1_KEY_PAIR && private_key_data.is_none() {
let generated = apdu.key_type() == P1_KEY_PAIR && private_key_data.is_none();
let response = if generated {
// Generate a new key pair
match curve {
ECCurve::NistP192 => generate_p192_keypair(obj_id, store),
Expand All @@ -129,7 +140,15 @@ pub fn handle_write_ec_key(
ApduResponse::success()
} else {
ApduResponse::error(SW_WRONG_DATA)
};
if response.sw == SW_NO_ERROR && !object_existed {
store.set_creation_metadata(
obj_id,
creation_policy,
if generated { 0x02 } else { 0x01 },
);
}
response
}

fn generate_p192_keypair(obj_id: [u8; 4], store: &mut ObjectStore) -> ApduResponse {
Expand Down
Loading
Loading