Compare commits

...
46 Commits
Author SHA1 Message Date
Enginex0 d21822eb9d docs(release): bump to v4.3 with changelog and update metadata 2026-03-11 13:12:03 +01:00
Enginex0 095a658996 ci(build): fix pipeline trigger and release job gating
paths-ignore for .github/** was preventing workflow-only pushes
from triggering the pipeline at all. Release job was gated to
push events only, so workflow_dispatch never published. Simplify
paths-ignore to just **.md and allow both push and dispatch to
trigger the release job.
2026-03-11 13:03:35 +01:00
Enginex0 70e8968e44 ci(build): use mv instead of cp to avoid duplicate artifacts
cp left the original zip alongside the renamed copy, so the glob
matched both — doubling artifact size. mv removes the original.
2026-03-11 12:51:02 +01:00
Enginex0 c122ded7bf perf(daemon): add restart backoff, process priority, and map eviction
Supervisor had zero-delay restart on crash loops — pins CPU core at
100% if daemon keeps dying. Add exponential backoff (500ms to 30s cap,
resets after 30s stable). Set nice=10 on daemon child to yield CPU
to foreground apps. Evict stale entries from fileLocks and rate limiter
ConcurrentHashMaps that grew unbounded. Upload pre-built flashable zips
in CI instead of unpacking and re-compressing loose files.
2026-03-11 12:41:57 +01:00
Enginex0 7b510a9915 perf(logging): gate debug-level logs behind isDebugBuild
debug() was hitting Log.d() unconditionally in release builds —
every intercepted binder transaction triggered string formatting
and logcat syscalls. verbose() already had the guard; debug() was
just missing it. Also remove dead SERVICE_SLEEP_MS constant.
2026-03-11 12:41:46 +01:00
Enginex0 8f63dda31b ci(build): add release job with changelog and both ZIPs
The workflow only uploaded unzipped contents as CI artifacts —
no GitHub release was ever created from CI. Restructured into
build + release jobs: build produces both debug and release ZIPs
(renamed to clean `TEESimulator-vX.Y-{Variant}.zip` format),
release extracts changelog from module/changelog.md and publishes
a GitHub release with both ZIPs attached.
2026-03-11 04:06:20 +01:00
Enginex0 9896df93de build(gradle): keep debug symbols in debug variant
Debug ZIPs now ship unstripped native libs sourced from
merged_native_libs instead of stripped_native_libs. Gives
meaningful stack traces for crash debugging on-device.
2026-03-11 00:45:00 +01:00
Enginex0 0280bcf189 docs(readme): add build badge and building-from-source section 2026-03-11 00:28:28 +01:00
Enginex0 438a462bdf ci(build): add Rust toolchain and cargo-ndk for native-certgen
Gradle's buildRustCertgen task requires cargo-ndk and Android NDK
targets to cross-compile libcertgen.so. Without these, CI fails on
any commit after 32cfcb3 which wired the Rust crate into the pipeline.
2026-03-11 00:12:02 +01:00
Enginex0 40b08cd648 docs(release): bump to v4.2 with changelog and update metadata 2026-03-10 16:55:39 +01:00
Enginex0 c5ed627f68 chore(module): bump versionCode to 95 2026-03-10 16:37:58 +01:00
Enginex0 bee73eb39b perf(binder): skip interception for system transaction codes
AIDL methods use codes 1..0x00ffffff. System transactions like
PING_TRANSACTION (0x5f4e4750) fall above that range. Intercepting
pings forces a full JNI round-trip to Java and back, adding enough
latency for timing detectors to flag the ratio (3.85x vs 3.0x
threshold). Early-return for codes above LAST_CALL_TRANSACTION
eliminates this overhead while preserving all AIDL interception.
2026-03-10 16:37:50 +01:00
Enginex0 a0ee77202c fix(attestation): correct leaf CN casing and enforce keystore2 parameter policy
Leaf cert Subject CN used "KeyStore" (capital S) but AOSP
KeyGenParameterSpec uses "Keystore" (lowercase s). Fixed in both
the Rust native certgen and BouncyCastle paths.

Replicate keystore2's security_level.rs parameter validation for
software-generated keys: reject CREATION_DATETIME (output-only tag,
ResponseCode 20) and device ID attestation tags (CANNOT_ATTEST_IDS
-66) that real keystore2 blocks before they reach the HAL.

Also fix createErrorReply parcel write order — AIDL protocol expects
exception_code, message, error_code but we had message and error_code
swapped, causing malformed replies for positive error codes.
2026-03-10 14:23:33 +01:00
Enginex0 09d9896228 docs(release): bump to v4.1 with changelog and update metadata
versionCode=94 matches post-commit count.
2026-03-10 13:00:57 +01:00
Enginex0 5a599025ad fix(attestation): persist vbmeta boot key and hash across reboots
resetprop overrides for ro.boot.* props don't survive reboots. On
devices where the kernel doesn't set ro.boot.vbmeta.public_key_digest,
the fallback chain hit random generation on every boot — producing a
different RootOfTrust hash each time.

Added file-based persistence (boot_hash.bin, boot_key.bin) as a
fallback layer between TEE cache and random generation. Once a value
is determined from any source, it's written to disk and reused on
subsequent boots.

Verified on Redmi 14C: second boot reads from persistent file instead
of regenerating random bytes.
2026-03-10 12:58:49 +01:00
Enginex0 f5c2bcc024 fix(module): align update.json versionCode with release ZIP
Release ZIP was built at 89 commits (versionCode=89) but update.json
had versionCode=90, causing an infinite update loop in KSU Manager.
2026-03-10 04:40:53 +01:00
Enginex0 85e34ed42f docs(release): write v4.0 changelog and update module metadata
Native Rust cert gen release. Points update.json to fork URLs.
2026-03-09 22:48:20 +01:00
Enginex0 52b04675e7 docs(readme): rewrite README for personal fork
Matches ZeroMount style — badges, feature checklists, compatibility
tables, config docs. Clarifies this is a fork of JingMatrix/TEESimulator.
2026-03-09 22:17:38 +01:00
Enginex0 ba0628c687 fix(attestation): reject oversized challenges and rewrite cert DER encoding
DuckDetector flagged two issues:
1. Oversized challenge accepted — 256-byte attestation challenge should
   return INVALID_INPUT_LENGTH (-21) like real KeyMint. Added early check
   in handleGenerateKey before any path decision.
2. Issuer/subject chain mismatch — rcgen's HashMap loses DN attribute
   ordering and converts PrintableString to UTF8String, producing
   different DER bytes. Replaced rcgen with manual DER assembly that
   injects raw keybox issuer_dn_der bytes directly.

Verified on device: TX_ID 315 rejects 256-byte challenge, TX_ID 501
generates valid 4-cert chain with correct issuer linkage.
2026-03-09 21:52:06 +01:00
Enginex0 c11465d660 chore(build): bump to v4.0, update module metadata and add package script
Native certgen integration milestone. Adds action.sh/uninstall.sh to
customize.sh extraction loop, points update.json to fork, includes
build/deploy helper script.
2026-03-09 21:51:48 +01:00
Enginex0 0d5fd44992 fix(attestation): null out all-zero verifiedBootHash from TEE cache
Matches the existing verifiedBootKey null-zero guard. When the TEE
returns a zeroed hash, fall through to the system property or random
fallback instead of embedding a detectable all-zero value.
2026-03-09 20:00:14 +01:00
Enginex0 776a7b2343 feat(interception): override pre-existing attest keys, skip GMS list hooking
Two changes to Keystore2Interceptor:

1. Hardware attest keys created before TEESimulator loads now get
   detected in the getKeyEntry post-hook via isAttestKey(). A software
   replacement keypair is generated, cached, and persisted so the
   unpatched hardware chain is never served.

2. GMS calls listEntries frequently. Skip the post-hook injection
   for com.google.android.gms to reduce log flooding and unnecessary
   key merging work.

Also adds null-alias guard in onPreTransact to avoid NPE on keys
looked up by domain/nspace without an alias.
2026-03-09 19:59:51 +01:00
Enginex0 14786ecce0 feat(attestation): add origin field and isAttestKey/isImportKey helpers
Parse ORIGIN tag from KeyParameter array into KeyMintAttestation
data class. Add isAttestKey() and isImportKey() convenience methods
to consolidate purpose/origin checks scattered across interceptors.
2026-03-09 19:59:38 +01:00
Enginex0 41b9cc8f10 fix(attestation): correct module_hash to match AOSP Keystore2
BouncyCastle DERSet() sorts by full encoded sequence, but AOSP
keystore2 maintenance.rs sorts by encoded name only. Replace
PackageManager-based APEX enumeration with filesystem scan of
/apex/ directories using a minimal protobuf parser for
apex_manifest.pb. Encode the DER SET tag manually to preserve
the name-only sort order.
2026-03-09 19:59:30 +01:00
Enginex0 6df266b688 fix(native-certgen): address production audit findings
Make logging init idempotent (swallow SetGlobalDefaultError on repeat
call), remove unused dumpLogs JNI params that violated the API contract,
and strip dead public_key_spki field + build_ec_spki() that were
computed on every keygen but never consumed by the cert builder.
2026-03-09 18:16:50 +01:00
Enginex0 32cfcb3ece build(native-certgen): wire Rust crate into Gradle pipeline
cargo-ndk builds libcertgen.so for arm64-v8a during prepareModuleFiles.
AGP mergeJniLibFolders picks up jniLibs/ and routes through
stripped_native_libs into the module ZIP. customize.sh extracts the .so
on device install. ProGuard keeps NativeCertGen JNI class and
CertGenConfig fields for runtime JNI field access.
2026-03-09 16:46:32 +01:00
Enginex0 727b32f6b0 fix(pki): align JNI signatures between Kotlin and Rust
initLogging now takes logDir param matching Rust entry point.
dumpLogs takes logDir+baseDir params matching Rust. Removed unused
generateSoftwareKeyPair declaration. Added buffer bounds checks in
parseNativeResult to prevent OOM on malformed native output.
2026-03-09 16:37:52 +01:00
Enginex0 ac9641ed2a feat(pki): integrate native cert gen with BouncyCastle fallback
NativeCertGen.kt provides CertGenConfig data class and JNI bridge
to libcertgen.so. KeyMintSecurityLevelInterceptor.doSoftwareKeyGen()
tries native path first, falls back to BouncyCastle on failure or
when library unavailable. App.kt loads libcertgen.so at daemon start.
2026-03-09 16:23:09 +01:00
Enginex0 c87759c6c3 feat(native-certgen): implement JNI bridge with panic-safe entry points
Three JNI exports: generateAttestedKeyPair (orchestrates keygen,
attestation, certbuilder, returns length-prefixed binary),
initLogging (multi-output tracing setup), dumpLogs (diagnostic ZIP).
CertGenConfig extraction via typed JNI field accessors. catch_unwind
on all FFI boundaries.
2026-03-09 16:13:41 +01:00
Enginex0 76b18706f1 fix(native-certgen): address Phase 3 validation findings
Remove ENCRYPT/VERIFY from KeyUsage mapping to match Kotlin behavior.
Document BasicConstraints and SKI suppression via rcgen NoCa default.
Fix rotating log off-by-one that kept one extra backup file. Handle
BMPString (UTF-16BE) and VisibleString in X.500 DN parser.
2026-03-09 16:08:07 +01:00
Enginex0 e76f5115e0 feat(native-certgen): implement X.509 certificate chain builder
Builds v3 leaf certificate with attestation extension and KeyUsage,
signs with keybox private key via rcgen 0.13.2. Assembles full chain
(leaf + keybox intermediates + root). Supports EC and RSA keybox
signing keys. Uses rcgen's signed_by() with a synthesized issuer
Certificate — no manual DER fallback needed.
2026-03-09 15:55:15 +01:00
Enginex0 0725aed094 feat(native-certgen): implement logging subsystem
Multi-output logging via tracing: /dev/kmsg for logcat, rotating file
appender (512KB, 3 files), stderr for debug. Diagnostic ZIP dump with
log files and TEE status snapshots. Verbose toggle via JNI flag or
.verbose marker file.
2026-03-09 15:41:46 +01:00
Enginex0 9d61af6ce8 feat(native-certgen): implement ASN.1 attestation extension encoder
DER encoder for Android KeyMint attestation extension (OID
1.3.6.1.4.1.11129.2.1.17). SecurityLevel and VerifiedBootState as
ENUMERATED, EXPLICIT context-specific tagging with long-form for
tags >= 31, sorted AuthorizationList fields, RootOfTrust with
BOOLEAN TRUE=0xFF, SET OF INTEGER with DER sort, DO_NOT_REPORT
sentinel omission.
2026-03-09 15:32:31 +01:00
Enginex0 7863e8dd17 fix(native-certgen): address Phase 0-1 validation findings
EC keygen now returns proper SPKI DER instead of raw point bytes.
RSA keygen uses caller-supplied exponent via new_with_exp() and
validates key size to 2048/3072/4096. Keybox parser extracts leaf
subject DN (not issuer). Added AttestKey=7 to KeyPurpose. Realigned
error variants with spec.
2026-03-09 15:21:33 +01:00
Enginex0 f840eed42b feat(native-certgen): implement keybox DER certificate chain parser
Splits concatenated DER cert chains into individual certificates,
extracts leaf issuer DN and notAfter via x509-cert crate. Handles
multi-byte DER length encoding (0x81-0x84).
2026-03-09 15:09:29 +01:00
Enginex0 4c7f0a09ea feat(native-certgen): scaffold Rust crate with foundation types and keygen
Cargo.toml with 16 dependencies per build spec, error types with
From impls for all upstream error types, CertGenParams mapping the
full JNI config contract, EC/RSA key generation via ring and rsa crates.

Compiles clean for aarch64-linux-android via cargo-ndk.
2026-03-09 15:05:25 +01:00
Enginex0 4971f7b4a5 Derive boot and vendor patch levels from system prop when system=prop
TrickyAddon fetches Pixel bulletin dates for boot/vendor but system=prop
resolves to the real device prop, creating a cross-component date mismatch
on non-Pixel devices. Force all three through the same prop resolution path.
2026-02-07 00:47:53 +01:00
Enginex0 9ea39f0545 Rate-limit per-UID hardware keygen and harden importKey eviction
Sliding window limits each UID to 2 hardware generateKey calls per
30s burst window with max 2 concurrent. Overflow falls back to
software cert generation.

importKey post-hook retains patched chains instead of full eviction,
preventing detectors from using generate-then-import to bypass
attestation patching. getKeyEntry serves retained chains for imported
keys that overwrote attested aliases.
2026-02-07 00:47:47 +01:00
Enginex0 c332f8ad0d Cap interceptable binder payload size at 256KB
Prevents thread starvation from flood attacks targeting the
binder interceptor with oversized payloads.
2026-02-07 00:47:42 +01:00
Enginex0 d76e6abeed Add file-level locking to prevent race conditions in key persistence
Per-key ReentrantLock prevents concurrent writes to same key file
2026-02-07 00:47:36 +01:00
Enginex0 142fe7bf13 Reject oversized aliases to prevent binder buffer exhaustion
MAX_ALIAS_LENGTH (256KB) with 4x safety margin for transaction overhead
2026-02-07 00:47:31 +01:00
Enginex0 e41a679928 fix(pki): strip HTML comments from PEM blocks before parsing
Some upstream keybox sources inject HTML comments inside PEM
certificate blocks. BouncyCastle's PEMParser chokes on these
non-base64 lines, silently failing to load the keybox.

Filter lines starting with <!-- in trimLines() before the content
reaches the PEM parser.
2026-02-07 00:47:26 +01:00
Enginex0 209d5c8902 fix(config): prevent FileObserver NPE on config file deletion
When a config file is deleted, the event handler sets file=null but
then force-unwraps it with file!! in the when block, crashing the
FileObserver thread. All subsequent config change notifications are
silently lost.

Replace force-unwrap with safe call, log a warning on deletion.
2026-02-07 00:47:20 +01:00
Enginex0 3817b37e18 Integrate key persistence with interceptors
Save keys on generation, restore on daemon startup, delete on cleanup.
Re-persist when cert chain updates via updateSubcomponents.
2026-02-07 00:47:16 +01:00
Enginex0 53ae250fc7 Add generated key persistence layer
Persist GENERATE-mode keys to disk so they survive daemon restarts.
Binary format with version header, atomic write via tmp+rename.
2026-02-07 00:47:10 +01:00
Enginex0 34ad97366e feat(module): add supervisor daemon with leak-safe restart and lifecycle scripts
Fork-based supervisor ensures the interceptor process survives crashes.
pingBinder() liveness check on pre-transact returns DEAD_OBJECT to
callers when interceptor is down, preventing real TEE state from leaking
during the restart window.

action.sh clears persistent key storage via KSU Action button.
uninstall.sh kills daemon processes and removes module artifacts while
preserving target.txt and keybox configuration.
2026-02-07 00:47:04 +01:00
46 changed files with 5606 additions and 385 deletions
+95 -68
View File
@@ -3,16 +3,10 @@ name: Build
on: on:
push: push:
branches: [ "main" ] branches: [ "main" ]
paths-ignore: paths-ignore: [ '**.md' ]
- '**.md'
- '.github/**'
- '!.github/workflows/**'
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
paths-ignore: paths-ignore: [ '**.md' ]
- '**.md'
- '.github/**'
- '!.github/workflows/**'
workflow_dispatch: workflow_dispatch:
concurrency: concurrency:
@@ -22,18 +16,9 @@ concurrency:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
attestations: write
contents: read
outputs:
releaseName: ${{ steps.prepareArtifact.outputs.releaseName }}
debugName: ${{ steps.prepareArtifact.outputs.debugName }}
steps: steps:
- name: Check out - uses: actions/checkout@v4
uses: actions/checkout@v4
with: with:
submodules: "recursive" submodules: "recursive"
fetch-depth: 0 fetch-depth: 0
@@ -45,6 +30,25 @@ jobs:
java-version: 21 java-version: 21
cache: 'gradle' 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 - name: Set up ccache
uses: hendrikmuhs/ccache-action@v1.2 uses: hendrikmuhs/ccache-action@v1.2
with: with:
@@ -60,73 +64,96 @@ jobs:
- name: Build with Gradle - name: Build with Gradle
run: | run: |
chmod +x ./gradlew chmod +x ./gradlew
./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m ./gradlew zipRelease zipDebug -Porg.gradle.parallel=true -Porg.gradle.vfs.watch=true -Dorg.gradle.jvmargs=-Xmx2048m
- name: Prepare artifact - name: Read version
if: success() id: ver
id: prepareArtifact run: |
ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "version=${ver}" >> "$GITHUB_OUTPUT"
- name: Rename ZIPs for release
run: | run: |
set -e
RELEASE_FILE=$(find out -name "*Release*.zip" | head -1) RELEASE_FILE=$(find out -name "*Release*.zip" | head -1)
DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1) DEBUG_FILE=$(find out -name "*Debug*.zip" | head -1)
if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then if [[ -z "$RELEASE_FILE" || -z "$DEBUG_FILE" ]]; then
echo "Error: Could not find release or debug files in out/" echo "::error::Could not find release or debug ZIPs in out/"
echo "Contents of out/ directory:" ls -la out/ || echo "out/ does not exist"
ls -la out/ || echo "out/ directory does not exist"
exit 1 exit 1
fi fi
# Extract names
RELEASE_NAME=$(basename "$RELEASE_FILE" .zip)
DEBUG_NAME=$(basename "$DEBUG_FILE" .zip)
echo "releaseName=$RELEASE_NAME" >> $GITHUB_OUTPUT
echo "debugName=$DEBUG_NAME" >> $GITHUB_OUTPUT
mkdir -p module-release module-debug mv "$RELEASE_FILE" "out/TEESimulator-${VER}-Release.zip"
unzip -q "$RELEASE_FILE" -d module-release mv "$DEBUG_FILE" "out/TEESimulator-${VER}-Debug.zip"
unzip -q "$DEBUG_FILE" -d module-debug
echo " Release: $RELEASE_NAME"
echo " Debug: $DEBUG_NAME"
- name: Upload release echo "Release: TEESimulator-${VER}-Release.zip ($(du -h "out/TEESimulator-${VER}-Release.zip" | cut -f1))"
if: success() echo "Debug: TEESimulator-${VER}-Debug.zip ($(du -h "out/TEESimulator-${VER}-Debug.zip" | cut -f1))"
id: release env:
uses: actions/upload-artifact@v4 VER: ${{ steps.ver.outputs.version }}
- uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.prepareArtifact.outputs.releaseName }} name: TEESimulator-release-zip
path: "./module-release/*" path: out/TEESimulator-*-Release.zip
retention-days: 30 retention-days: 30
compression-level: 6 compression-level: 0
- name: Upload debug - uses: actions/upload-artifact@v4
if: success()
id: debug
uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.prepareArtifact.outputs.debugName }} name: TEESimulator-debug-zip
path: "./module-debug/*" path: out/TEESimulator-*-Debug.zip
retention-days: 7 retention-days: 7
compression-level: 6 compression-level: 0
- name: Upload release mappings - uses: actions/upload-artifact@v4
if: success()
uses: actions/upload-artifact@v4
with: with:
name: release-mappings-${{ github.run_number }} name: release-mappings
path: "./app/build/outputs/mapping/release" path: app/build/outputs/mapping/release
retention-days: 30 retention-days: 30
compression-level: 9 compression-level: 9
- name: Summary release:
if: always() 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
- name: Read version
id: ver
run: | run: |
echo "## Build Summary" >> $GITHUB_STEP_SUMMARY ver=$(grep 'val verName' app/build.gradle.kts | sed 's/.*"\(.*\)".*/\1/')
echo "- **Status**: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY echo "version=${ver}" >> "$GITHUB_OUTPUT"
echo "- **Gradle Tasks**: assembleRelease, assembleDebug" >> $GITHUB_STEP_SUMMARY
if [[ "${{ job.status }}" == "success" ]]; then - uses: actions/download-artifact@v4
echo "- **Release Artifact**: ${{ steps.prepareArtifact.outputs.releaseName }}" >> $GITHUB_STEP_SUMMARY with:
echo "- **Debug Artifact**: ${{ steps.prepareArtifact.outputs.debugName }}" >> $GITHUB_STEP_SUMMARY name: TEESimulator-release-zip
fi path: zips
- uses: actions/download-artifact@v4
with:
name: TEESimulator-debug-zip
path: zips
- name: Extract changelog
run: |
ver="${VER#v}"
awk "/^## TEESimulator v${ver}/{flag=1; next} /^## TEESimulator 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
gh release create "$VER" \
--title "$VER" \
--latest \
--notes-file /tmp/notes.md \
"zips/TEESimulator-${VER}-Release.zip" \
"zips/TEESimulator-${VER}-Debug.zip"
env:
VER: ${{ steps.ver.outputs.version }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+191 -75
View File
@@ -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. ## 🧬 What is TEESimulator?
* **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.
## 📱 Requirements 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.
- Android 10 or above
## 📦 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. > **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.
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!
**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 ### 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
<?xml version="1.0"?> <?xml version="1.0"?>
@@ -40,101 +151,106 @@ This file provides the master cryptographic identity for the simulator. It conta
</AndroidAttestation> </AndroidAttestation>
``` ```
### Mode and Keybox Configuration (`target.txt`) ### Target Packages (`target.txt`)
TEESimulator currently operates in two primary modes as it transitions towards full emulation. Controls which apps get intercepted and what simulation mode to use.
You can control the simulation mode and the specific keybox.xml file used on a per-package basis.
#### Mode Suffixes #### Mode Suffixes
* **`!` → Force Generation Mode:** Creates a complete, software-based virtual key. This is the foundation of the full TEE simulation. * **`!` → Force Generation** Creates a complete software-based virtual key. 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. * **`?` → Force Leaf Hacking** — Real TEE key generated, but its attestation certificate is intercepted and patched.
* **No symbol → Automatic Mode:** The module selects the most appropriate mode for the device. * **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! com.google.android.gms!
io.github.vvb2060.keyattestation? io.github.vvb2060.keyattestation?
# Switch to a different keybox for the following apps. # Switch to a different keybox for the following apps
# The file must be located at /data/adb/tricky_store/aosp_keybox.xml
[aosp_keybox.xml] [aosp_keybox.xml]
com.google.android.gsf com.google.android.gsf
# Switch again to another keybox. # Another keybox
# The file must be located at /data/adb/tricky_store/demo_keybox.xml
[demo_keybox.xml] [demo_keybox.xml]
org.matrix.demo org.matrix.demo
``` ```
### Security Patch Level (`security_patch.txt`) ### 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. | Key | Scope |
* 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. |---|---|
| `system` | OS patch level |
#### Configuration Keys and Values | `vendor` | Vendor patch level |
| `boot` | Boot/kernel patch level |
You can specify the patch level for the following components using a `key=value` format: | `all` | Shorthand — sets all three at once |
* `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`).
#### Special Keywords #### 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. #### Example
* **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.
``` ```
# --- Global Configuration --- # Global — default for all apps
# 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.
system=YYYY-MM-05 system=YYYY-MM-05
vendor=device_default vendor=device_default
boot=no boot=no
# --- Per-Package Override for Google Play Services --- # Override for GMS
# 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).
[com.google.android.gms] [com.google.android.gms]
system=2024-10-01 system=2024-10-01
# --- Per-Package Override for a Demo App --- # Custom config for a demo app
# This app gets a completely custom configuration.
[org.matrix.demo] [org.matrix.demo]
# Set a base date for all patch levels...
all=2025-09-15 all=2025-09-15
# ...but make an exception: use the real boot patch level instead of the one from 'all'.
boot=device_default 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>
+40 -9
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt() val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir) val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v3.1" val verName = "v4.3"
android { android {
namespace = "org.matrix.TEESimulator" namespace = "org.matrix.TEESimulator"
@@ -71,6 +71,35 @@ dependencies {
implementation(libs.bcpkix) implementation(libs.bcpkix)
} }
// --- Rust native cert gen build task ---
val buildRustCertgen by tasks.registering(Exec::class) {
group = "TEESimulator 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 { androidComponents {
onVariants(selector().all()) { variant -> onVariants(selector().all()) { variant ->
val capitalized = variant.name.replaceFirstChar { it.uppercase() } val capitalized = variant.name.replaceFirstChar { it.uppercase() }
@@ -92,8 +121,9 @@ androidComponents {
dependsOn("package${capitalized}") dependsOn("package${capitalized}")
} else { } else {
dependsOn("minify${capitalized}WithR8") dependsOn("minify${capitalized}WithR8")
dependsOn("strip${capitalized}DebugSymbols")
} }
dependsOn("strip${capitalized}DebugSymbols") dependsOn(buildRustCertgen)
if (isDebug) { if (isDebug) {
from(variant.artifacts.get(SingleArtifact.APK)) { from(variant.artifacts.get(SingleArtifact.APK)) {
@@ -110,13 +140,14 @@ androidComponents {
} }
} }
from( val nativeLibsDir = if (isDebug) {
project.layout.buildDirectory.dir( "intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib"
"intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/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. from(project.layout.buildDirectory.dir(nativeLibsDir)) {
include("**/libinject.so", "**/libTEESimulator.so") into("lib")
include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so")
} }
// Now, copy and process the files from 'module' directory. // Now, copy and process the files from 'module' directory.
+6
View File
@@ -7,3 +7,9 @@
-keepclasseswithmembers class org.matrix.TEESimulator.App { -keepclasseswithmembers class org.matrix.TEESimulator.App {
public static void main(java.lang.String[]); public static void main(java.lang.String[]);
} }
-keepclasseswithmembers class org.matrix.TEESimulator.pki.NativeCertGen {
native <methods>;
*;
}
-keep class org.matrix.TEESimulator.pki.CertGenConfig { *; }
+3
View File
@@ -22,6 +22,9 @@ add_executable(libinject.so inject/main.cpp inject/utils.cpp)
target_include_directories(libinject.so PUBLIC include) target_include_directories(libinject.so PUBLIC include)
target_link_libraries(libinject.so PRIVATE lsplt_static) 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) add_library(${CMAKE_PROJECT_NAME} SHARED binder_interceptor.cpp)
target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include) target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC external/linux-kernel/include include)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE binder lsplt_static utils)
+24 -9
View File
@@ -348,15 +348,22 @@ static sp<BinderStub> g_stub_instance = nullptr;
namespace { namespace {
/** constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024;
* @brief Analyses a binder transaction. If the target is monitored,
* hijacks the transaction by rewriting its destination to our BinderStub.
* @param txn_data Pointer to the transaction data within the ioctl buffer.
*/
void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { void inspectAndRewriteTransaction(binder_transaction_data *txn_data) {
if (!txn_data || txn_data->target.ptr == 0) if (!txn_data || txn_data->target.ptr == 0)
return; return;
// Bypass interception for oversized payloads to prevent thread starvation from flood attacks
if (txn_data->data_size > kMaxInterceptableDataSize)
return;
// AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff).
// 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; bool hijack = false;
ThreadTransactionInfo info; ThreadTransactionInfo info;
@@ -592,9 +599,16 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sp<BBinder
Parcel pre_req, pre_resp; Parcel pre_req, pre_resp;
writeTransactionData(pre_req, tx_id, target, code, flags, request); writeTransactionData(pre_req, tx_id, target, code, flags, request);
if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) { status_t pre_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp);
LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id); if (pre_status != OK) {
return false; // Callback failed, proceed as if not intercepted // 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(); 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)); 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(); int32_t post_action = post_resp.readInt32();
if (post_action == intercept::kActionOverrideReply && reply) { if (post_action == intercept::kActionOverrideReply && reply) {
result = post_resp.readInt32(); // Read new status result = post_resp.readInt32(); // Read new status
+76
View File
@@ -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.Keystore2Interceptor
import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils import org.matrix.TEESimulator.util.AndroidDeviceUtils
/** /**
@@ -22,8 +23,6 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils
object App { object App {
// The delay in milliseconds before retrying to initialize the interceptor. // The delay in milliseconds before retrying to initialize the interceptor.
private const val RETRY_DELAY_MS = 1000L 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. * The main entry point of the TEESimulator application.
@@ -51,6 +50,8 @@ object App {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider()) Security.addProvider(BouncyCastleProvider())
NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so")
// This starts the message queue processing. It blocks here indefinitely // This starts the message queue processing. It blocks here indefinitely
// processing messages until Looper.myLooper().quit() is called. // processing messages until Looper.myLooper().quit() is called.
Looper.loop() Looper.loop()
@@ -112,6 +112,7 @@ object AttestationBuilder {
} }
val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid) val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid)
SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch")
properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] = properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] =
if (bootPatch != DO_NOT_REPORT) { if (bootPatch != DO_NOT_REPORT) {
DERTaggedObject( DERTaggedObject(
@@ -344,7 +345,7 @@ object AttestationBuilder {
* retrieved. * retrieved.
*/ */
@Throws(Throwable::class) @Throws(Throwable::class)
private fun createApplicationId(uid: Int): DEROctetString { internal fun createApplicationId(uid: Int): DEROctetString {
val pm = val pm =
ConfigurationManager.getPackageManager() ConfigurationManager.getPackageManager()
?: throw IllegalStateException("PackageManager not found!") ?: throw IllegalStateException("PackageManager not found!")
@@ -249,6 +249,10 @@ object DeviceAttestationService {
verifiedBootKey = null verifiedBootKey = null
} }
if (verifiedBootHash?.all { it == 0.toByte() } == true) {
verifiedBootHash = null
}
SystemLogger.info( 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()}" "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 package org.matrix.TEESimulator.attestation
import android.hardware.security.keymint.* import android.hardware.security.keymint.*
import android.hardware.security.keymint.KeyOrigin
import java.math.BigInteger import java.math.BigInteger
import java.util.Date import java.util.Date
import javax.security.auth.x500.X500Principal import javax.security.auth.x500.X500Principal
@@ -20,6 +21,7 @@ data class KeyMintAttestation(
val algorithm: Int, val algorithm: Int,
val ecCurve: Int, val ecCurve: Int,
val ecCurveName: String, val ecCurveName: String,
val origin: Int?,
val blockMode: List<Int>, val blockMode: List<Int>,
val padding: List<Int>, val padding: List<Int>,
val purpose: List<Int>, val purpose: List<Int>,
@@ -54,6 +56,9 @@ data class KeyMintAttestation(
ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0, ecCurve = params.findEcCurve(Tag.EC_CURVE) ?: 0,
ecCurveName = params.deriveEcCurveName(), ecCurveName = params.deriveEcCurveName(),
// AOSP: [key_param(tag = ORIGIN, field = Origin)]
origin = params.findOrigin(Tag.ORIGIN),
// AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)] // AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)]
blockMode = params.findAllBlockMode(Tag.BLOCK_MODE), blockMode = params.findAllBlockMode(Tag.BLOCK_MODE),
@@ -99,6 +104,10 @@ data class KeyMintAttestation(
// Log all parsed parameters for debugging purposes. // Log all parsed parameters for debugging purposes.
params.forEach { KeyMintParameterLogger.logParameter(it) } 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 --- // --- 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? = private fun Array<KeyParameter>.findEcCurve(tag: Int): Int? =
this.find { it.tag == tag }?.value?.ecCurve 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 */ /** Maps to AOSP field = LongInteger */
private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? = private fun Array<KeyParameter>.findLongInteger(tag: Int): BigInteger? =
this.find { it.tag == tag }?.value?.longInteger?.toBigInteger() this.find { it.tag == tag }?.value?.longInteger?.toBigInteger()
@@ -253,7 +253,14 @@ object ConfigurationManager {
} }
// Parse global and per-package configurations. // 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 contextLines.remove("") // Remove global context to iterate over packages next
for ((pkg, lines) in contextLines) { for ((pkg, lines) in contextLines) {
@@ -307,8 +314,10 @@ object ConfigurationManager {
val file = if (event != DELETE) File(configRoot, path) else null val file = if (event != DELETE) File(configRoot, path) else null
when (path) { when (path) {
TARGET_PACKAGES_FILE -> loadTargetPackages(file!!) TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) }
PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!) ?: 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. // Any change to an XML file is assumed to be a keybox.
// The cache in KeyBoxManager will handle reloading it on its next use. // The cache in KeyBoxManager will handle reloading it on its next use.
else -> else ->
@@ -12,6 +12,17 @@ data class KeyIdentifier(val uid: Int, val alias: String)
/** A collection of utility functions to support binder interception. */ /** A collection of utility functions to support binder interception. */
object InterceptorUtils { 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(errorCode)
}
return BinderInterceptor.TransactionResult.OverrideReply(parcel)
}
/** /**
* Uses reflection to get the integer transaction code for a given method name from a Stub * 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. * class. This is necessary for older Android versions where codes are not public constants.
@@ -1,21 +1,23 @@
package org.matrix.TEESimulator.interception.keystore package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcel import android.os.Parcel
import android.system.keystore2.IKeystoreService import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse import android.system.keystore2.KeyEntryResponse
import java.security.SecureRandom
import java.security.cert.Certificate import java.security.cert.Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager 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.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper import org.matrix.TEESimulator.pki.CertificateHelper
/** /**
@@ -73,6 +75,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val interceptor = val interceptor =
KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT)
register(backdoor, tee.asBinder(), interceptor) register(backdoor, tee.asBinder(), interceptor)
interceptor.loadPersistedKeys()
} }
} }
.onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) } .onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) }
@@ -84,6 +87,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val interceptor = val interceptor =
KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX) KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX)
register(backdoor, strongbox.asBinder(), interceptor) register(backdoor, strongbox.asBinder(), interceptor)
interceptor.loadPersistedKeys()
} }
} }
.onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) }
@@ -99,10 +103,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { 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 TransactionResult.ContinueAndSkipPost
}
return runCatching { return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
@@ -137,7 +145,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost ?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}") if (descriptor.alias != null) {
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
} else {
SystemLogger.info(
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
)
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias) val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) { if (code == DELETE_KEY_TRANSACTION) {
@@ -207,71 +222,124 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
TransactionResult.SkipTransaction TransactionResult.SkipTransaction
} }
} else if (code == GET_KEY_ENTRY_TRANSACTION) { } else if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val keyDescriptor = val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction ?: return TransactionResult.SkipTransaction
logTransaction(
txId,
"post-${transactionNames[code]!!} ${keyDescriptor.alias}",
callingUid,
callingPid,
)
if (!ConfigurationManager.shouldPatch(callingUid)) if (!ConfigurationManager.shouldPatch(callingUid))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}") runCatching {
return try { val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
val response = val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
reply.readTypedObject(KeyEntryResponse.CREATOR)
?: return TransactionResult.SkipTransaction
reply.setDataPosition(0) // Reset for potential reuse.
val originalChain = CertificateHelper.getCertificateChain(response) val authorizations = response.metadata.authorizations
val authorizations = response.metadata?.authorizations val parsedParameters =
val origin = KeyMintAttestation(
authorizations authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
?.find { it.keyParameter.tag == Tag.ORIGIN } )
?.let { it.keyParameter.value.origin }
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) { if (parsedParameters.isImportKey()) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported keys.") val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
return TransactionResult.SkipTransaction 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,
purposes = parsedParameters.purpose,
digests = parsedParameters.digest,
isAttestationKey = true,
)
return InterceptorUtils.createTypedObjectReply(response)
}
val originalChain = CertificateHelper.getCertificateChain(response)
if (originalChain == null || originalChain.size < 2) {
SystemLogger.info(
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
)
return TransactionResult.SkipTransaction
}
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
.getOrThrow()
return InterceptorUtils.createTypedObjectReply(response)
} }
.onFailure {
if (originalChain == null || originalChain.size < 2) { SystemLogger.error(
SystemLogger.info( "[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}." it,
) )
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
// Perform the attestation patch.
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
// First, try to retrieve the already-patched chain from our cache to ensure
// consistency.
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
// If no chain is cached (e.g., key existed before simulator started),
// perform a live patch as a fallback. This may still be detectable.
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
TransactionResult.SkipTransaction
}
} }
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
@@ -290,6 +358,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
metadata.certificate = publicCert metadata.certificate = publicCert
metadata.certificateChain = certificateChain metadata.certificateChain = certificateChain
GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo)
SystemLogger.verbose( SystemLogger.verbose(
"Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]" "Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]"
) )
@@ -407,8 +407,9 @@ private data class LegacyKeygenParameters(
return KeyMintAttestation( return KeyMintAttestation(
keySize = this.keySize, keySize = this.keySize,
algorithm = this.algorithm, algorithm = this.algorithm,
ecCurve = 0, // Not explicitly available in legacy args, but not critical ecCurve = 0,
ecCurveName = this.ecCurveName ?: "", ecCurveName = this.ecCurveName ?: "",
origin = null,
blockMode = listOf<Int>(), blockMode = listOf<Int>(),
padding = listOf<Int>(), padding = listOf<Int>(),
purpose = this.purpose, purpose = this.purpose,
@@ -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,
)
}
}
@@ -1,16 +1,24 @@
package org.matrix.TEESimulator.interception.keystore.shim package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.KeyParameter import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyPurpose
import android.hardware.security.keymint.Tag import android.hardware.security.keymint.Tag
import android.os.IBinder import android.os.IBinder
import android.os.Parcel import android.os.Parcel
import android.system.keystore2.* 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.KeyPair
import java.security.SecureRandom import java.security.SecureRandom
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
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.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
@@ -18,19 +26,18 @@ import org.matrix.TEESimulator.interception.core.BinderInterceptor
import org.matrix.TEESimulator.interception.keystore.InterceptorUtils import org.matrix.TEESimulator.interception.keystore.InterceptorUtils
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertGenConfig
import org.matrix.TEESimulator.pki.CertificateGenerator import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper import org.matrix.TEESimulator.pki.CertificateHelper
import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
* 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( class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel, private val original: IKeystoreSecurityLevel,
private val securityLevel: Int, private val securityLevel: Int,
) : BinderInterceptor() { ) : BinderInterceptor() {
// --- Data Structures for State Management ---
data class GeneratedKeyInfo( data class GeneratedKeyInfo(
val keyPair: KeyPair, val keyPair: KeyPair,
val nspace: Long, val nspace: Long,
@@ -52,7 +59,7 @@ class KeyMintSecurityLevelInterceptor(
GENERATE_KEY_TRANSACTION -> { GENERATE_KEY_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
if (!shouldSkip) return handleGenerateKey(callingUid, data) if (!shouldSkip) return handleGenerateKey(txId, callingUid, data)
} }
CREATE_OPERATION_TRANSACTION -> { CREATE_OPERATION_TRANSACTION -> {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid)
@@ -93,6 +100,11 @@ class KeyMintSecurityLevelInterceptor(
reply: Parcel?, reply: Parcel?,
resultCode: Int, resultCode: Int,
): TransactionResult { ): TransactionResult {
if (code == GENERATE_KEY_TRANSACTION && hardwareKeygenTxIds.remove(txId)) {
val remaining = hardwareKeygenCount(callingUid).decrementAndGet()
SystemLogger.info("[TX_ID: $txId] PERMIT_RELEASED uid=$callingUid concurrent_remaining=$remaining result=${if (resultCode == 0) "OK" else "ERROR($resultCode)"}")
}
// We only care about successful transactions. // We only care about successful transactions.
if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
@@ -104,7 +116,14 @@ class KeyMintSecurityLevelInterceptor(
val keyDescriptor = val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction ?: 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) { } else if (code == CREATE_OPERATION_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
@@ -172,11 +191,6 @@ class KeyMintSecurityLevelInterceptor(
return TransactionResult.SkipTransaction 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 handleCreateOperation( private fun handleCreateOperation(
txId: Long, txId: Long,
callingUid: Int, callingUid: Int,
@@ -217,27 +231,44 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
/** private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult {
* Handles the `generateKey` transaction. Based on the configuration for the calling UID, it if (data.dataSize() > MAX_ALIAS_LENGTH) {
* either generates a key in software or lets the call pass through to the hardware. SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes")
*/ return TransactionResult.ContinueAndSkipPost
private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult { }
return runCatching { return runCatching {
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR) val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR)
SystemLogger.debug( SystemLogger.debug(
"Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}" "Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}"
) )
val params = data.createTypedArray(KeyParameter.CREATOR)!! val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params) 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 val challenge = parsedParams.attestationChallenge
// if it's an attestation request in patch mode. 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 (parsedParams.serial != null || parsedParams.imei != null ||
parsedParams.meid != null || parsedParams.secondImei != null ||
params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION }) {
SystemLogger.warning("[TX_ID: $txId] Rejecting device ID attestation for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey()
val needsSoftwareGeneration = val needsSoftwareGeneration =
ConfigurationManager.shouldGenerate(callingUid) || ConfigurationManager.shouldGenerate(callingUid) ||
(ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) || (ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) ||
@@ -245,37 +276,29 @@ class KeyMintSecurityLevelInterceptor(
isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias)))
if (needsSoftwareGeneration) { if (needsSoftwareGeneration) {
keyDescriptor.nspace = secureRandom.nextLong() return doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest)
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)
} else if (parsedParams.attestationChallenge != null) { } 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 return TransactionResult.Continue
} }
// If not generating, clear any stale state for this alias and let the call proceed.
cleanupKeyData(keyId) cleanupKeyData(keyId)
TransactionResult.ContinueAndSkipPost TransactionResult.ContinueAndSkipPost
} }
@@ -285,9 +308,118 @@ class KeyMintSecurityLevelInterceptor(
} }
} }
/** private fun doSoftwareKeyGen(
* Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service. callingUid: Int,
*/ keyDescriptor: KeyDescriptor,
attestationKey: KeyDescriptor?,
parsedParams: KeyMintAttestation,
keyId: KeyIdentifier,
isAttestKeyRequest: Boolean,
): TransactionResult {
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(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,
purposes = parsedParams.purpose,
digests = parsedParams.digest,
isAttestationKey = isAttestKeyRequest,
)
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,
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( private fun buildKeyEntryResponse(
chain: List<Certificate>, chain: List<Certificate>,
params: KeyMintAttestation, params: KeyMintAttestation,
@@ -306,10 +438,129 @@ 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(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 { companion object {
private val secureRandom = SecureRandom() 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 KEYMINT_CANNOT_ATTEST_IDS = -66
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 hardwareKeygenCount(uid: Int): AtomicInteger =
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
private fun hardwareKeygenWindowCount(uid: Int): Int {
val now = System.currentTimeMillis()
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.removeAll { now - it > BURST_WINDOW_MS }
if (timestamps.isEmpty()) {
uidKeygenTimestamps.remove(uid, timestamps)
uidHardwareKeygenCount.remove(uid)
}
return timestamps.size
}
}
private fun recordHardwareKeygen(uid: Int) {
val timestamps = uidKeygenTimestamps.computeIfAbsent(uid) { mutableListOf() }
synchronized(timestamps) {
timestamps.add(System.currentTimeMillis())
}
}
private val GENERATE_KEY_TRANSACTION = private val GENERATE_KEY_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey")
private val IMPORT_KEY_TRANSACTION = private val IMPORT_KEY_TRANSACTION =
@@ -331,30 +582,16 @@ class KeyMintSecurityLevelInterceptor(
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
} }
// Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>() val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// Caches patched certificate chains to prevent re-generation and signature inconsistencies. // Caches patched chains to prevent re-generation and signature inconsistencies
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>() private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
// A set to quickly identify keys that were generated for attestation purposes. val attestationKeys: MutableSet<KeyIdentifier> = ConcurrentHashMap.newKeySet()
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Stores interceptors for active cryptographic operations.
private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>() private val interceptedOperations = ConcurrentHashMap<IBinder, OperationInterceptor>()
// --- Public Accessors for Other Interceptors ---
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response 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? { 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 if (nspace == null || nspace == 0L) return null
return generatedKeys.entries return generatedKeys.entries
.filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid } .filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid }
@@ -369,6 +606,7 @@ class KeyMintSecurityLevelInterceptor(
fun cleanupKeyData(keyId: KeyIdentifier) { fun cleanupKeyData(keyId: KeyIdentifier) {
if (generatedKeys.remove(keyId) != null) { if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}") SystemLogger.debug("Remove generated key ${keyId}")
GeneratedKeyPersistence.delete(keyId)
} }
if (patchedChains.remove(keyId) != null) { if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}") SystemLogger.debug("Remove patched chain for ${keyId}")
@@ -379,7 +617,6 @@ class KeyMintSecurityLevelInterceptor(
} }
fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) {
// Unregister from the native hook layer first.
unregister(backdoor, operationBinder) unregister(backdoor, operationBinder)
if (interceptedOperations.remove(operationBinder) != null) { if (interceptedOperations.remove(operationBinder) != null) {
@@ -387,33 +624,29 @@ 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) { fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: "" val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear() generatedKeys.clear()
patchedChains.clear() patchedChains.clear()
attestationKeys.clear() attestationKeys.clear()
GeneratedKeyPersistence.deleteAll()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") 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(securityLevel: Int): Array<Authorization> {
val authList = mutableListOf<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 { fun createAuth(tag: Int, value: KeyParameterValue): Authorization {
val param = val param =
KeyParameter().apply { KeyParameter().apply {
@@ -426,7 +659,6 @@ private fun KeyMintAttestation.toAuthorizations(securityLevel: Int): Array<Autho
} }
} }
// Use the helper to add each authorization entry cleanly.
this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) } this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) }
this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) } this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) }
@@ -19,6 +19,7 @@ object SystemLogger {
* @param message The message to log. * @param message The message to log.
*/ */
fun debug(message: String) { fun debug(message: String) {
if (!isDebugBuild) return
Log.d(TAG, message) Log.d(TAG, message)
} }
@@ -213,7 +213,7 @@ object CertificateGenerator {
uid: Int, uid: Int,
securityLevel: Int, securityLevel: Int,
): Certificate { ): Certificate {
val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key") val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key")
val leafNotAfter = val leafNotAfter =
(signingKeyPair.public as? X509Certificate)?.notAfter (signingKeyPair.public as? X509Certificate)?.notAfter
?: Date(System.currentTimeMillis() + 31536000000L) ?: Date(System.currentTimeMillis() + 31536000000L)
@@ -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" -> "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 package org.matrix.TEESimulator.util
import android.content.pm.PackageManager
import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.SecurityLevel
import android.os.Build import android.os.Build
import android.os.SystemProperties import android.os.SystemProperties
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.security.MessageDigest import java.security.MessageDigest
import java.time.LocalDate import java.time.LocalDate
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
@@ -11,7 +13,6 @@ import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.matrix.TEESimulator.attestation.DeviceAttestationService import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
@@ -90,27 +91,33 @@ object AndroidDeviceUtils {
attestationValueProvider: () -> ByteArray?, attestationValueProvider: () -> ByteArray?,
expectedSize: Int, expectedSize: Int,
): ByteArray { ): ByteArray {
// 1. Attempt to get the value from the system property.
getProperty(propertyName, expectedSize)?.let { getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}") SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
persistToFile(propertyName, it)
return it return it
} }
// 2. Fallback to the value from a cached TEE attestation.
try { try {
attestationValueProvider()?.let { attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}") SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
setProperty(propertyName, it) // Persist for consistency setProperty(propertyName, it)
persistToFile(propertyName, it)
return it return it
} }
} catch (e: Exception) { } catch (e: Exception) {
SystemLogger.error("Failed to get $propertyName from attestation.", e) 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 { return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}") SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
setProperty(propertyName, it) 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 = private fun generateRandomBytes(size: Int): ByteArray =
ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) } 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 --- // --- Patch Level Properties ---
fun getPatchLevel(uid: Int): Int { fun getPatchLevel(uid: Int): Int {
@@ -239,11 +273,12 @@ object AndroidDeviceUtils {
val resolvedValue = resolveDateKeywords(value) val resolvedValue = resolveDateKeywords(value)
return when { return when {
// "device_default" indicates falling back to the system property.
resolvedValue.equals("device_default", ignoreCase = true) -> null 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 resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT
// Otherwise, parse the resolved date string.
else -> parsePatchLevelValue(resolvedValue, isLong) else -> parsePatchLevelValue(resolvedValue, isLong)
} }
} }
@@ -370,52 +405,174 @@ object AndroidDeviceUtils {
// --- APEX and Module Hash Properties --- // --- APEX and Module Hash Properties ---
private val apexInfos: List<Pair<String, Long>> by lazy { // Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version)
runCatching { private class MinimalApexManifestParser(private val data: ByteArray) {
val pm = ConfigurationManager.getPackageManager() var pos = 0
val packages =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { fun parse(): Pair<String, Long>? {
pm?.getInstalledPackages(PackageManager.MATCH_APEX.toLong(), 0) var name: String? = null
} else { var version: Long? = null
@Suppress("DEPRECATION")
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0) while (pos < data.size) {
val tag = readVarint()
val fieldNum = tag ushr 3
val wireType = (tag and 0x07).toInt()
when (fieldNum) {
1L -> {
val length = readVarint().toInt()
if (pos + length > data.size) return null
name = String(data, pos, length, Charsets.UTF_8)
pos += length
} }
packages?.list.orEmpty().map { it.packageName to it.longVersionCode } 2L -> {
version = readVarint()
}
else -> skipField(wireType)
}
} }
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it) return if (name != null && version != null) {
emptyList() name to version
} else {
null
} }
}
private fun readVarint(): Long {
var value = 0L
var shift = 0
while (pos < data.size) {
val b = data[pos++].toInt()
value = value or ((b and 0x7F).toLong() shl shift)
if ((b and 0x80) == 0) return value
shift += 7
}
return value
}
private fun skipField(wireType: Int) {
when (wireType) {
0 -> readVarint()
1 -> pos += 8
2 -> {
val len = readVarint().toInt()
pos += len
}
5 -> pos += 4
else -> throw IllegalStateException("Unknown wire type $wireType")
}
}
}
private val apexInfos: List<Pair<String, Long>> by lazy {
val results = mutableListOf<Pair<String, Long>>()
val apexRoot = File("/apex")
if (!apexRoot.exists() || !apexRoot.isDirectory) {
return@lazy emptyList()
}
apexRoot.listFiles()?.forEach { file ->
if (!file.isDirectory) return@forEach
val name = file.name
if (name.startsWith(".")) return@forEach
if (name.contains("@")) return@forEach
if (name == "sharedlibs") return@forEach
val manifestFile = File(file, "apex_manifest.pb")
if (manifestFile.exists()) {
runCatching {
val bytes = FileInputStream(manifestFile).use { it.readBytes() }
val parser = MinimalApexManifestParser(bytes)
parser.parse()?.let { (pkgName, version) -> results.add(pkgName to version) }
}
}
}
results.distinctBy { it.first }
} }
val moduleHash: ByteArray by lazy { val moduleHash: ByteArray by lazy {
DeviceAttestationService.CachedAttestationData?.moduleHash DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching { ?: runCatching {
// TODO: figure out the correct calculation data class ModuleEntry(
val moduleSequences = ASN1EncodableVector() val nameEncoded: ByteArray,
val fullEncoded: ByteArray,
)
// 1. Create a DERSequence for each module. val modules =
apexInfos.forEach { (packageName, versionCode) -> apexInfos.map { (packageName, versionCode) ->
val moduleVector = ASN1EncodableVector() val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8))
// Use explicit UTF-8 encoding for the package name. val versionInt = ASN1Integer(versionCode)
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
moduleVector.add(ASN1Integer(versionCode))
moduleSequences.add(DERSequence(moduleVector))
}
// 2. Create a DERSet. Bouncy Castle will automatically handle val vec = ASN1EncodableVector()
// the sorting based on the DER-encoded value of each sequence. vec.add(nameOctet)
val modulesSet = DERSet(moduleSequences) vec.add(versionInt)
val sequence = DERSequence(vec)
// 3. Get the final DER-encoded byte array of the SET. // AOSP sorts by encoded name only, not full sequence
val encodedModules = modulesSet.encoded ModuleEntry(
nameEncoded = nameOctet.encoded,
fullEncoded = sequence.encoded,
)
}
// 4. Compute the SHA-256 hash. val sortedModules =
MessageDigest.getInstance("SHA-256").digest(encodedModules) modules.sortedWith { m1, m2 ->
compareByteArrays(m1.nameEncoded, m2.nameEncoded)
}
val payloadStream = ByteArrayOutputStream()
sortedModules.forEach { payloadStream.write(it.fullEncoded) }
val payload = payloadStream.toByteArray()
// Wrap in DER SET tag manually — DERSet() re-sorts by full encoding
val finalDerSet = encodeAsDerSet(payload)
MessageDigest.getInstance("SHA-256").digest(finalDerSet)
} }
.getOrElse { .getOrElse {
SystemLogger.error("Failed to compute module hash.", it) 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())
}
}
}
} }
@@ -6,7 +6,11 @@ package org.matrix.TEESimulator.util
* *
* @return A new string with each line individually trimmed. * @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. * Converts a ByteArray to its hexadecimal string representation.
+11
View File
@@ -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
+76 -17
View File
@@ -1,26 +1,85 @@
## 🎉 TEESimulator v3.1: Legacy Support & Resilience ## TEESimulator v4.3: Performance & Reliability
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 1012**). - **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.
### 🛡️ 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:
* **✅ Full Crypto Operations (`createOperation`)**: The simulator now correctly handles `SIGN`, `VERIFY`, `ENCRYPT`, and `DECRYPT` purposes for software-generated keys. ## TEESimulator v4.2: Detection Evasion Hardening
* **🔗 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 Fixes 6 detection vectors flagged by attestation validator apps.
Weve ironed out crashes and architecture-specific bugs to ensure a smooth experience across more devices:
* **Android 10**: Fixed a crash caused by the missing `waitForService` method. ### Attestation Policy Enforcement
* **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`.
### 🚀 The Road Ahead Replicate AOSP keystore2's `add_required_parameters()` validation that our software keygen path was bypassing:
We are aware of the remaining detection vectors (see the issues list) and have clear solutions mapped out for the next release. - **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).
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**. ### Certificate Fix
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. 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).
+5 -1
View File
@@ -48,7 +48,7 @@ install_file() {
# --- Installation --- # --- Installation ---
ui_print "- Extracting module files" 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" install_file "$file" "$MODPATH"
done done
@@ -67,10 +67,14 @@ ui_print ""
ui_print "- Extracting $ARCH libraries" ui_print "- Extracting $ARCH libraries"
install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH" install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH"
install_file "lib/$ABI_DIR/libinject.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 "" ui_print ""
mv "$MODPATH/libinject.so" "$MODPATH/inject" mv "$MODPATH/libinject.so" "$MODPATH/inject"
mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor"
chmod 755 "$MODPATH/inject" chmod 755 "$MODPATH/inject"
chmod 755 "$MODPATH/supervisor"
# --- Configuration Files --- # --- Configuration Files ---
if [ ! -d "$CONFIG_DIR" ]; then if [ ! -d "$CONFIG_DIR" ]; then
+2 -2
View File
@@ -2,6 +2,6 @@ id=tricky_store
name=TEESimulator name=TEESimulator
version=${REPLACEMEVER} version=${REPLACEMEVER}
versionCode=${REPLACEMEVERCODE} versionCode=${REPLACEMEVERCODE}
author=JingMatrix author=JingMatrix, Enginex0
description=Software simulation for Android hardware-backed key pairs with key attestation 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/main/module/update.json
+2 -8
View File
@@ -1,11 +1,5 @@
DEBUG=false
MODDIR=${0%/*} MODDIR=${0%/*}
cd $MODDIR cd $MODDIR
while true; do # Fork-based supervisor for instant restart
./daemon "$MODDIR" || exit 1 ./supervisor ./daemon "$MODDIR" &
# ensure keystore initialized
sleep 2
done &
+12
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{ {
"version": "v3.1", "version": "v4.3",
"versionCode": 59, "versionCode": 107,
"zipUrl": "https://github.com/JingMatrix/TEESimulator/releases/download/v3.1/TEESimulator-v3.1-59-Release.zip", "zipUrl": "https://github.com/Enginex0/TEESimulator/releases/download/v4.3/TEESimulator-v4.3-Release.zip",
"changelog": "https://raw.githubusercontent.com/JingMatrix/TEESimulator/main/module/changelog.md" "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator/main/module/changelog.md"
} }
+11
View File
@@ -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"
+1166
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -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"
+8
View File
@@ -0,0 +1,8 @@
[toolchain]
channel = "stable"
targets = [
"aarch64-linux-android",
"armv7-linux-androideabi",
"i686-linux-android",
"x86_64-linux-android",
]
+644
View File
@@ -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(&params.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(&params.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(&params.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(&params.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(&params.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(&params);
// 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(&params).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(&params).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(&params).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(&params).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
}
}
}
+523
View File
@@ -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, &params.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(&not_before, &not_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);
}
+75
View File
@@ -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>;
+96
View File
@@ -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))
}
+59
View File
@@ -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(),
})
}
+324
View File
@@ -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(&params.keybox_cert_chain, &params.keybox_private_key)?;
let attest_ext = attestation::build_attestation_extension(&params)?;
let cert_chain = certbuilder::build_certificate_chain(
&key_pair,
&attest_ext,
&keybox,
&params,
)?;
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
}
+208
View File
@@ -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(())
}
+93
View File
@@ -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());
}
}
+41
View File
@@ -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(())
}
+166
View File
@@ -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(&current_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;
}
}
}
+38
View File
@@ -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(())
}
+121
View File
@@ -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>,
}
+262
View File
@@ -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 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."