This commit is contained in:
2026-09-13 12:15:36 -07:00
commit e473d00f4f
104 changed files with 16080 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
# Testing & CLI Guide
This document covers the Portal TV / PortalCam security test suites and the
`portalkit-cli` tool used for pairing, control, and live MITM verification.
Related protocol design lives in [`portal-capability-test/README.md`](portal-capability-test/README.md)
(Authentication and Security Architecture) and [`mac2/README.md`](mac2/README.md).
---
## Overview
| Layer | Location | How to run | Scope |
|-------|----------|------------|-------|
| Android / JVM unit & integration | `portal-capability-test/` | `./run-tests.sh` | RFC 5054 vectors, SRP-6a math, channel binding, rate limits, padding, constant-time compare |
| Swift unit tests (`PortalKit`) | `mac2/PortalKit/` | `swift test` | Same crypto surface on the Mac client library |
| Live E2E + MITM | `tests/e2e_portal_test.py` | `python3 tests/e2e_portal_test.py` | Real Portal TV + `portalkit-cli` + rogue TLS proxy |
| CLI tool | `mac2/PortalKit``portalkit-cli` | see [portalkit-cli](#portalkit-cli) | Pair, status, control, MITM defense probe |
Hand-rolled SRP-6a is verified against **RFC 5054 Appendix B** test vectors on both
platforms (1024-bit group vectors from the RFC). Production pairing uses the
**RFC 5054 2048-bit** MODP group with SHA-256 hashing and TLS certificate channel
binding; those paths are covered by additional agreement / safety / MITM tests.
---
## Android / JVM test suite
### Layout
```
portal-capability-test/
├── run-tests.sh
├── src/com/portaltv/capability/
│ ├── PortalSrp.kt # server SRP-6a
│ └── PortalSrpClient.kt # client-side helpers used by tests
└── test/
├── android/util/Base64.java # JVM stub for android.util.Base64
└── com/portaltv/capability/test/
├── TestFramework.kt
├── TestRunner.kt
├── PortalSrpRfc5054Test.kt
├── PortalSrpMathTest.kt
├── PortalSrpSafetyTest.kt
├── PortalSrpRateLimitingTest.kt
├── PortalSrpChannelBindingTest.kt
├── PortalSrpPaddingTest.kt
├── PortalSrpConstantTimeTest.kt
└── PortalSrpIntegrationTest.kt
```
### Run
Requires `kotlinc` (Android Studios bundled Kotlin compiler is used if present)
and a JDK with `javac` / `java` on `PATH`.
```sh
cd portal-capability-test
./run-tests.sh
```
Expected summary (approximate counts):
```text
Total Suites: 8
Total Tests: 50
Passed: 50
Failed: 0
*** ALL 50 SRP-6a TESTS PASSED WITH 0 ERRORS ***
```
### What each suite covers
| Suite | File | Coverage |
|-------|------|----------|
| RFC 5054 Appendix B | `PortalSrpRfc5054Test.kt` | Official vectors for $k$, $x$, $v$, $A$, $B$, $u$, and premaster secret $S$ (client/server agreement + RFC match) |
| 2048-bit math | `PortalSrpMathTest.kt` | Group parameters ($N$, $g$, $k$), $K_{\text{client}} = K_{\text{server}}$, $M_1$/$M_2$, PIN variety, session entropy |
| Safety checks | `PortalSrpSafetyTest.kt` | Reject $A \bmod N = 0$, $B \bmod N = 0$, $u = 0$, malformed hex |
| Rate limiting | `PortalSrpRateLimitingTest.kt` | 3-attempt lifecycle ($3 \to 2 \to 1 \to 0$), session wipeout, recovery after one failure, expiry |
| Channel binding | `PortalSrpChannelBindingTest.kt` | Matching `tls_hash` succeeds; rogue / bit-flipped / empty hash rejected; forged $M_2$ rejected |
| Padding | `PortalSrpPaddingTest.kt` | `toPadded256` for zero, small, exact 256-byte, sign-byte, negative, oversized values |
| Constant-time compare | `PortalSrpConstantTimeTest.kt` | Equal / unequal / length-mismatch digests and keys |
| Protocol integration | `PortalSrpIntegrationTest.kt` | Simulated init → verify → bearer flow; MITM at HTTP layer; brute-force wipeout |
---
## Swift `PortalKit` unit tests
Pairing, TLS pinning, credential storage, and the high-level client live in the
`PortalKit` Swift package. `PortalCam.app` links this package; the CLI is the
same librarys executable target.
### Layout
```
mac2/PortalKit/
├── Package.swift
├── Sources/
│ ├── PortalKit/ # library (Crypto, TLS, Auth, Client, BigInt)
│ └── portalkit-cli/ # CLI executable
└── Tests/PortalKitTests/
├── Rfc5054VectorsTests.swift
├── Srp6aExchangeTests.swift
├── ChannelBindingTests.swift
├── SafetyChecksTests.swift
├── BigUIntPaddingTests.swift
├── ClientAndAuthTests.swift
└── SmokeTests.swift
```
### Run
```sh
cd mac2/PortalKit
swift test
```
Expected:
```text
Executed 33 tests, with 0 failures
```
### What each suite covers
| Suite | Coverage |
|-------|----------|
| `Rfc5054VectorsTests` | RFC 5054 Appendix B: $k$, $x$, $v$, $A$, $B$, $u$, client/server $S$ |
| `Srp6aExchangeTests` | 2048-bit end-to-end exchange, randomized sessions, wrong-PIN rejection |
| `ChannelBindingTests` | Matching cert hash succeeds; MITM / bit-flip / forged $M_2$ fail |
| `SafetyChecksTests` | Invalid salt, $B \bmod N = 0$, $u = 0$, empty PIN, bad $M_2$ |
| `BigUIntPaddingTests` | 256-byte padding, hex encode/decode, constant-time equality |
| `ClientAndAuthTests` | Credential storage, URL normalization, TLS challenge evaluation helpers |
| `SmokeTests` | Package / version sanity |
---
## Live E2E & MITM suite
### Prerequisites
- Portal TV reachable at `10.0.0.10:5654` with `com.portaltv.capability` running (HTTPS).
- `adb` authorized to the device (used to read the pairing PIN from logcat).
- Built CLI binary (see [Build portalkit-cli](#build-portalkit-cli)).
Default host and CLI path are configured at the top of `tests/e2e_portal_test.py`.
### Run
```sh
# from repo root
cd mac2/PortalKit && swift build -c release && cd ../..
python3 tests/e2e_portal_test.py
```
Expected:
```text
Ran 6 tests in ~13s
OK
```
### Scenarios
| # | Test | Asserts |
|---|------|---------|
| A | Status verification | `portalkit-cli status` → online; leaf cert SHA-256 present |
| B | Direct channel-binding defense | `portalkit-cli test-mitm` → server rejects; verdict `PASSED` |
| C | Live MITM TLS proxy | In-process proxy on `127.0.0.1:8888` presents a rogue cert; client binds $M_1$ to rogue hash; Portal returns **HTTP 401** |
| D | Pinned-cert hard-fail (all protected endpoints) | For each of `/control/state`, `/control/mode`, `/control/fixed`, `/control/desk`, `/control/events`, `/video.h264`, `/audio.aac`: client pinned to the genuine fingerprint must **abort at TLS** against the rogue proxy with **zero HTTP bytes** observed. Soft warnings / continued requests fail the test. Same for `portalkit-cli control` verbs (`state`, `mode`, `fixed`, `desk`). |
| E | Rate limiting | Three failed verifies → attempts $2 \to 1 \to 0$; fourth sees wiped session (`no_active_pairing`) |
| F | Legitimate pair + control | Read PIN from logcat → `pair` → pin cert → `control … mode Desk``{"ok":true}``control … state` shows Desk → restore `DefaultAuto` |
Scenario C is a real network MITM simulation (terminate TLS with a self-signed
rogue cert, forward upstream to the Portal over genuine TLS), not only an
in-memory hash swap.
---
## portalkit-cli
Command-line front end for `PortalKit`. Same pairing, pinning, and control
paths as `PortalCam.app`, without the UI or camera extension.
### Build portalkit-cli
```sh
cd mac2/PortalKit
swift build -c release
```
Binary:
```text
mac2/PortalKit/.build/release/portalkit-cli
```
Optional install:
```sh
cp .build/release/portalkit-cli /usr/local/bin/
```
Credentials are stored in the macOS Keychain when available, with a file fallback
at `~/.portalkit/credentials.json` (token + pinned cert SHA-256).
### Commands
#### `status <host>`
Probe service reachability and leaf certificate fingerprint; compare against any
stored pin.
```sh
portalkit-cli status 10.0.0.10:5654
```
#### `pair <host> [--pin <pin>]`
1. Connect over ephemeral HTTPS and capture leaf cert SHA-256.
2. `POST /auth/srp/init`.
3. Compute channel-bound $M_1$ from the PIN (prompted if `--pin` omitted).
4. `POST /auth/srp/verify`; verify $M_2$.
5. Persist bearer token + pinned fingerprint.
```sh
portalkit-cli pair 10.0.0.10:5654 --pin 123456
```
Read the live PIN from the TV (debug builds log it):
```sh
adb shell "logcat -d -s PortalService | grep 'SRP pairing started with PIN:' | tail -1"
```
#### `control <host> <command>`
Send camera control over **pinned** HTTPS with `Authorization: Bearer …`.
Mutations return `{"ok":true}`; `state` returns JSON camera state
`{"mode":"…","config":{…}}`. Errors are `{"error":"…","message":"…"}`.
```sh
portalkit-cli control 10.0.0.10:5654 state
portalkit-cli control 10.0.0.10:5654 mode Desk
portalkit-cli control 10.0.0.10:5654 mode DefaultAuto
portalkit-cli control 10.0.0.10:5654 fixed?x=0.5&y=0.5&scale=1.0
portalkit-cli control 10.0.0.10:5654 desk?tightness=0.5
```
Fails **hard** (non-zero exit, no request sent) if not paired, or if the
server cert no longer matches the pin.
#### `test-mitm <host>`
Initiate pairing, compute $M_1$ with an **altered** certificate hash, submit
verify, and expect Portal rejection (`Authentication failed (wrong PIN or MITM detected)`).
```sh
portalkit-cli test-mitm 10.0.0.10:5654
```
Successful defense prints `Verdict: PASSED`.
### Help
```sh
portalkit-cli --help
```
---
## Suggested local workflow
```sh
# 1. Crypto regressions (no device required)
cd portal-capability-test && ./run-tests.sh
cd ../mac2/PortalKit && swift test
# 2. Build CLI
swift build -c release
# 3. Deploy Portal APK (device required)
cd ../../portal-capability-test && ./deploy.sh
# 4. Live E2E including MITM proxy (device + ADB required)
cd .. && python3 tests/e2e_portal_test.py
```
---
## Notes
- **RFC vectors vs production group**: Appendix B uses the **1024-bit** RFC group
and the RFCs hash construction for those vectors. Production and most unit
tests use the **2048-bit** group with SHA-256 and
$x = \mathrm{SHA256}(s \parallel \mathrm{PIN})$ (no identity string). Both are
intentional: vectors prove the modular arithmetic; 2048-bit tests prove the
deployed protocol.
- **Channel binding**: $M_1$ and $M_2$ include $\mathrm{SHA256}(\mathrm{leaf\_cert\_DER})$.
A TLS-terminating proxy that presents a different cert cannot complete pairing.
- **Rate limit**: three failed verifies per pairing session; session and PIN are
wiped at zero attempts remaining or after the 120s expiry window.