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
12 changes: 11 additions & 1 deletion SE050Sim/sdk-test/run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ fi

# Platform SCP03 variant: provision the simulator and the SDK from the same
# key file so the two sides cannot drift.
if [ "$SE05X_AUTH" = "PlatfSCP03" ]; then
case "$SE05X_AUTH" in *PlatfSCP03) PLATFSCP=1 ;; *) PLATFSCP=0 ;; esac
if [ "$PLATFSCP" = 1 ]; then
echo "=== Platform SCP03 variant: provisioning keys from $KEYFILE ==="
export SE050_SIM_SCP03_ENC=$(awk '/^ENC /{print $2}' "$KEYFILE")
export SE050_SIM_SCP03_MAC=$(awk '/^MAC /{print $2}' "$KEYFILE")
Expand All @@ -25,6 +26,15 @@ if [ "$SE05X_AUTH" = "PlatfSCP03" ]; then
echo " simulator MAC=$SE050_SIM_SCP03_MAC"
fi

# Applet session variants: the SDK opens a UserID / AESKey / ECKey session with
# its demo Authentication Objects (ex_sss_auth.h); the simulator provisions them.
case "$SE05X_AUTH" in
UserID*|AESKey*|ECKey*)
echo "=== Applet session variant ($SE05X_AUTH): demo Authentication Objects ==="
export SE050_SIM_DEMO_AUTH=1
;;
esac

start_sim() {
/app/se050-sim-server &
SIM_PID=$!
Expand Down
1,138 changes: 1,138 additions & 0 deletions SE050Sim/se050-sim/src/applet_session.rs

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions SE050Sim/se050-sim/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore, scp_active: bool) ->
let base_ins = apdu.base_ins();
let cred_type = apdu.cred_type();

// Applet-reserved objects (e.g. the ECKey session key pair) are only
// readable: any other command naming one of them is refused.
let read_only = base_ins == INS_READ || (base_ins == INS_MGMT && apdu.p2 == P2_EXIST);
if !read_only {
if let Ok(tlvs) = apdu.parse_tlvs() {
if tlvs.iter().any(|t| crate::object_store::is_reserved_id(&t.value)) {
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}
}
}

