Hardware-backed, attestable EC P-256 keys for Flutter.
Generate non-exportable signing keys inside Android Keystore / StrongBox or the
iOS Secure Enclave, sign with ES256 (raw R‖S, JOSE/COSE-ready), and produce
a server-verifiable proof of hardware origin (Android Keystore attestation /
Apple App Attest). Every result honestly reports the assurance it actually
achieved — the library never silently downgrades.
Built for EUDI-wallet-grade apps, usable by any app that needs to generate and use keys that can't be exfiltrated. Modeled on the ergonomics of
flutter_secure_storage— but for keys, not data.
This repository is a pub workspace (a federated Flutter plugin plus a Node
verifier). App developers depend only on the published
attested_secure_keys package;
everything else here is the implementation and tooling behind it.
EUDI wallets (and many fintech/identity apps) must keep their holder-binding keys
inside the device's secure hardware, keep them non-exportable, and prove
to a server that a key was born in hardware. flutter_secure_storage only does
encrypted data storage — a private key kept there is still a software key that
round-trips through app memory. This package fills that gap: the private key never
leaves the secure element, and the manufacturer's signed attestation lets your
backend verify the hardware origin.
It does not certify keys itself. A key's hardware origin is vouched by the device manufacturer (Google Hardware Attestation Root / Apple App Attest Root); this library generates keys in that certified hardware and surfaces the signed proof for your server to verify.
| 🔑 Hardware keygen | Non-exportable EC P-256 in StrongBox → TEE → software (Android) / Secure Enclave → software (iOS), with an explicit, reported fallback ladder. |
| ✍️ ES256 signing | 64-byte raw R‖S output (base64url), ready to append to a JWS. No DER leaks to callers. |
| 📜 Key attestation | Android X.509 attestation chain; iOS App Attest. Returned verbatim for server-side verification. |
| 🧭 Honest reporting | Every key carries securityLevel + attestationType; requested vs effective levels are both visible — it never pretends. |
| 👆 Biometric / PIN gating | Optional per-use or time-bound user authentication on key use. |
| 🛡️ First-party only | Zero third-party cryptography — only platform frameworks + Google/Apple/Flutter/Dart official libraries. |
See the package READMEs for the full API and the assurance model.
.
├─ pubspec.yaml # pub workspace root
├─ CONTEXT.md # project revival/orientation doc
├─ doc/ # DESIGN.md · DEVICE_TESTING.md · FIREBASE_TEST_LAB.md
├─ .github/workflows/ # ci.yml · publish.yml · device-tests.yml
└─ packages/
├─ attested_secure_keys/ # app-facing facade (+ example app) ← the published package
├─ attested_secure_keys_platform_interface/ # contract + normalized model + Pigeon schema
├─ attested_secure_keys_android/ # Kotlin — first-party Keystore + androidx.biometric
├─ attested_secure_keys_ios/ # Swift — first-party Secure Enclave + App Attest
└─ attested_secure_keys_verifier/ # Node/TS — server-side attestation verifier
App developers depend only on attested_secure_keys; it endorses the platform
packages. The verifier is a separate Node package (not part of the pub workspace).
dependencies:
attested_secure_keys: ^0.1.0import 'package:attested_secure_keys/attested_secure_keys.dart';
const keys = AttestedSecureKeys();
// 1. Generate a non-exportable hardware key, binding a server nonce as the
// attestation challenge (Android fixes the challenge at keygen).
final key = await keys.generateKey(
alias: 'wallet.holderKey',
userAuth: const UserAuthPolicy.perUseBiometric(), // optional gating
attestationChallenge: serverNonce,
);
// 2. Sign — 64-byte raw R‖S, ready for a JWS.
final sig = await keys.sign(alias: 'wallet.holderKey', payload: message);
// 3. Export the attestation and hand it to your backend to verify.
final attestation = await keys.attest(alias: 'wallet.holderKey', nonce: serverNonce);Full usage, the normalized model, and the server-side assurance model live in
packages/attested_secure_keys/README.md.
# Resolve the whole workspace in one shot (always from the repo root):
flutter pub get
# Static analysis across every package:
flutter analyze
# Regenerate the typed platform-channel bindings after editing the Pigeon schema:
cd packages/attested_secure_keys_platform_interface
dart run pigeon --input pigeons/messages.dart
# Run the example app (use a real device — the hardware paths need real HW):
cd packages/attested_secure_keys/example && flutter runThis is a pub workspace, so the repo root has no test/ directory —
flutter test is run inside each package. There are four layers:
# 1. Dart unit tests — facade (fake platform, no native, fast). What CI gates on.
cd packages/attested_secure_keys && flutter test
# 2. Dart unit tests — platform interface (model, encoding, error translation).
cd packages/attested_secure_keys_platform_interface && flutter test
# 3. Dart INTEGRATION tests — exercise the real Kotlin/Swift on a device.
# Requires a connected device or emulator (real hardware to hit StrongBox/SE).
cd packages/attested_secure_keys/example && flutter test integration_test
# 4. Verifier (Node) — attestation chain verification + local self-check.
cd packages/attested_secure_keys_verifier && npm install && npm testWhat each layer covers:
| Layer | Where | Needs a device? | In CI? |
|---|---|---|---|
| Dart unit (facade + interface) | packages/*/test/ |
No (fake platform) | ✅ every push |
| Native code (Kotlin/Swift) | exercised via the integration test | Yes | compile-checked in CI |
| On-device integration | example/integration_test/ |
Yes | opt-in (see below) |
| Verifier | attested_secure_keys_verifier/test/ |
No | ✅ |
Native unit tests: the Android/iOS code has no standalone Kotlin/Swift unit suite — it's exercised through the Dart integration test on a real device and compile-checked in CI (
Build example (Android)/Build example (iOS)).
Emulators always report software / none, so the genuine Keystore / StrongBox /
attestation paths can only be exercised on real hardware. The
device-tests.yml workflow runs the
integration suite on Firebase Test Lab, but it is opt-in (manual dispatch or a
device-tests PR label) — see doc/FIREBASE_TEST_LAB.md
for setup and doc/DEVICE_TESTING.md for the manual
acceptance checklist.
The client-reported securityLevel is a hint — trust is always established
server-side by verifying the attestation against the genuine manufacturer
roots. This library is not a certified eIDAS WSCD and makes no Level-of-Assurance
claim. See SECURITY.md for the assurance model, scope notes, and
how to report a vulnerability (use GitHub's private vulnerability reporting — do not
open a public issue).
packages/attested_secure_keys/README.md— usage, API, assurance modelpackages/attested_secure_keys_verifier/README.md— server-side verificationdoc/DESIGN.md— design rationaleCONTEXT.md— orientation / pick-it-up-cold doc
Apache-2.0.