Skip to content

API guide

How to do the common jobs with the driver: bring the chip up, read its state, store data, use keys, send your own APDUs, open a secure channel and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with Se standing for se050::Device<MyI2c>. The API reference lists every member.

The shape of the API

One Device<Transport> per chip. It owns a T1Session (the link) and an scp03::Session (the optional secure channel), and offers the IoT applet's commands as member functions. Under it, free functions build and parse the bytes, so you can send any command the device object does not cover.

Part Where Use it for
Device commands se050_device.hpp The applet commands below, with typed arguments and results
T1() T1Session, se050_t1_session.hpp Link bring-up (ChipWarmReset(), GetAnswerToReset()), timing settings, recovery
Scp03() scp03::Session, se050_scp03.hpp Opening and closing the platform SCP03 channel
TransmitApdu() Device Any serialised C-APDU; wrapped when the channel is open
se050::apdu, se050::tlv, se050::cmd se050_apdu.hpp, se050_tlv.hpp, se050_commands.hpp Building and parsing APDUs, TLVs and applet commands yourself
se050::atr, se050::applet se050_atr.hpp, se050_applet.hpp ATR profile parsing; SELECT builders and AIDs
Device command Returns
SelectDefaultIoTApplet(), SelectCardManager() The raw R-APDU: check SW yourself
GetVersion() cmd::VersionInfo: applet version, configuration, secure box
GetRandom() The bytes you ask for
GetFreeMemory() Free bytes of one memory type (at most 32767)
WriteBinary(), DeleteSecureObject() Status
ReadObject(), CheckObjectExists() Data; bool
GenerateEcKeyPair(), ReadPublicEcKey() Status; the public key
EcdsaSign(), EcdsaVerify() DER signature; bool
WriteAesKey(), CipherAesEcb() Status; the output blocks
UnwrapAes256Rfc3394() A 32-byte key, unwrapped with a KEK in the chip

The commands are those of the IoT applet's APDU specification (AN12413): GenerateEcKeyPair() is WriteECKey without a key value, WriteAesKey() is WriteSymmKey, CipherAesEcb() is CipherOneShot.

Every call returns se050::Error. Every argument that receives data comes with its capacity and a length out. Timeouts are in milliseconds and apply to each I2C transfer of the exchange.

Bring the chip up

(void)se.HardwareReset(); // ENA low, then high (no-op when ENA is not wired)
// Erratum I2C.1: read any answer left over from before an MCU reset, then resynchronise.
if (se.T1().RecoverAfterHostReset(100) != se050::Error::Ok) {
  return -3;
}
if (se.T1().ChipWarmReset(100) != se050::Error::Ok) {
  return -1;
}

std::uint8_t atr[64]{};
std::size_t atr_len = 0;
se050::atr::Profile profile{};
if (se.T1().GetAnswerToReset(atr, sizeof(atr), &atr_len, 100) != se050::Error::Ok ||
    se050::atr::Parse(atr, atr_len, &profile) != se050::Error::Ok) {
  return -2;
}
std::printf("IFSC %u bytes, max I2C %u kHz\n", static_cast<unsigned>(profile.ifsc),
            static_cast<unsigned>(profile.max_i2c_khz));

std::uint8_t rsp[32]{};
std::size_t rsp_len = 0;
if (se.SelectDefaultIoTApplet(rsp, sizeof(rsp), &rsp_len, 300) != se050::Error::Ok ||
    !sw_ok(rsp, rsp_len)) {
  return -3;
}

The status-word helper used here and below:

// TransmitApdu() and the SELECT helpers return the raw R-APDU: check SW1 SW2 yourself.
bool sw_ok(const std::uint8_t* rapdu, std::size_t rapdu_len) {
  const std::uint8_t* data = nullptr;
  std::size_t data_len = 0;
  se050::apdu::StatusWords sw{};
  return se050::apdu::ParseResponse(rapdu, rapdu_len, &data, &data_len, &sw) == se050::Error::Ok &&
         se050::apdu::IsSuccess(sw);
}

SELECT must succeed once after every reset before applet commands work. T=1 over I2C explains the profile fields.

Read the chip's state

