Compare commits

..
19 Commits
Author SHA1 Message Date
Enginex0 18724cf40d docs(changelog): add AUTO mode banking app fix to v6.0.0 notes 2026-03-31 18:59:28 +01:00
Enginex0 1b7800345d fix(config): restore AUTO mode resolution for bare target entries
v6.0 changed bare target.txt entries from AUTO to GENERATE, breaking
apps like BHIM that need TEE-backed attestation keys. Restore AUTO as
default and resolve it at config level (PATCH if TEE works, GENERATE
if not) to bypass the non-deterministic raceTeePatch path.
2026-03-31 18:53:18 +01:00
Enginex0 54c12a9fd5 docs(readme): rewrite for TEESimulator-RS v6.0.0
Fix all links from old TEESimulator repo, strip emoji clutter,
add v6.0.0 changelog, update update.json to point at new repo.
2026-03-26 12:34:36 +01:00
Enginex0 1bc47840d5 chore(version): bump to v6.0.0 2026-03-26 12:28:23 +01:00
Enginex0 c8fadb07ae fix(certgen): self-signed certs for no-challenge keys per AOSP spec
AOSP ta/src/keys.rs:451-478 requires self-signed leaf (depth 1) when
no attestation challenge is provided. Both Kotlin and Rust paths now
return subject==issuer, signed by generated key, no attestation
extension. Adds cert chain trace logging in debug builds.
2026-03-26 12:28:17 +01:00
Enginex0 c0b14eeeb1 fix(operation): pass operation-time params through to CipherPrimitive
createOperation was building effectiveParams from key-generation params
but dropping operation-time fields (nonce, blockMode, padding,
minMacLength). This caused GCM decrypt to fail with
"IV must be specified in GCM mode" since the nonce from the begin call
never reached CipherPrimitive.

Also adds nonce field to KeyMintAttestation and handles GCM/CBC/CTR IV
initialization in CipherPrimitive.
2026-03-26 05:03:01 +01:00
Enginex0 47ab0225e1 fix(certgen): omit attestation extension when no challenge provided
AOSP KeyMint only includes the attestation extension (OID
1.3.6.1.4.1.11129.2.1.17) when ATTESTATION_CHALLENGE is present.
Without a challenge, generateKey produces a plain self-signed cert.
Our code unconditionally added the extension, which behavioral
probes detect by generating a key without a challenge and checking
for the OID.

Fixes both the Rust native-certgen and BouncyCastle paths.
Also skips AAID computation when no challenge is provided,
matching keystore2 security_level.rs:457 behavior.
2026-03-26 04:22:22 +01:00
Enginex0 ebb6336281 fix(interception): patch authorizations on import-overwrite path
The retained cert chain was applied to response metadata but the
authorizations array was left unpatched, allowing a detector to compare
metadata patch levels against cert attestation values and spot the
divergence. Refs upstream JingMatrix #164.
2026-03-26 02:49:01 +01:00
Enginex0 784373c8b5 feat(config): default bare target entries to GENERATE mode
PATCH and AUTO modes inherit the real TEE's attestation quirks (epoch 0
cert dates, version mismatch, missing USAGE_COUNT_LIMIT) which can't be
fixed in post-patch. GENERATE mode builds attestation from scratch with
full control over every field. Users who want real TEE key generation
can still use the ? suffix for explicit PATCH mode.
2026-03-26 02:32:14 +01:00
Enginex0 2241bfb13d perf(logging): add rate limiter and lazy formatting to SystemLogger
Under binder stress, debug builds hammered logd with 6-7 syscalls per
keygen, causing thread contention that spiked ping latency past G10b's
threshold. Rate-limit debug/info/verbose to 15 msgs per 1s window with
atomic CAS on window boundaries. Warnings and errors always pass.

Expensive verbose calls in AttestationBuilder, AttestationPatcher, and
DeviceAttestationService now use lazy lambdas so ASN.1 formatting only
runs when the message will actually be emitted.
2026-03-26 02:11:17 +01:00
Enginex0 954478b89b perf(interception): optimize ioctl hook hot path for ping latency
Strip LOGV from the buffer parse loop and add a fast pre-check that
peeks at the first binder command before entering the full parser.
Pings, ref ops, and looper management produce no BR_TRANSACTION, so
their buffers can be skipped entirely. Adds __builtin_expect hint
on the transaction branch for better pipeline prediction.

Drops G2 binder ping ratio from 3.95x to 1.17x in debug builds.
2026-03-26 01:59:32 +01:00
Enginex0 191085087b fix(interception): route oversized transactions to software gen
The 256KB native size guard skipped interception entirely for oversized
transactions, causing them to reach the real TEE which returns different
attestation values. This inconsistency is exactly what G10 detects.

Oversized requests now flow through to the Kotlin layer where they hit
doSoftwareKeyGen via the forceGenerate flag. Software gen produces
consistent attestation without forwarding to the real TEE, preserving
the anti-amplification defense that the original guard intended.
2026-03-26 01:30:07 +01:00
Enginex0 f870598e77 fix(interception): resolve B3, C2, F1 and harden AUTO mode
B3: AttestationPatcher now accepts optional notBefore/notAfter overrides
so the PATCH path honors CERTIFICATE_NOT_BEFORE instead of inheriting
the real TEE's epoch 0.

C2: getKeymasterVersion delegates to getAttestVersion directly, ensuring
attestationVersion == keymasterVersion regardless of cache source.

F1: Remove incorrect EC+DECRYPT guard in AuthorizeCreate that returned
UNSUPPORTED_PURPOSE instead of INCOMPATIBLE_PURPOSE.

AUTO mode: Replace volatile teeFunctional boolean with AtomicReference
tri-state (null/true/false) so the first race winner locks the path for
all subsequent requests, preventing mixed attestation under concurrency.
2026-03-26 01:27:43 +01:00
Enginex0 8fdc59a142 feat(interception): add AUTO mode TEE race for G10 attestation consistency
AUTO mode now races TEE hardware against software generation via
CompletableFuture. If TEE succeeds, the cert chain is patched and
cached in teeResponses before returning, making attestation
stress-resilient. If TEE fails, software fallback is used.

ConfigurationManager no longer resolves AUTO at config time; it
passes Mode.AUTO through to KeyMintSecurityLevelInterceptor for
runtime dispatch. shouldPatch() returns true for both PATCH and
AUTO modes. TEE status file persistence removed entirely.

Aligns handleGenerateKey with upstream PR #157 three-way dispatch:
forceGenerate, raceTeePatch, or hardware forwarding with post-patch.

Hardware keygen rate limiting removed (replaced by raceTeePatch for
AUTO, plain Continue for PATCH). Attest key override in
Keystore2Interceptor now patches authorizations and uses null-safe
nspace assignment.
2026-03-20 04:44:52 +01:00
Enginex0 b6f9d7b486 fix(interception): harden daemon against binder stress crashes
BinderInterceptor.onTransact now catches Throwable, preventing any
exception on a binder thread from killing the daemon. Adds a global
uncaught exception handler as defense in depth.

Replace Thread.sleep with TeeLatencySimulator (LockSupport.parkNanos +
statistical delay model) for keygen latency, reducing binder thread
blocking. Move GeneratedKeyPersistence.save to a background executor
to avoid disk I/O on binder threads.

Convert force-unwrap parcel reads to safe calls with early returns in
onPreTransact/onPostTransact hot paths. Add -DNDEBUG to native release
builds to compile out verbose logging from the ioctl hook.

Targets G2 (ping overhead) and G10 (stress attestation consistency).
2026-03-19 17:38:29 +01:00
Enginex0 99e6b0b5ea feat(certgen): add enforcement tags to native DER encoder and teeResponses cache
Extend Rust native cert gen with software-enforced attestation tags
(CALLER_NONCE, ACTIVE_DATETIME, ORIGINATION_EXPIRE_DATETIME,
USAGE_EXPIRE_DATETIME, USAGE_COUNT_LIMIT, UNLOCKED_DEVICE_REQUIRED)
and make NO_AUTH_REQUIRED conditional in teeEnforced. Fixes F5/F6
test failures where these tags were missing from NativeCertGen path.

Add teeResponses cache so PATCH mode keys patched in onPostTransact
return consistent attestation via getKeyEntry. Without this, getKeyEntry
fell through to real keystore2, returning unpatched metadata.

Remove dead Rust enums (KeyPurpose, SecurityLevel, VerifiedBootState)
that were never referenced by the DER encoder.
2026-03-19 15:43:17 +01:00
Enginex0 397bb6338b fix(interception): make NO_AUTH_REQUIRED conditional in KeyMetadata authorizations
Upstream removed the unconditional NO_AUTH_REQUIRED from toAuthorizations.
A key generated with auth requirements would incorrectly report
NO_AUTH_REQUIRED in metadata, creating a detectable inconsistency
with the attestation extension.
2026-03-19 09:48:08 +01:00
Enginex0 cb81116701 feat(interception): close remaining PR #157 compliance gaps
Full diff analysis against upstream's 50 commits revealed 8 functional
gaps after v5.0. These are detectable by conformance tests or detector
apps inspecting KeyMetadata authorizations and operation semantics.

KeyMetadata authorizations:
- Add 9 TEE-enforced tags (CALLER_NONCE, MIN_MAC_LENGTH, ROLLBACK_RESISTANCE,
  EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED,
  TRUSTED_CONFIRMATION_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL)
- Fix CREATION_DATETIME to SOFTWARE security level via createSwAuth
- Add SOFTWARE-enforced date enforcement, USAGE_COUNT_LIMIT, UNLOCKED_DEVICE_REQUIRED

Symmetric key support:
- Generate AES/HMAC keys in software via javax.crypto.KeyGenerator
- GeneratedKeyInfo expanded with nullable keyPair + secretKey fields
- CipherPrimitive accepts java.security.Key for symmetric operations
- SoftwareOperation routes ENCRYPT/DECRYPT to secretKey when available

Operation compliance:
- beginParameters property replaces manual IV wrapping for GCM
- KeyAgreementPrimitive for ECDH AGREE_KEY operations
- handleCreateOperation wrapped in runCatching (crash prevention)
- SECURE_HW_COMMUNICATION_FAILED on software gen failure

Certificate patching:
- Import key cert chain + authorization patching in onPostTransact
- patchAuthorizations added to post-generateKey PATCH mode path
2026-03-19 09:40:48 +01:00
Enginex0 45477a7898 feat(interception): add AOSP authorize_create enforcement and wire format fixes
Integrate upstream AOSP compliance checks that failed post-v5.0 testing:

- INCLUDE_UNIQUE_ID: SELinux gen_unique_id + Android permission gate
- Forced operation rejection with PERMISSION_DENIED
- Null purpose guard returning INVALID_ARGUMENT
- Wire format: use createServiceSpecificErrorReply for authorize_create
- USAGE_COUNT_LIMIT with AtomicInteger counters and onFinishCallback
- effectiveParams merging key digest with operation purpose
- AuthorizeCreate rewrite: algorithm-purpose before purpose-list (AOSP HAL order)
- CALLER_NONCE in attestation teeEnforced list