match base_ins {
INS_WRITE => match cred_type {
P1_EC => handlers::ec::handle_write_ec_key(apdu, store, version),
Expand Down
49 changes: 41 additions & 8 deletions SE050Sim/se050-sim/src/handlers/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR
// Check if this is key generation (P2=Generate) or has a key size tag
let generated = apdu.p2 == P2_GENERATE
|| key_data.as_ref().is_some_and(|d| d.len() <= 2);
let response = if generated {
let key = if generated {
// Key generation: Tag3 contains 2-byte key size
let key_len = key_data
.as_ref()
Expand All @@ -234,26 +234,42 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR

let mut key = vec![0u8; key_len_bytes];
rand::thread_rng().fill_bytes(&mut key);
store.insert(obj_id, SecureObject::AESKey { key });
ApduResponse::success()
key
} else if let Some(key) = key_data {
// Import key data
if key.len() != 16 && key.len() != 24 && key.len() != 32 {
return ApduResponse::error(SW_WRONG_DATA);
}
store.insert(obj_id, SecureObject::AESKey { key });
ApduResponse::success()
key
} else {
ApduResponse::error(SW_WRONG_DATA)
return ApduResponse::error(SW_WRONG_DATA);
};

// INS_AUTH_OBJECT: an AESKey Authentication Object, AES-128 only, with an
// optional maximum number of failed attempts. Checked before anything is
// stored; an existing Authentication Object stays AES-128 on update.
let auth_object = apdu.ins & 0x40 != 0;
let max_attempts = match crate::tlv::max_attempts(&tlvs, 0x7FFF) {
Ok(v) if auth_object => v,
Err(()) if auth_object => return ApduResponse::error(SW_WRONG_DATA),
_ => 0,
};
if response.sw == SW_NO_ERROR && !object_existed {
if (auth_object || store.auth_state(&obj_id).is_some()) && key.len() != 16 {
return ApduResponse::error(SW_WRONG_DATA);
}

store.insert(obj_id, SecureObject::AESKey { key });
if !object_existed {
store.set_creation_metadata(
obj_id,
creation_policy,
if generated { 0x02 } else { 0x01 },
);
if auth_object {
store.set_auth_object(obj_id, max_attempts);
}
}
response
ApduResponse::success()
}

/// Handle WRITE HMAC key command (WriteSymmKey with P1=HMAC).
Expand Down Expand Up @@ -310,6 +326,16 @@ pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> Apdu
}
}

/// Encrypting needs POLICY_OBJ_ALLOW_ENC, decrypting POLICY_OBJ_ALLOW_DEC,
/// for the current session owner.
fn cipher_permission(encrypting: bool) -> u32 {
if encrypting {
crate::policy::POLICY_OBJ_ALLOW_ENC
} else {
crate::policy::POLICY_OBJ_ALLOW_DEC
}
}

/// Shared body of the encrypt/decrypt one-shot handlers.
/// Tag1=key_id(4B), Tag2=cipher_mode(1B), Tag3=input, Tag4=IV(opt)
fn handle_cipher_oneshot(
Expand Down Expand Up @@ -354,6 +380,9 @@ fn handle_cipher_oneshot(
Some(obj) => obj.clone(),
None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
};
if !store.policy_allows(&key_id, cipher_permission(encrypting)) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}

let key_data = match &key_obj {
SecureObject::AESKey { key } => key,
Expand Down Expand Up @@ -416,6 +445,10 @@ pub fn handle_cipher_init(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduRes
if !store.exists(&key_id) {
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}
// The key is bound to the crypto object here; update and final reuse it.
if !store.policy_allows(&key_id, cipher_permission(encrypting)) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}

let mut chain = vec![0u8; 16];
if let Some(t) = tlv::find_tlv(&tlvs, TAG_4) {
Expand Down
26 changes: 26 additions & 0 deletions SE050Sim/se050-sim/src/handlers/ec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ pub fn handle_write_ec_key(
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}

// INS_AUTH_OBJECT: an ECKey Authentication Object, NIST P-256 only (the
// curve of ECKey sessions), with an optional maximum number of failed
// attempts (two bytes, 0 = unlimited, at most 0x7FFF). Checked before
// anything is stored; an existing Authentication Object keeps the curve.
let auth_object = apdu.ins & 0x40 != 0;
let max_attempts = match crate::tlv::max_attempts(&tlvs, 0x7FFF) {
Ok(v) if auth_object => v,
Ok(_) => 0,
Err(()) if auth_object => return ApduResponse::error(SW_WRONG_DATA),
Err(()) => 0,
};
if (auth_object || store.auth_state(&obj_id).is_some()) && !matches!(curve, ECCurve::NistP256) {
return ApduResponse::error(SW_WRONG_DATA);
}

// Check what key data is provided
let private_key_data = tlv::find_tlv(&tlvs, TAG_3).map(|t| t.value.clone());
let public_key_data = tlv::find_tlv(&tlvs, TAG_4)
Expand Down Expand Up @@ -147,6 +162,9 @@ pub fn handle_write_ec_key(
creation_policy,
if generated { 0x02 } else { 0x01 },
);
if auth_object {
store.set_auth_object(obj_id, max_attempts);
}
}
response
}
Expand Down Expand Up @@ -502,6 +520,10 @@ pub fn handle_sign(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse {
// on 3.1.1 and 7.2.0), not 0x6A82.
None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
};
// Signing needs POLICY_OBJ_ALLOW_SIGN for the current session owner.
if !store.policy_allows(&key_id, crate::policy::POLICY_OBJ_ALLOW_SIGN) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}

// Weierstrass ECDSA enforces input length == algo digest length.
if let SecureObject::ECKeyPair { curve, .. } = &key_obj {
Expand Down Expand Up @@ -750,6 +772,10 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) ->
Some(obj) => obj.clone(),
None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
};
// Key agreement needs POLICY_OBJ_ALLOW_KA for the current session owner.
if !store.policy_allows(&key_id, crate::policy::POLICY_OBJ_ALLOW_KA) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}

