Attested Secure Keys
A Flutter plugin that mints non-exportable signing keys inside the phone’s secure hardware — Android StrongBox/TEE or the iOS Secure Enclave — and produces a manufacturer-signed proof that the key was really born there, which your server can verify. Like flutter_secure_storage, but for keys, not data.
The brief
EU Digital Identity (EUDI) wallets — and most serious fintech and identity apps — have a hard requirement: the private key that binds a credential to its holder must live inside the device’s secure hardware, must be non-exportable, and the backend must be able to prove the key was actually born in hardware before it trusts anything signed with it.
This library began as the key layer for an EUDI-wallet proof-of-concept, then generalised so any app needing hardware-backed keys can reuse it. Its protocol companion — issuance, holding and selective presentation of credentials — lives in SD-JWT VC + OpenID4VC.
The gap it fills
As of mid-2026 no package on pub.dev exposed key attestation. The obvious candidate, flutter_secure_storage, solves a different problem: it encrypts data. A private key parked there is still a software key — it round-trips through your app’s memory every time you use it, so it can be lifted from a compromised process, and there is no way to prove to a server where it came from.
The fix is to never let the key exist in app memory at all. The OS-certified secure element generates it, holds it, and uses it — and the device manufacturer signs a certificate vouching that it did. This plugin wraps both platforms’ native APIs behind one small, honest Dart surface.
How it talks to the secure hardware
It’s a federated Flutter plugin. Your app depends only on the Dart facade; calls travel through a typed Pigeon channel (no stringly-typed maps) into first-party native code, which speaks directly to the hardware-backed keystore. Crucially, the boundary is one-way: the private key is created below the line and never crosses back up.
Proving the key was born in hardware
This is the whole point of the library. A key’s hardware origin is vouched for by the device manufacturer — Google’s Hardware Attestation Root on Android, Apple’s App Attest Root on iOS — not by the app and not by the library. The plugin generates the key in certified hardware and surfaces the manufacturer’s signed proof; your backend makes the trust decision.
End to end
Put together, a single server nonce ties the whole flow together: it’s bound into the key at generation, echoed back in the attestation (so a replay can’t be reused), and the same key later signs proofs-of-possession the server validates against the public key it bound at enrolment.
What it does
A facade modeled on the ergonomics of flutter_secure_storage, with eight methods:
- capabilities()
- Probe what the device can actually do — StrongBox / TEE / Secure Enclave, attestation and biometric support, best achievable security level.
- generateKey()
- Mint a non-exportable EC P-256 key in the strongest available hardware. Fails closed if the requested security floor can't be met.
- sign()
- ES256-sign inside the chip; returns the signature as raw 64-byte R‖S bytes plus a base64url .jose form (JOSE / COSE ready). Fires the biometric prompt for auth-gated keys.
- attest()
- Return the key's manufacturer-signed proof bound to a server nonce — an Android Keystore X.509 chain or an iOS App Attest object.
- getKeyInfo · containsKey
- Inspect live key metadata, or check whether an alias exists.
- deleteKey · listAliases
- Permanently delete a key, or enumerate the aliases this library manages.
final keys = AttestedSecureKeys();
// 1 · Mint a non-exportable P-256 key in the strongest hardware
// available, binding the server's nonce into the attestation.
final key = await keys.generateKey(
alias: 'wallet.holderKey',
minSecurityLevel: KeySecurityLevel.trustedEnvironment,
userAuth: const UserAuthPolicy.perUseBiometric(),
attestationChallenge: nonceFromServer,
);
// 2 · Hand the public key + manufacturer-signed proof to your backend.
final attestation =
await keys.attest(alias: key.alias, serverNonce: nonceFromServer);
await api.registerWalletKey(jwk: key.publicJwk, attestation: attestation);
// 3 · Later: sign a proof-of-possession. The biometric prompt fires
// automatically for an auth-gated key.
final sig = await keys.sign(alias: key.alias, payload: utf8.encode(jwt));
final jws = '$jwt.${sig.jose}';- Hardware
- StrongBox (API 28+) → TEE
- Attestation
- Keystore X.509 chain (API 24+)
- Signature
- DER → raw R‖S (JDK BigInteger)
- Min OS
- API 24
- Hardware
- Secure Enclave (iOS 13+)
- Attestation
- App Attest (iOS 14+)
- Signature
- native raw R‖S (no DER)
- Min OS
- iOS 13 / 14
Design decisions & honest trade-offs
Keys, signatures and attestation come solely from the platform’s own security frameworks (Android Keystore, Apple CryptoKit / Security / DeviceCheck) plus official libraries (Pigeon). No hand-rolled crypto, no heavyweight third-party dependency — the smallest, all-official surface, because this is the most security-sensitive part of the stack.
P-256 is the cross-platform floor (the iOS Secure Enclave is P-256-only) and the EUDI mdoc / SD-JWT VC baseline. No RSA, P-384 or EdDSA — one curve, both platforms, zero ambiguity.
The client-reported security level is a UX hint, never a trust decision. A key only counts once your backend verifies its attestation against the genuine Google / Apple roots — chain, security level, freshness, and the echoed nonce.
Every result states the assurance it actually achieved; the library never silently downgrades. If biometric gating is requested but the OS doesn’t enforce it, generation fails rather than handing back an ungated key.
iOS has no per-key X.509 attestation. App Attest attests the app instance, so the library binds the Secure Enclave key by hashing its JWK thumbprint + the server nonce into the App Attest challenge — one normalized model papering over a real platform difference.
It provides hardware-backed keys and the manufacturer’s attestation artifacts; it is not a certified eIDAS Wallet Secure Cryptographic Device and makes no Level-of-Assurance claim. Certification (CC / EUCC) is the integrator’s responsibility — stated plainly, on purpose.
Status
Published to pub.dev as 0.1.0-dev.1 while the API soaks toward a stable 0.1.0. Both platforms are device-verified — Android (StrongBox / TEE attestation) on real hardware via Firebase Test Lab, and iOS (Secure Enclave + App Attest) on a physical iPhone, with the exported Android attestation decoded end-to-end to a genuine Google root. It’s the key layer of a working EUDI wallet. Open source under Apache-2.0.