Based on upstream commits e55d16d, 3078ea9, 2bc46be, 07c98bc, 41abe77.
2026-03-19 09:21:52 +01:00
27 changed files with 1060 additions and 565 deletions
+74 -194
View File
@@ -1,259 +1,139 @@
<p align="center"> <p align="center">
<h1 align="center">🔐 TEESimulator</h1> <h1 align="center">TEESimulator-RS</h1>
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p> <p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
<p align="center"> <p align="center">
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a> <a href="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator-RS/actions/workflows/build.yml/badge.svg" alt="Build"></a>
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2"> <img src="https://img.shields.io/badge/Android-10%2B-green?logo=android" alt="Android 10+">
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+"> <a href="https://t.me/superpowers9"><img src="https://img.shields.io/badge/Telegram-community-blue?logo=telegram" alt="Telegram"></a>
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</p> </p>
</p> </p>
--- ---
> [!NOTE] > [!NOTE]
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo. > Fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator) with native Rust certificate generation, key persistence, and AOSP-compliant attestation behavior. For the upstream project, see the original repo.
--- ## What It Does
## 🧬 What is TEESimulator? TEESimulator intercepts Binder IPC at the `ioctl` level inside the `keystore2` process and generates entire certificate chains from scratch, signed by your keybox, with correct attestation extensions. Apps that verify hardware attestation see a legitimate device.
TEESimulator is a **complete software simulation** of Android's hardware-backed [Trusted Execution Environment](https://source.android.com/docs/security/features/trusty) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation). Instead of patching certificates from the real TEE after the fact, TEESimulator intercepts Binder IPC at the `ioctl` level and generates entire certificate chains from scratch — signed by your keybox, with correct attestation extensions, indistinguishable from hardware-generated keys. This is not TrickyStore. TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the internals are different: native Rust cert generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and AOSP-spec attestation behavior.
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader. ## Requirements
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
---
## 🔥 Why TEESimulator?
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
---
## ✨ Features
**Core Attestation Engine**
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
- [x] **Native Rust certgen**`libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
**Interception Layer**
- [x] **Binder ioctl hook**`lsplt` PLT hook on `libc.so` inside `keystore2` process
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
**Hardening**
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
**Configuration**
- [x] **Live config reload**`FileObserver` watches all config files, changes apply immediately
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
---
## 📋 Requirements
> [!IMPORTANT] > [!IMPORTANT]
> TEESimulator requires root access and a valid `keybox.xml` for hardware-level attestation results. Without a keybox, the module generates software-level certificates that won't pass strict hardware attestation checks. > A valid `keybox.xml` is required for hardware-level attestation. Without one, the module generates software-level certificates that won't pass strict hardware checks.
**You need:** 1. Android 10+
1. Android 10 or above 2. Root manager: KernelSU, Magisk, or APatch
2. A supported root manager (KernelSU, Magisk, or APatch) 3. `keybox.xml` at `/data/adb/tricky_store/keybox.xml`
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
--- ## Quick Start
## 📱 Compatibility 1. Download the latest ZIP from [Releases](https://github.com/Enginex0/TEESimulator-RS/releases)
2. Install via your root manager and reboot
3. Place your keybox at `/data/adb/tricky_store/keybox.xml`
4. Configure targets in `/data/adb/tricky_store/target.txt`
5. Verify with Play Integrity or Key Attestation Demo
### Root Managers ## Architecture
| Manager | Status | Notes | **Native Cert Generation**`libcertgen.so` generates X.509 chains in Rust using `ring` and manual DER encoding. BouncyCastle fallback for unsupported curves (P-224, P-521, Curve25519).
|---|---|---|
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
| Magisk | ✅ Supported | Standard module install |
| APatch | ✅ Supported | Standard module install |
### Tested Devices **Binder Interception** — PLT hook on `ioctl()` in `libc.so` via `lsplt` inside `keystore2`. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions.
| Device | Android | TEE | Status | **AOSP Compliance** — Self-signed certs for non-attested keys (matching `ta/src/keys.rs`), correct AuthorizationList tag ordering, version-guarded extension fields, `authorize_create` enforcement.
|---|---|---|---|
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results. **Key Persistence** — Generated keys survive reboots. File-backed with file-level locking.
--- **Rate Limiting** — Per-UID hardware keygen cap (2/30s window, 2 concurrent). Overflow falls to software certs.
## 🚀 Quick Start ## Configuration
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases) All config files live at `/data/adb/tricky_store/` and are hot-reloaded via `FileObserver`.
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
5. **Verify** — check Play Integrity or run Key Attestation Demo
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible. ### target.txt
--- Controls which apps get intercepted and the simulation mode.
## 🔨 Building from Source | Suffix | Mode |
|--------|------|
| `!` | Force software key generation |
| `?` | Force leaf certificate patching (real TEE key, patched cert) |
| *(none)* | Automatic selection |
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork. Multi-keybox support via `[filename.xml]` headers:
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone https://github.com/Enginex0/TEESimulator.git
cd TEESimulator
./gradlew zipRelease zipDebug
```
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
---
## ⚙️ Configuration
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
### The `keybox.xml` Root of Trust
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
```xml
<?xml version="1.0"?>
<AndroidAttestation>
<Keybox DeviceID="...">
<Key algorithm="ecdsa|rsa">
<PrivateKey format="pem">...</PrivateKey>
<CertificateChain>...</CertificateChain>
</Key>
</Keybox>
</AndroidAttestation>
```
### Target Packages (`target.txt`)
Controls which apps get intercepted and what simulation mode to use.
#### Mode Suffixes
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
* **No symbol → Automatic** — Module selects the best mode for your device.
#### Multi-Keybox
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
``` ```
# Default keybox
com.google.android.gms! com.google.android.gms!
io.github.vvb2060.keyattestation? io.github.vvb2060.keyattestation?
# Switch to a different keybox for the following apps
[aosp_keybox.xml] [aosp_keybox.xml]
com.google.android.gsf com.google.android.gsf
# Another keybox
[demo_keybox.xml]
org.matrix.demo
``` ```
### Security Patch Level (`security_patch.txt`) ### security_patch.txt
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties. Override patch levels reported in attestation certificates. Global defaults at top, per-package overrides with `[package.name]`.
#### Global and Per-Package
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
#### Keys
| Key | Scope | | Key | Scope |
|---|---| |-----|-------|
| `system` | OS patch level | | `system` | OS patch level |
| `vendor` | Vendor patch level | | `vendor` | Vendor patch level |
| `boot` | Boot/kernel patch level | | `boot` | Boot/kernel patch level |
| `all` | Shorthand — sets all three at once | | `all` | Sets all three |
#### Special Keywords Special values: `today`, `YYYY-MM-DD` templates, `no` (omit tag), `device_default`, `prop` (read from system property).
| Keyword | Effect |
|---|---|
| `today` | Current date, dynamically resolved on each attestation |
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
| `no` | Omit this patch level tag entirely from the attestation |
| `device_default` | Use the device's real hardware value |
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
#### Example
``` ```
# Global — default for all apps
system=YYYY-MM-05 system=YYYY-MM-05
vendor=device_default vendor=device_default
boot=no boot=no
# Override for GMS
[com.google.android.gms] [com.google.android.gms]
system=2024-10-01 system=2025-10-01
# Custom config for a demo app
[org.matrix.demo]
all=2025-09-15
boot=device_default
``` ```
--- ## Building from Source
## 💬 Community Prerequisites: JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
```bash
git clone --recursive https://github.com/Enginex0/TEESimulator-RS.git
cd TEESimulator-RS
./gradlew zipRelease zipDebug
```
Output ZIPs in `out/`. Gradle invokes `cargo ndk` automatically to cross-compile `libcertgen.so`.
Push to `main` or use **Actions > Build > Run workflow** to trigger CI.
## Compatibility
| Root Manager | Status |
|---|---|
| KernelSU | Tested (Action button + lifecycle scripts) |
| Magisk | Supported |
| APatch | Supported |
## Community
<p align="center"> <p align="center">
<a href="https://t.me/superpowers9"> <a href="https://t.me/superpowers9">
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram"> <img src="https://img.shields.io/badge/SuperPowers_Telegram-Join-blue?style=for-the-badge&logo=telegram" alt="Telegram">
</a> </a>
</p> </p>
--- ## Credits
## 🙏 Credits - [JingMatrix](https://github.com/JingMatrix/TEESimulator) — original TEESimulator and interception architecture
- [5ec1cff](https://github.com/5ec1cff/TrickyStore) — TrickyStore, the project that pioneered keystore interception
- [LSPlt](https://github.com/LSPosed/LSPlt) — PLT hook library
- [ring](https://github.com/briansmith/ring) — Rust cryptography library
- [MhmRdd](https://github.com/MhmRdd) — AOSP compliance work via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157)
- [fatalcoder524](https://github.com/fatalcoder524) — contributor and collaborator
- [huguangares](https://github.com/huguangares) — collaborator and tester
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture ## License
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
- **[MhmRdd](https://github.com/MhmRdd)** — AOSP compliance improvements via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157), including authorize_create enforcement, attestation extension alignment, and binder transaction filtering
- **[fatalcoder524](https://github.com/fatalcoder524)** — a real contributor and collaborator on this project
- **[huguangares](https://github.com/huguangares)** — collaborator and tester
--- [GNU General Public License v3.0](LICENSE)
## 📄 License
This project is licensed under the [GNU General Public License v3.0](LICENSE).
---
<p align="center">
<b>🔐 Because the best attestation is the one the TEE never generated.</b>
</p>
+1 -1
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt() val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir) val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v5.0" val verName = "v6.0.0"
android { android {
namespace = "org.matrix.TEESimulator" namespace = "org.matrix.TEESimulator"
+1
View File
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
# LSPlt configuration # LSPlt configuration
OPTION(LSPLT_BUILD_SHARED OFF) OPTION(LSPLT_BUILD_SHARED OFF)
+13 -30
View File
@@ -350,16 +350,10 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace { namespace {
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0) if (!txn_data || txn_data->target.ptr == 0)
return; return;
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff). // AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range. // System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
// Skip those — intercepting a ping adds measurable latency that timing detectors flag. // Skip those — intercepting a ping adds measurable latency that timing detectors flag.
@@ -434,44 +428,29 @@ void processBinderReadBuffer(const binder_write_read &bwr) {
uintptr_t ptr = bwr.read_buffer; uintptr_t ptr = bwr.read_buffer;
uintptr_t end = ptr + bwr.read_consumed; uintptr_t end = ptr + bwr.read_consumed;
LOGV("[Hook] Processing Read Buffer: Size=%llu, Consumed=%llu", bwr.read_size, bwr.read_consumed);
while (ptr < end) { while (ptr < end) {
// Ensure we can read at least the command header
if (end - ptr < sizeof(uint32_t)) if (end - ptr < sizeof(uint32_t))
break; break;
uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr); uint32_t cmd = *reinterpret_cast<const uint32_t *>(ptr);
ptr += sizeof(uint32_t); ptr += sizeof(uint32_t);
// Calculate payload size from the ioctl command code
size_t cmd_size = _IOC_SIZE(cmd); size_t cmd_size = _IOC_SIZE(cmd);
// Log the command using our generated to-string function
LOGV("[Driver -> User] Command: %s (0x%x), DataSize: %zu", getBinderReturnCommandName(cmd), cmd, cmd_size);
// Safety check: ensure the command's data does not exceed the buffer
if (ptr + cmd_size > end) { if (ptr + cmd_size > end) {
LOGE("[Hook] Buffer overflow detected while parsing command %s", getBinderReturnCommandName(cmd)); LOGE("[Hook] Buffer overrun parsing command 0x%x", cmd);
break; break;
} }
// We are primarily interested in BR_TRANSACTION commands to intercept if (__builtin_expect(cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX, 0)) {
if (cmd == BR_TRANSACTION || cmd == BR_TRANSACTION_SEC_CTX) { binder_transaction_data *txn;
binder_transaction_data *txn = nullptr;
if (cmd == BR_TRANSACTION_SEC_CTX) { if (cmd == BR_TRANSACTION_SEC_CTX) {
// The data is wrapped in a secctx struct txn = &reinterpret_cast<binder_transaction_data_secctx *>(ptr)->transaction_data;
auto *wrapper = reinterpret_cast<binder_transaction_data_secctx *>(ptr);
txn = &wrapper->transaction_data;
} else { } else {
txn = reinterpret_cast<binder_transaction_data *>(ptr); txn = reinterpret_cast<binder_transaction_data *>(ptr);
} }
inspectAndRewriteTransaction(txn); inspectAndRewriteTransaction(txn);
} }
// Advance pointer to the next command
ptr += cmd_size; ptr += cmd_size;
} }
} }
@@ -491,13 +470,17 @@ int intercepted_ioctl(int fd, int request, ...) {
// 1. Call original kernel ioctl to let the driver do its work // 1. Call original kernel ioctl to let the driver do its work
int result = g_original_ioctl(fd, request, arg); int result = g_original_ioctl(fd, request, arg);
// 2. After the call returns, check if it was a BINDER_WRITE_READ and if it succeeded
if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) { if (result >= 0 && request == BINDER_WRITE_READ && arg != nullptr) {
const auto *bwr = static_cast<const binder_write_read *>(arg); const auto *bwr = static_cast<const binder_write_read *>(arg);
// Fast reject: only enter the parser if the buffer could contain a BR_TRANSACTION.
// We only care about data read FROM the driver (i.e., incoming commands) // Pings, ref ops, and looper management never produce BR_TRANSACTION, so scanning
if (bwr->read_consumed > 0) { // their buffers is pure overhead (~2-5us per ioctl in debug builds).
processBinderReadBuffer(*bwr); if (bwr->read_consumed >= sizeof(uint32_t)) {
uint32_t first_cmd = *reinterpret_cast<const uint32_t *>(bwr->read_buffer);
if (first_cmd == BR_TRANSACTION || first_cmd == BR_TRANSACTION_SEC_CTX
|| bwr->read_consumed > sizeof(uint32_t) + _IOC_SIZE(first_cmd)) {
processBinderReadBuffer(*bwr);
}
} }
} }
@@ -33,8 +33,11 @@ object App {
fun main(args: Array<String>) { fun main(args: Array<String>) {
SystemLogger.info("Welcome to TEESimulator!") SystemLogger.info("Welcome to TEESimulator!")
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
}
try { try {
// Initialize the Android framework environment
prepareEnvironment() prepareEnvironment()
// Initialize and start the appropriate keystore interceptors. // Initialize and start the appropriate keystore interceptors.
initializeInterceptors() initializeInterceptors()
@@ -43,11 +43,12 @@ object AttestationBuilder {
securityLevel: Int, securityLevel: Int,
): Extension { ): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel) val keyDescription = buildKeyDescription(params, uid, securityLevel)
var formattedString = SystemLogger.verbose {
keyDescription.joinToString(separator = ", ") { val formattedString = keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it) AttestationPatcher.formatAsn1Primitive(it)
} }
SystemLogger.verbose("Forged attestation data: ${formattedString}") "Forged attestation data: $formattedString"
}
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded)) return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
} }
@@ -447,6 +448,11 @@ object AttestationBuilder {
) )
} }
if (params.callerNonce == true) {
list.add(
DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)
)
}
params.activeDateTime?.let { params.activeDateTime?.let {
list.add( list.add(
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time)) DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
@@ -16,6 +16,7 @@ import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.KeyBox import org.matrix.TEESimulator.pki.KeyBox
import org.matrix.TEESimulator.pki.KeyBoxManager import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.util.toHex import org.matrix.TEESimulator.util.toHex
import java.util.Date
/** /**
* Handles the modification (patching) of Android Key Attestation extensions within certificates. * Handles the modification (patching) of Android Key Attestation extensions within certificates.
@@ -36,7 +37,12 @@ object AttestationPatcher {
* @return A new, cryptographically valid, patched certificate chain. Returns the original chain * @return A new, cryptographically valid, patched certificate chain. Returns the original chain
* on any failure. * on any failure.
*/ */
fun patchCertificateChain(originalChain: Array<Certificate>?, uid: Int): Array<Certificate> { fun patchCertificateChain(
originalChain: Array<Certificate>?,
uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
): Array<Certificate> {
if (originalChain.isNullOrEmpty()) { if (originalChain.isNullOrEmpty()) {
SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.") SystemLogger.error("Attempted to patch a null or empty certificate chain for UID $uid.")
return originalChain ?: emptyArray() return originalChain ?: emptyArray()
@@ -63,6 +69,8 @@ object AttestationPatcher {
keybox, keybox,
originalLeaf.sigAlgName, originalLeaf.sigAlgName,
uid, uid,
notBefore,
notAfter,
) )
// 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's // 4. Construct the NEW, VALID chain by prepending the patched leaf to the keybox's
@@ -111,17 +119,27 @@ object AttestationPatcher {
keybox: KeyBox, keybox: KeyBox,
sigAlgName: String, sigAlgName: String,
uid: Int, uid: Int,
notBefore: Date? = null,
notAfter: Date? = null,
): Certificate { ): Certificate {
// The issuer of our new leaf is the subject of the first certificate in our custom keybox // The issuer of our new leaf is the subject of the first certificate in our custom keybox
// chain. // chain.
val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject val newIssuer = X509CertificateHolder(keybox.certificates[0].encoded).subject
val effectiveNotBefore = notBefore ?: originalLeafHolder.notBefore
val effectiveNotAfter = notAfter ?: originalLeafHolder.notAfter
if (notBefore != null || notAfter != null) {
SystemLogger.debug(
"Overriding cert dates: notBefore=${effectiveNotBefore} (was ${originalLeafHolder.notBefore}), notAfter=${effectiveNotAfter} (was ${originalLeafHolder.notAfter})"
)
}
val builder = val builder =
X509v3CertificateBuilder( X509v3CertificateBuilder(
newIssuer, newIssuer,
originalLeafHolder.serialNumber, originalLeafHolder.serialNumber,
originalLeafHolder.notBefore, effectiveNotBefore,
originalLeafHolder.notAfter, effectiveNotAfter,
originalLeafHolder.subject, originalLeafHolder.subject,
originalLeafHolder.subjectPublicKeyInfo, originalLeafHolder.subjectPublicKeyInfo,
) )
@@ -146,7 +164,7 @@ object AttestationPatcher {
// Log the signature of the newly created certificate to observe its non-deterministic // Log the signature of the newly created certificate to observe its non-deterministic
// nature. // nature.
val signatureBytes = (newCertificate as X509Certificate).signature val signatureBytes = (newCertificate as X509Certificate).signature
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}") SystemLogger.verbose { "Signature of patched leaf cert: ${signatureBytes.toHex()}" }
return newCertificate return newCertificate
} }
@@ -268,8 +286,10 @@ object AttestationPatcher {
private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension { private fun createPatchedAttestationExtension(parsed: ParsedAttestation, uid: Int): Extension {
val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed
var formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) } SystemLogger.verbose {
SystemLogger.verbose("Original attestation data: ${formattedString}") val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Original attestation data: $formattedString"
}
// Build the new Root of Trust and add/replace it in the map. // Build the new Root of Trust and add/replace it in the map.
val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust) val newRootOfTrust = AttestationBuilder.buildRootOfTrust(originalRootOfTrust)
@@ -296,8 +316,10 @@ object AttestationPatcher {
allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced
val patchedSequence = DERSequence(allFields) val patchedSequence = DERSequence(allFields)
formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) } SystemLogger.verbose {
SystemLogger.verbose("Patched attestation data: ${formattedString}") val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) }
"Patched attestation data: $formattedString"
}
val patchedOctets = DEROctetString(patchedSequence) val patchedOctets = DEROctetString(patchedSequence)
return Extension(ATTESTATION_OID, false, patchedOctets) return Extension(ATTESTATION_OID, false, patchedOctets)
@@ -148,11 +148,12 @@ object DeviceAttestationService {
// The extension's value is an ASN.1 sequence. // The extension's value is an ASN.1 sequence.
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets) val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
var formattedString = SystemLogger.verbose {
keyDescriptionSeq.joinToString(separator = ", ") { val formattedString = keyDescriptionSeq.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it) AttestationPatcher.formatAsn1Primitive(it)
} }
SystemLogger.verbose("Cached attestation data: ${formattedString}") "Cached attestation data: $formattedString"
}
val fields = keyDescriptionSeq.toArray() val fields = keyDescriptionSeq.toArray()
val attestVersion = val attestVersion =
@@ -46,6 +46,7 @@ data class KeyMintAttestation(
val usageExpireDateTime: Date?, val usageExpireDateTime: Date?,
val usageCountLimit: Int?, val usageCountLimit: Int?,
val callerNonce: Boolean?, val callerNonce: Boolean?,
val nonce: ByteArray?,
val unlockedDeviceRequired: Boolean?, val unlockedDeviceRequired: Boolean?,
val includeUniqueId: Boolean?, val includeUniqueId: Boolean?,
val rollbackResistance: Boolean?, val rollbackResistance: Boolean?,
@@ -121,6 +122,7 @@ data class KeyMintAttestation(
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME), usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT), usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
callerNonce = params.findBoolean(Tag.CALLER_NONCE), callerNonce = params.findBoolean(Tag.CALLER_NONCE),
nonce = params.findBlob(Tag.NONCE),
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED), unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID), includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE), rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
@@ -31,7 +31,6 @@ object ConfigurationManager {
// --- Configuration Paths --- // --- Configuration Paths ---
const val CONFIG_PATH = "/data/adb/tricky_store" const val CONFIG_PATH = "/data/adb/tricky_store"
private const val TARGET_PACKAGES_FILE = "target.txt" private const val TARGET_PACKAGES_FILE = "target.txt"
private const val TEE_STATUS_FILE = "tee_status.txt"
private const val PATCH_LEVEL_FILE = "security_patch.txt" private const val PATCH_LEVEL_FILE = "security_patch.txt"
private const val DEFAULT_KEYBOX_FILE = "keybox.xml" private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
private val configRoot = File(CONFIG_PATH) private val configRoot = File(CONFIG_PATH)
@@ -39,7 +38,6 @@ object ConfigurationManager {
// --- In-Memory Configuration State --- // --- In-Memory Configuration State ---
@Volatile private var packageModes = mapOf<String, Mode>() @Volatile private var packageModes = mapOf<String, Mode>()
@Volatile private var packageKeyboxes = mapOf<String, String>() @Volatile private var packageKeyboxes = mapOf<String, String>()
@Volatile private var isTeeBroken: Boolean? = null
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null @Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>() @Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
@@ -68,8 +66,6 @@ object ConfigurationManager {
// Initial load of all configuration files. // Initial load of all configuration files.
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE)) loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE)) loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
storeTeeStatus() // Check and store the current TEE status.
// Start watching for any subsequent file changes. // Start watching for any subsequent file changes.
ConfigObserver.startWatching() ConfigObserver.startWatching()
SystemLogger.info("Configuration initialized and file observer started.") SystemLogger.info("Configuration initialized and file observer started.")
@@ -87,33 +83,31 @@ object ConfigurationManager {
return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE
} }
/** Determines if the certificate for a given UID needs to be patched. */ fun shouldPatch(uid: Int): Boolean {
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH val mode = getPackageModeForUid(uid)
return mode == Mode.PATCH || mode == Mode.AUTO
}
/** Determines if a new certificate needs to be generated for a given UID. */ /** Determines if a new certificate needs to be generated for a given UID. */
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
/** Determines if no operation is needed for a given UID. */
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */ fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
private fun getPackageModeForUid(uid: Int): Mode? { private fun getPackageModeForUid(uid: Int): Mode? {
val packages = getPackagesForUid(uid) val packages = getPackagesForUid(uid)
if (packages.isEmpty()) return null if (packages.isEmpty()) return null
// Lazily load TEE status if it hasn't been checked yet.
if (isTeeBroken == null) loadTeeStatus()
// Find the first configured mode for any of the UID's packages.
for (pkg in packages) { for (pkg in packages) {
when (packageModes[pkg]) { when (packageModes[pkg]) {
Mode.GENERATE -> return Mode.GENERATE Mode.GENERATE -> return Mode.GENERATE
Mode.PATCH -> return Mode.PATCH Mode.PATCH -> return Mode.PATCH
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE
null -> continue // No config for this package, check the next one. null -> continue
} }
} }
return null // No configuration found for this UID. return null
} }
/** /**
@@ -171,7 +165,6 @@ object ConfigurationManager {
newModes[pkg] = Mode.PATCH newModes[pkg] = Mode.PATCH
newKeyboxes[pkg] = currentKeybox newKeyboxes[pkg] = currentKeybox
} }
// No suffix means AUTO mode.
else -> { else -> {
newModes[trimmedLine] = Mode.AUTO newModes[trimmedLine] = Mode.AUTO
newKeyboxes[trimmedLine] = currentKeybox newKeyboxes[trimmedLine] = currentKeybox
@@ -280,29 +273,6 @@ object ConfigurationManager {
} }
} }
/** Checks the device's TEE status and writes the result to a file for persistence. */
private fun storeTeeStatus() {
val statusFile = File(configRoot, TEE_STATUS_FILE)
isTeeBroken = !DeviceAttestationService.isTeeFunctional
try {
statusFile.writeText("tee_broken=$isTeeBroken")
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
} catch (e: Exception) {
SystemLogger.error("Failed to write TEE status to file.", e)
}
}
/** Loads the TEE status from the file. */
private fun loadTeeStatus() {
val statusFile = File(configRoot, TEE_STATUS_FILE)
isTeeBroken =
if (statusFile.exists()) {
statusFile.readText().trim() == "tee_broken=true"
} else {
null // Status is unknown.
}
}
/** /**
* A FileObserver that monitors the configuration directory for changes and triggers reloads of * A FileObserver that monitors the configuration directory for changes and triggers reloads of
* the relevant settings. * the relevant settings.
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
* `handlePostTransact`). * `handlePostTransact`).
*/ */
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
// The native hook prepends a transaction ID to the data parcel.
val txId = data.readLong() val txId = data.readLong()
val result = val result = try {
when (code) { when (code) {
// These codes are defined in the native layer to distinguish hook types.
PRE_TRANSACT_CODE -> handlePreTransact(txId, data) PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
POST_TRANSACT_CODE -> handlePostTransact(txId, data) POST_TRANSACT_CODE -> handlePostTransact(txId, data)
else -> return super.onTransact(code, data, reply, flags) else -> return super.onTransact(code, data, reply, flags)
} }
} catch (e: Throwable) {
// The reply parcel is guaranteed to be non-null for our custom transactions. SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
TransactionResult.ContinueAndSkipPost
}
writeResultToReply(result, reply!!) writeResultToReply(result, reply!!)
return true return true
} }
@@ -314,6 +314,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
if (userUpdatedKeys.remove(keyId)) { if (userUpdatedKeys.remove(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" }
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.") SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
@@ -324,18 +325,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray() authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
) )
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" }
if (parsedParameters.isImportKey()) { if (parsedParameters.isImportKey()) {
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) { if (retainedChain == null) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" }
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).") SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" }
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId") SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow() CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) { if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) {
SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" }
SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId") SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId")
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
@@ -358,13 +369,19 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
keyData.second.toTypedArray(), keyData.second.toTypedArray(),
) )
.getOrThrow() .getOrThrow()
response.metadata.authorizations =
InterceptorUtils.patchAuthorizations(
response.metadata.authorizations,
callingUid,
)
keyDescriptor.nspace = SecureRandom().nextLong() val newNspace = SecureRandom().nextLong()
response.metadata.key.nspace = keyDescriptor.nspace response.metadata.key?.let { it.nspace = newNspace }
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first, keyData.first,
keyDescriptor.nspace, null,
newNspace,
response, response,
parsedParameters, parsedParameters,
) )
@@ -373,7 +390,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
GeneratedKeyPersistence.save( GeneratedKeyPersistence.save(
keyId = keyId, keyId = keyId,
keyPair = keyData.first, keyPair = keyData.first,
nspace = keyDescriptor.nspace, nspace = newNspace,
securityLevel = response.metadata.keySecurityLevel, securityLevel = response.metadata.keySecurityLevel,
certChain = keyData.second, certChain = keyData.second,
algorithm = parsedParameters.algorithm, algorithm = parsedParameters.algorithm,
@@ -453,7 +470,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
} }
if (generatedKeyInfo == null) { if (generatedKeyInfo == null) {
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) } descriptor.alias?.let {
val kid = KeyIdentifier(callingUid, it)
userUpdatedKeys.add(kid)
SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" }
}
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
@@ -436,6 +436,7 @@ private data class LegacyKeygenParameters(
usageExpireDateTime = null, usageExpireDateTime = null,
usageCountLimit = null, usageCountLimit = null,
callerNonce = null, callerNonce = null,
nonce = null,
unlockedDeviceRequired = null, unlockedDeviceRequired = null,
includeUniqueId = null, includeUniqueId = null,
rollbackResistance = null, rollbackResistance = null,
@@ -14,40 +14,36 @@ object AuthorizeCreate {
rawOpParams: Array<KeyParameter>? = null, rawOpParams: Array<KeyParameter>? = null,
): Int? { ): Int? {
if (keyParams == null) return null if (keyParams == null) return null
return checkPurpose(keyParams, opParams) val purpose = opParams.purpose.firstOrNull() ?: return null
?: checkAlgorithmPurpose(keyParams, opParams) // Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior)
?: checkTemporalValidity(keyParams, opParams) return checkAlgorithmPurpose(keyParams, purpose)
?: checkCallerNonce(keyParams, rawOpParams) ?: checkPurpose(keyParams, purpose)
?: checkTemporalValidity(keyParams, purpose)
?: checkCallerNonce(keyParams, purpose, rawOpParams)
} }
private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
val requestedPurpose = opParams.purpose.firstOrNull() ?: return null val algo = keyParams.algorithm
if (requestedPurpose == KeyPurpose.WRAP_KEY) if ((algo == Algorithm.EC || algo == Algorithm.RSA) &&
(purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT)
) {
return KeystoreErrorCodes.unsupportedPurpose
}
if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY)
return KeystoreErrorCodes.unsupportedPurpose
return null
}
private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? {
if (purpose == KeyPurpose.WRAP_KEY)
return KeystoreErrorCodes.incompatiblePurpose return KeystoreErrorCodes.incompatiblePurpose
if (requestedPurpose !in keyParams.purpose) if (purpose !in keyParams.purpose)
return KeystoreErrorCodes.incompatiblePurpose return KeystoreErrorCodes.incompatiblePurpose
return null return null
} }
private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? {
val purpose = opParams.purpose.firstOrNull() ?: return null
return when (keyParams.algorithm) {
Algorithm.EC -> when (purpose) {
KeyPurpose.ENCRYPT, KeyPurpose.DECRYPT -> KeystoreErrorCodes.unsupportedPurpose
KeyPurpose.AGREE_KEY -> null
else -> null
}
Algorithm.RSA -> when (purpose) {
KeyPurpose.AGREE_KEY -> KeystoreErrorCodes.unsupportedPurpose
else -> null
}
else -> null
}
}
private fun checkTemporalValidity(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val purpose = opParams.purpose.firstOrNull()
keyParams.activeDateTime?.let { activeDate -> keyParams.activeDateTime?.let { activeDate ->
if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid
@@ -68,10 +64,11 @@ object AuthorizeCreate {
return null return null
} }
private fun checkCallerNonce(keyParams: KeyMintAttestation, rawOpParams: Array<KeyParameter>?): Int? { private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array<KeyParameter>?): Int? {
if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null
if (keyParams.callerNonce == true) return null if (keyParams.callerNonce == true) return null
val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true if (rawOpParams?.any { it.tag == Tag.NONCE } == true)
if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited return KeystoreErrorCodes.callerNonceProhibited
return null return null
} }
} }
@@ -303,9 +303,10 @@ object GeneratedKeyPersistence {
return return
} }
val keyPair = generatedKeyInfo.keyPair ?: return
save( save(
keyId = keyId, keyId = keyId,
keyPair = generatedKeyInfo.keyPair, keyPair = keyPair,
nspace = generatedKeyInfo.nspace, nspace = generatedKeyInfo.nspace,
securityLevel = secLevel, securityLevel = secLevel,
certChain = newChain.toList(), certChain = newChain.toList(),
@@ -20,9 +20,14 @@ import java.security.SecureRandom
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
import java.util.Date
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.locks.LockSupport
import org.matrix.TEESimulator.attestation.AttestationBuilder import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants import org.matrix.TEESimulator.attestation.AttestationConstants
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
@@ -39,6 +44,7 @@ import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.pki.NativeCertGen import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.AndroidPermissionUtils import org.matrix.TEESimulator.util.AndroidPermissionUtils
import org.matrix.TEESimulator.util.TeeLatencySimulator
class KeyMintSecurityLevelInterceptor( class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel, private val original: IKeystoreSecurityLevel,
@@ -46,12 +52,17 @@ class KeyMintSecurityLevelInterceptor(
) : BinderInterceptor() { ) : BinderInterceptor() {
data class GeneratedKeyInfo( data class GeneratedKeyInfo(
val keyPair: KeyPair, val keyPair: KeyPair?,
val secretKey: javax.crypto.SecretKey?,
val nspace: Long, val nspace: Long,
val response: KeyEntryResponse, val response: KeyEntryResponse,
val keyParams: KeyMintAttestation? = null, val keyParams: KeyMintAttestation? = null,
) )
// null = undecided, true = TEE works (use PATCH), false = TEE broken (use GENERATE)
// Instance field so TRUSTED_ENVIRONMENT and STRONGBOX decide independently
val teePathDecision = AtomicReference<Boolean?>(null)
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>() private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>() private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
@@ -70,7 +81,7 @@ class KeyMintSecurityLevelInterceptor(
GENERATE_KEY_TRANSACTION -> { GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data) if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data)
} }
CREATE_OPERATION_TRANSACTION -> { CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -81,7 +92,8 @@ class KeyMintSecurityLevelInterceptor(
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info( SystemLogger.info(
"[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]" "[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]"
) )
@@ -111,12 +123,6 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?, reply: Parcel?,
resultCode: Int, resultCode: Int,
): TransactionResult { ): TransactionResult {
if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) {
val remaining = hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}")
}
// We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
@@ -136,12 +142,37 @@ class KeyMintSecurityLevelInterceptor(
} }
attestationKeys.remove(keyId) attestationKeys.remove(keyId)
importedKeys.add(keyId) importedKeys.add(keyId)
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" }
if (!ConfigurationManager.shouldSkipUid(callingUid)) {
val metadata: KeyMetadata =
reply.readTypedObject(KeyMetadata.CREATOR)
?: return TransactionResult.SkipTransaction
val originalChain = CertificateHelper.getCertificateChain(metadata)
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: chainSize=${originalChain?.size ?: 0}" }
if (originalChain != null && originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
metadata.authorizations =
InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid)
patchedChains[keyId] = newChain
teeResponses[keyId] = KeyEntryResponse().apply {
this.metadata = metadata
iSecurityLevel = original
}
SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: PATCHED chain (chainSize=${newChain.size})" }
SystemLogger.debug("Cached patched certificate chain for imported key $keyId.")
return InterceptorUtils.createTypedObjectReply(metadata)
}
}
} else if (code == CREATE_OPERATION_TRANSACTION) { } else if (code == CREATE_OPERATION_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
val params = data.createTypedArray(KeyParameter.CREATOR)!! ?: return TransactionResult.SkipTransaction
val params = data.createTypedArray(KeyParameter.CREATOR)
?: return TransactionResult.SkipTransaction
val parsedParams = KeyMintAttestation(params) val parsedParams = KeyMintAttestation(params)
val forced = data.readBoolean() val forced = data.readBoolean()
if (forced) if (forced)
@@ -149,7 +180,8 @@ class KeyMintSecurityLevelInterceptor(
"[TX_ID: $txId] Current operation has a very high pruning power." "[TX_ID: $txId] Current operation has a very high pruning power."
) )
val response: CreateOperationResponse = val response: CreateOperationResponse =
reply.readTypedObject(CreateOperationResponse.CREATOR)!! reply.readTypedObject(CreateOperationResponse.CREATOR)
?: return TransactionResult.SkipTransaction
SystemLogger.verbose( SystemLogger.verbose(
"[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}" "[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}"
) )
@@ -181,18 +213,32 @@ class KeyMintSecurityLevelInterceptor(
CertificateHelper.getCertificateChain(metadata) CertificateHelper.getCertificateChain(metadata)
?: return TransactionResult.SkipTransaction ?: return TransactionResult.SkipTransaction
if (originalChain.size > 1) { if (originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) // Read the request parcel to extract keyDescriptor and cert date params.
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction
data.readTypedObject(KeyDescriptor.CREATOR) // skip attestationKey
val keyParams = data.createTypedArray(KeyParameter.CREATOR)
val certNotBefore = keyParams?.find { it.tag == Tag.CERTIFICATE_NOT_BEFORE }?.value?.dateTime?.let { Date(it) }
val certNotAfter = keyParams?.find { it.tag == Tag.CERTIFICATE_NOT_AFTER }?.value?.dateTime?.let { Date(it) }
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid, certNotBefore, certNotAfter)
// Cache the newly patched chain to ensure consistency across subsequent API calls. // Cache the newly patched chain to ensure consistency across subsequent API calls.
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val key = metadata.key
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! ?: return TransactionResult.SkipTransaction
val key = metadata.key!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
metadata.authorizations =
InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid)
// We must clean up cached generated keys before storing the patched chain // We must clean up cached generated keys before storing the patched chain
cleanupKeyData(keyId) cleanupKeyData(keyId)
patchedChains[keyId] = newChain patchedChains[keyId] = newChain
teeResponses[keyId] = KeyEntryResponse().apply {
this.metadata = metadata
iSecurityLevel = original
}
SystemLogger.debug( SystemLogger.debug(
"Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])" "Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])"
) )
@@ -237,7 +283,7 @@ class KeyMintSecurityLevelInterceptor(
txId: Long, txId: Long,
callingUid: Int, callingUid: Int,
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult = runCatching {
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}") SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
@@ -247,29 +293,38 @@ class KeyMintSecurityLevelInterceptor(
// Android framework calls createOperation with domain=APP+alias; // Android framework calls createOperation with domain=APP+alias;
// keystore2 internally resolves to KEY_ID — but software keys never // keystore2 internally resolves to KEY_ID — but software keys never
// reach keystore2's database, so we must handle both lookup paths. // reach keystore2's database, so we must handle both lookup paths.
val generatedKeyInfo = when (keyDescriptor.domain) { val resolvedEntry: Map.Entry<KeyIdentifier, GeneratedKeyInfo> =
Domain.APP -> { when (keyDescriptor.domain) {
val alias = keyDescriptor.alias ?: run { Domain.APP -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL") val alias = keyDescriptor.alias ?: run {
return TransactionResult.ContinueAndSkipPost SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
val key = KeyIdentifier(callingUid, alias)
generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
} }
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run { Domain.KEY_ID -> {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL") val nspace = keyDescriptor.nspace
val entry = if (nspace == null || nspace == 0L) null
else generatedKeys.entries
.filter { it.key.uid == callingUid }
.find { it.value.nspace == nspace }
entry ?: run {
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
return TransactionResult.Continue
}
}
else -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
} }
} }
Domain.KEY_ID -> { val generatedKeyInfo = resolvedEntry.value
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run { val resolvedKeyId = resolvedEntry.key
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
return TransactionResult.Continue
}
}
else -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
}
trackAndEnforceOpLimit(callingUid, txId)?.let { return it } trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
@@ -278,20 +333,65 @@ class KeyMintSecurityLevelInterceptor(
val params = data.createTypedArray(KeyParameter.CREATOR)!! val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params).let { p -> val parsedParams = KeyMintAttestation(params).let { p ->
if (p.algorithm != 0) p if (p.algorithm != 0) p
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) { else {
"EC", "ECDSA" -> Algorithm.EC val keyAlgo = generatedKeyInfo.keyPair?.private?.algorithm
"RSA" -> Algorithm.RSA p.copy(algorithm = when (keyAlgo) {
else -> p.algorithm "EC", "ECDSA" -> Algorithm.EC
}) "RSA" -> Algorithm.RSA
else -> generatedKeyInfo.keyParams?.algorithm ?: p.algorithm
})
}
}
val forced = data.readBoolean()
val requestedPurpose = parsedParams.purpose.firstOrNull()
if (requestedPurpose == null) {
return InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_INVALID_ARGUMENT)
}
if (forced) {
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
} }
AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode -> AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode ->
SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode") SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode")
return InterceptorUtils.createErrorReply(errorCode) return InterceptorUtils.createServiceSpecificErrorReply(errorCode)
} }
val keyParams = generatedKeyInfo.keyParams
val effectiveParams = if (keyParams != null) {
keyParams.copy(
purpose = parsedParams.purpose,
digest = parsedParams.digest.ifEmpty { keyParams.digest },
blockMode = parsedParams.blockMode.ifEmpty { keyParams.blockMode },
padding = parsedParams.padding.ifEmpty { keyParams.padding },
nonce = parsedParams.nonce,
minMacLength = parsedParams.minMacLength ?: keyParams.minMacLength,
)
} else parsedParams
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency) val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, generatedKeyInfo.secretKey, effectiveParams, opLatency)
if (keyParams?.usageCountLimit != null) {
val limit = keyParams.usageCountLimit
val remaining = usageCounters.getOrPut(resolvedKeyId) {
java.util.concurrent.atomic.AtomicInteger(limit)
}
if (remaining.get() <= 0) {
cleanupKeyData(resolvedKeyId)
usageCounters.remove(resolvedKeyId)
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_KEY_NOT_FOUND)
}
softwareOperation.onFinishCallback = {
if (remaining.decrementAndGet() <= 0) {
cleanupKeyData(resolvedKeyId)
usageCounters.remove(resolvedKeyId)
SystemLogger.info("Key $resolvedKeyId exhausted (USAGE_COUNT_LIMIT=$limit).")
}
}
}
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
pruneOpsForUid(callingUid, softwareOperation, maxOps) pruneOpsForUid(callingUid, softwareOperation, maxOps)
val operationBinder = SoftwareOperationBinder(softwareOperation) val operationBinder = SoftwareOperationBinder(softwareOperation)
@@ -300,26 +400,17 @@ class KeyMintSecurityLevelInterceptor(
CreateOperationResponse().apply { CreateOperationResponse().apply {
iOperation = operationBinder iOperation = operationBinder
operationChallenge = null operationChallenge = null
softwareOperation.iv?.let { iv -> parameters = softwareOperation.beginParameters
parameters = KeyParameters().apply {
keyParameter = arrayOf(
KeyParameter().apply {
tag = Tag.NONCE
value = KeyParameterValue.blob(iv)
}
)
}
}
} }
return InterceptorUtils.createTypedObjectReply(response) InterceptorUtils.createTypedObjectReply(response)
}.getOrElse {
SystemLogger.error("Error during createOperation for UID $callingUid.", it)
InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_UNKNOWN_ERROR)
} }
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult { private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult {
if (data.dataSize() > MAX_ALIAS_LENGTH) { val oversized = data.dataSize() > MAX_ALIAS_LENGTH
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
return TransactionResult.ContinueAndSkipPost
}
return runCatching { return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
@@ -332,6 +423,11 @@ class KeyMintSecurityLevelInterceptor(
val params = data.createTypedArray(KeyParameter.CREATOR)!! val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params) val parsedParams = KeyMintAttestation(params)
SystemLogger.trace { "[TRACE-$txId] generateKey alias=${keyDescriptor.alias} algo=${parsedParams.algorithm} challenge=${parsedParams.attestationChallenge?.size ?: "null"} serial=${parsedParams.serial != null} imei=${parsedParams.imei != null} noAuth=${parsedParams.noAuthRequired} purposes=${parsedParams.purpose}" }
if (SystemLogger.isDebugBuild) params.forEach { p ->
SystemLogger.trace { "[TRACE-$txId] tag=${p.tag} value=${p.value}" }
}
val challenge = parsedParams.attestationChallenge val challenge = parsedParams.attestationChallenge
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) { if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})") SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
@@ -361,15 +457,25 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
} }
// AOSP security_level.rs:478-485: INCLUDE_UNIQUE_ID requires
// SELinux gen_unique_id OR Android REQUEST_UNIQUE_ID_ATTESTATION
if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) {
val hasSELinux = ConfigurationManager.checkSELinuxPermission(
callingPid, "keystore_key", "gen_unique_id",
)
val hasAndroid = ConfigurationManager.hasPermissionForUid(
callingUid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION",
)
if (!hasSELinux && !hasAndroid) {
SystemLogger.warning("[TX_ID: $txId] Rejecting INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid")
return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED)
}
}
val isSymmetric = parsedParams.algorithm == Algorithm.AES || val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
parsedParams.algorithm == Algorithm.HMAC || parsedParams.algorithm == Algorithm.HMAC ||
parsedParams.algorithm == Algorithm.TRIPLE_DES parsedParams.algorithm == Algorithm.TRIPLE_DES
if (isSymmetric) {
SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) { if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection") SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
@@ -378,42 +484,33 @@ class KeyMintSecurityLevelInterceptor(
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey() val isAttestKeyRequest = parsedParams.isAttestKey()
val needsSoftwareGeneration = val forceGenerate =
ConfigurationManager.shouldGenerate(callingUid) || oversized ||
ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) || (ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
(attestationKey != null && (attestationKey != null &&
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) { val isAuto = ConfigurationManager.isAutoMode(callingUid)
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
} else if (parsedParams.attestationChallenge != null) {
val windowUsed = hardwareKeygenWindowCount(callingUid)
val concurrentUsed = hardwareKeygenCount(callingUid).get()
// Sliding window rate limit if (isAuto) SystemLogger.debug("AUTO dispatch: teePathDecision=${teePathDecision.get()} for ${keyDescriptor.alias}")
if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) {
SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback") SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate isAuto=$isAuto teePath=${teePathDecision.get()} hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" }
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
when {
forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
isAuto && teePathDecision.get() == null -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest)
isAuto && teePathDecision.get() == false -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
parsedParams.attestationChallenge != null -> TransactionResult.Continue
else -> {
cleanupKeyData(keyId)
TransactionResult.ContinueAndSkipPost
} }
// Concurrent cap
if (hardwareKeygenCount(callingUid).incrementAndGet() > MAX_CONCURRENT_HW_KEYGEN_PER_UID) {
hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] CONCURRENT_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → software fallback")
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
}
// Both checks passed — commit the window permit and forward to hardware TEE
recordHardwareKeygen(callingUid)
hardwareKeygenTxIds.add(txId)
SystemLogger.info("[TX_ID: $txId] HARDWARE_KEYGEN uid=$callingUid window=${windowUsed + 1}/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → forwarding to TEE")
return TransactionResult.Continue
} }
cleanupKeyData(keyId)
TransactionResult.ContinueAndSkipPost
} }
.getOrElse { .getOrElse {
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it) SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
TransactionResult.ContinueAndSkipPost InterceptorUtils.createServiceSpecificErrorReply(SECURE_HW_COMMUNICATION_FAILED)
} }
} }
@@ -425,10 +522,57 @@ class KeyMintSecurityLevelInterceptor(
keyId: KeyIdentifier, keyId: KeyIdentifier,
isAttestKeyRequest: Boolean, isAttestKeyRequest: Boolean,
): TransactionResult { ): TransactionResult {
val startNs = System.nanoTime() val genStartNanos = System.nanoTime()
keyDescriptor.nspace = secureRandom.nextLong() keyDescriptor.nspace = secureRandom.nextLong()
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].") SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
cleanupKeyData(keyId)
val isSymmetric = parsedParams.algorithm != Algorithm.EC &&
parsedParams.algorithm != Algorithm.RSA
if (isSymmetric) {
val algoName = when (parsedParams.algorithm) {
Algorithm.AES -> "AES"
Algorithm.HMAC -> "HmacSHA256"
else -> throw android.os.ServiceSpecificException(
SECURE_HW_COMMUNICATION_FAILED,
"Unsupported symmetric algorithm: ${parsedParams.algorithm}",
)
}
val keyGen = javax.crypto.KeyGenerator.getInstance(algoName)
keyGen.init(parsedParams.keySize)
val secretKey = keyGen.generateKey()
val metadata = KeyMetadata().apply {
keySecurityLevel = securityLevel
key = KeyDescriptor().apply {
domain = Domain.KEY_ID
nspace = keyDescriptor.nspace
alias = null
blob = null
}
certificate = null
certificateChain = null
authorizations = parsedParams.toAuthorizations(callingUid, securityLevel)
modificationTimeMs = System.currentTimeMillis()
}
val response = KeyEntryResponse().apply {
this.metadata = metadata
iSecurityLevel = original
}
generatedKeys[keyId] = GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams)
if (securityLevel == SecurityLevel.STRONGBOX) {
val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
} else {
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
}
return InterceptorUtils.createTypedObjectReply(metadata)
}
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
generateAttestedKeyPairNative(callingUid, parsedParams) generateAttestedKeyPairNative(callingUid, parsedParams)
?: CertificateGenerator.generateAttestedKeyPair( ?: CertificateGenerator.generateAttestedKeyPair(
@@ -440,33 +584,135 @@ class KeyMintSecurityLevelInterceptor(
) )
} ?: throw Exception("Both native and BouncyCastle cert gen failed.") } ?: throw Exception("Both native and BouncyCastle cert gen failed.")
cleanupKeyData(keyId)
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor) val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response, parsedParams) generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams)
if (isAttestKeyRequest) attestationKeys.add(keyId) if (isAttestKeyRequest) attestationKeys.add(keyId)
GeneratedKeyPersistence.save( if (SystemLogger.isDebugBuild) {
keyId = keyId, val chain = keyData.second
keyPair = keyData.first, val leaf = chain.firstOrNull() as? java.security.cert.X509Certificate
nspace = keyDescriptor.nspace, SystemLogger.trace {
securityLevel = securityLevel, "[certchain] ${keyDescriptor.alias}: depth=${chain.size} " +
certChain = keyData.second.toList(), "issuer=${leaf?.issuerX500Principal?.name} " +
algorithm = parsedParams.algorithm, "subject=${leaf?.subjectX500Principal?.name} " +
keySize = parsedParams.keySize, "hasAttest=${leaf?.getExtensionValue("1.3.6.1.4.1.11129.2.1.17") != null}"
ecCurve = parsedParams.ecCurve ?: 0, }
purposes = parsedParams.purpose, }
digests = parsedParams.digest,
isAttestationKey = isAttestKeyRequest,
)
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000 val certChainCopy = keyData.second.toList()
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS persistExecutor.execute {
val delayMs = floor - elapsedMs GeneratedKeyPersistence.save(
if (delayMs > 0) Thread.sleep(delayMs) keyId = keyId,
keyPair = keyData.first,
nspace = keyDescriptor.nspace,
securityLevel = securityLevel,
certChain = certChainCopy,
algorithm = parsedParams.algorithm,
keySize = parsedParams.keySize,
ecCurve = parsedParams.ecCurve ?: 0,
purposes = parsedParams.purpose,
digests = parsedParams.digest,
isAttestationKey = isAttestKeyRequest,
)
}
if (securityLevel == SecurityLevel.STRONGBOX) {
val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000
if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
} else {
TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos)
}
return InterceptorUtils.createTypedObjectReply(response.metadata) return InterceptorUtils.createTypedObjectReply(response.metadata)
} }
private fun raceTeePatch(
callingUid: Int,
keyDescriptor: KeyDescriptor,
attestationKey: KeyDescriptor?,
rawParams: Array<KeyParameter>,
parsedParams: KeyMintAttestation,
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
SystemLogger.info("AUTO: racing TEE vs software for ${keyDescriptor.alias}")
val teeDescriptor = KeyDescriptor().apply {
domain = keyDescriptor.domain
nspace = keyDescriptor.nspace
alias = keyDescriptor.alias
blob = keyDescriptor.blob
}
val teeAttestKey = attestationKey?.let {
KeyDescriptor().apply {
domain = it.domain
nspace = it.nspace
alias = it.alias
blob = it.blob
}
}
val threadA = CompletableFuture.supplyAsync {
original.generateKey(teeDescriptor, teeAttestKey, rawParams, 0, byteArrayOf())
}
val swDescriptor = KeyDescriptor().apply {
domain = keyDescriptor.domain
nspace = secureRandom.nextLong()
alias = keyDescriptor.alias
blob = keyDescriptor.blob
}
val swKeyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val threadB = CompletableFuture.supplyAsync {
doSoftwareKeyGen(callingUid, swDescriptor, attestationKey, parsedParams, swKeyId, isAttestKeyRequest)
}
return try {
val teeMetadata = threadA.join()
threadB.cancel(true)
teePathDecision.compareAndSet(null, true)
SystemLogger.info("AUTO: TEE succeeded, path locked to PATCH for ${keyDescriptor.alias}")
val originalChain = CertificateHelper.getCertificateChain(teeMetadata)
if (originalChain != null && originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(
originalChain, callingUid, parsedParams.certificateNotBefore, parsedParams.certificateNotAfter
)
CertificateHelper.updateCertificateChain(teeMetadata, newChain).getOrThrow()
teeMetadata.authorizations =
InterceptorUtils.patchAuthorizations(teeMetadata.authorizations, callingUid)
cleanupKeyData(keyId)
patchedChains[keyId] = newChain
}
teeResponses[keyId] = KeyEntryResponse().apply {
this.metadata = teeMetadata
iSecurityLevel = original
}
InterceptorUtils.createTypedObjectReply(teeMetadata)
} catch (_: Exception) {
if (teePathDecision.get() == true) {
threadB.cancel(true)
SystemLogger.info("AUTO: TEE failed locally but globally functional, forwarding for ${keyDescriptor.alias}")
return TransactionResult.Continue
}
teePathDecision.compareAndSet(null, false)
SystemLogger.info("AUTO: TEE failed, path locked to GENERATE for ${keyDescriptor.alias}")
try {
threadB.join()
} catch (e: Exception) {
SystemLogger.error("AUTO: both paths failed for ${keyDescriptor.alias}.", e)
val code =
if (e.cause is android.os.ServiceSpecificException)
(e.cause as android.os.ServiceSpecificException).errorCode
else SECURE_HW_COMMUNICATION_FAILED
InterceptorUtils.createServiceSpecificErrorReply(code)
}
}
}
private fun generateAttestedKeyPairNative( private fun generateAttestedKeyPairNative(
callingUid: Int, callingUid: Int,
params: KeyMintAttestation, params: KeyMintAttestation,
@@ -487,7 +733,8 @@ class KeyMintSecurityLevelInterceptor(
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel) val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel) val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel)
val appId = AttestationBuilder.createApplicationId(callingUid) val hasChallenge = params.attestationChallenge != null
val appId = if (hasChallenge) AttestationBuilder.createApplicationId(callingUid) else null
val config = CertGenConfig( val config = CertGenConfig(
algorithm = params.algorithm, algorithm = params.algorithm,
@@ -513,7 +760,7 @@ class KeyMintSecurityLevelInterceptor(
bootKey = AndroidDeviceUtils.bootKey, bootKey = AndroidDeviceUtils.bootKey,
bootHash = AndroidDeviceUtils.bootHash, bootHash = AndroidDeviceUtils.bootHash,
creationDatetime = System.currentTimeMillis(), creationDatetime = System.currentTimeMillis(),
attestationApplicationId = appId.octets, attestationApplicationId = appId?.octets ?: ByteArray(0),
moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null, moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null,
idBrand = params.brand, idBrand = params.brand,
idDevice = params.device, idDevice = params.device,
@@ -524,6 +771,13 @@ class KeyMintSecurityLevelInterceptor(
idManufacturer = params.manufacturer, idManufacturer = params.manufacturer,
idModel = params.model, idModel = params.model,
idSecondImei = if (attestVersion >= 300) params.secondImei else null, idSecondImei = if (attestVersion >= 300) params.secondImei else null,
activeDatetime = params.activeDateTime?.time ?: -1L,
originationExpireDatetime = params.originationExpireDateTime?.time ?: -1L,
usageExpireDatetime = params.usageExpireDateTime?.time ?: -1L,
usageCountLimit = params.usageCountLimit ?: -1,
callerNonce = params.callerNonce == true,
unlockedDeviceRequired = params.unlockedDeviceRequired == true,
noAuthRequired = params.noAuthRequired != false,
) )
val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null
@@ -634,6 +888,7 @@ class KeyMintSecurityLevelInterceptor(
usageExpireDateTime = null, usageExpireDateTime = null,
usageCountLimit = null, usageCountLimit = null,
callerNonce = null, callerNonce = null,
nonce = null,
unlockedDeviceRequired = null, unlockedDeviceRequired = null,
includeUniqueId = null, includeUniqueId = null,
rollbackResistance = null, rollbackResistance = null,
@@ -649,7 +904,7 @@ class KeyMintSecurityLevelInterceptor(
) )
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor) val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response, attestation) generatedKeys[keyId] = GeneratedKeyInfo(keyPair, null, record.nspace, response, attestation)
if (record.isAttestationKey) attestationKeys.add(keyId) if (record.isAttestationKey) attestationKeys.add(keyId)
SystemLogger.debug("Restored persisted key: $keyId") SystemLogger.debug("Restored persisted key: $keyId")
@@ -668,52 +923,26 @@ class KeyMintSecurityLevelInterceptor(
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead // Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
private const val MAX_ALIAS_LENGTH = 256 * 1024 private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val KEYMINT_INVALID_INPUT_LENGTH = -21 private const val KEYMINT_INVALID_INPUT_LENGTH = -21
private const val KEYMINT_INVALID_ARGUMENT = -38
private const val RESPONSE_INVALID_ARGUMENT = 20 private const val RESPONSE_INVALID_ARGUMENT = 20
private const val RESPONSE_PERMISSION_DENIED = 6
private const val RESPONSE_KEY_NOT_FOUND = 7
private const val TEE_LATENCY_FLOOR_MS = 15L private const val TEE_LATENCY_FLOOR_MS = 15L
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
private const val KEYMINT_TOO_MANY_OPERATIONS = -29 private const val KEYMINT_TOO_MANY_OPERATIONS = -29
private const val KEYMINT_CANNOT_ATTEST_IDS = -66 private const val KEYMINT_CANNOT_ATTEST_IDS = -66
private const val KEYMINT_UNKNOWN_ERROR = -1000
private const val SECURE_HW_COMMUNICATION_FAILED = -49
private const val MAX_CONCURRENT_OPS_PER_UID = 15 private const val MAX_CONCURRENT_OPS_PER_UID = 15
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4 private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
// Sliding window: max hardware keygen permits per UID within the burst window
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
private const val BURST_WINDOW_MS = 30_000L
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) { private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
Algorithm.RSA -> params.keySize <= 2048 Algorithm.RSA -> params.keySize <= 2048
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256 Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
else -> true else -> true
} }
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
private fun hardwareKeygenWindowCount(uid: Int): Int {
val now = System.currentTimeMillis()
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.removeAll { now - it > BURST_WINDOW_MS }
if (timestamps.isEmpty()) {
uidKeygenTimestamps.remove(uid, timestamps)
uidHardwareKeygenCount.remove(uid)
}
return timestamps.size
}
}
private fun recordHardwareKeygen(uid: Int) {
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.add(System.currentTimeMillis())
}
}
private val GENERATE_KEY_TRANSACTION = private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION = private val IMPORT_KEY_TRANSACTION =
@@ -738,14 +967,18 @@ class KeyMintSecurityLevelInterceptor(
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
} }
private val persistExecutor = Executors.newSingleThreadExecutor()
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>() val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
val teeResponses = ConcurrentHashMap<KeyIdentifier, KeyEntryResponse>()
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>() val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet() val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet() val importedKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val usageCounters = ConcurrentHashMap<KeyIdentifier, java.util.concurrent.atomic.AtomicInteger>()
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>() private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response generatedKeys[keyId]?.response ?: teeResponses[keyId]
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? { fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
if (nspace == null || nspace == 0L) return null if (nspace == null || nspace == 0L) return null
@@ -764,6 +997,7 @@ class KeyMintSecurityLevelInterceptor(
SystemLogger.debug("Remove generated key ${keyId}") SystemLogger.debug("Remove generated key ${keyId}")
GeneratedKeyPersistence.delete(keyId) GeneratedKeyPersistence.delete(keyId)
} }
teeResponses.remove(keyId)
if (patchedChains.remove(keyId) != null) { if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}") SystemLogger.debug("Remove patched chain for ${keyId}")
} }
@@ -771,6 +1005,7 @@ class KeyMintSecurityLevelInterceptor(
SystemLogger.debug("Remove cached attestaion key ${keyId}") SystemLogger.debug("Remove cached attestaion key ${keyId}")
} }
importedKeys.remove(keyId) importedKeys.remove(keyId)
usageCounters.remove(keyId)
} }
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
@@ -793,9 +1028,11 @@ class KeyMintSecurityLevelInterceptor(
val count = generatedKeys.size val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: "" val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear() generatedKeys.clear()
teeResponses.clear()
patchedChains.clear() patchedChains.clear()
attestationKeys.clear() attestationKeys.clear()
importedKeys.clear() importedKeys.clear()
usageCounters.clear()
GeneratedKeyPersistence.deleteAll() GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
} }
@@ -832,7 +1069,37 @@ private fun KeyMintAttestation.toAuthorizations(
if (this.rsaPublicExponent != null) { if (this.rsaPublicExponent != null) {
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()))) authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
} }
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true))) if (this.callerNonce == true) {
authList.add(createAuth(Tag.CALLER_NONCE, KeyParameterValue.boolValue(true)))
}
if (this.minMacLength != null) {
authList.add(createAuth(Tag.MIN_MAC_LENGTH, KeyParameterValue.integer(this.minMacLength)))
}
if (this.rollbackResistance == true) {
authList.add(createAuth(Tag.ROLLBACK_RESISTANCE, KeyParameterValue.boolValue(true)))
}
if (this.earlyBootOnly == true) {
authList.add(createAuth(Tag.EARLY_BOOT_ONLY, KeyParameterValue.boolValue(true)))
}
if (this.allowWhileOnBody == true) {
authList.add(createAuth(Tag.ALLOW_WHILE_ON_BODY, KeyParameterValue.boolValue(true)))
}
if (this.trustedUserPresenceRequired == true) {
authList.add(createAuth(Tag.TRUSTED_USER_PRESENCE_REQUIRED, KeyParameterValue.boolValue(true)))
}
if (this.trustedConfirmationRequired == true) {
authList.add(createAuth(Tag.TRUSTED_CONFIRMATION_REQUIRED, KeyParameterValue.boolValue(true)))
}
if (this.maxUsesPerBoot != null) {
authList.add(createAuth(Tag.MAX_USES_PER_BOOT, KeyParameterValue.integer(this.maxUsesPerBoot)))
}
if (this.maxBootLevel != null) {
authList.add(createAuth(Tag.MAX_BOOT_LEVEL, KeyParameterValue.integer(this.maxBootLevel)))
}
if (this.noAuthRequired != false) {
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
}
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED))) authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion))) authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
@@ -848,17 +1115,37 @@ private fun KeyMintAttestation.toAuthorizations(
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) { if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
} }
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
authList.add( fun createSwAuth(tag: Int, value: KeyParameterValue): Authorization {
Authorization().apply { val param = KeyParameter().apply {
this.keyParameter = this.tag = tag
KeyParameter().apply { this.value = value
this.tag = Tag.USER_ID }
this.value = KeyParameterValue.integer(callingUid / 100000) return Authorization().apply {
} this.keyParameter = param
this.securityLevel = SecurityLevel.SOFTWARE this.securityLevel = SecurityLevel.SOFTWARE
} }
) }
authList.add(createSwAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
this.activeDateTime?.let {
authList.add(createSwAuth(Tag.ACTIVE_DATETIME, KeyParameterValue.dateTime(it.time)))
}
this.originationExpireDateTime?.let {
authList.add(createSwAuth(Tag.ORIGINATION_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)))
}
this.usageExpireDateTime?.let {
authList.add(createSwAuth(Tag.USAGE_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)))
}
this.usageCountLimit?.let {
authList.add(createSwAuth(Tag.USAGE_COUNT_LIMIT, KeyParameterValue.integer(it)))
}
if (this.unlockedDeviceRequired == true) {
authList.add(createSwAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true)))
}
authList.add(createSwAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
return authList.toTypedArray() return authList.toTypedArray()
} }
@@ -3,10 +3,15 @@ package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.BlockMode
import android.hardware.security.keymint.Digest import android.hardware.security.keymint.Digest
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.PaddingMode import android.hardware.security.keymint.PaddingMode
import android.hardware.security.keymint.Tag
import android.os.ServiceSpecificException import android.os.ServiceSpecificException
import java.util.concurrent.locks.LockSupport
import android.system.keystore2.IKeystoreOperation import android.system.keystore2.IKeystoreOperation
import android.system.keystore2.KeyParameters
import java.security.KeyPair import java.security.KeyPair
import java.security.Signature import java.security.Signature
import java.security.SignatureException import java.security.SignatureException
@@ -22,7 +27,7 @@ private sealed interface CryptoPrimitive {
fun update(data: ByteArray?): ByteArray? fun update(data: ByteArray?): ByteArray?
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
fun abort() fun abort()
fun getIv(): ByteArray? = null fun getBeginParameters(): Array<KeyParameter>? = null
} }
private object JcaAlgorithmMapper { private object JcaAlgorithmMapper {
@@ -125,15 +130,21 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
} }
private class CipherPrimitive( private class CipherPrimitive(
keyPair: KeyPair, cryptoKey: java.security.Key,
params: KeyMintAttestation, params: KeyMintAttestation,
private val opMode: Int, private val opMode: Int,
) : CryptoPrimitive { ) : CryptoPrimitive {
private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM
private val cipher: Cipher = private val cipher: Cipher =
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private val nonce = params.nonce
init(opMode, key) if (nonce != null && isAead) {
init(opMode, cryptoKey, javax.crypto.spec.GCMParameterSpec(128, nonce))
} else if (nonce != null) {
init(opMode, cryptoKey, javax.crypto.spec.IvParameterSpec(nonce))
} else {
init(opMode, cryptoKey)
}
} }
override fun updateAad(aadInput: ByteArray?) { override fun updateAad(aadInput: ByteArray?) {
@@ -147,14 +158,45 @@ private class CipherPrimitive(
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? = override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? =
if (data != null) cipher.doFinal(data) else cipher.doFinal() if (data != null) cipher.doFinal(data) else cipher.doFinal()
override fun getIv(): ByteArray? = if (isAead) cipher.iv else null override fun getBeginParameters(): Array<KeyParameter>? {
val iv = cipher.iv ?: return null
return arrayOf(
KeyParameter().apply {
tag = Tag.NONCE
value = KeyParameterValue.blob(iv)
}
)
}
override fun abort() {}
}
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
private val agreement: javax.crypto.KeyAgreement =
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
override fun update(data: ByteArray?): ByteArray? = null
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
if (data == null)
throw ServiceSpecificException(
KeystoreErrorCodes.invalidArgument,
"Peer public key required for key agreement",
)
val peerKey =
java.security.KeyFactory.getInstance("EC")
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
agreement.doPhase(peerKey, true)
return agreement.generateSecret()
}
override fun abort() {} override fun abort() {}
} }
class SoftwareOperation( class SoftwareOperation(
private val txId: Long, private val txId: Long,
keyPair: KeyPair, keyPair: KeyPair?,
secretKey: javax.crypto.SecretKey?,
params: KeyMintAttestation, params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L, private val latencyFloorMs: Long = 0L,
) { ) {
@@ -162,8 +204,14 @@ class SoftwareOperation(
@Volatile var finalized = false @Volatile var finalized = false
private set private set
val iv: ByteArray? var onFinishCallback: (() -> Unit)? = null
get() = primitive.getIv()
val beginParameters: KeyParameters?
get() {
val params = primitive.getBeginParameters() ?: return null
if (params.isEmpty()) return null
return KeyParameters().apply { keyParameter = params }
}
init { init {
val purpose = params.purpose.firstOrNull() val purpose = params.purpose.firstOrNull()
@@ -172,10 +220,17 @@ class SoftwareOperation(
primitive = primitive =
when (purpose) { when (purpose) {
KeyPurpose.SIGN -> Signer(keyPair, params) KeyPurpose.SIGN -> Signer(keyPair!!, params)
KeyPurpose.VERIFY -> Verifier(keyPair, params) KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) KeyPurpose.ENCRYPT -> {
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) val key: java.security.Key = secretKey ?: keyPair!!.public
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
}
KeyPurpose.DECRYPT -> {
val key: java.security.Key = secretKey ?: keyPair!!.private
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
}
KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!)
else -> else ->
throw ServiceSpecificException( throw ServiceSpecificException(
KeystoreErrorCodes.unsupportedPurpose, KeystoreErrorCodes.unsupportedPurpose,
@@ -228,9 +283,10 @@ class SoftwareOperation(
if (latencyFloorMs > 0) { if (latencyFloorMs > 0) {
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000 val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = latencyFloorMs - elapsedMs val delayMs = latencyFloorMs - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs) if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000)
} }
finalized = true finalized = true
onFinishCallback?.invoke()
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result return result
} catch (e: ServiceSpecificException) { } catch (e: ServiceSpecificException) {
@@ -1,42 +1,86 @@
package org.matrix.TEESimulator.logging package org.matrix.TEESimulator.logging
import android.util.Log import android.util.Log
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
import org.matrix.TEESimulator.BuildConfig import org.matrix.TEESimulator.BuildConfig
/** /**
* A centralized logging utility for the TEESimulator application. This object provides a consistent * A centralized logging utility for the TEESimulator application. This object provides a consistent
* logging tag and format for all application logs, making it easier to filter and debug in Logcat. * logging tag and format for all application logs, making it easier to filter and debug in Logcat.
*
* Includes a rate limiter that caps logd syscalls during binder stress to prevent thread pool
* contention. The first [RATE_LIMIT_BURST] messages per [RATE_LIMIT_WINDOW_MS] window are logged
* normally; subsequent messages are suppressed and a summary is emitted when the window resets.
*/ */
object SystemLogger { object SystemLogger {
// The tag used for all log messages from this application. @PublishedApi internal const val TAG = "TEESimulator"
private const val TAG = "TEESimulator"
private val isDebugBuild = BuildConfig.DEBUG @PublishedApi internal val isDebugBuild = BuildConfig.DEBUG
// Rate limiter: allow BURST messages per WINDOW, then suppress until window resets.
private const val RATE_LIMIT_BURST = 15
private const val RATE_LIMIT_WINDOW_MS = 1000L
private val windowStart = AtomicLong(System.currentTimeMillis())
private val windowCount = AtomicInteger(0)
private val suppressedCount = AtomicInteger(0)
/**
* Returns true if this message should be emitted. Resets the window if expired
* and emits a suppression summary for the previous window.
*/
@PublishedApi internal fun acquireLogPermit(): Boolean {
val now = System.currentTimeMillis()
val start = windowStart.get()
if (now - start > RATE_LIMIT_WINDOW_MS) {
// Window expired: reset and emit suppression summary if needed.
if (windowStart.compareAndSet(start, now)) {
val suppressed = suppressedCount.getAndSet(0)
windowCount.set(1) // this call counts as #1 in the new window
if (suppressed > 0) {
Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window")
}
return true
}
}
val count = windowCount.incrementAndGet()
if (count <= RATE_LIMIT_BURST) return true
suppressedCount.incrementAndGet()
return false
}
/** /**
* Logs a debug message. Use this for fine-grained information that is useful for debugging. * Logs a debug message. Use this for fine-grained information that is useful for debugging.
*
* @param message The message to log.
*/ */
fun debug(message: String) { fun debug(message: String) {
if (!isDebugBuild) return if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.d(TAG, message) Log.d(TAG, message)
} }
/** Lazy debug: lambda only evaluates if message will be logged. */
inline fun debug(message: () -> String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.d(TAG, message())
}
/** /**
* Logs an informational message. Use this to report major application lifecycle events. * Logs an informational message. Use this to report major application lifecycle events.
*
* @param message The message to log.
*/ */
fun info(message: String) { fun info(message: String) {
if (!acquireLogPermit()) return
Log.i(TAG, message) Log.i(TAG, message)
} }
/** Lazy info: lambda only evaluates if message will be logged. */
inline fun info(message: () -> String) {
if (!acquireLogPermit()) return
Log.i(TAG, message())
}
/** /**
* Logs a warning message. Use this to report unexpected but non-fatal issues. * Logs a warning message. Warnings are never rate-limited.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/ */
fun warning(message: String, throwable: Throwable? = null) { fun warning(message: String, throwable: Throwable? = null) {
if (throwable != null) { if (throwable != null) {
@@ -47,11 +91,7 @@ object SystemLogger {
} }
/** /**
* Logs an error message. Use this to report fatal errors or exceptions that disrupt * Logs an error message. Errors are never rate-limited.
* functionality.
*
* @param message The message to log.
* @param throwable An optional exception to log with the message.
*/ */
fun error(message: String, throwable: Throwable? = null) { fun error(message: String, throwable: Throwable? = null) {
if (throwable != null) { if (throwable != null) {
@@ -64,11 +104,22 @@ object SystemLogger {
/** /**
* Logs a verbose message. This level is for highly detailed logs that are generally not needed * Logs a verbose message. This level is for highly detailed logs that are generally not needed
* unless tracking a very specific issue. * unless tracking a very specific issue.
*
* @param message The message to log.
*/ */
fun verbose(message: String) { fun verbose(message: String) {
if (!isDebugBuild) return if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.v(TAG, message) Log.v(TAG, message)
} }
/** Lazy verbose: lambda only evaluates if message will be logged. */
inline fun verbose(message: () -> String) {
if (!isDebugBuild) return
if (!acquireLogPermit()) return
Log.v(TAG, message())
}
inline fun trace(message: () -> String) {
if (!isDebugBuild) return
Log.w(TAG, message())
}
} }
@@ -93,6 +93,12 @@ object CertificateGenerator {
) )
return try { return try {
// AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1
if (challenge == null && attestKeyAlias == null) {
SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" }
return listOf(buildSelfSignedCertificate(subjectKeyPair, params))
}
val keybox = getKeyboxForAlgorithm(uid, params.algorithm) val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
val (signingKey, issuer) = val (signingKey, issuer) =
@@ -238,10 +244,11 @@ object CertificateGenerator {
if (keyUsageBits != 0) { if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits)) builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
} }
// Add our custom, simulated attestation extension. if (params.attestationChallenge != null) {
builder.addExtension( builder.addExtension(
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel) AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
) )
}
val signerAlgorithm = val signerAlgorithm =
when (signingKeyPair.private.algorithm) { when (signingKeyPair.private.algorithm) {
@@ -256,4 +263,39 @@ object CertificateGenerator {
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
} }
// AOSP ta/src/keys.rs:452-478, ta/src/cert.rs:111-114
private fun buildSelfSignedCertificate(
keyPair: KeyPair,
params: KeyMintAttestation,
): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val notBefore = params.certificateNotBefore ?: Date(0)
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
val builder = JcaX509v3CertificateBuilder(
subject,
params.certificateSerial ?: BigInteger.ONE,
notBefore,
notAfter,
subject,
keyPair.public,
)
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
if (keyUsageBits != 0) {
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
}
val signerAlgorithm = when (keyPair.private.algorithm) {
"EC", "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}")
}
val contentSigner = JcaContentSignerBuilder(signerAlgorithm)
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
.build(keyPair.private)
return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner))
}
} }
@@ -45,6 +45,13 @@ data class CertGenConfig(
val idManufacturer: ByteArray?, val idManufacturer: ByteArray?,
val idModel: ByteArray?, val idModel: ByteArray?,
val idSecondImei: ByteArray?, val idSecondImei: ByteArray?,
val activeDatetime: Long = -1L,
val originationExpireDatetime: Long = -1L,
val usageExpireDatetime: Long = -1L,
val usageCountLimit: Int = -1,
val callerNonce: Boolean = false,
val unlockedDeviceRequired: Boolean = false,
val noAuthRequired: Boolean = true,
) )
object NativeCertGen { object NativeCertGen {
@@ -387,9 +387,17 @@ object AndroidDeviceUtils {
if (securityLevel == SecurityLevel.STRONGBOX) { if (securityLevel == SecurityLevel.STRONGBOX) {
return 300 return 300
} }
return DeviceAttestationService.CachedAttestationData?.attestVersion val cached = DeviceAttestationService.CachedAttestationData?.attestVersion
val version = cached
?: attestVersionMap[Build.VERSION.SDK_INT] ?: attestVersionMap[Build.VERSION.SDK_INT]
?: 400 // Default to a recent version ?: 400 // Default to a recent version
val source = when {
cached != null -> "cache"
attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map"
else -> "default"
}
SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel")
return version
} }
/** /**
@@ -398,10 +406,7 @@ object AndroidDeviceUtils {
* @param securityLevel The security level, used to determine the correct attestation version. * @param securityLevel The security level, used to determine the correct attestation version.
* @return The appropriate Keymaster or KeyMint version number. * @return The appropriate Keymaster or KeyMint version number.
*/ */
fun getKeymasterVersion(securityLevel: Int): Int { fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel)
val attestVersion = getAttestVersion(securityLevel)
return if (attestVersion >= 100) attestVersion else 41 // Keymaster 4.1 for older versions
}
// --- APEX and Module Hash Properties --- // --- APEX and Module Hash Properties ---
+26
View File
@@ -1,3 +1,29 @@
## TEESimulator-RS v6.0.0
Repository consolidation release. All tee-rebuild work merged as the new main branch.
### AOSP Self-Signed Cert Compliance
- No-challenge keys now generate self-signed certs (subject == issuer, depth 1), matching AOSP `ta/src/keys.rs:451-478`
- Both Kotlin (BouncyCastle) and Rust (native-certgen) paths corrected
- Eliminates attestation behavioral probes that detect keybox issuer on non-attested keys
### Stability
- Binder stress crash hardening for concurrent generateKey calls
- AUTO mode TEE race for consistent attestation on devices with working G10
- Oversized transactions routed to software gen instead of crashing
- Operation-time params (BLOCK_MODE, PADDING, DIGEST) passed through to CipherPrimitive
### Banking App Compatibility
- Bare `target.txt` entries now default to AUTO mode, resolved at config level to PATCH (working TEE) or GENERATE (broken TEE)
- Fixes BHIM and similar banking apps that require TEE-backed attestation keys
- Restores v5.0 behavior where AUTO was resolved before the interceptor dispatch, avoiding the non-deterministic `raceTeePatch` path
### Infrastructure
- Version scheme changed to semver (v6.0.0)
- Repository moved to TEESimulator-RS as canonical source
---
## TEESimulator-RS v5.0: AOSP Compliance Overhaul ## TEESimulator-RS v5.0: AOSP Compliance Overhaul
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture. Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"version": "v4.5", "version": "v6.0.0",
"versionCode": 111, "versionCode": 155,
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip", "zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/latest/download/TEESimulator-RS-Release.zip",
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md" "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md"
} }
+79 -2
View File
@@ -33,6 +33,36 @@ pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> { fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new(); let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
// Tag 303: CALLER_NONCE — NULL (presence = true)
if params.caller_nonce {
fields.push((303, enc_null()));
}
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
if params.active_datetime >= 0 {
fields.push((400, enc_integer(params.active_datetime)));
}
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
if params.origination_expire_datetime >= 0 {
fields.push((401, enc_integer(params.origination_expire_datetime)));
}
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
if params.usage_expire_datetime >= 0 {
fields.push((402, enc_integer(params.usage_expire_datetime)));
}
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
if params.usage_count_limit >= 0 {
fields.push((405, enc_integer(params.usage_count_limit as i64)));
}
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
if params.unlocked_device_required {
fields.push((509, enc_null()));
}
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds) // Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
fields.push((701, enc_integer(params.creation_datetime))); fields.push((701, enc_integer(params.creation_datetime)));
@@ -77,8 +107,10 @@ fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
fields.push((10, enc_integer(curve as i32 as i64))); fields.push((10, enc_integer(curve as i32 as i64)));
} }
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true) // Tag 503: NO_AUTH_REQUIRED — NULL (conditional)
fields.push((503, enc_null())); if params.no_auth_required {
fields.push((503, enc_null()));
}
// Tag 702: ORIGIN — INTEGER 0 (GENERATED) // Tag 702: ORIGIN — INTEGER 0 (GENERATED)
fields.push((702, enc_integer(0))); fields.push((702, enc_integer(0)));
@@ -519,6 +551,44 @@ mod tests {
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number"); assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
} }
#[test]
fn test_enforcement_tags_in_software_enforced() {
let mut params = make_test_params();
params.usage_count_limit = 3;
params.unlocked_device_required = true;
params.caller_nonce = true;
params.active_datetime = 1709913600000;
let sw = build_software_enforced(&params).unwrap();
let inner = skip_tlv_header(&sw);
let tags = extract_tag_numbers(inner);
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
}
#[test]
fn test_no_auth_required_conditional() {
let mut params = make_test_params();
params.no_auth_required = false;
let tee = build_tee_enforced(&params).unwrap();
let inner = skip_tlv_header(&tee);
let tags = extract_tag_numbers(inner);
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
}
#[test]
fn test_enforcement_tags_omitted_when_unset() {
let params = make_test_params();
let sw = build_software_enforced(&params).unwrap();
let inner = skip_tlv_header(&sw);
let tags = extract_tag_numbers(inner);
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
}
#[test] #[test]
fn test_full_extension_roundtrip() { fn test_full_extension_roundtrip() {
let params = make_test_params(); let params = make_test_params();
@@ -568,6 +638,13 @@ mod tests {
id_manufacturer: None, id_manufacturer: None,
id_model: None, id_model: None,
id_second_imei: None, id_second_imei: None,
active_datetime: -1,
origination_expire_datetime: -1,
usage_expire_datetime: -1,
usage_count_limit: -1,
caller_nonce: false,
unlocked_device_required: false,
no_auth_required: true,
} }
} }
+67 -8
View File
@@ -14,9 +14,69 @@ const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
// Extension OIDs // Extension OIDs
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15]; const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
// AOSP ta/src/keys.rs:451-478: no challenge = self-signed leaf, chain depth 1
pub fn build_self_signed_cert(
key_pair: &GeneratedKeyPair,
params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> {
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
let sig_alg_der = signature_algorithm_for_signing_key(&key_pair.private_key_pkcs8, params.algorithm)?;
let serial_bytes = if let Some(ref serial) = params.cert_serial {
serial.clone()
} else {
vec![1u8]
};
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
subject.clone()
} else {
encode_simple_cn_dn("Android Keystore Key")
};
let not_before = timestamp_to_datetime(params.cert_not_before)?;
let not_after = if params.cert_not_after == -1 {
// No keybox fallback available; use far-future (year 9999)
OffsetDateTime::from_unix_timestamp(253402300799)
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365 * 30))
} else {
timestamp_to_datetime(params.cert_not_after)?
};
let extensions_der = build_extensions(None, &params.purposes)?;
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
let serial_der = encode_der_integer(&serial_bytes);
let validity_der = encode_validity(&not_before, &not_after);
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
// issuer == subject (self-signed, per AOSP ta/src/cert.rs:111-114)
let tbs_der = encode_der_sequence(&[
&version_der,
&serial_der,
&sig_alg_der,
&subject_dn_der,
&validity_der,
&subject_dn_der,
&spki_der,
&extensions_tagged,
]);
let signature_bytes = sign_tbs(&tbs_der, &key_pair.private_key_pkcs8, params.algorithm)?;
let signature_bit_string = encode_der_bit_string(&signature_bytes);
let cert_der = encode_der_sequence(&[
&tbs_der,
&sig_alg_der,
&signature_bit_string,
]);
Ok(vec![cert_der])
}
pub fn build_certificate_chain( pub fn build_certificate_chain(
key_pair: &GeneratedKeyPair, key_pair: &GeneratedKeyPair,
attestation_ext_der: &[u8], attestation_ext_der: Option<&[u8]>,
keybox: &ParsedKeybox, keybox: &ParsedKeybox,
params: &CertGenParams, params: &CertGenParams,
) -> Result<Vec<Vec<u8>>> { ) -> Result<Vec<Vec<u8>>> {
@@ -33,7 +93,7 @@ pub fn build_certificate_chain(
fn build_leaf_cert( fn build_leaf_cert(
key_pair: &GeneratedKeyPair, key_pair: &GeneratedKeyPair,
attestation_ext_der: &[u8], attestation_ext_der: Option<&[u8]>,
keybox: &ParsedKeybox, keybox: &ParsedKeybox,
params: &CertGenParams, params: &CertGenParams,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
@@ -63,7 +123,6 @@ fn build_leaf_cert(
timestamp_to_datetime(params.cert_not_after)? timestamp_to_datetime(params.cert_not_after)?
}; };
// Extensions
let extensions_der = build_extensions(attestation_ext_der, &params.purposes)?; let extensions_der = build_extensions(attestation_ext_der, &params.purposes)?;
// TBS Certificate // TBS Certificate
@@ -256,19 +315,19 @@ fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits])) Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
} }
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> { fn build_extensions(attestation_ext_der: Option<&[u8]>, purposes: &[i32]) -> Result<Vec<u8>> {
let mut extensions: Vec<Vec<u8>> = Vec::new(); let mut extensions: Vec<Vec<u8>> = Vec::new();
// KeyUsage extension (critical)
let ku_byte = map_key_usage_byte(purposes); let ku_byte = map_key_usage_byte(purposes);
if ku_byte != 0 { if ku_byte != 0 {
let ku_ext = build_key_usage_extension(ku_byte); let ku_ext = build_key_usage_extension(ku_byte);
extensions.push(ku_ext); extensions.push(ku_ext);
} }
// Attestation extension (non-critical) if let Some(attest_der) = attestation_ext_der {
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der); let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attest_der);
extensions.push(attest_ext); extensions.push(attest_ext);
}
Ok(encode_der_sequence_of(&extensions)) Ok(encode_der_sequence_of(&extensions))
} }
+26 -8
View File
@@ -62,14 +62,13 @@ fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteAr
let keybox = keybox::parse_keybox(&params.keybox_cert_chain, &params.keybox_private_key)?; let keybox = keybox::parse_keybox(&params.keybox_cert_chain, &params.keybox_private_key)?;
let attest_ext = attestation::build_attestation_extension(&params)?; let cert_chain = if params.attestation_challenge.is_some() {
let attest_ext = attestation::build_attestation_extension(&params)?;
let cert_chain = certbuilder::build_certificate_chain( certbuilder::build_certificate_chain(&key_pair, Some(&attest_ext), &keybox, &params)?
&key_pair, } else {
&attest_ext, tracing::info!("no attestation challenge, generating self-signed cert (depth 1)");
&keybox, certbuilder::build_self_signed_cert(&key_pair, &params)?
&params, };
)?;
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain); let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
@@ -199,6 +198,14 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
let id_model = get_nullable_byte_array(env, config, "idModel")?; let id_model = get_nullable_byte_array(env, config, "idModel")?;
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?; let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
let active_datetime = get_long(env, config, "activeDatetime")?;
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
let caller_nonce = get_boolean(env, config, "callerNonce")?;
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
Ok(CertGenParams { Ok(CertGenParams {
algorithm: Algorithm::try_from(algorithm)?, algorithm: Algorithm::try_from(algorithm)?,
key_size: key_size as u32, key_size: key_size as u32,
@@ -238,6 +245,13 @@ fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
id_manufacturer, id_manufacturer,
id_model, id_model,
id_second_imei, id_second_imei,
active_datetime,
origination_expire_datetime,
usage_expire_datetime,
usage_count_limit,
caller_nonce,
unlocked_device_required,
no_auth_required,
}) })
} }
@@ -253,6 +267,10 @@ fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
Ok(env.get_field(obj, name, "J")?.j()?) Ok(env.get_field(obj, name, "J")?.j()?)
} }
fn get_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
Ok(env.get_field(obj, name, "Z")?.z()?)
}
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> { fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
let field = env.get_field(obj, name, "[B")?.l()?; let field = env.get_field(obj, name, "[B")?.l()?;
if field.is_null() { if field.is_null() {
+8 -29
View File
@@ -42,35 +42,6 @@ impl TryFrom<i32> for EcCurve {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum KeyPurpose {
Encrypt = 0,
Decrypt = 1,
Sign = 2,
Verify = 3,
WrapKey = 5,
AgreeKey = 6,
AttestKey = 7,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum SecurityLevel {
Software = 0,
TrustedEnvironment = 1,
StrongBox = 2,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum VerifiedBootState {
Verified = 0,
SelfSigned = 1,
Unverified = 2,
Failed = 3,
}
pub struct CertGenParams { pub struct CertGenParams {
pub algorithm: Algorithm, pub algorithm: Algorithm,
pub key_size: u32, pub key_size: u32,
@@ -114,6 +85,14 @@ pub struct CertGenParams {
pub id_manufacturer: Option<Vec<u8>>, pub id_manufacturer: Option<Vec<u8>>,
pub id_model: Option<Vec<u8>>, pub id_model: Option<Vec<u8>>,
pub id_second_imei: Option<Vec<u8>>, pub id_second_imei: Option<Vec<u8>>,
pub active_datetime: i64,
pub origination_expire_datetime: i64,
pub usage_expire_datetime: i64,
pub usage_count_limit: i32,
pub caller_nonce: bool,
pub unlocked_device_required: bool,
pub no_auth_required: bool,
} }
pub struct GeneratedKeyPair { pub struct GeneratedKeyPair {