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.