The sirosid-dev environment provides complete support for testing the siros-sdk-kotlin sample app with all WSCD plugins enabled (softkey, R2PS, FIDO2) against a running conformance suite.
cd sirosid-dev
# Step 1: Start full environment
make up VC=yes CONFORMANCE=yes
# Step 2 (Optional): Use a custom app package name
# make android-setup APP_PACKAGE=com.example.myapp
# Step 3: Generate Android configuration
make android-setup
# Step 4: Verify APK key hash was generated
cat .env.android
# Should show: APK_KEY_HASH=xAfybcO0yPlUA_Gko2tH6oIRb9Y-qa3uVtm0m_qW0w
# Step 5: Start Android helpers
make android-up
# Step 6: Install sample app APK and run conformance tests
adb install -r ../siros-sdk-kotlin/app/build/outputs/apk/debug/app-debug.apk
# Step 7: Send test credential offer
make android-launch- Docker & Docker Compose
adb(Android Debug Bridge) for device communicationkeytoolfor keystore inspection- Waydroid or Android emulator running
- Android 11+ (API 30+)
- Network access to host at 192.168.240.1
- Development mode enabled
- USB debugging enabled (if physical device)
- Conformance suite TLS certificate installed in Android CA store
- App's APK signed with debug.keystore (created during build)
By default, the sample app uses org.siros.sdk.sample. To test with different package names:
# Run setup with a custom package name
make android-setup APP_PACKAGE=com.example.customwallet
# Verify in assetlinks.json
cat .well-known/assetlinks.json | jq '.[] | .target.package_name'
# Output: "com.example.customwallet"
# Build sample app with the matching package name
cd ../siros-sdk-kotlin
./gradlew build
# Deploy and test
adb install -r app/build/outputs/apk/debug/app-debug.apkWhy This Matters:
- App Link verification requires package name match
- Credential offers with deep links require correct package
- Each package name variant needs separate APK signing cert
- Test multiple wallet implementations side-by-side
# 1. Extracts APK key hash from ~/.android/debug.keystore
keytool -list -v -alias androiddebugkey -keystore ~/.android/debug.keystore \
-storepass android -keypass android | grep "SHA256:"
# Output: SHA256: C4 07 F2 6D C3 B4 C8 F9 54 03 F1 A4 A3 6B 47 EA 82 11 6F D6 3E A9 AD EE 56 D9 B4 9B FA 96 D3 B4
# 2. Converts hex format to base64 (Android's assetlinks.json format)
# C4:07:F2:6D:C3:B4:C8:F9:54:03:F1:A4:A3:6B:47:EA:82:11:6F:D6:3E:A9:AD:EE:56:D9:B4:9B:FA:96:D3:B4
# → xAfybcO0yPlUA_Gko2tH6oIRb9Y-qa3uVtm0m_qW0w
# 3. Generates .well-known/assetlinks.json
cat > .well-known/assetlinks.json <<EOF
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "org.siros.sdk.sample",
"sha256_cert_fingerprints": ["xAfybcO0yPlUA_Gko2tH6oIRb9Y-qa3uVtm0m_qW0w"]
}
}]
EOF
# 4. Creates .env.android for container environment
cat > .env.android <<EOF
APK_KEY_HASH=xAfybcO0yPlUA_Gko2tH6oIRb9Y-qa3uVtm0m_qW0w
EOF- App Link Verification: Android requires matching certificate signatures for deep links
- Credential Offer Handling: When issuer sends
openid-credential-offer://deep link, Android verifies:- URL domain matches app's URL schemes
- Domain's assetlinks.json matches APK's signing certificate
- Only then does it launch the app with the credential offer
# Waydroid runs Android in a container alongside docker-compose services
# All services accessible at 192.168.240.1 from Waydroid's perspective
docker-compose -f docker-compose.android.yml up -d
waydroid shell # Opens shell in Waydroid environment
adb devices # Lists connected Waydroid instanceAdvantages:
- No physical device required
- Fast iteration
- Reproducible in CI
Limitations:
- May be slower than native
- Requires adequate RAM
# Connect device via USB, ensure USB debugging is enabled and authorized
adb devices # Verify device appears
# Step 1: Start full environment
make up VC=yes CONFORMANCE=yes
# Step 2: Set up USB device (port forwarding + assetlinks + config)
make usb-android-setup
# Step 3: Start USB Android overlay
make usb-android-up
# Step 4: Install and launch sample app
adb install -r ../siros-sdk-kotlin/sample-app/build/outputs/apk/debug/sample-app-debug.apk
make usb-android-launchHow it works:
- Uses
adb reverseto forward ports from the device's localhost to the host machine - Device accesses all services (backend, frontend, OIDC, etc.) via
localhost:PORT - No special network gateway needed — works over USB regardless of Wi-Fi/network setup
Port forwarding (automatic via make usb-android-setup):
# These ports are forwarded (device localhost → host localhost):
# 3000 - wallet-frontend
# 8080 - wallet-backend
# 8081 - admin API
# 8082 - wallet-engine
# 9000 - VC issuer
# 9001 - VC verifier
# 9003 - VC API gateway
# 9004 - VC registry
# 9005 - mini-oidc
# 9011 - mock verifier
# 9081 - mock PDP
# 9095 - go-trust allowMulti-device support (when multiple devices connected):
# List devices
adb devices
# List of devices attached
# ABC123DEF456 device
# XYZ789GHI012 device
# Target specific device
ADB_SERIAL=ABC123DEF456 make usb-android-setup
ADB_SERIAL=ABC123DEF456 make usb-android-launchAdvantages:
- Real hardware capabilities (NFC, biometrics, secure element)
- Accurate performance characteristics
- Test FIDO2 plugin with actual YubiKey via NFC/USB
- Works without Wi-Fi — USB-only connectivity
Limitations:
- Requires physical device
- Port forwarding must be re-established if ADB disconnects
# Full automated conformance run
node run-android-usb-conformance.mjs --plan vci
# With specific device
node run-android-usb-conformance.mjs --plan all --serial ABC123DEF456
# Check device status
make usb-android-status| Task | Waydroid | USB Device |
|---|---|---|
| Start overlay | make android-up |
make usb-android-up |
| Stop overlay | make android-down |
make usb-android-down |
| Setup | make android-setup |
make usb-android-setup |
| Launch app | make android-launch |
make usb-android-launch |
| View logs | make android-logs |
make usb-android-logs |
| Full flow | make android-full |
make usb-android-full |
| Conformance | run-android-conformance.mjs |
run-android-usb-conformance.mjs |
| Device shell | waydroid shell |
adb shell |
| Host address | 192.168.240.1 |
localhost (via adb reverse) |
The sample app selects one of three signing backends at runtime via
WalletViewModel.selectPlugin("softkey" | "r2ps" | "fido2") (see the
sample app's Settings/WSCA developer screen, or drive it directly via the
config WSCA_TEST action documented below). There is no build-time or
shell-env-var switch — export R2PS_ENABLED=true has no effect; that name
is only a Gradle buildConfigField baked in at compile time
(sample-app/build.gradle.kts, currently false in both build types) used
purely as the initial value before the user/test picks a plugin at runtime.
All three plugins sit behind siros-sdk-keystore's WscdManager interface
(SirosWallet.wscdManager, non-null only for a WSCD-backed keystore):
The default KeystoreManager — a JWE-encrypted container backed by the
Android Keystore. No WscdManager registration needed; this is what you
get without calling either register*Plugin method below.
When to use: Development, testing without HSM Security: Medium (encrypted locally, no attestation)
val config = R2psConfig(
serverUrl = "http://192.168.240.1:9443", // or :8443 without conformance
clientId = "sdk-test",
// ...clientKeyPem / serverPublicKeyPem / authMode - see
// sdk/keystore/src/main/kotlin/org/siros/sdk/keystore/WscdManager.kt
)
wscdManager.registerR2psPlugin(config, OkHttpR2psTransport(config.serverUrl))All CBOR/OPAQUE protocol handling happens in Rust (siros-wscd-manager,
exposed via UniFFI) — the Kotlin side only supplies an HTTP transport and
connection parameters.
When to use: Production simulation, HSM-backed keys, audit trails Security: High (keys never leave HSM, PAKE authentication)
Environment Setup:
# Start with R2PS overlay
make up R2PS=yes
# Toggle the plugin and server URL at runtime (see "Available Actions" below)
adb shell am start -n org.siros.sdk.sample/.MainActivity \
-a org.siros.sdk.sample.WSCA_TEST --es wsca_action config \
--ez r2ps_enabled true --es r2ps_url http://192.168.240.1:9443
# View R2PS status
curl -s http://localhost:8444/admin/store/keys | jq .wscdManager.registerFido2Plugin(transport) // transport: Ctap2TransportProviderCTAP2 CBOR request-building/response-parsing happens in Rust; the Kotlin
Ctap2TransportProvider only moves raw command/response bytes over
USB/BLE/NFC.
When to use: Hardware-backed signing, high security requirements Security: Highest (keys on hardware device, user-verified)
Prerequisites:
- YubiKey 5 or later with FIDO2
- NFC/USB connection to Android device
1. Start environment with all plugins:
make up VC=yes CONFORMANCE=yes
make android-setup
2. Deploy sample app, then enable R2PS + set its server URL at runtime
(Settings/WSCA developer screen, or the `config` WSCA_TEST action -
see "WSCD Plugins Configuration" above):
./gradlew installDebug
adb shell am start -n org.siros.sdk.sample/.MainActivity \
-a org.siros.sdk.sample.WSCA_TEST --es wsca_action config \
--ez r2ps_enabled true --es r2ps_url http://192.168.240.1:9443
3. Initiate credential offer from conformance suite:
- Navigate to https://localhost.emobix.co.uk:8443/
- Create OID4VCI wallet test plan
- Conformance suite sends: openid-credential-offer://...
4. Android app receives offer:
- App Link verification succeeds (assetlinks.json matches APK)
- App launched with credential offer URI
- SDK parses offer and discovers issuer metadata
5. Wallet shows credential details:
- User confirms credential acceptance
- SDK selects signing backend:
a) plugin-softkey: uses local key (default)
b) plugin-r2ps: contacts R2PS server, registers, authenticates, generates key, signs
c) plugin-fido2: prompts YubiKey NFC/USB for signature
6. Backend receives credential request:
- Verifies signature using public key
- Issues credential (SD-JWT, mdoc, etc.)
7. Conformance suite validates:
- Receives issued credential
- Checks format, signatures, expiration
- Marks test module PASSED or FAILED
The WSCA lifecycle test automation uses ADB intents to drive real WSCA/WSCD operations on the sample app. This is not a mock or backend bypass — all actions dispatch to the real ViewModel methods, using the same code path as the UI buttons.
┌─────────────────┐ adb intent ┌────────────────────────┐
│ sirosid-tests │ ──────────────► │ Sample App (debug) │
│ Playwright + │ │ MainActivity │
│ wsca-automation│ │ └─ dispatchWscaTest() │
│ │ │ └─ ViewModel.*() │
│ │ ◄────────────── │ └─ logcat JSON │
│ (polls logcat) │ WSCA_TEST_RESULT│ │
└─────────────────┘ └────────────────────────┘
Key design decisions:
- Intent action:
org.siros.sdk.sample.WSCA_TEST - Only available in debug builds (via
src/debug/AndroidManifest.xml) - Results as structured JSON on logcat tag
WSCA_TEST_RESULT ensureAuthenticatedForTesting()auto-handles passkey registration/login
| Action | Description | Extra Params |
|---|---|---|
enroll |
Register + activate lifecycle | — |
rotate |
Rotate lifecycle keys | — |
destroy |
Destroy lifecycle context | mode: local, revoke, strict |
status |
Query lifecycle state + key inventory | — |
config |
Configure WSCA plugin settings | r2ps_enabled, r2ps_url |
refresh |
Refresh WSCD info display | — |
From the sirosid-tests directory:
# Run all WSCA lifecycle tests (softkey plugin)
make test-wsca-softkey
# Run with R2PS plugin (requires running R2PS service)
make test-wsca-r2ps R2PS_URL=http://192.168.240.1:9443
# Run both plugins
make test-wsca
# Or directly with playwright
npx playwright test specs/conformance/wsca-lifecycle-android.spec.ts# 1. Connect device via USB
adb devices # Verify device appears
# 2. Start environment with R2PS support
cd sirosid-dev
make up VC=yes R2PS=yes
make usb-android-setup
# 3. Build and install debug APK
cd ../siros-sdk-kotlin
./gradlew :sample-app:assembleDebug
adb install -r sample-app/build/outputs/apk/debug/sample-app-debug.apk
# 4. Run WSCA tests targeting the physical device
cd ../sirosid-tests
ANDROID_DEVICE_SERIAL=<serial> make test-wsca-softkey| Variable | Default | Description |
|---|---|---|
ANDROID_WALLET_PACKAGE |
org.siros.sdk.sample |
App package name |
ANDROID_WALLET_ACTIVITY |
.MainActivity |
Activity class |
ADB_PATH |
adb |
Path to adb binary |
ANDROID_DEVICE_SERIAL |
(auto) | Target specific device |
R2PS_URL |
— | R2PS server URL (enables R2PS tests) |
You can also drive WSCA actions manually for debugging:
# Enroll
adb shell am start -n org.siros.sdk.sample/.MainActivity \
-a org.siros.sdk.sample.WSCA_TEST \
--es wsca_action enroll
# Check result
adb logcat -d -s WSCA_TEST_RESULT:*
# Status
adb shell am start -n org.siros.sdk.sample/.MainActivity \
-a org.siros.sdk.sample.WSCA_TEST \
--es wsca_action status
# Destroy with revoke
adb shell am start -n org.siros.sdk.sample/.MainActivity \
-a org.siros.sdk.sample.WSCA_TEST \
--es wsca_action destroy --es mode revokeThe conformance spec tests the following lifecycle flows per plugin:
- Configure → set plugin (softkey/r2ps)
- Enroll → verify Active state
- Status → verify keys present
- Rotate → verify still Active
- Status → verify keys after rotation
- Destroy (local) → verify Destroyed state
- Re-enroll → verify recovery works
- Destroy (revoke) → verify with server-side revocation
Plus an independent Full Lifecycle Cycle test: enroll → rotate × 2 → destroy.
# Stream app logcat
adb logcat | grep siros
# Save to file for analysis
adb logcat > android-test.log &
# Specific tag filtering
adb logcat | grep "siros-wscd-manager"
# WSCA test results specifically
adb logcat -s WSCA_TEST_RESULT:*# On device/Waydroid
adb shell
# Inside shell:
ls -la /system/etc/security/cacerts/
# Should include conformance suite CA cert
# Test HTTPS connection
curl -v https://localhost.emobix.co.uk:8443/ 2>&1 | grep "SSL"# From Android device/Waydroid
adb shell curl -v http://192.168.240.1:9443/admin/store/keys
# Should return list of provisioned keys# Backend logs all wallet operations via SET framework
docker logs wallet-backend-e2e-test --follow | grep "Event"
# Look for audit events like:
# EventWICreated, EventWIDeactivated
# EventInviteCreated, EventInviteUpdated# If device cannot reach host:
# 1. Verify host IP from device perspective
adb shell getprop ro.hardware | grep -q emulator && \
BRIDGE_IP=$(hostname -I | awk '{print $1}') || \
BRIDGE_IP=192.168.1.X
# 2. Test connectivity
adb shell ping -c 1 192.168.240.1
# 3. Port forwarding (if on separate network)
adb reverse tcp:8080 tcp:8080
adb reverse tcp:8443 tcp:8443name: Android Conformance Tests
on: [push, pull_request]
jobs:
android-conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Start environment
run: |
cd sirosid-dev
make up VC=yes CONFORMANCE=yes
make android-setup
- name: Build sample app
run: |
cd ../siros-sdk-kotlin
./gradlew build
- name: Run conformance tests
run: |
cd ../sirosid-dev
make test-wallet # Via Playwright in WaydroidSymptom: App Link verification fails, deep link doesn't launch app
Solution:
# Verify file exists and is served
curl -s http://localhost:3000/.well-known/assetlinks.json | jq
# Check nginx config serves it
docker exec wallet-proxy-e2e curl -s http://wallet-frontend:3000/.well-known/assetlinks.json
# Regenerate
make android-setupSymptom: Plugin fails with "connection refused" to R2PS
Solution:
# Check R2PS container is running
docker ps | grep r2ps
# Verify port is exposed (should be 9443 with conformance)
docker port r2ps-server | grep 8443
# From device, test connectivity
adb shell curl -v http://192.168.240.1:9443/admin/store/keysSymptom: "Sign operation failed" error in app
Debugging:
# Check backend logs for key lookup errors
docker logs wallet-backend-e2e-test | grep -i "key\|sign"
# Verify key exists in WSCD
adb shell # On device
java -cp /system/app/siros-wscd-manager/siros-wscd-manager.jar \
org.sirosfoundation.wscd.ListKeys
# Check R2PS key inventory
curl -s http://r2ps-server:8443/admin/store/keys | jq '.[] | .kid'# In sample app, modify test to cycle through plugins:
val plugins = listOf(
SoftkeyPlugin(),
R2psPlugin(...),
PreviewSignPlugin()
)
for (plugin in plugins) {
val key = plugin.createKey(...)
val signature = plugin.sign(key, testData)
assert(verifySignature(signature)) { "Plugin ${plugin.name} failed" }
}val start = SystemClock.elapsedRealtime()
val signature = plugin.sign(keyId, data)
val elapsed = SystemClock.elapsedRealtime() - start
println("Sign time: ${elapsed}ms")// Each plugin has different concurrency limits
// SoftkeyPlugin: unlimited (local)
// R2psPlugin: limited by R2PS session pool
// PreviewSignPlugin: 1 at a time (FIDO2 device)
val jobs = (1..100).map { i ->
launchAsync {
plugin.sign(keyId, data)
}
}
jobs.awaitAll()# Full environment status
make status
# Show all running containers
docker ps -a
# Deep dive: specific service logs
docker logs <service> --follow
# Network inspection
docker network ls
docker network inspect sirosid-dev_default
# Database queries (VC)
docker exec -it conformance-suite-mongodb mongo
# Database queries (Wallet)
docker exec -it wallet-db psql -U wallet -c "SELECT * FROM wallet_instance;"Last Updated: 2026-08-07
Sample App: siros-sdk-kotlin, main (org.siros.sdk.sample)
WSCD Manager: siros-wscd-manager, main — UniFFI/Rust-backed WscdManager
plugins (softkey, r2ps, fido2); check Cargo.toml for the exact pinned version
Test Automation: sirosid-tests, main (WSCA lifecycle tests)