se050::cmd::VersionInfo v{};
if (se.GetVersion(&v, 300) != se050::Error::Ok) {
  return -1;
}
std::uint16_t free_bytes = 0;
if (se.GetFreeMemory(se050::cmd::MemoryType::Persistent, &free_bytes, 300) != se050::Error::Ok) {
  return -2;
}
std::printf("applet %u.%u.%u, %u bytes free\n", v.applet_major, v.applet_minor, v.applet_patch,
            static_cast<unsigned>(free_bytes));

GetFreeMemory() takes MemoryType::Persistent, TransientReset or TransientDeselect; the applet reports at most 32767 bytes.

Store and read data

const se050::cmd::ObjectId id{0x20, 0x00, 0x10, 0x01}; // a binary file you own
const std::uint8_t config[] = {0x01, 0x02, 0x03, 0x04};

bool exists = false;
if (se.CheckObjectExists(id, &exists, 300) != se050::Error::Ok) {
  return -1;
}
if (!exists && se.WriteBinary(id, config, sizeof(config), false, 0, true, sizeof(config), 500) !=
                   se050::Error::Ok) { // create with a fixed file length
  return -2;
}

std::uint8_t back[sizeof(config)]{};
std::size_t back_len = 0;
if (se.ReadObject(id, false, 0, false, 0, back, sizeof(back), &back_len, 300) !=
    se050::Error::Ok) {
  return -3;
}
// When the data is no longer needed:
if (se.DeleteSecureObject(id, 300) != se050::Error::Ok) {
  return -4;
}

WriteBinary() creates the object when you pass a file length (has_file_length, file_length), and writes at an offset into an existing one (has_offset, offset). ReadObject() reads all of it, or a range. The identifiers here are examples; Security model says how to choose yours.

Generate a key and sign

const se050::cmd::ObjectId key{0x20, 0x00, 0x20, 0x01}; // device identity key

bool exists = false;
if (se.CheckObjectExists(key, &exists, 300) != se050::Error::Ok) {
  return -1;
}
if (!exists &&
    se.GenerateEcKeyPair(key, se050::cmd::EcCurve::NistP256, 2000) != se050::Error::Ok) {
  return -2; // the private key is created inside the SE050
}

std::uint8_t pub[96]{};
std::size_t pub_len = 0; // the public key, for your backend
if (se.ReadPublicEcKey(key, pub, sizeof(pub), &pub_len, 300) != se050::Error::Ok) {
  return -3;
}

std::uint8_t sig[80]{};
std::size_t sig_len = 0; // ASN.1 DER ECDSA signature
if (se.EcdsaSign(key, se050::cmd::EcdsaAlgo::Sha256, sha256_digest, sizeof(sha256_digest), sig,
                 sizeof(sig), &sig_len, 1000) != se050::Error::Ok) {
  return -4;
}

bool valid = false;
if (se.EcdsaVerify(key, se050::cmd::EcdsaAlgo::Sha256, sha256_digest, sizeof(sha256_digest), sig,
                   sig_len, &valid, 1000) != se050::Error::Ok ||
    !valid) {
  return -5;
}

The private key is created inside the SE050 and never leaves it. EcdsaSign() signs a digest you computed on the host: pass 32 bytes for EcdsaAlgo::Sha256. The signature is ASN.1 DER (at most 72 bytes for P-256). GenerateEcKeyPair() takes EcCurve::NistP192 to NistP521; key generation takes longer than other commands, so give it a longer timeout.

Send your own APDU

// GET RANDOM by hand: CLA 80, INS 04 (management), P1 00, P2 49, data TLV 41 02 00 10 (16 bytes),
// Le 00.
std::uint8_t data[4]{};
std::size_t data_len = 0;
std::uint8_t capdu[16]{};
std::size_t capdu_len = 0;
if (se050::tlv::AppendU16Be(0x41, 16, data, sizeof(data), &data_len) != se050::Error::Ok ||
    se050::apdu::BuildCaseShort(0x80, 0x04, 0x00, 0x49, data, static_cast<std::uint8_t>(data_len),
                                true, 0x00, capdu, sizeof(capdu),
                                &capdu_len) != se050::Error::Ok) {
  return -1;
}
std::uint8_t rapdu[64]{};
std::size_t rapdu_len = 0;
if (se.TransmitApdu(capdu, capdu_len, rapdu, sizeof(rapdu), &rapdu_len, 300) !=
        se050::Error::Ok ||
    !sw_ok(rapdu, rapdu_len)) {
  return -2;
}