// On the real applet the Tag7 target must already exist as an HMACKey
// object; the applet refuses to create it implicitly. In lenient mode a
Expand Down
12 changes: 12 additions & 0 deletions SE050Sim/se050-sim/src/handlers/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ pub fn handle_oneshot(apdu: &ParsedApdu, store: &mut ObjectStore, validate: bool
Some(obj) => obj.clone(),
None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
};
// Generating a MAC needs POLICY_OBJ_ALLOW_SIGN for the current session
// owner; validating needs POLICY_OBJ_ALLOW_VERIFY (AN12543 policy table).
let needed = if validate { crate::policy::POLICY_OBJ_ALLOW_VERIFY } else { crate::policy::POLICY_OBJ_ALLOW_SIGN };
if !store.policy_allows(&key_id, needed) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}
match compute_mac(algo, &key_obj, &data) {
Ok(mac) => mac_response(mac, expected.as_deref()),
Err(sw) => ApduResponse::error(sw),
Expand Down Expand Up @@ -260,6 +266,12 @@ pub fn handle_final(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse
Some(obj) => obj.clone(),
None => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
};
// Generating a MAC needs POLICY_OBJ_ALLOW_SIGN for the current session
// owner; validating needs POLICY_OBJ_ALLOW_VERIFY (AN12543 policy table).
let needed = if validate { crate::policy::POLICY_OBJ_ALLOW_VERIFY } else { crate::policy::POLICY_OBJ_ALLOW_SIGN };
if !store.policy_allows(&key_id, needed) {
return ApduResponse::error(SW_COMMAND_NOT_ALLOWED);
}
match compute_mac(algo, &key_obj, &data) {
Ok(mac) => mac_response(mac, expected.as_deref()),
Err(sw) => ApduResponse::error(sw),
Expand Down
76 changes: 64 additions & 12 deletions SE050Sim/se050-sim/src/handlers/object_mgmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,49 @@ use crate::object_store::types::SecureObject;
use crate::object_store::ObjectStore;
use crate::tlv::{self, Tlv, TAG_1, TAG_2, TAG_3, TAG_4, TAG_POLICY};

/// WriteUserID (AN12543 4.7.1.5): policy and maximum attempts (< 256) only on
/// creation, TAG_1 object ID, TAG_2 value of 4 to 16 bytes. An existing UserID
/// cannot be updated.
fn handle_write_userid(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse {
let Ok(tlvs) = apdu.parse_tlvs() else {
return ApduResponse::error(SW_WRONG_DATA);
};
let Some(obj_id) = extract_object_id(&tlvs) else {
return ApduResponse::error(SW_WRONG_DATA);
};
let Some(value) = tlv::find_tlv(&tlvs, TAG_2).map(|t| t.value.clone()) else {
return ApduResponse::error(SW_WRONG_DATA);
};
if !(4..=16).contains(&value.len()) {
return ApduResponse::error(SW_WRONG_DATA);
}
if store.exists(&obj_id) {
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
}
let Ok(max) = crate::tlv::max_attempts(&tlvs, 255) else {
return ApduResponse::error(SW_WRONG_DATA);
};
let policy = match crate::policy::creation_policy(&tlvs) {
Ok(p) => p,
Err(_) => return ApduResponse::error(SW_WRONG_DATA),
};
store.insert(obj_id, SecureObject::UserID { value });
store.set_creation_metadata(obj_id, policy, 0x01);
store.set_auth_object(obj_id, max);
ApduResponse::success()
}

/// Handle WRITE commands for Binary, UserID, and Counter objects.
pub fn handle_write(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse {
match apdu.cred_type() {
P1_BINARY => handle_write_binary(apdu, store),
// WriteUserID is refused in plain (unauthenticated) sessions:
// bench-verified 0x6985 on SE050C applet 3.1.1 and SE051
// applet 7.2.0 alike, via both the raw APDU and the sss layer.
// The simulator only models plain sessions, so the write is
// always refused.
// applet 7.2.0 alike, via both the raw APDU and the sss layer
// (the transport refuses Authentication Object writes outside an
// authenticated channel, see applet_session::intercept). UserIDs
// exist only as Authentication Objects.
P1_USERID if apdu.ins & 0x40 != 0 => handle_write_userid(apdu, store),
P1_USERID => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
P1_COUNTER => handle_write_counter(apdu, store),
_ => ApduResponse::error(SW_WRONG_P1P2),
Expand Down Expand Up @@ -433,17 +467,36 @@ fn handle_read_attributes(
Some(id) => id,
None => return ApduResponse::error(SW_WRONG_DATA),
};
let Some(object) = store.get(&obj_id) else {
return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED);
};
match object_attributes(store, &obj_id) {
Some(attributes) => ApduResponse::success_with_tlvs(&[Tlv::new(TAG_3, &attributes)]),
None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED),
}
}

/// Attribute bytes of an object (AN12543 tables 3 and 4), for
/// ReadObjectAttributes and attested reads.
pub fn object_attributes(store: &ObjectStore, obj_id: &[u8; 4]) -> Option<Vec<u8>> {
let obj_id = *obj_id;
let object = store.get(&obj_id)?;

let mut attributes = Vec::new();
attributes.extend_from_slice(&obj_id);
attributes.push(object.type_code(true));
attributes.push(0x01); // kSE05x_SetIndicator_NOT_SET
attributes.extend_from_slice(&[0x00, 0x00]); // minimum AEAD tag length
attributes.extend_from_slice(&[0x00; 4]); // owner: unauthenticated session
attributes.extend_from_slice(&[0x00, 0x00]); // RFU
if let Some(auth) = store.auth_state(&obj_id) {
// Authentication Object layout (AN12543 table 3): indicator SET,
// failed attempts, owner, maximum attempts.
attributes.push(0x02); // kSE05x_SetIndicator_SET
// UserID attempts are counted but not reported (AN12543 3.2.4.4).
let shown = if matches!(object, SecureObject::UserID { .. }) { 0 } else { auth.attempts };
attributes.extend_from_slice(&shown.to_be_bytes());
attributes.extend_from_slice(&[0x00; 4]);
attributes.extend_from_slice(&auth.max_attempts.to_be_bytes());
} else {
attributes.push(0x01); // kSE05x_SetIndicator_NOT_SET
attributes.extend_from_slice(&[0x00, 0x00]); // minimum AEAD tag length
attributes.extend_from_slice(&[0x00; 4]); // owner: unauthenticated session
attributes.extend_from_slice(&[0x00, 0x00]); // RFU
}
if let Some(metadata) = store.metadata(&obj_id) {
if let Some(policy) = &metadata.policy {
attributes.extend_from_slice(policy);
Expand All @@ -453,8 +506,7 @@ fn handle_read_attributes(
attributes.push(0x01); // legacy store: treat as externally created
}
attributes.extend_from_slice(&[0x00; 4]); // object version

ApduResponse::success_with_tlvs(&[Tlv::new(TAG_3, &attributes)])
Some(attributes)
}

fn handle_check_exists(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse {
Expand Down
1 change: 1 addition & 0 deletions SE050Sim/se050-sim/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

pub mod apdu;
pub mod applet;
pub mod applet_session;
pub mod dispatch;
pub mod handlers;
pub mod object_store;
Expand Down
Loading