Few calls.
Any platform.
Identity Layer integrates with minimal surface by design. MicroPython on a microcontroller, Flutter on Android and Windows, or a PHP bridge dropped into your existing endpoints. The same protocol. The same cryptographic guarantees. Four calls.
Boot to verified. 6.5 seconds.
The same Ed25519 challenge/response protocol that authenticates a mobile user or a regulated operator runs on a microcontroller. Device-bound key. No credential database. JWT issued on verification.
IdentityIoT,
installed via mip.
# Install via mip — no external dependencies # import mip; mip.install("github:wide/identity-micropython") from identity_iot import IdentityIoT # 1. Init — configure node identity identity = IdentityIoT( base_url = cfg['base_url'], node_type = cfg['node_type'], tenant_id = cfg['tenant_id'], comm_key = cfg['comm_key'].encode(), comm_iv = cfg['comm_iv'].encode(), ) # 2. Register — device-bound key, runs once identity.ensure_registered() # 3. Authenticate — Ed25519 challenge / verify / JWT token = identity.authenticate() # 4. Authenticated request — AES-256-GCM encrypted payload response = identity.post_encrypted({'request': 'record_sensor_data', 'value': 42})
| Component | Model | Role |
|---|---|---|
| MCU | Raspberry Pi Pico 2W (RP2350) | TrustZone, WiFi, device-bound key storage |
| NFC reader | PN532 | I2C interface, reads NTAG424 DNA badges |
| Display | SSD1306 128x64 OLED | Boot sequence, verification status |
| NFC badge | NTAG424 DNA (NXP) | AES-128 onboard, non-exportable keys, dynamic CMAC |
Identity IoT nodes are not limited to NFC access. The same authenticated node can carry any sensor payload. All readings are transmitted via AES-256-GCM encrypted POST after the node obtains its JWT.
| Sensor type | Typical hardware | Payload field |
|---|---|---|
| Temperature | DHT22, DS18B20, BME280 | temperature (float, °C) |
| Humidity | DHT22, BME280 | humidity (float, %) |
| Pressure | BME280, BMP390 | pressure (float, hPa) |
| PIR motion | HC-SR501 | motion (bool) |
| GPS | NEO-6M, L76X | lat, lon, altitude (float) |
| Energy metering | PZEM-004T, INA219 | voltage, current, power (float) |
| NFC access | PN532 + NTAG424 DNA | badge_uid, granted (bool) |
{ "node_uid" : "273ec83a-84c8-4967-aafa-0780ab161cc1", "tenant_id" : "0b3cfe4d-fdf7-46c2-bd41-b533da415dd9", "node_type" : "sensor", "temperature": 24.3, "humidity" : 58.1, "pressure" : 1013.2, "updated_at" : "2026-06-09 11:42:07" }
Badge read. Challenge issued.
Ed25519 verified.
Identity Tag is the application layer built on top of Identity IoT. Each badge read triggers a full Ed25519 challenge/response cycle against the backend. No stored secrets on the reader. No cleartext identity on the badge. Multi-tenant by design.
# 1. Reader reads NTAG424 DNA NDEF payload # Fields: epk (encrypted private key), df2 (DEK fragment 2), mode (STD|HS) # 2. DEK reconstruction dek = df1_from_config XOR df2_from_badge # 3. Decrypt badge private key badge_privkey = AES_CBC_decrypt(dek, epk) # 4. Request challenge from backend challenge = POST identity_tag/it_request_challenge # { node_uid, badge_uid, tenant_id } # 5. Sign challenge with badge key (Ed25519) signature = ed25519_sign(badge_privkey, challenge_nonce) # 6. Verify at backend result = POST identity_tag/it_verify_badge # { challenge_id, signature_b64 } -> { granted: true } # 7. Reader actuates relay (GRANTED) or displays DENIED
| Mode | Description | Use case |
|---|---|---|
| STD | Badge works on any terminal in the authorised fleet for that tenant. | Office access, general entry points. |
| HS | Badge cryptographically bound to a specific terminal. The DEK derivation incorporates a hash of the terminal public key. Badge is invalid on any other reader. | High-security rooms, datacenter racks, equipment cabinets. |
| Endpoint | Method | Role |
|---|---|---|
| identity_tag/it_request_challenge | POST | Issue nonce for badge verification |
| identity_tag/it_verify_badge | POST | Verify Ed25519 signature, return GRANTED/DENIED |
| identity_tag/it_node_checkin | POST | Node heartbeat and JWT refresh |
| identity_tag/it_register_badge | POST (manager) | Write badge public key and entitlement |
| identity_tag/it_list_nodes | GET (manager) | Node fleet status for tenant |
The provisioning and management app runs on Android. It uses the device's NFC to write badge payloads and connects to the backend over authenticated Identity Layer channels.
// identity_tag package - badge provisioning final writer = IdentityTagWriter(api: tagApi); // Generate badge keypair and encrypt private key with split DEK final payload = await writer.prepareBadgePayload( tenantId : tenant.id, mode : BadgeMode.STD, // or BadgeMode.HS nodeUid : null, // required for HS mode ); // Write NDEF to NTAG424 DNA via flutter_nfc_kit await writer.writeToTag(payload);
Device-bound identity.
Four calls.
Local packages. No external identity service. The private key never leaves the device.
Secure storage via flutter_secure_storage
and TEE / Secure Enclave when available.
Payload encryption is end-to-end via ITEMSEncrypter.
// 1. Init — JWT identity, silent on subsequent boots final auth = IdentityAuthServiceStd(ITEMSGlobals.authURI); await auth.ensureJwt( preferredLocale: locale.languageCode, utcOffsetMin: DateTime.now().timeZoneOffset.inMinutes, ); // 2. Handshake — silent if identity already present on device final result = await api.mjHandshake( identityHash: identityHash, publicKey: publicKey, ); // 3. Store JWT — session active, no password, no credential database await _storage.write(key: 'token', value: result['token']); // 4. Every request — AES-256-GCM payload, end-to-end encrypted final response = await api.postEncrypted({'request': 'your_request'});
dependencies: # Identity Layer — local packages, no external registry identity_package: path: packages/identity_package identity_std: path: packages/identity_std identity_hs: path: packages/identity_hs items_crypto: path: packages/items_crypto
| Platform | Status | Secure storage |
|---|---|---|
| Android | Production, Google Play cleared | Android Keystore / TEE |
| Windows | Production, Microsoft Store cleared | Windows Credential Manager |
| Linux | Production | libsecret |
| iOS / macOS | On roadmap | Secure Enclave |
5 KB.
Your stack, unchanged.
The PHP bridge is a single require that sits in front of your existing endpoints. Your routes, your database, your business logic. Untouched.. Identity Layer handles authentication and payload encryption. Remove it the same way you added it.
require_once
and two function calls. Your endpoints start receiving verified, decrypted payloads.
No schema migration. No user table changes. No session storage.
The complete server-side stack is 225 KB.
// 1. Include the Identity Layer bridge — 5 KB require_once 'identity/_mj_auth.php'; // 2. Authenticate — JWT verified, identity_hash extracted // identity_hash is a 64-char SHA-256 hex — never a user ID [$identityHash, $tenantId, $claims, $role] = mj_authenticate_identity($encoder); // 3. Every response — AES-256-GCM encrypted, end-to-end sendEncryptedResponse($encoder, [ 'status' => 'ok', 'data' => $yourData, ], ['ts' => time()], 200);
mj_mandate_log endpoint
registers the mandate and opens a 72-hour export window.
Logs are destroyed after collection. The requesting authority bears legal responsibility.
// Requires role = admin — never operator [$userId, $tenantId, $claims, $role] = mj_authenticate($encoder); mj_require_role($encoder, $claims, 'admin'); // Register mandate — opens 72h export window // target_identity_hash: 64-char SHA-256, never plaintext identity // Ciphertext is never stored — metadata only $connector->executeDatabaseParameterQuery( "INSERT INTO audit_log (mandate_number, issuing_country, issuing_court, target_identity_hash, export_status) VALUES (?, ?, ?, ?, 'PENDING')", [$mandateNumber, $issuingCountry, $issuingCourt, $targetHash], $db );
| Component | Technology | Notes |
|---|---|---|
| Runtime | PHP 8.x | Vanilla, no framework required |
| Database | MySQL / MariaDB | Standard schema, no credential columns |
| Cache / ephemeral | Redis | Challenge tokens, message queues (TTL < 1 min) |
| Bridge size | 5 KB | Single PHP file, drop into any LAMP stack |
| Full stack size | 225 KB | Identity STD + HS + Keys + Secure Channel + IoT + Labels + Tag |
| Deployment | Container / on-premise / air-gapped | No mandatory cloud connectivity |