Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 | ||
|
|
ca3978888e | ||
|
|
023d7f929d | ||
|
|
bd40f4b950 | ||
|
|
23696d2f61 | ||
|
|
dfacb34cf9 | ||
|
|
06d9db443c | ||
|
|
7e87766493 | ||
|
|
f8bfa0dfd8 | ||
|
|
90ff59e0aa | ||
|
|
8bdf0d59fa | ||
|
|
6ab09f4889 | ||
|
|
f4559bcd19 | ||
|
|
3b5043a1bb | ||
|
|
8001a8678a | ||
|
|
d21822eb9d | ||
|
|
095a658996 | ||
|
|
70e8968e44 | ||
|
|
c122ded7bf | ||
|
|
7b510a9915 | ||
|
|
8f63dda31b | ||
|
|
9896df93de | ||
|
|
0280bcf189 | ||
|
|
438a462bdf | ||
|
|
40b08cd648 | ||
|
|
c5ed627f68 | ||
|
|
bee73eb39b | ||
|
|
a0ee77202c | ||
|
|
09d9896228 | ||
|
|
5a599025ad | ||
|
|
f5c2bcc024 | ||
|
|
85e34ed42f | ||
|
|
52b04675e7 | ||
|
|
ba0628c687 | ||
|
|
c11465d660 | ||
|
|
0d5fd44992 | ||
|
|
776a7b2343 | ||
|
|
14786ecce0 | ||
|
|
41b9cc8f10 | ||
|
|
6df266b688 | ||
|
|
32cfcb3ece | ||
|
|
727b32f6b0 | ||
|
|
ac9641ed2a | ||
|
|
c87759c6c3 | ||
|
|
76b18706f1 | ||
|
|
e76f5115e0 | ||
|
|
0725aed094 | ||
|
|
9d61af6ce8 | ||
|
|
7863e8dd17 | ||
|
|
f840eed42b | ||
|
|
4c7f0a09ea | ||
|
|
4971f7b4a5 | ||
|
|
9ea39f0545 | ||
|
|
c332f8ad0d | ||
|
|
d76e6abeed | ||
|
|
142fe7bf13 | ||
|
|
e41a679928 | ||
|
|
209d5c8902 | ||
|
|
3817b37e18 | ||
|
|
53ae250fc7 | ||
|
|
34ad97366e |
+92
-73
@@ -3,16 +3,10 @@ name: Build
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/**'
|
||||
paths-ignore: [ '**.md' ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/**'
|
||||
paths-ignore: [ '**.md' ]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
@@ -22,18 +16,9 @@ concurrency:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
attestations: write
|
||||
contents: read
|
||||
|
||||
outputs:
|
||||
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
|
||||
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
|
||||
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: "recursive"
|
||||
fetch-depth: 0
|
||||
@@ -45,6 +30,25 @@ jobs:
|
||||
java-version: 21
|
||||
cache: 'gradle'
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android
|
||||
|
||||
- name: Cache Rust artifacts
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
~/.cargo/bin/cargo-ndk
|
||||
native-certgen/target
|
||||
key: rust-${{ runner.os }}-${{ hashFiles('native-certgen/Cargo.lock') }}
|
||||
restore-keys: rust-${{ runner.os }}-
|
||||
|
||||
- name: Install cargo-ndk
|
||||
run: command -v cargo-ndk || cargo install cargo-ndk
|
||||
|
||||
- name: Set up ccache
|
||||
uses: hendrikmuhs/ccache-action@v1.2
|
||||
with:
|
||||
@@ -60,73 +64,88 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
|
||||
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
- name: Prepare artifact
|
||||
if: success()
|
||||
id: prepareArtifact
|
||||
- name: Read version
|
||||
id: ver
|
||||
run: |
|
||||
set -e
|
||||
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
|
||||
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
|
||||
|
||||
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
|
||||
echo "Error: Could not find release or debug files in out/"
|
||||
echo "Contents of out/ directory:"
|
||||
ls -la out/ || echo "out/ directory does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract names
|
||||
RELEASE_NAME=$(basename "$RELEASE_FILE" .zip)
|
||||
DEBUG_NAME=$(basename "$DEBUG_FILE" .zip)
|
||||
|
||||
echo "releaseName=$RELEASE_NAME" >> $GITHUB_OUTPUT
|
||||
echo "debugName=$DEBUG_NAME" >> $GITHUB_OUTPUT
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mkdir -p module-release module-debug
|
||||
unzip -q "$RELEASE_FILE" -d module-release
|
||||
unzip -q "$DEBUG_FILE" -d module-debug
|
||||
echo " Release: $RELEASE_NAME"
|
||||
echo " Debug: $DEBUG_NAME"
|
||||
- name: List build artifacts
|
||||
run: |
|
||||
echo "Release: $(ls out/*Release*.zip | head -1) ($(du -h out/*Release*.zip | head -1 | cut -f1))"
|
||||
echo "Debug: $(ls out/*Debug*.zip | head -1) ($(du -h out/*Debug*.zip | head -1 | cut -f1))"
|
||||
|
||||
- name: Upload release
|
||||
if: success()
|
||||
id: release
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepareArtifact.outputs.releaseName }}
|
||||
path: "./module-release/*"
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: out/TEESimulator-RS-*-Release.zip
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload debug
|
||||
if: success()
|
||||
id: debug
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepareArtifact.outputs.debugName }}
|
||||
path: "./module-debug/*"
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: out/TEESimulator-RS-*-Debug.zip
|
||||
retention-days: 7
|
||||
compression-level: 6
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload release mappings
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-mappings-${{ github.run_number }}
|
||||
path: "./app/build/outputs/mapping/release"
|
||||
name: release-mappings
|
||||
path: app/build/outputs/mapping/release
|
||||
retention-days: 30
|
||||
compression-level: 9
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
release:
|
||||
needs: build
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read version
|
||||
id: ver
|
||||
run: |
|
||||
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "${{ job.status }}" == "success" ]]; then
|
||||
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: zips
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: zips
|
||||
|
||||
- name: Extract changelog
|
||||
run: |
|
||||
ver="${VER#v}"
|
||||
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
|
||||
cat /tmp/notes.md
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
gh release delete "$VER" --yes 2>/dev/null || true
|
||||
RELEASE=$(ls zips/*Release*.zip | head -1)
|
||||
DEBUG=$(ls zips/*Debug*.zip | head -1)
|
||||
gh release create "$VER" \
|
||||
--title "$VER" \
|
||||
--latest \
|
||||
--notes-file /tmp/notes.md \
|
||||
"$RELEASE" \
|
||||
"$DEBUG"
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
out
|
||||
.gradle
|
||||
.kotlin
|
||||
app/build
|
||||
build
|
||||
native-certgen/target
|
||||
app/src/main/jniLibs
|
||||
|
||||
@@ -1,32 +1,143 @@
|
||||
# TEESimulator – A Full TEE Emulation Framework
|
||||
<p align="center">
|
||||
<h1 align="center">🔐 TEESimulator</h1>
|
||||
<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">
|
||||
<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>
|
||||
<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?style=for-the-badge&logo=android" alt="Android 10+">
|
||||
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||
</p>
|
||||
</p>
|
||||
|
||||
**TEESimulator** is a system module designed to create a complete, software-based simulation of a hardware-backed Trusted Execution Environment ([TEE](https://source.android.com/docs/security/features/trusty)) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation).
|
||||
---
|
||||
|
||||
The project's goal is to move beyond simple certificate patching and build a robust framework that can create and manage virtual, self-consistent cryptographic keys.
|
||||
> [!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.
|
||||
|
||||
## ✨ Core Principles
|
||||
---
|
||||
|
||||
* **Bypass Hardware-Backed Attestation:** The primary goal of this project is to defeat Key Attestation, a security mechanism that allows apps to verify that they are running on a secure, unmodified device. This module provides the tools to bypass these checks on rooted or modified devices.
|
||||
* **Stateful Emulation:** Instead of patching responses from the real TEE, the ultimate goal is to create and manage virtual keys entirely in a simulated software environment. Any request concerning a virtual key will be handled by the simulator, ensuring perfect consistency without ever touching the real hardware.
|
||||
* **Architectural Interception:** By hooking low-level Binder IPC calls to the Keystore, the framework can transparently redirect requests for virtual keys to the software-based simulator, while allowing requests for real keys to pass through to the hardware TEE.
|
||||
* **100% FOSS:** Licensed under GPLv3, ensuring it stays free, auditable, and compliant with open-source laws.
|
||||
## 🧬 What is TEESimulator?
|
||||
|
||||
## 📱 Requirements
|
||||
- Android 10 or above
|
||||
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.
|
||||
|
||||
## 📦 Installation & Configuration
|
||||
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
|
||||
|
||||
1. Flash this module via (Magisk / KernelSU / APatch) and reboot. It will replace [TrickyStore](https://github.com/5ec1cff/TrickyStore), [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS) and their forks.
|
||||
2. (Optional) Place a hardware-backed `keybox.xml` at `/data/adb/tricky_store/keybox.xml`. This provides the cryptographic "root of trust" for the simulator.
|
||||
3. (Optional) Customize target packages in `/data/adb/tricky_store/target.txt`.
|
||||
4. (Optional) Customize the simulated security patch level in `/data/adb/tricky_store/security_patch.txt`.
|
||||
5. Enjoy!
|
||||
> **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.
|
||||
|
||||
**All configuration files are monitored and will take effect immediately upon saving.**
|
||||
---
|
||||
|
||||
## 🔥 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]
|
||||
> 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.
|
||||
|
||||
**You need:**
|
||||
1. Android 10 or above
|
||||
2. A supported root manager (KernelSU, Magisk, or APatch)
|
||||
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
|
||||
|
||||
---
|
||||
|
||||
## 📱 Compatibility
|
||||
|
||||
### Root Managers
|
||||
|
||||
| Manager | Status | Notes |
|
||||
|---|---|---|
|
||||
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
|
||||
| Magisk | ✅ Supported | Standard module install |
|
||||
| APatch | ✅ Supported | Standard module install |
|
||||
|
||||
### Tested Devices
|
||||
|
||||
| Device | Android | TEE | Status |
|
||||
|---|---|---|---|
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases)
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🔨 Building from Source
|
||||
|
||||
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
|
||||
|
||||
**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 for the simulator. It contains a private key and a valid, hardware-backed certificate chain from a real device. The simulator uses this to sign the virtual certificates it generates, making them appear legitimate to verifiers.
|
||||
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"?>
|
||||
@@ -40,101 +151,106 @@ This file provides the master cryptographic identity for the simulator. It conta
|
||||
</AndroidAttestation>
|
||||
```
|
||||
|
||||
### Mode and Keybox Configuration (`target.txt`)
|
||||
### Target Packages (`target.txt`)
|
||||
|
||||
TEESimulator currently operates in two primary modes as it transitions towards full emulation.
|
||||
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
|
||||
Controls which apps get intercepted and what simulation mode to use.
|
||||
|
||||
#### Mode Suffixes
|
||||
|
||||
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation.
|
||||
* **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified.
|
||||
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
|
||||
* **`!` → 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 Configuration
|
||||
#### Multi-Keybox
|
||||
|
||||
You can specify different keybox files for different groups of applications. This is done by adding a line with the filename in square brackets (e.g., [demo_keybox.xml]).
|
||||
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`.
|
||||
|
||||
All applications listed after this line will use the specified keybox file, until a new keybox is declared. Applications listed before any custom keybox declaration will use the default `keybox.xml`.
|
||||
|
||||
For example:
|
||||
```
|
||||
# These two apps will use the default /data/adb/tricky_store/keybox.xml
|
||||
# Default keybox
|
||||
com.google.android.gms!
|
||||
io.github.vvb2060.keyattestation?
|
||||
|
||||
# Switch to a different keybox for the following apps.
|
||||
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
|
||||
# Switch to a different keybox for the following apps
|
||||
[aosp_keybox.xml]
|
||||
com.google.android.gsf
|
||||
|
||||
# Switch again to another keybox.
|
||||
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
|
||||
# Another keybox
|
||||
[demo_keybox.xml]
|
||||
org.matrix.demo
|
||||
```
|
||||
|
||||
### Security Patch Level (`security_patch.txt`)
|
||||
|
||||
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
|
||||
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties.
|
||||
|
||||
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
|
||||
#### Global and Per-Package
|
||||
|
||||
#### Global and Per-Package Configuration
|
||||
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
|
||||
|
||||
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
|
||||
#### Keys
|
||||
|
||||
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
|
||||
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
|
||||
|
||||
#### Configuration Keys and Values
|
||||
|
||||
You can specify the patch level for the following components using a `key=value` format:
|
||||
|
||||
* `system`: The main OS patch level.
|
||||
* `vendor`: The vendor patch level.
|
||||
* `boot`: The boot/kernel patch level.
|
||||
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
|
||||
|
||||
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
|
||||
| Key | Scope |
|
||||
|---|---|
|
||||
| `system` | OS patch level |
|
||||
| `vendor` | Vendor patch level |
|
||||
| `boot` | Boot/kernel patch level |
|
||||
| `all` | Shorthand — sets all three at once |
|
||||
|
||||
#### Special Keywords
|
||||
|
||||
In addition to static dates, several special keywords provide advanced, dynamic control:
|
||||
| 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) |
|
||||
|
||||
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
|
||||
|
||||
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
|
||||
|
||||
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
|
||||
|
||||
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
|
||||
|
||||
#### Example Configuration
|
||||
|
||||
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
|
||||
#### Example
|
||||
|
||||
```
|
||||
# --- Global Configuration ---
|
||||
# This is the default for all apps unless specified otherwise.
|
||||
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
|
||||
# - Use the device's real vendor patch level.
|
||||
# - Do not report a boot patch level at all.
|
||||
# Global — default for all apps
|
||||
system=YYYY-MM-05
|
||||
vendor=device_default
|
||||
boot=no
|
||||
|
||||
# --- Per-Package Override for Google Play Services ---
|
||||
# This app will report an older, specific date for its system patch.
|
||||
# It will inherit the global settings for vendor (device_default) and boot (no).
|
||||
# Override for GMS
|
||||
[com.google.android.gms]
|
||||
system=2024-10-01
|
||||
|
||||
# --- Per-Package Override for a Demo App ---
|
||||
# This app gets a completely custom configuration.
|
||||
# Custom config for a demo app
|
||||
[org.matrix.demo]
|
||||
# Set a base date for all patch levels...
|
||||
all=2025-09-15
|
||||
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
|
||||
boot=device_default
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💬 Community
|
||||
|
||||
<p align="center">
|
||||
<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">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture
|
||||
- **[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
|
||||
|
||||
---
|
||||
|
||||
## 📄 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>
|
||||
|
||||
+47
-17
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
|
||||
|
||||
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
|
||||
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
|
||||
val verName = "v3.2"
|
||||
val verName = "v4.8"
|
||||
|
||||
android {
|
||||
namespace = "org.matrix.TEESimulator"
|
||||
@@ -71,6 +71,35 @@ dependencies {
|
||||
implementation(libs.bcpkix)
|
||||
}
|
||||
|
||||
// --- Rust native cert gen build task ---
|
||||
val buildRustCertgen by tasks.registering(Exec::class) {
|
||||
group = "TEESimulator-RS Native Build"
|
||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||
|
||||
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||
|
||||
commandLine(
|
||||
"cargo", "ndk",
|
||||
"-t", "arm64-v8a",
|
||||
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
|
||||
"build", "--release"
|
||||
)
|
||||
|
||||
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
|
||||
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
|
||||
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
|
||||
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
||||
|
||||
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
||||
}
|
||||
|
||||
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
||||
tasks.configureEach {
|
||||
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
|
||||
dependsOn(buildRustCertgen)
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().all()) { variant ->
|
||||
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
||||
@@ -79,21 +108,22 @@ androidComponents {
|
||||
// --- Define output locations and file names ---
|
||||
// Stage all files in a temporary directory inside 'build' before zipping
|
||||
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
|
||||
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
|
||||
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
|
||||
|
||||
// Task 1: Prepare all module files in the temporary build directory.
|
||||
// Using Sync ensures that stale files from previous runs are removed.
|
||||
val prepareModuleFilesTask =
|
||||
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Prepares all files for the ${variant.name} module zip."
|
||||
|
||||
if (isDebug) {
|
||||
dependsOn("package${capitalized}")
|
||||
} else {
|
||||
dependsOn("minify${capitalized}WithR8")
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
}
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
dependsOn(buildRustCertgen)
|
||||
|
||||
if (isDebug) {
|
||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||
@@ -110,13 +140,14 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
from(
|
||||
project.layout.buildDirectory.dir(
|
||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||
)
|
||||
) {
|
||||
into("lib") // Place them in the 'lib' subfolder of the staging directory.
|
||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so")
|
||||
val nativeLibsDir = if (isDebug) {
|
||||
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||
} else {
|
||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||
}
|
||||
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
||||
into("lib")
|
||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||
}
|
||||
|
||||
// Now, copy and process the files from 'module' directory.
|
||||
@@ -131,8 +162,7 @@ androidComponents {
|
||||
// Use expand() for simple key-value replacement.
|
||||
expand(
|
||||
"REPLACEMEVERCODE" to gitCommitCount.toString(),
|
||||
"REPLACEMEVER" to
|
||||
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
|
||||
"REPLACEMEVER" to "$verName-$gitCommitCount",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,7 +173,7 @@ androidComponents {
|
||||
// Task 2: Zip the prepared files from the temporary directory.
|
||||
val zipTask =
|
||||
tasks.register<Zip>("zip${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Creates the flashable zip for the ${variant.name} module."
|
||||
dependsOn(prepareModuleFilesTask)
|
||||
|
||||
@@ -156,7 +186,7 @@ androidComponents {
|
||||
fun createInstallTasks(rootProvider: String, installCli: String) {
|
||||
val pushTask =
|
||||
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description =
|
||||
"Pushes the ${variant.name} module to the device for $rootProvider."
|
||||
dependsOn(zipTask)
|
||||
@@ -170,7 +200,7 @@ androidComponents {
|
||||
|
||||
val installTask =
|
||||
tasks.register<Exec>("install${rootProvider}${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider."
|
||||
dependsOn(pushTask)
|
||||
commandLine(
|
||||
@@ -183,7 +213,7 @@ androidComponents {
|
||||
}
|
||||
|
||||
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider and reboots."
|
||||
dependsOn(installTask)
|
||||
commandLine("adb", "reboot")
|
||||
|
||||
Vendored
+6
@@ -7,3 +7,9 @@
|
||||
-keepclasseswithmembers class org.matrix.TEESimulator.App {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class org.matrix.TEESimulator.pki.NativeCertGen {
|
||||
native <methods>;
|
||||
*;
|
||||
}
|
||||
-keep class org.matrix.TEESimulator.pki.CertGenConfig { *; }
|
||||
|
||||
@@ -358,6 +358,12 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
||||
return;
|
||||
|
||||
// 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.
|
||||
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
||||
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||
return;
|
||||
|
||||
bool hijack = false;
|
||||
ThreadTransactionInfo info;
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/resource.h>
|
||||
#include <signal.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
static volatile sig_atomic_t should_exit = 0;
|
||||
|
||||
@@ -27,7 +29,12 @@ int main(int argc, char *argv[]) {
|
||||
const char *daemon_path = argv[1];
|
||||
char **daemon_argv = &argv[1];
|
||||
|
||||
int backoff_ms = 500;
|
||||
|
||||
while (!should_exit) {
|
||||
struct timespec child_start;
|
||||
clock_gettime(CLOCK_MONOTONIC, &child_start);
|
||||
|
||||
pid_t pid = fork();
|
||||
|
||||
if (pid < 0) {
|
||||
@@ -39,6 +46,7 @@ int main(int argc, char *argv[]) {
|
||||
if (pid == 0) {
|
||||
// Child: become the daemon
|
||||
prctl(PR_SET_PDEATHSIG, SIGKILL); // Die if parent dies
|
||||
setpriority(PRIO_PROCESS, 0, 10); // lower CPU priority than foreground
|
||||
execv(daemon_path, daemon_argv);
|
||||
perror("execv failed");
|
||||
_exit(127);
|
||||
@@ -50,7 +58,18 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
if (should_exit) break;
|
||||
|
||||
// Instant restart - no delay
|
||||
// Exponential backoff on rapid crashes, reset if child was stable
|
||||
struct timespec now;
|
||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||
long lived_ms = (now.tv_sec - child_start.tv_sec) * 1000 +
|
||||
(now.tv_nsec - child_start.tv_nsec) / 1000000;
|
||||
|
||||
if (lived_ms > 30000) {
|
||||
backoff_ms = 500;
|
||||
} else {
|
||||
usleep(backoff_ms * 1000);
|
||||
if (backoff_ms < 30000) backoff_ms *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -13,8 +13,8 @@ import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor
|
||||
import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor
|
||||
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Main application object for TEESimulator. This object manages the application's lifecycle,
|
||||
@@ -23,8 +23,6 @@ import kotlin.system.exitProcess
|
||||
object App {
|
||||
// The delay in milliseconds before retrying to initialize the interceptor.
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
// The sleep duration in milliseconds for the main service loop to keep the process alive.
|
||||
private const val SERVICE_SLEEP_MS = 1000000L
|
||||
|
||||
/**
|
||||
* The main entry point of the TEESimulator application.
|
||||
@@ -33,11 +31,6 @@ object App {
|
||||
*/
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
SystemLogger.error("Uncaught exception on thread '${thread.name}'. Exiting for restart.", throwable)
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
SystemLogger.info("Welcome to TEESimulator!")
|
||||
|
||||
try {
|
||||
@@ -57,6 +50,8 @@ object App {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
||||
|
||||
// This starts the message queue processing. It blocks here indefinitely
|
||||
// processing messages until Looper.myLooper().quit() is called.
|
||||
Looper.loop()
|
||||
|
||||
@@ -182,11 +182,40 @@ object AttestationBuilder {
|
||||
AttestationConstants.TAG_DIGEST,
|
||||
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
),
|
||||
)
|
||||
|
||||
if (params.ecCurve != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_EC_CURVE,
|
||||
ASN1Integer(params.ecCurve.toLong()),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.padding.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_PADDING,
|
||||
DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rsaPublicExponent != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_PUBLIC_EXPONENT,
|
||||
ASN1Integer(params.rsaPublicExponent.toLong()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
@@ -199,6 +228,7 @@ object AttestationBuilder {
|
||||
buildRootOfTrust(null),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||
@@ -345,7 +375,7 @@ object AttestationBuilder {
|
||||
* retrieved.
|
||||
*/
|
||||
@Throws(Throwable::class)
|
||||
private fun createApplicationId(uid: Int): DEROctetString {
|
||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||
val pm =
|
||||
ConfigurationManager.getPackageManager()
|
||||
?: throw IllegalStateException("PackageManager not found!")
|
||||
|
||||
@@ -89,5 +89,5 @@ object AttestationConstants {
|
||||
|
||||
// --- Other Constants ---
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128
|
||||
}
|
||||
|
||||
@@ -249,6 +249,10 @@ object DeviceAttestationService {
|
||||
verifiedBootKey = null
|
||||
}
|
||||
|
||||
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
|
||||
verifiedBootHash = null
|
||||
}
|
||||
|
||||
SystemLogger.info(
|
||||
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.hardware.security.keymint.*
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import java.math.BigInteger
|
||||
import java.util.Date
|
||||
import javax.security.auth.x500.X500Principal
|
||||
@@ -18,8 +19,9 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
data class KeyMintAttestation(
|
||||
val keySize: Int,
|
||||
val algorithm: Int,
|
||||
val ecCurve: Int,
|
||||
val ecCurve: Int?,
|
||||
val ecCurveName: String,
|
||||
val origin: Int?,
|
||||
val blockMode: List<Int>,
|
||||
val padding: List<Int>,
|
||||
val purpose: List<Int>,
|
||||
@@ -51,9 +53,12 @@ data class KeyMintAttestation(
|
||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||
ecCurveName = params.deriveEcCurveName(),
|
||||
|
||||
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||
origin = params.findOrigin(Tag.ORIGIN),
|
||||
|
||||
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
||||
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||
|
||||
@@ -99,6 +104,10 @@ data class KeyMintAttestation(
|
||||
// Log all parsed parameters for debugging purposes.
|
||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||
}
|
||||
|
||||
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
|
||||
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||
}
|
||||
|
||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||
@@ -115,6 +124,10 @@ private fun Array<KeyParameter>.findAlgorithm(tag: Int): Int? =
|
||||
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
|
||||
this.find { it.tag == tag }?.value?.ecCurve
|
||||
|
||||
/** Maps to AOSP field = Origin */
|
||||
private fun Array<KeyParameter>.findOrigin(tag: Int): Int? =
|
||||
this.find { it.tag == tag }?.value?.origin
|
||||
|
||||
/** Maps to AOSP field = LongInteger */
|
||||
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
|
||||
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
|
||||
|
||||
@@ -327,10 +327,10 @@ object ConfigurationManager {
|
||||
)
|
||||
KeyBoxManager.invalidateCache(path)
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
// Patched chains are stale; generated keys survive rotation
|
||||
// Clear cached keys possibly containing old certificates
|
||||
org.matrix.TEESimulator.interception.keystore.shim
|
||||
.KeyMintSecurityLevelInterceptor
|
||||
.invalidatePatchedChains("keybox change: $path")
|
||||
.clearAllGeneratedKeys("updating $file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -12,6 +12,18 @@ data class KeyIdentifier(val uid: Int, val alias: String)
|
||||
/** A collection of utility functions to support binder interception. */
|
||||
object InterceptorUtils {
|
||||
|
||||
private const val EX_SERVICE_SPECIFIC = -8
|
||||
|
||||
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeInt(EX_SERVICE_SPECIFIC)
|
||||
writeString(null)
|
||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||
writeInt(errorCode)
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses reflection to get the integer transaction code for a given method name from a Stub
|
||||
* class. This is necessary for older Android versions where codes are not public constants.
|
||||
@@ -108,6 +120,8 @@ object InterceptorUtils {
|
||||
|
||||
/** Checks if a reply parcel contains an exception without consuming it. */
|
||||
fun hasException(reply: Parcel): Boolean {
|
||||
return runCatching { reply.readException() }.exceptionOrNull() != null
|
||||
val exception = runCatching { reply.readException() }.exceptionOrNull()
|
||||
if (exception != null) reply.setDataPosition(0)
|
||||
return exception != null
|
||||
}
|
||||
}
|
||||
|
||||
+151
-61
@@ -1,28 +1,38 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||
|
||||
/**
|
||||
* Interceptor for the `IKeystoreService` on Android S (API 31) and newer.
|
||||
*
|
||||
* This version of Keystore delegates most cryptographic operations to `IKeystoreSecurityLevel`
|
||||
* sub-services (for TEE, StrongBox, etc.). This interceptor's main role is to set up interceptors
|
||||
* for those sub-services and to patch certificate chains on their way out.
|
||||
*/
|
||||
@SuppressLint("BlockedPrivateApi")
|
||||
object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
private val stubBinderClass = IKeystoreService.Stub::class.java
|
||||
|
||||
// Transaction codes for the IKeystoreService interface methods we are interested in.
|
||||
private val GET_KEY_ENTRY_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getKeyEntry")
|
||||
private val DELETE_KEY_TRANSACTION =
|
||||
@@ -45,16 +55,24 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||
}
|
||||
|
||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||
|
||||
/**
|
||||
* This method is called once the main service is hooked. It proceeds to find and hook the
|
||||
* security level sub-services (e.g., TEE, StrongBox).
|
||||
*/
|
||||
override fun onInterceptorReady(service: IBinder, backdoor: IBinder) {
|
||||
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
||||
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
||||
}
|
||||
|
||||
private fun setupSecurityLevelInterceptors(service: IKeystoreService, backdoor: IBinder) {
|
||||
// Attempt to get and intercept the TEE security level service.
|
||||
runCatching {
|
||||
service.getSecurityLevel(SecurityLevel.TRUSTED_ENVIRONMENT)?.let { tee ->
|
||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||
@@ -66,6 +84,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
||||
|
||||
// Attempt to get and intercept the StrongBox security level service.
|
||||
runCatching {
|
||||
service.getSecurityLevel(SecurityLevel.STRONGBOX)?.let { strongbox ->
|
||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||
@@ -88,10 +107,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
|
||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||
val isGMS = packages.contains("com.google.android.gms")
|
||||
|
||||
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||
@@ -126,12 +149,21 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
if (descriptor.alias != null) {
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
} else {
|
||||
SystemLogger.info(
|
||||
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
|
||||
)
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
if (wasSoftwareKey) {
|
||||
deletedSoftwareKeys.add(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
@@ -140,9 +172,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val response =
|
||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
?: return TransactionResult.Continue
|
||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
if (response == null) {
|
||||
if (deletedSoftwareKeys.remove(keyId)) {
|
||||
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||
}
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||
@@ -162,6 +199,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
}
|
||||
|
||||
// Let most calls go through to the real service.
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
@@ -195,73 +233,125 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
} else if (code == GET_KEY_ENTRY_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val keyDescriptor =
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.SkipTransaction
|
||||
|
||||
logTransaction(
|
||||
txId,
|
||||
"post-${transactionNames[code]!!} ${keyDescriptor.alias}",
|
||||
callingUid,
|
||||
callingPid,
|
||||
)
|
||||
|
||||
if (!ConfigurationManager.shouldPatch(callingUid))
|
||||
return TransactionResult.SkipTransaction
|
||||
|
||||
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}")
|
||||
return try {
|
||||
val response =
|
||||
reply.readTypedObject(KeyEntryResponse.CREATOR)
|
||||
?: return TransactionResult.SkipTransaction
|
||||
reply.setDataPosition(0) // Reset for potential reuse.
|
||||
|
||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||
val authorizations = response.metadata?.authorizations
|
||||
val origin =
|
||||
authorizations
|
||||
?.find { it.keyParameter.tag == Tag.ORIGIN }
|
||||
?.let { it.keyParameter.value.origin }
|
||||
|
||||
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
|
||||
runCatching {
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
if (retainedChain == null) {
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||
|
||||
val authorizations = response.metadata.authorizations
|
||||
val parsedParameters =
|
||||
KeyMintAttestation(
|
||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||
)
|
||||
|
||||
if (parsedParameters.isImportKey()) {
|
||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
if (retainedChain == null) {
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
if (parsedParameters.isAttestKey()) {
|
||||
SystemLogger.warning(
|
||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||
)
|
||||
val keyData =
|
||||
CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid,
|
||||
keyId.alias,
|
||||
null,
|
||||
parsedParameters,
|
||||
response.metadata.keySecurityLevel,
|
||||
) ?: throw Exception("Failed to create overriding attest key pair.")
|
||||
|
||||
CertificateHelper.updateCertificateChain(
|
||||
response.metadata,
|
||||
keyData.second.toTypedArray(),
|
||||
)
|
||||
.getOrThrow()
|
||||
|
||||
keyDescriptor.nspace = SecureRandom().nextLong()
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||
keyData.first,
|
||||
keyDescriptor.nspace,
|
||||
response,
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||
|
||||
GeneratedKeyPersistence.save(
|
||||
keyId = keyId,
|
||||
keyPair = keyData.first,
|
||||
nspace = keyDescriptor.nspace,
|
||||
securityLevel = response.metadata.keySecurityLevel,
|
||||
certChain = keyData.second,
|
||||
algorithm = parsedParameters.algorithm,
|
||||
keySize = parsedParameters.keySize,
|
||||
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||
purposes = parsedParameters.purpose,
|
||||
digests = parsedParameters.digest,
|
||||
isAttestationKey = true,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||
|
||||
if (originalChain == null || originalChain.size < 2) {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||
)
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||
|
||||
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
|
||||
val finalChain: Array<Certificate>
|
||||
if (cachedChain != null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
|
||||
)
|
||||
finalChain = cachedChain
|
||||
} else {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
||||
)
|
||||
finalChain =
|
||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
.getOrThrow()
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
if (originalChain == null || originalChain.size < 2) {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||
.onFailure {
|
||||
SystemLogger.error(
|
||||
"[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
|
||||
it,
|
||||
)
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
|
||||
val finalChain: Array<Certificate>
|
||||
if (cachedChain != null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
|
||||
)
|
||||
finalChain = cachedChain
|
||||
} else {
|
||||
// Live patch fallback for keys created before simulator started
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
|
||||
)
|
||||
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
|
||||
|
||||
InterceptorUtils.createTypedObjectReply(response)
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
}
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
+2
-1
@@ -407,8 +407,9 @@ private data class LegacyKeygenParameters(
|
||||
return KeyMintAttestation(
|
||||
keySize = this.keySize,
|
||||
algorithm = this.algorithm,
|
||||
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||
ecCurve = 0,
|
||||
ecCurveName = this.ecCurveName ?: "",
|
||||
origin = null,
|
||||
blockMode = listOf<Int>(),
|
||||
padding = listOf<Int>(),
|
||||
purpose = this.purpose,
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ object ListEntriesHandler {
|
||||
startPastAlias: String?,
|
||||
): List<KeyDescriptor> {
|
||||
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
|
||||
.map { keyId ->
|
||||
KeyDescriptor().apply {
|
||||
this.domain = Domain.APP
|
||||
|
||||
+2
@@ -129,6 +129,7 @@ object GeneratedKeyPersistence {
|
||||
val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias))
|
||||
if (file.exists()) {
|
||||
if (file.delete()) {
|
||||
fileLocks.remove(keyFileName(keyId.uid, keyId.alias))
|
||||
SystemLogger.debug("Deleted persisted key: $keyId")
|
||||
} else {
|
||||
SystemLogger.warning("Failed to delete persisted key file: ${file.name}")
|
||||
@@ -158,6 +159,7 @@ object GeneratedKeyPersistence {
|
||||
if (file.delete()) count++
|
||||
}
|
||||
}
|
||||
fileLocks.clear()
|
||||
SystemLogger.info("Deleted $count persisted key files")
|
||||
}.onFailure { e ->
|
||||
SystemLogger.error("Failed to delete all persisted keys", e)
|
||||
|
||||
+281
-36
@@ -1,13 +1,16 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.EcCurve
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.*
|
||||
import android.util.Pair as AndroidPair
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
@@ -16,7 +19,10 @@ import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
@@ -24,8 +30,13 @@ import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
|
||||
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.CertGenConfig
|
||||
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
import org.matrix.TEESimulator.util.AndroidPermissionUtils
|
||||
|
||||
class KeyMintSecurityLevelInterceptor(
|
||||
private val original: IKeystoreSecurityLevel,
|
||||
@@ -38,6 +49,9 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val response: KeyEntryResponse,
|
||||
)
|
||||
|
||||
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
|
||||
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
|
||||
|
||||
override fun onPreTransact(
|
||||
txId: Long,
|
||||
target: IBinder,
|
||||
@@ -185,35 +199,90 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
|
||||
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
|
||||
val before = ops.size
|
||||
ops.removeIf { it.finalized }
|
||||
val afterClean = ops.size
|
||||
while (ops.size >= maxOps) {
|
||||
val oldest = ops.pollFirst() ?: break
|
||||
if (!oldest.finalized) {
|
||||
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
|
||||
oldest.abort()
|
||||
}
|
||||
}
|
||||
ops.addLast(newOp)
|
||||
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
|
||||
}
|
||||
|
||||
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
|
||||
if (securityLevel != SecurityLevel.STRONGBOX) return null
|
||||
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
|
||||
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
|
||||
timestamps.removeIf { it < cutoff }
|
||||
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
|
||||
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
|
||||
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
|
||||
}
|
||||
timestamps.addLast(System.nanoTime())
|
||||
return null
|
||||
}
|
||||
|
||||
private fun handleCreateOperation(
|
||||
txId: Long,
|
||||
callingUid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||
|
||||
// An operation must use the KEY_ID domain.
|
||||
if (keyDescriptor.domain != Domain.KEY_ID) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
|
||||
|
||||
// Android framework calls createOperation with domain=APP+alias;
|
||||
// keystore2 internally resolves to KEY_ID — but software keys never
|
||||
// reach keystore2's database, so we must handle both lookup paths.
|
||||
val generatedKeyInfo = when (keyDescriptor.domain) {
|
||||
Domain.APP -> {
|
||||
val alias = keyDescriptor.alias ?: run {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
}
|
||||
Domain.KEY_ID -> {
|
||||
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: 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
|
||||
}
|
||||
}
|
||||
|
||||
val nspace = keyDescriptor.nspace
|
||||
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
||||
)
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
|
||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
|
||||
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val parsedParams = KeyMintAttestation(params)
|
||||
val parsedParams = KeyMintAttestation(params).let { p ->
|
||||
if (p.algorithm != 0) p
|
||||
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> Algorithm.EC
|
||||
"RSA" -> Algorithm.RSA
|
||||
else -> p.algorithm
|
||||
})
|
||||
}
|
||||
|
||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
|
||||
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
|
||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency)
|
||||
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
|
||||
pruneOpsForUid(callingUid, softwareOperation, maxOps)
|
||||
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
||||
|
||||
val response =
|
||||
@@ -241,10 +310,52 @@ class KeyMintSecurityLevelInterceptor(
|
||||
)
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val parsedParams = KeyMintAttestation(params)
|
||||
|
||||
val challenge = parsedParams.attestationChallenge
|
||||
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})")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
|
||||
}
|
||||
|
||||
if (params.any { it.tag == Tag.CREATION_DATETIME }) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params")
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
|
||||
}
|
||||
|
||||
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||
}
|
||||
|
||||
val hasDeviceIdAttestation = params.any {
|
||||
it.tag == Tag.ATTESTATION_ID_IMEI ||
|
||||
it.tag == Tag.ATTESTATION_ID_MEID ||
|
||||
it.tag == Tag.ATTESTATION_ID_SERIAL ||
|
||||
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
|
||||
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
|
||||
}
|
||||
|
||||
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||
}
|
||||
|
||||
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
|
||||
parsedParams.algorithm == Algorithm.HMAC ||
|
||||
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)) {
|
||||
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
val isAttestKeyRequest =
|
||||
parsedParams.purpose.size == 1 &&
|
||||
parsedParams.purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
val isAttestKeyRequest = parsedParams.isAttestKey()
|
||||
|
||||
val needsSoftwareGeneration =
|
||||
ConfigurationManager.shouldGenerate(callingUid) ||
|
||||
@@ -293,15 +404,23 @@ class KeyMintSecurityLevelInterceptor(
|
||||
keyId: KeyIdentifier,
|
||||
isAttestKeyRequest: Boolean,
|
||||
): TransactionResult {
|
||||
val startNs = System.nanoTime()
|
||||
keyDescriptor.nspace = secureRandom.nextLong()
|
||||
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
||||
|
||||
val keyData = CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
|
||||
) ?: throw Exception("CertificateGenerator failed to create key pair.")
|
||||
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
|
||||
generateAttestedKeyPairNative(callingUid, parsedParams)
|
||||
?: CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
|
||||
)
|
||||
} else {
|
||||
CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
|
||||
)
|
||||
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
||||
|
||||
cleanupKeyData(keyId)
|
||||
val response = buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
|
||||
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||
|
||||
@@ -313,26 +432,108 @@ class KeyMintSecurityLevelInterceptor(
|
||||
certChain = keyData.second.toList(),
|
||||
algorithm = parsedParams.algorithm,
|
||||
keySize = parsedParams.keySize,
|
||||
ecCurve = parsedParams.ecCurve,
|
||||
ecCurve = parsedParams.ecCurve ?: 0,
|
||||
purposes = parsedParams.purpose,
|
||||
digests = parsedParams.digest,
|
||||
isAttestationKey = isAttestKeyRequest,
|
||||
)
|
||||
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS
|
||||
val delayMs = floor - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||
}
|
||||
|
||||
private fun generateAttestedKeyPairNative(
|
||||
callingUid: Int,
|
||||
params: KeyMintAttestation,
|
||||
): AndroidPair<KeyPair, List<Certificate>>? {
|
||||
return runCatching {
|
||||
val algorithmName = when (params.algorithm) {
|
||||
Algorithm.EC -> "EC"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else -> return null
|
||||
}
|
||||
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(callingUid)
|
||||
val keybox = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) ?: return null
|
||||
|
||||
val keyboxPrivateKeyBytes = keybox.keyPair.private.encoded
|
||||
val keyboxCertChainBytes = keybox.certificates
|
||||
.map { it.encoded }
|
||||
.fold(ByteArray(0)) { acc, der -> acc + der }
|
||||
|
||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||
val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel)
|
||||
val appId = AttestationBuilder.createApplicationId(callingUid)
|
||||
|
||||
val config = CertGenConfig(
|
||||
algorithm = params.algorithm,
|
||||
keySize = params.keySize,
|
||||
ecCurve = params.ecCurve ?: 0,
|
||||
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
|
||||
attestationChallenge = params.attestationChallenge,
|
||||
purposes = params.purpose.toIntArray(),
|
||||
digests = params.digest.toIntArray(),
|
||||
certSerial = params.certificateSerial?.toByteArray(),
|
||||
certSubject = params.certificateSubject?.encoded,
|
||||
certNotBefore = params.certificateNotBefore?.time ?: -1L,
|
||||
certNotAfter = params.certificateNotAfter?.time ?: -1L,
|
||||
keyboxPrivateKey = keyboxPrivateKeyBytes,
|
||||
keyboxCertChain = keyboxCertChainBytes,
|
||||
securityLevel = securityLevel,
|
||||
attestVersion = attestVersion,
|
||||
keymasterVersion = keymasterVersion,
|
||||
osVersion = AndroidDeviceUtils.osVersion,
|
||||
osPatchLevel = AndroidDeviceUtils.getPatchLevel(callingUid),
|
||||
vendorPatchLevel = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid),
|
||||
bootPatchLevel = AndroidDeviceUtils.getBootPatchLevelLong(callingUid),
|
||||
bootKey = AndroidDeviceUtils.bootKey,
|
||||
bootHash = AndroidDeviceUtils.bootHash,
|
||||
creationDatetime = System.currentTimeMillis(),
|
||||
attestationApplicationId = appId.octets,
|
||||
moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null,
|
||||
idBrand = params.brand,
|
||||
idDevice = params.device,
|
||||
idProduct = params.product,
|
||||
idSerial = params.serial,
|
||||
idImei = params.imei,
|
||||
idMeid = params.meid,
|
||||
idManufacturer = params.manufacturer,
|
||||
idModel = params.model,
|
||||
idSecondImei = if (attestVersion >= 300) params.secondImei else null,
|
||||
)
|
||||
|
||||
val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null
|
||||
val (keyPair, certs) = NativeCertGen.parseNativeResult(resultBytes)
|
||||
SystemLogger.info("NativeCertGen: generated key pair successfully (${certs.size} certs)")
|
||||
AndroidPair(keyPair, certs)
|
||||
}.onFailure {
|
||||
SystemLogger.error("NativeCertGen: generation failed, falling back to BouncyCastle", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun buildKeyEntryResponse(
|
||||
callingUid: Int,
|
||||
chain: List<Certificate>,
|
||||
params: KeyMintAttestation,
|
||||
descriptor: KeyDescriptor,
|
||||
): KeyEntryResponse {
|
||||
val normalizedKeyDescriptor =
|
||||
KeyDescriptor().apply {
|
||||
domain = Domain.KEY_ID
|
||||
nspace = descriptor.nspace
|
||||
alias = null
|
||||
blob = null
|
||||
}
|
||||
val metadata =
|
||||
KeyMetadata().apply {
|
||||
keySecurityLevel = securityLevel
|
||||
key = descriptor
|
||||
key = normalizedKeyDescriptor
|
||||
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
|
||||
authorizations = params.toAuthorizations(securityLevel)
|
||||
authorizations = params.toAuthorizations(callingUid, securityLevel)
|
||||
modificationTimeMs = System.currentTimeMillis()
|
||||
}
|
||||
return KeyEntryResponse().apply {
|
||||
this.metadata = metadata
|
||||
@@ -387,6 +588,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
algorithm = record.algorithm,
|
||||
ecCurve = record.ecCurve,
|
||||
ecCurveName = "",
|
||||
origin = null,
|
||||
blockMode = emptyList(),
|
||||
padding = emptyList(),
|
||||
purpose = record.purposes,
|
||||
@@ -408,7 +610,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
secondImei = null,
|
||||
)
|
||||
|
||||
val response = buildKeyEntryResponse(certChain, attestation, descriptor)
|
||||
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
||||
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||
|
||||
@@ -427,15 +629,30 @@ class KeyMintSecurityLevelInterceptor(
|
||||
// Maximum alias length to prevent binder buffer exhaustion (Issue #109)
|
||||
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
||||
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
||||
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
||||
private const val RESPONSE_INVALID_ARGUMENT = 20
|
||||
private const val TEE_LATENCY_FLOOR_MS = 15L
|
||||
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
|
||||
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
|
||||
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
|
||||
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
|
||||
private const val MAX_CONCURRENT_OPS_PER_UID = 15
|
||||
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
|
||||
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) {
|
||||
Algorithm.RSA -> params.keySize <= 2048
|
||||
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
||||
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
||||
|
||||
@@ -444,6 +661,10 @@ class KeyMintSecurityLevelInterceptor(
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -477,9 +698,8 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
|
||||
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
||||
// Caches patched chains to prevent re-generation and signature inconsistencies
|
||||
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||
|
||||
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
||||
@@ -538,7 +758,10 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
|
||||
private fun KeyMintAttestation.toAuthorizations(
|
||||
callingUid: Int,
|
||||
securityLevel: Int,
|
||||
): Array<Authorization> {
|
||||
val authList = mutableListOf<Authorization>()
|
||||
|
||||
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
|
||||
@@ -553,13 +776,35 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
|
||||
}
|
||||
}
|
||||
|
||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||
if (this.ecCurve != null) {
|
||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||
}
|
||||
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
||||
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||
|
||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
|
||||
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||
if (this.rsaPublicExponent != null) {
|
||||
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
|
||||
}
|
||||
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.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
|
||||
|
||||
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch)))
|
||||
}
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch)))
|
||||
}
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
|
||||
}
|
||||
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
|
||||
authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
|
||||
|
||||
return authList.toTypedArray()
|
||||
}
|
||||
|
||||
+89
-26
@@ -6,6 +6,7 @@ import android.hardware.security.keymint.Digest
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.os.RemoteException
|
||||
import android.os.ServiceSpecificException
|
||||
import android.system.keystore2.IKeystoreOperation
|
||||
import java.security.KeyPair
|
||||
import java.security.Signature
|
||||
@@ -17,10 +18,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||
private sealed interface CryptoPrimitive {
|
||||
fun updateAad(aadInput: ByteArray?) {}
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
}
|
||||
|
||||
@@ -34,16 +34,17 @@ private object JcaAlgorithmMapper {
|
||||
Digest.SHA_2_512 -> "SHA512"
|
||||
else -> "NONE"
|
||||
}
|
||||
val keyAlgo =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
)
|
||||
return when (params.algorithm) {
|
||||
Algorithm.EC -> "${digest}withECDSA"
|
||||
Algorithm.RSA -> {
|
||||
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||
@@ -60,16 +61,18 @@ private object JcaAlgorithmMapper {
|
||||
when (params.blockMode.firstOrNull()) {
|
||||
BlockMode.ECB -> "ECB"
|
||||
BlockMode.CBC -> "CBC"
|
||||
BlockMode.CTR -> "CTR"
|
||||
BlockMode.GCM -> "GCM"
|
||||
else -> "ECB" // Default for RSA
|
||||
else -> "ECB"
|
||||
}
|
||||
val padding =
|
||||
when (params.padding.firstOrNull()) {
|
||||
PaddingMode.NONE -> "NoPadding"
|
||||
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||
else -> "NoPadding" // Default for GCM
|
||||
else -> "NoPadding"
|
||||
}
|
||||
return "$keyAlgo/$blockMode/$padding"
|
||||
}
|
||||
@@ -142,17 +145,17 @@ private class CipherPrimitive(
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
||||
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
||||
*/
|
||||
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
||||
// This now holds the specific strategy object (Signer, Verifier, etc.)
|
||||
class SoftwareOperation(
|
||||
private val txId: Long,
|
||||
keyPair: KeyPair,
|
||||
params: KeyMintAttestation,
|
||||
private val latencyFloorMs: Long = 0L,
|
||||
) {
|
||||
private val primitive: CryptoPrimitive
|
||||
@Volatile var finalized = false
|
||||
private set
|
||||
|
||||
init {
|
||||
// The "Strategy" pattern: choose the implementation based on the purpose.
|
||||
// For simplicity, we only consider the first purpose listed.
|
||||
val purpose = params.purpose.firstOrNull()
|
||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||
@@ -168,9 +171,35 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkActive() {
|
||||
if (finalized) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputLength(data: ByteArray?) {
|
||||
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAad(aadInput: ByteArray?) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(aadInput)
|
||||
primitive.updateAad(aadInput)
|
||||
}
|
||||
|
||||
fun update(data: ByteArray?): ByteArray? {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
return primitive.update(data)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
throw e
|
||||
@@ -178,38 +207,72 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
}
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||
val result = primitive.finish(data, signature)
|
||||
if (latencyFloorMs > 0) {
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val delayMs = latencyFloorMs - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
}
|
||||
finalized = true
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||
return result
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||
// Re-throw the exception so the binder can report it to the client.
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
finalized = true
|
||||
primitive.abort()
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
private object KeystoreErrorCodes {
|
||||
val tooMuchData: Int by lazy {
|
||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||
}
|
||||
|
||||
val invalidOperationHandle: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||
}
|
||||
|
||||
private fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||
runCatching {
|
||||
Class.forName(className).getField(fieldName).getInt(null)
|
||||
}.getOrElse {
|
||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
/** The Binder interface for our [SoftwareOperation]. */
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
operation.updateAad(aadInput)
|
||||
}
|
||||
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
return operation.update(input)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ object SystemLogger {
|
||||
* @param message The message to log.
|
||||
*/
|
||||
fun debug(message: String) {
|
||||
if (!isDebugBuild) return
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ object CertificateGenerator {
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
@@ -239,10 +239,10 @@ object CertificateGenerator {
|
||||
)
|
||||
|
||||
val signerAlgorithm =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "SHA256withECDSA"
|
||||
Algorithm.RSA -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||
when (signingKeyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||
"RSA" -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
||||
}
|
||||
val contentSigner =
|
||||
JcaContentSignerBuilder(signerAlgorithm)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.matrix.TEESimulator.pki
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
data class CertGenConfig(
|
||||
val algorithm: Int,
|
||||
val keySize: Int,
|
||||
val ecCurve: Int,
|
||||
val rsaPublicExponent: Long,
|
||||
val attestationChallenge: ByteArray?,
|
||||
val purposes: IntArray,
|
||||
val digests: IntArray,
|
||||
val certSerial: ByteArray?,
|
||||
val certSubject: ByteArray?,
|
||||
val certNotBefore: Long,
|
||||
val certNotAfter: Long,
|
||||
val keyboxPrivateKey: ByteArray,
|
||||
val keyboxCertChain: ByteArray,
|
||||
val securityLevel: Int,
|
||||
val attestVersion: Int,
|
||||
val keymasterVersion: Int,
|
||||
val osVersion: Int,
|
||||
val osPatchLevel: Int,
|
||||
val vendorPatchLevel: Int,
|
||||
val bootPatchLevel: Int,
|
||||
val bootKey: ByteArray,
|
||||
val bootHash: ByteArray,
|
||||
val creationDatetime: Long,
|
||||
val attestationApplicationId: ByteArray,
|
||||
val moduleHash: ByteArray?,
|
||||
val idBrand: ByteArray?,
|
||||
val idDevice: ByteArray?,
|
||||
val idProduct: ByteArray?,
|
||||
val idSerial: ByteArray?,
|
||||
val idImei: ByteArray?,
|
||||
val idMeid: ByteArray?,
|
||||
val idManufacturer: ByteArray?,
|
||||
val idModel: ByteArray?,
|
||||
val idSecondImei: ByteArray?,
|
||||
)
|
||||
|
||||
object NativeCertGen {
|
||||
|
||||
private const val LOG_DIR = "/data/adb/tricky_store/logs"
|
||||
|
||||
@Volatile
|
||||
var isAvailable: Boolean = false
|
||||
private set
|
||||
|
||||
fun initialize(libraryPath: String) {
|
||||
try {
|
||||
System.load(libraryPath)
|
||||
initLogging(false, LOG_DIR)
|
||||
isAvailable = true
|
||||
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
|
||||
}
|
||||
}
|
||||
|
||||
external fun generateAttestedKeyPair(config: CertGenConfig): ByteArray?
|
||||
|
||||
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
|
||||
|
||||
private external fun dumpLogs(): String?
|
||||
|
||||
fun dump(): String? = if (isAvailable) dumpLogs() else null
|
||||
|
||||
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
|
||||
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val pkLen = buf.getInt()
|
||||
if (pkLen < 0 || pkLen > buf.remaining()) {
|
||||
throw IllegalStateException("Invalid private key length: $pkLen")
|
||||
}
|
||||
val pkBytes = ByteArray(pkLen)
|
||||
buf.get(pkBytes)
|
||||
|
||||
val numCerts = buf.getInt()
|
||||
if (numCerts < 0 || numCerts > buf.remaining()) {
|
||||
throw IllegalStateException("Invalid cert count: $numCerts")
|
||||
}
|
||||
val certs = mutableListOf<Certificate>()
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
repeat(numCerts) {
|
||||
val certLen = buf.getInt()
|
||||
if (certLen < 0 || certLen > buf.remaining()) {
|
||||
throw IllegalStateException("Invalid cert length: $certLen")
|
||||
}
|
||||
val certBytes = ByteArray(certLen)
|
||||
buf.get(certBytes)
|
||||
certs.add(certFactory.generateCertificate(ByteArrayInputStream(certBytes)))
|
||||
}
|
||||
|
||||
if (certs.isEmpty()) {
|
||||
throw IllegalStateException("No certificates in native result")
|
||||
}
|
||||
|
||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||
"EC", "ECDSA" -> "EC"
|
||||
"RSA" -> "RSA"
|
||||
else -> certs[0].publicKey.algorithm
|
||||
}
|
||||
val keyFactory = KeyFactory.getInstance(algorithmName)
|
||||
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
|
||||
val publicKey = certs[0].publicKey
|
||||
return Pair(KeyPair(publicKey, privateKey), certs)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.Build
|
||||
import android.os.SystemProperties
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.time.LocalDate
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
@@ -11,7 +13,6 @@ import org.bouncycastle.asn1.ASN1EncodableVector
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import org.bouncycastle.asn1.DERSet
|
||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
@@ -90,27 +91,33 @@ object AndroidDeviceUtils {
|
||||
attestationValueProvider: () -> ByteArray?,
|
||||
expectedSize: Int,
|
||||
): ByteArray {
|
||||
// 1. Attempt to get the value from the system property.
|
||||
getProperty(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||
persistToFile(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
// 2. Fallback to the value from a cached TEE attestation.
|
||||
try {
|
||||
attestationValueProvider()?.let {
|
||||
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||
setProperty(propertyName, it) // Persist for consistency
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
return it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||
}
|
||||
|
||||
// 3. As a final fallback, generate a random value.
|
||||
readFromFile(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
return generateRandomBytes(expectedSize).also {
|
||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,10 +164,37 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a cryptographically random byte array of a specified length. */
|
||||
private fun generateRandomBytes(size: Int): ByteArray =
|
||||
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
||||
|
||||
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
||||
|
||||
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
||||
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
||||
}
|
||||
|
||||
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
||||
try {
|
||||
fileForProperty(propertyName).writeBytes(bytes)
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to persist $propertyName to file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
|
||||
return try {
|
||||
val file = fileForProperty(propertyName)
|
||||
if (!file.exists()) return null
|
||||
val bytes = file.readBytes()
|
||||
if (bytes.size == expectedSize) bytes else null
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to read $propertyName from file.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Patch Level Properties ---
|
||||
|
||||
fun getPatchLevel(uid: Int): Int {
|
||||
@@ -371,52 +405,174 @@ object AndroidDeviceUtils {
|
||||
|
||||
// --- APEX and Module Hash Properties ---
|
||||
|
||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
||||
runCatching {
|
||||
val pm = ConfigurationManager.getPackageManager()
|
||||
val packages =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
||||
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
|
||||
private class MinimalApexManifestParser(private val data: ByteArray) {
|
||||
var pos = 0
|
||||
|
||||
fun parse(): Pair<String, Long>? {
|
||||
var name: String? = null
|
||||
var version: Long? = null
|
||||
|
||||
while (pos < data.size) {
|
||||
val tag = readVarint()
|
||||
val fieldNum = tag ushr 3
|
||||
val wireType = (tag and 0x07).toInt()
|
||||
|
||||
when (fieldNum) {
|
||||
1L -> {
|
||||
val length = readVarint().toInt()
|
||||
if (pos + length > data.size) return null
|
||||
name = String(data, pos, length, Charsets.UTF_8)
|
||||
pos += length
|
||||
}
|
||||
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
|
||||
2L -> {
|
||||
version = readVarint()
|
||||
}
|
||||
else -> skipField(wireType)
|
||||
}
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("Failed to get APEX package information.", it)
|
||||
emptyList()
|
||||
|
||||
return if (name != null && version != null) {
|
||||
name to version
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readVarint(): Long {
|
||||
var value = 0L
|
||||
var shift = 0
|
||||
while (pos < data.size) {
|
||||
val b = data[pos++].toInt()
|
||||
value = value or ((b and 0x7F).toLong() shl shift)
|
||||
if ((b and 0x80) == 0) return value
|
||||
shift += 7
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun skipField(wireType: Int) {
|
||||
when (wireType) {
|
||||
0 -> readVarint()
|
||||
1 -> pos += 8
|
||||
2 -> {
|
||||
val len = readVarint().toInt()
|
||||
pos += len
|
||||
}
|
||||
5 -> pos += 4
|
||||
else -> throw IllegalStateException("Unknown wire type $wireType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
||||
val results = mutableListOf<Pair<String, Long>>()
|
||||
val apexRoot = File("/apex")
|
||||
|
||||
if (!apexRoot.exists() || !apexRoot.isDirectory) {
|
||||
return@lazy emptyList()
|
||||
}
|
||||
|
||||
apexRoot.listFiles()?.forEach { file ->
|
||||
if (!file.isDirectory) return@forEach
|
||||
val name = file.name
|
||||
|
||||
if (name.startsWith(".")) return@forEach
|
||||
if (name.contains("@")) return@forEach
|
||||
if (name == "sharedlibs") return@forEach
|
||||
|
||||
val manifestFile = File(file, "apex_manifest.pb")
|
||||
if (manifestFile.exists()) {
|
||||
runCatching {
|
||||
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
|
||||
val parser = MinimalApexManifestParser(bytes)
|
||||
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.distinctBy { it.first }
|
||||
}
|
||||
|
||||
val moduleHash: ByteArray by lazy {
|
||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||
?: runCatching {
|
||||
// TODO: figure out the correct calculation
|
||||
val moduleSequences = ASN1EncodableVector()
|
||||
data class ModuleEntry(
|
||||
val nameEncoded: ByteArray,
|
||||
val fullEncoded: ByteArray,
|
||||
)
|
||||
|
||||
// 1. Create a DERSequence for each module.
|
||||
apexInfos.forEach { (packageName, versionCode) ->
|
||||
val moduleVector = ASN1EncodableVector()
|
||||
// Use explicit UTF-8 encoding for the package name.
|
||||
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
|
||||
moduleVector.add(ASN1Integer(versionCode))
|
||||
moduleSequences.add(DERSequence(moduleVector))
|
||||
}
|
||||
val modules =
|
||||
apexInfos.map { (packageName, versionCode) ->
|
||||
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
||||
val versionInt = ASN1Integer(versionCode)
|
||||
|
||||
// 2. Create a DERSet. Bouncy Castle will automatically handle
|
||||
// the sorting based on the DER-encoded value of each sequence.
|
||||
val modulesSet = DERSet(moduleSequences)
|
||||
val vec = ASN1EncodableVector()
|
||||
vec.add(nameOctet)
|
||||
vec.add(versionInt)
|
||||
val sequence = DERSequence(vec)
|
||||
|
||||
// 3. Get the final DER-encoded byte array of the SET.
|
||||
val encodedModules = modulesSet.encoded
|
||||
// AOSP sorts by encoded name only, not full sequence
|
||||
ModuleEntry(
|
||||
nameEncoded = nameOctet.encoded,
|
||||
fullEncoded = sequence.encoded,
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Compute the SHA-256 hash.
|
||||
MessageDigest.getInstance("SHA-256").digest(encodedModules)
|
||||
val sortedModules =
|
||||
modules.sortedWith { m1, m2 ->
|
||||
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||
}
|
||||
|
||||
val payloadStream = ByteArrayOutputStream()
|
||||
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||
val payload = payloadStream.toByteArray()
|
||||
|
||||
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
|
||||
val finalDerSet = encodeAsDerSet(payload)
|
||||
|
||||
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("Failed to compute module hash.", it)
|
||||
ByteArray(32) // Return empty hash on failure
|
||||
ByteArray(32)
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
||||
val length = minOf(a.size, b.size)
|
||||
for (i in 0 until length) {
|
||||
val byteA = a[i].toInt() and 0xFF
|
||||
val byteB = b[i].toInt() and 0xFF
|
||||
if (byteA != byteB) {
|
||||
return byteA - byteB
|
||||
}
|
||||
}
|
||||
return a.size - b.size
|
||||
}
|
||||
|
||||
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x31)
|
||||
writeDerLength(out, payload.size)
|
||||
out.write(payload)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||
if (length < 128) {
|
||||
out.write(length)
|
||||
} else {
|
||||
var size = length
|
||||
val bytes = ArrayList<Byte>()
|
||||
while (size > 0) {
|
||||
bytes.add((size and 0xFF).toByte())
|
||||
size = size ushr 8
|
||||
}
|
||||
out.write(0x80 or bytes.size)
|
||||
for (i in bytes.indices.reversed()) {
|
||||
out.write(bytes[i].toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
object AndroidPermissionUtils {
|
||||
|
||||
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||
private fun getGlobalContext(): Context? {
|
||||
return try {
|
||||
// 1. Get the hidden ActivityThread class via reflection
|
||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||
|
||||
// 2. Invoke the static currentActivityThread() method
|
||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||
currentActivityThreadMethod.isAccessible = true
|
||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||
|
||||
if (activityThread == null) {
|
||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. Try to get the application context
|
||||
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||
getApplicationMethod.isAccessible = true
|
||||
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||
|
||||
if (application != null) return application
|
||||
|
||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||
getSystemContextMethod.isAccessible = true
|
||||
getSystemContextMethod.invoke(activityThread) as? Context
|
||||
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core permission check.
|
||||
*/
|
||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||
val context = getGlobalContext() ?: run {
|
||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||
return false
|
||||
}
|
||||
|
||||
val result = context.checkPermission(permission, -1, uid)
|
||||
return result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||
}
|
||||
|
||||
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||
}
|
||||
|
||||
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||
}
|
||||
|
||||
fun hasDumpPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.DUMP")
|
||||
}
|
||||
}
|
||||
+2
-20
@@ -2,28 +2,10 @@
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_DIR=/data/adb/tricky_store
|
||||
|
||||
echo "============================================"
|
||||
echo " TEESimulator — Key Storage Maintenance"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
|
||||
KEY_COUNT=$(find "$CONFIG_DIR/persistent_keys" -name "*.bin" 2>/dev/null | wc -l)
|
||||
STORAGE_SIZE=$(du -sh "$CONFIG_DIR/persistent_keys" 2>/dev/null | cut -f1)
|
||||
|
||||
echo " Cached keys found : $KEY_COUNT"
|
||||
echo " Storage used : $STORAGE_SIZE"
|
||||
echo ""
|
||||
|
||||
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||
mkdir -p "$CONFIG_DIR/persistent_keys"
|
||||
|
||||
echo " [OK] All cached attestation keys purged"
|
||||
echo " [OK] Fresh keys will generate on next request"
|
||||
echo "Persistent key storage cleared"
|
||||
else
|
||||
echo " No persistent key storage found"
|
||||
echo " Nothing to clear"
|
||||
echo "No persistent key storage found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
|
||||
+110
-28
@@ -1,43 +1,125 @@
|
||||
## TEESimulator v3.2: Anti-Detection Hardening & Key Persistence
|
||||
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||
|
||||
This release hardens TEESimulator against active attestation probing by detector apps (DuckDetector, Luna, GarfieldHan) while introducing persistent key storage that survives daemon restarts and reboots.
|
||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).
|
||||
|
||||
- **PADDING encoding** — Fixed ASN.1 encoding of PADDING tag in attestation extension from individual `[6] INTEGER` entries to `[6] SET OF INTEGER`, matching AOSP `attestation_record.h` schema. Broke all RSA key attestation since v4.6.
|
||||
- **Operation error-path conformance** — Software operations now track finalized state and return `INVALID_OPERATION_HANDLE (-28)` on post-abort calls. Input length guard (32KB) returns `TOO_MUCH_DATA` matching AOSP `operation.rs`. Passes KeyDetector's OperationErrorPathChecker.
|
||||
- **updateAad support** — Added `updateAad` to `SoftwareOperationBinder`, fixing `AbstractMethodError` on Android 16 where the runtime Stub declares it abstract.
|
||||
- **Algorithm inference** — `createOperation` now infers algorithm from the stored key pair when operation params omit the ALGORITHM tag, matching AOSP behavior.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.6: Rebrand & Detection Fix
|
||||
|
||||
- **RTT normalization rework** — Replaced Gaussian sleep (mean=55ms) with a 15ms floor fence. The old approach triggered Chunqiu Native Check 2.8 timing analysis; the floor-only approach satisfies the minimum RTT threshold without creating a detectable delay pattern.
|
||||
- **Cross-algorithm attestation** — Signing algorithm now derived from the attestation key's actual type, not the generated key's algorithm. Fixes BouncyCastle crash when signing RSA keys with EC attestation keys (Shizuku attestation flow).
|
||||
- **Device ID attestation** — Serial/IMEI/MEID/secondImei tags now flow through to software cert gen instead of blanket rejection. Only DEVICE_UNIQUE_ATTESTATION is rejected, matching AOSP keystore2 policy.
|
||||
- **Rebrand to TEESimulator-RS** — Distinguishes this fork from upstream. Version scheme simplified to v{major}.{minor}-{commitCount}.
|
||||
- **CI streamlined** — Release pipeline uses Gradle-generated filenames directly, eliminating the rename step.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.5: Detection Hardening
|
||||
|
||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
|
||||
|
||||
- **Key deletion consistency** — After deleting a software-generated key, `getKeyEntry` now correctly returns `KEY_NOT_FOUND` instead of falling through to a stale live-patch fallback. Fixes binder consistency checks that detect ghost key responses.
|
||||
- **generateKey timing normalization** — Software key generation RTT now matches real TEE latency profile (Gaussian distribution, mean=55ms, floor=15ms). Previously completed in ~4ms, which is an immediate timing side-channel.
|
||||
- **Delete cleanup scope** — `deleteKey` now clears all cached state (patched chains, attestation keys) regardless of whether the key was software or hardware-generated.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.4: AOSP Conformance
|
||||
|
||||
- **Binder error reply format** — Aligned EX_SERVICE_SPECIFIC wire layout with AOSP Status.cpp, including the remote stack trace header field.
|
||||
- **Key enumeration** — Corrected list_past_alias pagination order to match AOSP database.rs semantics.
|
||||
- **KeyMetadata fields** — Generated key responses now include modificationTimeMs, Tag.ORIGIN, and normalized KeyDescriptor fields per AOSP Keystore2.
|
||||
- **Parcel handling** — hasException() preserves reply position for downstream consumers.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.3: Performance & Reliability
|
||||
|
||||
- **Debug log gating** — `SystemLogger.debug()` now skipped entirely in release builds, eliminating unnecessary logcat syscalls on every intercepted transaction.
|
||||
- **Supervisor backoff** — Exponential restart delay (500ms → 30s cap) prevents CPU spin if the daemon crashes repeatedly. Resets automatically once stable.
|
||||
- **Process priority** — Daemon runs at nice=10, yielding CPU to foreground apps on constrained devices.
|
||||
- **Map eviction** — Rate limiter and file lock maps now evict stale entries instead of growing unbounded.
|
||||
- **CI pipeline** — Single-trigger build→release pipeline with proper changelog extraction and correctly sized artifacts.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.2: Detection Evasion Hardening
|
||||
|
||||
Fixes 6 detection vectors flagged by attestation validator apps.
|
||||
|
||||
### Attestation Policy Enforcement
|
||||
|
||||
Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
|
||||
|
||||
- **CREATION_DATETIME** — Reject caller-provided input with `INVALID_ARGUMENT (20)`, matching `security_level.rs:424`. Our cert gen still adds its own timestamp, same as real keystore2.
|
||||
- **Device ID attestation** — Reject ATTESTATION_ID_SERIAL, IMEI, MEID, SECOND_IMEI, and DEVICE_UNIQUE_ATTESTATION with `CANNOT_ATTEST_IDS (-66)`. No consumer app has READ_PRIVILEGED_PHONE_STATE.
|
||||
- **Error reply format** — Fixed AIDL ServiceSpecificException parcel write order (was errorCode→message, now message→errorCode).
|
||||
|
||||
### Certificate Fix
|
||||
|
||||
Leaf certificate Subject CN corrected from "Android KeyStore Key" to "Android Keystore Key" (lowercase s), matching AOSP `KeyGenParameterSpec.java:282`. Both Kotlin and Rust paths.
|
||||
|
||||
### Binder Timing
|
||||
|
||||
Skip interception for system transaction codes (PING, INTERFACE, DUMP) above LAST_CALL_TRANSACTION. Eliminates the JNI round-trip that inflated binder ping ratio to 3.85x (detector threshold: 3.0x).
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.1: Boot Identity Persistence
|
||||
|
||||
Bugfix release. The vbmeta boot key digest was randomizing on every reboot, producing a different RootOfTrust in attestation certificates each boot.
|
||||
|
||||
On devices where the kernel doesn't set `ro.boot.vbmeta.public_key_digest`, the fallback chain hit random generation every boot because `resetprop` overrides for `ro.boot.*` props don't survive reboots. Added file-based persistence (`boot_hash.bin`, `boot_key.bin`) between the TEE cache and random fallback. Once determined, boot identity values persist across reboots.
|
||||
|
||||
Verified on Redmi 14C: second boot reads from persistent file instead of regenerating.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator v4.0: Native Rust Cert Generation
|
||||
|
||||
Major release. Certificate chain generation rebuilt from the ground up in Rust, replacing the BouncyCastle Java path for EC and RSA keys. Hardened against every known detector app.
|
||||
|
||||
### Native Cert Generation
|
||||
|
||||
The headline feature. `libcertgen.so` generates X.509 certificate chains using `ring` (EC-P256/P384) and `rsa` (RSA-2048/4096) with manual DER assembly. No more BouncyCastle quirks — issuer/subject DN bytes are injected directly from the keybox, ensuring byte-perfect chain linkage. BouncyCastle remains as fallback for unsupported curves (P-224, P-521, Curve25519).
|
||||
|
||||
### Anti-Detection Hardening
|
||||
|
||||
* **Per-UID Hardware Keygen Rate Limiter**: Caps hardware key generation at 2 per 30-second window with 2 max concurrent requests per UID. Overflow requests fall back to software certificate generation, preventing binder thread starvation from flood attacks.
|
||||
* **importKey Eviction Defense**: Retains patched attestation chains when `importKey` overwrites an attested alias. Blocks the generate-then-import attack vector used by GarfieldHan and similar detectors.
|
||||
* **Native Binder Payload Cap**: Bypasses interception for payloads exceeding 256KB, preventing thread starvation from oversized binder transactions.
|
||||
* **Oversized Alias Rejection**: Rejects aliases that would exhaust the binder buffer, closing another flooding vector.
|
||||
|
||||
### Security Patch Consistency
|
||||
|
||||
* **Three-Way Patch Level Alignment**: When `system=prop` in `security_patch.txt`, boot and vendor patch levels are forced to `prop` as well. All three ASN.1 attestation tags (706/718/719) now resolve via `SystemProperties.get()` to match what detector apps see through `getprop`.
|
||||
- **Challenge validation** — Oversized attestation challenges (>128 bytes) now return `INVALID_INPUT_LENGTH (-21)`, matching real KeyMint behavior. Previously accepted silently — DuckDetector exploited this.
|
||||
- **Per-UID rate limiter** — 2 hardware keygens per 30s burst, 2 concurrent max. Overflow falls back to software certs. Blocks DuckDetector-style keygen flooding that starves GMS.
|
||||
- **importKey eviction guard** — Retained patch chains prevent generate-then-import attacks that evict cached attestation data.
|
||||
- **256KB native payload cap** — Oversized binder payloads bypass interception cleanly instead of stalling threads.
|
||||
- **Alias size rejection** — Oversized key aliases rejected before they hit the binder buffer.
|
||||
|
||||
### Key Persistence
|
||||
|
||||
* **Generated Key Persistence Layer**: Keys from `generateKey` are persisted to disk in binary format with version headers and atomic writes (tmp + rename).
|
||||
* **Automatic Restoration**: Persisted keys are restored on daemon startup without re-attestation.
|
||||
* **Keybox Rotation Survival**: Generated keys survive keybox.xml changes — only PATCH-mode cert chains are invalidated.
|
||||
* **File-Level Locking**: Concurrent read/write access to persisted keys is serialized to prevent corruption.
|
||||
Generated keys now survive reboots. File-backed storage with file-level locking, preserved across keybox rotations. Banking and biometric apps that cache attestation keys no longer break after restart.
|
||||
|
||||
### Process Reliability
|
||||
### Attestation Fixes
|
||||
|
||||
* **Fork-Based Supervisor Daemon**: Replaces the restart loop with a native fork-based supervisor for near-instant recovery.
|
||||
* **Attestation Leak Blocking**: Returns `DEAD_OBJECT` to callers when the interceptor service is unavailable, preventing unpatched attestation from leaking through.
|
||||
* **Global Exception Handler**: Catches uncaught exceptions and triggers clean daemon restart instead of silent death.
|
||||
* **FileObserver NPE Fix**: Prevents crash when config files are deleted while being observed.
|
||||
|
||||
### Upstream Cherry-Picks
|
||||
|
||||
* **KeyUsage per HAL spec** (#119): Correct certificate KeyUsage based on KeyPurpose.
|
||||
* **Reference leak fix** (#122): Resolve strong reference leak and warnings in binder interception.
|
||||
- Null out all-zero `verifiedBootHash` from TEE cache (fingerprinting vector)
|
||||
- Correct `module_hash` field to match AOSP Keystore2 format
|
||||
- Override pre-existing attest keys instead of skipping them
|
||||
- Strip HTML comments from PEM blocks in keybox parsing
|
||||
- Security patch consistency — `system=prop` forces boot/vendor to match
|
||||
|
||||
### Module Lifecycle
|
||||
|
||||
* **`action.sh`**: Purge persistent key storage via KSU Manager Action button. Shows key count and storage size before clearing.
|
||||
* **`uninstall.sh`**: Clean module removal — kills daemon, removes generated data, preserves `target.txt`, `keybox.xml`, and `security_patch.txt`.
|
||||
- Supervisor daemon keeps the interceptor alive
|
||||
- KSU Action button clears persistent key cache
|
||||
- Clean uninstall removes all traces (persistent keys, TEE status, daemon)
|
||||
|
||||
### PKI Fixes
|
||||
### Stability
|
||||
|
||||
* Strip HTML comments from PEM blocks before parsing.
|
||||
- FileObserver NPE on config deletion fixed
|
||||
- Global uncaught exception handler — daemon stays alive on unexpected errors
|
||||
- PEM parsing hardened against malformed keybox files
|
||||
|
||||
### Tested Against
|
||||
|
||||
DuckDetector, Luna, Play Integrity, Key Attestation Demo — all passing on Redmi 14C (Android 14, Beanpod KeyMaster, KSU).
|
||||
|
||||
+2
-1
@@ -15,7 +15,7 @@ fi
|
||||
|
||||
# --- Version Info ---
|
||||
VERSION=$(grep_prop version "${TMPDIR}/module.prop")
|
||||
ui_print "- Installing TEESimulator $VERSION"
|
||||
ui_print "- Installing TEESimulator-RS $VERSION"
|
||||
ui_print ""
|
||||
|
||||
# --- Architecture Handling ---
|
||||
@@ -68,6 +68,7 @@ ui_print "- Extracting $ARCH libraries"
|
||||
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
|
||||
install_file "lib/$ABI_DIR/libinject.so" "$MODPATH"
|
||||
install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH"
|
||||
install_file "lib/$ABI_DIR/libcertgen.so" "$MODPATH"
|
||||
ui_print ""
|
||||
|
||||
mv "$MODPATH/libinject.so" "$MODPATH/inject"
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
id=tricky_store
|
||||
name=TEESimulator
|
||||
name=TEESimulator-RS
|
||||
version=${REPLACEMEVER}
|
||||
versionCode=${REPLACEMEVERCODE}
|
||||
author=JingMatrix, Enginex0
|
||||
description=Software simulation for Android hardware-backed key pairs with key attestation
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/update.json
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
|
||||
|
||||
@@ -9,3 +9,4 @@ done
|
||||
|
||||
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||
rm -f "$CONFIG_DIR/boot_hash.bin" "$CONFIG_DIR/boot_key.bin"
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "v3.2",
|
||||
"versionCode": 82,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v3.2/TEESimulator-v3.2-82-Release.zip",
|
||||
"version": "v4.5",
|
||||
"versionCode": 111,
|
||||
"zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.5/TEESimulator-v4.5-Release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[target.aarch64-linux-android]
|
||||
linker = "aarch64-linux-android29-clang"
|
||||
|
||||
[target.armv7-linux-androideabi]
|
||||
linker = "armv7a-linux-androideabi29-clang"
|
||||
|
||||
[target.i686-linux-android]
|
||||
linker = "i686-linux-android29-clang"
|
||||
|
||||
[target.x86_64-linux-android]
|
||||
linker = "x86_64-linux-android29-clang"
|
||||
Generated
+1166
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "certgen"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
jni = { version = "0.21.1", default-features = false }
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9", features = ["sha2"] }
|
||||
pkcs8 = { version = "0.10", features = ["alloc"] }
|
||||
rand = "0.8"
|
||||
der = { version = "0.7.10", features = ["alloc", "oid"] }
|
||||
const-oid = "0.9.6"
|
||||
x509-cert = { version = "0.2.5", features = ["pem"] }
|
||||
time = { version = "0.3", features = ["std"] }
|
||||
anyhow = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
libc = "0.2"
|
||||
zip = { version = "2.2", default-features = false, features = ["deflate"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = "symbols"
|
||||
panic = "abort"
|
||||
@@ -0,0 +1,8 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
targets = [
|
||||
"aarch64-linux-android",
|
||||
"armv7-linux-androideabi",
|
||||
"i686-linux-android",
|
||||
"x86_64-linux-android",
|
||||
]
|
||||
@@ -0,0 +1,644 @@
|
||||
use crate::error::Result;
|
||||
use crate::types::CertGenParams;
|
||||
|
||||
const DO_NOT_REPORT: i32 = -1;
|
||||
|
||||
pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let sw = build_software_enforced(params)?;
|
||||
let tee = build_tee_enforced(params)?;
|
||||
|
||||
let mut inner = Vec::new();
|
||||
// attestationVersion — INTEGER
|
||||
inner.extend_from_slice(&enc_integer(params.attest_version as i64));
|
||||
// attestationSecurityLevel — ENUMERATED, not INTEGER
|
||||
inner.extend_from_slice(&enc_enumerated(params.security_level));
|
||||
// keymintVersion — INTEGER
|
||||
inner.extend_from_slice(&enc_integer(params.keymaster_version as i64));
|
||||
// keymintSecurityLevel — ENUMERATED, not INTEGER
|
||||
inner.extend_from_slice(&enc_enumerated(params.security_level));
|
||||
// attestationChallenge — OCTET STRING
|
||||
inner.extend_from_slice(&enc_octet_string(
|
||||
params.attestation_challenge.as_deref().unwrap_or(&[]),
|
||||
));
|
||||
// uniqueId — OCTET STRING (always empty)
|
||||
inner.extend_from_slice(&enc_octet_string(&[]));
|
||||
// softwareEnforced
|
||||
inner.extend_from_slice(&sw);
|
||||
// teeEnforced
|
||||
inner.extend_from_slice(&tee);
|
||||
|
||||
Ok(enc_sequence(&inner))
|
||||
}
|
||||
|
||||
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
|
||||
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||
fields.push((701, enc_integer(params.creation_datetime)));
|
||||
|
||||
// Tag 709: ATTESTATION_APPLICATION_ID — OCTET STRING
|
||||
// The bytes are already the DER-encoded AttestationApplicationId wrapped in OCTET STRING
|
||||
// by the Kotlin layer. We wrap them in an EXPLICIT tag.
|
||||
if !params.attestation_application_id.is_empty() {
|
||||
fields.push((709, enc_octet_string(¶ms.attestation_application_id)));
|
||||
}
|
||||
|
||||
// Tag 724: MODULE_HASH — OCTET STRING (only if attestVersion >= 400)
|
||||
if params.attest_version >= 400 {
|
||||
if let Some(ref hash) = params.module_hash {
|
||||
fields.push((724, enc_octet_string(hash)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(build_authorization_list(&mut fields))
|
||||
}
|
||||
|
||||
fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
|
||||
// Tag 1: PURPOSE — SET OF INTEGER
|
||||
if !params.purposes.is_empty() {
|
||||
fields.push((1, build_set_of_integer(¶ms.purposes)));
|
||||
}
|
||||
|
||||
// Tag 2: ALGORITHM — INTEGER
|
||||
fields.push((2, enc_integer(params.algorithm as i32 as i64)));
|
||||
|
||||
// Tag 3: KEY_SIZE — INTEGER
|
||||
fields.push((3, enc_integer(params.key_size as i64)));
|
||||
|
||||
// Tag 5: DIGEST — SET OF INTEGER
|
||||
if !params.digests.is_empty() {
|
||||
fields.push((5, build_set_of_integer(¶ms.digests)));
|
||||
}
|
||||
|
||||
// Tag 10: EC_CURVE — INTEGER (only for EC keys)
|
||||
if let Some(curve) = params.ec_curve {
|
||||
fields.push((10, enc_integer(curve as i32 as i64)));
|
||||
}
|
||||
|
||||
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true)
|
||||
fields.push((503, enc_null()));
|
||||
|
||||
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
||||
fields.push((702, enc_integer(0)));
|
||||
|
||||
// Tag 704: ROOT_OF_TRUST — SEQUENCE
|
||||
fields.push((704, build_root_of_trust(params)));
|
||||
|
||||
// Tag 705: OS_VERSION — INTEGER
|
||||
if params.os_version != DO_NOT_REPORT {
|
||||
fields.push((705, enc_integer(params.os_version as i64)));
|
||||
}
|
||||
|
||||
// Tag 706: OS_PATCHLEVEL — INTEGER
|
||||
if params.os_patch_level != DO_NOT_REPORT {
|
||||
fields.push((706, enc_integer(params.os_patch_level as i64)));
|
||||
}
|
||||
|
||||
// Tags 710-717: ATTESTATION_ID_* — OCTET STRING (optional)
|
||||
if let Some(ref v) = params.id_brand {
|
||||
fields.push((710, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_device {
|
||||
fields.push((711, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_product {
|
||||
fields.push((712, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_serial {
|
||||
fields.push((713, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_imei {
|
||||
fields.push((714, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_meid {
|
||||
fields.push((715, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_manufacturer {
|
||||
fields.push((716, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_model {
|
||||
fields.push((717, enc_octet_string(v)));
|
||||
}
|
||||
|
||||
// Tag 718: VENDOR_PATCHLEVEL — INTEGER
|
||||
if params.vendor_patch_level != DO_NOT_REPORT {
|
||||
fields.push((718, enc_integer(params.vendor_patch_level as i64)));
|
||||
}
|
||||
|
||||
// Tag 719: BOOT_PATCHLEVEL — INTEGER
|
||||
if params.boot_patch_level != DO_NOT_REPORT {
|
||||
fields.push((719, enc_integer(params.boot_patch_level as i64)));
|
||||
}
|
||||
|
||||
// Tag 723: ATTESTATION_ID_SECOND_IMEI — OCTET STRING (only if attestVersion >= 300)
|
||||
if params.attest_version >= 300 {
|
||||
if let Some(ref v) = params.id_second_imei {
|
||||
fields.push((723, enc_octet_string(v)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(build_authorization_list(&mut fields))
|
||||
}
|
||||
|
||||
fn build_root_of_trust(params: &CertGenParams) -> Vec<u8> {
|
||||
let mut inner = Vec::new();
|
||||
// verifiedBootKey — OCTET STRING (32 bytes)
|
||||
inner.extend_from_slice(&enc_octet_string(¶ms.boot_key));
|
||||
// deviceLocked — BOOLEAN TRUE (0xFF, not 0x01)
|
||||
inner.extend_from_slice(&enc_boolean(true));
|
||||
// verifiedBootState — ENUMERATED 0 (Verified), not INTEGER
|
||||
inner.extend_from_slice(&enc_enumerated(0));
|
||||
// verifiedBootHash — OCTET STRING (32 bytes)
|
||||
inner.extend_from_slice(&enc_octet_string(¶ms.boot_hash));
|
||||
enc_sequence(&inner)
|
||||
}
|
||||
|
||||
fn build_authorization_list(fields: &mut Vec<(u32, Vec<u8>)>) -> Vec<u8> {
|
||||
fields.sort_by_key(|(tag, _)| *tag);
|
||||
let mut inner = Vec::new();
|
||||
for (tag, value) in fields.iter() {
|
||||
inner.extend_from_slice(&enc_explicit_tag(*tag, value));
|
||||
}
|
||||
enc_sequence(&inner)
|
||||
}
|
||||
|
||||
fn build_set_of_integer(values: &[i32]) -> Vec<u8> {
|
||||
// DER SET OF: elements sorted by encoded byte value
|
||||
let mut encoded: Vec<Vec<u8>> = values.iter().map(|v| enc_integer(*v as i64)).collect();
|
||||
encoded.sort();
|
||||
let mut inner = Vec::new();
|
||||
for e in &encoded {
|
||||
inner.extend_from_slice(e);
|
||||
}
|
||||
enc_set(&inner)
|
||||
}
|
||||
|
||||
// --- DER primitives ---
|
||||
|
||||
fn enc_length(len: usize) -> Vec<u8> {
|
||||
if len < 0x80 {
|
||||
vec![len as u8]
|
||||
} else if len <= 0xFF {
|
||||
vec![0x81, len as u8]
|
||||
} else if len <= 0xFFFF {
|
||||
vec![0x82, (len >> 8) as u8, len as u8]
|
||||
} else if len <= 0xFF_FFFF {
|
||||
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||
} else {
|
||||
vec![
|
||||
0x84,
|
||||
(len >> 24) as u8,
|
||||
(len >> 16) as u8,
|
||||
(len >> 8) as u8,
|
||||
len as u8,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn enc_integer(value: i64) -> Vec<u8> {
|
||||
// DER INTEGER: tag 0x02, minimal two's complement big-endian
|
||||
let bytes = integer_bytes(value);
|
||||
let mut out = vec![0x02];
|
||||
out.extend_from_slice(&enc_length(bytes.len()));
|
||||
out.extend_from_slice(&bytes);
|
||||
out
|
||||
}
|
||||
|
||||
fn integer_bytes(value: i64) -> Vec<u8> {
|
||||
if value == 0 {
|
||||
return vec![0x00];
|
||||
}
|
||||
let raw = value.to_be_bytes();
|
||||
// Find first significant byte
|
||||
let mut start = 0;
|
||||
if value > 0 {
|
||||
while start < 7 && raw[start] == 0x00 {
|
||||
start += 1;
|
||||
}
|
||||
// If high bit set, need leading 0x00 to keep positive
|
||||
if raw[start] & 0x80 != 0 {
|
||||
let mut out = vec![0x00];
|
||||
out.extend_from_slice(&raw[start..]);
|
||||
return out;
|
||||
}
|
||||
} else {
|
||||
while start < 7 && raw[start] == 0xFF {
|
||||
start += 1;
|
||||
}
|
||||
// If high bit clear, need leading 0xFF to keep negative
|
||||
if raw[start] & 0x80 == 0 {
|
||||
let mut out = vec![0xFF];
|
||||
out.extend_from_slice(&raw[start..]);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
raw[start..].to_vec()
|
||||
}
|
||||
|
||||
fn enc_enumerated(value: i32) -> Vec<u8> {
|
||||
// DER ENUMERATED: tag 0x0A, same value encoding as INTEGER
|
||||
let bytes = integer_bytes(value as i64);
|
||||
let mut out = vec![0x0A];
|
||||
out.extend_from_slice(&enc_length(bytes.len()));
|
||||
out.extend_from_slice(&bytes);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_octet_string(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![0x04];
|
||||
out.extend_from_slice(&enc_length(data.len()));
|
||||
out.extend_from_slice(data);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn enc_boolean(value: bool) -> Vec<u8> {
|
||||
// DER BOOLEAN: TRUE = 0xFF, FALSE = 0x00
|
||||
vec![0x01, 0x01, if value { 0xFF } else { 0x00 }]
|
||||
}
|
||||
|
||||
fn enc_sequence(contents: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![0x30];
|
||||
out.extend_from_slice(&enc_length(contents.len()));
|
||||
out.extend_from_slice(contents);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_set(contents: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![0x31];
|
||||
out.extend_from_slice(&enc_length(contents.len()));
|
||||
out.extend_from_slice(contents);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_explicit_tag(tag_number: u32, inner: &[u8]) -> Vec<u8> {
|
||||
// EXPLICIT context-specific constructed tag
|
||||
let mut out = Vec::new();
|
||||
if tag_number < 31 {
|
||||
// Short form: single byte 0xA0 | tag_number
|
||||
out.push(0xA0 | tag_number as u8);
|
||||
} else {
|
||||
// Long form: 0xBF followed by base-128 encoding of tag number
|
||||
out.push(0xBF);
|
||||
enc_base128_tag(&mut out, tag_number);
|
||||
}
|
||||
out.extend_from_slice(&enc_length(inner.len()));
|
||||
out.extend_from_slice(inner);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_base128_tag(out: &mut Vec<u8>, tag: u32) {
|
||||
// Base-128 with continuation bits: MSB first, bit 7 set on all but last byte
|
||||
let mut digits = Vec::new();
|
||||
let mut val = tag;
|
||||
digits.push((val & 0x7F) as u8);
|
||||
val >>= 7;
|
||||
while val > 0 {
|
||||
digits.push((val & 0x7F) as u8 | 0x80);
|
||||
val >>= 7;
|
||||
}
|
||||
// Written MSB first
|
||||
for b in digits.iter().rev() {
|
||||
out.push(*b);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{Algorithm, EcCurve};
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_zero() {
|
||||
assert_eq!(enc_integer(0), vec![0x02, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_small_positive() {
|
||||
assert_eq!(enc_integer(3), vec![0x02, 0x01, 0x03]);
|
||||
assert_eq!(enc_integer(127), vec![0x02, 0x01, 0x7F]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_needs_leading_zero() {
|
||||
// 128 = 0x80, high bit set so needs 0x00 prefix
|
||||
assert_eq!(enc_integer(128), vec![0x02, 0x02, 0x00, 0x80]);
|
||||
assert_eq!(enc_integer(256), vec![0x02, 0x02, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_multi_byte() {
|
||||
// 140000 = 0x02_22_E0
|
||||
assert_eq!(enc_integer(140000), vec![0x02, 0x03, 0x02, 0x22, 0xE0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_large() {
|
||||
// 20250301 = 0x01_34_FE_BD
|
||||
assert_eq!(
|
||||
enc_integer(20250301),
|
||||
vec![0x02, 0x04, 0x01, 0x34, 0xFE, 0xBD]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_enumerated() {
|
||||
// SecurityLevel TEE = 1
|
||||
assert_eq!(enc_enumerated(1), vec![0x0A, 0x01, 0x01]);
|
||||
// VerifiedBootState Verified = 0
|
||||
assert_eq!(enc_enumerated(0), vec![0x0A, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_boolean_true() {
|
||||
// DER: TRUE = 0xFF
|
||||
assert_eq!(enc_boolean(true), vec![0x01, 0x01, 0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_null() {
|
||||
assert_eq!(enc_null(), vec![0x05, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_octet_string_empty() {
|
||||
assert_eq!(enc_octet_string(&[]), vec![0x04, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_short() {
|
||||
// Tag 1 wrapping INTEGER 2: A1 03 02 01 02
|
||||
let inner = enc_integer(2);
|
||||
let tagged = enc_explicit_tag(1, &inner);
|
||||
assert_eq!(tagged, vec![0xA1, 0x03, 0x02, 0x01, 0x02]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_10() {
|
||||
// Tag 10: 0xAA
|
||||
let inner = enc_integer(1);
|
||||
let tagged = enc_explicit_tag(10, &inner);
|
||||
assert_eq!(tagged[0], 0xAA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_503() {
|
||||
// Tag 503: 0xBF 0x83 0x77
|
||||
// 503 = 3*128 + 119 => 0x83 0x77
|
||||
let inner = enc_null();
|
||||
let tagged = enc_explicit_tag(503, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x83, 0x77]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_704() {
|
||||
// Tag 704: 0xBF 0x85 0x40
|
||||
// 704 = 5*128 + 64 => 0x85 0x40
|
||||
let inner = enc_sequence(&[]);
|
||||
let tagged = enc_explicit_tag(704, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x40]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_718() {
|
||||
// Tag 718: 0xBF 0x85 0x4E
|
||||
let inner = enc_integer(20250301);
|
||||
let tagged = enc_explicit_tag(718, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4E]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_719() {
|
||||
// Tag 719: 0xBF 0x85 0x4F
|
||||
let inner = enc_integer(20250301);
|
||||
let tagged = enc_explicit_tag(719, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4F]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_701() {
|
||||
// Tag 701: 0xBF 0x85 0x3D
|
||||
let inner = enc_integer(1000);
|
||||
let tagged = enc_explicit_tag(701, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x3D]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_709() {
|
||||
// Tag 709: 0xBF 0x85 0x45
|
||||
let inner = enc_octet_string(&[0x01]);
|
||||
let tagged = enc_explicit_tag(709, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x45]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_set_of_integer_sorted() {
|
||||
// SET OF INTEGER must sort by encoded bytes
|
||||
let result = build_set_of_integer(&[3, 2]);
|
||||
// Expect sorted: INTEGER 2 before INTEGER 3
|
||||
let expected = enc_set(&[0x02, 0x01, 0x02, 0x02, 0x01, 0x03]);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_root_of_trust_structure() {
|
||||
let params = make_test_params();
|
||||
let rot = build_root_of_trust(¶ms);
|
||||
// Should be a SEQUENCE (0x30)
|
||||
assert_eq!(rot[0], 0x30);
|
||||
// Find BOOLEAN TRUE inside
|
||||
let rot_inner = &rot[2..]; // skip tag+length
|
||||
// First: OCTET STRING (32 bytes boot key)
|
||||
assert_eq!(rot_inner[0], 0x04);
|
||||
assert_eq!(rot_inner[1], 0x20); // 32 bytes
|
||||
// After boot key (34 bytes): BOOLEAN TRUE
|
||||
assert_eq!(rot_inner[34], 0x01); // BOOLEAN tag
|
||||
assert_eq!(rot_inner[35], 0x01); // length 1
|
||||
assert_eq!(rot_inner[36], 0xFF); // TRUE = 0xFF
|
||||
// Then ENUMERATED 0 (verifiedBootState)
|
||||
assert_eq!(rot_inner[37], 0x0A); // ENUMERATED tag, not 0x02
|
||||
assert_eq!(rot_inner[38], 0x01);
|
||||
assert_eq!(rot_inner[39], 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_do_not_report_omits_fields() {
|
||||
let mut params = make_test_params();
|
||||
params.os_patch_level = DO_NOT_REPORT;
|
||||
params.vendor_patch_level = DO_NOT_REPORT;
|
||||
params.boot_patch_level = DO_NOT_REPORT;
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let hex = hex_string(&tee);
|
||||
// Tags 706, 718, 719 should not appear
|
||||
// Tag 706 = BF 85 42, 718 = BF 85 4E, 719 = BF 85 4F
|
||||
assert!(!hex.contains("bf8542"), "os_patch_level should be omitted");
|
||||
assert!(
|
||||
!hex.contains("bf854e"),
|
||||
"vendor_patch_level should be omitted"
|
||||
);
|
||||
assert!(
|
||||
!hex.contains("bf854f"),
|
||||
"boot_patch_level should be omitted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_description_security_level_is_enumerated() {
|
||||
let params = make_test_params();
|
||||
let ext = build_attestation_extension(¶ms).unwrap();
|
||||
// KeyDescription is a SEQUENCE: 0x30 ...
|
||||
assert_eq!(ext[0], 0x30);
|
||||
// Skip SEQUENCE tag + length to get to inner fields
|
||||
let inner = skip_tlv_header(&ext);
|
||||
// Field 0: attestationVersion — INTEGER (0x02)
|
||||
assert_eq!(inner[0], 0x02);
|
||||
let (_, rest) = skip_one_tlv(inner);
|
||||
// Field 1: attestationSecurityLevel — ENUMERATED (0x0A)
|
||||
assert_eq!(rest[0], 0x0A, "attestationSecurityLevel must be ENUMERATED");
|
||||
let (_, rest) = skip_one_tlv(rest);
|
||||
// Field 2: keymintVersion — INTEGER (0x02)
|
||||
assert_eq!(rest[0], 0x02);
|
||||
let (_, rest) = skip_one_tlv(rest);
|
||||
// Field 3: keymintSecurityLevel — ENUMERATED (0x0A)
|
||||
assert_eq!(rest[0], 0x0A, "keymintSecurityLevel must be ENUMERATED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authorization_list_sorted_by_tag() {
|
||||
let params = make_test_params();
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&tee);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
let mut sorted = tags.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_extension_roundtrip() {
|
||||
let params = make_test_params();
|
||||
let ext = build_attestation_extension(¶ms).unwrap();
|
||||
// Must be valid DER: starts with SEQUENCE tag
|
||||
assert_eq!(ext[0], 0x30);
|
||||
// Length must account for all inner bytes
|
||||
let (header_len, total_content_len) = parse_tlv_lengths(&ext);
|
||||
assert_eq!(ext.len(), header_len + total_content_len);
|
||||
}
|
||||
|
||||
// --- test helpers ---
|
||||
|
||||
fn make_test_params() -> CertGenParams {
|
||||
CertGenParams {
|
||||
algorithm: Algorithm::Ec,
|
||||
key_size: 256,
|
||||
ec_curve: Some(EcCurve::P256),
|
||||
rsa_public_exponent: 0,
|
||||
attestation_challenge: Some(vec![0xAB; 32]),
|
||||
purposes: vec![2, 3],
|
||||
digests: vec![4],
|
||||
cert_serial: None,
|
||||
cert_subject: None,
|
||||
cert_not_before: -1,
|
||||
cert_not_after: -1,
|
||||
keybox_private_key: vec![],
|
||||
keybox_cert_chain: vec![],
|
||||
security_level: 1,
|
||||
attest_version: 200,
|
||||
keymaster_version: 200,
|
||||
os_version: 140000,
|
||||
os_patch_level: 202503,
|
||||
vendor_patch_level: 20250301,
|
||||
boot_patch_level: 20250301,
|
||||
boot_key: vec![0x01; 32],
|
||||
boot_hash: vec![0x02; 32],
|
||||
creation_datetime: 1709913600000,
|
||||
attestation_application_id: vec![0xDE, 0xAD],
|
||||
module_hash: None,
|
||||
id_brand: None,
|
||||
id_device: None,
|
||||
id_product: None,
|
||||
id_serial: None,
|
||||
id_imei: None,
|
||||
id_meid: None,
|
||||
id_manufacturer: None,
|
||||
id_model: None,
|
||||
id_second_imei: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_string(data: &[u8]) -> String {
|
||||
data.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
fn skip_tlv_header(data: &[u8]) -> &[u8] {
|
||||
let (header_len, _) = parse_tlv_lengths(data);
|
||||
&data[header_len..]
|
||||
}
|
||||
|
||||
fn skip_one_tlv(data: &[u8]) -> (usize, &[u8]) {
|
||||
let (header_len, content_len) = parse_tlv_lengths(data);
|
||||
let total = header_len + content_len;
|
||||
(total, &data[total..])
|
||||
}
|
||||
|
||||
fn parse_tlv_lengths(data: &[u8]) -> (usize, usize) {
|
||||
// Returns (header_bytes, content_bytes)
|
||||
let tag_len = tag_byte_len(data);
|
||||
let len_start = tag_len;
|
||||
if data[len_start] < 0x80 {
|
||||
(len_start + 1, data[len_start] as usize)
|
||||
} else {
|
||||
let num_len_bytes = (data[len_start] & 0x7F) as usize;
|
||||
let mut content_len = 0usize;
|
||||
for i in 0..num_len_bytes {
|
||||
content_len = (content_len << 8) | data[len_start + 1 + i] as usize;
|
||||
}
|
||||
(len_start + 1 + num_len_bytes, content_len)
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_byte_len(data: &[u8]) -> usize {
|
||||
if data[0] & 0x1F != 0x1F {
|
||||
1
|
||||
} else {
|
||||
let mut i = 1;
|
||||
while data[i] & 0x80 != 0 {
|
||||
i += 1;
|
||||
}
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_tag_numbers(mut data: &[u8]) -> Vec<u32> {
|
||||
let mut tags = Vec::new();
|
||||
while !data.is_empty() {
|
||||
let tag = read_tag_number(data);
|
||||
tags.push(tag);
|
||||
let (_, rest) = skip_one_tlv(data);
|
||||
data = rest;
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
fn read_tag_number(data: &[u8]) -> u32 {
|
||||
if data[0] & 0x1F != 0x1F {
|
||||
(data[0] & 0x1F) as u32
|
||||
} else {
|
||||
let mut val = 0u32;
|
||||
let mut i = 1;
|
||||
loop {
|
||||
val = (val << 7) | (data[i] & 0x7F) as u32;
|
||||
if data[i] & 0x80 == 0 {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
val
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
use crate::error::{CertGenError, Result};
|
||||
use crate::keybox::ParsedKeybox;
|
||||
use crate::types::{Algorithm, CertGenParams, GeneratedKeyPair};
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 1, 17];
|
||||
|
||||
// Signature algorithm OIDs
|
||||
const OID_SHA256_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 2];
|
||||
const OID_SHA384_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 3];
|
||||
const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
|
||||
|
||||
// Extension OIDs
|
||||
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
||||
|
||||
pub fn build_certificate_chain(
|
||||
key_pair: &GeneratedKeyPair,
|
||||
attestation_ext_der: &[u8],
|
||||
keybox: &ParsedKeybox,
|
||||
params: &CertGenParams,
|
||||
) -> Result<Vec<Vec<u8>>> {
|
||||
let leaf_der = build_leaf_cert(key_pair, attestation_ext_der, keybox, params)?;
|
||||
|
||||
let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len());
|
||||
chain.push(leaf_der);
|
||||
for cert_der in &keybox.cert_chain_ders {
|
||||
chain.push(cert_der.clone());
|
||||
}
|
||||
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
fn build_leaf_cert(
|
||||
key_pair: &GeneratedKeyPair,
|
||||
attestation_ext_der: &[u8],
|
||||
keybox: &ParsedKeybox,
|
||||
params: &CertGenParams,
|
||||
) -> Result<Vec<u8>> {
|
||||
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
|
||||
let sig_alg_der = signature_algorithm_for_signing_key(&keybox.signing_key_der, params.algorithm)?;
|
||||
|
||||
// Serial number
|
||||
let serial_bytes = if let Some(ref serial) = params.cert_serial {
|
||||
serial.clone()
|
||||
} else {
|
||||
vec![1u8]
|
||||
};
|
||||
|
||||
// Subject DN
|
||||
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||
subject.clone()
|
||||
} else {
|
||||
encode_simple_cn_dn("Android Keystore Key")
|
||||
};
|
||||
|
||||
// Validity
|
||||
let not_before = timestamp_to_datetime(params.cert_not_before)?;
|
||||
let not_after = if params.cert_not_after == -1 {
|
||||
OffsetDateTime::from_unix_timestamp(keybox.leaf_not_after)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365))
|
||||
} else {
|
||||
timestamp_to_datetime(params.cert_not_after)?
|
||||
};
|
||||
|
||||
// Extensions
|
||||
let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?;
|
||||
|
||||
// TBS Certificate
|
||||
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(¬_before, ¬_after);
|
||||
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
|
||||
|
||||
let tbs_der = encode_der_sequence(&[
|
||||
&version_der,
|
||||
&serial_der,
|
||||
&sig_alg_der,
|
||||
&keybox.issuer_dn_der, // RAW bytes — no re-encoding
|
||||
&validity_der,
|
||||
&subject_dn_der,
|
||||
&spki_der,
|
||||
&extensions_tagged,
|
||||
]);
|
||||
|
||||
// Sign the TBS
|
||||
let signature_bytes = sign_tbs(&tbs_der, &keybox.signing_key_der, params.algorithm)?;
|
||||
let signature_bit_string = encode_der_bit_string(&signature_bytes);
|
||||
|
||||
// Final certificate: SEQUENCE { TBS, sigAlgorithm, signature }
|
||||
let cert_der = encode_der_sequence(&[
|
||||
&tbs_der,
|
||||
&sig_alg_der,
|
||||
&signature_bit_string,
|
||||
]);
|
||||
|
||||
Ok(cert_der)
|
||||
}
|
||||
|
||||
fn sign_tbs(tbs_der: &[u8], signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
|
||||
match algorithm {
|
||||
Algorithm::Ec => sign_tbs_ec(tbs_der, signing_key_der),
|
||||
Algorithm::Rsa => sign_tbs_rsa(tbs_der, signing_key_der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_tbs_ec(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
|
||||
// Determine EC curve from the signing key's PKCS8 AlgorithmIdentifier
|
||||
let alg = detect_ec_signing_algorithm(signing_key_der)?;
|
||||
|
||||
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(alg, signing_key_der, &ring::rand::SystemRandom::new())
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("EC key parse: {e}")))?;
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let sig = key_pair.sign(&rng, tbs_der)
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("EC sign: {e}")))?;
|
||||
|
||||
Ok(sig.as_ref().to_vec())
|
||||
}
|
||||
|
||||
fn detect_ec_signing_algorithm(pkcs8_der: &[u8]) -> Result<&'static ring::signature::EcdsaSigningAlgorithm> {
|
||||
use der::Decode;
|
||||
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("PKCS8 parse: {e}")))?;
|
||||
|
||||
let params_oid = info.algorithm.parameters_oid()
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("EC curve OID: {e}")))?;
|
||||
|
||||
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
|
||||
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
|
||||
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
|
||||
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
|
||||
|
||||
if params_oid == p256_oid {
|
||||
Ok(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING)
|
||||
} else if params_oid == p384_oid {
|
||||
Ok(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING)
|
||||
} else {
|
||||
Err(CertGenError::SigningFailed(format!("unsupported EC curve OID: {params_oid}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_tbs_rsa(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
|
||||
use rsa::pkcs8::DecodePrivateKey;
|
||||
use rsa::signature::{SignatureEncoding, SignerMut};
|
||||
use rsa::pkcs1v15::SigningKey;
|
||||
use rsa::sha2::Sha256;
|
||||
|
||||
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(signing_key_der)
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("RSA key parse: {e}")))?;
|
||||
|
||||
let mut signing_key = SigningKey::<Sha256>::new(private_key);
|
||||
let signature = signing_key.sign(tbs_der);
|
||||
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
|
||||
fn signature_algorithm_for_signing_key(signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
|
||||
match algorithm {
|
||||
Algorithm::Ec => {
|
||||
let ring_alg = detect_ec_signing_algorithm(signing_key_der)?;
|
||||
// Determine OID from the algorithm used
|
||||
let oid = if std::ptr::eq(ring_alg, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) {
|
||||
OID_SHA384_WITH_ECDSA
|
||||
} else {
|
||||
OID_SHA256_WITH_ECDSA
|
||||
};
|
||||
let oid_der = encode_der_oid(oid);
|
||||
Ok(encode_der_sequence(&[&oid_der]))
|
||||
}
|
||||
Algorithm::Rsa => {
|
||||
let oid_der = encode_der_oid(OID_SHA256_WITH_RSA);
|
||||
let null_der = vec![0x05, 0x00];
|
||||
Ok(encode_der_sequence(&[&oid_der, &null_der]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_spki_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||
use der::Decode;
|
||||
|
||||
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("PKCS8 parse for SPKI: {e}")))?;
|
||||
|
||||
// Reconstruct SPKI from AlgorithmIdentifier + public key
|
||||
// For EC: derive public key from private key via ring
|
||||
// For RSA: derive from rsa crate
|
||||
let alg_id_oid = info.algorithm.oid;
|
||||
let ec_oid: const_oid::ObjectIdentifier = "1.2.840.10045.2.1".parse()
|
||||
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||
|
||||
if alg_id_oid == ec_oid {
|
||||
extract_ec_spki(pkcs8_der, &info)
|
||||
} else {
|
||||
extract_rsa_spki(pkcs8_der)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_ec_spki(pkcs8_der: &[u8], info: &pkcs8::PrivateKeyInfo) -> Result<Vec<u8>> {
|
||||
use ring::signature::KeyPair as _;
|
||||
let params_oid = info.algorithm.parameters_oid()
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("EC curve OID: {e}")))?;
|
||||
|
||||
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
|
||||
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
|
||||
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||
|
||||
let (ring_alg, curve_oid_der): (&ring::signature::EcdsaSigningAlgorithm, Vec<u8>) = if params_oid == p256_oid {
|
||||
(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7]))
|
||||
} else if params_oid == p384_oid {
|
||||
(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, encode_der_oid(&[1, 3, 132, 0, 34]))
|
||||
} else {
|
||||
return Err(CertGenError::CertBuildFailed(format!("unsupported EC curve: {params_oid}")));
|
||||
};
|
||||
|
||||
let kp = ring::signature::EcdsaKeyPair::from_pkcs8(
|
||||
ring_alg,
|
||||
pkcs8_der,
|
||||
&ring::rand::SystemRandom::new(),
|
||||
).map_err(|e| CertGenError::CertBuildFailed(format!("EC key parse: {e}")))?;
|
||||
let ec_kp = kp.public_key().as_ref().to_vec();
|
||||
|
||||
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (public key) }
|
||||
// AlgorithmIdentifier = SEQUENCE { ecPublicKey OID, curve OID }
|
||||
let ec_oid_der = encode_der_oid(&[1, 2, 840, 10045, 2, 1]);
|
||||
let alg_id = encode_der_sequence(&[&ec_oid_der, &curve_oid_der]);
|
||||
let pub_key_bits = encode_der_bit_string(&ec_kp);
|
||||
|
||||
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||
}
|
||||
|
||||
fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||
use rsa::pkcs8::DecodePrivateKey;
|
||||
|
||||
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der)
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("RSA key parse: {e}")))?;
|
||||
|
||||
let public_key = rsa::RsaPublicKey::from(&private_key);
|
||||
|
||||
// Encode RSA public key as DER: SEQUENCE { n INTEGER, e INTEGER }
|
||||
use rsa::traits::PublicKeyParts;
|
||||
let n_bytes = public_key.n().to_bytes_be();
|
||||
let e_bytes = public_key.e().to_bytes_be();
|
||||
let rsa_pub_der = encode_der_sequence(&[
|
||||
&encode_der_integer(&n_bytes),
|
||||
&encode_der_integer(&e_bytes),
|
||||
]);
|
||||
|
||||
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (DER-encoded RSAPublicKey) }
|
||||
let rsa_oid_der = encode_der_oid(&[1, 2, 840, 113549, 1, 1, 1]);
|
||||
let null_der = vec![0x05, 0x00];
|
||||
let alg_id = encode_der_sequence(&[&rsa_oid_der, &null_der]);
|
||||
let pub_key_bits = encode_der_bit_string(&rsa_pub_der);
|
||||
|
||||
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||
}
|
||||
|
||||
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> {
|
||||
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
// KeyUsage extension (critical)
|
||||
let ku_byte = map_key_usage_byte(purposes);
|
||||
if ku_byte != 0 {
|
||||
let ku_ext = build_key_usage_extension(ku_byte);
|
||||
extensions.push(ku_ext);
|
||||
}
|
||||
|
||||
// Attestation extension (non-critical)
|
||||
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
|
||||
extensions.push(attest_ext);
|
||||
|
||||
Ok(encode_der_sequence_of(&extensions))
|
||||
}
|
||||
|
||||
fn build_extension(oid_der: &[u8], critical: bool, value_der: &[u8]) -> Vec<u8> {
|
||||
let value_octet_string = encode_der_octet_string(value_der);
|
||||
if critical {
|
||||
let critical_der = encode_der_boolean(true);
|
||||
encode_der_sequence(&[oid_der, &critical_der, &value_octet_string])
|
||||
} else {
|
||||
encode_der_sequence(&[oid_der, &value_octet_string])
|
||||
}
|
||||
}
|
||||
|
||||
fn build_key_usage_extension(ku_byte: u8) -> Vec<u8> {
|
||||
// DER BIT STRING: minimal encoding requires trimming trailing zero bits
|
||||
let unused_bits = ku_byte.trailing_zeros().min(7) as u8;
|
||||
|
||||
// BIT STRING = tag (0x03) + length(2) + unused_bits + byte
|
||||
let bit_string = vec![0x03, 0x02, unused_bits, ku_byte];
|
||||
|
||||
let oid_der = encode_der_oid(OID_KEY_USAGE);
|
||||
let value_octet_string = encode_der_octet_string(&bit_string);
|
||||
let critical_der = encode_der_boolean(true);
|
||||
|
||||
encode_der_sequence(&[&oid_der, &critical_der, &value_octet_string])
|
||||
}
|
||||
|
||||
// KeyUsage BIT STRING byte layout (RFC 5280):
|
||||
// byte[0] bit 7 = digitalSignature (0x80)
|
||||
// byte[0] bit 6 = nonRepudiation (0x40)
|
||||
// byte[0] bit 5 = keyEncipherment (0x20)
|
||||
// byte[0] bit 4 = dataEncipherment (0x10)
|
||||
// byte[0] bit 3 = keyAgreement (0x08)
|
||||
// byte[0] bit 2 = keyCertSign (0x04)
|
||||
// byte[0] bit 1 = cRLSign (0x02)
|
||||
// byte[0] bit 0 = encipherOnly (0x01)
|
||||
// byte[1] bit 7 = decipherOnly (0x80)
|
||||
fn map_key_usage_byte(purposes: &[i32]) -> u8 {
|
||||
let mut bits: u8 = 0;
|
||||
for &purpose in purposes {
|
||||
match purpose {
|
||||
2 => bits |= 0x80, // SIGN -> digitalSignature
|
||||
1 => bits |= 0x10, // DECRYPT -> dataEncipherment
|
||||
5 => bits |= 0x20, // WRAP_KEY -> keyEncipherment
|
||||
6 => bits |= 0x08, // AGREE_KEY -> keyAgreement
|
||||
7 => bits |= 0x04, // ATTEST_KEY -> keyCertSign
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
fn encode_validity(not_before: &OffsetDateTime, not_after: &OffsetDateTime) -> Vec<u8> {
|
||||
let nb = encode_time(not_before);
|
||||
let na = encode_time(not_after);
|
||||
encode_der_sequence(&[&nb, &na])
|
||||
}
|
||||
|
||||
fn encode_time(dt: &OffsetDateTime) -> Vec<u8> {
|
||||
let year = dt.year();
|
||||
if (1950..2050).contains(&year) {
|
||||
encode_utctime(dt)
|
||||
} else {
|
||||
encode_gentime(dt)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_utctime(dt: &OffsetDateTime) -> Vec<u8> {
|
||||
// UTCTime: YYMMDDHHMMSSZ
|
||||
let year = dt.year() % 100;
|
||||
let s = format!(
|
||||
"{:02}{:02}{:02}{:02}{:02}{:02}Z",
|
||||
year, dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
|
||||
);
|
||||
let mut out = Vec::with_capacity(2 + s.len());
|
||||
out.push(0x17); // UTCTime tag
|
||||
out.extend_from_slice(&encode_der_length_bytes(s.len()));
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_gentime(dt: &OffsetDateTime) -> Vec<u8> {
|
||||
// GeneralizedTime: YYYYMMDDHHMMSSZ
|
||||
let s = format!(
|
||||
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
|
||||
dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
|
||||
);
|
||||
let mut out = Vec::with_capacity(2 + s.len());
|
||||
out.push(0x18); // GeneralizedTime tag
|
||||
out.extend_from_slice(&encode_der_length_bytes(s.len()));
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_simple_cn_dn(cn: &str) -> Vec<u8> {
|
||||
// Name = SEQUENCE OF RelativeDistinguishedName
|
||||
// RDN = SET OF AttributeTypeAndValue
|
||||
// ATV = SEQUENCE { OID, UTF8String }
|
||||
let cn_oid = encode_der_oid(&[2, 5, 4, 3]);
|
||||
let cn_value = encode_der_utf8string(cn);
|
||||
let atv = encode_der_sequence(&[&cn_oid, &cn_value]);
|
||||
let rdn = encode_der_set(&[&atv]);
|
||||
encode_der_sequence(&[&rdn])
|
||||
}
|
||||
|
||||
fn timestamp_to_datetime(ts: i64) -> Result<OffsetDateTime> {
|
||||
if ts == -1 {
|
||||
return Ok(OffsetDateTime::now_utc());
|
||||
}
|
||||
OffsetDateTime::from_unix_timestamp(ts / 1000)
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("invalid timestamp {ts}: {e}")))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DER encoding primitives
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn encode_der_length_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 0x80 {
|
||||
vec![len as u8]
|
||||
} else if len <= 0xFF {
|
||||
vec![0x81, len as u8]
|
||||
} else if len <= 0xFFFF {
|
||||
vec![0x82, (len >> 8) as u8, len as u8]
|
||||
} else if len <= 0xFF_FFFF {
|
||||
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||
} else {
|
||||
vec![0x84, (len >> 24) as u8, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_der_tag_length_value(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(1 + 4 + content.len());
|
||||
out.push(tag);
|
||||
out.extend_from_slice(&encode_der_length_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_der_sequence(items: &[&[u8]]) -> Vec<u8> {
|
||||
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||
let mut content = Vec::with_capacity(total);
|
||||
for item in items {
|
||||
content.extend_from_slice(item);
|
||||
}
|
||||
encode_der_tag_length_value(0x30, &content)
|
||||
}
|
||||
|
||||
fn encode_der_sequence_of(items: &[Vec<u8>]) -> Vec<u8> {
|
||||
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||
let mut content = Vec::with_capacity(total);
|
||||
for item in items {
|
||||
content.extend_from_slice(item);
|
||||
}
|
||||
encode_der_tag_length_value(0x30, &content)
|
||||
}
|
||||
|
||||
fn encode_der_set(items: &[&[u8]]) -> Vec<u8> {
|
||||
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||
let mut content = Vec::with_capacity(total);
|
||||
for item in items {
|
||||
content.extend_from_slice(item);
|
||||
}
|
||||
encode_der_tag_length_value(0x31, &content)
|
||||
}
|
||||
|
||||
fn encode_der_explicit_tag(tag_num: u8, content: &[u8]) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0xA0 | tag_num, content)
|
||||
}
|
||||
|
||||
fn encode_der_integer(value: &[u8]) -> Vec<u8> {
|
||||
// DER INTEGER must have minimal encoding and leading 0x00 if high bit set
|
||||
if value.is_empty() {
|
||||
return encode_der_tag_length_value(0x02, &[0x00]);
|
||||
}
|
||||
|
||||
// Strip leading zeros (but keep at least one byte)
|
||||
let mut start = 0;
|
||||
while start < value.len() - 1 && value[start] == 0 {
|
||||
start += 1;
|
||||
}
|
||||
let trimmed = &value[start..];
|
||||
|
||||
// Add leading 0x00 if high bit is set (positive integer)
|
||||
if trimmed[0] & 0x80 != 0 {
|
||||
let mut padded = Vec::with_capacity(1 + trimmed.len());
|
||||
padded.push(0x00);
|
||||
padded.extend_from_slice(trimmed);
|
||||
encode_der_tag_length_value(0x02, &padded)
|
||||
} else {
|
||||
encode_der_tag_length_value(0x02, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_der_bit_string(bits: &[u8]) -> Vec<u8> {
|
||||
// BIT STRING: tag 0x03, length, unused_bits (0), content
|
||||
let mut content = Vec::with_capacity(1 + bits.len());
|
||||
content.push(0x00); // 0 unused bits
|
||||
content.extend_from_slice(bits);
|
||||
encode_der_tag_length_value(0x03, &content)
|
||||
}
|
||||
|
||||
fn encode_der_octet_string(content: &[u8]) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0x04, content)
|
||||
}
|
||||
|
||||
fn encode_der_utf8string(s: &str) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0x0C, s.as_bytes())
|
||||
}
|
||||
|
||||
fn encode_der_boolean(val: bool) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0x01, &[if val { 0xFF } else { 0x00 }])
|
||||
}
|
||||
|
||||
fn encode_der_oid(components: &[u64]) -> Vec<u8> {
|
||||
if components.len() < 2 {
|
||||
return encode_der_tag_length_value(0x06, &[]);
|
||||
}
|
||||
|
||||
let mut content = Vec::new();
|
||||
// First two components encoded as 40 * c[0] + c[1]
|
||||
content.push((components[0] * 40 + components[1]) as u8);
|
||||
|
||||
for &c in &components[2..] {
|
||||
encode_oid_subidentifier(&mut content, c);
|
||||
}
|
||||
|
||||
encode_der_tag_length_value(0x06, &content)
|
||||
}
|
||||
|
||||
fn encode_oid_subidentifier(buf: &mut Vec<u8>, mut value: u64) {
|
||||
if value == 0 {
|
||||
buf.push(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Encode in base-128 with continuation bits
|
||||
let mut bytes = Vec::new();
|
||||
while value > 0 {
|
||||
bytes.push((value & 0x7F) as u8);
|
||||
value >>= 7;
|
||||
}
|
||||
bytes.reverse();
|
||||
|
||||
// Set high bit on all but the last byte
|
||||
for i in 0..bytes.len() - 1 {
|
||||
bytes[i] |= 0x80;
|
||||
}
|
||||
|
||||
buf.extend_from_slice(&bytes);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CertGenError {
|
||||
Jni(String),
|
||||
NullParam(&'static str),
|
||||
UnsupportedAlgorithm(i32),
|
||||
UnsupportedEcCurve(i32),
|
||||
KeyGenFailed(String),
|
||||
CertBuildFailed(String),
|
||||
KeyboxParseFailed(String),
|
||||
AttestationBuildFailed(String),
|
||||
DerError(der::Error),
|
||||
EmptyKeyboxChain,
|
||||
ChallengeTooLong(usize),
|
||||
InvalidParameter(String),
|
||||
SigningFailed(String),
|
||||
SerializationFailed(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CertGenError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Jni(msg) => write!(f, "JNI error: {}", msg),
|
||||
Self::NullParam(name) => write!(f, "null required parameter: {}", name),
|
||||
Self::UnsupportedAlgorithm(v) => write!(f, "unsupported algorithm: {}", v),
|
||||
Self::UnsupportedEcCurve(v) => write!(f, "unsupported EC curve: {}", v),
|
||||
Self::KeyGenFailed(msg) => write!(f, "key generation failed: {}", msg),
|
||||
Self::CertBuildFailed(msg) => write!(f, "certificate build failed: {}", msg),
|
||||
Self::KeyboxParseFailed(msg) => write!(f, "keybox parse failed: {}", msg),
|
||||
Self::AttestationBuildFailed(msg) => write!(f, "attestation build failed: {}", msg),
|
||||
Self::DerError(e) => write!(f, "DER error: {}", e),
|
||||
Self::EmptyKeyboxChain => write!(f, "keybox certificate chain is empty"),
|
||||
Self::ChallengeTooLong(len) => write!(f, "attestation challenge too long: {} bytes (max 128)", len),
|
||||
Self::InvalidParameter(msg) => write!(f, "invalid parameter: {}", msg),
|
||||
Self::SigningFailed(msg) => write!(f, "signing failed: {}", msg),
|
||||
Self::SerializationFailed(msg) => write!(f, "serialization failed: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CertGenError {}
|
||||
|
||||
impl From<jni::errors::Error> for CertGenError {
|
||||
fn from(e: jni::errors::Error) -> Self {
|
||||
Self::Jni(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<der::Error> for CertGenError {
|
||||
fn from(e: der::Error) -> Self {
|
||||
Self::DerError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ring::error::Unspecified> for CertGenError {
|
||||
fn from(e: ring::error::Unspecified) -> Self {
|
||||
Self::KeyGenFailed(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ring::error::KeyRejected> for CertGenError {
|
||||
fn from(e: ring::error::KeyRejected) -> Self {
|
||||
Self::KeyGenFailed(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rsa::Error> for CertGenError {
|
||||
fn from(e: rsa::Error) -> Self {
|
||||
Self::KeyGenFailed(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CertGenError>;
|
||||
@@ -0,0 +1,96 @@
|
||||
use crate::error::{CertGenError, Result};
|
||||
use der::{Decode, Encode};
|
||||
use x509_cert::Certificate;
|
||||
|
||||
pub struct ParsedKeybox {
|
||||
pub signing_key_der: Vec<u8>,
|
||||
pub issuer_dn_der: Vec<u8>,
|
||||
pub cert_chain_ders: Vec<Vec<u8>>,
|
||||
pub leaf_not_after: i64,
|
||||
}
|
||||
|
||||
pub fn parse_keybox(cert_chain_bytes: &[u8], private_key_bytes: &[u8]) -> Result<ParsedKeybox> {
|
||||
let certs = split_der_certificates(cert_chain_bytes)?;
|
||||
if certs.is_empty() {
|
||||
return Err(CertGenError::KeyboxParseFailed("no certificates found".into()));
|
||||
}
|
||||
|
||||
let leaf = Certificate::from_der(&certs[0])
|
||||
.map_err(|e| CertGenError::KeyboxParseFailed(format!("leaf cert parse: {e}")))?;
|
||||
|
||||
let issuer_dn_der = leaf.tbs_certificate.subject.to_der()
|
||||
.map_err(|e| CertGenError::KeyboxParseFailed(format!("subject DN encode: {e}")))?;
|
||||
|
||||
let not_after = leaf.tbs_certificate.validity.not_after;
|
||||
let leaf_not_after = not_after.to_unix_duration().as_secs() as i64;
|
||||
|
||||
Ok(ParsedKeybox {
|
||||
signing_key_der: private_key_bytes.to_vec(),
|
||||
issuer_dn_der,
|
||||
cert_chain_ders: certs,
|
||||
leaf_not_after,
|
||||
})
|
||||
}
|
||||
|
||||
fn split_der_certificates(data: &[u8]) -> Result<Vec<Vec<u8>>> {
|
||||
let mut certs = Vec::new();
|
||||
let mut offset = 0;
|
||||
|
||||
while offset < data.len() {
|
||||
if data[offset] != 0x30 {
|
||||
return Err(CertGenError::KeyboxParseFailed(
|
||||
format!("expected SEQUENCE tag 0x30 at offset {offset}, got 0x{:02x}", data[offset])
|
||||
));
|
||||
}
|
||||
|
||||
let (content_len, header_len) = parse_der_length(&data[offset + 1..])?;
|
||||
let total_len = 1 + header_len + content_len;
|
||||
|
||||
if offset + total_len > data.len() {
|
||||
return Err(CertGenError::KeyboxParseFailed(
|
||||
format!("cert at offset {offset} extends beyond buffer: need {total_len}, have {}", data.len() - offset)
|
||||
));
|
||||
}
|
||||
|
||||
certs.push(data[offset..offset + total_len].to_vec());
|
||||
offset += total_len;
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
return Err(CertGenError::KeyboxParseFailed("no certificates in chain".into()));
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
// Returns (content_length, number_of_length_bytes_consumed)
|
||||
fn parse_der_length(data: &[u8]) -> Result<(usize, usize)> {
|
||||
if data.is_empty() {
|
||||
return Err(CertGenError::KeyboxParseFailed("truncated DER length".into()));
|
||||
}
|
||||
|
||||
let first = data[0];
|
||||
|
||||
if first < 0x80 {
|
||||
// Short form: length is the byte itself
|
||||
return Ok((first as usize, 1));
|
||||
}
|
||||
|
||||
// Long form: low 7 bits = number of subsequent length bytes
|
||||
let num_bytes = (first & 0x7f) as usize;
|
||||
if num_bytes == 0 || num_bytes > 4 {
|
||||
return Err(CertGenError::KeyboxParseFailed(
|
||||
format!("unsupported DER length encoding: 0x{first:02x}")
|
||||
));
|
||||
}
|
||||
if 1 + num_bytes > data.len() {
|
||||
return Err(CertGenError::KeyboxParseFailed("truncated multi-byte DER length".into()));
|
||||
}
|
||||
|
||||
let mut len: usize = 0;
|
||||
for i in 0..num_bytes {
|
||||
len = (len << 8) | (data[1 + i] as usize);
|
||||
}
|
||||
|
||||
Ok((len, 1 + num_bytes))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::error::{CertGenError, Result};
|
||||
use crate::types::{Algorithm, EcCurve, GeneratedKeyPair};
|
||||
|
||||
pub fn generate_key_pair(
|
||||
algorithm: Algorithm,
|
||||
key_size: u32,
|
||||
ec_curve: Option<EcCurve>,
|
||||
rsa_public_exponent: u64,
|
||||
) -> Result<GeneratedKeyPair> {
|
||||
match algorithm {
|
||||
Algorithm::Ec => {
|
||||
let curve = ec_curve.ok_or_else(|| CertGenError::InvalidParameter("ec_curve required for EC".into()))?;
|
||||
generate_ec_key_pair(curve)
|
||||
}
|
||||
Algorithm::Rsa => generate_rsa_key_pair(key_size, rsa_public_exponent),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_ec_key_pair(curve: EcCurve) -> Result<GeneratedKeyPair> {
|
||||
let alg = match curve {
|
||||
EcCurve::P256 => &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||
EcCurve::P384 => &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
|
||||
_ => return Err(CertGenError::UnsupportedEcCurve(curve as i32)),
|
||||
};
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let pkcs8_doc = ring::signature::EcdsaKeyPair::generate_pkcs8(alg, &rng)?;
|
||||
|
||||
Ok(GeneratedKeyPair {
|
||||
private_key_pkcs8: pkcs8_doc.as_ref().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_rsa_key_pair(key_size: u32, rsa_public_exponent: u64) -> Result<GeneratedKeyPair> {
|
||||
use pkcs8::EncodePrivateKey;
|
||||
|
||||
if !matches!(key_size, 2048 | 3072 | 4096) {
|
||||
return Err(CertGenError::InvalidParameter(
|
||||
format!("RSA key size must be 2048, 3072, or 4096; got {key_size}")
|
||||
));
|
||||
}
|
||||
|
||||
let exp = if rsa_public_exponent == 0 {
|
||||
rsa::BigUint::from(65537u64)
|
||||
} else {
|
||||
rsa::BigUint::from(rsa_public_exponent)
|
||||
};
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let private_key = rsa::RsaPrivateKey::new_with_exp(&mut rng, key_size as usize, &exp)
|
||||
.map_err(|e| CertGenError::KeyGenFailed(e.to_string()))?;
|
||||
|
||||
let pkcs8_der = private_key.to_pkcs8_der()
|
||||
.map_err(|e| CertGenError::SerializationFailed(e.to_string()))?;
|
||||
|
||||
Ok(GeneratedKeyPair {
|
||||
private_key_pkcs8: pkcs8_der.as_bytes().to_vec(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod error;
|
||||
mod types;
|
||||
mod keygen;
|
||||
pub mod keybox;
|
||||
pub mod attestation;
|
||||
pub mod certbuilder;
|
||||
pub mod logging;
|
||||
|
||||
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
|
||||
use jni::sys::{jboolean, jbyteArray, jstring};
|
||||
use jni::JNIEnv;
|
||||
|
||||
use crate::error::{CertGenError, Result};
|
||||
use crate::types::{Algorithm, CertGenParams, EcCurve};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI entry: generateAttestedKeyPair
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_generateAttestedKeyPair(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
config: JObject,
|
||||
) -> jbyteArray {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
generate_attested_inner(&mut env, &config)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(raw)) => raw,
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(%e, "generateAttestedKeyPair failed");
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
format!("NativeCertGen: {e}"),
|
||||
);
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("generateAttestedKeyPair panicked");
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
"NativeCertGen: internal panic",
|
||||
);
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteArray> {
|
||||
let params = extract_config(env, config)?;
|
||||
|
||||
let key_pair = keygen::generate_key_pair(
|
||||
params.algorithm,
|
||||
params.key_size,
|
||||
params.ec_curve,
|
||||
params.rsa_public_exponent,
|
||||
)?;
|
||||
|
||||
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
||||
|
||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||
|
||||
let cert_chain = certbuilder::build_certificate_chain(
|
||||
&key_pair,
|
||||
&attest_ext,
|
||||
&keybox,
|
||||
¶ms,
|
||||
)?;
|
||||
|
||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||
|
||||
let out = env.byte_array_from_slice(&blob)?;
|
||||
Ok(out.into_raw())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI entry: initLogging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_initLogging(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
verbose: jboolean,
|
||||
log_dir: JString,
|
||||
) -> jboolean {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
init_logging_inner(&mut env, verbose, &log_dir)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => 1,
|
||||
Ok(Err(e)) => {
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
format!("NativeCertGen initLogging: {e}"),
|
||||
);
|
||||
0
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
"NativeCertGen initLogging: internal panic",
|
||||
);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logging_inner(env: &mut JNIEnv, verbose: jboolean, log_dir: &JString) -> Result<()> {
|
||||
let dir: String = env.get_string(log_dir)?.into();
|
||||
logging::init(verbose != 0, &dir, 2, 3)
|
||||
.map_err(|e| CertGenError::Jni(format!("logging init failed: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI entry: dumpLogs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_dumpLogs(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jstring {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
dump_logs_inner(&mut env)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(raw)) => raw,
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(%e, "dumpLogs failed");
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("dumpLogs panicked");
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_logs_inner(env: &mut JNIEnv) -> Result<jstring> {
|
||||
logging::dump::execute_dump()
|
||||
.map_err(|e| CertGenError::Jni(format!("dump failed: {e}")))?;
|
||||
|
||||
// Read the dump path written by execute_dump
|
||||
let path = std::fs::read_to_string("/data/adb/tricky_store/.dump_path")
|
||||
.map_err(|e| CertGenError::Jni(format!("read dump path: {e}")))?;
|
||||
|
||||
let jpath = env.new_string(&path)?;
|
||||
Ok(jpath.into_raw())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config extraction from Java CertGenConfig object
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||
let algorithm = get_int(env, config, "algorithm")?;
|
||||
let key_size = get_int(env, config, "keySize")?;
|
||||
let ec_curve_raw = get_int(env, config, "ecCurve")?;
|
||||
let rsa_pub_exp = get_long(env, config, "rsaPublicExponent")?;
|
||||
|
||||
let cert_not_before = get_long(env, config, "certNotBefore")?;
|
||||
let cert_not_after = get_long(env, config, "certNotAfter")?;
|
||||
let security_level = get_int(env, config, "securityLevel")?;
|
||||
let attest_version = get_int(env, config, "attestVersion")?;
|
||||
let keymaster_version = get_int(env, config, "keymasterVersion")?;
|
||||
let os_version = get_int(env, config, "osVersion")?;
|
||||
let os_patch_level = get_int(env, config, "osPatchLevel")?;
|
||||
let vendor_patch_level = get_int(env, config, "vendorPatchLevel")?;
|
||||
let boot_patch_level = get_int(env, config, "bootPatchLevel")?;
|
||||
let creation_datetime = get_long(env, config, "creationDatetime")?;
|
||||
|
||||
let attestation_challenge = get_nullable_byte_array(env, config, "attestationChallenge")?;
|
||||
let purposes = get_int_array(env, config, "purposes")?;
|
||||
let digests = get_int_array(env, config, "digests")?;
|
||||
let cert_serial = get_nullable_byte_array(env, config, "certSerial")?;
|
||||
let cert_subject = get_nullable_byte_array(env, config, "certSubject")?;
|
||||
let keybox_private_key = get_byte_array(env, config, "keyboxPrivateKey")?;
|
||||
let keybox_cert_chain = get_byte_array(env, config, "keyboxCertChain")?;
|
||||
let boot_key = get_byte_array(env, config, "bootKey")?;
|
||||
let boot_hash = get_byte_array(env, config, "bootHash")?;
|
||||
let attestation_app_id = get_byte_array(env, config, "attestationApplicationId")?;
|
||||
let module_hash = get_nullable_byte_array(env, config, "moduleHash")?;
|
||||
|
||||
let id_brand = get_nullable_byte_array(env, config, "idBrand")?;
|
||||
let id_device = get_nullable_byte_array(env, config, "idDevice")?;
|
||||
let id_product = get_nullable_byte_array(env, config, "idProduct")?;
|
||||
let id_serial = get_nullable_byte_array(env, config, "idSerial")?;
|
||||
let id_imei = get_nullable_byte_array(env, config, "idImei")?;
|
||||
let id_meid = get_nullable_byte_array(env, config, "idMeid")?;
|
||||
let id_manufacturer = get_nullable_byte_array(env, config, "idManufacturer")?;
|
||||
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
||||
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
||||
|
||||
Ok(CertGenParams {
|
||||
algorithm: Algorithm::try_from(algorithm)?,
|
||||
key_size: key_size as u32,
|
||||
ec_curve: if algorithm == 3 {
|
||||
Some(EcCurve::try_from(ec_curve_raw)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
rsa_public_exponent: rsa_pub_exp as u64,
|
||||
attestation_challenge,
|
||||
purposes,
|
||||
digests,
|
||||
cert_serial,
|
||||
cert_subject,
|
||||
cert_not_before,
|
||||
cert_not_after,
|
||||
keybox_private_key,
|
||||
keybox_cert_chain,
|
||||
security_level,
|
||||
attest_version,
|
||||
keymaster_version,
|
||||
os_version,
|
||||
os_patch_level,
|
||||
vendor_patch_level,
|
||||
boot_patch_level,
|
||||
boot_key,
|
||||
boot_hash,
|
||||
creation_datetime,
|
||||
attestation_application_id: attestation_app_id,
|
||||
module_hash,
|
||||
id_brand,
|
||||
id_device,
|
||||
id_product,
|
||||
id_serial,
|
||||
id_imei,
|
||||
id_meid,
|
||||
id_manufacturer,
|
||||
id_model,
|
||||
id_second_imei,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI field accessor helpers — called 35+ times, justifies the abstraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn get_int(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i32> {
|
||||
Ok(env.get_field(obj, name, "I")?.i()?)
|
||||
}
|
||||
|
||||
fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
|
||||
Ok(env.get_field(obj, name, "J")?.j()?)
|
||||
}
|
||||
|
||||
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||
if field.is_null() {
|
||||
return Err(CertGenError::NullParam(name));
|
||||
}
|
||||
let arr: JByteArray = field.into();
|
||||
let len = env.get_array_length(&arr)?;
|
||||
let mut buf = vec![0i8; len as usize];
|
||||
env.get_byte_array_region(&arr, 0, &mut buf)?;
|
||||
env.delete_local_ref(arr)?;
|
||||
Ok(buf.into_iter().map(|b| b as u8).collect())
|
||||
}
|
||||
|
||||
fn get_nullable_byte_array(
|
||||
env: &mut JNIEnv,
|
||||
obj: &JObject,
|
||||
name: &str,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||
if field.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
let arr: JByteArray = field.into();
|
||||
let len = env.get_array_length(&arr)?;
|
||||
let mut buf = vec![0i8; len as usize];
|
||||
env.get_byte_array_region(&arr, 0, &mut buf)?;
|
||||
env.delete_local_ref(arr)?;
|
||||
Ok(Some(buf.into_iter().map(|b| b as u8).collect()))
|
||||
}
|
||||
|
||||
fn get_int_array(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<Vec<i32>> {
|
||||
let field = env.get_field(obj, name, "[I")?.l()?;
|
||||
if field.is_null() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let arr: JIntArray = field.into();
|
||||
let len = env.get_array_length(&arr)?;
|
||||
let mut buf = vec![0i32; len as usize];
|
||||
env.get_int_array_region(&arr, 0, &mut buf)?;
|
||||
env.delete_local_ref(arr)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Binary result assembly (doc 09 section 4.1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn assemble_result(private_key: &[u8], cert_chain: &[Vec<u8>]) -> Vec<u8> {
|
||||
let total = 4 + private_key.len()
|
||||
+ 4
|
||||
+ cert_chain.iter().map(|c| 4 + c.len()).sum::<usize>();
|
||||
|
||||
let mut buf = Vec::with_capacity(total);
|
||||
|
||||
// Private key segment
|
||||
buf.extend_from_slice(&(private_key.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(private_key);
|
||||
|
||||
// Cert count
|
||||
buf.extend_from_slice(&(cert_chain.len() as u32).to_be_bytes());
|
||||
|
||||
// Each cert: length-prefixed DER
|
||||
for cert in cert_chain {
|
||||
buf.extend_from_slice(&(cert.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(cert);
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const DUMP_DIR: &str = "/sdcard/Download";
|
||||
const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock";
|
||||
const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path";
|
||||
const LOG_DIR: &str = "/data/adb/tricky_store/logs";
|
||||
const BASE_DIR: &str = "/data/adb/tricky_store";
|
||||
const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024;
|
||||
|
||||
struct FlockGuard {
|
||||
_file: File,
|
||||
}
|
||||
|
||||
impl FlockGuard {
|
||||
fn acquire() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
if let Some(parent) = Path::new(LOCK_PATH).parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = File::create(LOCK_PATH)?;
|
||||
let fd = {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
file.as_raw_fd()
|
||||
};
|
||||
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
|
||||
if ret != 0 {
|
||||
return Err("dump already in progress".into());
|
||||
}
|
||||
Ok(Self { _file: file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FlockGuard {
|
||||
fn drop(&mut self) {
|
||||
// flock released automatically when file descriptor closes
|
||||
}
|
||||
}
|
||||
|
||||
fn random_name(len: usize) -> String {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
let idx = rng.gen_range(0..36u8);
|
||||
if idx < 10 {
|
||||
(b'0' + idx) as char
|
||||
} else {
|
||||
(b'a' + idx - 10) as char
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_logcat(tag: &str) -> Vec<u8> {
|
||||
let output = Command::new("logcat")
|
||||
.args(["-d", "-s", tag])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(o) => {
|
||||
let mut data = o.stdout;
|
||||
data.truncate(LOGCAT_SIZE_LIMIT);
|
||||
data
|
||||
}
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_device_info() -> String {
|
||||
let mut info = String::new();
|
||||
|
||||
if let Ok(output) = Command::new("uname").arg("-a").output() {
|
||||
info.push_str(&format!(
|
||||
"uname={}\n",
|
||||
String::from_utf8_lossy(&output.stdout).trim()
|
||||
));
|
||||
}
|
||||
|
||||
for (key, prop) in [
|
||||
("device", "ro.product.device"),
|
||||
("build", "ro.build.display.id"),
|
||||
("android", "ro.build.version.release"),
|
||||
] {
|
||||
if let Ok(output) = Command::new("getprop").arg(prop).output() {
|
||||
info.push_str(&format!(
|
||||
"{}={}\n",
|
||||
key,
|
||||
String::from_utf8_lossy(&output.stdout).trim()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// KSU version
|
||||
if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") {
|
||||
info.push_str(&format!("ksu={}\n", ver.trim()));
|
||||
}
|
||||
|
||||
// Module version from module.prop
|
||||
if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") {
|
||||
for line in prop.lines() {
|
||||
if let Some(ver) = line.strip_prefix("version=") {
|
||||
info.push_str(&format!("module={}\n", ver.trim()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
File::open(path).ok()?.read_to_end(&mut buf).ok()?;
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
fn epoch_millis() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn execute_dump() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _lock = FlockGuard::acquire()?;
|
||||
|
||||
let _ = fs::create_dir_all(DUMP_DIR);
|
||||
let zip_name = format!("{}.zip", random_name(8));
|
||||
let zip_path = format!("{}/{}", DUMP_DIR, zip_name);
|
||||
|
||||
let zip_file = File::create(&zip_path)?;
|
||||
let mut zip = zip::ZipWriter::new(zip_file);
|
||||
let options =
|
||||
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
let mut file_count = 0u32;
|
||||
|
||||
// Log files
|
||||
let log_files = [
|
||||
"certgen.log",
|
||||
"certgen.log.1",
|
||||
"certgen.log.2",
|
||||
"certgen.log.3",
|
||||
"certgen.log.4",
|
||||
];
|
||||
for name in &log_files {
|
||||
let path = format!("{}/{}", LOG_DIR, name);
|
||||
if let Some(data) = read_file_bytes(&path) {
|
||||
zip.start_file(*name, options)?;
|
||||
zip.write_all(&data)?;
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Logcat
|
||||
let logcat = collect_logcat("TEESimulator");
|
||||
if !logcat.is_empty() {
|
||||
zip.start_file("logcat-teesimulator.log", options)?;
|
||||
zip.write_all(&logcat)?;
|
||||
file_count += 1;
|
||||
}
|
||||
|
||||
// Config files
|
||||
for name in ["tee_status.txt", "security_patch.txt"] {
|
||||
let path = format!("{}/{}", BASE_DIR, name);
|
||||
if let Some(data) = read_file_bytes(&path) {
|
||||
zip.start_file(name, options)?;
|
||||
zip.write_all(&data)?;
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Device info
|
||||
let device_info = collect_device_info();
|
||||
if !device_info.is_empty() {
|
||||
zip.start_file("device-info.txt", options)?;
|
||||
zip.write_all(device_info.as_bytes())?;
|
||||
file_count += 1;
|
||||
}
|
||||
|
||||
// Manifest
|
||||
let manifest = serde_json::json!({
|
||||
"timestamp": epoch_millis(),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"files": file_count,
|
||||
});
|
||||
zip.start_file("manifest.json", options)?;
|
||||
zip.write_all(manifest.to_string().as_bytes())?;
|
||||
|
||||
zip.finish()?;
|
||||
|
||||
let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0);
|
||||
fs::write(DUMP_PATH_FILE, &zip_path)?;
|
||||
|
||||
let result = serde_json::json!({
|
||||
"zip": zip_path,
|
||||
"size": zip_size,
|
||||
"files": file_count + 1, // +1 for manifest
|
||||
});
|
||||
println!("{}", result);
|
||||
|
||||
tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
use tracing::field::{Field, Visit};
|
||||
use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::layer::Context;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
const KMSG_PATH: &str = "/dev/kmsg";
|
||||
const TAG: &str = "TEESimulator";
|
||||
|
||||
pub struct KmsgLayer {
|
||||
writer: Mutex<Option<File>>,
|
||||
}
|
||||
|
||||
impl KmsgLayer {
|
||||
pub fn new() -> Self {
|
||||
let file = OpenOptions::new().write(true).open(KMSG_PATH).ok();
|
||||
Self {
|
||||
writer: Mutex::new(file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn syslog_priority(level: &Level) -> u8 {
|
||||
match *level {
|
||||
Level::ERROR => 3,
|
||||
Level::WARN => 4,
|
||||
Level::INFO => 6,
|
||||
Level::DEBUG | Level::TRACE => 7,
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageVisitor {
|
||||
message: String,
|
||||
fields: String,
|
||||
}
|
||||
|
||||
impl MessageVisitor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message: String::new(),
|
||||
fields: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for MessageVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "message" {
|
||||
let raw = format!("{:?}", value);
|
||||
// Strip surrounding debug quotes if present
|
||||
self.message = raw
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(&raw)
|
||||
.to_string();
|
||||
} else {
|
||||
if !self.fields.is_empty() {
|
||||
self.fields.push(' ');
|
||||
}
|
||||
self.fields.push_str(&format!("{}={:?}", field.name(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Subscriber> Layer<S> for KmsgLayer {
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let mut guard = match self.writer.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return,
|
||||
};
|
||||
let file = match guard.as_mut() {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let priority = syslog_priority(event.metadata().level());
|
||||
let mut visitor = MessageVisitor::new();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let line = if visitor.fields.is_empty() {
|
||||
format!("<{}>{}: {}\n", priority, TAG, visitor.message)
|
||||
} else {
|
||||
format!(
|
||||
"<{}>{}: {} {}\n",
|
||||
priority, TAG, visitor.message, visitor.fields
|
||||
)
|
||||
};
|
||||
|
||||
let _ = file.write_all(line.as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
mod kmsg;
|
||||
mod rotating;
|
||||
pub mod sysfs;
|
||||
pub mod dump;
|
||||
|
||||
use std::path::Path;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
||||
|
||||
pub fn init(
|
||||
verbose_flag: bool,
|
||||
log_dir: &str,
|
||||
max_size_mb: u64,
|
||||
max_files: usize,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let verbose = verbose_flag || Path::new(VERBOSE_MARKER).exists();
|
||||
|
||||
let (max_size, max_files) = if verbose {
|
||||
(5 * 1024 * 1024, 5)
|
||||
} else {
|
||||
(max_size_mb * 1024 * 1024, max_files)
|
||||
};
|
||||
|
||||
let level = if verbose { "trace" } else { "info" };
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
|
||||
|
||||
let kmsg_layer = kmsg::KmsgLayer::new();
|
||||
let rotating_layer = rotating::RotatingFileLayer::new(log_dir, max_size, max_files);
|
||||
let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
|
||||
|
||||
// Idempotent — second call returns Ok instead of propagating SetGlobalDefaultError
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(kmsg_layer)
|
||||
.with(rotating_layer)
|
||||
.with(stderr_layer)
|
||||
.try_init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::field::{Field, Visit};
|
||||
use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::layer::Context;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
struct RotatingState {
|
||||
dir: PathBuf,
|
||||
current: Option<File>,
|
||||
current_size: u64,
|
||||
max_size: u64,
|
||||
max_files: usize,
|
||||
}
|
||||
|
||||
pub struct RotatingFileLayer {
|
||||
state: Mutex<RotatingState>,
|
||||
}
|
||||
|
||||
impl RotatingFileLayer {
|
||||
pub fn new(dir: &str, max_size: u64, max_files: usize) -> Self {
|
||||
let dir = PathBuf::from(dir);
|
||||
let _ = fs::create_dir_all(&dir);
|
||||
let (file, size) = open_current_log(&dir);
|
||||
Self {
|
||||
state: Mutex::new(RotatingState {
|
||||
dir,
|
||||
current: file,
|
||||
current_size: size,
|
||||
max_size,
|
||||
max_files,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_current_log(dir: &Path) -> (Option<File>, u64) {
|
||||
let path = dir.join("certgen.log");
|
||||
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.ok();
|
||||
(file, size)
|
||||
}
|
||||
|
||||
fn rotate(state: &mut RotatingState) {
|
||||
// Close current handle before renaming
|
||||
state.current.take();
|
||||
|
||||
let dir = &state.dir;
|
||||
// Delete the oldest rotated file before shifting
|
||||
let oldest = dir.join(format!("certgen.log.{}", state.max_files));
|
||||
if oldest.exists() {
|
||||
let _ = fs::remove_file(&oldest);
|
||||
}
|
||||
// Shift older files up: .{N} -> .{N+1}
|
||||
for i in (1..state.max_files).rev() {
|
||||
let from = dir.join(format!("certgen.log.{}", i));
|
||||
let to = dir.join(format!("certgen.log.{}", i + 1));
|
||||
if from.exists() {
|
||||
let _ = fs::rename(&from, &to);
|
||||
}
|
||||
}
|
||||
// Current -> .1
|
||||
let current_path = dir.join("certgen.log");
|
||||
let first_rotated = dir.join("certgen.log.1");
|
||||
if current_path.exists() {
|
||||
let _ = fs::rename(¤t_path, &first_rotated);
|
||||
}
|
||||
|
||||
let (file, size) = open_current_log(dir);
|
||||
state.current = file;
|
||||
state.current_size = size;
|
||||
}
|
||||
|
||||
fn epoch_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn level_str(level: &Level) -> &'static str {
|
||||
match *level {
|
||||
Level::ERROR => "ERROR",
|
||||
Level::WARN => "WARN",
|
||||
Level::INFO => "INFO",
|
||||
Level::DEBUG => "DEBUG",
|
||||
Level::TRACE => "TRACE",
|
||||
}
|
||||
}
|
||||
|
||||
struct LogVisitor {
|
||||
message: String,
|
||||
fields: String,
|
||||
}
|
||||
|
||||
impl LogVisitor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message: String::new(),
|
||||
fields: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for LogVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "message" {
|
||||
let raw = format!("{:?}", value);
|
||||
self.message = raw
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(&raw)
|
||||
.to_string();
|
||||
} else {
|
||||
if !self.fields.is_empty() {
|
||||
self.fields.push(' ');
|
||||
}
|
||||
self.fields.push_str(&format!("{}={:?}", field.name(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Subscriber> Layer<S> for RotatingFileLayer {
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let mut state = match self.state.lock() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if state.current_size >= state.max_size {
|
||||
rotate(&mut state);
|
||||
}
|
||||
|
||||
let file = match state.current.as_mut() {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let ts = epoch_secs();
|
||||
let lvl = level_str(event.metadata().level());
|
||||
let target = event.metadata().target();
|
||||
|
||||
let mut visitor = LogVisitor::new();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let line = if visitor.fields.is_empty() {
|
||||
format!("{} [{}] {}: {}\n", ts, lvl, target, visitor.message)
|
||||
} else {
|
||||
format!(
|
||||
"{} [{}] {}: {} {}\n",
|
||||
ts, lvl, target, visitor.message, visitor.fields
|
||||
)
|
||||
};
|
||||
|
||||
if file.write_all(line.as_bytes()).is_ok() {
|
||||
state.current_size += line.len() as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
||||
|
||||
pub fn is_verbose() -> bool {
|
||||
Path::new(VERBOSE_MARKER).exists()
|
||||
}
|
||||
|
||||
pub fn set_verbose_marker(enabled: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if enabled {
|
||||
if let Some(parent) = Path::new(VERBOSE_MARKER).parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(VERBOSE_MARKER, "")?;
|
||||
} else if Path::new(VERBOSE_MARKER).exists() {
|
||||
fs::remove_file(VERBOSE_MARKER)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable() -> Result<(), Box<dyn std::error::Error>> {
|
||||
set_verbose_marker(true)?;
|
||||
tracing::info!("verbose logging enabled via marker file");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable() -> Result<(), Box<dyn std::error::Error>> {
|
||||
set_verbose_marker(false)?;
|
||||
tracing::info!("verbose logging disabled, marker file removed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn status() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let state = if is_verbose() { "enabled" } else { "disabled" };
|
||||
tracing::info!(verbose = state, "verbose marker status");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::error::CertGenError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Algorithm {
|
||||
Rsa = 1,
|
||||
Ec = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for Algorithm {
|
||||
type Error = CertGenError;
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
1 => Ok(Self::Rsa),
|
||||
3 => Ok(Self::Ec),
|
||||
_ => Err(CertGenError::UnsupportedAlgorithm(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum EcCurve {
|
||||
P224 = 0,
|
||||
P256 = 1,
|
||||
P384 = 2,
|
||||
P521 = 3,
|
||||
Curve25519 = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for EcCurve {
|
||||
type Error = CertGenError;
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::P224),
|
||||
1 => Ok(Self::P256),
|
||||
2 => Ok(Self::P384),
|
||||
3 => Ok(Self::P521),
|
||||
4 => Ok(Self::Curve25519),
|
||||
_ => Err(CertGenError::UnsupportedEcCurve(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 algorithm: Algorithm,
|
||||
pub key_size: u32,
|
||||
pub ec_curve: Option<EcCurve>,
|
||||
pub rsa_public_exponent: u64,
|
||||
|
||||
pub attestation_challenge: Option<Vec<u8>>,
|
||||
pub purposes: Vec<i32>,
|
||||
pub digests: Vec<i32>,
|
||||
|
||||
pub cert_serial: Option<Vec<u8>>,
|
||||
pub cert_subject: Option<Vec<u8>>,
|
||||
pub cert_not_before: i64,
|
||||
pub cert_not_after: i64,
|
||||
|
||||
pub keybox_private_key: Vec<u8>,
|
||||
pub keybox_cert_chain: Vec<u8>,
|
||||
|
||||
pub security_level: i32,
|
||||
pub attest_version: i32,
|
||||
pub keymaster_version: i32,
|
||||
|
||||
pub os_version: i32,
|
||||
pub os_patch_level: i32,
|
||||
pub vendor_patch_level: i32,
|
||||
pub boot_patch_level: i32,
|
||||
|
||||
pub boot_key: Vec<u8>,
|
||||
pub boot_hash: Vec<u8>,
|
||||
|
||||
pub creation_datetime: i64,
|
||||
pub attestation_application_id: Vec<u8>,
|
||||
pub module_hash: Option<Vec<u8>>,
|
||||
|
||||
pub id_brand: Option<Vec<u8>>,
|
||||
pub id_device: Option<Vec<u8>>,
|
||||
pub id_product: Option<Vec<u8>>,
|
||||
pub id_serial: Option<Vec<u8>>,
|
||||
pub id_imei: Option<Vec<u8>>,
|
||||
pub id_meid: Option<Vec<u8>>,
|
||||
pub id_manufacturer: Option<Vec<u8>>,
|
||||
pub id_model: Option<Vec<u8>>,
|
||||
pub id_second_imei: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct GeneratedKeyPair {
|
||||
pub private_key_pkcs8: Vec<u8>,
|
||||
}
|
||||
Executable
+262
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build, package, deploy, and verify TEESimulator module ZIPs.
|
||||
# Usage: ./scripts/package.sh [flags]
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/package.sh --release # build release ZIP
|
||||
# ./scripts/package.sh --all --clean # clean build, both variants
|
||||
# ./scripts/package.sh --release --deploy --reboot # build, push, install, reboot
|
||||
# ./scripts/package.sh --deploy --verify # deploy latest ZIP + verify via logcat
|
||||
# ./scripts/package.sh --rust --release # build Rust crate first, then release
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
OUT_DIR="$PROJECT_ROOT/out"
|
||||
|
||||
VARIANT=""
|
||||
CLEAN=false
|
||||
DEPLOY=false
|
||||
REBOOT=false
|
||||
VERIFY=false
|
||||
BUILD_RUST=false
|
||||
CLEAR_KEYS=false
|
||||
TRACE=false
|
||||
ROOT_PROVIDER="ksu"
|
||||
|
||||
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
|
||||
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Build variants (pick one, or --all):
|
||||
--release Build release variant (default if none specified)
|
||||
--debug Build debug variant
|
||||
--all Build both debug and release
|
||||
|
||||
Build options:
|
||||
--clean Run gradle clean before building
|
||||
--rust Build native-certgen Rust crate before Gradle
|
||||
|
||||
Deploy options:
|
||||
--deploy Push ZIP to device and install
|
||||
--reboot Reboot device after install
|
||||
--clear-keys Clear persistent_keys before deploy
|
||||
--verify Run logcat verification after deploy
|
||||
--root PROVIDER Root provider: ksu (default), magisk, apatch
|
||||
|
||||
Misc:
|
||||
-v, --verbose Print every command as it runs (set -x)
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--release) VARIANT="release"; shift ;;
|
||||
--debug) VARIANT="debug"; shift ;;
|
||||
--all) VARIANT="all"; shift ;;
|
||||
--clean) CLEAN=true; shift ;;
|
||||
--deploy) DEPLOY=true; shift ;;
|
||||
--reboot) REBOOT=true; shift ;;
|
||||
--verify) VERIFY=true; shift ;;
|
||||
--rust) BUILD_RUST=true; shift ;;
|
||||
--clear-keys) CLEAR_KEYS=true; shift ;;
|
||||
-v|--verbose) TRACE=true; shift ;;
|
||||
--root) ROOT_PROVIDER="$2"; shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
*) red "Unknown flag: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$VARIANT" ]] && VARIANT="release"
|
||||
[[ "$TRACE" == true ]] && set -x
|
||||
|
||||
case "$ROOT_PROVIDER" in
|
||||
ksu) INSTALL_CMD="ksud module install" ;;
|
||||
magisk) INSTALL_CMD="magisk --install-module" ;;
|
||||
apatch) INSTALL_CMD="/data/adb/apd module install" ;;
|
||||
*) red "Unknown root provider: $ROOT_PROVIDER"; exit 1 ;;
|
||||
esac
|
||||
|
||||
build_rust() {
|
||||
local cargo_toml="$PROJECT_ROOT/native-certgen/Cargo.toml"
|
||||
if [[ ! -f "$cargo_toml" ]]; then
|
||||
red "native-certgen/Cargo.toml not found — skipping Rust build"
|
||||
return 0
|
||||
fi
|
||||
|
||||
bold "==> Building native-certgen (aarch64)"
|
||||
|
||||
if ! command -v cargo-ndk &>/dev/null; then
|
||||
red "cargo-ndk not found. Install: cargo install cargo-ndk"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(cd "$PROJECT_ROOT/native-certgen" && \
|
||||
cargo ndk -t arm64-v8a --platform 29 -- build --release)
|
||||
|
||||
local so="$PROJECT_ROOT/native-certgen/target/aarch64-linux-android/release/libcertgen.so"
|
||||
if [[ -f "$so" ]]; then
|
||||
local size
|
||||
size=$(du -h "$so" | cut -f1)
|
||||
green " libcertgen.so built ($size)"
|
||||
else
|
||||
red " libcertgen.so not found after build"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gradle_build() {
|
||||
local tasks=()
|
||||
|
||||
[[ "$CLEAN" == true ]] && tasks+=(clean)
|
||||
|
||||
case "$VARIANT" in
|
||||
release) tasks+=(zipRelease) ;;
|
||||
debug) tasks+=(zipDebug) ;;
|
||||
all) tasks+=(zipDebug zipRelease) ;;
|
||||
esac
|
||||
|
||||
bold "==> Gradle: ${tasks[*]}"
|
||||
(cd "$PROJECT_ROOT" && ./gradlew "${tasks[@]}")
|
||||
}
|
||||
|
||||
find_latest_zip() {
|
||||
local pattern="$1"
|
||||
ls -t "$OUT_DIR"/$pattern 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
deploy_zip() {
|
||||
local zip="$1"
|
||||
local name
|
||||
name=$(basename "$zip")
|
||||
|
||||
if ! adb get-state &>/dev/null; then
|
||||
red "No ADB device connected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$CLEAR_KEYS" == true ]]; then
|
||||
bold "==> Clearing persistent_keys"
|
||||
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
bold "==> Deploying $name"
|
||||
adb push "$zip" /data/local/tmp/module.zip
|
||||
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
|
||||
green " Installed via $ROOT_PROVIDER"
|
||||
|
||||
if [[ "$REBOOT" == true ]]; then
|
||||
bold "==> Rebooting"
|
||||
adb reboot
|
||||
echo " Waiting for device..."
|
||||
adb wait-for-device
|
||||
sleep 10
|
||||
local pid
|
||||
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
|
||||
if [[ -n "$pid" ]]; then
|
||||
green " Daemon alive (PID $pid)"
|
||||
else
|
||||
yellow " Daemon not yet started — check logcat"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
verify_device() {
|
||||
bold "==> Verification"
|
||||
|
||||
if ! adb get-state &>/dev/null; then
|
||||
red "No ADB device connected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local pid
|
||||
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
|
||||
if [[ -n "$pid" ]]; then
|
||||
green " Daemon: running (PID $pid)"
|
||||
else
|
||||
red " Daemon: not running"
|
||||
fi
|
||||
|
||||
local tee_status
|
||||
tee_status=$(adb shell "cat /data/adb/tricky_store/tee_status.txt" 2>/dev/null || echo "N/A")
|
||||
echo " TEE status: $tee_status"
|
||||
|
||||
local sec_patch
|
||||
sec_patch=$(adb shell "cat /data/adb/tricky_store/security_patch.txt" 2>/dev/null || echo "N/A")
|
||||
echo " Security patch config: $(echo "$sec_patch" | head -1)"
|
||||
|
||||
local errors
|
||||
errors=$(adb logcat -d -s TEESimulator 2>/dev/null | \
|
||||
grep -iE "error|exception" | \
|
||||
grep -v "StrongBox\|SurfaceRuntime\|ClassLoader\|HARDWARE_TYPE_UNAVAILABLE" | \
|
||||
wc -l)
|
||||
if [[ "$errors" -eq 0 ]]; then
|
||||
green " Logcat errors: 0"
|
||||
else
|
||||
yellow " Logcat errors: $errors (run: adb logcat -d -s TEESimulator | grep -iE 'error|exception')"
|
||||
fi
|
||||
|
||||
local throttle_events
|
||||
throttle_events=$(adb logcat -d -s TEESimulator 2>/dev/null | \
|
||||
grep -cE "RATE_LIMITED|CONCURRENT_LIMITED" || true)
|
||||
echo " Rate limit events: $throttle_events"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo ""
|
||||
bold "==> Build Summary"
|
||||
|
||||
local variants=()
|
||||
case "$VARIANT" in
|
||||
release) variants=(Release) ;;
|
||||
debug) variants=(Debug) ;;
|
||||
all) variants=(Debug Release) ;;
|
||||
esac
|
||||
|
||||
for v in "${variants[@]}"; do
|
||||
local zip
|
||||
zip=$(find_latest_zip "*-${v}.zip")
|
||||
if [[ -n "$zip" ]]; then
|
||||
local size
|
||||
size=$(du -h "$zip" | cut -f1)
|
||||
green " $v: $(basename "$zip") ($size)"
|
||||
else
|
||||
red " $v: ZIP not found"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
echo ""
|
||||
bold "TEESimulator-RS package pipeline"
|
||||
echo ""
|
||||
|
||||
[[ "$BUILD_RUST" == true ]] && build_rust
|
||||
|
||||
gradle_build
|
||||
print_summary
|
||||
|
||||
if [[ "$DEPLOY" == true ]]; then
|
||||
local_variant="$VARIANT"
|
||||
[[ "$local_variant" == "all" ]] && local_variant="release"
|
||||
|
||||
cap="${local_variant^}"
|
||||
zip=$(find_latest_zip "*-${cap}.zip")
|
||||
if [[ -z "$zip" ]]; then
|
||||
red "No $cap ZIP found to deploy"
|
||||
exit 1
|
||||
fi
|
||||
deploy_zip "$zip"
|
||||
fi
|
||||
|
||||
[[ "$VERIFY" == true ]] && verify_device
|
||||
|
||||
echo ""
|
||||
green "Done."
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "TEESimulator"
|
||||
rootProject.name = "TEESimulator-RS"
|
||||
|
||||
include(":stub")
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package android.os;
|
||||
|
||||
public class ServiceSpecificException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user