Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b8a92fbd | ||
|
|
0723865eab | ||
|
|
7cb44b9999 | ||
|
|
eddd9908af | ||
|
|
36ccd22cdc | ||
|
|
81e6fbf97e | ||
|
|
7f63713f07 | ||
|
|
5df76eacd1 | ||
|
|
ca3978888e | ||
|
|
023d7f929d | ||
|
|
bd40f4b950 | ||
|
|
23696d2f61 | ||
|
|
dfacb34cf9 | ||
|
|
06d9db443c | ||
|
|
7e87766493 | ||
|
|
f8bfa0dfd8 | ||
|
|
90ff59e0aa | ||
|
|
8bdf0d59fa | ||
|
|
6ab09f4889 | ||
|
|
f4559bcd19 | ||
|
|
3b5043a1bb | ||
|
|
8001a8678a | ||
|
|
d21822eb9d | ||
|
|
095a658996 | ||
|
|
70e8968e44 | ||
|
|
c122ded7bf | ||
|
|
7b510a9915 | ||
|
|
8f63dda31b | ||
|
|
9896df93de | ||
|
|
0280bcf189 | ||
|
|
438a462bdf | ||
|
|
40b08cd648 | ||
|
|
c5ed627f68 | ||
|
|
bee73eb39b | ||
|
|
a0ee77202c | ||
|
|
09d9896228 | ||
|
|
5a599025ad | ||
|
|
f5c2bcc024 | ||
|
|
85e34ed42f | ||
|
|
52b04675e7 | ||
|
|
ba0628c687 | ||
|
|
c11465d660 | ||
|
|
0d5fd44992 | ||
|
|
776a7b2343 | ||
|
|
14786ecce0 | ||
|
|
41b9cc8f10 | ||
|
|
6df266b688 | ||
|
|
32cfcb3ece | ||
|
|
727b32f6b0 | ||
|
|
ac9641ed2a | ||
|
|
c87759c6c3 | ||
|
|
76b18706f1 | ||
|
|
e76f5115e0 | ||
|
|
0725aed094 | ||
|
|
9d61af6ce8 | ||
|
|
7863e8dd17 | ||
|
|
f840eed42b | ||
|
|
4c7f0a09ea | ||
|
|
4971f7b4a5 | ||
|
|
9ea39f0545 | ||
|
|
c332f8ad0d | ||
|
|
d76e6abeed | ||
|
|
142fe7bf13 | ||
|
|
e41a679928 | ||
|
|
209d5c8902 | ||
|
|
3817b37e18 | ||
|
|
53ae250fc7 | ||
|
|
34ad97366e |
+91
-72
@@ -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)
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
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
|
||||
- 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))"
|
||||
|
||||
# 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
|
||||
|
||||
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: Upload release
|
||||
if: success()
|
||||
id: release
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepareArtifact.outputs.releaseName }}
|
||||
path: "./module-release/*"
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: out/TEESimulator-RS-*-Release.zip
|
||||
retention-days: 30
|
||||
compression-level: 6
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload debug
|
||||
if: success()
|
||||
id: debug
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ steps.prepareArtifact.outputs.debugName }}
|
||||
path: "./module-debug/*"
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: out/TEESimulator-RS-*-Debug.zip
|
||||
retention-days: 7
|
||||
compression-level: 6
|
||||
compression-level: 0
|
||||
|
||||
- name: Upload release mappings
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-mappings-${{ github.run_number }}
|
||||
path: "./app/build/outputs/mapping/release"
|
||||
name: release-mappings
|
||||
path: app/build/outputs/mapping/release
|
||||
retention-days: 30
|
||||
compression-level: 9
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
release:
|
||||
needs: build
|
||||
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read version
|
||||
id: ver
|
||||
run: |
|
||||
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
|
||||
if [[ "${{ job.status }}" == "success" ]]; then
|
||||
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
|
||||
count=$(git rev-list HEAD --count)
|
||||
echo "version=${ver}-${count}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-RS-release-zip
|
||||
path: zips
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: TEESimulator-RS-debug-zip
|
||||
path: zips
|
||||
|
||||
- name: Extract changelog
|
||||
run: |
|
||||
ver="${VER#v}"
|
||||
awk "/^## TEESimulator-RS v${ver%%-*}/{flag=1; next} /^## TEESimulator-RS v/{if(flag) exit} flag" module/changelog.md > /tmp/notes.md
|
||||
cat /tmp/notes.md
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
|
||||
- name: Create release
|
||||
run: |
|
||||
gh release delete "$VER" --yes 2>/dev/null || true
|
||||
RELEASE=$(ls zips/*Release*.zip | head -1)
|
||||
DEBUG=$(ls zips/*Debug*.zip | head -1)
|
||||
gh release create "$VER" \
|
||||
--title "$VER" \
|
||||
--latest \
|
||||
--notes-file /tmp/notes.md \
|
||||
"$RELEASE" \
|
||||
"$DEBUG"
|
||||
env:
|
||||
VER: ${{ steps.ver.outputs.version }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
out
|
||||
.gradle
|
||||
.kotlin
|
||||
app/build
|
||||
build
|
||||
native-certgen/target
|
||||
app/src/main/jniLibs
|
||||
|
||||
@@ -1,32 +1,143 @@
|
||||
# TEESimulator – A Full TEE Emulation Framework
|
||||
<p align="center">
|
||||
<h1 align="center">🔐 TEESimulator</h1>
|
||||
<p align="center"><b>Full TEE Emulation for Rooted Android</b></p>
|
||||
<p align="center">Hardware attestation. Software keys. Zero detection.</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml"><img src="https://github.com/Enginex0/TEESimulator/actions/workflows/build.yml/badge.svg" alt="Build"></a>
|
||||
<img src="https://img.shields.io/badge/version-v4.2-blue?style=for-the-badge" alt="v4.2">
|
||||
<img src="https://img.shields.io/badge/Android-10%2B-green?style=for-the-badge&logo=android" alt="Android 10+">
|
||||
<img src="https://img.shields.io/badge/Telegram-community-blue?style=for-the-badge&logo=telegram" alt="Telegram">
|
||||
</p>
|
||||
</p>
|
||||
|
||||
**TEESimulator** is a system module designed to create a complete, software-based simulation of a hardware-backed Trusted Execution Environment ([TEE](https://source.android.com/docs/security/features/trusty)) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation).
|
||||
---
|
||||
|
||||
The project's goal is to move beyond simple certificate patching and build a robust framework that can create and manage virtual, self-consistent cryptographic keys.
|
||||
> [!NOTE]
|
||||
> **This is a personal fork of [JingMatrix/TEESimulator](https://github.com/JingMatrix/TEESimulator)** with additional hardening, native Rust certificate generation, key persistence, and anti-detection features. For the upstream project, see the original repo.
|
||||
|
||||
## ✨ Core Principles
|
||||
---
|
||||
|
||||
* **Bypass Hardware-Backed Attestation:** The primary goal of this project is to defeat Key Attestation, a security mechanism that allows apps to verify that they are running on a secure, unmodified device. This module provides the tools to bypass these checks on rooted or modified devices.
|
||||
* **Stateful Emulation:** Instead of patching responses from the real TEE, the ultimate goal is to create and manage virtual keys entirely in a simulated software environment. Any request concerning a virtual key will be handled by the simulator, ensuring perfect consistency without ever touching the real hardware.
|
||||
* **Architectural Interception:** By hooking low-level Binder IPC calls to the Keystore, the framework can transparently redirect requests for virtual keys to the software-based simulator, while allowing requests for real keys to pass through to the hardware TEE.
|
||||
* **100% FOSS:** Licensed under GPLv3, ensuring it stays free, auditable, and compliant with open-source laws.
|
||||
## 🧬 What is TEESimulator?
|
||||
|
||||
## 📱 Requirements
|
||||
- Android 10 or above
|
||||
TEESimulator is a **complete software simulation** of Android's hardware-backed [Trusted Execution Environment](https://source.android.com/docs/security/features/trusty) for [Key Attestation](https://developer.android.com/privacy-and-security/security-key-attestation). Instead of patching certificates from the real TEE after the fact, TEESimulator intercepts Binder IPC at the `ioctl` level and generates entire certificate chains from scratch — signed by your keybox, with correct attestation extensions, indistinguishable from hardware-generated keys.
|
||||
|
||||
## 📦 Installation & Configuration
|
||||
The result: **apps that verify hardware attestation see a legitimate, unmodified device** — even on rooted hardware with an unlocked bootloader.
|
||||
|
||||
1. Flash this module via (Magisk / KernelSU / APatch) and reboot. It will replace [TrickyStore](https://github.com/5ec1cff/TrickyStore), [TrickyStoreOSS](https://github.com/beakthoven/TrickyStoreOSS) and their forks.
|
||||
2. (Optional) Place a hardware-backed `keybox.xml` at `/data/adb/tricky_store/keybox.xml`. This provides the cryptographic "root of trust" for the simulator.
|
||||
3. (Optional) Customize target packages in `/data/adb/tricky_store/target.txt`.
|
||||
4. (Optional) Customize the simulated security patch level in `/data/adb/tricky_store/security_patch.txt`.
|
||||
5. Enjoy!
|
||||
> **This is not TrickyStore.** TEESimulator replaces TrickyStore and its forks entirely. It shares the same config paths for drop-in compatibility, but the architecture is fundamentally different: native Rust certificate generation, binder-level interception via `lsplt`, per-UID rate limiting, key persistence, and a multi-layer defense against detector apps.
|
||||
|
||||
**All configuration files are monitored and will take effect immediately upon saving.**
|
||||
---
|
||||
|
||||
## 🔥 Why TEESimulator?
|
||||
|
||||
🔐 **Native Cert Generation** — v4.0 generates X.509 certificate chains in Rust with `ring` and manual DER encoding. No BouncyCastle overhead, no Java crypto quirks, byte-perfect issuer chain linkage.
|
||||
|
||||
🎯 **Binder-Level Interception** — Hooks `ioctl()` on `libc.so` via `lsplt` inside the `keystore2` process. Intercepts `generateKey`, `importKey`, and `getKeyEntry` transactions before the HAL ever sees them.
|
||||
|
||||
🛡️ **Detector Resistant** — Per-UID rate limiting blocks DuckDetector-style keygen flooding. Oversized challenges rejected with real KeyMint error codes. Chain consistency verified byte-for-byte.
|
||||
|
||||
💾 **Key Persistence** — Generated keys survive reboots. Apps that store attestation keys (banking, biometrics) don't break after a restart.
|
||||
|
||||
🔧 **Drop-In Replacement** — Same config paths as TrickyStore (`/data/adb/tricky_store/`). Swap the module ZIP, keep your keybox and target list.
|
||||
|
||||
---
|
||||
|
||||
## ✨ Features
|
||||
|
||||
**Core Attestation Engine**
|
||||
- [x] **Full certificate chain generation** — leaf + intermediates + root, signed by your keybox
|
||||
- [x] **Native Rust certgen** — `libcertgen.so` built with `ring`, `rsa`, and manual DER assembly
|
||||
- [x] **BouncyCastle fallback** — unsupported curves (P-224, P-521, Curve25519) fall back to Java
|
||||
- [x] **ASN.1 attestation extensions** — OID 1.3.6.1.4.1.11129.2.1.17 with all AOSP-specified tags
|
||||
- [x] **Multi-keybox support** — different keybox files per app group via `target.txt`
|
||||
|
||||
**Interception Layer**
|
||||
- [x] **Binder ioctl hook** — `lsplt` PLT hook on `libc.so` inside `keystore2` process
|
||||
- [x] **generateKey / importKey / getKeyEntry** — all three transaction types intercepted
|
||||
- [x] **256KB native payload cap** — oversized binder payloads bypass interception cleanly
|
||||
- [x] **Challenge validation** — rejects >128-byte attestation challenges with `INVALID_INPUT_LENGTH`
|
||||
|
||||
**Hardening**
|
||||
- [x] **Per-UID rate limiter** — 2 hardware keygens per 30s burst window, software fallback on overflow
|
||||
- [x] **importKey eviction guard** — retained patch chains prevent generate-then-import cache attacks
|
||||
- [x] **Key persistence** — file-backed storage with file-level locking, survives reboots and keybox rotations
|
||||
- [x] **Global exception handler** — uncaught exceptions logged, daemon stays alive
|
||||
|
||||
**Configuration**
|
||||
- [x] **Live config reload** — `FileObserver` watches all config files, changes apply immediately
|
||||
- [x] **Security patch spoofing** — per-package `system`, `vendor`, `boot` patch levels with dynamic templates
|
||||
- [x] **Lifecycle scripts** — KSU Action button clears key cache, uninstall removes all traces
|
||||
|
||||
---
|
||||
|
||||
## 📋 Requirements
|
||||
|
||||
> [!IMPORTANT]
|
||||
> TEESimulator requires root access and a valid `keybox.xml` for hardware-level attestation results. Without a keybox, the module generates software-level certificates that won't pass strict hardware attestation checks.
|
||||
|
||||
**You need:**
|
||||
1. Android 10 or above
|
||||
2. A supported root manager (KernelSU, Magisk, or APatch)
|
||||
3. A hardware-backed `keybox.xml` placed at `/data/adb/tricky_store/keybox.xml`
|
||||
|
||||
---
|
||||
|
||||
## 📱 Compatibility
|
||||
|
||||
### Root Managers
|
||||
|
||||
| Manager | Status | Notes |
|
||||
|---|---|---|
|
||||
| KernelSU | ✅ Tested | Full support including Action button and lifecycle scripts |
|
||||
| Magisk | ✅ Supported | Standard module install |
|
||||
| APatch | ✅ Supported | Standard module install |
|
||||
|
||||
### Tested Devices
|
||||
|
||||
| Device | Android | TEE | Status |
|
||||
|---|---|---|---|
|
||||
| Redmi 14C (2409BRN2CA) | 14 (SDK 34) | Beanpod KeyMaster | ✅ Daily driver |
|
||||
|
||||
> Tested against DuckDetector, Luna, Play Integrity, and Key Attestation Demo. If you test on a different device, [open an issue](https://github.com/Enginex0/TEESimulator/issues) with your results.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. **Download** the latest release ZIP from [Releases](https://github.com/Enginex0/TEESimulator/releases)
|
||||
2. **Install** via your root manager (KSU / Magisk / APatch) and reboot
|
||||
3. **Place your keybox** at `/data/adb/tricky_store/keybox.xml`
|
||||
4. **Configure targets** in `/data/adb/tricky_store/target.txt`
|
||||
5. **Verify** — check Play Integrity or run Key Attestation Demo
|
||||
|
||||
TEESimulator replaces TrickyStore, TrickyStoreOSS, and their forks. Existing config files are compatible.
|
||||
|
||||
---
|
||||
|
||||
## 🔨 Building from Source
|
||||
|
||||
The CI workflow builds on every push to `main`. You can also build locally or trigger a build from your own fork.
|
||||
|
||||
**Prerequisites:** JDK 21, Android SDK/NDK 27, Rust stable with `aarch64-linux-android` target, `cargo-ndk`.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Enginex0/TEESimulator.git
|
||||
cd TEESimulator
|
||||
./gradlew zipRelease zipDebug
|
||||
```
|
||||
|
||||
Output ZIPs land in `out/`. The Gradle build automatically invokes `cargo ndk` to cross-compile `libcertgen.so` before packaging.
|
||||
|
||||
To rebuild from a fork, push to `main` or use **Actions → Build → Run workflow**. The workflow installs all toolchains (Java, Rust, cargo-ndk, ccache) and uploads Release + Debug ZIPs as artifacts.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
All configuration files live at `/data/adb/tricky_store/` and are monitored by `FileObserver` — changes take effect immediately without rebooting.
|
||||
|
||||
### The `keybox.xml` Root of Trust
|
||||
|
||||
This file provides the master cryptographic identity for the simulator. It contains a private key and a valid, hardware-backed certificate chain from a real device. The simulator uses this to sign the virtual certificates it generates, making them appear legitimate to verifiers.
|
||||
This file provides the master cryptographic identity. It contains a private key and a hardware-backed certificate chain from a real device. TEESimulator signs all generated certificates with this key, making them appear legitimate to verifiers.
|
||||
|
||||
```xml
|
||||
<?xml version="1.0"?>
|
||||
@@ -40,101 +151,106 @@ This file provides the master cryptographic identity for the simulator. It conta
|
||||
</AndroidAttestation>
|
||||
```
|
||||
|
||||
### Mode and Keybox Configuration (`target.txt`)
|
||||
### Target Packages (`target.txt`)
|
||||
|
||||
TEESimulator currently operates in two primary modes as it transitions towards full emulation.
|
||||
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
|
||||
Controls which apps get intercepted and what simulation mode to use.
|
||||
|
||||
#### Mode Suffixes
|
||||
|
||||
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation.
|
||||
* **`?` → Force Leaf Hacking Mode:** A legacy mode where a real TEE key is generated, but its attestation certificate is intercepted and modified.
|
||||
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device.
|
||||
* **`!` → Force Generation** — Creates a complete software-based virtual key. Full TEE simulation.
|
||||
* **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
|
||||
* **No symbol → Automatic** — Module selects the best mode for your device.
|
||||
|
||||
#### Multi-Keybox Configuration
|
||||
#### Multi-Keybox
|
||||
|
||||
You can specify different keybox files for different groups of applications. This is done by adding a line with the filename in square brackets (e.g., [demo_keybox.xml]).
|
||||
Specify different keybox files for different app groups. Apps listed after a `[filename.xml]` line use that keybox. Apps before any declaration use the default `keybox.xml`.
|
||||
|
||||
All applications listed after this line will use the specified keybox file, until a new keybox is declared. Applications listed before any custom keybox declaration will use the default `keybox.xml`.
|
||||
|
||||
For example:
|
||||
```
|
||||
# These two apps will use the default /data/adb/tricky_store/keybox.xml
|
||||
# Default keybox
|
||||
com.google.android.gms!
|
||||
io.github.vvb2060.keyattestation?
|
||||
|
||||
# Switch to a different keybox for the following apps.
|
||||
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
|
||||
# Switch to a different keybox for the following apps
|
||||
[aosp_keybox.xml]
|
||||
com.google.android.gsf
|
||||
|
||||
# Switch again to another keybox.
|
||||
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
|
||||
# Another keybox
|
||||
[demo_keybox.xml]
|
||||
org.matrix.demo
|
||||
```
|
||||
|
||||
### Security Patch Level (`security_patch.txt`)
|
||||
|
||||
This file allows you to configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` that the simulator will report in its patched or forged attestation certificates.
|
||||
Configure the `osPatchLevel`, `vendorPatchLevel`, and `bootPatchLevel` reported in attestation certificates. This only affects attestation data — it does not change actual system properties.
|
||||
|
||||
**Note:** This only affects the Key Attestation data generated by the simulator. It does not change the actual system properties of your device.
|
||||
#### Global and Per-Package
|
||||
|
||||
#### Global and Per-Package Configuration
|
||||
Settings at the top of the file are global defaults. Add `[package.name]` to override for specific apps.
|
||||
|
||||
You can set a global patch level that applies to all applications, and you can also override these settings for specific packages. The syntax is hierarchical:
|
||||
#### Keys
|
||||
|
||||
* Settings defined at the top of the file, before any `[package.name]` line, are **global** and serve as the default for all apps.
|
||||
* To create a specific configuration for an application, add its package name in square brackets (e.g., `[com.google.android.gms]`). All settings following this line will apply *only* to that package until a new package context is declared.
|
||||
|
||||
#### Configuration Keys and Values
|
||||
|
||||
You can specify the patch level for the following components using a `key=value` format:
|
||||
|
||||
* `system`: The main OS patch level.
|
||||
* `vendor`: The vendor patch level.
|
||||
* `boot`: The boot/kernel patch level.
|
||||
* `all`: A convenient shorthand to set the same date for `system`, `vendor`, and `boot` simultaneously. Any individual key can still be used to override the value set by `all`.
|
||||
|
||||
Dates should be provided in `YYYY-MM-DD` format (e.g., `2025-11-05`).
|
||||
| Key | Scope |
|
||||
|---|---|
|
||||
| `system` | OS patch level |
|
||||
| `vendor` | Vendor patch level |
|
||||
| `boot` | Boot/kernel patch level |
|
||||
| `all` | Shorthand — sets all three at once |
|
||||
|
||||
#### Special Keywords
|
||||
|
||||
In addition to static dates, several special keywords provide advanced, dynamic control:
|
||||
| Keyword | Effect |
|
||||
|---|---|
|
||||
| `today` | Current date, dynamically resolved on each attestation |
|
||||
| `YYYY-MM-DD` templates | Semi-dynamic — `YYYY-MM-05` resolves to the 5th of the current month |
|
||||
| `no` | Omit this patch level tag entirely from the attestation |
|
||||
| `device_default` | Use the device's real hardware value |
|
||||
| `prop` | Read from `ro.build.version.security_patch` (matches what detectors see via getprop) |
|
||||
|
||||
* **`today`**: Dynamically uses the current date every time an attestation is generated. This ensures the device always appears up-to-date without needing manual edits.
|
||||
|
||||
* **Date Templates**: You can create semi-dynamic dates using `YYYY`, `MM`, and `DD` as placeholders for the current year, month, and day. For example, `YYYY-MM-05` will always resolve to the 5th of the current month and year.
|
||||
|
||||
* **`no`**: This keyword instructs the simulator to **completely omit** the corresponding patch level tag from the generated attestation.
|
||||
|
||||
* **`device_default`**: This keyword forces the simulator to fall back and use the device's **real hardware value** for that specific patch level. This is essential for creating exceptions to a global override or an `all` rule.
|
||||
|
||||
#### Example Configuration
|
||||
|
||||
This example demonstrates how to combine global settings, per-package overrides, and special keywords for fine-grained control.
|
||||
#### Example
|
||||
|
||||
```
|
||||
# --- Global Configuration ---
|
||||
# This is the default for all apps unless specified otherwise.
|
||||
# - Forge a recent system patch level, the 5th of the current month (a common patch date).
|
||||
# - Use the device's real vendor patch level.
|
||||
# - Do not report a boot patch level at all.
|
||||
# Global — default for all apps
|
||||
system=YYYY-MM-05
|
||||
vendor=device_default
|
||||
boot=no
|
||||
|
||||
# --- Per-Package Override for Google Play Services ---
|
||||
# This app will report an older, specific date for its system patch.
|
||||
# It will inherit the global settings for vendor (device_default) and boot (no).
|
||||
# Override for GMS
|
||||
[com.google.android.gms]
|
||||
system=2024-10-01
|
||||
|
||||
# --- Per-Package Override for a Demo App ---
|
||||
# This app gets a completely custom configuration.
|
||||
# Custom config for a demo app
|
||||
[org.matrix.demo]
|
||||
# Set a base date for all patch levels...
|
||||
all=2025-09-15
|
||||
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
|
||||
boot=device_default
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💬 Community
|
||||
|
||||
<p align="center">
|
||||
<a href="https://t.me/superpowers9">
|
||||
<img src="https://img.shields.io/badge/⚡_JOIN_THE_GRID-SuperPowers_Telegram-black?style=for-the-badge&logo=telegram&logoColor=cyan&labelColor=0d1117&color=00d4ff" alt="Telegram">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Credits
|
||||
|
||||
- **[JingMatrix](https://github.com/JingMatrix/TEESimulator)** — original author of TEESimulator and the interception architecture
|
||||
- **[5ec1cff](https://github.com/5ec1cff/TrickyStore)** — TrickyStore, the project that pioneered keystore interception on Android
|
||||
- **[LSPlt](https://github.com/LSPosed/LSPlt)** — PLT hook library used for binder interception
|
||||
- **[ring](https://github.com/briansmith/ring)** — Rust cryptography library powering native cert generation
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the [GNU General Public License v3.0](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<b>🔐 Because the best attestation is the one the TEE never generated.</b>
|
||||
</p>
|
||||
|
||||
+46
-16
@@ -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 = "v4.8"
|
||||
|
||||
android {
|
||||
namespace = "org.matrix.TEESimulator"
|
||||
@@ -71,6 +71,35 @@ dependencies {
|
||||
implementation(libs.bcpkix)
|
||||
}
|
||||
|
||||
// --- Rust native cert gen build task ---
|
||||
val buildRustCertgen by tasks.registering(Exec::class) {
|
||||
group = "TEESimulator-RS Native Build"
|
||||
description = "Builds libcertgen.so via cargo-ndk for arm64-v8a."
|
||||
|
||||
workingDir = rootProject.projectDir.resolve("native-certgen")
|
||||
|
||||
commandLine(
|
||||
"cargo", "ndk",
|
||||
"-t", "arm64-v8a",
|
||||
"-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath,
|
||||
"build", "--release"
|
||||
)
|
||||
|
||||
inputs.dir(rootProject.projectDir.resolve("native-certgen/src"))
|
||||
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml"))
|
||||
inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock"))
|
||||
outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs"))
|
||||
|
||||
environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath)
|
||||
}
|
||||
|
||||
// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency
|
||||
tasks.configureEach {
|
||||
if (name.endsWith("JniLibFolders") && name.startsWith("merge")) {
|
||||
dependsOn(buildRustCertgen)
|
||||
}
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants(selector().all()) { variant ->
|
||||
val capitalized = variant.name.replaceFirstChar { it.uppercase() }
|
||||
@@ -79,21 +108,22 @@ androidComponents {
|
||||
// --- Define output locations and file names ---
|
||||
// Stage all files in a temporary directory inside 'build' before zipping
|
||||
val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}")
|
||||
val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip"
|
||||
val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip"
|
||||
|
||||
// Task 1: Prepare all module files in the temporary build directory.
|
||||
// Using Sync ensures that stale files from previous runs are removed.
|
||||
val prepareModuleFilesTask =
|
||||
tasks.register<Sync>("prepareModuleFiles${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Prepares all files for the ${variant.name} module zip."
|
||||
|
||||
if (isDebug) {
|
||||
dependsOn("package${capitalized}")
|
||||
} else {
|
||||
dependsOn("minify${capitalized}WithR8")
|
||||
}
|
||||
dependsOn("strip${capitalized}DebugSymbols")
|
||||
}
|
||||
dependsOn(buildRustCertgen)
|
||||
|
||||
if (isDebug) {
|
||||
from(variant.artifacts.get(SingleArtifact.APK)) {
|
||||
@@ -110,13 +140,14 @@ androidComponents {
|
||||
}
|
||||
}
|
||||
|
||||
from(
|
||||
project.layout.buildDirectory.dir(
|
||||
val nativeLibsDir = if (isDebug) {
|
||||
"intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
|
||||
} else {
|
||||
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib"
|
||||
)
|
||||
) {
|
||||
into("lib") // Place them in the 'lib' subfolder of the staging directory.
|
||||
include("**/libinject.so", "**/libTEESimulator.so")
|
||||
}
|
||||
from(project.layout.buildDirectory.dir(nativeLibsDir)) {
|
||||
into("lib")
|
||||
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
|
||||
}
|
||||
|
||||
// Now, copy and process the files from 'module' directory.
|
||||
@@ -131,8 +162,7 @@ androidComponents {
|
||||
// Use expand() for simple key-value replacement.
|
||||
expand(
|
||||
"REPLACEMEVERCODE" to gitCommitCount.toString(),
|
||||
"REPLACEMEVER" to
|
||||
"$verName ($gitCommitCount-$gitCommitHash-${variant.name})",
|
||||
"REPLACEMEVER" to "$verName-$gitCommitCount",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,7 +173,7 @@ androidComponents {
|
||||
// Task 2: Zip the prepared files from the temporary directory.
|
||||
val zipTask =
|
||||
tasks.register<Zip>("zip${capitalized}") {
|
||||
group = "TEESimulator Module Packaging"
|
||||
group = "TEESimulator-RS Module Packaging"
|
||||
description = "Creates the flashable zip for the ${variant.name} module."
|
||||
dependsOn(prepareModuleFilesTask)
|
||||
|
||||
@@ -156,7 +186,7 @@ androidComponents {
|
||||
fun createInstallTasks(rootProvider: String, installCli: String) {
|
||||
val pushTask =
|
||||
tasks.register<Exec>("push${rootProvider}Module${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description =
|
||||
"Pushes the ${variant.name} module to the device for $rootProvider."
|
||||
dependsOn(zipTask)
|
||||
@@ -170,7 +200,7 @@ androidComponents {
|
||||
|
||||
val installTask =
|
||||
tasks.register<Exec>("install${rootProvider}${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider."
|
||||
dependsOn(pushTask)
|
||||
commandLine(
|
||||
@@ -183,7 +213,7 @@ androidComponents {
|
||||
}
|
||||
|
||||
tasks.register<Exec>("install${rootProvider}AndReboot${capitalized}") {
|
||||
group = "TEESimulator Module Installation"
|
||||
group = "TEESimulator-RS Module Installation"
|
||||
description = "Installs the ${variant.name} module via $rootProvider and reboots."
|
||||
dependsOn(installTask)
|
||||
commandLine("adb", "reboot")
|
||||
|
||||
Vendored
+6
@@ -7,3 +7,9 @@
|
||||
-keepclasseswithmembers class org.matrix.TEESimulator.App {
|
||||
public static void main(java.lang.String[]);
|
||||
}
|
||||
|
||||
-keepclasseswithmembers class org.matrix.TEESimulator.pki.NativeCertGen {
|
||||
native <methods>;
|
||||
*;
|
||||
}
|
||||
-keep class org.matrix.TEESimulator.pki.CertGenConfig { *; }
|
||||
|
||||
@@ -22,6 +22,9 @@ 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_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)
|
||||
|
||||
@@ -348,15 +348,22 @@ 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.
|
||||
*/
|
||||
constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
|
||||
|
||||
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
|
||||
if (!txn_data || txn_data->target.ptr == 0)
|
||||
return;
|
||||
|
||||
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
|
||||
if (txn_data->data_size > kMaxInterceptableDataSize)
|
||||
return;
|
||||
|
||||
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
|
||||
// System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range.
|
||||
// Skip those — intercepting a ping adds measurable latency that timing detectors flag.
|
||||
if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode)
|
||||
return;
|
||||
|
||||
bool hijack = false;
|
||||
ThreadTransactionInfo info;
|
||||
|
||||
@@ -592,9 +599,16 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
||||
Parcel pre_req, pre_resp;
|
||||
writeTransactionData(pre_req, tx_id, target, code, flags, request);
|
||||
|
||||
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
|
||||
status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
|
||||
if (pre_status != OK) {
|
||||
// Block when interceptor is dead to prevent privacy leak to third-party apps
|
||||
if (callback->pingBinder() != OK) {
|
||||
LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id);
|
||||
result = DEAD_OBJECT;
|
||||
return true;
|
||||
}
|
||||
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t action = pre_resp.readInt32();
|
||||
@@ -647,7 +661,8 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
|
||||
VALIDATE_STATUS(tx_id, post_req.appendFrom(reply, 0, reply_size));
|
||||
}
|
||||
|
||||
if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) {
|
||||
status_t post_status = callback->transact(intercept::kPostTransact, post_req, &post_resp);
|
||||
if (post_status == OK) {
|
||||
int32_t post_action = post_resp.readInt32();
|
||||
if (post_action == intercept::kActionOverrideReply && reply) {
|
||||
result = post_resp.readInt32(); // Read new status
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
@@ -22,8 +23,6 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
object App {
|
||||
// The delay in milliseconds before retrying to initialize the interceptor.
|
||||
private const val RETRY_DELAY_MS = 1000L
|
||||
// The sleep duration in milliseconds for the main service loop to keep the process alive.
|
||||
private const val SERVICE_SLEEP_MS = 1000000L
|
||||
|
||||
/**
|
||||
* The main entry point of the TEESimulator application.
|
||||
@@ -51,6 +50,8 @@ object App {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
|
||||
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
|
||||
|
||||
// This starts the message queue processing. It blocks here indefinitely
|
||||
// processing messages until Looper.myLooper().quit() is called.
|
||||
Looper.loop()
|
||||
|
||||
@@ -112,6 +112,7 @@ object AttestationBuilder {
|
||||
}
|
||||
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
|
||||
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
|
||||
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
|
||||
if (bootPatch != DO_NOT_REPORT) {
|
||||
DERTaggedObject(
|
||||
@@ -181,11 +182,40 @@ object AttestationBuilder {
|
||||
AttestationConstants.TAG_DIGEST,
|
||||
DERSet(params.digest.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
),
|
||||
)
|
||||
|
||||
if (params.ecCurve != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_EC_CURVE,
|
||||
ASN1Integer(params.ecCurve.toLong()),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.padding.isNotEmpty()) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_PADDING,
|
||||
DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (params.rsaPublicExponent != null) {
|
||||
list.add(
|
||||
DERTaggedObject(
|
||||
true,
|
||||
AttestationConstants.TAG_RSA_PUBLIC_EXPONENT,
|
||||
ASN1Integer(params.rsaPublicExponent.toLong()),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
list.addAll(
|
||||
listOf(
|
||||
DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE),
|
||||
DERTaggedObject(
|
||||
true,
|
||||
@@ -198,6 +228,7 @@ object AttestationBuilder {
|
||||
buildRootOfTrust(null),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
// Use the same logic as getSimulatedHardwareProperties to conditionally add patch levels.
|
||||
val simulatedProperties = getSimulatedHardwareProperties(uid)
|
||||
@@ -344,7 +375,7 @@ object AttestationBuilder {
|
||||
* retrieved.
|
||||
*/
|
||||
@Throws(Throwable::class)
|
||||
private fun createApplicationId(uid: Int): DEROctetString {
|
||||
internal fun createApplicationId(uid: Int): DEROctetString {
|
||||
val pm =
|
||||
ConfigurationManager.getPackageManager()
|
||||
?: throw IllegalStateException("PackageManager not found!")
|
||||
|
||||
@@ -89,5 +89,5 @@ object AttestationConstants {
|
||||
|
||||
// --- Other Constants ---
|
||||
// https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength
|
||||
const val CHALLENGE_LENGTH_LIMIT = 128
|
||||
}
|
||||
|
||||
@@ -249,6 +249,10 @@ object DeviceAttestationService {
|
||||
verifiedBootKey = null
|
||||
}
|
||||
|
||||
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
|
||||
verifiedBootHash = null
|
||||
}
|
||||
|
||||
SystemLogger.info(
|
||||
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, osPatch=$osPatchLevel, vendorPatch=$vendorPatchLevel, bootPatch=$bootPatchLevel, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.matrix.TEESimulator.attestation
|
||||
|
||||
import android.hardware.security.keymint.*
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import java.math.BigInteger
|
||||
import java.util.Date
|
||||
import javax.security.auth.x500.X500Principal
|
||||
@@ -18,8 +19,9 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
data class KeyMintAttestation(
|
||||
val keySize: Int,
|
||||
val algorithm: Int,
|
||||
val ecCurve: Int,
|
||||
val ecCurve: Int?,
|
||||
val ecCurveName: String,
|
||||
val origin: Int?,
|
||||
val blockMode: List<Int>,
|
||||
val padding: List<Int>,
|
||||
val purpose: List<Int>,
|
||||
@@ -51,9 +53,12 @@ data class KeyMintAttestation(
|
||||
algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0,
|
||||
|
||||
// AOSP: [key_param(tag = EC_CURVE, field = EcCurve)]
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
|
||||
ecCurve = params.findEcCurve(Tag.EC_CURVE),
|
||||
ecCurveName = params.deriveEcCurveName(),
|
||||
|
||||
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
|
||||
origin = params.findOrigin(Tag.ORIGIN),
|
||||
|
||||
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
|
||||
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
|
||||
|
||||
@@ -99,6 +104,10 @@ data class KeyMintAttestation(
|
||||
// Log all parsed parameters for debugging purposes.
|
||||
params.forEach { KeyMintParameterLogger.logParameter(it) }
|
||||
}
|
||||
|
||||
fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
|
||||
fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED
|
||||
}
|
||||
|
||||
// --- Private helper extension functions for parsing KeyParameter arrays ---
|
||||
@@ -115,6 +124,10 @@ private fun Array<KeyParameter>.findAlgorithm(tag: Int): Int? =
|
||||
private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
|
||||
this.find { it.tag == tag }?.value?.ecCurve
|
||||
|
||||
/** Maps to AOSP field = Origin */
|
||||
private fun Array<KeyParameter>.findOrigin(tag: Int): Int? =
|
||||
this.find { it.tag == tag }?.value?.origin
|
||||
|
||||
/** Maps to AOSP field = LongInteger */
|
||||
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
|
||||
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
|
||||
|
||||
@@ -253,7 +253,14 @@ object ConfigurationManager {
|
||||
}
|
||||
|
||||
// Parse global and per-package configurations.
|
||||
val newGlobalLevel = parseLines(contextLines[""])
|
||||
var newGlobalLevel = parseLines(contextLines[""])
|
||||
// TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop
|
||||
// resolves to the real device prop — force boot/vendor through the same path
|
||||
// to prevent cross-component date mismatches on non-Pixel devices.
|
||||
if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) {
|
||||
SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})")
|
||||
newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop")
|
||||
}
|
||||
contextLines.remove("") // Remove global context to iterate over packages next
|
||||
|
||||
for ((pkg, lines) in contextLines) {
|
||||
@@ -307,8 +314,10 @@ object ConfigurationManager {
|
||||
|
||||
val file = if (event != DELETE) File(configRoot, path) else null
|
||||
when (path) {
|
||||
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!)
|
||||
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!)
|
||||
TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
|
||||
?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.")
|
||||
PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) }
|
||||
?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.")
|
||||
// Any change to an XML file is assumed to be a keybox.
|
||||
// The cache in KeyBoxManager will handle reloading it on its next use.
|
||||
else ->
|
||||
|
||||
+15
-1
@@ -12,6 +12,18 @@ data class KeyIdentifier(val uid: Int, val alias: String)
|
||||
/** A collection of utility functions to support binder interception. */
|
||||
object InterceptorUtils {
|
||||
|
||||
private const val EX_SERVICE_SPECIFIC = -8
|
||||
|
||||
fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply {
|
||||
val parcel = Parcel.obtain().apply {
|
||||
writeInt(EX_SERVICE_SPECIFIC)
|
||||
writeString(null)
|
||||
writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196)
|
||||
writeInt(errorCode)
|
||||
}
|
||||
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses reflection to get the integer transaction code for a given method name from a Stub
|
||||
* class. This is necessary for older Android versions where codes are not public constants.
|
||||
@@ -108,6 +120,8 @@ object InterceptorUtils {
|
||||
|
||||
/** Checks if a reply parcel contains an exception without consuming it. */
|
||||
fun hasException(reply: Parcel): Boolean {
|
||||
return runCatching { reply.readException() }.exceptionOrNull() != null
|
||||
val exception = runCatching { reply.readException() }.exceptionOrNull()
|
||||
if (exception != null) reply.setDataPosition(0)
|
||||
return exception != null
|
||||
}
|
||||
}
|
||||
|
||||
+120
-37
@@ -1,21 +1,24 @@
|
||||
package org.matrix.TEESimulator.interception.keystore
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.IKeystoreService
|
||||
import android.system.keystore2.KeyDescriptor
|
||||
import android.system.keystore2.KeyEntryResponse
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.Certificate
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
|
||||
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
|
||||
import org.matrix.TEESimulator.logging.KeyMintParameterLogger
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||
|
||||
/**
|
||||
@@ -52,6 +55,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||
}
|
||||
|
||||
private const val RESPONSE_KEY_NOT_FOUND = 7
|
||||
private val deletedSoftwareKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
|
||||
override val serviceName = "android.system.keystore2.IKeystoreService/default"
|
||||
override val processName = "keystore2"
|
||||
override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry"
|
||||
@@ -73,6 +79,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
|
||||
register(backdoor, tee.asBinder(), interceptor)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
|
||||
@@ -84,6 +91,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
val interceptor =
|
||||
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
|
||||
register(backdoor, strongbox.asBinder(), interceptor)
|
||||
interceptor.loadPersistedKeys()
|
||||
}
|
||||
}
|
||||
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
|
||||
@@ -99,10 +107,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
|
||||
|
||||
if (ConfigurationManager.shouldSkipUid(callingUid))
|
||||
val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
|
||||
val isGMS = packages.contains("com.google.android.gms")
|
||||
|
||||
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
|
||||
@@ -137,12 +149,21 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.ContinueAndSkipPost
|
||||
|
||||
if (descriptor.alias != null) {
|
||||
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
|
||||
} else {
|
||||
SystemLogger.info(
|
||||
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
|
||||
)
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
val keyId = KeyIdentifier(callingUid, descriptor.alias)
|
||||
|
||||
if (code == DELETE_KEY_TRANSACTION) {
|
||||
if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) {
|
||||
val wasSoftwareKey = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null
|
||||
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
|
||||
if (wasSoftwareKey) {
|
||||
deletedSoftwareKeys.add(keyId)
|
||||
SystemLogger.info(
|
||||
"[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response."
|
||||
)
|
||||
@@ -151,9 +172,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val response =
|
||||
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
?: return TransactionResult.Continue
|
||||
val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
|
||||
if (response == null) {
|
||||
if (deletedSoftwareKeys.remove(keyId)) {
|
||||
SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}")
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND)
|
||||
}
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId))
|
||||
SystemLogger.info("${descriptor.alias} was an attestation key")
|
||||
@@ -207,34 +233,88 @@ 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 }
|
||||
val authorizations = response.metadata.authorizations
|
||||
val parsedParameters =
|
||||
KeyMintAttestation(
|
||||
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
|
||||
)
|
||||
|
||||
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.")
|
||||
if (parsedParameters.isImportKey()) {
|
||||
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
|
||||
if (retainedChain == null) {
|
||||
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
|
||||
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
if (parsedParameters.isAttestKey()) {
|
||||
SystemLogger.warning(
|
||||
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
|
||||
)
|
||||
val keyData =
|
||||
CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid,
|
||||
keyId.alias,
|
||||
null,
|
||||
parsedParameters,
|
||||
response.metadata.keySecurityLevel,
|
||||
) ?: throw Exception("Failed to create overriding attest key pair.")
|
||||
|
||||
CertificateHelper.updateCertificateChain(
|
||||
response.metadata,
|
||||
keyData.second.toTypedArray(),
|
||||
)
|
||||
.getOrThrow()
|
||||
|
||||
keyDescriptor.nspace = SecureRandom().nextLong()
|
||||
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
|
||||
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
|
||||
keyData.first,
|
||||
keyDescriptor.nspace,
|
||||
response,
|
||||
)
|
||||
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
|
||||
|
||||
GeneratedKeyPersistence.save(
|
||||
keyId = keyId,
|
||||
keyPair = keyData.first,
|
||||
nspace = keyDescriptor.nspace,
|
||||
securityLevel = response.metadata.keySecurityLevel,
|
||||
certChain = keyData.second,
|
||||
algorithm = parsedParameters.algorithm,
|
||||
keySize = parsedParameters.keySize,
|
||||
ecCurve = parsedParameters.ecCurve ?: 0,
|
||||
purposes = parsedParameters.purpose,
|
||||
digests = parsedParameters.digest,
|
||||
isAttestationKey = true,
|
||||
)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
val originalChain = CertificateHelper.getCertificateChain(response)
|
||||
|
||||
if (originalChain == null || originalChain.size < 2) {
|
||||
SystemLogger.info(
|
||||
@@ -243,11 +323,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
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>
|
||||
@@ -257,20 +332,25 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
)
|
||||
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)
|
||||
finalChain =
|
||||
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
|
||||
KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain
|
||||
}
|
||||
|
||||
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
|
||||
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 InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
.onFailure {
|
||||
SystemLogger.error(
|
||||
"[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
|
||||
it,
|
||||
)
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
}
|
||||
return TransactionResult.SkipTransaction
|
||||
@@ -290,6 +370,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
|
||||
|
||||
metadata.certificate = publicCert
|
||||
metadata.certificateChain = certificateChain
|
||||
|
||||
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
|
||||
|
||||
SystemLogger.verbose(
|
||||
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
|
||||
)
|
||||
|
||||
+2
-1
@@ -407,8 +407,9 @@ private data class LegacyKeygenParameters(
|
||||
return KeyMintAttestation(
|
||||
keySize = this.keySize,
|
||||
algorithm = this.algorithm,
|
||||
ecCurve = 0, // Not explicitly available in legacy args, but not critical
|
||||
ecCurve = 0,
|
||||
ecCurveName = this.ecCurveName ?: "",
|
||||
origin = null,
|
||||
blockMode = listOf<Int>(),
|
||||
padding = listOf<Int>(),
|
||||
purpose = this.purpose,
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ object ListEntriesHandler {
|
||||
startPastAlias: String?,
|
||||
): List<KeyDescriptor> {
|
||||
return KeyMintSecurityLevelInterceptor.generatedKeys.keys
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) }
|
||||
.filter { it.uid == uid && (startPastAlias == null || it.alias > startPastAlias) }
|
||||
.map { keyId ->
|
||||
KeyDescriptor().apply {
|
||||
this.domain = Domain.APP
|
||||
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
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
|
||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Re-persist updates the cert chain for an already-persisted key without
|
||||
// reconstructing authorization parameters from the response. This avoids
|
||||
// pulling keymint Tag dependencies into this file and is correct because
|
||||
// the only field that changes post-generation is the patched cert chain.
|
||||
fun rePersistIfNeeded(
|
||||
callingUid: Int,
|
||||
generatedKeyInfo: KeyMintSecurityLevelInterceptor.GeneratedKeyInfo,
|
||||
) {
|
||||
val metadata = generatedKeyInfo.response.metadata
|
||||
if (metadata == null) {
|
||||
SystemLogger.debug("rePersist: no metadata, skipping")
|
||||
return
|
||||
}
|
||||
val secLevel = metadata.keySecurityLevel
|
||||
|
||||
val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) ->
|
||||
id.uid == callingUid && info.nspace == generatedKeyInfo.nspace
|
||||
}
|
||||
if (entry == null) {
|
||||
SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}")
|
||||
return
|
||||
}
|
||||
|
||||
val keyId = entry.key
|
||||
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||
val existing = File(PERSISTENCE_DIR, filename)
|
||||
|
||||
if (!existing.exists()) {
|
||||
SystemLogger.debug("rePersist: no existing file for $keyId, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
val newChain = CertificateHelper.getCertificateChain(metadata)
|
||||
if (newChain == null) {
|
||||
SystemLogger.warning("rePersist: could not extract cert chain for $keyId")
|
||||
return
|
||||
}
|
||||
|
||||
val persisted = runCatching {
|
||||
DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input ->
|
||||
val version = input.readInt()
|
||||
if (version != FORMAT_VERSION) {
|
||||
SystemLogger.warning("rePersist: unknown format version $version for $keyId")
|
||||
return
|
||||
}
|
||||
readPersistedKeyData(input)
|
||||
}
|
||||
}.getOrNull()
|
||||
if (persisted == null) {
|
||||
SystemLogger.warning("rePersist: failed to read existing data for $keyId")
|
||||
return
|
||||
}
|
||||
|
||||
save(
|
||||
keyId = keyId,
|
||||
keyPair = generatedKeyInfo.keyPair,
|
||||
nspace = generatedKeyInfo.nspace,
|
||||
securityLevel = secLevel,
|
||||
certChain = newChain.toList(),
|
||||
algorithm = persisted.algorithm,
|
||||
keySize = persisted.keySize,
|
||||
ecCurve = persisted.ecCurve,
|
||||
purposes = persisted.purposes,
|
||||
digests = persisted.digests,
|
||||
isAttestationKey = persisted.isAttestationKey,
|
||||
)
|
||||
SystemLogger.debug("Re-persisted key $keyId with updated cert chain")
|
||||
}
|
||||
|
||||
// Corrupted binary files can have arbitrary length fields — cap allocations
|
||||
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"
|
||||
}
|
||||
|
||||
// Reads all fields after version has already been consumed
|
||||
private fun readPersistedKeyData(input: DataInputStream): PersistedKeyData {
|
||||
val secLevel = 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
|
||||
}
|
||||
|
||||
return PersistedKeyData(
|
||||
uid = uid,
|
||||
alias = alias,
|
||||
nspace = nspace,
|
||||
securityLevel = secLevel,
|
||||
isAttestationKey = isAttestKey,
|
||||
algorithm = algo,
|
||||
keySize = kSize,
|
||||
ecCurve = curve,
|
||||
purposes = purposes,
|
||||
digests = digests,
|
||||
privateKeyBytes = pkBytes,
|
||||
certChainBytes = certChainBytes,
|
||||
)
|
||||
}
|
||||
}
|
||||
+477
-106
@@ -1,16 +1,28 @@
|
||||
package org.matrix.TEESimulator.interception.keystore.shim
|
||||
|
||||
import android.hardware.security.keymint.Algorithm
|
||||
import android.hardware.security.keymint.EcCurve
|
||||
import android.hardware.security.keymint.KeyParameter
|
||||
import android.hardware.security.keymint.KeyParameterValue
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.KeyOrigin
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.hardware.security.keymint.Tag
|
||||
import android.os.IBinder
|
||||
import android.os.Parcel
|
||||
import android.system.keystore2.*
|
||||
import android.util.Pair as AndroidPair
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedDeque
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import org.matrix.TEESimulator.attestation.AttestationBuilder
|
||||
import org.matrix.TEESimulator.attestation.AttestationConstants
|
||||
import org.matrix.TEESimulator.attestation.AttestationPatcher
|
||||
import org.matrix.TEESimulator.attestation.KeyMintAttestation
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
@@ -18,25 +30,28 @@ import org.matrix.TEESimulator.interception.core.BinderInterceptor
|
||||
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
|
||||
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
import org.matrix.TEESimulator.pki.CertGenConfig
|
||||
import org.matrix.TEESimulator.pki.CertificateGenerator
|
||||
import org.matrix.TEESimulator.pki.CertificateHelper
|
||||
import org.matrix.TEESimulator.pki.KeyBoxManager
|
||||
import org.matrix.TEESimulator.pki.NativeCertGen
|
||||
import org.matrix.TEESimulator.util.AndroidDeviceUtils
|
||||
import org.matrix.TEESimulator.util.AndroidPermissionUtils
|
||||
|
||||
/**
|
||||
* Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where
|
||||
* the core logic for key generation and import handling for modern Android resides.
|
||||
*/
|
||||
class KeyMintSecurityLevelInterceptor(
|
||||
private val original: IKeystoreSecurityLevel,
|
||||
private val securityLevel: Int,
|
||||
) : BinderInterceptor() {
|
||||
|
||||
// --- Data Structures for State Management ---
|
||||
data class GeneratedKeyInfo(
|
||||
val keyPair: KeyPair,
|
||||
val nspace: Long,
|
||||
val response: KeyEntryResponse,
|
||||
)
|
||||
|
||||
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
|
||||
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
|
||||
|
||||
override fun onPreTransact(
|
||||
txId: Long,
|
||||
target: IBinder,
|
||||
@@ -52,7 +67,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
GENERATE_KEY_TRANSACTION -> {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
|
||||
if (!shouldSkip) return handleGenerateKey(callingUid, data)
|
||||
if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
|
||||
}
|
||||
CREATE_OPERATION_TRANSACTION -> {
|
||||
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
|
||||
@@ -93,6 +108,11 @@ class KeyMintSecurityLevelInterceptor(
|
||||
reply: Parcel?,
|
||||
resultCode: Int,
|
||||
): TransactionResult {
|
||||
if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) {
|
||||
val remaining = hardwareKeygenCount(callingUid).decrementAndGet()
|
||||
SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}")
|
||||
}
|
||||
|
||||
// We only care about successful transactions.
|
||||
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
|
||||
return TransactionResult.SkipTransaction
|
||||
@@ -104,7 +124,14 @@ class KeyMintSecurityLevelInterceptor(
|
||||
val keyDescriptor =
|
||||
data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
?: return TransactionResult.SkipTransaction
|
||||
cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias))
|
||||
// Evict generated key data but retain patched chains so detectors
|
||||
// can't use importKey to force unpatched getKeyEntry responses.
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
if (generatedKeys.remove(keyId) != null) {
|
||||
SystemLogger.debug("Remove generated key on importKey $keyId")
|
||||
GeneratedKeyPersistence.delete(keyId)
|
||||
}
|
||||
attestationKeys.remove(keyId)
|
||||
} else if (code == CREATE_OPERATION_TRANSACTION) {
|
||||
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
|
||||
|
||||
@@ -172,40 +199,90 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return TransactionResult.SkipTransaction
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the `createOperation` transaction. It checks if the operation is for a key that was
|
||||
* generated in software. If so, it creates a software-based operation handler. Otherwise, it
|
||||
* lets the call proceed to the real hardware service.
|
||||
*/
|
||||
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
|
||||
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
|
||||
val before = ops.size
|
||||
ops.removeIf { it.finalized }
|
||||
val afterClean = ops.size
|
||||
while (ops.size >= maxOps) {
|
||||
val oldest = ops.pollFirst() ?: break
|
||||
if (!oldest.finalized) {
|
||||
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
|
||||
oldest.abort()
|
||||
}
|
||||
}
|
||||
ops.addLast(newOp)
|
||||
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
|
||||
}
|
||||
|
||||
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
|
||||
if (securityLevel != SecurityLevel.STRONGBOX) return null
|
||||
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
|
||||
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
|
||||
timestamps.removeIf { it < cutoff }
|
||||
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
|
||||
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
|
||||
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
|
||||
}
|
||||
timestamps.addLast(System.nanoTime())
|
||||
return null
|
||||
}
|
||||
|
||||
private fun handleCreateOperation(
|
||||
txId: Long,
|
||||
callingUid: Int,
|
||||
data: Parcel,
|
||||
): TransactionResult {
|
||||
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||
|
||||
// An operation must use the KEY_ID domain.
|
||||
if (keyDescriptor.domain != Domain.KEY_ID) {
|
||||
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
|
||||
|
||||
// Android framework calls createOperation with domain=APP+alias;
|
||||
// keystore2 internally resolves to KEY_ID — but software keys never
|
||||
// reach keystore2's database, so we must handle both lookup paths.
|
||||
val generatedKeyInfo = when (keyDescriptor.domain) {
|
||||
Domain.APP -> {
|
||||
val alias = keyDescriptor.alias ?: run {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val nspace = keyDescriptor.nspace
|
||||
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
|
||||
|
||||
if (generatedKeyInfo == null) {
|
||||
SystemLogger.debug(
|
||||
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
|
||||
)
|
||||
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
}
|
||||
Domain.KEY_ID -> {
|
||||
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run {
|
||||
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
}
|
||||
|
||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
|
||||
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
|
||||
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val parsedParams = KeyMintAttestation(params)
|
||||
val parsedParams = KeyMintAttestation(params).let { p ->
|
||||
if (p.algorithm != 0) p
|
||||
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> Algorithm.EC
|
||||
"RSA" -> Algorithm.RSA
|
||||
else -> p.algorithm
|
||||
})
|
||||
}
|
||||
|
||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
|
||||
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
|
||||
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency)
|
||||
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
|
||||
pruneOpsForUid(callingUid, softwareOperation, maxOps)
|
||||
val operationBinder = SoftwareOperationBinder(softwareOperation)
|
||||
|
||||
val response =
|
||||
@@ -217,27 +294,69 @@ class KeyMintSecurityLevelInterceptor(
|
||||
return InterceptorUtils.createTypedObjectReply(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it
|
||||
* either generates a key in software or lets the call pass through to the hardware.
|
||||
*/
|
||||
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult {
|
||||
private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
|
||||
if (data.dataSize() > MAX_ALIAS_LENGTH) {
|
||||
SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
return runCatching {
|
||||
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
|
||||
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
|
||||
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
|
||||
|
||||
SystemLogger.debug(
|
||||
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
|
||||
)
|
||||
val params = data.createTypedArray(KeyParameter.CREATOR)!!
|
||||
val parsedParams = KeyMintAttestation(params)
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
val isAttestKeyRequest =
|
||||
parsedParams.purpose.size == 1 &&
|
||||
parsedParams.purpose.contains(KeyPurpose.ATTEST_KEY)
|
||||
|
||||
// Determine if we need to generate a key based on config or
|
||||
// if it's an attestation request in patch mode.
|
||||
val challenge = parsedParams.attestationChallenge
|
||||
if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH)
|
||||
}
|
||||
|
||||
if (params.any { it.tag == Tag.CREATION_DATETIME }) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params")
|
||||
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
|
||||
}
|
||||
|
||||
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||
}
|
||||
|
||||
val hasDeviceIdAttestation = params.any {
|
||||
it.tag == Tag.ATTESTATION_ID_IMEI ||
|
||||
it.tag == Tag.ATTESTATION_ID_MEID ||
|
||||
it.tag == Tag.ATTESTATION_ID_SERIAL ||
|
||||
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
|
||||
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
|
||||
}
|
||||
|
||||
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
|
||||
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
|
||||
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
|
||||
}
|
||||
|
||||
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
|
||||
parsedParams.algorithm == Algorithm.HMAC ||
|
||||
parsedParams.algorithm == Algorithm.TRIPLE_DES
|
||||
|
||||
if (isSymmetric) {
|
||||
SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
|
||||
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
|
||||
return TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
|
||||
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
|
||||
val isAttestKeyRequest = parsedParams.isAttestKey()
|
||||
|
||||
val needsSoftwareGeneration =
|
||||
ConfigurationManager.shouldGenerate(callingUid) ||
|
||||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
|
||||
@@ -245,37 +364,29 @@ class KeyMintSecurityLevelInterceptor(
|
||||
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
|
||||
|
||||
if (needsSoftwareGeneration) {
|
||||
keyDescriptor.nspace = secureRandom.nextLong()
|
||||
SystemLogger.info(
|
||||
"Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]."
|
||||
)
|
||||
|
||||
// Generate the key pair and certificate chain.
|
||||
val keyData =
|
||||
CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid,
|
||||
keyDescriptor.alias,
|
||||
attestationKey?.alias,
|
||||
parsedParams,
|
||||
securityLevel,
|
||||
) ?: throw Exception("CertificateGenerator failed to create key pair.")
|
||||
|
||||
// It is unnecessary but a good practice to clean up possible caches
|
||||
cleanupKeyData(keyId)
|
||||
// Store the generated key data.
|
||||
val response =
|
||||
buildKeyEntryResponse(keyData.second, parsedParams, keyDescriptor)
|
||||
generatedKeys[keyId] =
|
||||
GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||
|
||||
// Return the metadata of our generated key, skipping the real hardware call.
|
||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||
} else if (parsedParams.attestationChallenge != null) {
|
||||
val windowUsed = hardwareKeygenWindowCount(callingUid)
|
||||
val concurrentUsed = hardwareKeygenCount(callingUid).get()
|
||||
|
||||
// Sliding window rate limit
|
||||
if (windowUsed >= MAX_HW_KEYGEN_PER_WINDOW) {
|
||||
SystemLogger.info("[TX_ID: $txId] RATE_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=$concurrentUsed → software fallback")
|
||||
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||
}
|
||||
// Concurrent cap
|
||||
if (hardwareKeygenCount(callingUid).incrementAndGet() > MAX_CONCURRENT_HW_KEYGEN_PER_UID) {
|
||||
hardwareKeygenCount(callingUid).decrementAndGet()
|
||||
SystemLogger.info("[TX_ID: $txId] CONCURRENT_LIMITED uid=$callingUid window=$windowUsed/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → software fallback")
|
||||
return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
|
||||
}
|
||||
// Both checks passed — commit the window permit and forward to hardware TEE
|
||||
recordHardwareKeygen(callingUid)
|
||||
hardwareKeygenTxIds.add(txId)
|
||||
SystemLogger.info("[TX_ID: $txId] HARDWARE_KEYGEN uid=$callingUid window=${windowUsed + 1}/$MAX_HW_KEYGEN_PER_WINDOW concurrent=${concurrentUsed + 1}/$MAX_CONCURRENT_HW_KEYGEN_PER_UID → forwarding to TEE")
|
||||
return TransactionResult.Continue
|
||||
}
|
||||
|
||||
// If not generating, clear any stale state for this alias and let the call proceed.
|
||||
cleanupKeyData(keyId)
|
||||
TransactionResult.ContinueAndSkipPost
|
||||
}
|
||||
@@ -285,20 +396,144 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service.
|
||||
*/
|
||||
private fun doSoftwareKeyGen(
|
||||
callingUid: Int,
|
||||
keyDescriptor: KeyDescriptor,
|
||||
attestationKey: KeyDescriptor?,
|
||||
parsedParams: KeyMintAttestation,
|
||||
keyId: KeyIdentifier,
|
||||
isAttestKeyRequest: Boolean,
|
||||
): TransactionResult {
|
||||
val startNs = System.nanoTime()
|
||||
keyDescriptor.nspace = secureRandom.nextLong()
|
||||
SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].")
|
||||
|
||||
val keyData = if (NativeCertGen.isAvailable && attestationKey == null) {
|
||||
generateAttestedKeyPairNative(callingUid, parsedParams)
|
||||
?: CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
|
||||
)
|
||||
} else {
|
||||
CertificateGenerator.generateAttestedKeyPair(
|
||||
callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel,
|
||||
)
|
||||
} ?: throw Exception("Both native and BouncyCastle cert gen failed.")
|
||||
|
||||
cleanupKeyData(keyId)
|
||||
val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response)
|
||||
if (isAttestKeyRequest) attestationKeys.add(keyId)
|
||||
|
||||
GeneratedKeyPersistence.save(
|
||||
keyId = keyId,
|
||||
keyPair = keyData.first,
|
||||
nspace = keyDescriptor.nspace,
|
||||
securityLevel = securityLevel,
|
||||
certChain = keyData.second.toList(),
|
||||
algorithm = parsedParams.algorithm,
|
||||
keySize = parsedParams.keySize,
|
||||
ecCurve = parsedParams.ecCurve ?: 0,
|
||||
purposes = parsedParams.purpose,
|
||||
digests = parsedParams.digest,
|
||||
isAttestationKey = isAttestKeyRequest,
|
||||
)
|
||||
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS
|
||||
val delayMs = floor - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
|
||||
return InterceptorUtils.createTypedObjectReply(response.metadata)
|
||||
}
|
||||
|
||||
private fun generateAttestedKeyPairNative(
|
||||
callingUid: Int,
|
||||
params: KeyMintAttestation,
|
||||
): AndroidPair<KeyPair, List<Certificate>>? {
|
||||
return runCatching {
|
||||
val algorithmName = when (params.algorithm) {
|
||||
Algorithm.EC -> "EC"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else -> return null
|
||||
}
|
||||
val keyboxFile = ConfigurationManager.getKeyboxFileForUid(callingUid)
|
||||
val keybox = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) ?: return null
|
||||
|
||||
val keyboxPrivateKeyBytes = keybox.keyPair.private.encoded
|
||||
val keyboxCertChainBytes = keybox.certificates
|
||||
.map { it.encoded }
|
||||
.fold(ByteArray(0)) { acc, der -> acc + der }
|
||||
|
||||
val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel)
|
||||
val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel)
|
||||
val appId = AttestationBuilder.createApplicationId(callingUid)
|
||||
|
||||
val config = CertGenConfig(
|
||||
algorithm = params.algorithm,
|
||||
keySize = params.keySize,
|
||||
ecCurve = params.ecCurve ?: 0,
|
||||
rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L,
|
||||
attestationChallenge = params.attestationChallenge,
|
||||
purposes = params.purpose.toIntArray(),
|
||||
digests = params.digest.toIntArray(),
|
||||
certSerial = params.certificateSerial?.toByteArray(),
|
||||
certSubject = params.certificateSubject?.encoded,
|
||||
certNotBefore = params.certificateNotBefore?.time ?: -1L,
|
||||
certNotAfter = params.certificateNotAfter?.time ?: -1L,
|
||||
keyboxPrivateKey = keyboxPrivateKeyBytes,
|
||||
keyboxCertChain = keyboxCertChainBytes,
|
||||
securityLevel = securityLevel,
|
||||
attestVersion = attestVersion,
|
||||
keymasterVersion = keymasterVersion,
|
||||
osVersion = AndroidDeviceUtils.osVersion,
|
||||
osPatchLevel = AndroidDeviceUtils.getPatchLevel(callingUid),
|
||||
vendorPatchLevel = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid),
|
||||
bootPatchLevel = AndroidDeviceUtils.getBootPatchLevelLong(callingUid),
|
||||
bootKey = AndroidDeviceUtils.bootKey,
|
||||
bootHash = AndroidDeviceUtils.bootHash,
|
||||
creationDatetime = System.currentTimeMillis(),
|
||||
attestationApplicationId = appId.octets,
|
||||
moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null,
|
||||
idBrand = params.brand,
|
||||
idDevice = params.device,
|
||||
idProduct = params.product,
|
||||
idSerial = params.serial,
|
||||
idImei = params.imei,
|
||||
idMeid = params.meid,
|
||||
idManufacturer = params.manufacturer,
|
||||
idModel = params.model,
|
||||
idSecondImei = if (attestVersion >= 300) params.secondImei else null,
|
||||
)
|
||||
|
||||
val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null
|
||||
val (keyPair, certs) = NativeCertGen.parseNativeResult(resultBytes)
|
||||
SystemLogger.info("NativeCertGen: generated key pair successfully (${certs.size} certs)")
|
||||
AndroidPair(keyPair, certs)
|
||||
}.onFailure {
|
||||
SystemLogger.error("NativeCertGen: generation failed, falling back to BouncyCastle", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
private fun buildKeyEntryResponse(
|
||||
callingUid: Int,
|
||||
chain: List<Certificate>,
|
||||
params: KeyMintAttestation,
|
||||
descriptor: KeyDescriptor,
|
||||
): KeyEntryResponse {
|
||||
val normalizedKeyDescriptor =
|
||||
KeyDescriptor().apply {
|
||||
domain = Domain.KEY_ID
|
||||
nspace = descriptor.nspace
|
||||
alias = null
|
||||
blob = null
|
||||
}
|
||||
val metadata =
|
||||
KeyMetadata().apply {
|
||||
keySecurityLevel = securityLevel
|
||||
key = descriptor
|
||||
key = normalizedKeyDescriptor
|
||||
CertificateHelper.updateCertificateChain(this, chain.toTypedArray()).getOrThrow()
|
||||
authorizations = params.toAuthorizations(securityLevel)
|
||||
authorizations = params.toAuthorizations(callingUid, securityLevel)
|
||||
modificationTimeMs = System.currentTimeMillis()
|
||||
}
|
||||
return KeyEntryResponse().apply {
|
||||
this.metadata = metadata
|
||||
@@ -306,10 +541,141 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPersistedKeys() {
|
||||
val records = GeneratedKeyPersistence.loadAll(securityLevel)
|
||||
if (records.isEmpty()) {
|
||||
SystemLogger.debug("No persisted keys to restore for security level $securityLevel")
|
||||
return
|
||||
}
|
||||
|
||||
SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel")
|
||||
|
||||
for (record in records) {
|
||||
runCatching {
|
||||
val keyId = KeyIdentifier(record.uid, record.alias)
|
||||
if (generatedKeys.containsKey(keyId)) {
|
||||
SystemLogger.debug("Skipping already-loaded key: $keyId")
|
||||
return@runCatching
|
||||
}
|
||||
|
||||
val algorithmName = when (record.algorithm) {
|
||||
Algorithm.EC -> "EC"
|
||||
Algorithm.RSA -> "RSA"
|
||||
else -> throw IllegalArgumentException("Unknown algorithm: ${record.algorithm}")
|
||||
}
|
||||
|
||||
val keyFactory = KeyFactory.getInstance(algorithmName)
|
||||
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(record.privateKeyBytes))
|
||||
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
val certChain = record.certChainBytes.map { bytes ->
|
||||
certFactory.generateCertificate(ByteArrayInputStream(bytes))
|
||||
}
|
||||
require(certChain.isNotEmpty()) { "Persisted key has empty certificate chain" }
|
||||
|
||||
val publicKey = certChain[0].publicKey
|
||||
val keyPair = KeyPair(publicKey, privateKey)
|
||||
|
||||
val descriptor = KeyDescriptor().apply {
|
||||
domain = Domain.APP
|
||||
nspace = record.nspace
|
||||
alias = record.alias
|
||||
blob = null
|
||||
}
|
||||
|
||||
val attestation = KeyMintAttestation(
|
||||
keySize = record.keySize,
|
||||
algorithm = record.algorithm,
|
||||
ecCurve = record.ecCurve,
|
||||
ecCurveName = "",
|
||||
origin = null,
|
||||
blockMode = emptyList(),
|
||||
padding = emptyList(),
|
||||
purpose = record.purposes,
|
||||
digest = record.digests,
|
||||
rsaPublicExponent = null,
|
||||
certificateSerial = null,
|
||||
certificateSubject = null,
|
||||
certificateNotBefore = null,
|
||||
certificateNotAfter = null,
|
||||
attestationChallenge = null,
|
||||
brand = null,
|
||||
device = null,
|
||||
product = null,
|
||||
serial = null,
|
||||
imei = null,
|
||||
meid = null,
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
secondImei = null,
|
||||
)
|
||||
|
||||
val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor)
|
||||
generatedKeys[keyId] = GeneratedKeyInfo(keyPair, record.nspace, response)
|
||||
if (record.isAttestationKey) attestationKeys.add(keyId)
|
||||
|
||||
SystemLogger.debug("Restored persisted key: $keyId")
|
||||
}.onFailure {
|
||||
SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it)
|
||||
}
|
||||
}
|
||||
|
||||
SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val secureRandom = SecureRandom()
|
||||
|
||||
// Transaction codes for IKeystoreSecurityLevel interface.
|
||||
// Maximum alias length to prevent binder buffer exhaustion (Issue #109)
|
||||
// Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead
|
||||
private const val MAX_ALIAS_LENGTH = 256 * 1024
|
||||
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
|
||||
private const val RESPONSE_INVALID_ARGUMENT = 20
|
||||
private const val TEE_LATENCY_FLOOR_MS = 15L
|
||||
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
|
||||
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
|
||||
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
|
||||
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
|
||||
private const val MAX_CONCURRENT_OPS_PER_UID = 15
|
||||
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
|
||||
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
|
||||
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
|
||||
// Sliding window: max hardware keygen permits per UID within the burst window
|
||||
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
|
||||
private const val BURST_WINDOW_MS = 30_000L
|
||||
private val uidHardwareKeygenCount = ConcurrentHashMap<Int, AtomicInteger>()
|
||||
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
|
||||
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
|
||||
|
||||
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
|
||||
Algorithm.RSA -> params.keySize <= 2048
|
||||
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
|
||||
else -> true
|
||||
}
|
||||
|
||||
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
|
||||
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
|
||||
|
||||
private fun hardwareKeygenWindowCount(uid: Int): Int {
|
||||
val now = System.currentTimeMillis()
|
||||
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
||||
synchronized(timestamps) {
|
||||
timestamps.removeAll { now - it > BURST_WINDOW_MS }
|
||||
if (timestamps.isEmpty()) {
|
||||
uidKeygenTimestamps.remove(uid, timestamps)
|
||||
uidHardwareKeygenCount.remove(uid)
|
||||
}
|
||||
return timestamps.size
|
||||
}
|
||||
}
|
||||
|
||||
private fun recordHardwareKeygen(uid: Int) {
|
||||
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
|
||||
synchronized(timestamps) {
|
||||
timestamps.add(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
||||
private val GENERATE_KEY_TRANSACTION =
|
||||
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
|
||||
private val IMPORT_KEY_TRANSACTION =
|
||||
@@ -331,30 +697,15 @@ class KeyMintSecurityLevelInterceptor(
|
||||
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
|
||||
}
|
||||
|
||||
// Stores keys generated entirely in software.
|
||||
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
|
||||
// Caches patched certificate chains to prevent re-generation and signature inconsistencies.
|
||||
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
// A set to quickly identify keys that were generated for attestation purposes.
|
||||
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
|
||||
// Stores interceptors for active cryptographic operations.
|
||||
val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
|
||||
val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
|
||||
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
|
||||
|
||||
// --- Public Accessors for Other Interceptors ---
|
||||
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
|
||||
generatedKeys[keyId]?.response
|
||||
|
||||
/**
|
||||
* Finds a software-generated key by first filtering all known keys by the caller's UID, and
|
||||
* then matching the specific nspace.
|
||||
*
|
||||
* @param callingUid The UID of the process that initiated the createOperation call.
|
||||
* @param nspace The unique key identifier from the operation's KeyDescriptor.
|
||||
* @return The matching GeneratedKeyInfo if found, otherwise null.
|
||||
*/
|
||||
fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? {
|
||||
// Iterate through all entries in the map to check both the key (for UID) and value (for
|
||||
// nspace).
|
||||
if (nspace == null || nspace == 0L) return null
|
||||
return generatedKeys.entries
|
||||
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
|
||||
@@ -369,6 +720,7 @@ class KeyMintSecurityLevelInterceptor(
|
||||
fun cleanupKeyData(keyId: KeyIdentifier) {
|
||||
if (generatedKeys.remove(keyId) != null) {
|
||||
SystemLogger.debug("Remove generated key ${keyId}")
|
||||
GeneratedKeyPersistence.delete(keyId)
|
||||
}
|
||||
if (patchedChains.remove(keyId) != null) {
|
||||
SystemLogger.debug("Remove patched chain for ${keyId}")
|
||||
@@ -379,7 +731,6 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
|
||||
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
|
||||
// Unregister from the native hook layer first.
|
||||
unregister(backdoor, operationBinder)
|
||||
|
||||
if (interceptedOperations.remove(operationBinder) != null) {
|
||||
@@ -387,33 +738,32 @@ class KeyMintSecurityLevelInterceptor(
|
||||
}
|
||||
}
|
||||
|
||||
// Clears all cached keys.
|
||||
fun invalidatePatchedChains(reason: String? = null) {
|
||||
val count = patchedChains.size
|
||||
if (count == 0) return
|
||||
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
||||
patchedChains.clear()
|
||||
SystemLogger.info("Invalidated $count patched cert chains$reasonMessage.")
|
||||
}
|
||||
|
||||
fun clearAllGeneratedKeys(reason: String? = null) {
|
||||
val count = generatedKeys.size
|
||||
val reasonMessage = reason?.let { " due to $it" } ?: ""
|
||||
generatedKeys.clear()
|
||||
patchedChains.clear()
|
||||
attestationKeys.clear()
|
||||
GeneratedKeyPersistence.deleteAll()
|
||||
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function to convert parsed `KeyMintAttestation` parameters back into an array of
|
||||
* `Authorization` objects for the fake `KeyMetadata`. This version correctly handles the
|
||||
* instantiation of Authorization objects.
|
||||
*/
|
||||
private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Authorization> {
|
||||
private fun KeyMintAttestation.toAuthorizations(
|
||||
callingUid: Int,
|
||||
securityLevel: Int,
|
||||
): Array<Authorization> {
|
||||
val authList = mutableListOf<Authorization>()
|
||||
|
||||
/**
|
||||
* Helper function to create a fully-formed Authorization object.
|
||||
*
|
||||
* @param tag The KeyMint tag (e.g., Tag.ALGORITHM).
|
||||
* @param value The value for the tag, wrapped in a KeyParameterValue.
|
||||
* @return A populated Authorization object.
|
||||
*/
|
||||
fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
|
||||
val param =
|
||||
KeyParameter().apply {
|
||||
@@ -426,14 +776,35 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
|
||||
}
|
||||
}
|
||||
|
||||
// Use the helper to add each authorization entry cleanly.
|
||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||
if (this.ecCurve != null) {
|
||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||
}
|
||||
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
|
||||
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
|
||||
|
||||
authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm)))
|
||||
this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) }
|
||||
authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize)))
|
||||
authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve)))
|
||||
if (this.rsaPublicExponent != null) {
|
||||
authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong())))
|
||||
}
|
||||
authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true)))
|
||||
authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)))
|
||||
authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)))
|
||||
|
||||
val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid)
|
||||
if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch)))
|
||||
}
|
||||
val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid)
|
||||
if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch)))
|
||||
}
|
||||
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid)
|
||||
if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) {
|
||||
authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch)))
|
||||
}
|
||||
authList.add(createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())))
|
||||
authList.add(createAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000)))
|
||||
|
||||
return authList.toTypedArray()
|
||||
}
|
||||
|
||||
+85
-22
@@ -6,6 +6,7 @@ import android.hardware.security.keymint.Digest
|
||||
import android.hardware.security.keymint.KeyPurpose
|
||||
import android.hardware.security.keymint.PaddingMode
|
||||
import android.os.RemoteException
|
||||
import android.os.ServiceSpecificException
|
||||
import android.system.keystore2.IKeystoreOperation
|
||||
import java.security.KeyPair
|
||||
import java.security.Signature
|
||||
@@ -17,10 +18,9 @@ import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
// A sealed interface to represent the different cryptographic operations we can perform.
|
||||
private sealed interface CryptoPrimitive {
|
||||
fun updateAad(aadInput: ByteArray?) {}
|
||||
fun update(data: ByteArray?): ByteArray?
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray?
|
||||
|
||||
fun abort()
|
||||
}
|
||||
|
||||
@@ -34,16 +34,17 @@ private object JcaAlgorithmMapper {
|
||||
Digest.SHA_2_512 -> "SHA512"
|
||||
else -> "NONE"
|
||||
}
|
||||
val keyAlgo =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "ECDSA"
|
||||
Algorithm.RSA -> "RSA"
|
||||
return when (params.algorithm) {
|
||||
Algorithm.EC -> "${digest}withECDSA"
|
||||
Algorithm.RSA -> {
|
||||
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
|
||||
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
|
||||
}
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Unsupported signature algorithm: ${params.algorithm}"
|
||||
)
|
||||
}
|
||||
return "${digest}with${keyAlgo}"
|
||||
}
|
||||
|
||||
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
|
||||
@@ -60,16 +61,18 @@ private object JcaAlgorithmMapper {
|
||||
when (params.blockMode.firstOrNull()) {
|
||||
BlockMode.ECB -> "ECB"
|
||||
BlockMode.CBC -> "CBC"
|
||||
BlockMode.CTR -> "CTR"
|
||||
BlockMode.GCM -> "GCM"
|
||||
else -> "ECB" // Default for RSA
|
||||
else -> "ECB"
|
||||
}
|
||||
val padding =
|
||||
when (params.padding.firstOrNull()) {
|
||||
PaddingMode.NONE -> "NoPadding"
|
||||
PaddingMode.PKCS7 -> "PKCS7Padding"
|
||||
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
|
||||
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
|
||||
PaddingMode.RSA_OAEP -> "OAEPPadding"
|
||||
else -> "NoPadding" // Default for GCM
|
||||
else -> "NoPadding"
|
||||
}
|
||||
return "$keyAlgo/$blockMode/$padding"
|
||||
}
|
||||
@@ -142,17 +145,17 @@ private class CipherPrimitive(
|
||||
override fun abort() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* A software-only implementation of a cryptographic operation. This class acts as a controller,
|
||||
* delegating to a specific cryptographic primitive based on the operation's purpose.
|
||||
*/
|
||||
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
|
||||
// This now holds the specific strategy object (Signer, Verifier, etc.)
|
||||
class SoftwareOperation(
|
||||
private val txId: Long,
|
||||
keyPair: KeyPair,
|
||||
params: KeyMintAttestation,
|
||||
private val latencyFloorMs: Long = 0L,
|
||||
) {
|
||||
private val primitive: CryptoPrimitive
|
||||
@Volatile var finalized = false
|
||||
private set
|
||||
|
||||
init {
|
||||
// The "Strategy" pattern: choose the implementation based on the purpose.
|
||||
// For simplicity, we only consider the first purpose listed.
|
||||
val purpose = params.purpose.firstOrNull()
|
||||
val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN"
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.")
|
||||
@@ -168,9 +171,35 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkActive() {
|
||||
if (finalized) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkInputLength(data: ByteArray?) {
|
||||
if (data != null && data.size > MAX_RECEIVE_DATA) {
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
|
||||
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAad(aadInput: ByteArray?) {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(aadInput)
|
||||
primitive.updateAad(aadInput)
|
||||
}
|
||||
|
||||
fun update(data: ByteArray?): ByteArray? {
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
return primitive.update(data)
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
|
||||
throw e
|
||||
@@ -178,38 +207,72 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
|
||||
}
|
||||
|
||||
fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
checkActive()
|
||||
checkInputLength(data)
|
||||
try {
|
||||
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
|
||||
val result = primitive.finish(data, signature)
|
||||
if (latencyFloorMs > 0) {
|
||||
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
|
||||
val delayMs = latencyFloorMs - elapsedMs
|
||||
if (delayMs > 0) Thread.sleep(delayMs)
|
||||
}
|
||||
finalized = true
|
||||
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
|
||||
return result
|
||||
} catch (e: ServiceSpecificException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
|
||||
// Re-throw the exception so the binder can report it to the client.
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun abort() {
|
||||
finalized = true
|
||||
primitive.abort()
|
||||
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
// AOSP keystore2 operation.rs: const MAX_RECEIVE_DATA: usize = 0x8000
|
||||
private const val MAX_RECEIVE_DATA = 0x8000
|
||||
}
|
||||
}
|
||||
|
||||
private object KeystoreErrorCodes {
|
||||
val tooMuchData: Int by lazy {
|
||||
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
|
||||
}
|
||||
|
||||
val invalidOperationHandle: Int by lazy {
|
||||
resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28)
|
||||
}
|
||||
|
||||
private fun resolveField(className: String, fieldName: String, fallback: Int): Int =
|
||||
runCatching {
|
||||
Class.forName(className).getField(fieldName).getInt(null)
|
||||
}.getOrElse {
|
||||
SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback")
|
||||
fallback
|
||||
}
|
||||
}
|
||||
|
||||
/** The Binder interface for our [SoftwareOperation]. */
|
||||
class SoftwareOperationBinder(private val operation: SoftwareOperation) :
|
||||
IKeystoreOperation.Stub() {
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun updateAad(aadInput: ByteArray?) {
|
||||
operation.updateAad(aadInput)
|
||||
}
|
||||
|
||||
override fun update(input: ByteArray?): ByteArray? {
|
||||
return operation.update(input)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? {
|
||||
return operation.finish(input, signature)
|
||||
}
|
||||
|
||||
@Throws(RemoteException::class)
|
||||
override fun abort() {
|
||||
operation.abort()
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ object SystemLogger {
|
||||
* @param message The message to log.
|
||||
*/
|
||||
fun debug(message: String) {
|
||||
if (!isDebugBuild) return
|
||||
Log.d(TAG, message)
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ object CertificateGenerator {
|
||||
uid: Int,
|
||||
securityLevel: Int,
|
||||
): Certificate {
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key")
|
||||
val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
|
||||
val leafNotAfter =
|
||||
(signingKeyPair.public as? X509Certificate)?.notAfter
|
||||
?: Date(System.currentTimeMillis() + 31536000000L)
|
||||
@@ -239,10 +239,10 @@ object CertificateGenerator {
|
||||
)
|
||||
|
||||
val signerAlgorithm =
|
||||
when (params.algorithm) {
|
||||
Algorithm.EC -> "SHA256withECDSA"
|
||||
Algorithm.RSA -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}")
|
||||
when (signingKeyPair.private.algorithm) {
|
||||
"EC", "ECDSA" -> "SHA256withECDSA"
|
||||
"RSA" -> "SHA256withRSA"
|
||||
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
|
||||
}
|
||||
val contentSigner =
|
||||
JcaContentSignerBuilder(signerAlgorithm)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.matrix.TEESimulator.pki
|
||||
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
data class CertGenConfig(
|
||||
val algorithm: Int,
|
||||
val keySize: Int,
|
||||
val ecCurve: Int,
|
||||
val rsaPublicExponent: Long,
|
||||
val attestationChallenge: ByteArray?,
|
||||
val purposes: IntArray,
|
||||
val digests: IntArray,
|
||||
val certSerial: ByteArray?,
|
||||
val certSubject: ByteArray?,
|
||||
val certNotBefore: Long,
|
||||
val certNotAfter: Long,
|
||||
val keyboxPrivateKey: ByteArray,
|
||||
val keyboxCertChain: ByteArray,
|
||||
val securityLevel: Int,
|
||||
val attestVersion: Int,
|
||||
val keymasterVersion: Int,
|
||||
val osVersion: Int,
|
||||
val osPatchLevel: Int,
|
||||
val vendorPatchLevel: Int,
|
||||
val bootPatchLevel: Int,
|
||||
val bootKey: ByteArray,
|
||||
val bootHash: ByteArray,
|
||||
val creationDatetime: Long,
|
||||
val attestationApplicationId: ByteArray,
|
||||
val moduleHash: ByteArray?,
|
||||
val idBrand: ByteArray?,
|
||||
val idDevice: ByteArray?,
|
||||
val idProduct: ByteArray?,
|
||||
val idSerial: ByteArray?,
|
||||
val idImei: ByteArray?,
|
||||
val idMeid: ByteArray?,
|
||||
val idManufacturer: ByteArray?,
|
||||
val idModel: ByteArray?,
|
||||
val idSecondImei: ByteArray?,
|
||||
)
|
||||
|
||||
object NativeCertGen {
|
||||
|
||||
private const val LOG_DIR = "/data/adb/tricky_store/logs"
|
||||
|
||||
@Volatile
|
||||
var isAvailable: Boolean = false
|
||||
private set
|
||||
|
||||
fun initialize(libraryPath: String) {
|
||||
try {
|
||||
System.load(libraryPath)
|
||||
initLogging(false, LOG_DIR)
|
||||
isAvailable = true
|
||||
SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully")
|
||||
} catch (e: UnsatisfiedLinkError) {
|
||||
SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e)
|
||||
}
|
||||
}
|
||||
|
||||
external fun generateAttestedKeyPair(config: CertGenConfig): ByteArray?
|
||||
|
||||
private external fun initLogging(verbose: Boolean, logDir: String): Boolean
|
||||
|
||||
private external fun dumpLogs(): String?
|
||||
|
||||
fun dump(): String? = if (isAvailable) dumpLogs() else null
|
||||
|
||||
fun parseNativeResult(bytes: ByteArray): Pair<KeyPair, List<Certificate>> {
|
||||
val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN)
|
||||
|
||||
val pkLen = buf.getInt()
|
||||
if (pkLen < 0 || pkLen > buf.remaining()) {
|
||||
throw IllegalStateException("Invalid private key length: $pkLen")
|
||||
}
|
||||
val pkBytes = ByteArray(pkLen)
|
||||
buf.get(pkBytes)
|
||||
|
||||
val numCerts = buf.getInt()
|
||||
if (numCerts < 0 || numCerts > buf.remaining()) {
|
||||
throw IllegalStateException("Invalid cert count: $numCerts")
|
||||
}
|
||||
val certs = mutableListOf<Certificate>()
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
repeat(numCerts) {
|
||||
val certLen = buf.getInt()
|
||||
if (certLen < 0 || certLen > buf.remaining()) {
|
||||
throw IllegalStateException("Invalid cert length: $certLen")
|
||||
}
|
||||
val certBytes = ByteArray(certLen)
|
||||
buf.get(certBytes)
|
||||
certs.add(certFactory.generateCertificate(ByteArrayInputStream(certBytes)))
|
||||
}
|
||||
|
||||
if (certs.isEmpty()) {
|
||||
throw IllegalStateException("No certificates in native result")
|
||||
}
|
||||
|
||||
val algorithmName = when (certs[0].publicKey.algorithm) {
|
||||
"EC", "ECDSA" -> "EC"
|
||||
"RSA" -> "RSA"
|
||||
else -> certs[0].publicKey.algorithm
|
||||
}
|
||||
val keyFactory = KeyFactory.getInstance(algorithmName)
|
||||
val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes))
|
||||
val publicKey = certs[0].publicKey
|
||||
return Pair(KeyPair(publicKey, privateKey), certs)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.security.keymint.SecurityLevel
|
||||
import android.os.Build
|
||||
import android.os.SystemProperties
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.time.LocalDate
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
@@ -11,7 +13,6 @@ import org.bouncycastle.asn1.ASN1EncodableVector
|
||||
import org.bouncycastle.asn1.ASN1Integer
|
||||
import org.bouncycastle.asn1.DEROctetString
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import org.bouncycastle.asn1.DERSet
|
||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
@@ -90,27 +91,33 @@ object AndroidDeviceUtils {
|
||||
attestationValueProvider: () -> ByteArray?,
|
||||
expectedSize: Int,
|
||||
): ByteArray {
|
||||
// 1. Attempt to get the value from the system property.
|
||||
getProperty(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
|
||||
persistToFile(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
// 2. Fallback to the value from a cached TEE attestation.
|
||||
try {
|
||||
attestationValueProvider()?.let {
|
||||
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
|
||||
setProperty(propertyName, it) // Persist for consistency
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
return it
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to get $propertyName from attestation.", e)
|
||||
}
|
||||
|
||||
// 3. As a final fallback, generate a random value.
|
||||
readFromFile(propertyName, expectedSize)?.let {
|
||||
SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
return it
|
||||
}
|
||||
|
||||
return generateRandomBytes(expectedSize).also {
|
||||
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
|
||||
setProperty(propertyName, it)
|
||||
persistToFile(propertyName, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,10 +164,37 @@ object AndroidDeviceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a cryptographically random byte array of a specified length. */
|
||||
private fun generateRandomBytes(size: Int): ByteArray =
|
||||
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) }
|
||||
|
||||
private val PERSIST_DIR = File("/data/adb/tricky_store")
|
||||
|
||||
private fun fileForProperty(propertyName: String): File = when (propertyName) {
|
||||
"ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin")
|
||||
"ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin")
|
||||
else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin")
|
||||
}
|
||||
|
||||
private fun persistToFile(propertyName: String, bytes: ByteArray) {
|
||||
try {
|
||||
fileForProperty(propertyName).writeBytes(bytes)
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to persist $propertyName to file.", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? {
|
||||
return try {
|
||||
val file = fileForProperty(propertyName)
|
||||
if (!file.exists()) return null
|
||||
val bytes = file.readBytes()
|
||||
if (bytes.size == expectedSize) bytes else null
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Failed to read $propertyName from file.", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// --- Patch Level Properties ---
|
||||
|
||||
fun getPatchLevel(uid: Int): Int {
|
||||
@@ -239,11 +273,12 @@ object AndroidDeviceUtils {
|
||||
val resolvedValue = resolveDateKeywords(value)
|
||||
|
||||
return when {
|
||||
// "device_default" indicates falling back to the system property.
|
||||
resolvedValue.equals("device_default", ignoreCase = true) -> null
|
||||
// "no" indicates this value should not be reported.
|
||||
// Resolve from live system prop — matches what detectors see via getprop,
|
||||
// even when PIF has spoofed ro.build.version.security_patch via resetprop
|
||||
resolvedValue.equals("prop", ignoreCase = true) ->
|
||||
parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong)
|
||||
resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
|
||||
// Otherwise, parse the resolved date string.
|
||||
else -> parsePatchLevelValue(resolvedValue, isLong)
|
||||
}
|
||||
}
|
||||
@@ -370,52 +405,174 @@ object AndroidDeviceUtils {
|
||||
|
||||
// --- APEX and Module Hash Properties ---
|
||||
|
||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
||||
runCatching {
|
||||
val pm = ConfigurationManager.getPackageManager()
|
||||
val packages =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0)
|
||||
// Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
|
||||
private class MinimalApexManifestParser(private val data: ByteArray) {
|
||||
var pos = 0
|
||||
|
||||
fun parse(): Pair<String, Long>? {
|
||||
var name: String? = null
|
||||
var version: Long? = null
|
||||
|
||||
while (pos < data.size) {
|
||||
val tag = readVarint()
|
||||
val fieldNum = tag ushr 3
|
||||
val wireType = (tag and 0x07).toInt()
|
||||
|
||||
when (fieldNum) {
|
||||
1L -> {
|
||||
val length = readVarint().toInt()
|
||||
if (pos + length > data.size) return null
|
||||
name = String(data, pos, length, Charsets.UTF_8)
|
||||
pos += length
|
||||
}
|
||||
2L -> {
|
||||
version = readVarint()
|
||||
}
|
||||
else -> skipField(wireType)
|
||||
}
|
||||
}
|
||||
|
||||
return if (name != null && version != null) {
|
||||
name to version
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
|
||||
null
|
||||
}
|
||||
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("Failed to get APEX package information.", it)
|
||||
emptyList()
|
||||
|
||||
private fun readVarint(): Long {
|
||||
var value = 0L
|
||||
var shift = 0
|
||||
while (pos < data.size) {
|
||||
val b = data[pos++].toInt()
|
||||
value = value or ((b and 0x7F).toLong() shl shift)
|
||||
if ((b and 0x80) == 0) return value
|
||||
shift += 7
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private fun skipField(wireType: Int) {
|
||||
when (wireType) {
|
||||
0 -> readVarint()
|
||||
1 -> pos += 8
|
||||
2 -> {
|
||||
val len = readVarint().toInt()
|
||||
pos += len
|
||||
}
|
||||
5 -> pos += 4
|
||||
else -> throw IllegalStateException("Unknown wire type $wireType")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val apexInfos: List<Pair<String, Long>> by lazy {
|
||||
val results = mutableListOf<Pair<String, Long>>()
|
||||
val apexRoot = File("/apex")
|
||||
|
||||
if (!apexRoot.exists() || !apexRoot.isDirectory) {
|
||||
return@lazy emptyList()
|
||||
}
|
||||
|
||||
apexRoot.listFiles()?.forEach { file ->
|
||||
if (!file.isDirectory) return@forEach
|
||||
val name = file.name
|
||||
|
||||
if (name.startsWith(".")) return@forEach
|
||||
if (name.contains("@")) return@forEach
|
||||
if (name == "sharedlibs") return@forEach
|
||||
|
||||
val manifestFile = File(file, "apex_manifest.pb")
|
||||
if (manifestFile.exists()) {
|
||||
runCatching {
|
||||
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
|
||||
val parser = MinimalApexManifestParser(bytes)
|
||||
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.distinctBy { it.first }
|
||||
}
|
||||
|
||||
val moduleHash: ByteArray by lazy {
|
||||
DeviceAttestationService.CachedAttestationData?.moduleHash
|
||||
?: runCatching {
|
||||
// TODO: figure out the correct calculation
|
||||
val moduleSequences = ASN1EncodableVector()
|
||||
data class ModuleEntry(
|
||||
val nameEncoded: ByteArray,
|
||||
val fullEncoded: ByteArray,
|
||||
)
|
||||
|
||||
// 1. Create a DERSequence for each module.
|
||||
apexInfos.forEach { (packageName, versionCode) ->
|
||||
val moduleVector = ASN1EncodableVector()
|
||||
// Use explicit UTF-8 encoding for the package name.
|
||||
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
|
||||
moduleVector.add(ASN1Integer(versionCode))
|
||||
moduleSequences.add(DERSequence(moduleVector))
|
||||
val modules =
|
||||
apexInfos.map { (packageName, versionCode) ->
|
||||
val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
|
||||
val versionInt = ASN1Integer(versionCode)
|
||||
|
||||
val vec = ASN1EncodableVector()
|
||||
vec.add(nameOctet)
|
||||
vec.add(versionInt)
|
||||
val sequence = DERSequence(vec)
|
||||
|
||||
// AOSP sorts by encoded name only, not full sequence
|
||||
ModuleEntry(
|
||||
nameEncoded = nameOctet.encoded,
|
||||
fullEncoded = sequence.encoded,
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Create a DERSet. Bouncy Castle will automatically handle
|
||||
// the sorting based on the DER-encoded value of each sequence.
|
||||
val modulesSet = DERSet(moduleSequences)
|
||||
val sortedModules =
|
||||
modules.sortedWith { m1, m2 ->
|
||||
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
|
||||
}
|
||||
|
||||
// 3. Get the final DER-encoded byte array of the SET.
|
||||
val encodedModules = modulesSet.encoded
|
||||
val payloadStream = ByteArrayOutputStream()
|
||||
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
|
||||
val payload = payloadStream.toByteArray()
|
||||
|
||||
// 4. Compute the SHA-256 hash.
|
||||
MessageDigest.getInstance("SHA-256").digest(encodedModules)
|
||||
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
|
||||
val finalDerSet = encodeAsDerSet(payload)
|
||||
|
||||
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
|
||||
}
|
||||
.getOrElse {
|
||||
SystemLogger.error("Failed to compute module hash.", it)
|
||||
ByteArray(32) // Return empty hash on failure
|
||||
ByteArray(32)
|
||||
}
|
||||
}
|
||||
|
||||
private fun compareByteArrays(a: ByteArray, b: ByteArray): Int {
|
||||
val length = minOf(a.size, b.size)
|
||||
for (i in 0 until length) {
|
||||
val byteA = a[i].toInt() and 0xFF
|
||||
val byteB = b[i].toInt() and 0xFF
|
||||
if (byteA != byteB) {
|
||||
return byteA - byteB
|
||||
}
|
||||
}
|
||||
return a.size - b.size
|
||||
}
|
||||
|
||||
private fun encodeAsDerSet(payload: ByteArray): ByteArray {
|
||||
val out = ByteArrayOutputStream()
|
||||
out.write(0x31)
|
||||
writeDerLength(out, payload.size)
|
||||
out.write(payload)
|
||||
return out.toByteArray()
|
||||
}
|
||||
|
||||
private fun writeDerLength(out: ByteArrayOutputStream, length: Int) {
|
||||
if (length < 128) {
|
||||
out.write(length)
|
||||
} else {
|
||||
var size = length
|
||||
val bytes = ArrayList<Byte>()
|
||||
while (size > 0) {
|
||||
bytes.add((size and 0xFF).toByte())
|
||||
size = size ushr 8
|
||||
}
|
||||
out.write(0x80 or bytes.size)
|
||||
for (i in bytes.indices.reversed()) {
|
||||
out.write(bytes[i].toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.matrix.TEESimulator.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import org.matrix.TEESimulator.logging.SystemLogger
|
||||
|
||||
object AndroidPermissionUtils {
|
||||
|
||||
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
|
||||
private fun getGlobalContext(): Context? {
|
||||
return try {
|
||||
// 1. Get the hidden ActivityThread class via reflection
|
||||
val activityThreadClass = Class.forName("android.app.ActivityThread")
|
||||
|
||||
// 2. Invoke the static currentActivityThread() method
|
||||
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
|
||||
currentActivityThreadMethod.isAccessible = true
|
||||
val activityThread = currentActivityThreadMethod.invoke(null)
|
||||
|
||||
if (activityThread == null) {
|
||||
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
|
||||
return null
|
||||
}
|
||||
|
||||
// 3. Try to get the application context
|
||||
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
|
||||
getApplicationMethod.isAccessible = true
|
||||
val application = getApplicationMethod.invoke(activityThread) as? Context
|
||||
|
||||
if (application != null) return application
|
||||
|
||||
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
|
||||
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
|
||||
getSystemContextMethod.isAccessible = true
|
||||
getSystemContextMethod.invoke(activityThread) as? Context
|
||||
|
||||
} catch (e: Exception) {
|
||||
SystemLogger.error("Reflection failed to get global context for permission check", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Core permission check.
|
||||
*/
|
||||
fun hasPermission(uid: Int, permission: String): Boolean {
|
||||
val context = getGlobalContext() ?: run {
|
||||
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
|
||||
return false
|
||||
}
|
||||
|
||||
val result = context.checkPermission(permission, -1, uid)
|
||||
return result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
fun hasDeviceAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
|
||||
}
|
||||
|
||||
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
|
||||
}
|
||||
|
||||
fun hasManageUsersPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.MANAGE_USERS")
|
||||
}
|
||||
|
||||
fun hasDumpPermission(uid: Int): Boolean {
|
||||
return hasPermission(uid, "android.permission.DUMP")
|
||||
}
|
||||
}
|
||||
@@ -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,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
|
||||
+116
-17
@@ -1,26 +1,125 @@
|
||||
## 🎉 TEESimulator v3.1: Legacy Support & Resilience
|
||||
## TEESimulator-RS v4.7: Operation & Attestation Fixes
|
||||
|
||||
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**).
|
||||
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).
|
||||
|
||||
### 🛡️ 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:
|
||||
- **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.
|
||||
|
||||
* **✅ 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`.
|
||||
---
|
||||
|
||||
### 🔧 Compatibility & Stability
|
||||
We’ve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
|
||||
## TEESimulator-RS v4.6: Rebrand & Detection Fix
|
||||
|
||||
* **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`.
|
||||
- **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.
|
||||
|
||||
### 🚀 The Road Ahead
|
||||
---
|
||||
|
||||
We are aware of the remaining detection vectors (see the issues list) and have clear solutions mapped out for the next release.
|
||||
## TEESimulator v4.5: Detection Hardening
|
||||
|
||||
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**.
|
||||
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) (23-check attestation validator). All keystore-level checks now pass.
|
||||
|
||||
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.
|
||||
- **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).
|
||||
|
||||
+6
-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
|
||||
|
||||
+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,644 @@
|
||||
use crate::error::Result;
|
||||
use crate::types::CertGenParams;
|
||||
|
||||
const DO_NOT_REPORT: i32 = -1;
|
||||
|
||||
pub fn build_attestation_extension(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let sw = build_software_enforced(params)?;
|
||||
let tee = build_tee_enforced(params)?;
|
||||
|
||||
let mut inner = Vec::new();
|
||||
// attestationVersion — INTEGER
|
||||
inner.extend_from_slice(&enc_integer(params.attest_version as i64));
|
||||
// attestationSecurityLevel — ENUMERATED, not INTEGER
|
||||
inner.extend_from_slice(&enc_enumerated(params.security_level));
|
||||
// keymintVersion — INTEGER
|
||||
inner.extend_from_slice(&enc_integer(params.keymaster_version as i64));
|
||||
// keymintSecurityLevel — ENUMERATED, not INTEGER
|
||||
inner.extend_from_slice(&enc_enumerated(params.security_level));
|
||||
// attestationChallenge — OCTET STRING
|
||||
inner.extend_from_slice(&enc_octet_string(
|
||||
params.attestation_challenge.as_deref().unwrap_or(&[]),
|
||||
));
|
||||
// uniqueId — OCTET STRING (always empty)
|
||||
inner.extend_from_slice(&enc_octet_string(&[]));
|
||||
// softwareEnforced
|
||||
inner.extend_from_slice(&sw);
|
||||
// teeEnforced
|
||||
inner.extend_from_slice(&tee);
|
||||
|
||||
Ok(enc_sequence(&inner))
|
||||
}
|
||||
|
||||
fn build_software_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
|
||||
// Tag 701: CREATION_DATETIME — INTEGER (milliseconds)
|
||||
fields.push((701, enc_integer(params.creation_datetime)));
|
||||
|
||||
// Tag 709: ATTESTATION_APPLICATION_ID — OCTET STRING
|
||||
// The bytes are already the DER-encoded AttestationApplicationId wrapped in OCTET STRING
|
||||
// by the Kotlin layer. We wrap them in an EXPLICIT tag.
|
||||
if !params.attestation_application_id.is_empty() {
|
||||
fields.push((709, enc_octet_string(¶ms.attestation_application_id)));
|
||||
}
|
||||
|
||||
// Tag 724: MODULE_HASH — OCTET STRING (only if attestVersion >= 400)
|
||||
if params.attest_version >= 400 {
|
||||
if let Some(ref hash) = params.module_hash {
|
||||
fields.push((724, enc_octet_string(hash)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(build_authorization_list(&mut fields))
|
||||
}
|
||||
|
||||
fn build_tee_enforced(params: &CertGenParams) -> Result<Vec<u8>> {
|
||||
let mut fields: Vec<(u32, Vec<u8>)> = Vec::new();
|
||||
|
||||
// Tag 1: PURPOSE — SET OF INTEGER
|
||||
if !params.purposes.is_empty() {
|
||||
fields.push((1, build_set_of_integer(¶ms.purposes)));
|
||||
}
|
||||
|
||||
// Tag 2: ALGORITHM — INTEGER
|
||||
fields.push((2, enc_integer(params.algorithm as i32 as i64)));
|
||||
|
||||
// Tag 3: KEY_SIZE — INTEGER
|
||||
fields.push((3, enc_integer(params.key_size as i64)));
|
||||
|
||||
// Tag 5: DIGEST — SET OF INTEGER
|
||||
if !params.digests.is_empty() {
|
||||
fields.push((5, build_set_of_integer(¶ms.digests)));
|
||||
}
|
||||
|
||||
// Tag 10: EC_CURVE — INTEGER (only for EC keys)
|
||||
if let Some(curve) = params.ec_curve {
|
||||
fields.push((10, enc_integer(curve as i32 as i64)));
|
||||
}
|
||||
|
||||
// Tag 503: NO_AUTH_REQUIRED — NULL (presence = true)
|
||||
fields.push((503, enc_null()));
|
||||
|
||||
// Tag 702: ORIGIN — INTEGER 0 (GENERATED)
|
||||
fields.push((702, enc_integer(0)));
|
||||
|
||||
// Tag 704: ROOT_OF_TRUST — SEQUENCE
|
||||
fields.push((704, build_root_of_trust(params)));
|
||||
|
||||
// Tag 705: OS_VERSION — INTEGER
|
||||
if params.os_version != DO_NOT_REPORT {
|
||||
fields.push((705, enc_integer(params.os_version as i64)));
|
||||
}
|
||||
|
||||
// Tag 706: OS_PATCHLEVEL — INTEGER
|
||||
if params.os_patch_level != DO_NOT_REPORT {
|
||||
fields.push((706, enc_integer(params.os_patch_level as i64)));
|
||||
}
|
||||
|
||||
// Tags 710-717: ATTESTATION_ID_* — OCTET STRING (optional)
|
||||
if let Some(ref v) = params.id_brand {
|
||||
fields.push((710, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_device {
|
||||
fields.push((711, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_product {
|
||||
fields.push((712, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_serial {
|
||||
fields.push((713, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_imei {
|
||||
fields.push((714, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_meid {
|
||||
fields.push((715, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_manufacturer {
|
||||
fields.push((716, enc_octet_string(v)));
|
||||
}
|
||||
if let Some(ref v) = params.id_model {
|
||||
fields.push((717, enc_octet_string(v)));
|
||||
}
|
||||
|
||||
// Tag 718: VENDOR_PATCHLEVEL — INTEGER
|
||||
if params.vendor_patch_level != DO_NOT_REPORT {
|
||||
fields.push((718, enc_integer(params.vendor_patch_level as i64)));
|
||||
}
|
||||
|
||||
// Tag 719: BOOT_PATCHLEVEL — INTEGER
|
||||
if params.boot_patch_level != DO_NOT_REPORT {
|
||||
fields.push((719, enc_integer(params.boot_patch_level as i64)));
|
||||
}
|
||||
|
||||
// Tag 723: ATTESTATION_ID_SECOND_IMEI — OCTET STRING (only if attestVersion >= 300)
|
||||
if params.attest_version >= 300 {
|
||||
if let Some(ref v) = params.id_second_imei {
|
||||
fields.push((723, enc_octet_string(v)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(build_authorization_list(&mut fields))
|
||||
}
|
||||
|
||||
fn build_root_of_trust(params: &CertGenParams) -> Vec<u8> {
|
||||
let mut inner = Vec::new();
|
||||
// verifiedBootKey — OCTET STRING (32 bytes)
|
||||
inner.extend_from_slice(&enc_octet_string(¶ms.boot_key));
|
||||
// deviceLocked — BOOLEAN TRUE (0xFF, not 0x01)
|
||||
inner.extend_from_slice(&enc_boolean(true));
|
||||
// verifiedBootState — ENUMERATED 0 (Verified), not INTEGER
|
||||
inner.extend_from_slice(&enc_enumerated(0));
|
||||
// verifiedBootHash — OCTET STRING (32 bytes)
|
||||
inner.extend_from_slice(&enc_octet_string(¶ms.boot_hash));
|
||||
enc_sequence(&inner)
|
||||
}
|
||||
|
||||
fn build_authorization_list(fields: &mut Vec<(u32, Vec<u8>)>) -> Vec<u8> {
|
||||
fields.sort_by_key(|(tag, _)| *tag);
|
||||
let mut inner = Vec::new();
|
||||
for (tag, value) in fields.iter() {
|
||||
inner.extend_from_slice(&enc_explicit_tag(*tag, value));
|
||||
}
|
||||
enc_sequence(&inner)
|
||||
}
|
||||
|
||||
fn build_set_of_integer(values: &[i32]) -> Vec<u8> {
|
||||
// DER SET OF: elements sorted by encoded byte value
|
||||
let mut encoded: Vec<Vec<u8>> = values.iter().map(|v| enc_integer(*v as i64)).collect();
|
||||
encoded.sort();
|
||||
let mut inner = Vec::new();
|
||||
for e in &encoded {
|
||||
inner.extend_from_slice(e);
|
||||
}
|
||||
enc_set(&inner)
|
||||
}
|
||||
|
||||
// --- DER primitives ---
|
||||
|
||||
fn enc_length(len: usize) -> Vec<u8> {
|
||||
if len < 0x80 {
|
||||
vec![len as u8]
|
||||
} else if len <= 0xFF {
|
||||
vec![0x81, len as u8]
|
||||
} else if len <= 0xFFFF {
|
||||
vec![0x82, (len >> 8) as u8, len as u8]
|
||||
} else if len <= 0xFF_FFFF {
|
||||
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||
} else {
|
||||
vec![
|
||||
0x84,
|
||||
(len >> 24) as u8,
|
||||
(len >> 16) as u8,
|
||||
(len >> 8) as u8,
|
||||
len as u8,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
fn enc_integer(value: i64) -> Vec<u8> {
|
||||
// DER INTEGER: tag 0x02, minimal two's complement big-endian
|
||||
let bytes = integer_bytes(value);
|
||||
let mut out = vec![0x02];
|
||||
out.extend_from_slice(&enc_length(bytes.len()));
|
||||
out.extend_from_slice(&bytes);
|
||||
out
|
||||
}
|
||||
|
||||
fn integer_bytes(value: i64) -> Vec<u8> {
|
||||
if value == 0 {
|
||||
return vec![0x00];
|
||||
}
|
||||
let raw = value.to_be_bytes();
|
||||
// Find first significant byte
|
||||
let mut start = 0;
|
||||
if value > 0 {
|
||||
while start < 7 && raw[start] == 0x00 {
|
||||
start += 1;
|
||||
}
|
||||
// If high bit set, need leading 0x00 to keep positive
|
||||
if raw[start] & 0x80 != 0 {
|
||||
let mut out = vec![0x00];
|
||||
out.extend_from_slice(&raw[start..]);
|
||||
return out;
|
||||
}
|
||||
} else {
|
||||
while start < 7 && raw[start] == 0xFF {
|
||||
start += 1;
|
||||
}
|
||||
// If high bit clear, need leading 0xFF to keep negative
|
||||
if raw[start] & 0x80 == 0 {
|
||||
let mut out = vec![0xFF];
|
||||
out.extend_from_slice(&raw[start..]);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
raw[start..].to_vec()
|
||||
}
|
||||
|
||||
fn enc_enumerated(value: i32) -> Vec<u8> {
|
||||
// DER ENUMERATED: tag 0x0A, same value encoding as INTEGER
|
||||
let bytes = integer_bytes(value as i64);
|
||||
let mut out = vec![0x0A];
|
||||
out.extend_from_slice(&enc_length(bytes.len()));
|
||||
out.extend_from_slice(&bytes);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_octet_string(data: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![0x04];
|
||||
out.extend_from_slice(&enc_length(data.len()));
|
||||
out.extend_from_slice(data);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_null() -> Vec<u8> {
|
||||
vec![0x05, 0x00]
|
||||
}
|
||||
|
||||
fn enc_boolean(value: bool) -> Vec<u8> {
|
||||
// DER BOOLEAN: TRUE = 0xFF, FALSE = 0x00
|
||||
vec![0x01, 0x01, if value { 0xFF } else { 0x00 }]
|
||||
}
|
||||
|
||||
fn enc_sequence(contents: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![0x30];
|
||||
out.extend_from_slice(&enc_length(contents.len()));
|
||||
out.extend_from_slice(contents);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_set(contents: &[u8]) -> Vec<u8> {
|
||||
let mut out = vec![0x31];
|
||||
out.extend_from_slice(&enc_length(contents.len()));
|
||||
out.extend_from_slice(contents);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_explicit_tag(tag_number: u32, inner: &[u8]) -> Vec<u8> {
|
||||
// EXPLICIT context-specific constructed tag
|
||||
let mut out = Vec::new();
|
||||
if tag_number < 31 {
|
||||
// Short form: single byte 0xA0 | tag_number
|
||||
out.push(0xA0 | tag_number as u8);
|
||||
} else {
|
||||
// Long form: 0xBF followed by base-128 encoding of tag number
|
||||
out.push(0xBF);
|
||||
enc_base128_tag(&mut out, tag_number);
|
||||
}
|
||||
out.extend_from_slice(&enc_length(inner.len()));
|
||||
out.extend_from_slice(inner);
|
||||
out
|
||||
}
|
||||
|
||||
fn enc_base128_tag(out: &mut Vec<u8>, tag: u32) {
|
||||
// Base-128 with continuation bits: MSB first, bit 7 set on all but last byte
|
||||
let mut digits = Vec::new();
|
||||
let mut val = tag;
|
||||
digits.push((val & 0x7F) as u8);
|
||||
val >>= 7;
|
||||
while val > 0 {
|
||||
digits.push((val & 0x7F) as u8 | 0x80);
|
||||
val >>= 7;
|
||||
}
|
||||
// Written MSB first
|
||||
for b in digits.iter().rev() {
|
||||
out.push(*b);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{Algorithm, EcCurve};
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_zero() {
|
||||
assert_eq!(enc_integer(0), vec![0x02, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_small_positive() {
|
||||
assert_eq!(enc_integer(3), vec![0x02, 0x01, 0x03]);
|
||||
assert_eq!(enc_integer(127), vec![0x02, 0x01, 0x7F]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_needs_leading_zero() {
|
||||
// 128 = 0x80, high bit set so needs 0x00 prefix
|
||||
assert_eq!(enc_integer(128), vec![0x02, 0x02, 0x00, 0x80]);
|
||||
assert_eq!(enc_integer(256), vec![0x02, 0x02, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_multi_byte() {
|
||||
// 140000 = 0x02_22_E0
|
||||
assert_eq!(enc_integer(140000), vec![0x02, 0x03, 0x02, 0x22, 0xE0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_integer_large() {
|
||||
// 20250301 = 0x01_34_FE_BD
|
||||
assert_eq!(
|
||||
enc_integer(20250301),
|
||||
vec![0x02, 0x04, 0x01, 0x34, 0xFE, 0xBD]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_enumerated() {
|
||||
// SecurityLevel TEE = 1
|
||||
assert_eq!(enc_enumerated(1), vec![0x0A, 0x01, 0x01]);
|
||||
// VerifiedBootState Verified = 0
|
||||
assert_eq!(enc_enumerated(0), vec![0x0A, 0x01, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_boolean_true() {
|
||||
// DER: TRUE = 0xFF
|
||||
assert_eq!(enc_boolean(true), vec![0x01, 0x01, 0xFF]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_null() {
|
||||
assert_eq!(enc_null(), vec![0x05, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_octet_string_empty() {
|
||||
assert_eq!(enc_octet_string(&[]), vec![0x04, 0x00]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_short() {
|
||||
// Tag 1 wrapping INTEGER 2: A1 03 02 01 02
|
||||
let inner = enc_integer(2);
|
||||
let tagged = enc_explicit_tag(1, &inner);
|
||||
assert_eq!(tagged, vec![0xA1, 0x03, 0x02, 0x01, 0x02]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_10() {
|
||||
// Tag 10: 0xAA
|
||||
let inner = enc_integer(1);
|
||||
let tagged = enc_explicit_tag(10, &inner);
|
||||
assert_eq!(tagged[0], 0xAA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_503() {
|
||||
// Tag 503: 0xBF 0x83 0x77
|
||||
// 503 = 3*128 + 119 => 0x83 0x77
|
||||
let inner = enc_null();
|
||||
let tagged = enc_explicit_tag(503, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x83, 0x77]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_704() {
|
||||
// Tag 704: 0xBF 0x85 0x40
|
||||
// 704 = 5*128 + 64 => 0x85 0x40
|
||||
let inner = enc_sequence(&[]);
|
||||
let tagged = enc_explicit_tag(704, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x40]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_718() {
|
||||
// Tag 718: 0xBF 0x85 0x4E
|
||||
let inner = enc_integer(20250301);
|
||||
let tagged = enc_explicit_tag(718, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4E]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_719() {
|
||||
// Tag 719: 0xBF 0x85 0x4F
|
||||
let inner = enc_integer(20250301);
|
||||
let tagged = enc_explicit_tag(719, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x4F]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_701() {
|
||||
// Tag 701: 0xBF 0x85 0x3D
|
||||
let inner = enc_integer(1000);
|
||||
let tagged = enc_explicit_tag(701, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x3D]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enc_explicit_tag_709() {
|
||||
// Tag 709: 0xBF 0x85 0x45
|
||||
let inner = enc_octet_string(&[0x01]);
|
||||
let tagged = enc_explicit_tag(709, &inner);
|
||||
assert_eq!(&tagged[..3], &[0xBF, 0x85, 0x45]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_set_of_integer_sorted() {
|
||||
// SET OF INTEGER must sort by encoded bytes
|
||||
let result = build_set_of_integer(&[3, 2]);
|
||||
// Expect sorted: INTEGER 2 before INTEGER 3
|
||||
let expected = enc_set(&[0x02, 0x01, 0x02, 0x02, 0x01, 0x03]);
|
||||
assert_eq!(result, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_root_of_trust_structure() {
|
||||
let params = make_test_params();
|
||||
let rot = build_root_of_trust(¶ms);
|
||||
// Should be a SEQUENCE (0x30)
|
||||
assert_eq!(rot[0], 0x30);
|
||||
// Find BOOLEAN TRUE inside
|
||||
let rot_inner = &rot[2..]; // skip tag+length
|
||||
// First: OCTET STRING (32 bytes boot key)
|
||||
assert_eq!(rot_inner[0], 0x04);
|
||||
assert_eq!(rot_inner[1], 0x20); // 32 bytes
|
||||
// After boot key (34 bytes): BOOLEAN TRUE
|
||||
assert_eq!(rot_inner[34], 0x01); // BOOLEAN tag
|
||||
assert_eq!(rot_inner[35], 0x01); // length 1
|
||||
assert_eq!(rot_inner[36], 0xFF); // TRUE = 0xFF
|
||||
// Then ENUMERATED 0 (verifiedBootState)
|
||||
assert_eq!(rot_inner[37], 0x0A); // ENUMERATED tag, not 0x02
|
||||
assert_eq!(rot_inner[38], 0x01);
|
||||
assert_eq!(rot_inner[39], 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_do_not_report_omits_fields() {
|
||||
let mut params = make_test_params();
|
||||
params.os_patch_level = DO_NOT_REPORT;
|
||||
params.vendor_patch_level = DO_NOT_REPORT;
|
||||
params.boot_patch_level = DO_NOT_REPORT;
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let hex = hex_string(&tee);
|
||||
// Tags 706, 718, 719 should not appear
|
||||
// Tag 706 = BF 85 42, 718 = BF 85 4E, 719 = BF 85 4F
|
||||
assert!(!hex.contains("bf8542"), "os_patch_level should be omitted");
|
||||
assert!(
|
||||
!hex.contains("bf854e"),
|
||||
"vendor_patch_level should be omitted"
|
||||
);
|
||||
assert!(
|
||||
!hex.contains("bf854f"),
|
||||
"boot_patch_level should be omitted"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_description_security_level_is_enumerated() {
|
||||
let params = make_test_params();
|
||||
let ext = build_attestation_extension(¶ms).unwrap();
|
||||
// KeyDescription is a SEQUENCE: 0x30 ...
|
||||
assert_eq!(ext[0], 0x30);
|
||||
// Skip SEQUENCE tag + length to get to inner fields
|
||||
let inner = skip_tlv_header(&ext);
|
||||
// Field 0: attestationVersion — INTEGER (0x02)
|
||||
assert_eq!(inner[0], 0x02);
|
||||
let (_, rest) = skip_one_tlv(inner);
|
||||
// Field 1: attestationSecurityLevel — ENUMERATED (0x0A)
|
||||
assert_eq!(rest[0], 0x0A, "attestationSecurityLevel must be ENUMERATED");
|
||||
let (_, rest) = skip_one_tlv(rest);
|
||||
// Field 2: keymintVersion — INTEGER (0x02)
|
||||
assert_eq!(rest[0], 0x02);
|
||||
let (_, rest) = skip_one_tlv(rest);
|
||||
// Field 3: keymintSecurityLevel — ENUMERATED (0x0A)
|
||||
assert_eq!(rest[0], 0x0A, "keymintSecurityLevel must be ENUMERATED");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_authorization_list_sorted_by_tag() {
|
||||
let params = make_test_params();
|
||||
let tee = build_tee_enforced(¶ms).unwrap();
|
||||
let inner = skip_tlv_header(&tee);
|
||||
let tags = extract_tag_numbers(inner);
|
||||
let mut sorted = tags.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(tags, sorted, "AuthorizationList fields must be sorted by tag number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_extension_roundtrip() {
|
||||
let params = make_test_params();
|
||||
let ext = build_attestation_extension(¶ms).unwrap();
|
||||
// Must be valid DER: starts with SEQUENCE tag
|
||||
assert_eq!(ext[0], 0x30);
|
||||
// Length must account for all inner bytes
|
||||
let (header_len, total_content_len) = parse_tlv_lengths(&ext);
|
||||
assert_eq!(ext.len(), header_len + total_content_len);
|
||||
}
|
||||
|
||||
// --- test helpers ---
|
||||
|
||||
fn make_test_params() -> CertGenParams {
|
||||
CertGenParams {
|
||||
algorithm: Algorithm::Ec,
|
||||
key_size: 256,
|
||||
ec_curve: Some(EcCurve::P256),
|
||||
rsa_public_exponent: 0,
|
||||
attestation_challenge: Some(vec![0xAB; 32]),
|
||||
purposes: vec![2, 3],
|
||||
digests: vec![4],
|
||||
cert_serial: None,
|
||||
cert_subject: None,
|
||||
cert_not_before: -1,
|
||||
cert_not_after: -1,
|
||||
keybox_private_key: vec![],
|
||||
keybox_cert_chain: vec![],
|
||||
security_level: 1,
|
||||
attest_version: 200,
|
||||
keymaster_version: 200,
|
||||
os_version: 140000,
|
||||
os_patch_level: 202503,
|
||||
vendor_patch_level: 20250301,
|
||||
boot_patch_level: 20250301,
|
||||
boot_key: vec![0x01; 32],
|
||||
boot_hash: vec![0x02; 32],
|
||||
creation_datetime: 1709913600000,
|
||||
attestation_application_id: vec![0xDE, 0xAD],
|
||||
module_hash: None,
|
||||
id_brand: None,
|
||||
id_device: None,
|
||||
id_product: None,
|
||||
id_serial: None,
|
||||
id_imei: None,
|
||||
id_meid: None,
|
||||
id_manufacturer: None,
|
||||
id_model: None,
|
||||
id_second_imei: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hex_string(data: &[u8]) -> String {
|
||||
data.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
fn skip_tlv_header(data: &[u8]) -> &[u8] {
|
||||
let (header_len, _) = parse_tlv_lengths(data);
|
||||
&data[header_len..]
|
||||
}
|
||||
|
||||
fn skip_one_tlv(data: &[u8]) -> (usize, &[u8]) {
|
||||
let (header_len, content_len) = parse_tlv_lengths(data);
|
||||
let total = header_len + content_len;
|
||||
(total, &data[total..])
|
||||
}
|
||||
|
||||
fn parse_tlv_lengths(data: &[u8]) -> (usize, usize) {
|
||||
// Returns (header_bytes, content_bytes)
|
||||
let tag_len = tag_byte_len(data);
|
||||
let len_start = tag_len;
|
||||
if data[len_start] < 0x80 {
|
||||
(len_start + 1, data[len_start] as usize)
|
||||
} else {
|
||||
let num_len_bytes = (data[len_start] & 0x7F) as usize;
|
||||
let mut content_len = 0usize;
|
||||
for i in 0..num_len_bytes {
|
||||
content_len = (content_len << 8) | data[len_start + 1 + i] as usize;
|
||||
}
|
||||
(len_start + 1 + num_len_bytes, content_len)
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_byte_len(data: &[u8]) -> usize {
|
||||
if data[0] & 0x1F != 0x1F {
|
||||
1
|
||||
} else {
|
||||
let mut i = 1;
|
||||
while data[i] & 0x80 != 0 {
|
||||
i += 1;
|
||||
}
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_tag_numbers(mut data: &[u8]) -> Vec<u32> {
|
||||
let mut tags = Vec::new();
|
||||
while !data.is_empty() {
|
||||
let tag = read_tag_number(data);
|
||||
tags.push(tag);
|
||||
let (_, rest) = skip_one_tlv(data);
|
||||
data = rest;
|
||||
}
|
||||
tags
|
||||
}
|
||||
|
||||
fn read_tag_number(data: &[u8]) -> u32 {
|
||||
if data[0] & 0x1F != 0x1F {
|
||||
(data[0] & 0x1F) as u32
|
||||
} else {
|
||||
let mut val = 0u32;
|
||||
let mut i = 1;
|
||||
loop {
|
||||
val = (val << 7) | (data[i] & 0x7F) as u32;
|
||||
if data[i] & 0x80 == 0 {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
val
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
use crate::error::{CertGenError, Result};
|
||||
use crate::keybox::ParsedKeybox;
|
||||
use crate::types::{Algorithm, CertGenParams, GeneratedKeyPair};
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
const ATTESTATION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 11129, 2, 1, 17];
|
||||
|
||||
// Signature algorithm OIDs
|
||||
const OID_SHA256_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 2];
|
||||
const OID_SHA384_WITH_ECDSA: &[u64] = &[1, 2, 840, 10045, 4, 3, 3];
|
||||
const OID_SHA256_WITH_RSA: &[u64] = &[1, 2, 840, 113549, 1, 1, 11];
|
||||
|
||||
// Extension OIDs
|
||||
const OID_KEY_USAGE: &[u64] = &[2, 5, 29, 15];
|
||||
|
||||
pub fn build_certificate_chain(
|
||||
key_pair: &GeneratedKeyPair,
|
||||
attestation_ext_der: &[u8],
|
||||
keybox: &ParsedKeybox,
|
||||
params: &CertGenParams,
|
||||
) -> Result<Vec<Vec<u8>>> {
|
||||
let leaf_der = build_leaf_cert(key_pair, attestation_ext_der, keybox, params)?;
|
||||
|
||||
let mut chain = Vec::with_capacity(1 + keybox.cert_chain_ders.len());
|
||||
chain.push(leaf_der);
|
||||
for cert_der in &keybox.cert_chain_ders {
|
||||
chain.push(cert_der.clone());
|
||||
}
|
||||
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
fn build_leaf_cert(
|
||||
key_pair: &GeneratedKeyPair,
|
||||
attestation_ext_der: &[u8],
|
||||
keybox: &ParsedKeybox,
|
||||
params: &CertGenParams,
|
||||
) -> Result<Vec<u8>> {
|
||||
let spki_der = extract_spki_from_pkcs8(&key_pair.private_key_pkcs8)?;
|
||||
let sig_alg_der = signature_algorithm_for_signing_key(&keybox.signing_key_der, params.algorithm)?;
|
||||
|
||||
// Serial number
|
||||
let serial_bytes = if let Some(ref serial) = params.cert_serial {
|
||||
serial.clone()
|
||||
} else {
|
||||
vec![1u8]
|
||||
};
|
||||
|
||||
// Subject DN
|
||||
let subject_dn_der = if let Some(ref subject) = params.cert_subject {
|
||||
subject.clone()
|
||||
} else {
|
||||
encode_simple_cn_dn("Android Keystore Key")
|
||||
};
|
||||
|
||||
// Validity
|
||||
let not_before = timestamp_to_datetime(params.cert_not_before)?;
|
||||
let not_after = if params.cert_not_after == -1 {
|
||||
OffsetDateTime::from_unix_timestamp(keybox.leaf_not_after)
|
||||
.unwrap_or_else(|_| OffsetDateTime::now_utc() + time::Duration::days(365))
|
||||
} else {
|
||||
timestamp_to_datetime(params.cert_not_after)?
|
||||
};
|
||||
|
||||
// Extensions
|
||||
let extensions_der = build_extensions(attestation_ext_der, ¶ms.purposes)?;
|
||||
|
||||
// TBS Certificate
|
||||
let version_der = encode_der_explicit_tag(0, &encode_der_integer(&[2]));
|
||||
let serial_der = encode_der_integer(&serial_bytes);
|
||||
let validity_der = encode_validity(¬_before, ¬_after);
|
||||
let extensions_tagged = encode_der_explicit_tag(3, &extensions_der);
|
||||
|
||||
let tbs_der = encode_der_sequence(&[
|
||||
&version_der,
|
||||
&serial_der,
|
||||
&sig_alg_der,
|
||||
&keybox.issuer_dn_der, // RAW bytes — no re-encoding
|
||||
&validity_der,
|
||||
&subject_dn_der,
|
||||
&spki_der,
|
||||
&extensions_tagged,
|
||||
]);
|
||||
|
||||
// Sign the TBS
|
||||
let signature_bytes = sign_tbs(&tbs_der, &keybox.signing_key_der, params.algorithm)?;
|
||||
let signature_bit_string = encode_der_bit_string(&signature_bytes);
|
||||
|
||||
// Final certificate: SEQUENCE { TBS, sigAlgorithm, signature }
|
||||
let cert_der = encode_der_sequence(&[
|
||||
&tbs_der,
|
||||
&sig_alg_der,
|
||||
&signature_bit_string,
|
||||
]);
|
||||
|
||||
Ok(cert_der)
|
||||
}
|
||||
|
||||
fn sign_tbs(tbs_der: &[u8], signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
|
||||
match algorithm {
|
||||
Algorithm::Ec => sign_tbs_ec(tbs_der, signing_key_der),
|
||||
Algorithm::Rsa => sign_tbs_rsa(tbs_der, signing_key_der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_tbs_ec(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
|
||||
// Determine EC curve from the signing key's PKCS8 AlgorithmIdentifier
|
||||
let alg = detect_ec_signing_algorithm(signing_key_der)?;
|
||||
|
||||
let key_pair = ring::signature::EcdsaKeyPair::from_pkcs8(alg, signing_key_der, &ring::rand::SystemRandom::new())
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("EC key parse: {e}")))?;
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let sig = key_pair.sign(&rng, tbs_der)
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("EC sign: {e}")))?;
|
||||
|
||||
Ok(sig.as_ref().to_vec())
|
||||
}
|
||||
|
||||
fn detect_ec_signing_algorithm(pkcs8_der: &[u8]) -> Result<&'static ring::signature::EcdsaSigningAlgorithm> {
|
||||
use der::Decode;
|
||||
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("PKCS8 parse: {e}")))?;
|
||||
|
||||
let params_oid = info.algorithm.parameters_oid()
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("EC curve OID: {e}")))?;
|
||||
|
||||
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
|
||||
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
|
||||
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
|
||||
.map_err(|_| CertGenError::SigningFailed("OID parse".into()))?;
|
||||
|
||||
if params_oid == p256_oid {
|
||||
Ok(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING)
|
||||
} else if params_oid == p384_oid {
|
||||
Ok(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING)
|
||||
} else {
|
||||
Err(CertGenError::SigningFailed(format!("unsupported EC curve OID: {params_oid}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_tbs_rsa(tbs_der: &[u8], signing_key_der: &[u8]) -> Result<Vec<u8>> {
|
||||
use rsa::pkcs8::DecodePrivateKey;
|
||||
use rsa::signature::{SignatureEncoding, SignerMut};
|
||||
use rsa::pkcs1v15::SigningKey;
|
||||
use rsa::sha2::Sha256;
|
||||
|
||||
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(signing_key_der)
|
||||
.map_err(|e| CertGenError::SigningFailed(format!("RSA key parse: {e}")))?;
|
||||
|
||||
let mut signing_key = SigningKey::<Sha256>::new(private_key);
|
||||
let signature = signing_key.sign(tbs_der);
|
||||
|
||||
Ok(signature.to_vec())
|
||||
}
|
||||
|
||||
fn signature_algorithm_for_signing_key(signing_key_der: &[u8], algorithm: Algorithm) -> Result<Vec<u8>> {
|
||||
match algorithm {
|
||||
Algorithm::Ec => {
|
||||
let ring_alg = detect_ec_signing_algorithm(signing_key_der)?;
|
||||
// Determine OID from the algorithm used
|
||||
let oid = if std::ptr::eq(ring_alg, &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING) {
|
||||
OID_SHA384_WITH_ECDSA
|
||||
} else {
|
||||
OID_SHA256_WITH_ECDSA
|
||||
};
|
||||
let oid_der = encode_der_oid(oid);
|
||||
Ok(encode_der_sequence(&[&oid_der]))
|
||||
}
|
||||
Algorithm::Rsa => {
|
||||
let oid_der = encode_der_oid(OID_SHA256_WITH_RSA);
|
||||
let null_der = vec![0x05, 0x00];
|
||||
Ok(encode_der_sequence(&[&oid_der, &null_der]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_spki_from_pkcs8(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||
use der::Decode;
|
||||
|
||||
let info = pkcs8::PrivateKeyInfo::from_der(pkcs8_der)
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("PKCS8 parse for SPKI: {e}")))?;
|
||||
|
||||
// Reconstruct SPKI from AlgorithmIdentifier + public key
|
||||
// For EC: derive public key from private key via ring
|
||||
// For RSA: derive from rsa crate
|
||||
let alg_id_oid = info.algorithm.oid;
|
||||
let ec_oid: const_oid::ObjectIdentifier = "1.2.840.10045.2.1".parse()
|
||||
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||
|
||||
if alg_id_oid == ec_oid {
|
||||
extract_ec_spki(pkcs8_der, &info)
|
||||
} else {
|
||||
extract_rsa_spki(pkcs8_der)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_ec_spki(pkcs8_der: &[u8], info: &pkcs8::PrivateKeyInfo) -> Result<Vec<u8>> {
|
||||
use ring::signature::KeyPair as _;
|
||||
let params_oid = info.algorithm.parameters_oid()
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("EC curve OID: {e}")))?;
|
||||
|
||||
let p256_oid: const_oid::ObjectIdentifier = "1.2.840.10045.3.1.7".parse()
|
||||
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||
let p384_oid: const_oid::ObjectIdentifier = "1.3.132.0.34".parse()
|
||||
.map_err(|_| CertGenError::CertBuildFailed("OID parse".into()))?;
|
||||
|
||||
let (ring_alg, curve_oid_der): (&ring::signature::EcdsaSigningAlgorithm, Vec<u8>) = if params_oid == p256_oid {
|
||||
(&ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, encode_der_oid(&[1, 2, 840, 10045, 3, 1, 7]))
|
||||
} else if params_oid == p384_oid {
|
||||
(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, encode_der_oid(&[1, 3, 132, 0, 34]))
|
||||
} else {
|
||||
return Err(CertGenError::CertBuildFailed(format!("unsupported EC curve: {params_oid}")));
|
||||
};
|
||||
|
||||
let kp = ring::signature::EcdsaKeyPair::from_pkcs8(
|
||||
ring_alg,
|
||||
pkcs8_der,
|
||||
&ring::rand::SystemRandom::new(),
|
||||
).map_err(|e| CertGenError::CertBuildFailed(format!("EC key parse: {e}")))?;
|
||||
let ec_kp = kp.public_key().as_ref().to_vec();
|
||||
|
||||
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (public key) }
|
||||
// AlgorithmIdentifier = SEQUENCE { ecPublicKey OID, curve OID }
|
||||
let ec_oid_der = encode_der_oid(&[1, 2, 840, 10045, 2, 1]);
|
||||
let alg_id = encode_der_sequence(&[&ec_oid_der, &curve_oid_der]);
|
||||
let pub_key_bits = encode_der_bit_string(&ec_kp);
|
||||
|
||||
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||
}
|
||||
|
||||
fn extract_rsa_spki(pkcs8_der: &[u8]) -> Result<Vec<u8>> {
|
||||
use rsa::pkcs8::DecodePrivateKey;
|
||||
|
||||
let private_key = rsa::RsaPrivateKey::from_pkcs8_der(pkcs8_der)
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("RSA key parse: {e}")))?;
|
||||
|
||||
let public_key = rsa::RsaPublicKey::from(&private_key);
|
||||
|
||||
// Encode RSA public key as DER: SEQUENCE { n INTEGER, e INTEGER }
|
||||
use rsa::traits::PublicKeyParts;
|
||||
let n_bytes = public_key.n().to_bytes_be();
|
||||
let e_bytes = public_key.e().to_bytes_be();
|
||||
let rsa_pub_der = encode_der_sequence(&[
|
||||
&encode_der_integer(&n_bytes),
|
||||
&encode_der_integer(&e_bytes),
|
||||
]);
|
||||
|
||||
// SPKI = SEQUENCE { AlgorithmIdentifier, BIT STRING (DER-encoded RSAPublicKey) }
|
||||
let rsa_oid_der = encode_der_oid(&[1, 2, 840, 113549, 1, 1, 1]);
|
||||
let null_der = vec![0x05, 0x00];
|
||||
let alg_id = encode_der_sequence(&[&rsa_oid_der, &null_der]);
|
||||
let pub_key_bits = encode_der_bit_string(&rsa_pub_der);
|
||||
|
||||
Ok(encode_der_sequence(&[&alg_id, &pub_key_bits]))
|
||||
}
|
||||
|
||||
fn build_extensions(attestation_ext_der: &[u8], purposes: &[i32]) -> Result<Vec<u8>> {
|
||||
let mut extensions: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
// KeyUsage extension (critical)
|
||||
let ku_byte = map_key_usage_byte(purposes);
|
||||
if ku_byte != 0 {
|
||||
let ku_ext = build_key_usage_extension(ku_byte);
|
||||
extensions.push(ku_ext);
|
||||
}
|
||||
|
||||
// Attestation extension (non-critical)
|
||||
let attest_ext = build_extension(&encode_der_oid(ATTESTATION_OID), false, attestation_ext_der);
|
||||
extensions.push(attest_ext);
|
||||
|
||||
Ok(encode_der_sequence_of(&extensions))
|
||||
}
|
||||
|
||||
fn build_extension(oid_der: &[u8], critical: bool, value_der: &[u8]) -> Vec<u8> {
|
||||
let value_octet_string = encode_der_octet_string(value_der);
|
||||
if critical {
|
||||
let critical_der = encode_der_boolean(true);
|
||||
encode_der_sequence(&[oid_der, &critical_der, &value_octet_string])
|
||||
} else {
|
||||
encode_der_sequence(&[oid_der, &value_octet_string])
|
||||
}
|
||||
}
|
||||
|
||||
fn build_key_usage_extension(ku_byte: u8) -> Vec<u8> {
|
||||
// DER BIT STRING: minimal encoding requires trimming trailing zero bits
|
||||
let unused_bits = ku_byte.trailing_zeros().min(7) as u8;
|
||||
|
||||
// BIT STRING = tag (0x03) + length(2) + unused_bits + byte
|
||||
let bit_string = vec![0x03, 0x02, unused_bits, ku_byte];
|
||||
|
||||
let oid_der = encode_der_oid(OID_KEY_USAGE);
|
||||
let value_octet_string = encode_der_octet_string(&bit_string);
|
||||
let critical_der = encode_der_boolean(true);
|
||||
|
||||
encode_der_sequence(&[&oid_der, &critical_der, &value_octet_string])
|
||||
}
|
||||
|
||||
// KeyUsage BIT STRING byte layout (RFC 5280):
|
||||
// byte[0] bit 7 = digitalSignature (0x80)
|
||||
// byte[0] bit 6 = nonRepudiation (0x40)
|
||||
// byte[0] bit 5 = keyEncipherment (0x20)
|
||||
// byte[0] bit 4 = dataEncipherment (0x10)
|
||||
// byte[0] bit 3 = keyAgreement (0x08)
|
||||
// byte[0] bit 2 = keyCertSign (0x04)
|
||||
// byte[0] bit 1 = cRLSign (0x02)
|
||||
// byte[0] bit 0 = encipherOnly (0x01)
|
||||
// byte[1] bit 7 = decipherOnly (0x80)
|
||||
fn map_key_usage_byte(purposes: &[i32]) -> u8 {
|
||||
let mut bits: u8 = 0;
|
||||
for &purpose in purposes {
|
||||
match purpose {
|
||||
2 => bits |= 0x80, // SIGN -> digitalSignature
|
||||
1 => bits |= 0x10, // DECRYPT -> dataEncipherment
|
||||
5 => bits |= 0x20, // WRAP_KEY -> keyEncipherment
|
||||
6 => bits |= 0x08, // AGREE_KEY -> keyAgreement
|
||||
7 => bits |= 0x04, // ATTEST_KEY -> keyCertSign
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
bits
|
||||
}
|
||||
|
||||
fn encode_validity(not_before: &OffsetDateTime, not_after: &OffsetDateTime) -> Vec<u8> {
|
||||
let nb = encode_time(not_before);
|
||||
let na = encode_time(not_after);
|
||||
encode_der_sequence(&[&nb, &na])
|
||||
}
|
||||
|
||||
fn encode_time(dt: &OffsetDateTime) -> Vec<u8> {
|
||||
let year = dt.year();
|
||||
if (1950..2050).contains(&year) {
|
||||
encode_utctime(dt)
|
||||
} else {
|
||||
encode_gentime(dt)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_utctime(dt: &OffsetDateTime) -> Vec<u8> {
|
||||
// UTCTime: YYMMDDHHMMSSZ
|
||||
let year = dt.year() % 100;
|
||||
let s = format!(
|
||||
"{:02}{:02}{:02}{:02}{:02}{:02}Z",
|
||||
year, dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
|
||||
);
|
||||
let mut out = Vec::with_capacity(2 + s.len());
|
||||
out.push(0x17); // UTCTime tag
|
||||
out.extend_from_slice(&encode_der_length_bytes(s.len()));
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_gentime(dt: &OffsetDateTime) -> Vec<u8> {
|
||||
// GeneralizedTime: YYYYMMDDHHMMSSZ
|
||||
let s = format!(
|
||||
"{:04}{:02}{:02}{:02}{:02}{:02}Z",
|
||||
dt.year(), dt.month() as u8, dt.day(), dt.hour(), dt.minute(), dt.second()
|
||||
);
|
||||
let mut out = Vec::with_capacity(2 + s.len());
|
||||
out.push(0x18); // GeneralizedTime tag
|
||||
out.extend_from_slice(&encode_der_length_bytes(s.len()));
|
||||
out.extend_from_slice(s.as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_simple_cn_dn(cn: &str) -> Vec<u8> {
|
||||
// Name = SEQUENCE OF RelativeDistinguishedName
|
||||
// RDN = SET OF AttributeTypeAndValue
|
||||
// ATV = SEQUENCE { OID, UTF8String }
|
||||
let cn_oid = encode_der_oid(&[2, 5, 4, 3]);
|
||||
let cn_value = encode_der_utf8string(cn);
|
||||
let atv = encode_der_sequence(&[&cn_oid, &cn_value]);
|
||||
let rdn = encode_der_set(&[&atv]);
|
||||
encode_der_sequence(&[&rdn])
|
||||
}
|
||||
|
||||
fn timestamp_to_datetime(ts: i64) -> Result<OffsetDateTime> {
|
||||
if ts == -1 {
|
||||
return Ok(OffsetDateTime::now_utc());
|
||||
}
|
||||
OffsetDateTime::from_unix_timestamp(ts / 1000)
|
||||
.map_err(|e| CertGenError::CertBuildFailed(format!("invalid timestamp {ts}: {e}")))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DER encoding primitives
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn encode_der_length_bytes(len: usize) -> Vec<u8> {
|
||||
if len < 0x80 {
|
||||
vec![len as u8]
|
||||
} else if len <= 0xFF {
|
||||
vec![0x81, len as u8]
|
||||
} else if len <= 0xFFFF {
|
||||
vec![0x82, (len >> 8) as u8, len as u8]
|
||||
} else if len <= 0xFF_FFFF {
|
||||
vec![0x83, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||
} else {
|
||||
vec![0x84, (len >> 24) as u8, (len >> 16) as u8, (len >> 8) as u8, len as u8]
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_der_tag_length_value(tag: u8, content: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(1 + 4 + content.len());
|
||||
out.push(tag);
|
||||
out.extend_from_slice(&encode_der_length_bytes(content.len()));
|
||||
out.extend_from_slice(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_der_sequence(items: &[&[u8]]) -> Vec<u8> {
|
||||
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||
let mut content = Vec::with_capacity(total);
|
||||
for item in items {
|
||||
content.extend_from_slice(item);
|
||||
}
|
||||
encode_der_tag_length_value(0x30, &content)
|
||||
}
|
||||
|
||||
fn encode_der_sequence_of(items: &[Vec<u8>]) -> Vec<u8> {
|
||||
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||
let mut content = Vec::with_capacity(total);
|
||||
for item in items {
|
||||
content.extend_from_slice(item);
|
||||
}
|
||||
encode_der_tag_length_value(0x30, &content)
|
||||
}
|
||||
|
||||
fn encode_der_set(items: &[&[u8]]) -> Vec<u8> {
|
||||
let total: usize = items.iter().map(|i| i.len()).sum();
|
||||
let mut content = Vec::with_capacity(total);
|
||||
for item in items {
|
||||
content.extend_from_slice(item);
|
||||
}
|
||||
encode_der_tag_length_value(0x31, &content)
|
||||
}
|
||||
|
||||
fn encode_der_explicit_tag(tag_num: u8, content: &[u8]) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0xA0 | tag_num, content)
|
||||
}
|
||||
|
||||
fn encode_der_integer(value: &[u8]) -> Vec<u8> {
|
||||
// DER INTEGER must have minimal encoding and leading 0x00 if high bit set
|
||||
if value.is_empty() {
|
||||
return encode_der_tag_length_value(0x02, &[0x00]);
|
||||
}
|
||||
|
||||
// Strip leading zeros (but keep at least one byte)
|
||||
let mut start = 0;
|
||||
while start < value.len() - 1 && value[start] == 0 {
|
||||
start += 1;
|
||||
}
|
||||
let trimmed = &value[start..];
|
||||
|
||||
// Add leading 0x00 if high bit is set (positive integer)
|
||||
if trimmed[0] & 0x80 != 0 {
|
||||
let mut padded = Vec::with_capacity(1 + trimmed.len());
|
||||
padded.push(0x00);
|
||||
padded.extend_from_slice(trimmed);
|
||||
encode_der_tag_length_value(0x02, &padded)
|
||||
} else {
|
||||
encode_der_tag_length_value(0x02, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_der_bit_string(bits: &[u8]) -> Vec<u8> {
|
||||
// BIT STRING: tag 0x03, length, unused_bits (0), content
|
||||
let mut content = Vec::with_capacity(1 + bits.len());
|
||||
content.push(0x00); // 0 unused bits
|
||||
content.extend_from_slice(bits);
|
||||
encode_der_tag_length_value(0x03, &content)
|
||||
}
|
||||
|
||||
fn encode_der_octet_string(content: &[u8]) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0x04, content)
|
||||
}
|
||||
|
||||
fn encode_der_utf8string(s: &str) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0x0C, s.as_bytes())
|
||||
}
|
||||
|
||||
fn encode_der_boolean(val: bool) -> Vec<u8> {
|
||||
encode_der_tag_length_value(0x01, &[if val { 0xFF } else { 0x00 }])
|
||||
}
|
||||
|
||||
fn encode_der_oid(components: &[u64]) -> Vec<u8> {
|
||||
if components.len() < 2 {
|
||||
return encode_der_tag_length_value(0x06, &[]);
|
||||
}
|
||||
|
||||
let mut content = Vec::new();
|
||||
// First two components encoded as 40 * c[0] + c[1]
|
||||
content.push((components[0] * 40 + components[1]) as u8);
|
||||
|
||||
for &c in &components[2..] {
|
||||
encode_oid_subidentifier(&mut content, c);
|
||||
}
|
||||
|
||||
encode_der_tag_length_value(0x06, &content)
|
||||
}
|
||||
|
||||
fn encode_oid_subidentifier(buf: &mut Vec<u8>, mut value: u64) {
|
||||
if value == 0 {
|
||||
buf.push(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Encode in base-128 with continuation bits
|
||||
let mut bytes = Vec::new();
|
||||
while value > 0 {
|
||||
bytes.push((value & 0x7F) as u8);
|
||||
value >>= 7;
|
||||
}
|
||||
bytes.reverse();
|
||||
|
||||
// Set high bit on all but the last byte
|
||||
for i in 0..bytes.len() - 1 {
|
||||
bytes[i] |= 0x80;
|
||||
}
|
||||
|
||||
buf.extend_from_slice(&bytes);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CertGenError {
|
||||
Jni(String),
|
||||
NullParam(&'static str),
|
||||
UnsupportedAlgorithm(i32),
|
||||
UnsupportedEcCurve(i32),
|
||||
KeyGenFailed(String),
|
||||
CertBuildFailed(String),
|
||||
KeyboxParseFailed(String),
|
||||
AttestationBuildFailed(String),
|
||||
DerError(der::Error),
|
||||
EmptyKeyboxChain,
|
||||
ChallengeTooLong(usize),
|
||||
InvalidParameter(String),
|
||||
SigningFailed(String),
|
||||
SerializationFailed(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for CertGenError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Jni(msg) => write!(f, "JNI error: {}", msg),
|
||||
Self::NullParam(name) => write!(f, "null required parameter: {}", name),
|
||||
Self::UnsupportedAlgorithm(v) => write!(f, "unsupported algorithm: {}", v),
|
||||
Self::UnsupportedEcCurve(v) => write!(f, "unsupported EC curve: {}", v),
|
||||
Self::KeyGenFailed(msg) => write!(f, "key generation failed: {}", msg),
|
||||
Self::CertBuildFailed(msg) => write!(f, "certificate build failed: {}", msg),
|
||||
Self::KeyboxParseFailed(msg) => write!(f, "keybox parse failed: {}", msg),
|
||||
Self::AttestationBuildFailed(msg) => write!(f, "attestation build failed: {}", msg),
|
||||
Self::DerError(e) => write!(f, "DER error: {}", e),
|
||||
Self::EmptyKeyboxChain => write!(f, "keybox certificate chain is empty"),
|
||||
Self::ChallengeTooLong(len) => write!(f, "attestation challenge too long: {} bytes (max 128)", len),
|
||||
Self::InvalidParameter(msg) => write!(f, "invalid parameter: {}", msg),
|
||||
Self::SigningFailed(msg) => write!(f, "signing failed: {}", msg),
|
||||
Self::SerializationFailed(msg) => write!(f, "serialization failed: {}", msg),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CertGenError {}
|
||||
|
||||
impl From<jni::errors::Error> for CertGenError {
|
||||
fn from(e: jni::errors::Error) -> Self {
|
||||
Self::Jni(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<der::Error> for CertGenError {
|
||||
fn from(e: der::Error) -> Self {
|
||||
Self::DerError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ring::error::Unspecified> for CertGenError {
|
||||
fn from(e: ring::error::Unspecified) -> Self {
|
||||
Self::KeyGenFailed(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ring::error::KeyRejected> for CertGenError {
|
||||
fn from(e: ring::error::KeyRejected) -> Self {
|
||||
Self::KeyGenFailed(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rsa::Error> for CertGenError {
|
||||
fn from(e: rsa::Error) -> Self {
|
||||
Self::KeyGenFailed(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CertGenError>;
|
||||
@@ -0,0 +1,96 @@
|
||||
use crate::error::{CertGenError, Result};
|
||||
use der::{Decode, Encode};
|
||||
use x509_cert::Certificate;
|
||||
|
||||
pub struct ParsedKeybox {
|
||||
pub signing_key_der: Vec<u8>,
|
||||
pub issuer_dn_der: Vec<u8>,
|
||||
pub cert_chain_ders: Vec<Vec<u8>>,
|
||||
pub leaf_not_after: i64,
|
||||
}
|
||||
|
||||
pub fn parse_keybox(cert_chain_bytes: &[u8], private_key_bytes: &[u8]) -> Result<ParsedKeybox> {
|
||||
let certs = split_der_certificates(cert_chain_bytes)?;
|
||||
if certs.is_empty() {
|
||||
return Err(CertGenError::KeyboxParseFailed("no certificates found".into()));
|
||||
}
|
||||
|
||||
let leaf = Certificate::from_der(&certs[0])
|
||||
.map_err(|e| CertGenError::KeyboxParseFailed(format!("leaf cert parse: {e}")))?;
|
||||
|
||||
let issuer_dn_der = leaf.tbs_certificate.subject.to_der()
|
||||
.map_err(|e| CertGenError::KeyboxParseFailed(format!("subject DN encode: {e}")))?;
|
||||
|
||||
let not_after = leaf.tbs_certificate.validity.not_after;
|
||||
let leaf_not_after = not_after.to_unix_duration().as_secs() as i64;
|
||||
|
||||
Ok(ParsedKeybox {
|
||||
signing_key_der: private_key_bytes.to_vec(),
|
||||
issuer_dn_der,
|
||||
cert_chain_ders: certs,
|
||||
leaf_not_after,
|
||||
})
|
||||
}
|
||||
|
||||
fn split_der_certificates(data: &[u8]) -> Result<Vec<Vec<u8>>> {
|
||||
let mut certs = Vec::new();
|
||||
let mut offset = 0;
|
||||
|
||||
while offset < data.len() {
|
||||
if data[offset] != 0x30 {
|
||||
return Err(CertGenError::KeyboxParseFailed(
|
||||
format!("expected SEQUENCE tag 0x30 at offset {offset}, got 0x{:02x}", data[offset])
|
||||
));
|
||||
}
|
||||
|
||||
let (content_len, header_len) = parse_der_length(&data[offset + 1..])?;
|
||||
let total_len = 1 + header_len + content_len;
|
||||
|
||||
if offset + total_len > data.len() {
|
||||
return Err(CertGenError::KeyboxParseFailed(
|
||||
format!("cert at offset {offset} extends beyond buffer: need {total_len}, have {}", data.len() - offset)
|
||||
));
|
||||
}
|
||||
|
||||
certs.push(data[offset..offset + total_len].to_vec());
|
||||
offset += total_len;
|
||||
}
|
||||
|
||||
if certs.is_empty() {
|
||||
return Err(CertGenError::KeyboxParseFailed("no certificates in chain".into()));
|
||||
}
|
||||
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
// Returns (content_length, number_of_length_bytes_consumed)
|
||||
fn parse_der_length(data: &[u8]) -> Result<(usize, usize)> {
|
||||
if data.is_empty() {
|
||||
return Err(CertGenError::KeyboxParseFailed("truncated DER length".into()));
|
||||
}
|
||||
|
||||
let first = data[0];
|
||||
|
||||
if first < 0x80 {
|
||||
// Short form: length is the byte itself
|
||||
return Ok((first as usize, 1));
|
||||
}
|
||||
|
||||
// Long form: low 7 bits = number of subsequent length bytes
|
||||
let num_bytes = (first & 0x7f) as usize;
|
||||
if num_bytes == 0 || num_bytes > 4 {
|
||||
return Err(CertGenError::KeyboxParseFailed(
|
||||
format!("unsupported DER length encoding: 0x{first:02x}")
|
||||
));
|
||||
}
|
||||
if 1 + num_bytes > data.len() {
|
||||
return Err(CertGenError::KeyboxParseFailed("truncated multi-byte DER length".into()));
|
||||
}
|
||||
|
||||
let mut len: usize = 0;
|
||||
for i in 0..num_bytes {
|
||||
len = (len << 8) | (data[1 + i] as usize);
|
||||
}
|
||||
|
||||
Ok((len, 1 + num_bytes))
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::error::{CertGenError, Result};
|
||||
use crate::types::{Algorithm, EcCurve, GeneratedKeyPair};
|
||||
|
||||
pub fn generate_key_pair(
|
||||
algorithm: Algorithm,
|
||||
key_size: u32,
|
||||
ec_curve: Option<EcCurve>,
|
||||
rsa_public_exponent: u64,
|
||||
) -> Result<GeneratedKeyPair> {
|
||||
match algorithm {
|
||||
Algorithm::Ec => {
|
||||
let curve = ec_curve.ok_or_else(|| CertGenError::InvalidParameter("ec_curve required for EC".into()))?;
|
||||
generate_ec_key_pair(curve)
|
||||
}
|
||||
Algorithm::Rsa => generate_rsa_key_pair(key_size, rsa_public_exponent),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_ec_key_pair(curve: EcCurve) -> Result<GeneratedKeyPair> {
|
||||
let alg = match curve {
|
||||
EcCurve::P256 => &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING,
|
||||
EcCurve::P384 => &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
|
||||
_ => return Err(CertGenError::UnsupportedEcCurve(curve as i32)),
|
||||
};
|
||||
|
||||
let rng = ring::rand::SystemRandom::new();
|
||||
let pkcs8_doc = ring::signature::EcdsaKeyPair::generate_pkcs8(alg, &rng)?;
|
||||
|
||||
Ok(GeneratedKeyPair {
|
||||
private_key_pkcs8: pkcs8_doc.as_ref().to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_rsa_key_pair(key_size: u32, rsa_public_exponent: u64) -> Result<GeneratedKeyPair> {
|
||||
use pkcs8::EncodePrivateKey;
|
||||
|
||||
if !matches!(key_size, 2048 | 3072 | 4096) {
|
||||
return Err(CertGenError::InvalidParameter(
|
||||
format!("RSA key size must be 2048, 3072, or 4096; got {key_size}")
|
||||
));
|
||||
}
|
||||
|
||||
let exp = if rsa_public_exponent == 0 {
|
||||
rsa::BigUint::from(65537u64)
|
||||
} else {
|
||||
rsa::BigUint::from(rsa_public_exponent)
|
||||
};
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let private_key = rsa::RsaPrivateKey::new_with_exp(&mut rng, key_size as usize, &exp)
|
||||
.map_err(|e| CertGenError::KeyGenFailed(e.to_string()))?;
|
||||
|
||||
let pkcs8_der = private_key.to_pkcs8_der()
|
||||
.map_err(|e| CertGenError::SerializationFailed(e.to_string()))?;
|
||||
|
||||
Ok(GeneratedKeyPair {
|
||||
private_key_pkcs8: pkcs8_der.as_bytes().to_vec(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
mod error;
|
||||
mod types;
|
||||
mod keygen;
|
||||
pub mod keybox;
|
||||
pub mod attestation;
|
||||
pub mod certbuilder;
|
||||
pub mod logging;
|
||||
|
||||
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JString};
|
||||
use jni::sys::{jboolean, jbyteArray, jstring};
|
||||
use jni::JNIEnv;
|
||||
|
||||
use crate::error::{CertGenError, Result};
|
||||
use crate::types::{Algorithm, CertGenParams, EcCurve};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI entry: generateAttestedKeyPair
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_generateAttestedKeyPair(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
config: JObject,
|
||||
) -> jbyteArray {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
generate_attested_inner(&mut env, &config)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(raw)) => raw,
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(%e, "generateAttestedKeyPair failed");
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
format!("NativeCertGen: {e}"),
|
||||
);
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("generateAttestedKeyPair panicked");
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
"NativeCertGen: internal panic",
|
||||
);
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_attested_inner(env: &mut JNIEnv, config: &JObject) -> Result<jbyteArray> {
|
||||
let params = extract_config(env, config)?;
|
||||
|
||||
let key_pair = keygen::generate_key_pair(
|
||||
params.algorithm,
|
||||
params.key_size,
|
||||
params.ec_curve,
|
||||
params.rsa_public_exponent,
|
||||
)?;
|
||||
|
||||
let keybox = keybox::parse_keybox(¶ms.keybox_cert_chain, ¶ms.keybox_private_key)?;
|
||||
|
||||
let attest_ext = attestation::build_attestation_extension(¶ms)?;
|
||||
|
||||
let cert_chain = certbuilder::build_certificate_chain(
|
||||
&key_pair,
|
||||
&attest_ext,
|
||||
&keybox,
|
||||
¶ms,
|
||||
)?;
|
||||
|
||||
let blob = assemble_result(&key_pair.private_key_pkcs8, &cert_chain);
|
||||
|
||||
let out = env.byte_array_from_slice(&blob)?;
|
||||
Ok(out.into_raw())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI entry: initLogging
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_initLogging(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
verbose: jboolean,
|
||||
log_dir: JString,
|
||||
) -> jboolean {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
init_logging_inner(&mut env, verbose, &log_dir)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(())) => 1,
|
||||
Ok(Err(e)) => {
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
format!("NativeCertGen initLogging: {e}"),
|
||||
);
|
||||
0
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = env.throw_new(
|
||||
"java/lang/RuntimeException",
|
||||
"NativeCertGen initLogging: internal panic",
|
||||
);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_logging_inner(env: &mut JNIEnv, verbose: jboolean, log_dir: &JString) -> Result<()> {
|
||||
let dir: String = env.get_string(log_dir)?.into();
|
||||
logging::init(verbose != 0, &dir, 2, 3)
|
||||
.map_err(|e| CertGenError::Jni(format!("logging init failed: {e}")))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI entry: dumpLogs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "system" fn Java_org_matrix_TEESimulator_pki_NativeCertGen_dumpLogs(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
) -> jstring {
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
dump_logs_inner(&mut env)
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(Ok(raw)) => raw,
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(%e, "dumpLogs failed");
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
Err(_) => {
|
||||
tracing::error!("dumpLogs panicked");
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_logs_inner(env: &mut JNIEnv) -> Result<jstring> {
|
||||
logging::dump::execute_dump()
|
||||
.map_err(|e| CertGenError::Jni(format!("dump failed: {e}")))?;
|
||||
|
||||
// Read the dump path written by execute_dump
|
||||
let path = std::fs::read_to_string("/data/adb/tricky_store/.dump_path")
|
||||
.map_err(|e| CertGenError::Jni(format!("read dump path: {e}")))?;
|
||||
|
||||
let jpath = env.new_string(&path)?;
|
||||
Ok(jpath.into_raw())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config extraction from Java CertGenConfig object
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn extract_config(env: &mut JNIEnv, config: &JObject) -> Result<CertGenParams> {
|
||||
let algorithm = get_int(env, config, "algorithm")?;
|
||||
let key_size = get_int(env, config, "keySize")?;
|
||||
let ec_curve_raw = get_int(env, config, "ecCurve")?;
|
||||
let rsa_pub_exp = get_long(env, config, "rsaPublicExponent")?;
|
||||
|
||||
let cert_not_before = get_long(env, config, "certNotBefore")?;
|
||||
let cert_not_after = get_long(env, config, "certNotAfter")?;
|
||||
let security_level = get_int(env, config, "securityLevel")?;
|
||||
let attest_version = get_int(env, config, "attestVersion")?;
|
||||
let keymaster_version = get_int(env, config, "keymasterVersion")?;
|
||||
let os_version = get_int(env, config, "osVersion")?;
|
||||
let os_patch_level = get_int(env, config, "osPatchLevel")?;
|
||||
let vendor_patch_level = get_int(env, config, "vendorPatchLevel")?;
|
||||
let boot_patch_level = get_int(env, config, "bootPatchLevel")?;
|
||||
let creation_datetime = get_long(env, config, "creationDatetime")?;
|
||||
|
||||
let attestation_challenge = get_nullable_byte_array(env, config, "attestationChallenge")?;
|
||||
let purposes = get_int_array(env, config, "purposes")?;
|
||||
let digests = get_int_array(env, config, "digests")?;
|
||||
let cert_serial = get_nullable_byte_array(env, config, "certSerial")?;
|
||||
let cert_subject = get_nullable_byte_array(env, config, "certSubject")?;
|
||||
let keybox_private_key = get_byte_array(env, config, "keyboxPrivateKey")?;
|
||||
let keybox_cert_chain = get_byte_array(env, config, "keyboxCertChain")?;
|
||||
let boot_key = get_byte_array(env, config, "bootKey")?;
|
||||
let boot_hash = get_byte_array(env, config, "bootHash")?;
|
||||
let attestation_app_id = get_byte_array(env, config, "attestationApplicationId")?;
|
||||
let module_hash = get_nullable_byte_array(env, config, "moduleHash")?;
|
||||
|
||||
let id_brand = get_nullable_byte_array(env, config, "idBrand")?;
|
||||
let id_device = get_nullable_byte_array(env, config, "idDevice")?;
|
||||
let id_product = get_nullable_byte_array(env, config, "idProduct")?;
|
||||
let id_serial = get_nullable_byte_array(env, config, "idSerial")?;
|
||||
let id_imei = get_nullable_byte_array(env, config, "idImei")?;
|
||||
let id_meid = get_nullable_byte_array(env, config, "idMeid")?;
|
||||
let id_manufacturer = get_nullable_byte_array(env, config, "idManufacturer")?;
|
||||
let id_model = get_nullable_byte_array(env, config, "idModel")?;
|
||||
let id_second_imei = get_nullable_byte_array(env, config, "idSecondImei")?;
|
||||
|
||||
Ok(CertGenParams {
|
||||
algorithm: Algorithm::try_from(algorithm)?,
|
||||
key_size: key_size as u32,
|
||||
ec_curve: if algorithm == 3 {
|
||||
Some(EcCurve::try_from(ec_curve_raw)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
rsa_public_exponent: rsa_pub_exp as u64,
|
||||
attestation_challenge,
|
||||
purposes,
|
||||
digests,
|
||||
cert_serial,
|
||||
cert_subject,
|
||||
cert_not_before,
|
||||
cert_not_after,
|
||||
keybox_private_key,
|
||||
keybox_cert_chain,
|
||||
security_level,
|
||||
attest_version,
|
||||
keymaster_version,
|
||||
os_version,
|
||||
os_patch_level,
|
||||
vendor_patch_level,
|
||||
boot_patch_level,
|
||||
boot_key,
|
||||
boot_hash,
|
||||
creation_datetime,
|
||||
attestation_application_id: attestation_app_id,
|
||||
module_hash,
|
||||
id_brand,
|
||||
id_device,
|
||||
id_product,
|
||||
id_serial,
|
||||
id_imei,
|
||||
id_meid,
|
||||
id_manufacturer,
|
||||
id_model,
|
||||
id_second_imei,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// JNI field accessor helpers — called 35+ times, justifies the abstraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn get_int(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i32> {
|
||||
Ok(env.get_field(obj, name, "I")?.i()?)
|
||||
}
|
||||
|
||||
fn get_long(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<i64> {
|
||||
Ok(env.get_field(obj, name, "J")?.j()?)
|
||||
}
|
||||
|
||||
fn get_byte_array(env: &mut JNIEnv, obj: &JObject, name: &'static str) -> Result<Vec<u8>> {
|
||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||
if field.is_null() {
|
||||
return Err(CertGenError::NullParam(name));
|
||||
}
|
||||
let arr: JByteArray = field.into();
|
||||
let len = env.get_array_length(&arr)?;
|
||||
let mut buf = vec![0i8; len as usize];
|
||||
env.get_byte_array_region(&arr, 0, &mut buf)?;
|
||||
env.delete_local_ref(arr)?;
|
||||
Ok(buf.into_iter().map(|b| b as u8).collect())
|
||||
}
|
||||
|
||||
fn get_nullable_byte_array(
|
||||
env: &mut JNIEnv,
|
||||
obj: &JObject,
|
||||
name: &str,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let field = env.get_field(obj, name, "[B")?.l()?;
|
||||
if field.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
let arr: JByteArray = field.into();
|
||||
let len = env.get_array_length(&arr)?;
|
||||
let mut buf = vec![0i8; len as usize];
|
||||
env.get_byte_array_region(&arr, 0, &mut buf)?;
|
||||
env.delete_local_ref(arr)?;
|
||||
Ok(Some(buf.into_iter().map(|b| b as u8).collect()))
|
||||
}
|
||||
|
||||
fn get_int_array(env: &mut JNIEnv, obj: &JObject, name: &str) -> Result<Vec<i32>> {
|
||||
let field = env.get_field(obj, name, "[I")?.l()?;
|
||||
if field.is_null() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let arr: JIntArray = field.into();
|
||||
let len = env.get_array_length(&arr)?;
|
||||
let mut buf = vec![0i32; len as usize];
|
||||
env.get_int_array_region(&arr, 0, &mut buf)?;
|
||||
env.delete_local_ref(arr)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Binary result assembly (doc 09 section 4.1)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn assemble_result(private_key: &[u8], cert_chain: &[Vec<u8>]) -> Vec<u8> {
|
||||
let total = 4 + private_key.len()
|
||||
+ 4
|
||||
+ cert_chain.iter().map(|c| 4 + c.len()).sum::<usize>();
|
||||
|
||||
let mut buf = Vec::with_capacity(total);
|
||||
|
||||
// Private key segment
|
||||
buf.extend_from_slice(&(private_key.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(private_key);
|
||||
|
||||
// Cert count
|
||||
buf.extend_from_slice(&(cert_chain.len() as u32).to_be_bytes());
|
||||
|
||||
// Each cert: length-prefixed DER
|
||||
for cert in cert_chain {
|
||||
buf.extend_from_slice(&(cert.len() as u32).to_be_bytes());
|
||||
buf.extend_from_slice(cert);
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const DUMP_DIR: &str = "/sdcard/Download";
|
||||
const LOCK_PATH: &str = "/data/adb/tricky_store/.dump_lock";
|
||||
const DUMP_PATH_FILE: &str = "/data/adb/tricky_store/.dump_path";
|
||||
const LOG_DIR: &str = "/data/adb/tricky_store/logs";
|
||||
const BASE_DIR: &str = "/data/adb/tricky_store";
|
||||
const LOGCAT_SIZE_LIMIT: usize = 2 * 1024 * 1024;
|
||||
|
||||
struct FlockGuard {
|
||||
_file: File,
|
||||
}
|
||||
|
||||
impl FlockGuard {
|
||||
fn acquire() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
if let Some(parent) = Path::new(LOCK_PATH).parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = File::create(LOCK_PATH)?;
|
||||
let fd = {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
file.as_raw_fd()
|
||||
};
|
||||
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
|
||||
if ret != 0 {
|
||||
return Err("dump already in progress".into());
|
||||
}
|
||||
Ok(Self { _file: file })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for FlockGuard {
|
||||
fn drop(&mut self) {
|
||||
// flock released automatically when file descriptor closes
|
||||
}
|
||||
}
|
||||
|
||||
fn random_name(len: usize) -> String {
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
let idx = rng.gen_range(0..36u8);
|
||||
if idx < 10 {
|
||||
(b'0' + idx) as char
|
||||
} else {
|
||||
(b'a' + idx - 10) as char
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_logcat(tag: &str) -> Vec<u8> {
|
||||
let output = Command::new("logcat")
|
||||
.args(["-d", "-s", tag])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(o) => {
|
||||
let mut data = o.stdout;
|
||||
data.truncate(LOGCAT_SIZE_LIMIT);
|
||||
data
|
||||
}
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_device_info() -> String {
|
||||
let mut info = String::new();
|
||||
|
||||
if let Ok(output) = Command::new("uname").arg("-a").output() {
|
||||
info.push_str(&format!(
|
||||
"uname={}\n",
|
||||
String::from_utf8_lossy(&output.stdout).trim()
|
||||
));
|
||||
}
|
||||
|
||||
for (key, prop) in [
|
||||
("device", "ro.product.device"),
|
||||
("build", "ro.build.display.id"),
|
||||
("android", "ro.build.version.release"),
|
||||
] {
|
||||
if let Ok(output) = Command::new("getprop").arg(prop).output() {
|
||||
info.push_str(&format!(
|
||||
"{}={}\n",
|
||||
key,
|
||||
String::from_utf8_lossy(&output.stdout).trim()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// KSU version
|
||||
if let Ok(ver) = fs::read_to_string("/data/adb/ksu/version") {
|
||||
info.push_str(&format!("ksu={}\n", ver.trim()));
|
||||
}
|
||||
|
||||
// Module version from module.prop
|
||||
if let Ok(prop) = fs::read_to_string("/data/adb/modules/tricky_store/module.prop") {
|
||||
for line in prop.lines() {
|
||||
if let Some(ver) = line.strip_prefix("version=") {
|
||||
info.push_str(&format!("module={}\n", ver.trim()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
fn read_file_bytes(path: &str) -> Option<Vec<u8>> {
|
||||
let mut buf = Vec::new();
|
||||
File::open(path).ok()?.read_to_end(&mut buf).ok()?;
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
fn epoch_millis() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn execute_dump() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _lock = FlockGuard::acquire()?;
|
||||
|
||||
let _ = fs::create_dir_all(DUMP_DIR);
|
||||
let zip_name = format!("{}.zip", random_name(8));
|
||||
let zip_path = format!("{}/{}", DUMP_DIR, zip_name);
|
||||
|
||||
let zip_file = File::create(&zip_path)?;
|
||||
let mut zip = zip::ZipWriter::new(zip_file);
|
||||
let options =
|
||||
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
|
||||
|
||||
let mut file_count = 0u32;
|
||||
|
||||
// Log files
|
||||
let log_files = [
|
||||
"certgen.log",
|
||||
"certgen.log.1",
|
||||
"certgen.log.2",
|
||||
"certgen.log.3",
|
||||
"certgen.log.4",
|
||||
];
|
||||
for name in &log_files {
|
||||
let path = format!("{}/{}", LOG_DIR, name);
|
||||
if let Some(data) = read_file_bytes(&path) {
|
||||
zip.start_file(*name, options)?;
|
||||
zip.write_all(&data)?;
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Logcat
|
||||
let logcat = collect_logcat("TEESimulator");
|
||||
if !logcat.is_empty() {
|
||||
zip.start_file("logcat-teesimulator.log", options)?;
|
||||
zip.write_all(&logcat)?;
|
||||
file_count += 1;
|
||||
}
|
||||
|
||||
// Config files
|
||||
for name in ["tee_status.txt", "security_patch.txt"] {
|
||||
let path = format!("{}/{}", BASE_DIR, name);
|
||||
if let Some(data) = read_file_bytes(&path) {
|
||||
zip.start_file(name, options)?;
|
||||
zip.write_all(&data)?;
|
||||
file_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Device info
|
||||
let device_info = collect_device_info();
|
||||
if !device_info.is_empty() {
|
||||
zip.start_file("device-info.txt", options)?;
|
||||
zip.write_all(device_info.as_bytes())?;
|
||||
file_count += 1;
|
||||
}
|
||||
|
||||
// Manifest
|
||||
let manifest = serde_json::json!({
|
||||
"timestamp": epoch_millis(),
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"files": file_count,
|
||||
});
|
||||
zip.start_file("manifest.json", options)?;
|
||||
zip.write_all(manifest.to_string().as_bytes())?;
|
||||
|
||||
zip.finish()?;
|
||||
|
||||
let zip_size = fs::metadata(&zip_path).map(|m| m.len()).unwrap_or(0);
|
||||
fs::write(DUMP_PATH_FILE, &zip_path)?;
|
||||
|
||||
let result = serde_json::json!({
|
||||
"zip": zip_path,
|
||||
"size": zip_size,
|
||||
"files": file_count + 1, // +1 for manifest
|
||||
});
|
||||
println!("{}", result);
|
||||
|
||||
tracing::info!(path = %zip_path, size = zip_size, "diagnostic dump created");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::sync::Mutex;
|
||||
use tracing::field::{Field, Visit};
|
||||
use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::layer::Context;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
const KMSG_PATH: &str = "/dev/kmsg";
|
||||
const TAG: &str = "TEESimulator";
|
||||
|
||||
pub struct KmsgLayer {
|
||||
writer: Mutex<Option<File>>,
|
||||
}
|
||||
|
||||
impl KmsgLayer {
|
||||
pub fn new() -> Self {
|
||||
let file = OpenOptions::new().write(true).open(KMSG_PATH).ok();
|
||||
Self {
|
||||
writer: Mutex::new(file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn syslog_priority(level: &Level) -> u8 {
|
||||
match *level {
|
||||
Level::ERROR => 3,
|
||||
Level::WARN => 4,
|
||||
Level::INFO => 6,
|
||||
Level::DEBUG | Level::TRACE => 7,
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageVisitor {
|
||||
message: String,
|
||||
fields: String,
|
||||
}
|
||||
|
||||
impl MessageVisitor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message: String::new(),
|
||||
fields: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for MessageVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "message" {
|
||||
let raw = format!("{:?}", value);
|
||||
// Strip surrounding debug quotes if present
|
||||
self.message = raw
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(&raw)
|
||||
.to_string();
|
||||
} else {
|
||||
if !self.fields.is_empty() {
|
||||
self.fields.push(' ');
|
||||
}
|
||||
self.fields.push_str(&format!("{}={:?}", field.name(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Subscriber> Layer<S> for KmsgLayer {
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let mut guard = match self.writer.lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return,
|
||||
};
|
||||
let file = match guard.as_mut() {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let priority = syslog_priority(event.metadata().level());
|
||||
let mut visitor = MessageVisitor::new();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let line = if visitor.fields.is_empty() {
|
||||
format!("<{}>{}: {}\n", priority, TAG, visitor.message)
|
||||
} else {
|
||||
format!(
|
||||
"<{}>{}: {} {}\n",
|
||||
priority, TAG, visitor.message, visitor.fields
|
||||
)
|
||||
};
|
||||
|
||||
let _ = file.write_all(line.as_bytes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
mod kmsg;
|
||||
mod rotating;
|
||||
pub mod sysfs;
|
||||
pub mod dump;
|
||||
|
||||
use std::path::Path;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
||||
|
||||
pub fn init(
|
||||
verbose_flag: bool,
|
||||
log_dir: &str,
|
||||
max_size_mb: u64,
|
||||
max_files: usize,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let verbose = verbose_flag || Path::new(VERBOSE_MARKER).exists();
|
||||
|
||||
let (max_size, max_files) = if verbose {
|
||||
(5 * 1024 * 1024, 5)
|
||||
} else {
|
||||
(max_size_mb * 1024 * 1024, max_files)
|
||||
};
|
||||
|
||||
let level = if verbose { "trace" } else { "info" };
|
||||
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level));
|
||||
|
||||
let kmsg_layer = kmsg::KmsgLayer::new();
|
||||
let rotating_layer = rotating::RotatingFileLayer::new(log_dir, max_size, max_files);
|
||||
let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
|
||||
|
||||
// Idempotent — second call returns Ok instead of propagating SetGlobalDefaultError
|
||||
let _ = tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(kmsg_layer)
|
||||
.with(rotating_layer)
|
||||
.with(stderr_layer)
|
||||
.try_init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::field::{Field, Visit};
|
||||
use tracing::{Event, Level, Subscriber};
|
||||
use tracing_subscriber::layer::Context;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
struct RotatingState {
|
||||
dir: PathBuf,
|
||||
current: Option<File>,
|
||||
current_size: u64,
|
||||
max_size: u64,
|
||||
max_files: usize,
|
||||
}
|
||||
|
||||
pub struct RotatingFileLayer {
|
||||
state: Mutex<RotatingState>,
|
||||
}
|
||||
|
||||
impl RotatingFileLayer {
|
||||
pub fn new(dir: &str, max_size: u64, max_files: usize) -> Self {
|
||||
let dir = PathBuf::from(dir);
|
||||
let _ = fs::create_dir_all(&dir);
|
||||
let (file, size) = open_current_log(&dir);
|
||||
Self {
|
||||
state: Mutex::new(RotatingState {
|
||||
dir,
|
||||
current: file,
|
||||
current_size: size,
|
||||
max_size,
|
||||
max_files,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_current_log(dir: &Path) -> (Option<File>, u64) {
|
||||
let path = dir.join("certgen.log");
|
||||
let size = fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.ok();
|
||||
(file, size)
|
||||
}
|
||||
|
||||
fn rotate(state: &mut RotatingState) {
|
||||
// Close current handle before renaming
|
||||
state.current.take();
|
||||
|
||||
let dir = &state.dir;
|
||||
// Delete the oldest rotated file before shifting
|
||||
let oldest = dir.join(format!("certgen.log.{}", state.max_files));
|
||||
if oldest.exists() {
|
||||
let _ = fs::remove_file(&oldest);
|
||||
}
|
||||
// Shift older files up: .{N} -> .{N+1}
|
||||
for i in (1..state.max_files).rev() {
|
||||
let from = dir.join(format!("certgen.log.{}", i));
|
||||
let to = dir.join(format!("certgen.log.{}", i + 1));
|
||||
if from.exists() {
|
||||
let _ = fs::rename(&from, &to);
|
||||
}
|
||||
}
|
||||
// Current -> .1
|
||||
let current_path = dir.join("certgen.log");
|
||||
let first_rotated = dir.join("certgen.log.1");
|
||||
if current_path.exists() {
|
||||
let _ = fs::rename(¤t_path, &first_rotated);
|
||||
}
|
||||
|
||||
let (file, size) = open_current_log(dir);
|
||||
state.current = file;
|
||||
state.current_size = size;
|
||||
}
|
||||
|
||||
fn epoch_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn level_str(level: &Level) -> &'static str {
|
||||
match *level {
|
||||
Level::ERROR => "ERROR",
|
||||
Level::WARN => "WARN",
|
||||
Level::INFO => "INFO",
|
||||
Level::DEBUG => "DEBUG",
|
||||
Level::TRACE => "TRACE",
|
||||
}
|
||||
}
|
||||
|
||||
struct LogVisitor {
|
||||
message: String,
|
||||
fields: String,
|
||||
}
|
||||
|
||||
impl LogVisitor {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
message: String::new(),
|
||||
fields: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for LogVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||
if field.name() == "message" {
|
||||
let raw = format!("{:?}", value);
|
||||
self.message = raw
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(&raw)
|
||||
.to_string();
|
||||
} else {
|
||||
if !self.fields.is_empty() {
|
||||
self.fields.push(' ');
|
||||
}
|
||||
self.fields.push_str(&format!("{}={:?}", field.name(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Subscriber> Layer<S> for RotatingFileLayer {
|
||||
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
|
||||
let mut state = match self.state.lock() {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if state.current_size >= state.max_size {
|
||||
rotate(&mut state);
|
||||
}
|
||||
|
||||
let file = match state.current.as_mut() {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let ts = epoch_secs();
|
||||
let lvl = level_str(event.metadata().level());
|
||||
let target = event.metadata().target();
|
||||
|
||||
let mut visitor = LogVisitor::new();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let line = if visitor.fields.is_empty() {
|
||||
format!("{} [{}] {}: {}\n", ts, lvl, target, visitor.message)
|
||||
} else {
|
||||
format!(
|
||||
"{} [{}] {}: {} {}\n",
|
||||
ts, lvl, target, visitor.message, visitor.fields
|
||||
)
|
||||
};
|
||||
|
||||
if file.write_all(line.as_bytes()).is_ok() {
|
||||
state.current_size += line.len() as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
const VERBOSE_MARKER: &str = "/data/adb/tricky_store/.verbose";
|
||||
|
||||
pub fn is_verbose() -> bool {
|
||||
Path::new(VERBOSE_MARKER).exists()
|
||||
}
|
||||
|
||||
pub fn set_verbose_marker(enabled: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if enabled {
|
||||
if let Some(parent) = Path::new(VERBOSE_MARKER).parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(VERBOSE_MARKER, "")?;
|
||||
} else if Path::new(VERBOSE_MARKER).exists() {
|
||||
fs::remove_file(VERBOSE_MARKER)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable() -> Result<(), Box<dyn std::error::Error>> {
|
||||
set_verbose_marker(true)?;
|
||||
tracing::info!("verbose logging enabled via marker file");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable() -> Result<(), Box<dyn std::error::Error>> {
|
||||
set_verbose_marker(false)?;
|
||||
tracing::info!("verbose logging disabled, marker file removed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn status() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let state = if is_verbose() { "enabled" } else { "disabled" };
|
||||
tracing::info!(verbose = state, "verbose marker status");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::error::CertGenError;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Algorithm {
|
||||
Rsa = 1,
|
||||
Ec = 3,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for Algorithm {
|
||||
type Error = CertGenError;
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
1 => Ok(Self::Rsa),
|
||||
3 => Ok(Self::Ec),
|
||||
_ => Err(CertGenError::UnsupportedAlgorithm(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum EcCurve {
|
||||
P224 = 0,
|
||||
P256 = 1,
|
||||
P384 = 2,
|
||||
P521 = 3,
|
||||
Curve25519 = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<i32> for EcCurve {
|
||||
type Error = CertGenError;
|
||||
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::P224),
|
||||
1 => Ok(Self::P256),
|
||||
2 => Ok(Self::P384),
|
||||
3 => Ok(Self::P521),
|
||||
4 => Ok(Self::Curve25519),
|
||||
_ => Err(CertGenError::UnsupportedEcCurve(value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum KeyPurpose {
|
||||
Encrypt = 0,
|
||||
Decrypt = 1,
|
||||
Sign = 2,
|
||||
Verify = 3,
|
||||
WrapKey = 5,
|
||||
AgreeKey = 6,
|
||||
AttestKey = 7,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum SecurityLevel {
|
||||
Software = 0,
|
||||
TrustedEnvironment = 1,
|
||||
StrongBox = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum VerifiedBootState {
|
||||
Verified = 0,
|
||||
SelfSigned = 1,
|
||||
Unverified = 2,
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
pub struct CertGenParams {
|
||||
pub algorithm: Algorithm,
|
||||
pub key_size: u32,
|
||||
pub ec_curve: Option<EcCurve>,
|
||||
pub rsa_public_exponent: u64,
|
||||
|
||||
pub attestation_challenge: Option<Vec<u8>>,
|
||||
pub purposes: Vec<i32>,
|
||||
pub digests: Vec<i32>,
|
||||
|
||||
pub cert_serial: Option<Vec<u8>>,
|
||||
pub cert_subject: Option<Vec<u8>>,
|
||||
pub cert_not_before: i64,
|
||||
pub cert_not_after: i64,
|
||||
|
||||
pub keybox_private_key: Vec<u8>,
|
||||
pub keybox_cert_chain: Vec<u8>,
|
||||
|
||||
pub security_level: i32,
|
||||
pub attest_version: i32,
|
||||
pub keymaster_version: i32,
|
||||
|
||||
pub os_version: i32,
|
||||
pub os_patch_level: i32,
|
||||
pub vendor_patch_level: i32,
|
||||
pub boot_patch_level: i32,
|
||||
|
||||
pub boot_key: Vec<u8>,
|
||||
pub boot_hash: Vec<u8>,
|
||||
|
||||
pub creation_datetime: i64,
|
||||
pub attestation_application_id: Vec<u8>,
|
||||
pub module_hash: Option<Vec<u8>>,
|
||||
|
||||
pub id_brand: Option<Vec<u8>>,
|
||||
pub id_device: Option<Vec<u8>>,
|
||||
pub id_product: Option<Vec<u8>>,
|
||||
pub id_serial: Option<Vec<u8>>,
|
||||
pub id_imei: Option<Vec<u8>>,
|
||||
pub id_meid: Option<Vec<u8>>,
|
||||
pub id_manufacturer: Option<Vec<u8>>,
|
||||
pub id_model: Option<Vec<u8>>,
|
||||
pub id_second_imei: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
pub struct GeneratedKeyPair {
|
||||
pub private_key_pkcs8: Vec<u8>,
|
||||
}
|
||||
Executable
+262
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build, package, deploy, and verify TEESimulator module ZIPs.
|
||||
# Usage: ./scripts/package.sh [flags]
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/package.sh --release # build release ZIP
|
||||
# ./scripts/package.sh --all --clean # clean build, both variants
|
||||
# ./scripts/package.sh --release --deploy --reboot # build, push, install, reboot
|
||||
# ./scripts/package.sh --deploy --verify # deploy latest ZIP + verify via logcat
|
||||
# ./scripts/package.sh --rust --release # build Rust crate first, then release
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
OUT_DIR="$PROJECT_ROOT/out"
|
||||
|
||||
VARIANT=""
|
||||
CLEAN=false
|
||||
DEPLOY=false
|
||||
REBOOT=false
|
||||
VERIFY=false
|
||||
BUILD_RUST=false
|
||||
CLEAR_KEYS=false
|
||||
TRACE=false
|
||||
ROOT_PROVIDER="ksu"
|
||||
|
||||
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
|
||||
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Build variants (pick one, or --all):
|
||||
--release Build release variant (default if none specified)
|
||||
--debug Build debug variant
|
||||
--all Build both debug and release
|
||||
|
||||
Build options:
|
||||
--clean Run gradle clean before building
|
||||
--rust Build native-certgen Rust crate before Gradle
|
||||
|
||||
Deploy options:
|
||||
--deploy Push ZIP to device and install
|
||||
--reboot Reboot device after install
|
||||
--clear-keys Clear persistent_keys before deploy
|
||||
--verify Run logcat verification after deploy
|
||||
--root PROVIDER Root provider: ksu (default), magisk, apatch
|
||||
|
||||
Misc:
|
||||
-v, --verbose Print every command as it runs (set -x)
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--release) VARIANT="release"; shift ;;
|
||||
--debug) VARIANT="debug"; shift ;;
|
||||
--all) VARIANT="all"; shift ;;
|
||||
--clean) CLEAN=true; shift ;;
|
||||
--deploy) DEPLOY=true; shift ;;
|
||||
--reboot) REBOOT=true; shift ;;
|
||||
--verify) VERIFY=true; shift ;;
|
||||
--rust) BUILD_RUST=true; shift ;;
|
||||
--clear-keys) CLEAR_KEYS=true; shift ;;
|
||||
-v|--verbose) TRACE=true; shift ;;
|
||||
--root) ROOT_PROVIDER="$2"; shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
*) red "Unknown flag: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$VARIANT" ]] && VARIANT="release"
|
||||
[[ "$TRACE" == true ]] && set -x
|
||||
|
||||
case "$ROOT_PROVIDER" in
|
||||
ksu) INSTALL_CMD="ksud module install" ;;
|
||||
magisk) INSTALL_CMD="magisk --install-module" ;;
|
||||
apatch) INSTALL_CMD="/data/adb/apd module install" ;;
|
||||
*) red "Unknown root provider: $ROOT_PROVIDER"; exit 1 ;;
|
||||
esac
|
||||
|
||||
build_rust() {
|
||||
local cargo_toml="$PROJECT_ROOT/native-certgen/Cargo.toml"
|
||||
if [[ ! -f "$cargo_toml" ]]; then
|
||||
red "native-certgen/Cargo.toml not found — skipping Rust build"
|
||||
return 0
|
||||
fi
|
||||
|
||||
bold "==> Building native-certgen (aarch64)"
|
||||
|
||||
if ! command -v cargo-ndk &>/dev/null; then
|
||||
red "cargo-ndk not found. Install: cargo install cargo-ndk"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(cd "$PROJECT_ROOT/native-certgen" && \
|
||||
cargo ndk -t arm64-v8a --platform 29 -- build --release)
|
||||
|
||||
local so="$PROJECT_ROOT/native-certgen/target/aarch64-linux-android/release/libcertgen.so"
|
||||
if [[ -f "$so" ]]; then
|
||||
local size
|
||||
size=$(du -h "$so" | cut -f1)
|
||||
green " libcertgen.so built ($size)"
|
||||
else
|
||||
red " libcertgen.so not found after build"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
gradle_build() {
|
||||
local tasks=()
|
||||
|
||||
[[ "$CLEAN" == true ]] && tasks+=(clean)
|
||||
|
||||
case "$VARIANT" in
|
||||
release) tasks+=(zipRelease) ;;
|
||||
debug) tasks+=(zipDebug) ;;
|
||||
all) tasks+=(zipDebug zipRelease) ;;
|
||||
esac
|
||||
|
||||
bold "==> Gradle: ${tasks[*]}"
|
||||
(cd "$PROJECT_ROOT" && ./gradlew "${tasks[@]}")
|
||||
}
|
||||
|
||||
find_latest_zip() {
|
||||
local pattern="$1"
|
||||
ls -t "$OUT_DIR"/$pattern 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
deploy_zip() {
|
||||
local zip="$1"
|
||||
local name
|
||||
name=$(basename "$zip")
|
||||
|
||||
if ! adb get-state &>/dev/null; then
|
||||
red "No ADB device connected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$CLEAR_KEYS" == true ]]; then
|
||||
bold "==> Clearing persistent_keys"
|
||||
adb shell "rm -rf /data/adb/tricky_store/persistent_keys/*" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
bold "==> Deploying $name"
|
||||
adb push "$zip" /data/local/tmp/module.zip
|
||||
adb shell "su -c '$INSTALL_CMD /data/local/tmp/module.zip'"
|
||||
green " Installed via $ROOT_PROVIDER"
|
||||
|
||||
if [[ "$REBOOT" == true ]]; then
|
||||
bold "==> Rebooting"
|
||||
adb reboot
|
||||
echo " Waiting for device..."
|
||||
adb wait-for-device
|
||||
sleep 10
|
||||
local pid
|
||||
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
|
||||
if [[ -n "$pid" ]]; then
|
||||
green " Daemon alive (PID $pid)"
|
||||
else
|
||||
yellow " Daemon not yet started — check logcat"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
verify_device() {
|
||||
bold "==> Verification"
|
||||
|
||||
if ! adb get-state &>/dev/null; then
|
||||
red "No ADB device connected"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local pid
|
||||
pid=$(adb shell "pidof TEESimulator" 2>/dev/null || true)
|
||||
if [[ -n "$pid" ]]; then
|
||||
green " Daemon: running (PID $pid)"
|
||||
else
|
||||
red " Daemon: not running"
|
||||
fi
|
||||
|
||||
local tee_status
|
||||
tee_status=$(adb shell "cat /data/adb/tricky_store/tee_status.txt" 2>/dev/null || echo "N/A")
|
||||
echo " TEE status: $tee_status"
|
||||
|
||||
local sec_patch
|
||||
sec_patch=$(adb shell "cat /data/adb/tricky_store/security_patch.txt" 2>/dev/null || echo "N/A")
|
||||
echo " Security patch config: $(echo "$sec_patch" | head -1)"
|
||||
|
||||
local errors
|
||||
errors=$(adb logcat -d -s TEESimulator 2>/dev/null | \
|
||||
grep -iE "error|exception" | \
|
||||
grep -v "StrongBox\|SurfaceRuntime\|ClassLoader\|HARDWARE_TYPE_UNAVAILABLE" | \
|
||||
wc -l)
|
||||
if [[ "$errors" -eq 0 ]]; then
|
||||
green " Logcat errors: 0"
|
||||
else
|
||||
yellow " Logcat errors: $errors (run: adb logcat -d -s TEESimulator | grep -iE 'error|exception')"
|
||||
fi
|
||||
|
||||
local throttle_events
|
||||
throttle_events=$(adb logcat -d -s TEESimulator 2>/dev/null | \
|
||||
grep -cE "RATE_LIMITED|CONCURRENT_LIMITED" || true)
|
||||
echo " Rate limit events: $throttle_events"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
echo ""
|
||||
bold "==> Build Summary"
|
||||
|
||||
local variants=()
|
||||
case "$VARIANT" in
|
||||
release) variants=(Release) ;;
|
||||
debug) variants=(Debug) ;;
|
||||
all) variants=(Debug Release) ;;
|
||||
esac
|
||||
|
||||
for v in "${variants[@]}"; do
|
||||
local zip
|
||||
zip=$(find_latest_zip "*-${v}.zip")
|
||||
if [[ -n "$zip" ]]; then
|
||||
local size
|
||||
size=$(du -h "$zip" | cut -f1)
|
||||
green " $v: $(basename "$zip") ($size)"
|
||||
else
|
||||
red " $v: ZIP not found"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
echo ""
|
||||
bold "TEESimulator-RS package pipeline"
|
||||
echo ""
|
||||
|
||||
[[ "$BUILD_RUST" == true ]] && build_rust
|
||||
|
||||
gradle_build
|
||||
print_summary
|
||||
|
||||
if [[ "$DEPLOY" == true ]]; then
|
||||
local_variant="$VARIANT"
|
||||
[[ "$local_variant" == "all" ]] && local_variant="release"
|
||||
|
||||
cap="${local_variant^}"
|
||||
zip=$(find_latest_zip "*-${cap}.zip")
|
||||
if [[ -z "$zip" ]]; then
|
||||
red "No $cap ZIP found to deploy"
|
||||
exit 1
|
||||
fi
|
||||
deploy_zip "$zip"
|
||||
fi
|
||||
|
||||
[[ "$VERIFY" == true ]] && verify_device
|
||||
|
||||
echo ""
|
||||
green "Done."
|
||||
+1
-1
@@ -14,7 +14,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "TEESimulator"
|
||||
rootProject.name = "TEESimulator-RS"
|
||||
|
||||
include(":stub")
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package android.os;
|
||||
|
||||
public class ServiceSpecificException extends RuntimeException {
|
||||
public final int errorCode;
|
||||
|
||||
public ServiceSpecificException(int errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ServiceSpecificException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user