TransmitApdu() takes a serialised C-APDU and returns the R-APDU with SW1 SW2 at the end; it wraps and unwraps when an SCP03 channel is open. apdu::BuildCaseShort() serialises a command with up to 255 data bytes; apdu::BuildCase4Extended() uses the short form when the data fits and extended length above that. Use BuildCaseShort() only for commands that carry data: with zero data bytes it still writes an Lc byte.

Open an SCP03 channel

// Keys come from your provisioning store at run time. Never compile keys into the firmware.
std::uint8_t enc[16]{};
std::uint8_t mac[16]{};
std::uint8_t challenge[8]{};
if (!load_platform_scp03_keys(enc, mac) || !host_random(challenge, sizeof(challenge))) {
  return -1;
}
se050::scp03::StaticKeys keys{};
keys.host_static_key_enc = enc;
keys.host_static_key_enc_len = sizeof(enc);
keys.host_static_key_cmac = mac;
keys.host_static_key_cmac_len = sizeof(mac);
keys.host_challenge = challenge;
keys.host_challenge_len = sizeof(challenge);

// kWrapScratchBytes (320) is enough for short commands; this size covers extended ones too.
static std::uint8_t wrap_scratch[se050::scp03::kWrapScratchBytesExtended]; // not on a small task stack
se.BindScp03WrapScratch(wrap_scratch, sizeof(wrap_scratch));

const se050::Error e = se.Scp03().OpenSecureChannel(se.T1(), keys, 500);
// The session keys are derived now: wipe the static key copies.
platform_zeroize(enc, sizeof(enc));
platform_zeroize(mac, sizeof(mac));
if (e != se050::Error::Ok) {
  return -2; // NotSupported, or Protocol: wrong keys
}
// From here TransmitApdu() and every Device command are wrapped (C-MAC, C-ENC, R-MAC, R-ENC).
// A response without a valid R-MAC returns Error::Authentication and closes the channel; Close()
// wipes the session keys.

OpenSecureChannel() returns Error::NotSupported when a key or the 8-byte host challenge is missing, and Error::Protocol when the card cryptogram does not match (wrong keys). Inside the channel any response without a valid R-MAC returns Error::Authentication and closes the channel; later commands return the same until Scp03().Close() or a new OpenSecureChannel(). The Security model page covers provisioning and the limits of the channel.

Handle errors

se050::Error Meaning and what to do
Ok Success
NotInitialized The port's EnsureInitialized() returned false. Check bus set-up
Transport, Timeout An I2C write failed, or a read failed after the retries. Wiring, address, power, ENA; Troubleshooting
Crc Blocks kept arriving with a wrong CRC after the retransmissions (SetMaxRetransmissions()). Noise, speed, a port that splits writes; recover
Sequence The SE050's N(S) is not the expected one. Recover
Protocol Bad framing, too many WTX requests or retransmissions, or a typed command answered SW other than 90 00. Recover; for a status word, send the command with TransmitApdu() to see it
BufferTooSmall The answer does not fit your buffer, or the SCP03 scratch is missing. Pass a larger buffer
InvalidArgument A null pointer or a length out of range. Fix the call
NotSupported SCP03 without keys, or wrap without an open channel. Provision keys
Authentication Inside the SCP03 channel, a response without a valid R-MAC; or a command after the channel was lost. The keys are wiped. Treat as tampering; Scp03().Close() or open a new channel

After any error below the applet level (Transport, Timeout, Crc, Sequence, Protocol from framing), the T=1 state of host and chip may differ. Start over:

// After any T=1-level error the sequence numbers may disagree with the chip: start over.
se050::Error recover(Se& se) {
  se.T1().ResetSequenceState();
  return se.T1().ChipWarmReset(100);
}

If that fails too, power-cycle the SE050 with HardwareReset() (ENA) and bring it up again. A chip that stretches the clock forever needs a power cycle: see erratum I2C.1 on the Hardware page. T1().RecoverAfterHostReset() at start-up prevents the usual cause, a host reset in the middle of an exchange.