Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e83a846c9 | ||
|
|
3aff09e7dd | ||
|
|
c7b0af2d29 | ||
|
|
13a1dd7887 | ||
|
|
21a3cb1ec0 | ||
|
|
2b31d4ef47 | ||
|
|
38e9b547a5 | ||
|
|
94c8e5b182 | ||
|
|
8fdc59a142 | ||
|
|
b6f9d7b486 | ||
|
|
99e6b0b5ea | ||
|
|
397bb6338b | ||
|
|
cb81116701 | ||
|
|
45477a7898 | ||
|
|
634a1293c1 | ||
|
|
f115eda2dc | ||
|
|
217edf61fe | ||
|
|
ee5bf2e1a7 | ||
|
|
2181157cb6 | ||
|
|
aa4917e623 | ||
|
|
f06cb30b40 | ||
|
|
03c71bd202 | ||
|
|
7e2fc0b288 | ||
|
|
258a65ba59 | ||
|
|
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 | ||
|
|
593bcfef83 | ||
|
|
5e68cb5f4b |
@@ -0,0 +1,4 @@
|
||||
# Ensure shell scripts always have LF line endings, even on Windows.
|
||||
# These get packaged into flashable zips and run on Android devices.
|
||||
*.sh text eol=lf
|
||||
module/daemon text eol=lf
|
||||
+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,109 @@ 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
|
||||
- **[MhmRdd](https://github.com/MhmRdd)** — AOSP compliance improvements via upstream [PR #157](https://github.com/JingMatrix/TEESimulator/pull/157), including authorize_create enforcement, attestation extension alignment, and binder transaction filtering
|
||||
- **[fatalcoder524](https://github.com/fatalcoder524)** — a real contributor and collaborator on this project
|
||||
- **[huguangares](https://github.com/huguangares)** — collaborator and tester
|
||||
|
||||
---
|
||||
|
||||
## 📄 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>
|
||||
|
||||
+40
-11
@@ -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.1"
|
||||
val verName = "v5.1"
|
||||
|
||||
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,13 +108,13 @@ 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) {
|
||||
@@ -94,6 +123,7 @@ androidComponents {
|
||||
dependsOn("minify${capitalized}WithR8")
|
||||
}
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
dependsOn(buildRustCertgen)
|
||||
|
||||
if (isDebug) {
|
||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||
@@ -115,8 +145,8 @@ androidComponents {
|
||||
"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")
|
||||
into("lib")
|
||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||
}
|
||||
|
||||
// Now, copy and process the files from 'module' directory.
|
||||
@@ -131,8 +161,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 +172,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 +185,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 +199,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 +212,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 { *; }
|
||||
|
||||
@@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 23)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -DNDEBUG")
|
||||
|
||||
# LSPlt configuration
|
||||
OPTION(LSPLT_BUILD_SHARED OFF)
|
||||
@@ -12,7 +13,7 @@ add_subdirectory(external/LSPlt/lsplt/src/main/jni)
|
||||
|
||||
add_compile_definitions(BINDER_DISABLE_NATIVE_HANDLE)
|
||||
add_library(utils SHARED stub/stub_utils.cpp)
|
||||
target_include_directories(utils PUBLIC external/AOSP/include compat)
|
||||
target_include_directories(utils PUBLIC external/AOSP/include)
|
||||
|
||||
add_library(binder SHARED stub/stub_binder.cpp)
|
||||
target_include_directories(binder PUBLIC external/AOSP/include)
|
||||
@@ -22,7 +23,10 @@ add_executable(libinject.so inject/main.cpp inject/utils.cpp)
|
||||
target_include_directories(libinject.so PUBLIC include)
|
||||
target_link_libraries(libinject.so PRIVATE lsplt_static)
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp compat/refbase_compat.cpp)
|
||||
add_executable(libsupervisor.so supervisor.cpp)
|
||||
target_link_libraries(libsupervisor.so PRIVATE log)
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
|
||||
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
|
||||
|
||||
|
||||
@@ -235,6 +235,8 @@ class BinderInterceptor : public BBinder {
|
||||
struct RegistrationEntry {
|
||||
wp<IBinder> target;
|
||||
sp<IBinder> callback_interface;
|
||||
// Transaction codes to intercept. Empty = intercept all (legacy behavior).
|
||||
std::vector<uint32_t> filtered_codes;
|
||||
};
|
||||
|
||||
// Reader-Writer lock for the registry to allow concurrent reads (lookups)
|
||||
@@ -244,10 +246,15 @@ class BinderInterceptor : public BBinder {
|
||||
public:
|
||||
BinderInterceptor() = default;
|
||||
|
||||
// Checks if a specific Binder instance is currently registered for interception
|
||||
bool isBinderIntercepted(const wp<BBinder> &target) const {
|
||||
// Checks if a specific Binder+code combination should be intercepted.
|
||||
// Returns true if the binder is registered AND the code is in its filter
|
||||
// (or the filter is empty, meaning intercept everything).
|
||||
bool shouldIntercept(const wp<BBinder> &target, uint32_t code) const {
|
||||
std::shared_lock lock(registry_mutex_);
|
||||
return registry_.find(target) != registry_.end();
|
||||
auto it = registry_.find(target);
|
||||
if (it == registry_.end()) return false;
|
||||
const auto &codes = it->second.filtered_codes;
|
||||
return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end();
|
||||
}
|
||||
|
||||
// Main entry point for processing the "Man-in-the-Middle" logic
|
||||
@@ -276,6 +283,12 @@ static sp<BinderInterceptor> g_interceptor_instance = nullptr;
|
||||
// =============================================================================================
|
||||
|
||||
class BinderStub : public BBinder {
|
||||
public:
|
||||
const String16& getInterfaceDescriptor() const override {
|
||||
static const String16 kDescriptor("org.matrix.TEESimulator.BinderStub");
|
||||
return kDescriptor;
|
||||
}
|
||||
|
||||
protected:
|
||||
status_t onTransact(uint32_t code, const Parcel &data, Parcel *reply, uint32_t flags) override {
|
||||
if (code != intercept::kBackdoorCode) {
|
||||
@@ -342,15 +355,14 @@ static sp<BinderStub> g_stub_instance = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief Analyses a binder transaction. If the target is monitored,
|
||||
* hijacks the transaction by rewriting its destination to our BinderStub.
|
||||
* @param txn_data Pointer to the transaction data within the ioctl buffer.
|
||||
*/
|
||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
if (!txn_data || txn_data->target.ptr == 0)
|
||||
return;
|
||||
|
||||
// Skip system transactions (PING, INTERFACE, DUMP) to avoid latency detectors
|
||||
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||
return;
|
||||
|
||||
bool hijack = false;
|
||||
ThreadTransactionInfo info;
|
||||
|
||||
@@ -376,18 +388,17 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
// The raw pointer to the binder object itself is stored in the cookie
|
||||
BBinder *target_binder_ptr = reinterpret_cast<BBinder *>(txn_data->cookie);
|
||||
|
||||
// This is safe ONLY because we successfully called attemptIncStrong().
|
||||
// The sp<> constructor will not increment the ref count again, it just adopts the one we have.
|
||||
// When sp_target goes out of scope, it will call decStrong(), releasing our temporary reference.
|
||||
sp<BBinder> sp_target = sp<BBinder>::fromExisting(target_binder_ptr);
|
||||
// Create a weak pointer for the lookup and to store in our context map.
|
||||
// This is safe because we are holding a strong reference.
|
||||
wp<BBinder> wp_target = target_binder_ptr;
|
||||
|
||||
// Now we can safely use sp_target (which implicitly converts to a wp) for the lookup.
|
||||
if (g_interceptor_instance->isBinderIntercepted(sp_target)) {
|
||||
if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) {
|
||||
info.transaction_code = txn_data->code;
|
||||
info.target_binder = sp_target; // Assign the valid weak pointer
|
||||
info.target_binder = wp_target; // Assign the valid weak pointer
|
||||
hijack = true;
|
||||
}
|
||||
// No need to manually call decStrong(); the sp destructor handles it.
|
||||
// Manually release the temporary strong reference we acquired at the start.
|
||||
target_binder_ptr->decStrong(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,12 +543,26 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) {
|
||||
return BAD_TYPE;
|
||||
}
|
||||
|
||||
// Read optional transaction code filter. If present: int32 count + count * uint32 codes.
|
||||
// If absent or count <= 0: intercept all transaction codes (legacy behavior).
|
||||
std::vector<uint32_t> codes;
|
||||
int32_t code_count = 0;
|
||||
if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) {
|
||||
codes.reserve(code_count);
|
||||
for (int32_t i = 0; i < code_count; i++) {
|
||||
uint32_t c = 0;
|
||||
if (data.readUint32(&c) == OK) codes.push_back(c);
|
||||
}
|
||||
LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size());
|
||||
} else {
|
||||
LOGI("Interceptor registered for binder %p (all codes)", target.get());
|
||||
}
|
||||
|
||||
wp<IBinder> weak_target = target;
|
||||
|
||||
std::unique_lock lock(registry_mutex_);
|
||||
registry_[weak_target] = {weak_target, callback};
|
||||
registry_[weak_target] = {weak_target, callback, std::move(codes)};
|
||||
|
||||
LOGI("Interceptor registered for binder %p", target.get());
|
||||
return OK;
|
||||
}
|
||||
|
||||
@@ -589,7 +614,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
||||
|
||||
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) {
|
||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id);
|
||||
return false; // Callback failed, proceed as if not intercepted
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t action = pre_resp.readInt32();
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
#include "refbase_compat.h"
|
||||
#include "utils/RefBase.h"
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <cstring> // For memcpy
|
||||
#include <dlfcn.h>
|
||||
#include <mutex>
|
||||
#include <sys/system_properties.h>
|
||||
|
||||
namespace android {
|
||||
|
||||
// Helper function to get the Android API level at runtime.
|
||||
// It caches the result for performance.
|
||||
int32_t get_android_api_level() {
|
||||
static std::atomic<int32_t> api_level = -1;
|
||||
if (api_level.load(std::memory_order_relaxed) == -1) {
|
||||
char sdk_version_str[PROP_VALUE_MAX];
|
||||
if (__system_property_get("ro.build.version.sdk", sdk_version_str) > 0) {
|
||||
api_level.store(atoi(sdk_version_str), std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
return api_level.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Define the function pointer type for the const member function
|
||||
// RefBase::incStrongRequireStrong.
|
||||
using incStrongRequireStrong_t = void (RefBase::*)(const void *) const;
|
||||
|
||||
// This is the implementation of our compatibility wrapper.
|
||||
void incStrongFromExisting(const RefBase *ref, const void *id) {
|
||||
// Only attempt to use the new function on Android 12 (API 31) or higher.
|
||||
if (get_android_api_level() >= 31) {
|
||||
static incStrongRequireStrong_t sIncStrongRequireStrong = nullptr;
|
||||
static std::once_flag sFlag;
|
||||
|
||||
// Thread-safe, one-time initialization.
|
||||
std::call_once(sFlag, []() {
|
||||
// Find the symbol in the already loaded libraries.
|
||||
// The mangled symbol is _ZNK7android7RefBase22incStrongRequireStrongEPKv
|
||||
void *sym = dlsym(RTLD_DEFAULT,
|
||||
"_ZNK7android7RefBase22incStrongRequireStrongEPKv");
|
||||
if (sym) {
|
||||
// Safely cast the void* symbol to our member function pointer.
|
||||
memcpy(&sIncStrongRequireStrong, &sym, sizeof(void *));
|
||||
}
|
||||
});
|
||||
|
||||
if (sIncStrongRequireStrong) {
|
||||
// If the symbol was found, call it as member function.
|
||||
(ref->*sIncStrongRequireStrong)(id);
|
||||
return; // Success, we are done.
|
||||
}
|
||||
// If dlsym failed for any reason, we fall through to the old method.
|
||||
}
|
||||
|
||||
// Fallback for older Android versions or if dlsym failed.
|
||||
// This calls the universally available incStrong method.
|
||||
ref->incStrong(id);
|
||||
}
|
||||
|
||||
} // namespace android
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace android {
|
||||
|
||||
// Forward-declare the RefBase class.
|
||||
class RefBase;
|
||||
|
||||
// Declares our compatibility function.
|
||||
void incStrongFromExisting(const RefBase *ref, const void *id);
|
||||
|
||||
} // namespace android
|
||||
@@ -17,7 +17,6 @@
|
||||
#ifndef ANDROID_STRONG_POINTER_H
|
||||
#define ANDROID_STRONG_POINTER_H
|
||||
|
||||
#include "refbase_compat.h"
|
||||
#include <functional>
|
||||
#include <type_traits> // for common_type.
|
||||
|
||||
@@ -213,7 +212,7 @@ sp<T> sp<T>::make(Args&&... args) {
|
||||
template <typename T>
|
||||
sp<T> sp<T>::fromExisting(T* other) {
|
||||
if (other) {
|
||||
incStrongFromExisting(other, other);
|
||||
other->incStrongRequireStrong(other);
|
||||
sp<T> result;
|
||||
result.m_ptr = other;
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Fork-based supervisor for instant daemon restart
|
||||
#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;
|
||||
|
||||
static void signal_handler(int sig) {
|
||||
should_exit = 1;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: %s <daemon> [args...]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Forward termination signals to exit cleanly
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGINT, signal_handler);
|
||||
|
||||
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) {
|
||||
perror("fork failed");
|
||||
usleep(100000); // 100ms backoff on fork failure
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Parent: wait for child to exit
|
||||
int status;
|
||||
waitpid(pid, &status, 0);
|
||||
|
||||
if (should_exit) break;
|
||||
|
||||
// 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,6 +13,7 @@ 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
|
||||
|
||||
/**
|
||||
@@ -34,15 +35,16 @@ object App {
|
||||
fun main(args: Array<String>) {
|
||||
SystemLogger.info("Welcome to TEESimulator!")
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
|
||||
SystemLogger.error("Uncaught exception on ${thread.name}", throwable)
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize the Android framework environment
|
||||
prepareEnvironment()
|
||||
// Initialize and start the appropriate keystore interceptors.
|
||||
initializeInterceptors()
|
||||
|
||||
// Load the package configuration.
|
||||
ConfigurationManager.initialize()
|
||||
// Set up the device's boot key and hash, which are crucial for attestation.
|
||||
AndroidDeviceUtils.setupBootKeyAndHash()
|
||||
|
||||
// Android ships with a stripped-down Bouncy Castle provider under the name "BC".
|
||||
@@ -51,6 +53,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()
|
||||
|
||||
@@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import org.bouncycastle.asn1.ASN1Boolean
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1Enumerated
|
||||
@@ -132,8 +135,16 @@ object AttestationBuilder {
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): ASN1Sequence {
|
||||
val creationTime = System.currentTimeMillis()
|
||||
val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel)
|
||||
val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime)
|
||||
|
||||
val uniqueId =
|
||||
if (params.includeUniqueId == true && params.attestationChallenge != null) {
|
||||
computeUniqueId(creationTime, createApplicationId(uid).octets)
|
||||
} else {
|
||||
ByteArray(0)
|
||||
}
|
||||
|
||||
val fields =
|
||||
arrayOf(
|
||||
@@ -146,13 +157,49 @@ object AttestationBuilder {
|
||||
), // keymasterVersion
|
||||
ASN1Enumerated(securityLevel), // keymasterSecurityLevel
|
||||
DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge
|
||||
DEROctetString(ByteArray(0)), // uniqueId
|
||||
DEROctetString(uniqueId),
|
||||
softwareEnforced,
|
||||
teeEnforced,
|
||||
)
|
||||
return DERSequence(fields)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the unique ID per the KeyMint HAL spec:
|
||||
* HMAC-SHA256(T || C || R, HBK) truncated to 128 bits.
|
||||
*
|
||||
* T = temporal counter (creationTime / 2592000000, i.e. 30-day periods since epoch)
|
||||
* C = DER-encoded ATTESTATION_APPLICATION_ID
|
||||
* R = 0x00 (no factory reset since ID rotation)
|
||||
* HBK = device-unique secret generated once during module installation
|
||||
*/
|
||||
private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray {
|
||||
val temporalCounter = creationTimeMs / 2592000000L
|
||||
|
||||
val message =
|
||||
ByteBuffer.allocate(8 + aaidDer.size + 1)
|
||||
.putLong(temporalCounter)
|
||||
.put(aaidDer)
|
||||
.put(0x00) // RESET_SINCE_ID_ROTATION = false
|
||||
.array()
|
||||
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(SecretKeySpec(hbk, "HmacSHA256"))
|
||||
return mac.doFinal(message).copyOf(16)
|
||||
}
|
||||
|
||||
/** Device-unique key seed, generated once at module installation. */
|
||||
private val hbk: ByteArray by lazy {
|
||||
val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk")
|
||||
if (file.exists() && file.length() == 32L) {
|
||||
file.readBytes()
|
||||
} else {
|
||||
// Fallback: generate in-memory (won't persist across reboots)
|
||||
SystemLogger.warning("hbk not found, generating ephemeral HBK.")
|
||||
ByteArray(32).also { java.security.SecureRandom().nextBytes(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */
|
||||
private fun buildTeeEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
@@ -181,23 +228,128 @@ 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()),
|
||||
),
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.blockMode.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_BLOCK_MODE,
|
||||
DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||
|
||||
if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST,
|
||||
DERSet(
|
||||
params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rollbackResistance == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROLLBACK_RESISTANCE,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.earlyBootOnly == true && attestVersion >= 4) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.noAuthRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.allowWhileOnBody == true) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ALLOW_WHILE_ON_BODY,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedUserPresenceRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.trustedConfirmationRequired == true && attestVersion >= 3) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ORIGIN,
|
||||
ASN1Integer(0L),
|
||||
), // KeyOrigin.GENERATED
|
||||
ASN1Integer((params.origin ?: 0).toLong()),
|
||||
),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ROOT_OF_TRUST,
|
||||
buildRootOfTrust(null),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||
@@ -294,20 +446,32 @@ object AttestationBuilder {
|
||||
* Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by
|
||||
* Keystore.
|
||||
*/
|
||||
private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence {
|
||||
val list =
|
||||
mutableListOf<ASN1Encodable>(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(System.currentTimeMillis()),
|
||||
),
|
||||
private fun buildSoftwareEnforcedList(
|
||||
params: KeyMintAttestation,
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
creationTimeMs: Long = System.currentTimeMillis(),
|
||||
): DERSequence {
|
||||
val list = mutableListOf<ASN1Encodable>()
|
||||
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_CREATION_DATETIME,
|
||||
ASN1Integer(creationTimeMs),
|
||||
)
|
||||
)
|
||||
|
||||
// ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present.
|
||||
if (params.attestationChallenge != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ATTESTATION_APPLICATION_ID,
|
||||
createApplicationId(uid),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
@@ -317,7 +481,52 @@ object AttestationBuilder {
|
||||
)
|
||||
)
|
||||
}
|
||||
return DERSequence(list.toTypedArray())
|
||||
|
||||
// Keystore2-enforced tags belong in softwareEnforced, not teeEnforced.
|
||||
// The HAL does not enforce these; keystore2's authorize_create handles them.
|
||||
params.activeDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time))
|
||||
)
|
||||
}
|
||||
params.originationExpireDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME,
|
||||
ASN1Integer(it.time),
|
||||
)
|
||||
)
|
||||
}
|
||||
params.usageExpireDateTime?.let {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_USAGE_EXPIRE_DATETIME,
|
||||
ASN1Integer(it.time),
|
||||
)
|
||||
)
|
||||
}
|
||||
params.usageCountLimit?.let {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_USAGE_COUNT_LIMIT,
|
||||
ASN1Integer(it.toLong()),
|
||||
)
|
||||
)
|
||||
}
|
||||
if (params.unlockedDeviceRequired == true) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED,
|
||||
DERNull.INSTANCE,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,7 +553,18 @@ object AttestationBuilder {
|
||||
* retrieved.
|
||||
*/
|
||||
@Throws(Throwable::class)
|
||||
private fun createApplicationId(uid: Int): DEROctetString {
|
||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||
// AOSP keystore_attestation_id.cpp: gather_attestation_application_id()
|
||||
// uses a hardcoded identity for AID_SYSTEM (1000) and AID_ROOT (0):
|
||||
// packageName = "AndroidSystem", versionCode = 1, no signing digests.
|
||||
val appUid = uid % 100000
|
||||
if (appUid == 0 || appUid == 1000) {
|
||||
return buildApplicationIdDer(
|
||||
listOf("AndroidSystem" to 1L),
|
||||
emptySet(),
|
||||
)
|
||||
}
|
||||
|
||||
val pm =
|
||||
ConfigurationManager.getPackageManager()
|
||||
?: throw IllegalStateException("PackageManager not found!")
|
||||
@@ -352,12 +572,11 @@ object AttestationBuilder {
|
||||
pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid")
|
||||
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
val packageInfoList = mutableListOf<DERSequence>()
|
||||
val packageInfoList = mutableListOf<Pair<String, Long>>()
|
||||
val signatureDigests = mutableSetOf<Digest>()
|
||||
|
||||
// Process all packages associated with the UID in a single loop.
|
||||
val userId = uid / 100000
|
||||
packages.forEach { packageName ->
|
||||
val userId = uid / 100000
|
||||
val packageInfo =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.getPackageInfo(
|
||||
@@ -370,34 +589,36 @@ object AttestationBuilder {
|
||||
pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId)
|
||||
}
|
||||
|
||||
// Add package information (name and version code) to our list.
|
||||
packageInfoList.add(
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(packageInfo.longVersionCode),
|
||||
)
|
||||
)
|
||||
)
|
||||
packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode)
|
||||
|
||||
// Collect unique signature digests from the signing history.
|
||||
packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature ->
|
||||
val digest = sha256.digest(signature.toByteArray())
|
||||
signatureDigests.add(Digest(digest))
|
||||
signatureDigests.add(Digest(sha256.digest(signature.toByteArray())))
|
||||
}
|
||||
}
|
||||
|
||||
// The application ID is a sequence of two sets:
|
||||
// 1. A set of package information (name and version).
|
||||
// 2. A set of SHA-256 digests of the signing certificates.
|
||||
return buildApplicationIdDer(packageInfoList, signatureDigests)
|
||||
}
|
||||
|
||||
private fun buildApplicationIdDer(
|
||||
packages: List<Pair<String, Long>>,
|
||||
digests: Set<Digest>,
|
||||
): DEROctetString {
|
||||
val packageInfoList =
|
||||
packages.map { (name, version) ->
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DEROctetString(name.toByteArray(StandardCharsets.UTF_8)),
|
||||
ASN1Integer(version),
|
||||
)
|
||||
)
|
||||
}
|
||||
val applicationIdSequence =
|
||||
DERSequence(
|
||||
arrayOf(
|
||||
DERSet(packageInfoList.toTypedArray()),
|
||||
DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
|
||||
return DEROctetString(applicationIdSequence.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ object AttestationConstants {
|
||||
|
||||
// --- Key Lifetime and Usage Control ---
|
||||
const val TAG_ROLLBACK_RESISTANCE = 303
|
||||
const val TAG_EARLY_BOOT_ONLY = 305
|
||||
const val TAG_ACTIVE_DATETIME = 400
|
||||
const val TAG_ORIGINATION_EXPIRE_DATETIME = 401
|
||||
const val TAG_USAGE_EXPIRE_DATETIME = 402
|
||||
const val TAG_MAX_BOOT_LEVEL = 403
|
||||
const val TAG_MAX_USES_PER_BOOT = 404
|
||||
const val TAG_USAGE_COUNT_LIMIT = 405
|
||||
|
||||
@@ -56,6 +58,10 @@ object AttestationConstants {
|
||||
const val TAG_NO_AUTH_REQUIRED = 503
|
||||
const val TAG_USER_AUTH_TYPE = 504
|
||||
const val TAG_AUTH_TIMEOUT = 505
|
||||
const val TAG_ALLOW_WHILE_ON_BODY = 506
|
||||
const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507
|
||||
const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508
|
||||
const val TAG_UNLOCKED_DEVICE_REQUIRED = 509
|
||||
|
||||
// --- Attestation and Application Info ---
|
||||
const val TAG_APPLICATION_ID = 601
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.ASN1OctetString
|
||||
@@ -57,55 +52,14 @@ object DeviceAttestationService {
|
||||
val bootPatchLevel: Int?,
|
||||
)
|
||||
|
||||
// A unique alias for the key used to perform the TEE functionality check.
|
||||
private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck"
|
||||
|
||||
/**
|
||||
* Lazily determines if the device's TEE is functional by attempting to generate an
|
||||
* attestation-backed key pair. The result is cached.
|
||||
*/
|
||||
val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() }
|
||||
|
||||
/**
|
||||
* Lazily fetches and parses attestation data from a genuinely generated certificate. The result
|
||||
* is cached. Returns null if the TEE is not functional or parsing fails.
|
||||
*/
|
||||
val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() }
|
||||
|
||||
/**
|
||||
* Checks if the TEE is working correctly by generating a key in the Android Keystore with an
|
||||
* attestation challenge.
|
||||
*
|
||||
* @return `true` if a key with attestation was generated successfully, `false` otherwise.
|
||||
*/
|
||||
private fun checkTeeFunctionality(): Boolean {
|
||||
SystemLogger.info("Performing TEE functionality check...")
|
||||
return try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
val keyPairGenerator =
|
||||
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
|
||||
|
||||
// A random challenge is required for attestation.
|
||||
val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) }
|
||||
|
||||
val spec =
|
||||
KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAttestationChallenge(challenge)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(spec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
SystemLogger.info("TEE functionality check successful.")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.warning("TEE functionality check failed.", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the attestation certificate generated during the TEE check. The key entry is
|
||||
* deleted after retrieval to clean up.
|
||||
@@ -113,8 +67,6 @@ object DeviceAttestationService {
|
||||
* @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable.
|
||||
*/
|
||||
private fun getAttestationCertificate(): X509Certificate? {
|
||||
if (!isTeeFunctional) return null
|
||||
|
||||
return try {
|
||||
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
|
||||
val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS)
|
||||
@@ -249,6 +201,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()}"
|
||||
)
|
||||
|
||||
@@ -16,10 +16,12 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
// Reference:
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs
|
||||
data class KeyMintAttestation(
|
||||
val keySize: Int,
|
||||
val algorithm: Int,
|
||||
val ecCurve: Int,
|
||||
val ecCurve: Int?,
|
||||
val ecCurveName: String,
|
||||
val keySize: Int,
|
||||
val origin: Int?,
|
||||
val noAuthRequired: Boolean?,
|
||||
val blockMode: List<Int>,
|
||||
val padding: List<Int>,
|
||||
val purpose: List<Int>,
|
||||
@@ -39,21 +41,45 @@ data class KeyMintAttestation(
|
||||
val manufacturer: ByteArray?,
|
||||
val model: ByteArray?,
|
||||
val secondImei: ByteArray?,
|
||||
// Enforcement tags
|
||||
val activeDateTime: Date?,
|
||||
val originationExpireDateTime: Date?,
|
||||
val usageExpireDateTime: Date?,
|
||||
val usageCountLimit: Int?,
|
||||
val callerNonce: Boolean?,
|
||||
val unlockedDeviceRequired: Boolean?,
|
||||
val includeUniqueId: Boolean?,
|
||||
val rollbackResistance: Boolean?,
|
||||
val earlyBootOnly: Boolean?,
|
||||
val allowWhileOnBody: Boolean?,
|
||||
val trustedUserPresenceRequired: Boolean?,
|
||||
val trustedConfirmationRequired: Boolean?,
|
||||
val maxUsesPerBoot: Int?,
|
||||
val maxBootLevel: Int?,
|
||||
val minMacLength: Int?,
|
||||
val rsaOaepMgfDigest: List<Int>,
|
||||
) {
|
||||
/** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */
|
||||
constructor(
|
||||
params: Array<KeyParameter>
|
||||
) : this(
|
||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = ALGORITHM, field = Algorithm)]
|
||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = KEY_SIZE, field = Integer)]
|
||||
// For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent.
|
||||
keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(),
|
||||
|
||||
// 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 = NO_AUTH_REQUIRED, field = BoolValue)]
|
||||
noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED),
|
||||
|
||||
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
||||
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||
|
||||
@@ -95,14 +121,44 @@ data class KeyMintAttestation(
|
||||
manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER),
|
||||
model = params.findBlob(Tag.ATTESTATION_ID_MODEL),
|
||||
secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI),
|
||||
|
||||
// Enforcement tags
|
||||
activeDateTime = params.findDate(Tag.ACTIVE_DATETIME),
|
||||
originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME),
|
||||
usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME),
|
||||
usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT),
|
||||
callerNonce = params.findBoolean(Tag.CALLER_NONCE),
|
||||
unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED),
|
||||
includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID),
|
||||
rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE),
|
||||
earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY),
|
||||
allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY),
|
||||
trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED),
|
||||
trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED),
|
||||
maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT),
|
||||
maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL),
|
||||
minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH),
|
||||
rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST),
|
||||
) {
|
||||
// Log all parsed parameters for debugging purposes.
|
||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||
}
|
||||
|
||||
fun isAttestKey(): Boolean {
|
||||
return purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
}
|
||||
|
||||
fun isImportKey(): Boolean {
|
||||
return origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||
}
|
||||
}
|
||||
|
||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||
|
||||
/** Maps to AOSP field = Integer */
|
||||
private fun Array<KeyParameter>.findBoolean(tag: Int): Boolean? =
|
||||
this.find { it.tag == tag }?.value?.boolValue
|
||||
|
||||
/** Maps to AOSP field = Integer */
|
||||
private fun Array<KeyParameter>.findInteger(tag: Int): Int? =
|
||||
this.find { it.tag == tag }?.value?.integer
|
||||
@@ -115,6 +171,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()
|
||||
@@ -131,7 +191,7 @@ private fun Array<KeyParameter>.findBlob(tag: Int): ByteArray? =
|
||||
private fun Array<KeyParameter>.findAllBlockMode(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.blockMode }
|
||||
|
||||
/** Maps to AOSP field = BlockMode (Repeated) */
|
||||
/** Maps to AOSP field = PaddingMode (Repeated) */
|
||||
private fun Array<KeyParameter>.findAllPaddingMode(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.paddingMode }
|
||||
|
||||
@@ -143,6 +203,19 @@ private fun Array<KeyParameter>.findAllKeyPurpose(tag: Int): List<Int> =
|
||||
private fun Array<KeyParameter>.findAllDigests(tag: Int): List<Int> =
|
||||
this.filter { it.tag == tag }.map { it.value.digest }
|
||||
|
||||
/** Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. */
|
||||
private fun Array<KeyParameter>.deriveKeySizeFromCurve(): Int {
|
||||
val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0
|
||||
return when (curveId) {
|
||||
EcCurve.P_224 -> 224
|
||||
EcCurve.P_256 -> 256
|
||||
EcCurve.P_384 -> 384
|
||||
EcCurve.P_521 -> 521
|
||||
EcCurve.CURVE_25519 -> 256
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back
|
||||
* to KEY_SIZE (field=Integer).
|
||||
|
||||
@@ -7,7 +7,6 @@ import android.os.IBinder
|
||||
import android.os.ServiceManager
|
||||
import java.io.File
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
|
||||
@@ -31,7 +30,6 @@ object ConfigurationManager {
|
||||
// --- Configuration Paths ---
|
||||
const val CONFIG_PATH = "/data/adb/tricky_store"
|
||||
private const val TARGET_PACKAGES_FILE = "target.txt"
|
||||
private const val TEE_STATUS_FILE = "tee_status.txt"
|
||||
private const val PATCH_LEVEL_FILE = "security_patch.txt"
|
||||
private const val DEFAULT_KEYBOX_FILE = "keybox.xml"
|
||||
private val configRoot = File(CONFIG_PATH)
|
||||
@@ -39,7 +37,6 @@ object ConfigurationManager {
|
||||
// --- In-Memory Configuration State ---
|
||||
@Volatile private var packageModes = mapOf<String, Mode>()
|
||||
@Volatile private var packageKeyboxes = mapOf<String, String>()
|
||||
@Volatile private var isTeeBroken: Boolean? = null
|
||||
@Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null
|
||||
@Volatile private var packagePatchLevels = mapOf<String, CustomPatchLevel>()
|
||||
|
||||
@@ -68,7 +65,6 @@ object ConfigurationManager {
|
||||
// Initial load of all configuration files.
|
||||
loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE))
|
||||
loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE))
|
||||
storeTeeStatus() // Check and store the current TEE status.
|
||||
|
||||
// Start watching for any subsequent file changes.
|
||||
ConfigObserver.startWatching()
|
||||
@@ -88,7 +84,10 @@ object ConfigurationManager {
|
||||
}
|
||||
|
||||
/** Determines if the certificate for a given UID needs to be patched. */
|
||||
fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH
|
||||
fun shouldPatch(uid: Int): Boolean {
|
||||
val mode = getPackageModeForUid(uid)
|
||||
return mode == Mode.PATCH || mode == Mode.AUTO
|
||||
}
|
||||
|
||||
/** Determines if a new certificate needs to be generated for a given UID. */
|
||||
fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE
|
||||
@@ -96,24 +95,23 @@ object ConfigurationManager {
|
||||
/** Determines if no operation is needed for a given UID. */
|
||||
fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null
|
||||
|
||||
/** Determines if the UID is in AUTO mode (no explicit ! or ? suffix). */
|
||||
fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO
|
||||
|
||||
/** Resolves the operating mode for a given UID based on its packages and the TEE status. */
|
||||
private fun getPackageModeForUid(uid: Int): Mode? {
|
||||
val packages = getPackagesForUid(uid)
|
||||
if (packages.isEmpty()) return null
|
||||
|
||||
// Lazily load TEE status if it hasn't been checked yet.
|
||||
if (isTeeBroken == null) loadTeeStatus()
|
||||
|
||||
// Find the first configured mode for any of the UID's packages.
|
||||
for (pkg in packages) {
|
||||
when (packageModes[pkg]) {
|
||||
Mode.GENERATE -> return Mode.GENERATE
|
||||
Mode.PATCH -> return Mode.PATCH
|
||||
Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH
|
||||
null -> continue // No config for this package, check the next one.
|
||||
Mode.AUTO -> return Mode.AUTO
|
||||
null -> continue
|
||||
}
|
||||
}
|
||||
return null // No configuration found for this UID.
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,25 +156,25 @@ object ConfigurationManager {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val mode: Mode
|
||||
val rawPkg: String
|
||||
when {
|
||||
// Suffix '!' means force GENERATE mode.
|
||||
trimmedLine.endsWith("!") -> {
|
||||
val pkg = trimmedLine.removeSuffix("!").trim()
|
||||
newModes[pkg] = Mode.GENERATE
|
||||
newKeyboxes[pkg] = currentKeybox
|
||||
mode = Mode.GENERATE
|
||||
rawPkg = trimmedLine.removeSuffix("!").trim()
|
||||
}
|
||||
// Suffix '?' means force PATCH mode.
|
||||
trimmedLine.endsWith("?") -> {
|
||||
val pkg = trimmedLine.removeSuffix("?").trim()
|
||||
newModes[pkg] = Mode.PATCH
|
||||
newKeyboxes[pkg] = currentKeybox
|
||||
mode = Mode.PATCH
|
||||
rawPkg = trimmedLine.removeSuffix("?").trim()
|
||||
}
|
||||
// No suffix means AUTO mode.
|
||||
else -> {
|
||||
newModes[trimmedLine] = Mode.AUTO
|
||||
newKeyboxes[trimmedLine] = currentKeybox
|
||||
mode = Mode.AUTO
|
||||
rawPkg = trimmedLine
|
||||
}
|
||||
}
|
||||
|
||||
newModes[rawPkg] = mode
|
||||
newKeyboxes[rawPkg] = currentKeybox
|
||||
}
|
||||
|
||||
// Atomically update the configuration maps.
|
||||
@@ -273,29 +271,6 @@ object ConfigurationManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks the device's TEE status and writes the result to a file for persistence. */
|
||||
private fun storeTeeStatus() {
|
||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
||||
isTeeBroken = !DeviceAttestationService.isTeeFunctional
|
||||
try {
|
||||
statusFile.writeText("tee_broken=$isTeeBroken")
|
||||
SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to write TEE status to file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** Loads the TEE status from the file. */
|
||||
private fun loadTeeStatus() {
|
||||
val statusFile = File(configRoot, TEE_STATUS_FILE)
|
||||
isTeeBroken =
|
||||
if (statusFile.exists()) {
|
||||
statusFile.readText().trim() == "tee_broken=true"
|
||||
} else {
|
||||
null // Status is unknown.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A FileObserver that monitors the configuration directory for changes and triggers reloads of
|
||||
* the relevant settings.
|
||||
@@ -351,6 +326,32 @@ object ConfigurationManager {
|
||||
return iPackageManager
|
||||
}
|
||||
|
||||
/** Checks if any package belonging to the UID holds the given permission. */
|
||||
/** Checks a SELinux permission for a caller identified by PID against the keystore context. */
|
||||
fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean {
|
||||
return try {
|
||||
val callerCtx =
|
||||
java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||
val selfCtx =
|
||||
java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n')
|
||||
android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm)
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if any package belonging to the UID holds the given permission. */
|
||||
fun hasPermissionForUid(uid: Int, permission: String): Boolean {
|
||||
val userId = uid / 100000
|
||||
return getPackagesForUid(uid).any { pkg ->
|
||||
try {
|
||||
getPackageManager()?.checkPermission(permission, pkg, userId) == 0
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Retrieves the package names associated with a UID. */
|
||||
fun getPackagesForUid(uid: Int): Array<String> {
|
||||
return uidToPackagesCache.getOrPut(uid) {
|
||||
|
||||
@@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() {
|
||||
* `handlePostTransact`).
|
||||
*/
|
||||
final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||
// The native hook prepends a transaction ID to the data parcel.
|
||||
val txId = data.readLong()
|
||||
val result =
|
||||
val result = try {
|
||||
when (code) {
|
||||
// These codes are defined in the native layer to distinguish hook types.
|
||||
PRE_TRANSACT_CODE -> handlePreTransact(txId, data)
|
||||
POST_TRANSACT_CODE -> handlePostTransact(txId, data)
|
||||
else -> return super.onTransact(code, data, reply, flags)
|
||||
}
|
||||
|
||||
// The reply parcel is guaranteed to be non-null for our custom transactions.
|
||||
} catch (e: Throwable) {
|
||||
SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e)
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
writeResultToReply(result, reply!!)
|
||||
return true
|
||||
}
|
||||
@@ -293,15 +293,27 @@ abstract class BinderInterceptor : Binder() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Uses the backdoor binder to register an interceptor for a specific target service. */
|
||||
fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) {
|
||||
/**
|
||||
* Uses the backdoor binder to register an interceptor for a specific target service.
|
||||
*
|
||||
* @param filteredCodes If non-empty, only these transaction codes will be intercepted at
|
||||
* the native level. All other codes pass through without the round-trip to Java.
|
||||
*/
|
||||
fun register(
|
||||
backdoor: IBinder,
|
||||
target: IBinder,
|
||||
interceptor: BinderInterceptor,
|
||||
filteredCodes: IntArray = intArrayOf(),
|
||||
) {
|
||||
val data = Parcel.obtain()
|
||||
val reply = Parcel.obtain()
|
||||
try {
|
||||
data.writeStrongBinder(target)
|
||||
data.writeStrongBinder(interceptor)
|
||||
data.writeInt(filteredCodes.size)
|
||||
for (code in filteredCodes) data.writeInt(code)
|
||||
backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0)
|
||||
SystemLogger.info("Registered interceptor for target: $target")
|
||||
SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)")
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to register binder interceptor.", e)
|
||||
} finally {
|
||||
|
||||
+7
-1
@@ -68,11 +68,17 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transaction codes this interceptor needs to handle at the native level. Override in
|
||||
* subclasses to filter; empty means intercept everything (legacy behavior).
|
||||
*/
|
||||
protected open val interceptedCodes: IntArray = intArrayOf()
|
||||
|
||||
/** Registers this interceptor with the native hook layer and sets up a death recipient. */
|
||||
private fun setupInterceptor(service: IBinder, backdoor: IBinder) {
|
||||
keystoreService = service
|
||||
SystemLogger.info("Registering interceptor for service: $serviceName")
|
||||
register(backdoor, service, this)
|
||||
register(backdoor, service, this, interceptedCodes)
|
||||
service.linkToDeath(createDeathRecipient(), 0)
|
||||
onInterceptorReady(service, backdoor)
|
||||
}
|
||||
|
||||
+81
-1
@@ -1,17 +1,34 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.security.KeyStore
|
||||
import android.security.keystore.KeystoreResponse
|
||||
import android.system.keystore2.Authorization
|
||||
import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
|
||||
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)
|
||||
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 +125,69 @@ 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error
|
||||
* code via EX_SERVICE_SPECIFIC.
|
||||
*/
|
||||
fun createServiceSpecificErrorReply(
|
||||
errorCode: Int
|
||||
): BinderInterceptor.TransactionResult.OverrideReply {
|
||||
val parcel =
|
||||
Parcel.obtain().apply {
|
||||
writeException(android.os.ServiceSpecificException(errorCode))
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Patches the system-level authorization values (OS_PATCHLEVEL, VENDOR_PATCHLEVEL,
|
||||
* BOOT_PATCHLEVEL) in an authorization array to match the configured patch levels for the
|
||||
* given calling UID. Each authorization's original [Authorization.securityLevel] is preserved.
|
||||
*
|
||||
* When a patch level is configured as "no" ([AndroidDeviceUtils.DO_NOT_REPORT]), the original
|
||||
* hardware value is kept as-is.
|
||||
*/
|
||||
fun patchAuthorizations(
|
||||
authorizations: Array<Authorization>?,
|
||||
callingUid: Int,
|
||||
): Array<Authorization>? {
|
||||
if (authorizations == null) return null
|
||||
|
||||
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
|
||||
return authorizations
|
||||
.map { auth ->
|
||||
val replacement =
|
||||
when (auth.keyParameter.tag) {
|
||||
Tag.OS_PATCHLEVEL ->
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null
|
||||
Tag.VENDOR_PATCHLEVEL ->
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch
|
||||
else null
|
||||
Tag.BOOT_PATCHLEVEL ->
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null
|
||||
else -> null
|
||||
}
|
||||
if (replacement != null) {
|
||||
Authorization().apply {
|
||||
keyParameter =
|
||||
KeyParameter().apply {
|
||||
tag = auth.keyParameter.tag
|
||||
value = KeyParameterValue.integer(replacement)
|
||||
}
|
||||
securityLevel = auth.securityLevel
|
||||
}
|
||||
} else {
|
||||
auth
|
||||
}
|
||||
}
|
||||
.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
+347
-86
@@ -1,21 +1,26 @@
|
||||
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.Domain
|
||||
import android.system.keystore2.IKeystoreSecurityLevel
|
||||
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.Collections
|
||||
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.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
|
||||
|
||||
/**
|
||||
@@ -42,6 +47,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (Build.VERSION.SDK_INT >= 34)
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched")
|
||||
else null
|
||||
private val GET_NUMBER_OF_ENTRIES_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries")
|
||||
private val GET_SECURITY_LEVEL_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(stubBinderClass, "getSecurityLevel")
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
stubBinderClass.declaredFields
|
||||
@@ -52,15 +61,43 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||
}
|
||||
|
||||
// Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry.
|
||||
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||
|
||||
// Backdoor binder for registering new interceptors at runtime.
|
||||
private var backdoorBinder: IBinder? = null
|
||||
|
||||
// Per-security-level interceptor instances, keyed by SecurityLevel constant.
|
||||
private val securityLevelInterceptors = ConcurrentHashMap<Int, KeyMintSecurityLevelInterceptor>()
|
||||
|
||||
// Identity set of SecurityLevel binders already registered with the native hook,
|
||||
// tracked by System.identityHashCode to avoid re-registering the same BBinder.
|
||||
private val registeredSecurityLevelBinders: MutableSet<Int> =
|
||||
Collections.newSetFromMap(ConcurrentHashMap())
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||
|
||||
override val interceptedCodes: IntArray by lazy {
|
||||
listOfNotNull(
|
||||
GET_KEY_ENTRY_TRANSACTION,
|
||||
DELETE_KEY_TRANSACTION,
|
||||
UPDATE_SUBCOMPONENT_TRANSACTION,
|
||||
LIST_ENTRIES_TRANSACTION,
|
||||
LIST_ENTRIES_BATCHED_TRANSACTION,
|
||||
GET_NUMBER_OF_ENTRIES_TRANSACTION,
|
||||
GET_SECURITY_LEVEL_TRANSACTION,
|
||||
)
|
||||
.toIntArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
backdoorBinder = backdoor
|
||||
val keystoreInterface = IKeystoreService.Stub.asInterface(service)
|
||||
setupSecurityLevelInterceptors(keystoreInterface, backdoor)
|
||||
}
|
||||
@@ -72,7 +109,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
register(backdoor, tee.asBinder(), interceptor)
|
||||
securityLevelInterceptors[SecurityLevel.TRUSTED_ENVIRONMENT] = interceptor
|
||||
registerSecurityLevelBinder(
|
||||
backdoor,
|
||||
tee.asBinder(),
|
||||
interceptor,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
||||
@@ -83,12 +126,42 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...")
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||
register(backdoor, strongbox.asBinder(), interceptor)
|
||||
securityLevelInterceptors[SecurityLevel.STRONGBOX] = interceptor
|
||||
registerSecurityLevelBinder(
|
||||
backdoor,
|
||||
strongbox.asBinder(),
|
||||
interceptor,
|
||||
)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an interceptor for a SecurityLevel binder, tracking the binder identity
|
||||
* to avoid duplicate registrations when keystore2 returns the same BBinder.
|
||||
*/
|
||||
private fun registerSecurityLevelBinder(
|
||||
backdoor: IBinder,
|
||||
binder: IBinder,
|
||||
interceptor: KeyMintSecurityLevelInterceptor,
|
||||
) {
|
||||
val identity = System.identityHashCode(binder)
|
||||
if (registeredSecurityLevelBinders.add(identity)) {
|
||||
register(
|
||||
backdoor,
|
||||
binder,
|
||||
interceptor,
|
||||
KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES,
|
||||
)
|
||||
} else {
|
||||
SystemLogger.debug(
|
||||
"SecurityLevel binder $binder (identity=$identity) already registered, skipping."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPreTransact(
|
||||
txId: Long,
|
||||
target: IBinder,
|
||||
@@ -98,27 +171,22 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
callingPid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
return if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
else TransactionResult.Continue
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
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
|
||||
if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) {
|
||||
TransactionResult.Continue
|
||||
} else {
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error(
|
||||
"[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}",
|
||||
it,
|
||||
)
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
} else {
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
} else if (
|
||||
code == GET_KEY_ENTRY_TRANSACTION ||
|
||||
code == DELETE_KEY_TRANSACTION ||
|
||||
@@ -137,20 +205,40 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
||||
// Handle delete by alias (APP domain) or nspace (KEY_ID domain).
|
||||
val keyId =
|
||||
if (descriptor.alias != null) {
|
||||
KeyIdentifier(callingUid, descriptor.alias)
|
||||
} else if (descriptor.domain == Domain.KEY_ID) {
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid, descriptor.nspace
|
||||
)?.let { info ->
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.entries
|
||||
.find { it.value.nspace == info.nspace && it.key.uid == callingUid }
|
||||
?.key
|
||||
}
|
||||
} else null
|
||||
|
||||
if (keyId != null) {
|
||||
val isSoftwareKey =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
if (isSoftwareKey) {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
|
||||
)
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
}
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
if (descriptor.alias == null) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
val response =
|
||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
?: return TransactionResult.Continue
|
||||
@@ -163,6 +251,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
KeyMintParameterLogger.logParameter(it.keyParameter)
|
||||
}
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
} else if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||
// Pass through to post-hook so we can register interceptors for newly-created
|
||||
// SecurityLevel binders. keystore2 may create a new BBinder per call, so the
|
||||
// initial registration in setupSecurityLevelInterceptors might not cover all
|
||||
// binder instances that clients receive.
|
||||
logTransaction(txId, "getSecurityLevel", callingUid, callingPid)
|
||||
return TransactionResult.Continue
|
||||
} else {
|
||||
logTransaction(
|
||||
txId,
|
||||
@@ -191,12 +286,39 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
|
||||
return TransactionResult.SkipTransaction
|
||||
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
if (code == GET_SECURITY_LEVEL_TRANSACTION) {
|
||||
return handlePostGetSecurityLevel(txId, data, reply)
|
||||
}
|
||||
|
||||
if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
return runCatching {
|
||||
val hardwareCount = reply.readInt()
|
||||
val softwareCount =
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys.keys.count {
|
||||
it.uid == callingUid
|
||||
}
|
||||
val totalCount = hardwareCount + softwareCount
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeNoException()
|
||||
writeInt(totalCount)
|
||||
}
|
||||
TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
} else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
return runCatching {
|
||||
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||
val params =
|
||||
ListEntriesHandler.cacheParameters(txId, data, isBatchMode)
|
||||
?: throw Exception("Abort updating entries for invalid parameters.")
|
||||
val updatedKeyDescriptors =
|
||||
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply)
|
||||
ListEntriesHandler.injectGeneratedKeys(txId, callingUid, params, reply)
|
||||
InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors)
|
||||
}
|
||||
.getOrElse {
|
||||
@@ -207,71 +329,128 @@ 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.
|
||||
runCatching {
|
||||
val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
|
||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||
val authorizations = response.metadata?.authorizations
|
||||
val origin =
|
||||
authorizations
|
||||
?.find { it.keyParameter.tag == Tag.ORIGIN }
|
||||
?.let { it.keyParameter.value.origin }
|
||||
// Skip patching for keys whose certs were explicitly set via updateSubcomponent.
|
||||
if (userUpdatedKeys.remove(keyId)) {
|
||||
SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.")
|
||||
return TransactionResult.SkipTransaction
|
||||
val authorizations = response.metadata.authorizations
|
||||
val parsedParameters =
|
||||
KeyMintAttestation(
|
||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||
)
|
||||
|
||||
if (parsedParameters.isAttestKey() &&
|
||||
!KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)
|
||||
) {
|
||||
SystemLogger.warning(
|
||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||
)
|
||||
// Attest keys that are not under our control should be overriden.
|
||||
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()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
val newNspace = SecureRandom().nextLong()
|
||||
response.metadata.key?.let { it.nspace = newNspace }
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||
keyData.first,
|
||||
null,
|
||||
newNspace,
|
||||
response,
|
||||
parsedParameters,
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||
|
||||
// Check if we should perform attestation patch.
|
||||
if (originalChain == null || originalChain.size < 2) {
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
|
||||
)
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
// First, try to retrieve the already-patched chain from our cache to ensure
|
||||
// consistency.
|
||||
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 {
|
||||
// If no chain is cached (e.g., key existed before simulator started),
|
||||
// perform a live patch as a fallback. This may still be detectable.
|
||||
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
|
||||
SystemLogger.debug("Cached patched certificate chain for $keyId.")
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
|
||||
.getOrThrow()
|
||||
response.metadata.authorizations =
|
||||
InterceptorUtils.patchAuthorizations(
|
||||
response.metadata.authorizations,
|
||||
callingUid,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Perform the attestation patch.
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
|
||||
// First, try to retrieve the already-patched chain from our cache to ensure
|
||||
// consistency.
|
||||
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 {
|
||||
// If no chain is cached (e.g., key existed before simulator started),
|
||||
// perform a live patch as a fallback. This may still be detectable.
|
||||
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
|
||||
}
|
||||
@@ -279,9 +458,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val descriptor = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
// Resolve by nspace (KEY_ID) or alias (APP), same as createOperation.
|
||||
val generatedKeyInfo =
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
when (descriptor.domain) {
|
||||
Domain.KEY_ID ->
|
||||
KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(
|
||||
callingUid,
|
||||
descriptor.nspace,
|
||||
)
|
||||
Domain.APP ->
|
||||
descriptor.alias?.let {
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)]
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
// Hardware key: mark so getKeyEntry skips cert re-patching.
|
||||
descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) }
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]")
|
||||
val metadata = generatedKeyInfo.response.metadata
|
||||
@@ -296,4 +494,67 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
|
||||
return InterceptorUtils.createSuccessReply(writeResultCode = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts the reply from getSecurityLevel to dynamically register our interceptor
|
||||
* for the returned IKeystoreSecurityLevel binder.
|
||||
*
|
||||
* keystore2 may create a new BBinder for each getSecurityLevel call, so the binder
|
||||
* registered during initial setup (in setupSecurityLevelInterceptors) might not be the
|
||||
* same one that client apps receive. By intercepting every getSecurityLevel reply, we
|
||||
* ensure that all SecurityLevel binders are covered.
|
||||
*/
|
||||
private fun handlePostGetSecurityLevel(
|
||||
txId: Long,
|
||||
data: Parcel,
|
||||
reply: Parcel,
|
||||
): TransactionResult {
|
||||
val backdoor = backdoorBinder
|
||||
if (backdoor == null) {
|
||||
SystemLogger.warning("[TX_ID: $txId] post-getSecurityLevel: backdoor not available")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
// Read the security level argument from the original request.
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
val requestedLevel = data.readInt()
|
||||
|
||||
// hasException already consumed the exception header from the reply.
|
||||
// Next item is the IKeystoreSecurityLevel binder.
|
||||
val secLevelBinder = reply.readStrongBinder()
|
||||
if (secLevelBinder == null) {
|
||||
SystemLogger.verbose(
|
||||
"[TX_ID: $txId] getSecurityLevel($requestedLevel) returned null binder"
|
||||
)
|
||||
return@runCatching TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
// Only intercept TEE and StrongBox security levels.
|
||||
if (requestedLevel != SecurityLevel.TRUSTED_ENVIRONMENT &&
|
||||
requestedLevel != SecurityLevel.STRONGBOX
|
||||
) {
|
||||
return@runCatching TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
// Get or create the interceptor for this security level. The interceptor may not
|
||||
// exist yet if the initial setupSecurityLevelInterceptors call failed for this level.
|
||||
val interceptor = securityLevelInterceptors.getOrPut(requestedLevel) {
|
||||
val secLevelInterface =
|
||||
IKeystoreSecurityLevel.Stub.asInterface(secLevelBinder)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Late-creating interceptor for security level $requestedLevel"
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor(secLevelInterface, requestedLevel).also {
|
||||
it.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
|
||||
registerSecurityLevelBinder(backdoor, secLevelBinder, interceptor)
|
||||
TransactionResult.SkipTransaction
|
||||
}.getOrElse {
|
||||
SystemLogger.error("[TX_ID: $txId] Failed to process post-getSecurityLevel.", it)
|
||||
TransactionResult.SkipTransaction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-2
@@ -399,16 +399,18 @@ private data class LegacyKeygenParameters(
|
||||
|
||||
/**
|
||||
* Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is
|
||||
* required by the refactored [AttestationBuilder] and [CertificateGenerator].
|
||||
* required by [AttestationBuilder] and [CertificateGenerator].
|
||||
*/
|
||||
fun toKeyMintAttestation(): KeyMintAttestation {
|
||||
// This conversion acts as a bridge, allowing our new generic components
|
||||
// to be used by the legacy interceptor.
|
||||
return KeyMintAttestation(
|
||||
keySize = this.keySize,
|
||||
algorithm = this.algorithm,
|
||||
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||
ecCurveName = this.ecCurveName ?: "",
|
||||
keySize = this.keySize,
|
||||
origin = null, // Not needed to build attestaion
|
||||
noAuthRequired = null,
|
||||
blockMode = listOf<Int>(),
|
||||
padding = listOf<Int>(),
|
||||
purpose = this.purpose,
|
||||
@@ -430,6 +432,22 @@ private data class LegacyKeygenParameters(
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
secondImei = null,
|
||||
activeDateTime = null,
|
||||
originationExpireDateTime = null,
|
||||
usageExpireDateTime = null,
|
||||
usageCountLimit = null,
|
||||
callerNonce = null,
|
||||
unlockedDeviceRequired = null,
|
||||
includeUniqueId = null,
|
||||
rollbackResistance = null,
|
||||
earlyBootOnly = null,
|
||||
allowWhileOnBody = null,
|
||||
trustedUserPresenceRequired = null,
|
||||
trustedConfirmationRequired = null,
|
||||
maxUsesPerBoot = null,
|
||||
maxBootLevel = null,
|
||||
minMacLength = null,
|
||||
rsaOaepMgfDigest = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+15
-21
@@ -5,7 +5,6 @@ import android.system.keystore2.Domain
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import java.util.TreeMap
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
@@ -22,15 +21,6 @@ object ListEntriesHandler {
|
||||
// Estimate for maximum size of a Binder response in bytes.
|
||||
private const val RESPONSE_SIZE_LIMIT = 358400
|
||||
|
||||
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||
private data class ListEntriesParams(
|
||||
val domain: Int,
|
||||
val namespace: Long,
|
||||
val startPastAlias: String?,
|
||||
)
|
||||
|
||||
private val pendingParams = ConcurrentHashMap<Long, ListEntriesParams>()
|
||||
|
||||
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
|
||||
private fun estimateSafeAmountToReturn(
|
||||
keyDescriptors: Array<KeyDescriptor>,
|
||||
@@ -60,7 +50,7 @@ object ListEntriesHandler {
|
||||
}
|
||||
|
||||
// Parse and store parameters for later use (in post-transaction).
|
||||
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean {
|
||||
fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): ListEntriesParams? {
|
||||
data.enforceInterface(IKeystoreService.DESCRIPTOR)
|
||||
|
||||
val domain = data.readInt()
|
||||
@@ -71,20 +61,21 @@ object ListEntriesHandler {
|
||||
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||
// Note that all generated keys belong to Domain::APP.
|
||||
if (domain == Domain.APP) {
|
||||
pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias)
|
||||
SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.")
|
||||
return true
|
||||
val params = ListEntriesParams(domain, namespace, startPastAlias)
|
||||
SystemLogger.debug("[TX_ID: $txId] Cached $params.")
|
||||
return params
|
||||
}
|
||||
|
||||
return false
|
||||
return null
|
||||
}
|
||||
|
||||
// Merge software-backed keys with hardware-backed keys in the reply parcel.
|
||||
fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array<KeyDescriptor> {
|
||||
val params =
|
||||
pendingParams.remove(txId)
|
||||
?: throw IllegalStateException("No params found for listing entries")
|
||||
|
||||
fun injectGeneratedKeys(
|
||||
txId: Long,
|
||||
callingUid: Int,
|
||||
params: ListEntriesParams,
|
||||
reply: Parcel,
|
||||
): Array<KeyDescriptor> {
|
||||
// By default we use the calling uid as namespace if domain is Domain::APP.
|
||||
// The namespace parameter is thus ignored for non-privileged applications.
|
||||
// See AOSP function `get_key_descriptor_for_lookup` in service.rs.
|
||||
@@ -129,7 +120,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
|
||||
@@ -140,3 +131,6 @@ object ListEntriesHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parameters of AOSP function `list_key_entries` in utils.rs.
|
||||
data class ListEntriesParams(val domain: Int, val namespace: Long, val startPastAlias: String?)
|
||||
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.DataInputStream
|
||||
import java.io.DataOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.security.KeyPair
|
||||
import java.security.MessageDigest
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
||||
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
|
||||
data class PersistedKeyData(
|
||||
val uid: Int,
|
||||
val alias: String,
|
||||
val nspace: Long,
|
||||
val securityLevel: Int,
|
||||
val isAttestationKey: Boolean,
|
||||
val algorithm: Int,
|
||||
val keySize: Int,
|
||||
val ecCurve: Int,
|
||||
val purposes: List<Int>,
|
||||
val digests: List<Int>,
|
||||
val privateKeyBytes: ByteArray,
|
||||
val certChainBytes: List<ByteArray>,
|
||||
)
|
||||
|
||||
object GeneratedKeyPersistence {
|
||||
|
||||
private const val FORMAT_VERSION = 1
|
||||
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
||||
|
||||
// Per-filename locks to prevent concurrent writes to the same key file
|
||||
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
|
||||
|
||||
private fun getLockForKey(filename: String): ReentrantLock {
|
||||
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
|
||||
}
|
||||
|
||||
fun save(
|
||||
keyId: KeyIdentifier,
|
||||
keyPair: KeyPair,
|
||||
nspace: Long,
|
||||
securityLevel: Int,
|
||||
certChain: List<Certificate>,
|
||||
algorithm: Int,
|
||||
keySize: Int,
|
||||
ecCurve: Int,
|
||||
purposes: List<Int>,
|
||||
digests: List<Int>,
|
||||
isAttestationKey: Boolean,
|
||||
) {
|
||||
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||
val lock = getLockForKey(filename)
|
||||
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
|
||||
lock.lock()
|
||||
try {
|
||||
SystemLogger.debug("[Persistence] Lock acquired for $filename")
|
||||
runCatching {
|
||||
PERSISTENCE_DIR.mkdirs()
|
||||
val finalFile = File(PERSISTENCE_DIR, filename)
|
||||
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
|
||||
|
||||
try {
|
||||
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
|
||||
out.writeInt(FORMAT_VERSION)
|
||||
out.writeInt(securityLevel)
|
||||
out.writeInt(keyId.uid)
|
||||
out.writeUTF(keyId.alias)
|
||||
out.writeLong(nspace)
|
||||
out.writeBoolean(isAttestationKey)
|
||||
out.writeInt(algorithm)
|
||||
out.writeInt(keySize)
|
||||
out.writeInt(ecCurve)
|
||||
|
||||
out.writeInt(purposes.size)
|
||||
purposes.forEach { out.writeInt(it) }
|
||||
|
||||
out.writeInt(digests.size)
|
||||
digests.forEach { out.writeInt(it) }
|
||||
|
||||
val pkBytes = keyPair.private.encoded
|
||||
out.writeInt(pkBytes.size)
|
||||
out.write(pkBytes)
|
||||
|
||||
out.writeInt(certChain.size)
|
||||
certChain.forEach { cert ->
|
||||
val encoded = cert.encoded
|
||||
out.writeInt(encoded.size)
|
||||
out.write(encoded)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
tmpFile.delete()
|
||||
throw e
|
||||
}
|
||||
|
||||
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
|
||||
if (!tmpFile.renameTo(finalFile)) {
|
||||
tmpFile.delete()
|
||||
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
|
||||
}
|
||||
|
||||
// Verify write succeeded - catches disk-full or filesystem errors
|
||||
if (!finalFile.exists() || finalFile.length() < 20) {
|
||||
throw IOException("File write verification failed - possible disk full")
|
||||
}
|
||||
|
||||
SystemLogger.debug("Persisted key: $keyId")
|
||||
}.onFailure { e ->
|
||||
SystemLogger.error("Failed to persist key $keyId", e)
|
||||
}
|
||||
} finally {
|
||||
lock.unlock()
|
||||
SystemLogger.debug("[Persistence] Lock released for $filename")
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(keyId: KeyIdentifier) {
|
||||
runCatching {
|
||||
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}")
|
||||
}
|
||||
} else {
|
||||
SystemLogger.debug("No persisted file to delete for: $keyId")
|
||||
}
|
||||
}.onFailure { e ->
|
||||
SystemLogger.error("Failed to delete persisted key $keyId", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAll() {
|
||||
runCatching {
|
||||
if (!PERSISTENCE_DIR.exists()) {
|
||||
SystemLogger.debug("No persistent_keys directory, nothing to delete")
|
||||
return
|
||||
}
|
||||
val files = PERSISTENCE_DIR.listFiles()
|
||||
if (files == null) {
|
||||
SystemLogger.warning("Cannot list persistent_keys directory")
|
||||
return
|
||||
}
|
||||
var count = 0
|
||||
files.forEach { file ->
|
||||
if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) {
|
||||
if (file.delete()) count++
|
||||
}
|
||||
}
|
||||
fileLocks.clear()
|
||||
SystemLogger.info("Deleted $count persisted key files")
|
||||
}.onFailure { e ->
|
||||
SystemLogger.error("Failed to delete all persisted keys", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAll(securityLevel: Int): List<PersistedKeyData> {
|
||||
if (!PERSISTENCE_DIR.exists()) {
|
||||
SystemLogger.debug("No persistent_keys directory, nothing to load")
|
||||
return emptyList()
|
||||
}
|
||||
val files = PERSISTENCE_DIR.listFiles { _, name -> name.endsWith(".bin") }
|
||||
if (files == null) {
|
||||
SystemLogger.warning("Cannot read persistent_keys directory")
|
||||
return emptyList()
|
||||
}
|
||||
if (files.isEmpty()) {
|
||||
SystemLogger.debug("No persisted key files found")
|
||||
return emptyList()
|
||||
}
|
||||
SystemLogger.info("Found ${files.size} persisted key files to process")
|
||||
|
||||
val result = mutableListOf<PersistedKeyData>()
|
||||
|
||||
for (file in files) {
|
||||
runCatching {
|
||||
DataInputStream(BufferedInputStream(FileInputStream(file))).use { input ->
|
||||
val version = input.readInt()
|
||||
if (version != FORMAT_VERSION) {
|
||||
SystemLogger.warning(
|
||||
"Skipping ${file.name}: unknown format version $version"
|
||||
)
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
val storedSecLevel = input.readInt()
|
||||
val uid = input.readInt()
|
||||
val alias = input.readUTF()
|
||||
val nspace = input.readLong()
|
||||
val isAttestKey = input.readBoolean()
|
||||
val algo = input.readInt()
|
||||
val kSize = input.readInt()
|
||||
val curve = input.readInt()
|
||||
|
||||
val purposeCount = requireBounds(input.readInt(), 64, "purposeCount")
|
||||
val purposes = (0 until purposeCount).map { input.readInt() }
|
||||
|
||||
val digestCount = requireBounds(input.readInt(), 64, "digestCount")
|
||||
val digests = (0 until digestCount).map { input.readInt() }
|
||||
|
||||
val pkLen = requireBounds(input.readInt(), 8192, "pkLen")
|
||||
val pkBytes = ByteArray(pkLen)
|
||||
input.readFully(pkBytes)
|
||||
|
||||
val certCount = requireBounds(input.readInt(), 10, "certCount")
|
||||
val certChainBytes = (0 until certCount).map {
|
||||
val certLen = requireBounds(input.readInt(), 65536, "certLen")
|
||||
val certBytes = ByteArray(certLen)
|
||||
input.readFully(certBytes)
|
||||
certBytes
|
||||
}
|
||||
|
||||
if (storedSecLevel == securityLevel) {
|
||||
result.add(
|
||||
PersistedKeyData(
|
||||
uid = uid,
|
||||
alias = alias,
|
||||
nspace = nspace,
|
||||
securityLevel = storedSecLevel,
|
||||
isAttestationKey = isAttestKey,
|
||||
algorithm = algo,
|
||||
keySize = kSize,
|
||||
ecCurve = curve,
|
||||
purposes = purposes,
|
||||
digests = digests,
|
||||
privateKeyBytes = pkBytes,
|
||||
certChainBytes = certChainBytes,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onFailure { e ->
|
||||
SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e)
|
||||
}
|
||||
}
|
||||
|
||||
SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel")
|
||||
return result
|
||||
}
|
||||
|
||||
private fun requireBounds(value: Int, max: Int, name: String): Int {
|
||||
require(value in 0..max) { "$name out of bounds: $value (max $max)" }
|
||||
return value
|
||||
}
|
||||
|
||||
private fun keyFileName(uid: Int, alias: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest("$uid:$alias".toByteArray(Charsets.UTF_8))
|
||||
return digest.joinToString("") { "%02x".format(it) } + ".bin"
|
||||
}
|
||||
}
|
||||
+828
-86
File diff suppressed because it is too large
Load Diff
+3
@@ -44,6 +44,9 @@ class OperationInterceptor(
|
||||
private val ABORT_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort")
|
||||
|
||||
/** Only intercept finish/abort for cleanup. Other ops pass through without round-trip. */
|
||||
val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION)
|
||||
|
||||
private val transactionNames: Map<Int, String> by lazy {
|
||||
IKeystoreOperation.Stub::class
|
||||
.java
|
||||
|
||||
+205
-28
@@ -3,10 +3,15 @@ package org.matrix.TEESimulator.interception.keystore.shim
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.BlockMode
|
||||
import android.hardware.security.keymint.Digest
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.RemoteException
|
||||
import android.os.ServiceSpecificException
|
||||
import android.system.keystore2.IKeystoreOperation
|
||||
import android.system.keystore2.KeyParameters
|
||||
import java.security.KeyPair
|
||||
import java.security.Signature
|
||||
import java.security.SignatureException
|
||||
@@ -15,13 +20,45 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
/** Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. */
|
||||
internal object KeystoreErrorCode {
|
||||
const val INVALID_OPERATION_HANDLE = -28
|
||||
const val VERIFICATION_FAILED = -30
|
||||
const val UNSUPPORTED_PURPOSE = -2
|
||||
const val INCOMPATIBLE_PURPOSE = -3
|
||||
const val SYSTEM_ERROR = 4
|
||||
const val TOO_MUCH_DATA = 21
|
||||
const val KEY_EXPIRED = -25
|
||||
const val KEY_NOT_YET_VALID = -24
|
||||
|
||||
/** KeyMint ErrorCode::CALLER_NONCE_PROHIBITED */
|
||||
const val CALLER_NONCE_PROHIBITED = -55
|
||||
|
||||
/** KeyMint ErrorCode::INVALID_ARGUMENT */
|
||||
const val INVALID_ARGUMENT = -38
|
||||
|
||||
/** KeyMint ErrorCode::INVALID_TAG */
|
||||
const val INVALID_TAG = -40
|
||||
|
||||
/** Keystore2 ResponseCode::PERMISSION_DENIED */
|
||||
const val PERMISSION_DENIED = 6
|
||||
|
||||
/** Keystore2 ResponseCode::KEY_NOT_FOUND */
|
||||
const val KEY_NOT_FOUND = 7
|
||||
}
|
||||
|
||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||
private sealed interface CryptoPrimitive {
|
||||
fun updateAad(data: ByteArray?)
|
||||
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
|
||||
/** Returns parameters from the begin phase (e.g. GCM nonce), or null if none. */
|
||||
fun getBeginParameters(): Array<KeyParameter>? = null
|
||||
}
|
||||
|
||||
// Helper object to map KeyMint constants to JCA algorithm strings.
|
||||
@@ -39,8 +76,9 @@ private object JcaAlgorithmMapper {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.SYSTEM_ERROR,
|
||||
"Unsupported signature algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
@@ -52,8 +90,9 @@ private object JcaAlgorithmMapper {
|
||||
Algorithm.RSA -> "RSA"
|
||||
Algorithm.AES -> "AES"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported cipher algorithm: ${params.algorithm}"
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.SYSTEM_ERROR,
|
||||
"Unsupported cipher algorithm: ${params.algorithm}",
|
||||
)
|
||||
}
|
||||
val blockMode =
|
||||
@@ -82,6 +121,10 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi
|
||||
initSign(keyPair.private)
|
||||
}
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? {
|
||||
if (data != null) signature.update(data)
|
||||
return null
|
||||
@@ -102,6 +145,10 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
initVerify(keyPair.public)
|
||||
}
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? {
|
||||
if (data != null) signature.update(data)
|
||||
return null
|
||||
@@ -109,12 +156,17 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data != null) update(data)
|
||||
if (signature == null) throw SignatureException("Signature to verify is null")
|
||||
if (signature == null)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||
"Signature to verify is null",
|
||||
)
|
||||
if (!this.signature.verify(signature)) {
|
||||
// Throwing an exception is how Keystore signals verification failure.
|
||||
throw SignatureException("Signature verification failed")
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.VERIFICATION_FAILED,
|
||||
"Signature/MAC verification failed",
|
||||
)
|
||||
}
|
||||
// A successful verification returns no data.
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -123,16 +175,19 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri
|
||||
|
||||
// Concrete implementation for Encryption/Decryption.
|
||||
private class CipherPrimitive(
|
||||
keyPair: KeyPair,
|
||||
cryptoKey: java.security.Key,
|
||||
params: KeyMintAttestation,
|
||||
private val opMode: Int,
|
||||
) : CryptoPrimitive {
|
||||
private val cipher: Cipher =
|
||||
Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply {
|
||||
val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private
|
||||
init(opMode, key)
|
||||
init(opMode, cryptoKey)
|
||||
}
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
if (data != null) cipher.updateAAD(data)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? =
|
||||
if (data != null) cipher.update(data) else null
|
||||
|
||||
@@ -140,77 +195,199 @@ private class CipherPrimitive(
|
||||
if (data != null) cipher.doFinal(data) else cipher.doFinal()
|
||||
|
||||
override fun abort() {}
|
||||
|
||||
/** Returns the cipher IV as a NONCE parameter for GCM operations. */
|
||||
override fun getBeginParameters(): Array<KeyParameter>? {
|
||||
val iv = cipher.iv ?: return null
|
||||
return arrayOf(
|
||||
KeyParameter().apply {
|
||||
tag = Tag.NONCE
|
||||
value = KeyParameterValue.blob(iv)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete implementation for ECDH Key Agreement.
|
||||
private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive {
|
||||
private val agreement: javax.crypto.KeyAgreement =
|
||||
javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) }
|
||||
|
||||
override fun updateAad(data: ByteArray?) {
|
||||
throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG)
|
||||
}
|
||||
|
||||
override fun update(data: ByteArray?): ByteArray? = null
|
||||
|
||||
override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
if (data == null)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.INVALID_ARGUMENT,
|
||||
"Peer public key required for key agreement",
|
||||
)
|
||||
val peerKey =
|
||||
java.security.KeyFactory.getInstance("EC")
|
||||
.generatePublic(java.security.spec.X509EncodedKeySpec(data))
|
||||
agreement.doPhase(peerKey, true)
|
||||
return agreement.generateSecret()
|
||||
}
|
||||
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls throw
|
||||
* [ServiceSpecificException] with [KeystoreErrorCode.INVALID_OPERATION_HANDLE].
|
||||
*/
|
||||
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?,
|
||||
secretKey: javax.crypto.SecretKey?,
|
||||
params: KeyMintAttestation,
|
||||
var onFinishCallback: (() -> Unit)? = null,
|
||||
) {
|
||||
private val primitive: CryptoPrimitive
|
||||
|
||||
@Volatile private var finalized = false
|
||||
|
||||
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.")
|
||||
|
||||
primitive =
|
||||
when (purpose) {
|
||||
KeyPurpose.SIGN -> Signer(keyPair, params)
|
||||
KeyPurpose.VERIFY -> Verifier(keyPair, params)
|
||||
KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE)
|
||||
KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE)
|
||||
KeyPurpose.SIGN -> Signer(keyPair!!, params)
|
||||
KeyPurpose.VERIFY -> Verifier(keyPair!!, params)
|
||||
KeyPurpose.ENCRYPT -> {
|
||||
val key: java.security.Key = secretKey ?: keyPair!!.public
|
||||
CipherPrimitive(key, params, Cipher.ENCRYPT_MODE)
|
||||
}
|
||||
KeyPurpose.DECRYPT -> {
|
||||
val key: java.security.Key = secretKey ?: keyPair!!.private
|
||||
CipherPrimitive(key, params, Cipher.DECRYPT_MODE)
|
||||
}
|
||||
KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!)
|
||||
else ->
|
||||
throw UnsupportedOperationException("Unsupported operation purpose: $purpose")
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.UNSUPPORTED_PURPOSE,
|
||||
"Unsupported operation purpose: $purpose",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Parameters produced during begin (e.g. GCM nonce), to populate CreateOperationResponse. */
|
||||
val beginParameters: KeyParameters?
|
||||
get() {
|
||||
val params = primitive.getBeginParameters() ?: return null
|
||||
if (params.isEmpty()) return null
|
||||
return KeyParameters().apply { keyParameter = params }
|
||||
}
|
||||
|
||||
private fun checkActive() {
|
||||
if (finalized)
|
||||
throw ServiceSpecificException(
|
||||
KeystoreErrorCode.INVALID_OPERATION_HANDLE,
|
||||
"Operation already finalized.",
|
||||
)
|
||||
}
|
||||
|
||||
fun updateAad(data: ByteArray?) {
|
||||
checkActive()
|
||||
try {
|
||||
primitive.updateAad(data)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
finalized = true
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
finalized = true
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
|
||||
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(data: ByteArray?): ByteArray? {
|
||||
checkActive()
|
||||
try {
|
||||
return primitive.update(data)
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
finalized = true
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
finalized = true
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
checkActive()
|
||||
try {
|
||||
val result = primitive.finish(data, signature)
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||
onFinishCallback?.invoke()
|
||||
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
|
||||
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
|
||||
} finally {
|
||||
finalized = true
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
checkActive()
|
||||
finalized = true
|
||||
primitive.abort()
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||
}
|
||||
}
|
||||
|
||||
/** The Binder interface for our [SoftwareOperation]. */
|
||||
/** Binder interface for [SoftwareOperation]. Synchronized and input-length validated. */
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
private fun checkInputLength(data: ByteArray?) {
|
||||
if (data != null && data.size > MAX_RECEIVE_DATA)
|
||||
throw ServiceSpecificException(KeystoreErrorCode.TOO_MUCH_DATA)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
synchronized(this) {
|
||||
checkInputLength(aadInput)
|
||||
operation.updateAad(aadInput)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
return operation.update(input)
|
||||
synchronized(this) {
|
||||
checkInputLength(input)
|
||||
return operation.update(input)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
synchronized(this) {
|
||||
checkInputLength(input)
|
||||
checkInputLength(signature)
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
synchronized(this) { operation.abort() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,22 @@ object KeyMintParameterLogger {
|
||||
.associate { field -> (field.get(null) as Int) to field.name }
|
||||
}
|
||||
|
||||
val hardwareAuthenticatorTypeNames: Map<Int, String> by lazy {
|
||||
HardwareAuthenticatorType::class
|
||||
.java
|
||||
.fields
|
||||
.filter { it.type == Int::class.java }
|
||||
.associate { field -> (field.get(null) as Int) to field.name }
|
||||
}
|
||||
|
||||
val keyOriginNames: Map<Int, String> by lazy {
|
||||
KeyOrigin::class
|
||||
.java
|
||||
.fields
|
||||
.filter { it.type == Int::class.java }
|
||||
.associate { field -> (field.get(null) as Int) to field.name }
|
||||
}
|
||||
|
||||
val paddingNames: Map<Int, String> by lazy {
|
||||
PaddingMode::class
|
||||
.java
|
||||
@@ -81,22 +97,33 @@ object KeyMintParameterLogger {
|
||||
when (param.tag) {
|
||||
Tag.ALGORITHM -> algorithmNames[value.algorithm]
|
||||
Tag.BLOCK_MODE -> blockModeNames[value.blockMode]
|
||||
Tag.DIGEST -> digestNames[value.digest]
|
||||
Tag.EC_CURVE -> ecCurveNames[value.ecCurve]
|
||||
Tag.ORIGIN -> keyOriginNames[value.origin]
|
||||
Tag.PADDING -> paddingNames[value.paddingMode]
|
||||
Tag.PURPOSE -> purposeNames[value.keyPurpose]
|
||||
Tag.DIGEST -> digestNames[value.digest]
|
||||
Tag.USER_AUTH_TYPE ->
|
||||
hardwareAuthenticatorTypeNames[value.hardwareAuthenticatorType]
|
||||
Tag.AUTH_TIMEOUT,
|
||||
Tag.BOOT_PATCHLEVEL,
|
||||
Tag.KEY_SIZE,
|
||||
Tag.MIN_MAC_LENGTH -> value.integer.toString()
|
||||
Tag.MAC_LENGTH,
|
||||
Tag.MIN_MAC_LENGTH,
|
||||
Tag.OS_VERSION,
|
||||
Tag.OS_PATCHLEVEL,
|
||||
Tag.USER_ID,
|
||||
Tag.VENDOR_PATCHLEVEL -> value.integer.toString()
|
||||
Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString()
|
||||
Tag.ACTIVE_DATETIME,
|
||||
Tag.CERTIFICATE_NOT_AFTER,
|
||||
Tag.CERTIFICATE_NOT_BEFORE,
|
||||
Tag.CREATION_DATETIME,
|
||||
Tag.ORIGINATION_EXPIRE_DATETIME,
|
||||
Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString()
|
||||
Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString()
|
||||
Tag.USER_SECURE_ID,
|
||||
Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString()
|
||||
Tag.NO_AUTH_REQUIRED -> "true"
|
||||
Tag.NO_AUTH_REQUIRED -> value.boolValue.toString()
|
||||
Tag.ATTESTATION_CHALLENGE,
|
||||
Tag.ATTESTATION_ID_BRAND,
|
||||
Tag.ATTESTATION_ID_DEVICE,
|
||||
|
||||
@@ -19,6 +19,7 @@ object SystemLogger {
|
||||
* @param message The message to log.
|
||||
*/
|
||||
fun debug(message: String) {
|
||||
if (!isDebugBuild) return
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package org.matrix.TEESimulator.pki
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.os.Build
|
||||
import android.util.Pair
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
import java.security.spec.RSAKeyGenParameterSpec
|
||||
import java.util.Date
|
||||
@@ -35,6 +35,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
*/
|
||||
object CertificateGenerator {
|
||||
|
||||
// RFC 5280 GeneralizedTime maximum: 9999-12-31T23:59:59 UTC (millis since epoch).
|
||||
private const val UNDEFINED_NOT_AFTER = 253402300799000L
|
||||
|
||||
/**
|
||||
* Generates a software-based cryptographic key pair.
|
||||
*
|
||||
@@ -48,7 +51,10 @@ object CertificateGenerator {
|
||||
Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName)
|
||||
Algorithm.RSA ->
|
||||
"RSA" to
|
||||
RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent)
|
||||
RSAKeyGenParameterSpec(
|
||||
params.keySize,
|
||||
params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4,
|
||||
)
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported algorithm: ${params.algorithm}"
|
||||
@@ -87,11 +93,9 @@ object CertificateGenerator {
|
||||
"Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})"
|
||||
)
|
||||
|
||||
return runCatching {
|
||||
return try {
|
||||
val keybox = getKeyboxForAlgorithm(uid, params.algorithm)
|
||||
|
||||
// Determine the signing key and issuer. If an attestKey is provided, use it.
|
||||
// Otherwise, fall back to the root key from the keybox.
|
||||
val (signingKey, issuer) =
|
||||
if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second }
|
||||
@@ -100,20 +104,20 @@ object CertificateGenerator {
|
||||
keybox.keyPair to getIssuerFromKeybox(keybox)
|
||||
}
|
||||
|
||||
// Build the new leaf certificate with the simulated attestation.
|
||||
val leafCert =
|
||||
buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel)
|
||||
|
||||
// If not self-attesting, the chain is just the leaf. Otherwise, append the keybox
|
||||
// chain.
|
||||
if (attestKeyAlias != null) {
|
||||
listOf(leafCert)
|
||||
} else {
|
||||
listOf(leafCert) + keybox.certificates
|
||||
}
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate certificate chain.", e)
|
||||
null
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to generate certificate chain.", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +131,7 @@ object CertificateGenerator {
|
||||
params: KeyMintAttestation,
|
||||
securityLevel: Int,
|
||||
): Pair<KeyPair, List<Certificate>>? {
|
||||
return runCatching {
|
||||
return try {
|
||||
SystemLogger.info(
|
||||
"Generating new attested key pair for alias: '$alias' (UID: $uid)"
|
||||
)
|
||||
@@ -143,11 +147,12 @@ object CertificateGenerator {
|
||||
"Successfully generated new certificate chain for alias: '$alias'."
|
||||
)
|
||||
Pair(newKeyPair, chain)
|
||||
} catch (e: android.os.ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e)
|
||||
null
|
||||
}
|
||||
.onFailure {
|
||||
SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
fun getIssuerFromKeybox(keybox: KeyBox) =
|
||||
@@ -162,7 +167,10 @@ object CertificateGenerator {
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm")
|
||||
}
|
||||
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
|
||||
?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName")
|
||||
?: throw android.os.ServiceSpecificException(
|
||||
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
|
||||
"No attestation key for algorithm $algorithmName in $keyboxFile",
|
||||
)
|
||||
}
|
||||
|
||||
/** Retrieves the key pair and issuer name for a given attestation key alias. */
|
||||
@@ -187,6 +195,24 @@ object CertificateGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
/** Maps KeyPurpose values to X.509 KeyUsage bits per KeyCreationResult.aidl spec */
|
||||
private fun buildKeyUsageFromPurposes(purposes: List<Int>): Int {
|
||||
var bits = 0
|
||||
for (purpose in purposes) {
|
||||
bits =
|
||||
bits or
|
||||
when (purpose) {
|
||||
KeyPurpose.SIGN -> KeyUsage.digitalSignature
|
||||
KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment
|
||||
KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment
|
||||
KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement
|
||||
KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
return bits
|
||||
}
|
||||
|
||||
/** Constructs a new X.509 certificate with a simulated attestation extension. */
|
||||
private fun buildCertificate(
|
||||
subjectKeyPair: KeyPair,
|
||||
@@ -196,33 +222,42 @@ object CertificateGenerator {
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
|
||||
// Default validity: epoch to 9999-12-31T23:59:59 UTC (matches add_required_parameters).
|
||||
val notBefore = params.certificateNotBefore ?: Date(0)
|
||||
val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER)
|
||||
|
||||
val builder =
|
||||
JcaX509v3CertificateBuilder(
|
||||
issuer,
|
||||
params.certificateSerial ?: BigInteger.ONE,
|
||||
params.certificateNotBefore ?: Date(),
|
||||
params.certificateNotAfter ?: leafNotAfter,
|
||||
notBefore,
|
||||
notAfter,
|
||||
subject,
|
||||
subjectKeyPair.public,
|
||||
)
|
||||
|
||||
// Add standard extensions.
|
||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(KeyUsage.keyCertSign))
|
||||
// Add KeyUsage extension only if purposes map to valid bits
|
||||
val keyUsageBits = buildKeyUsageFromPurposes(params.purpose)
|
||||
if (keyUsageBits != 0) {
|
||||
builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits))
|
||||
}
|
||||
// Add our custom, simulated attestation extension.
|
||||
builder.addExtension(
|
||||
AttestationBuilder.buildAttestationExtension(params, uid, securityLevel)
|
||||
)
|
||||
|
||||
// The signature algorithm must match the SIGNING key, not the subject key.
|
||||
// An EC attestation key may sign an RSA subject key's certificate (or vice versa).
|
||||
val signerAlgorithm =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "SHA256withECDSA"
|
||||
Algorithm.RSA -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||
when (signingKeyPair.private) {
|
||||
is java.security.interfaces.ECKey -> "SHA256withECDSA"
|
||||
is java.security.interfaces.RSAKey -> "SHA256withRSA"
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signing key type: ${signingKeyPair.private.javaClass}"
|
||||
)
|
||||
}
|
||||
val contentSigner =
|
||||
JcaContentSignerBuilder(signerAlgorithm)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
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?,
|
||||
val activeDatetime: Long = -1L,
|
||||
val originationExpireDatetime: Long = -1L,
|
||||
val usageExpireDatetime: Long = -1L,
|
||||
val usageCountLimit: Int = -1,
|
||||
val callerNonce: Boolean = false,
|
||||
val unlockedDeviceRequired: Boolean = false,
|
||||
val noAuthRequired: Boolean = true,
|
||||
)
|
||||
|
||||
object NativeCertGen {
|
||||
|
||||
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
|
||||
@@ -370,52 +371,206 @@ 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)
|
||||
// https://cs.android.com/android/platform/superproject/+/android-latest-release:system/apex/proto/apex_manifest.proto
|
||||
// --- Minimal Protobuf Parser for ApexManifest ---
|
||||
// Field 1: name (string)
|
||||
// Field 2: version (int64)
|
||||
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 -> { // name
|
||||
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
|
||||
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() // Varint
|
||||
1 -> pos += 8 // 64-bit
|
||||
2 -> { // Length-delimited
|
||||
val len = readVarint().toInt()
|
||||
pos += len
|
||||
}
|
||||
5 -> pos += 4 // 32-bit
|
||||
else -> throw IllegalStateException("Unknown wire type $wireType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/apex/libs/libapexutil/apexutil.cpp
|
||||
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()
|
||||
}
|
||||
|
||||
// Logic from: GetActivePackages in apexutil.cpp
|
||||
apexRoot.listFiles()?.forEach { file ->
|
||||
if (!file.isDirectory) return@forEach
|
||||
val name = file.name
|
||||
|
||||
// 1. Ignore "." (and implicitly "..")
|
||||
if (name.startsWith(".")) return@forEach
|
||||
|
||||
// 2. Ignore directories containing '@' (active mounts usually don't have version in
|
||||
// path)
|
||||
if (name.contains("@")) return@forEach
|
||||
|
||||
// 3. Ignore "sharedlibs"
|
||||
if (name == "sharedlibs") return@forEach
|
||||
|
||||
// 4. Parse apex_manifest.pb
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure uniqueness (though filesystem scan usually prevents exact dupes,
|
||||
// strictly speaking we want to behave like a Map keyed by package name)
|
||||
results.distinctBy { it.first }
|
||||
}
|
||||
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/maintenance.rs
|
||||
val moduleHash: ByteArray by lazy {
|
||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||
?: runCatching {
|
||||
// TODO: figure out the correct calculation
|
||||
val moduleSequences = ASN1EncodableVector()
|
||||
// 1. Create a container to hold the sort key (name encoded) and the full data
|
||||
// (sequence encoded)
|
||||
data class ModuleEntry(
|
||||
val nameEncoded: ByteArray, // The sort key
|
||||
val fullEncoded: ByteArray, // The data to hash
|
||||
)
|
||||
|
||||
// 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) ->
|
||||
// Create the components
|
||||
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)
|
||||
// Create the Sequence: SEQUENCE { packageName, version }
|
||||
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
|
||||
// We store the encoded name separately because Rust sorts ONLY by this
|
||||
ModuleEntry(
|
||||
nameEncoded = nameOctet.encoded,
|
||||
fullEncoded = sequence.encoded,
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Compute the SHA-256 hash.
|
||||
MessageDigest.getInstance("SHA-256").digest(encodedModules)
|
||||
// 2. Sort manually based on the encoded Package Name (lexicographically)
|
||||
// This mimics the Rust 'impl DerOrd for ModuleInfo' which delegates to
|
||||
// 'self.name'
|
||||
val sortedModules =
|
||||
modules.sortedWith { m1, m2 ->
|
||||
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||
}
|
||||
|
||||
// 3. Concatenate the full sequences in the specific sorted order
|
||||
val payloadStream = ByteArrayOutputStream()
|
||||
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||
val payload = payloadStream.toByteArray()
|
||||
|
||||
// 4. Wrap manually in a DER SET tag (0x31)
|
||||
// We cannot use DERSet(vector) because it would re-sort incorrectly.
|
||||
val finalDerSet = encodeAsDerSet(payload)
|
||||
|
||||
// 5. Compute SHA-256
|
||||
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("Failed to compute module hash.", it)
|
||||
ByteArray(32) // Return empty hash on failure
|
||||
ByteArray(32)
|
||||
}
|
||||
}
|
||||
|
||||
/** Compares two byte arrays lexicographically (unsigned). */
|
||||
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
|
||||
}
|
||||
|
||||
/** Manually wraps the payload in an ASN.1 SET (0x31) tag with correct length encoding. */
|
||||
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x31) // ASN.1 Tag for SET
|
||||
writeDerLength(out, payload.size)
|
||||
out.write(payload)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
/** Writes the ASN.1 length field to the stream. */
|
||||
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||
if (length < 128) {
|
||||
// Short form
|
||||
out.write(length)
|
||||
} else {
|
||||
// Long form
|
||||
var size = length
|
||||
val bytes = ArrayList<Byte>()
|
||||
while (size > 0) {
|
||||
bytes.add((size and 0xFF).toByte())
|
||||
size = size ushr 8
|
||||
}
|
||||
// First byte: 0x80 | number of length bytes
|
||||
out.write(0x80 or bytes.size)
|
||||
// Write length bytes in big-endian (reverse of how we extracted them)
|
||||
for (i in bytes.indices.reversed()) {
|
||||
out.write(bytes[i].toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ package org.matrix.TEESimulator.util
|
||||
*
|
||||
* @return A new string with each line individually trimmed.
|
||||
*/
|
||||
fun String.trimLines(): String = this.trim().lines().joinToString("\n") { it.trim() }
|
||||
fun String.trimLines(): String =
|
||||
this.trim()
|
||||
.lines()
|
||||
.filter { !it.trim().startsWith("<!--") }
|
||||
.joinToString("\n") { it.trim() }
|
||||
|
||||
/**
|
||||
* Converts a ByteArray to its hexadecimal string representation.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import java.security.SecureRandom
|
||||
import java.util.concurrent.locks.LockSupport
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Simulates realistic TEE hardware latency for software key generation.
|
||||
*
|
||||
* The delay model is derived from 64+ timing measurements across QTEE (Qualcomm) and Trustonic
|
||||
* (MediaTek) hardware. It combines four independent noise sources that model different physical
|
||||
* latency origins in a real TrustZone-based TEE:
|
||||
*
|
||||
* 1. Base crypto processing (log-normal): hardware RNG + key derivation + cert signing
|
||||
* 2. Binder/kernel transit (exponential): IPC scheduling, context switches
|
||||
* 3. TrustZone scheduler jitter (Gaussian): world-switch non-determinism
|
||||
* 4. Cold-start penalty (half-normal): first operation after idle is slower due to TEE
|
||||
* secure world re-initialization and TLB/cache warming
|
||||
*
|
||||
* Per-boot session bias models manufacturing variance between TEE hardware instances.
|
||||
*/
|
||||
object TeeLatencySimulator {
|
||||
|
||||
private val rng = SecureRandom()
|
||||
|
||||
private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 }
|
||||
private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) }
|
||||
|
||||
@Volatile private var firstCall = true
|
||||
|
||||
fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) {
|
||||
val elapsedMs = elapsedNanos / 1_000_000.0
|
||||
val targetMs = sampleTotalDelay(algorithm)
|
||||
val remainingMs = targetMs - elapsedMs
|
||||
|
||||
if (remainingMs > 1.0) {
|
||||
LockSupport.parkNanos((remainingMs * 1_000_000).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun sampleTotalDelay(algorithm: Int): Double {
|
||||
val base = sampleBaseCryptoDelay(algorithm)
|
||||
val transit = sampleExponential(2.5)
|
||||
val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0)
|
||||
|
||||
var cold = 0.0
|
||||
if (firstCall) {
|
||||
firstCall = false
|
||||
cold = coldPenaltyMs
|
||||
}
|
||||
|
||||
return max(20.0, base + transit + jitter + sessionBiasMs + cold)
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-normal base delay. Parameters tuned to match observed hardware profiles:
|
||||
* EC P-256 on QTEE averages ~65ms, RSA-2048 ~75ms, AES ~40ms.
|
||||
* Sigma kept low (0.08) to match the tight clustering seen in real measurements.
|
||||
*/
|
||||
private fun sampleBaseCryptoDelay(algorithm: Int): Double {
|
||||
val (mu, sigma) =
|
||||
when (algorithm) {
|
||||
Algorithm.EC -> ln(60.0) to 0.08
|
||||
Algorithm.RSA -> ln(70.0) to 0.08
|
||||
Algorithm.AES -> ln(35.0) to 0.10
|
||||
else -> ln(40.0) to 0.10
|
||||
}
|
||||
return sampleLogNormal(mu, sigma)
|
||||
}
|
||||
|
||||
private fun sampleLogNormal(mu: Double, sigma: Double): Double {
|
||||
return exp(mu + sigma * rng.nextGaussian())
|
||||
}
|
||||
|
||||
private fun sampleExponential(mean: Double): Double {
|
||||
var u = rng.nextDouble()
|
||||
while (u == 0.0) u = rng.nextDouble()
|
||||
return -mean * ln(u)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/system/bin/sh
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_DIR=/data/adb/tricky_store
|
||||
|
||||
if [ -d "$CONFIG_DIR/persistent_keys" ]; then
|
||||
rm -rf "$CONFIG_DIR/persistent_keys"
|
||||
mkdir -p "$CONFIG_DIR/persistent_keys"
|
||||
echo "Persistent key storage cleared"
|
||||
else
|
||||
echo "No persistent key storage found"
|
||||
fi
|
||||
+228
-17
@@ -1,26 +1,237 @@
|
||||
## 🎉 TEESimulator v3.1: Legacy Support & Resilience
|
||||
## TEESimulator-RS v5.1: Interception Architecture Rewrite
|
||||
|
||||
This release marks a significant step forward in our mission, focusing on breathing life into devices with **broken TEEs** and extending full support to older Android versions (**Android 10–12**).
|
||||
Major release. 27 files changed, 2300 lines rewritten. The entire Kotlin interception layer has been rebuilt with a clean architecture, proper AIDL alignment, and significantly lower binder overhead.
|
||||
|
||||
### 🛡️ Enhanced Keystore2 Emulation
|
||||
We have implemented critical APIs to support devices where the hardware TEE is broken or for applications configured to use key generation mode. These improvements directly address detection vectors identified in v3.0:
|
||||
### Interception Layer Rewrite
|
||||
- KeyMintSecurityLevelInterceptor completely restructured: GeneratedKeyInfo now carries full KeyMintAttestation instead of nullable stub, eliminating scattered null checks across every operation path
|
||||
- SoftwareOperation rewritten with sealed CryptoPrimitive interface separating Signer, Verifier, Encryptor, and Decryptor into isolated implementations with proper JCA algorithm mapping
|
||||
- KeystoreErrorCode centralized object replaces scattered magic numbers for all KeyMint and Keystore2 error codes
|
||||
- listEntries moved from pre-transact parameter caching to post-transact injection, eliminating a race condition where cached params could go stale
|
||||
- deleteKey now handles both APP domain (by alias) and KEY_ID domain (by nspace) resolution paths correctly
|
||||
- AuthorizeCreate and AndroidPermissionUtils removed, authorization logic consolidated into the operation dispatch path
|
||||
- DeviceAttestationService removed, attestation routing simplified into the main interceptor
|
||||
|
||||
* **✅ Full Crypto Operations (`createOperation`)**: The simulator now correctly handles `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` purposes for software-generated keys.
|
||||
* **🔗 Certificate Chain Updates (`updateSubcomponent`)**: Added support for applications updating the certificate chain of virtual keys (e.g., via `KeyStore.setKeyEntry`).
|
||||
* **📋 Enumeration Support (`listEntries`)**: Generated keys are now properly visible in enumeration APIs like `KeyStore.aliases()`, thanks to the implementation of `listEntries` and `listEntriesBatched`.
|
||||
### Software Crypto Operations
|
||||
- GCM nonce returned in CreateOperationResponse.parameters for encrypt operations, matching real KeyMint HAL behavior
|
||||
- updateAad correctly throws INVALID_TAG on non-AEAD operations instead of silently succeeding
|
||||
- Cipher algorithm mapping cleaned up: dropped CTR block mode and RSA_PKCS1_1_5_SIGN padding that caused JCA provider mismatches
|
||||
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes instead of raw exceptions
|
||||
|
||||
### 🔧 Compatibility & Stability
|
||||
We’ve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
|
||||
### Attestation & Certificate Generation
|
||||
- CertificateGenerator rewritten with clean Kotlin Pair return type instead of Android's util.Pair
|
||||
- AttestationBuilder field ordering aligned with AOSP KeyDescription ASN.1 schema
|
||||
- Unique ID computation follows KeyMint HAL spec: HMAC-SHA256(temporal_counter || AAID || reset_flag, HBK) truncated to 128 bits
|
||||
- Patch level logging removed from hot path to reduce logcat noise on every attestation
|
||||
|
||||
* **Android 10**: Fixed a crash caused by the missing `waitForService` method.
|
||||
* **Android 11**: Implemented environment initialization and daemon UID spoofing to successfully bypass keystore generation permission checks.
|
||||
* **ARM 32-bit (Android 12)**: Resolved `ptrace` compatibility issues by falling back to `PTRACE_GETREGS` and `PTRACE_SETREGS`.
|
||||
* **x86_64 Emulators**: Enforced respect for the stack pointer "red zone" and added a staging fallback mechanism for file descriptor transfering of `libTEESimulator.so`.
|
||||
### Configuration & Device Properties
|
||||
- ConfigurationManager target package parsing refactored: mode/package extraction deduplicated across GENERATE/PATCH/AUTO branches
|
||||
- system=prop forced boot/vendor override removed, now respects explicit per-component patch level configuration
|
||||
- FileObserver delete handler simplified with direct file access instead of defensive null-checks
|
||||
- AndroidDeviceUtils expanded with additional device property accessors for attestation fields
|
||||
|
||||
### 🚀 The Road Ahead
|
||||
### Binder Performance
|
||||
- Safe parcel reads at 6 deserialization sites, replacing force-unwrap NPE paths with early-return on null. A single NPE generates a full stack trace that blocks the binder thread for ~2ms
|
||||
- teeResponses cache populated on generateKey/importKey post-transact, reducing getKeyEntry from 2+ binder round-trips to 1
|
||||
- pingBinder liveness check removed from pre-transact failure path, eliminating a synchronous IPC call on every failed transaction
|
||||
- Native transaction code filtering at C++ level, skipping JNI entirely for PING/INTERFACE/DUMP
|
||||
|
||||
We are aware of the remaining detection vectors (see the issues list) and have clear solutions mapped out for the next release.
|
||||
### Dynamic SecurityLevel Binder Registration
|
||||
- Intercepts getSecurityLevel replies to register hooks on every new BBinder instance keystore2 returns, not just the initial one from setup
|
||||
- Identity hash deduplication prevents double-hooking when keystore2 returns the same binder across multiple calls
|
||||
- Resolves apps that call getSecurityLevel independently and receive a different binder than the one registered at startup
|
||||
|
||||
Google's aggressive push for **Remote Key Provisioning (RKP)** and the drying up of leaked keyboxes is **not** the end for TEESimulator. Our ultimate goal remains unchanged: defeating Keystore attestation **without relying on a valid keybox**.
|
||||
### Build & Packaging
|
||||
- Rust native build task integrated into Gradle with cargo-ndk for aarch64/armv7/x86/x86_64
|
||||
- Module ZIP includes all 4 native libraries (libTEESimulator, libsupervisor, libcertgen, libinject)
|
||||
- customize.sh extraction restored for supervisor daemon and native cert gen library
|
||||
- TeeLatencySimulator added as standalone utility for log-normal hardware latency emulation
|
||||
|
||||
We are inching closer to this milestone, but the fight for device freedom is complex and resource-intensive. Your patience and support (both time and financial) are vital as we conquer these new challenges.
|
||||
---
|
||||
|
||||
## TEESimulator-RS v5.0: AOSP Compliance Overhaul
|
||||
|
||||
Major release integrating 30+ AOSP compliance improvements from upstream PR #157 analysis, layered on top of our StrongBox hardening and native cert gen architecture.
|
||||
|
||||
### Attestation Extension Alignment
|
||||
- 17 enforcement tags added to KeyMintAttestation (ACTIVE_DATETIME, ORIGINATION_EXPIRE, USAGE_EXPIRE, USAGE_COUNT_LIMIT, CALLER_NONCE, UNLOCKED_DEVICE_REQUIRED, INCLUDE_UNIQUE_ID, ROLLBACK_RESISTANCE, EARLY_BOOT_ONLY, ALLOW_WHILE_ON_BODY, TRUSTED_USER_PRESENCE_REQUIRED, TRUSTED_CONFIRMATION_REQUIRED, NO_AUTH_REQUIRED, MAX_USES_PER_BOOT, MAX_BOOT_LEVEL, MIN_MAC_LENGTH, RSA_OAEP_MGF_DIGEST)
|
||||
- BLOCK_MODE encoded as SET OF INTEGER per AOSP attestation_record.h
|
||||
- Version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4)
|
||||
- INCLUDE_UNIQUE_ID computed via HMAC-SHA256 per KeyMint HAL spec using device HBK
|
||||
- AAID gated on attestation challenge presence
|
||||
- Certificate validity defaults aligned with AOSP (epoch notBefore, 9999-12-31 notAfter)
|
||||
|
||||
### Binder Infrastructure
|
||||
- Native transaction code filtering at C++ level, skipping JNI for non-intercepted codes
|
||||
- getNumberOfEntries includes software-generated key count
|
||||
- deleteKey resolves KEY_ID domain via generatedKeys lookup
|
||||
- patchAuthorizations for OS/VENDOR/BOOT patch levels in authorization arrays
|
||||
|
||||
### Software Operation AOSP Conformance
|
||||
- updateAad on non-AEAD operations returns INVALID_TAG (-76), matching AOSP operation.rs
|
||||
- All crypto exceptions wrapped as ServiceSpecificException with correct KeyMint error codes
|
||||
- GCM IV returned in CreateOperationResponse.parameters for encrypt operations
|
||||
- SoftwareOperationBinder methods @Synchronized, matching AOSP Mutex per operation
|
||||
- authorize_create enforcement: PURPOSE validation, algorithm-purpose compatibility, temporal constraints, CALLER_NONCE prohibition, WRAP_KEY rejection
|
||||
|
||||
### Security and Configuration
|
||||
- SELinux permission checks via /proc/pid/attr/current
|
||||
- Per-UID permission verification through IPackageManager.checkPermission
|
||||
- Imported key tracking prevents stale attest-key overrides in getKeyEntry
|
||||
- nspace consistency fix in attest-key override path
|
||||
- TeeLatencySimulator with log-normal distribution matching real hardware profiles
|
||||
- Device-unique HBK seed generated on install (32 bytes from /dev/random)
|
||||
|
||||
### Preserved from v4.8
|
||||
- StrongBox op limits (4 concurrent max, TOO_MANY_OPERATIONS rejection)
|
||||
- LRU operation pruning per security level
|
||||
- Hardware keygen rate limiting (2/30s sliding window, 2 concurrent cap)
|
||||
- Native Rust cert generation with BouncyCastle fallback
|
||||
- Key persistence across reboots
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8.1: StrongBox Op Rejection Fix
|
||||
|
||||
- **StrongBox op limit gate fix** — `trackAndEnforceOpLimit` was only called in the `Domain.KEY_ID` not-found path, so software-generated keys (found via `Domain.APP`) bypassed `STRONGBOX_MAX_CONCURRENT_OPS=4` entirely. DuckDetector's concurrent signing handles test created 24+ operations that all succeeded via LRU pruning instead of being rejected with `TOO_MANY_OPERATIONS (-29)`. Now enforced for all StrongBox createOperation paths.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
|
||||
|
||||
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
|
||||
|
||||
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
|
||||
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
|
||||
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
|
||||
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
|
||||
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
|
||||
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
|
||||
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
|
||||
|
||||
---
|
||||
|
||||
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||
|
||||
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
|
||||
|
||||
- **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 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.
|
||||
|
||||
### Attestation Fixes
|
||||
|
||||
- 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
|
||||
|
||||
- Supervisor daemon keeps the interceptor alive
|
||||
- KSU Action button clears persistent key cache
|
||||
- Clean uninstall removes all traces (persistent keys, TEE status, daemon)
|
||||
|
||||
### Stability
|
||||
|
||||
- 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).
|
||||
|
||||
+13
-2
@@ -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 ---
|
||||
@@ -48,7 +48,7 @@ install_file() {
|
||||
|
||||
# --- Installation ---
|
||||
ui_print "- Extracting module files"
|
||||
for file in customize.sh module.prop service.sh sepolicy.rule daemon; do
|
||||
for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh uninstall.sh; do
|
||||
install_file "$file" "$MODPATH"
|
||||
done
|
||||
|
||||
@@ -67,10 +67,14 @@ ui_print ""
|
||||
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"
|
||||
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
|
||||
chmod 755 "$MODPATH/inject"
|
||||
chmod 755 "$MODPATH/supervisor"
|
||||
|
||||
# --- Configuration Files ---
|
||||
if [ ! -d "$CONFIG_DIR" ]; then
|
||||
@@ -87,3 +91,10 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then
|
||||
ui_print "- Adding default target scope"
|
||||
install_file "target.txt" "$CONFIG_DIR"
|
||||
fi
|
||||
|
||||
rm -f "$CONFIG_DIR/tee_status.txt"
|
||||
|
||||
if [ ! -f "$CONFIG_DIR/hbk" ]; then
|
||||
ui_print "- Generating device-unique hardware-bound key seed"
|
||||
head -c 32 /dev/random > "$CONFIG_DIR/hbk"
|
||||
fi
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
id=tricky_store
|
||||
name=TEESimulator
|
||||
name=TEESimulator-RS
|
||||
version=${REPLACEMEVER}
|
||||
versionCode=${REPLACEMEVERCODE}
|
||||
author=JingMatrix
|
||||
author=JingMatrix, Enginex0
|
||||
description=Software simulation for Android hardware-backed key pairs with key attestation
|
||||
updateJson=https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/update.json
|
||||
updateJson=https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/update.json
|
||||
|
||||
+2
-8
@@ -1,11 +1,5 @@
|
||||
DEBUG=false
|
||||
|
||||
MODDIR=${0%/*}
|
||||
|
||||
cd $MODDIR
|
||||
|
||||
while true; do
|
||||
./daemon "$MODDIR" || exit 1
|
||||
# ensure keystore initialized
|
||||
sleep 2
|
||||
done &
|
||||
# Fork-based supervisor for instant restart
|
||||
./supervisor ./daemon "$MODDIR" &
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#!/system/bin/sh
|
||||
MODDIR=${0%/*}
|
||||
CONFIG_DIR=/data/adb/tricky_store
|
||||
|
||||
# Kill daemon and supervisor
|
||||
for pid in $(pidof TEESimulator) $(pidof supervisor) $(pidof daemon); do
|
||||
kill -9 "$pid" 2>/dev/null
|
||||
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"
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": "v3.1",
|
||||
"versionCode": 59,
|
||||
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.1/TEESimulator-v3.1-59-Release.zip",
|
||||
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md"
|
||||
"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,721 @@
|
||||
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 303: CALLER_NONCE — NULL (presence = true)
|
||||
if params.caller_nonce {
|
||||
fields.push((303, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 400: ACTIVE_DATETIME — INTEGER (milliseconds)
|
||||
if params.active_datetime >= 0 {
|
||||
fields.push((400, enc_integer(params.active_datetime)));
|
||||
}
|
||||
|
||||
// Tag 401: ORIGINATION_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||
if params.origination_expire_datetime >= 0 {
|
||||
fields.push((401, enc_integer(params.origination_expire_datetime)));
|
||||
}
|
||||
|
||||
// Tag 402: USAGE_EXPIRE_DATETIME — INTEGER (milliseconds)
|
||||
if params.usage_expire_datetime >= 0 {
|
||||
fields.push((402, enc_integer(params.usage_expire_datetime)));
|
||||
}
|
||||
|
||||
// Tag 405: USAGE_COUNT_LIMIT — INTEGER
|
||||
if params.usage_count_limit >= 0 {
|
||||
fields.push((405, enc_integer(params.usage_count_limit as i64)));
|
||||
}
|
||||
|
||||
// Tag 509: UNLOCKED_DEVICE_REQUIRED — NULL
|
||||
if params.unlocked_device_required {
|
||||
fields.push((509, enc_null()));
|
||||
}
|
||||
|
||||
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||
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 (conditional)
|
||||
if params.no_auth_required {
|
||||
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_enforcement_tags_in_software_enforced() {
|
||||
let mut params = make_test_params();
|
||||
params.usage_count_limit = 3;
|
||||
params.unlocked_device_required = true;
|
||||
params.caller_nonce = true;
|
||||
params.active_datetime = 1709913600000;
|
||||
let sw = build_software_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&sw);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(tags.contains(&303), "CALLER_NONCE (303) must be in softwareEnforced");
|
||||
assert!(tags.contains(&400), "ACTIVE_DATETIME (400) must be in softwareEnforced");
|
||||
assert!(tags.contains(&405), "USAGE_COUNT_LIMIT (405) must be in softwareEnforced");
|
||||
assert!(tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED (509) must be in softwareEnforced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_auth_required_conditional() {
|
||||
let mut params = make_test_params();
|
||||
params.no_auth_required = false;
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&tee);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(!tags.contains(&503), "NO_AUTH_REQUIRED (503) must be absent when false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enforcement_tags_omitted_when_unset() {
|
||||
let params = make_test_params();
|
||||
let sw = build_software_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&sw);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
assert!(!tags.contains(&303), "CALLER_NONCE should be absent when false");
|
||||
assert!(!tags.contains(&400), "ACTIVE_DATETIME should be absent when -1");
|
||||
assert!(!tags.contains(&405), "USAGE_COUNT_LIMIT should be absent when -1");
|
||||
assert!(!tags.contains(&509), "UNLOCKED_DEVICE_REQUIRED should be absent when false");
|
||||
}
|
||||
|
||||
#[test]
|
||||
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,
|
||||
active_datetime: -1,
|
||||
origination_expire_datetime: -1,
|
||||
usage_expire_datetime: -1,
|
||||
usage_count_limit: -1,
|
||||
caller_nonce: false,
|
||||
unlocked_device_required: false,
|
||||
no_auth_required: true,
|
||||
}
|
||||
}
|
||||
|
||||
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,343 @@
|
||||
#![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")?;
|
||||
|
||||
let active_datetime = get_long(env, config, "activeDatetime")?;
|
||||
let origination_expire_datetime = get_long(env, config, "originationExpireDatetime")?;
|
||||
let usage_expire_datetime = get_long(env, config, "usageExpireDatetime")?;
|
||||
let usage_count_limit = get_int(env, config, "usageCountLimit")?;
|
||||
let caller_nonce = get_boolean(env, config, "callerNonce")?;
|
||||
let unlocked_device_required = get_boolean(env, config, "unlockedDeviceRequired")?;
|
||||
let no_auth_required = get_boolean(env, config, "noAuthRequired")?;
|
||||
|
||||
Ok(CertGenParams {
|
||||
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,
|
||||
active_datetime,
|
||||
origination_expire_datetime,
|
||||
usage_expire_datetime,
|
||||
usage_count_limit,
|
||||
caller_nonce,
|
||||
unlocked_device_required,
|
||||
no_auth_required,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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_boolean(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<bool> {
|
||||
Ok(env.get_field(obj, name, "Z")?.z()?)
|
||||
}
|
||||
|
||||
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||
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,100 @@
|
||||
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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 active_datetime: i64,
|
||||
pub origination_expire_datetime: i64,
|
||||
pub usage_expire_datetime: i64,
|
||||
pub usage_count_limit: i32,
|
||||
pub caller_nonce: bool,
|
||||
pub unlocked_device_required: bool,
|
||||
pub no_auth_required: bool,
|
||||
}
|
||||
|
||||
pub struct GeneratedKeyPair {
|
||||
pub 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")
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ public interface IPackageManager {
|
||||
|
||||
ParceledListSlice<PackageInfo> getInstalledPackages(long flags, int userId);
|
||||
|
||||
int checkPermission(String permName, String pkgName, int userId);
|
||||
|
||||
class Stub {
|
||||
public static IPackageManager asInterface(IBinder binder) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package android.hardware.security.keymint;
|
||||
|
||||
public @interface HardwareAuthenticatorType {
|
||||
int NONE = 0;
|
||||
int PASSWORD = 1;
|
||||
int FINGERPRINT = 2;
|
||||
int ANY = -1;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package android.os;
|
||||
|
||||
/** Stub for android.os.SELinux. */
|
||||
public class SELinux {
|
||||
public static boolean checkSELinuxAccess(
|
||||
String scon, String tcon, String tclass, String perm) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,10 @@ public class ServiceManager {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static boolean isDeclared(String name) {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
public static String[] listServices() {
|
||||
throw new UnsupportedOperationException("STUB!");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package android.os;
|
||||
|
||||
/**
|
||||
* Stub for android.os.ServiceSpecificException.
|
||||
*
|
||||
* <p>Used by AIDL-generated binder stubs to report service-specific errors with numeric codes.
|
||||
* The binder framework serializes this as EX_SERVICE_SPECIFIC on the wire, preserving the integer
|
||||
* error code for the client.
|
||||
*/
|
||||
public class ServiceSpecificException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public ServiceSpecificException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this(errorCode, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user