diff --git a/SE050Sim/Dockerfile.wolfcrypt b/SE050Sim/Dockerfile.wolfcrypt index 3b84dae..6c3cf07 100644 --- a/SE050Sim/Dockerfile.wolfcrypt +++ b/SE050Sim/Dockerfile.wolfcrypt @@ -109,6 +109,11 @@ RUN ln -sf /app/simw-top/build/sss/ex/src/libex_common.a \ # ---- Pass-B: rebuild wolfSSL with --with-se050 ---- # Links against the patched SDK we just built. Install overwrites Pass-A. +# WOLFSSL_EXTRA_CFLAGS lets CI build variants (e.g. +# -DWOLFSSL_SE050_ONLY_KEY_ID) without editing this file. It must reach both +# the library build and the test.c/main.c compiles below, since the macro +# gates test coverage in wolfcrypt/test/test.c as well. +ARG WOLFSSL_EXTRA_CFLAGS="" WORKDIR /app/wolfssl RUN make clean 2>&1 && \ ./configure \ @@ -125,7 +130,8 @@ RUN make clean 2>&1 && \ --disable-examples \ --enable-crypttests \ CFLAGS="-DWOLFSSL_SE050_INIT -DWOLFSSL_SE050_NO_TRNG -DSIZEOF_LONG_LONG=8 \ - -DECC_USER_CURVES -DHAVE_ECC224 -DHAVE_ECC256 -DHAVE_ECC384" \ + -DECC_USER_CURVES -DHAVE_ECC224 -DHAVE_ECC256 -DHAVE_ECC384 \ + ${WOLFSSL_EXTRA_CFLAGS}" \ LDFLAGS="-L/app/simw-top/build" \ 2>&1 && \ make -j$(nproc) 2>&1 && \ @@ -149,10 +155,10 @@ ENV SDK_INCS="-I/app/wolfssl \ -I/app/simw-top/build" RUN gcc -c -o /tmp/test.o /app/wolfssl/wolfcrypt/test/test.c \ - -DNO_MAIN_DRIVER -DHAVE_CONFIG_H $SDK_INCS 2>&1 + -DNO_MAIN_DRIVER -DHAVE_CONFIG_H ${WOLFSSL_EXTRA_CFLAGS} $SDK_INCS 2>&1 RUN gcc -c -o /tmp/main.o /app/wolfcrypt_test_main.c \ - -DHAVE_CONFIG_H $SDK_INCS 2>&1 + -DHAVE_CONFIG_H ${WOLFSSL_EXTRA_CFLAGS} $SDK_INCS 2>&1 RUN gcc -o /app/wolfcrypt_se050_test /tmp/main.o /tmp/test.o \ -L/usr/local/lib \ diff --git a/SE050Sim/sdk-test/test_se050.c b/SE050Sim/sdk-test/test_se050.c index 1eed29e..ce6502d 100644 --- a/SE050Sim/sdk-test/test_se050.c +++ b/SE050Sim/sdk-test/test_se050.c @@ -332,6 +332,7 @@ static void test_ecdh(const char *name, uint32_t obj_a, uint32_t obj_b, uint8_t shared[64] = {0}; size_t shared_len = sizeof(shared); size_t shared_bits = 0; + uint8_t dummy[64] = {0}; /* Generate two key pairs */ cleanup_object(obj_a); @@ -357,7 +358,7 @@ static void test_ecdh(const char *name, uint32_t obj_a, uint32_t obj_b, /* Compute ECDH(A_priv, B_pub) */ sss_key_object_init(&derived_key, &g_ks); status = sss_key_object_allocate_handle(&derived_key, obj_ss, - kSSS_KeyPart_Default, kSSS_CipherType_Binary, key_bytes, + kSSS_KeyPart_Default, kSSS_CipherType_HMAC, key_bytes, kKeyObject_Mode_Transient); ASSERT_OK(status, "derived allocate"); @@ -366,12 +367,20 @@ static void test_ecdh(const char *name, uint32_t obj_a, uint32_t obj_b, ASSERT_OK(status, "derive_key_context_init"); sss_key_store_erase_key(&g_ks, &derived_key); + /* Applet 7.2+ stores the shared secret into a pre-existing HMACKey + * object whose size must equal the secret exactly; create it before + * the derive */ + status = sss_key_store_set_key(&g_ks, &derived_key, dummy, + key_bytes, key_bytes * 8, NULL, 0); + ASSERT_OK(status, "derived pre-create"); status = sss_derive_key_dh(&derive_ctx, &key_b, &derived_key); ASSERT_OK(status, "derive_key_dh"); sss_derive_key_context_free(&derive_ctx); - /* Read shared secret */ + /* Read shared secret. sss_key_store_get_key has no HMAC read case, so + * read the object back as AES type (both are a plain ReadObject). */ + derived_key.cipherType = kSSS_CipherType_AES; status = sss_key_store_get_key(&g_ks, &derived_key, shared, &shared_len, &shared_bits); ASSERT_OK(status, "get shared secret"); @@ -1390,6 +1399,7 @@ static void test_x25519_ecdh(void) size_t shared_a_len = sizeof(shared_a), shared_b_len = sizeof(shared_b); size_t shared_bits = 0; uint8_t zeros[32] = {0}; + uint8_t dummy[32] = {0}; cleanup_object(id_a); cleanup_object(id_b); @@ -1417,10 +1427,16 @@ static void test_x25519_ecdh(void) /* ECDH(A_priv, B_pub) */ sss_key_object_init(&derived_a, &g_ks); status = sss_key_object_allocate_handle(&derived_a, id_ss_a, - kSSS_KeyPart_Default, kSSS_CipherType_Binary, 32, + kSSS_KeyPart_Default, kSSS_CipherType_HMAC, 32, kKeyObject_Mode_Transient); ASSERT_OK(status, "derived_a allocate"); + /* Applet 7.2+: derive target must be a pre-existing HMACKey object + * sized exactly to the shared secret */ + status = sss_key_store_set_key(&g_ks, &derived_a, dummy, + sizeof(dummy), sizeof(dummy) * 8, NULL, 0); + ASSERT_OK(status, "derived_a pre-create"); + status = sss_derive_key_context_init(&derive_ctx, &g_ctx.session, &key_a, kAlgorithm_SSS_ECDH, kMode_SSS_ComputeSharedSecret); ASSERT_OK(status, "derive_a context_init"); @@ -1428,6 +1444,7 @@ static void test_x25519_ecdh(void) ASSERT_OK(status, "derive_a dh"); sss_derive_key_context_free(&derive_ctx); + derived_a.cipherType = kSSS_CipherType_AES; status = sss_key_store_get_key(&g_ks, &derived_a, shared_a, &shared_a_len, &shared_bits); ASSERT_OK(status, "get shared_a"); @@ -1435,10 +1452,14 @@ static void test_x25519_ecdh(void) /* ECDH(B_priv, A_pub) */ sss_key_object_init(&derived_b, &g_ks); status = sss_key_object_allocate_handle(&derived_b, id_ss_b, - kSSS_KeyPart_Default, kSSS_CipherType_Binary, 32, + kSSS_KeyPart_Default, kSSS_CipherType_HMAC, 32, kKeyObject_Mode_Transient); ASSERT_OK(status, "derived_b allocate"); + status = sss_key_store_set_key(&g_ks, &derived_b, dummy, + sizeof(dummy), sizeof(dummy) * 8, NULL, 0); + ASSERT_OK(status, "derived_b pre-create"); + status = sss_derive_key_context_init(&derive_ctx, &g_ctx.session, &key_b, kAlgorithm_SSS_ECDH, kMode_SSS_ComputeSharedSecret); ASSERT_OK(status, "derive_b context_init"); @@ -1446,6 +1467,7 @@ static void test_x25519_ecdh(void) ASSERT_OK(status, "derive_b dh"); sss_derive_key_context_free(&derive_ctx); + derived_b.cipherType = kSSS_CipherType_AES; status = sss_key_store_get_key(&g_ks, &derived_b, shared_b, &shared_b_len, &shared_bits); ASSERT_OK(status, "get shared_b"); diff --git a/SE050Sim/se050-sim/src/dispatch.rs b/SE050Sim/se050-sim/src/dispatch.rs index 9e71529..4dd0429 100644 --- a/SE050Sim/se050-sim/src/dispatch.rs +++ b/SE050Sim/se050-sim/src/dispatch.rs @@ -45,6 +45,7 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { P1_EC => handlers::ec::handle_write_ec_key(apdu, store), P1_RSA => handlers::rsa::handle_write_rsa_key(apdu, store), P1_AES => handlers::aes::handle_write_aes_key(apdu, store), + P1_HMAC => handlers::aes::handle_write_hmac_key(apdu, store), P1_CRYPTO_OBJ => handlers::crypto_obj::handle_create(apdu, store), P1_CURVE => { // CreateECCurve / SetECCurveParam: our crypto libs have curves built-in @@ -128,7 +129,8 @@ pub fn dispatch(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { (P1_SIGNATURE, P2_VERIFY) => handlers::ec::handle_verify(apdu, store), // ECDH shared secret (P2_DH=0x0F or P2_DH_REVERSE=0x59) - (P1_EC, P2_DH) | (P1_EC, 0x59) => handlers::ec::handle_ecdh(apdu, store), + (P1_EC, P2_DH) | (P1_EC, 0x59) => handlers::ec::handle_ecdh( + apdu, store, handlers::ec::strict_ecdh_from_env()), // AES cipher oneshot (P1_CIPHER, P2_ENCRYPT_ONESHOT) => { diff --git a/SE050Sim/se050-sim/src/handlers/aes.rs b/SE050Sim/se050-sim/src/handlers/aes.rs index 5bd88b3..0fced73 100644 --- a/SE050Sim/se050-sim/src/handlers/aes.rs +++ b/SE050Sim/se050-sim/src/handlers/aes.rs @@ -81,6 +81,33 @@ pub fn handle_write_aes_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduR } } +/// Handle WRITE HMAC key command (WriteSymmKey with P1=HMAC). +/// Tag1=obj_id(4B), Tag3=key_data. HMAC keys have no fixed length, so any +/// non-empty Tag3 value is accepted as-is. +pub fn handle_write_hmac_key(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { + let tlvs = match apdu.parse_tlvs() { + Ok(t) => t, + Err(_) => return ApduResponse::error(SW_WRONG_DATA), + }; + + let obj_id = match tlv::find_tlv(&tlvs, TAG_1) { + Some(t) if t.value.len() == 4 => { + let mut id = [0u8; 4]; + id.copy_from_slice(&t.value); + id + } + _ => return ApduResponse::error(SW_WRONG_DATA), + }; + + match tlv::find_tlv(&tlvs, TAG_3) { + Some(t) if !t.value.is_empty() => { + store.insert(obj_id, SecureObject::HMACKey { key: t.value.clone() }); + ApduResponse::success() + } + _ => ApduResponse::error(SW_WRONG_DATA), + } +} + /// Handle AES Encrypt Oneshot. /// INS=Crypto, P1=Cipher, P2=EncryptOneshot /// Tag1=key_id(4B), Tag2=cipher_mode(1B), Tag3=plaintext, Tag4=IV(opt) @@ -385,3 +412,53 @@ where Some(result) } + +#[cfg(test)] +mod hmac_write_tests { + use super::*; + use crate::dispatch::dispatch; + + #[test] + fn test_write_hmac_key_via_dispatch() { + // WriteSymmKey with P1=HMAC and the transient INS bit set + // (kSE05x_INS_WRITE | kSE05x_INS_TRANSIENT = 0x21), as sent by + // sss_key_store_set_key for a kSSS_CipherType_HMAC object. + let key = vec![0xA5u8; 32]; + let mut data = vec![TAG_1, 0x04, 0x00, 0x00, 0x00, 0x66]; + data.push(TAG_3); + data.push(key.len() as u8); + data.extend_from_slice(&key); + + let apdu = ParsedApdu { + cla: 0x80, + ins: 0x21, + p1: P1_HMAC, + p2: P2_DEFAULT, + data, + le: None, + }; + let mut store = ObjectStore::new(); + let resp = dispatch(&apdu, &mut store); + assert_eq!(resp.sw, 0x9000); + match store.get(&[0x00, 0x00, 0x00, 0x66]) { + Some(SecureObject::HMACKey { key: stored }) => assert_eq!(stored, &key), + _ => panic!("HMACKey object not stored"), + } + } + + #[test] + fn test_write_hmac_key_empty_value_rejected() { + let data = vec![TAG_1, 0x04, 0x00, 0x00, 0x00, 0x66, TAG_3, 0x00]; + let apdu = ParsedApdu { + cla: 0x80, + ins: 0x01, + p1: P1_HMAC, + p2: P2_DEFAULT, + data, + le: None, + }; + let mut store = ObjectStore::new(); + let resp = dispatch(&apdu, &mut store); + assert_eq!(resp.sw, SW_WRONG_DATA); + } +} diff --git a/SE050Sim/se050-sim/src/handlers/ec.rs b/SE050Sim/se050-sim/src/handlers/ec.rs index 594983d..435d3d9 100644 --- a/SE050Sim/se050-sim/src/handlers/ec.rs +++ b/SE050Sim/se050-sim/src/handlers/ec.rs @@ -183,9 +183,11 @@ fn import_ec_key( _key_type: u8, store: &mut ObjectStore, ) -> ApduResponse { - // Ed25519 verify needs the stored public key (ed25519_dalek cannot derive - // a verifying key from a signature alone). Derive it at import time. - // ECC verify paths derive pub-from-priv on demand so they don't need this. + // Derive the public key from the private key at import time for every + // curve. Ed25519 verify needs it (ed25519_dalek cannot derive a verifying + // key from a signature alone), and ReadObject returns the public part of + // an asymmetric object, which hosts parse (e.g. wc_ecc_use_key_id reads + // the public key back after importing a private-only key). let public_key = match curve { ECCurve::Ed25519 if private_key_data.len() == 32 => { let mut priv_bytes = [0u8; 32]; @@ -195,6 +197,35 @@ fn import_ec_key( .to_bytes() .to_vec() } + ECCurve::NistP224 if private_key_data.len() == 28 => { + match p224::ecdsa::SigningKey::from_bytes(private_key_data.into()) { + Ok(sk) => sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(), + Err(_) => vec![], + } + } + ECCurve::NistP256 if private_key_data.len() == 32 => { + match p256::ecdsa::SigningKey::from_bytes(private_key_data.into()) { + Ok(sk) => sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(), + Err(_) => vec![], + } + } + ECCurve::NistP384 if private_key_data.len() == 48 => { + match p384::ecdsa::SigningKey::from_bytes(private_key_data.into()) { + Ok(sk) => sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(), + Err(_) => vec![], + } + } + ECCurve::Curve25519 if private_key_data.len() == 32 => { + // Stored reversed (BE) like generate_x25519_keypair: reverse the + // private key to LE, derive, store the public key reversed again. + let mut priv_bytes = [0u8; 32]; + priv_bytes.copy_from_slice(private_key_data); + priv_bytes.reverse(); + let secret = x25519_dalek::StaticSecret::from(priv_bytes); + let mut pub_bytes = x25519_dalek::PublicKey::from(&secret).to_bytes(); + pub_bytes.reverse(); + pub_bytes.to_vec() + } _ => vec![], }; store.insert( @@ -430,11 +461,27 @@ pub fn handle_verify(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse ApduResponse::success_with_tlvs(&[Tlv::new(TAG_1, &[result_byte])]) } +/// Whether the applet 7.2 strict ECDH InObject contract is enforced. +/// Off by default so hosts that predate the contract keep working; set +/// SE050_SIM_STRICT_ECDH=1 to enforce it. +pub fn strict_ecdh_from_env() -> bool { + std::env::var("SE050_SIM_STRICT_ECDH").map(|v| v == "1").unwrap_or(false) +} + /// Handle ECDH shared secret generation. /// INS=Crypto, P1=EC, P2=DH(0x0F) /// Tag1=privateKeyID(4B), Tag2=peerPublicKey, Tag7=sharedSecretOutputID(4B) -/// The shared secret is stored as a binary object at sharedSecretOutputID. -pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { +/// Per the SE05x IoT applet spec (7.2+, see Se05x_API_ECDHGenerateSharedSecret +/// in the Plug & Trust middleware), the Tag7 target must reference an existing +/// HMACKey object whose size equals the shared secret exactly; otherwise the +/// applet returns SW_CONDITIONS_NOT_SATISFIED. The shared secret overwrites +/// that object's value. +/// With `strict` false (the default, see strict_ecdh_from_env) a missing +/// target instead falls back to the legacy simulator behavior of implicitly +/// creating a Binary object, so hosts that predate the applet 7.2 contract +/// keep working. A pre-existing HMACKey target gets the exact-size contract +/// in both modes. +pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore, strict: bool) -> ApduResponse { let tlvs = match apdu.parse_tlvs() { Ok(t) => t, Err(_) => return ApduResponse::error(SW_WRONG_DATA), @@ -468,6 +515,16 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { None => return ApduResponse::error(SW_FILE_NOT_FOUND), }; + // 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 + // missing (or non-HMACKey) target keeps the legacy implicit-create + // behavior instead (target_len None). + let target_len = match store.get(&output_id) { + Some(SecureObject::HMACKey { key }) => Some(key.len()), + _ if strict => return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), + _ => None, + }; + let shared_secret = match &key_obj { SecureObject::ECKeyPair { curve: ECCurve::NistP224, private_key, .. } => { p224_ecdh(private_key, peer_pubkey) @@ -486,7 +543,19 @@ pub fn handle_ecdh(apdu: &ParsedApdu, store: &mut ObjectStore) -> ApduResponse { match shared_secret { Some(secret) => { - store.insert(output_id, SecureObject::Binary { data: secret.clone() }); + match target_len { + Some(len) => { + if secret.len() != len { + return ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED); + } + store.insert(output_id, SecureObject::HMACKey { key: secret }); + } + None => { + // Legacy lenient behavior: implicitly create the target + // as a Binary object. + store.insert(output_id, SecureObject::Binary { data: secret }); + } + } ApduResponse::success() } None => ApduResponse::error(SW_CONDITIONS_NOT_SATISFIED), @@ -590,6 +659,144 @@ mod tests { let tlvs = crate::tlv::parse_tlvs(&resp.data).unwrap(); assert!(p384_verify(&private_key, &hash, &tlvs[0].value)); } + + fn tlv_bytes(tag: u8, value: &[u8]) -> Vec { + let mut v = vec![tag, value.len() as u8]; + v.extend_from_slice(value); + v + } + + /// Build an ECDHGenerateSharedSecret_InObject APDU and a store holding a + /// P-256 key pair at 0x64. Returns (apdu, store, expected_secret). + fn ecdh_inobject_fixture() -> (ParsedApdu, ObjectStore, Vec) { + let sk = p256::ecdsa::SigningKey::random(&mut OsRng); + let private_key = sk.to_bytes().to_vec(); + let public_key = sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(); + + let peer = p256::ecdsa::SigningKey::random(&mut OsRng); + let peer_pub = peer.verifying_key().to_encoded_point(false).as_bytes().to_vec(); + + let expected = p256_ecdh(&private_key, &peer_pub).unwrap(); + + let mut store = ObjectStore::new(); + store.insert([0, 0, 0, 0x64], SecureObject::ECKeyPair { + curve: ECCurve::NistP256, + private_key, + public_key, + }); + + let mut data = tlv_bytes(TAG_1, &[0, 0, 0, 0x64]); + data.extend(tlv_bytes(TAG_2, &peer_pub)); + data.extend(tlv_bytes(TAG_7, &[0, 0, 0, 0x66])); + + let apdu = ParsedApdu { + cla: 0x80, + ins: 0x03, + p1: P1_EC, + p2: P2_DH, + data, + le: None, + }; + (apdu, store, expected) + } + + #[test] + fn test_ecdh_strict_tag7_target_missing_returns_6985() { + // Applet 7.2 behavior: the Tag7 HMACKey object must be pre-created, + // otherwise SW_CONDITIONS_NOT_SATISFIED. This is the failure mode of + // wolfSSL's se050_ecc_shared_secret against middleware built for + // applet >= 07_02 (never creates the target object). + let (apdu, mut store, _) = ecdh_inobject_fixture(); + let resp = handle_ecdh(&apdu, &mut store, true); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + assert!(store.get(&[0, 0, 0, 0x66]).is_none()); + } + + #[test] + fn test_ecdh_strict_tag7_wrong_type_target_returns_6985() { + let (apdu, mut store, _) = ecdh_inobject_fixture(); + store.insert([0, 0, 0, 0x66], SecureObject::Binary { data: vec![0u8; 32] }); + let resp = handle_ecdh(&apdu, &mut store, true); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED); + } + + #[test] + fn test_ecdh_tag7_wrong_size_hmackey_returns_6985_both_modes() { + // The exact-size contract applies whenever the target exists as an + // HMACKey, in strict and lenient mode alike. + for strict in [true, false] { + let (apdu, mut store, _) = ecdh_inobject_fixture(); + store.insert([0, 0, 0, 0x66], SecureObject::HMACKey { key: vec![0u8; 16] }); + let resp = handle_ecdh(&apdu, &mut store, strict); + assert_eq!(resp.sw, SW_CONDITIONS_NOT_SATISFIED, "strict={}", strict); + } + } + + #[test] + fn test_ecdh_tag7_precreated_hmackey_succeeds_both_modes() { + for strict in [true, false] { + let (apdu, mut store, expected) = ecdh_inobject_fixture(); + store.insert([0, 0, 0, 0x66], SecureObject::HMACKey { key: vec![0u8; 32] }); + let resp = handle_ecdh(&apdu, &mut store, strict); + assert_eq!(resp.sw, 0x9000, "strict={}", strict); + match store.get(&[0, 0, 0, 0x66]) { + Some(SecureObject::HMACKey { key }) => assert_eq!(key, &expected), + other => panic!("expected HMACKey with shared secret, got {:?}", other.is_some()), + } + } + } + + #[test] + fn test_import_private_only_p256_derives_public() { + // wc_ecc_use_key_id imports a private-only key pair and then reads + // the public part back via ReadObject, so import must derive it. + let sk = p256::ecdsa::SigningKey::random(&mut OsRng); + let expected_pub = sk.verifying_key().to_encoded_point(false).as_bytes().to_vec(); + + let mut store = ObjectStore::new(); + let resp = import_ec_key([0, 0, 0, 0x32], ECCurve::NistP256, + &sk.to_bytes().to_vec(), P1_KEY_PAIR, &mut store); + assert_eq!(resp.sw, 0x9000); + match store.get(&[0, 0, 0, 0x32]) { + Some(SecureObject::ECKeyPair { public_key, .. }) => { + assert_eq!(public_key, &expected_pub); + } + other => panic!("expected ECKeyPair, got {:?}", other.is_some()), + } + } + + #[test] + fn test_import_private_only_x25519_derives_public() { + let secret = x25519_dalek::StaticSecret::random_from_rng(OsRng); + let mut expected_pub = x25519_dalek::PublicKey::from(&secret).to_bytes(); + expected_pub.reverse(); + let mut priv_be = secret.to_bytes(); + priv_be.reverse(); + + let mut store = ObjectStore::new(); + let resp = import_ec_key([0, 0, 0, 0x33], ECCurve::Curve25519, + &priv_be, P1_KEY_PAIR, &mut store); + assert_eq!(resp.sw, 0x9000); + match store.get(&[0, 0, 0, 0x33]) { + Some(SecureObject::ECKeyPair { public_key, .. }) => { + assert_eq!(public_key, &expected_pub.to_vec()); + } + other => panic!("expected ECKeyPair, got {:?}", other.is_some()), + } + } + + #[test] + fn test_ecdh_lenient_tag7_target_missing_creates_binary() { + // Legacy behavior for hosts that predate the applet 7.2 contract: + // a missing target is implicitly created as a Binary object. + let (apdu, mut store, expected) = ecdh_inobject_fixture(); + let resp = handle_ecdh(&apdu, &mut store, false); + assert_eq!(resp.sw, 0x9000); + match store.get(&[0, 0, 0, 0x66]) { + Some(SecureObject::Binary { data }) => assert_eq!(data, &expected), + other => panic!("expected Binary with shared secret, got {:?}", other.is_some()), + } + } } #[cfg(test)]