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
695 changes: 695 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
32 changes: 32 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,27 @@ 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 = if auth_object {
match tlv::find_tlv(&tlvs, crate::tlv::TAG_MAX_ATTEMPTS) {
None => 0,
Some(t) if t.value.len() == 2 => match u16::from_be_bytes([t.value[0], t.value[1]]) {
v @ 0..=0x7FFF => v,
_ => return ApduResponse::error(SW_WRONG_DATA),
},
Some(_) => return ApduResponse::error(SW_WRONG_DATA),
}
} else {
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 +168,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 +526,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 +778,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
17 changes: 13 additions & 4 deletions SE050Sim/se050-sim/src/handlers/object_mgmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,10 +440,19 @@ fn handle_read_attributes(
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
attributes.extend_from_slice(&auth.attempts.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 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
97 changes: 92 additions & 5 deletions SE050Sim/se050-sim/src/object_store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ pub struct ObjectStore {
/// defaults (or environment overrides). Factory reset deliberately does
/// not restore these keys, matching the irrecoverable real-card behavior.
platform_scp: Option<Scp03Config>,
/// Authentication Object ID of the session running the current command
/// (all zeros = the default session). Set by the transport for each
/// command; policies are evaluated for this owner. Not persisted.
session_owner: [u8; 4],
}

/// RESERVED_ID_ECKEY_SESSION (kSE05x_AppletResID_KP_ECKEY_USER): the SE's
/// own NIST P-256 key pair used for ECKey session key agreement.
pub const RESERVED_ID_ECKEY_SESSION: [u8; 4] = [0x7F, 0xFF, 0x02, 0x01];

/// Applet-reserved objects the simulator manages itself. Hosts may read them
/// (public part, attributes, existence) but not create, replace, delete or
/// use them in generic crypto commands; only the applet's own flows use the
/// private keys.
pub const RESERVED_IDS: &[[u8; 4]] = &[RESERVED_ID_ECKEY_SESSION];

pub fn is_reserved_id(id: &[u8]) -> bool {
RESERVED_IDS.iter().any(|r| r[..] == *id)
}

fn default_curves() -> HashMap<u8, u8> {
Expand All @@ -102,6 +120,7 @@ impl ObjectStore {
crypto_object_types: HashMap::new(),
scp_required: false,
platform_scp: None,
session_owner: [0; 4],
}
}

Expand All @@ -115,6 +134,7 @@ impl ObjectStore {
crypto_object_types: HashMap::new(),
scp_required: false,
platform_scp: None,
session_owner: [0; 4],
};
store.load();
store
Expand Down Expand Up @@ -143,21 +163,88 @@ impl ObjectStore {
) {
self.metadata
.entry(id)
.or_insert(ObjectMetadata { policy, origin });
.or_insert(ObjectMetadata { policy, origin, auth: None });
self.persist();
}

/// Mark a freshly created object as an Authentication Object.
pub fn set_auth_object(&mut self, id: [u8; 4], max_attempts: u16) {
if let Some(m) = self.metadata.get_mut(&id) {
let instance = rand::RngCore::next_u64(&mut rand::rngs::OsRng);
m.auth = Some(crate::object_store::types::AuthState { max_attempts, attempts: 0, instance });
self.persist();
}
}

pub fn auth_state(&self, id: &[u8; 4]) -> Option<crate::object_store::types::AuthState> {
self.metadata.get(id).and_then(|m| m.auth)
}

/// True when the Authentication Object has used up its attempts.
pub fn auth_blocked(&self, id: &[u8; 4]) -> bool {
matches!(self.auth_state(id), Some(a) if a.max_attempts != 0 && a.attempts >= a.max_attempts)
}

/// Count a failed authentication (only when a maximum is set).
pub fn auth_failed(&mut self, id: &[u8; 4]) {
if let Some(a) = self.metadata.get_mut(id).and_then(|m| m.auth.as_mut()) {
if a.max_attempts != 0 && a.attempts < a.max_attempts {
a.attempts += 1;
self.persist();
}
}
}

/// A successful authentication resets the counter.
pub fn auth_succeeded(&mut self, id: &[u8; 4]) {
if let Some(a) = self.metadata.get_mut(id).and_then(|m| m.auth.as_mut()) {
if a.attempts != 0 {
a.attempts = 0;
self.persist();
}
}
}

pub fn session_owner(&self) -> [u8; 4] {
self.session_owner
}

pub fn set_session_owner(&mut self, owner: [u8; 4]) {
self.session_owner = owner;
}

/// The SE's ECKey session key pair, created on first use and persisted.
pub fn ensure_eckey_session_key(&mut self) {
if self.objects.contains_key(&RESERVED_ID_ECKEY_SESSION) {
return;
}
use p256::elliptic_curve::sec1::ToEncodedPoint;
let sk = p256::SecretKey::random(&mut rand::rngs::OsRng);
let pk = sk.public_key().to_encoded_point(false).as_bytes().to_vec();
self.objects.insert(RESERVED_ID_ECKEY_SESSION, SecureObject::ECKeyPair {
curve: crate::object_store::types::ECCurve::NistP256,
private_key: sk.to_bytes().to_vec(),
public_key: pk,
Comment on lines +224 to +227

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c2bd02. Applet-reserved objects are now read only at dispatch: reads, attributes and existence checks work (the host reads the public key during session setup); any other command naming one of them in its TLVs (ECDH, ECDH into it, sign, write, delete) gets 0x6985, including inside a session and before the key is first provisioned. Only the session flow uses the private key. Covered by reserved_session_key_is_read_only.

});
self.persist();
}

pub fn metadata(&self, id: &[u8; 4]) -> Option<&ObjectMetadata> {
self.metadata.get(id)
}

/// No attached policy means the applet default allows the operation.
/// Once a policy is attached, an omitted permission is a denial.
/// No attached policy means the applet default: everything for ordinary
/// objects, read only for Authentication Objects (AN12543 table 17).
/// Once a policy is attached, only the access rule of the current session
/// owner applies (rules are not inherited across owners, AN12543 3.7.1.2);
/// an owner without a rule, or an omitted permission, is a denial.
pub fn policy_allows(&self, id: &[u8; 4], permission: u32) -> bool {
match self.metadata.get(id).and_then(|m| m.policy.as_deref()) {
Some(raw) => crate::policy::ar_header_union(raw)
let meta = self.metadata.get(id);
match meta.and_then(|m| m.policy.as_deref()) {
Some(raw) => crate::policy::ar_header_for(raw, &self.session_owner)
.map(|header| header & permission != 0)
.unwrap_or(false),
None if meta.and_then(|m| m.auth).is_some() => permission == crate::policy::POLICY_OBJ_ALLOW_READ,
None => true,
}
}
Expand Down
19 changes: 19 additions & 0 deletions SE050Sim/se050-sim/src/object_store/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,32 @@ pub struct ObjectMetadata {
/// trust-provisioned=3.
#[serde(default = "default_object_origin")]
pub origin: u8,
/// Authentication Object (created with INS_AUTH_OBJECT): it can open an
/// applet session, and its failed attempts are counted (AN12543 3.2.4).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<AuthState>,
}

/// Attempt counter of an Authentication Object. With `max_attempts` != 0 the
/// object no longer authenticates once `attempts` reaches it; a successful
/// authentication resets `attempts` to 0. Persisted with the object.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct AuthState {
pub max_attempts: u16,
pub attempts: u16,
/// Random tag given when the object is created. A session remembers it,
/// so a session outlives neither its Authentication Object nor a new
/// object later created under the same ID.
#[serde(default)]
pub instance: u64,
}

impl Default for ObjectMetadata {
fn default() -> Self {
Self {
policy: None,
origin: default_object_origin(),
auth: None,
}
}
}
Expand Down
21 changes: 21 additions & 0 deletions SE050Sim/se050-sim/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
pub const POLICY_OBJ_ALLOW_READ: u32 = 0x0020_0000;
pub const POLICY_OBJ_ALLOW_WRITE: u32 = 0x0010_0000;
pub const POLICY_OBJ_ALLOW_DELETE: u32 = 0x0004_0000;
pub const POLICY_OBJ_ALLOW_SIGN: u32 = 0x1000_0000;
pub const POLICY_OBJ_ALLOW_VERIFY: u32 = 0x0800_0000;
pub const POLICY_OBJ_ALLOW_KA: u32 = 0x0400_0000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidPolicy;
Expand Down Expand Up @@ -77,6 +80,24 @@ pub fn ar_header_union(value: &[u8]) -> Option<u32> {
}
}

/// Access rule header for one Authentication Object ID (all zeros = the
/// default session). None when the policy has no rule for that owner or is
/// malformed.
pub fn ar_header_for(value: &[u8], owner: &[u8; 4]) -> Option<u32> {
let mut rest = value;
while !rest.is_empty() {
let len = rest[0] as usize;
if len < 8 || rest.len() < 1 + len {
return None;
}
if rest[1..5] == owner[..] {
return Some(u32::from_be_bytes([rest[5], rest[6], rest[7], rest[8]]));
}
rest = &rest[1 + len..];
}
None
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
16 changes: 16 additions & 0 deletions SE050Sim/se050-sim/src/scp03/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,22 @@ impl Scp03State {
}

impl Session {
/// A session whose keys come from elsewhere: the ECKey applet session
/// derives them from its master key (AN12543 3.6.3.3) and starts with the
/// command counter at 1 and an initial MAC chaining value.
pub fn from_keys(s_enc: Vec<u8>, s_mac: Vec<u8>, s_rmac: Vec<u8>, mcv: [u8; 16], security_level: u8) -> Self {
Session {
s_enc,
s_mac,
s_rmac,
security_level,
mcv,
cmd_counter: 1,
last_cmd_len: 0,
legacy: false,
}
}

/// Unwrap a secured command: verify the C-MAC, decrypt the data field if
/// C-DECRYPTION is active, and return the inner plain APDU for dispatch.
/// A MAC/padding failure returns Err(SW) and the caller drops the session.
Expand Down
Loading