Investigation: Merchant Terminal READY but Attestation Fails¶
Date: 2026-05-21
Symptom: Settings show Worldline SDK credentials: READY; card charge fails at attestation (Soft Space refreshToken()). Sagio API env vars and /merchant-app/fasspay-config are believed correct.
App: io.sagio.merchant (apps/sagio)
Audience: Engineers continuing this work or onboarding Soft Space / Play signing
Operational runbooks (what to do next):
- softspace-play-integrity-whitelist.md — fingerprints, Soft Space email, scripts
- merchant-terminal-card-attestation.md — config layers, triage checklist
- merchant-terminal-launch.md — full terminal launch
1. Problem statement¶
Chris Kwon reported:
- Terminal UI and connecting animation work.
- Attestation fails when creating / proceeding with a card payment.
- Question: are any configs needed beyond SAGIO API credentials?
Follow-up from SAGIO: env vars are set and fetched; symptom is exactly Settings READY + attestation fails.
A Cloud HSM export was then provided:
- File:
sagio-production-ring-sagio-app-signer-1-CAVIUM_V2_COMPRESSED-attestation.dat - KMS:
projects/sagio-484122/locations/australia-southeast1/keyRings/sagio-production-ring/cryptoKeys/sagio-app-signer/cryptoKeyVersions/1 - Public key PEM (RSA 2048)
Chris later shared the Soft Space exchange archive on his machine (~/wl): SAGIO_id_cert, sagio_app_final.cert, sagio_application_id.txt, sagio_pk.pem, softspace-attestation.asc. See softspace-exchange-archive.md.
Soft Space email (Liew Zhi Ying, UAT onboarding) — sent to Deb after Chris shared the cert file:
- Confirmed Chris’s cert file was verified
- Confirmed
io.sagio.merchantwhitelisted in UAT - Issued integrator SDK credentials (
accessKey+secretKey) for UAT
Store these only in apps/api/.env.local and Cloudflare Worker secrets — never in the APK or git. The accessKey in the email matched our .env.local; the secretKey did not until 2026-05-22 (updated + redeployed to sagio-api-prod and sagio-api-dev via wrangler on nzsagio@gmail.com).
Goal: determine what that material is, whether it fixes attestation, and what Soft Space / Play actually require.
2. How we narrowed the failure (methodology)¶
2.1 Map the card charge pipeline in code¶
We traced apps/sagio from UI to network:
| Step | Component | What it proves |
|---|---|---|
| Auth | SettingsRepository → POST /merchant-app/auth |
SAGIO_API_KEY / SAGIO_API_SECRET work |
| Config fetch | GET /merchant-app/fasspay-config |
Fasstap env on API worker (FASSTAP_*) |
| Settings READY | MerchantConfig.isValid() |
Only 4 fields: accessKey, secretKey, uniqueId, developerId |
| Payment request | PaymentRequestRepository.createPaymentRequest() |
Sagio backend accepts WORLDLINE rail |
| SDK init | FasstapManager.initialize() |
Soft Space SDK config applied |
| Attestation | FasstapManager.ensureTokenRefreshed() → refreshToken() |
Failure reported here |
| NFC | startTransaction() |
Not reached if refresh fails |
Relevant files:
apps/sagio/app/src/main/java/io/sagio/merchant/viewmodel/PaymentViewModel.kt(steps 1–5 inprocessCardPayment)apps/sagio/app/src/main/java/io/sagio/merchant/sdk/FasstapManager.ktapps/sagio/app/src/main/java/io/sagio/merchant/data/MerchantConfig.kt(isValid())apps/sagio/app/src/main/java/io/sagio/merchant/ui/screens/SettingsScreen.kt(READY =uiState.isValid)apps/api/src/services/merchantAppService.ts(getFasstapConfigForMerchant)apps/api/src/config/index.ts(buildFasspayConfig()— UAT hosts, cert pin, Play project757874674469)
Conclusion: If READY and payment request creation succeed, the Sagio API + Fasstap credential fetch path is working. Attestation is a separate step inside the Soft Space SDK talking to mpos-uat.fasspay.com:9001 with Play Integrity tied to the APK signing certificate.
2.2 What “READY” does and does not mean¶
From MerchantConfig.kt:
fun isValid(): Boolean {
return accessKey.isNotBlank() &&
secretKey.isNotBlank() &&
uniqueId.isNotBlank() &&
developerId.isNotBlank()
}
READY means: server returned the four Soft Space integrator/portal fields and the app persisted them.
READY does not mean:
SSMPOSSDK.init()succeeded (check logcat:SDK initialized: version=3.6.5.0)refreshToken()/ attestation succeeded- Play Integrity passed
- Soft Space whitelisted this device’s signing cert
This explains the reported symptom precisely: env vars OK + READY + attestation fail.
2.3 Ruled out vs still in play (verified 2026-05-22)¶
Cloudflare account: nzsagio@gmail.com (29d5f87ef1dc233655dc802fc1c55317) via wrangler whoami.
Workers: sagio-api-prod (default env ""), sagio-api-dev (--env=sagio-api-dev).
Secrets refreshed from apps/api/.env.local:
cd apps/api
bun run deploy:fasstap-secrets -- .env.local # prod (sagio-api-prod)
bun run deploy:fasstap-secrets -- .env.local sagio-api-dev
Live prod smoke test (Chris Kwon merchant terminal key from apps/sagio/local.properties):
| Endpoint | Result |
|---|---|
POST /merchant-app/auth |
success: true, merchant Chris Kwon |
GET /merchant-app/fasspay-config |
success: true, environment: UAT, all four required fields set, attestationHost + googlePlayProjectNumber: 757874674469, keyloadingCACert present (1020 chars) |
Ruled out (with evidence)¶
| Item | Why |
|---|---|
Missing FASSTAP_* on prod worker |
All six secrets on sagio-api-prod; live fasspay-config returns full payload |
Missing FASSTAP_* on dev worker |
All six secrets on sagio-api-dev after 2026-05-22 deploy |
| Wrong / missing Sagio terminal API key | Prod auth succeeds for Chris’s minted key |
buildFasspayConfig() empty on Workers |
Historical bug (import-time config); current worker returns hosts, pins, CA, Play project number |
| Sagio API layer alone blocking attestation | Attestation runs in SDK after READY; API path verified end-to-end |
Wrong FASSTAP_ACCESS_KEY vs Soft Space email |
Matched Soft Space UAT accessKey in .env.local |
Wrong FASSTAP_SECRET_KEY vs Soft Space email |
Was mismatched — fixed 2026-05-22; redeployed to prod + dev wrangler |
Fixed 2026-05-22 (was still in play)¶
| Item | Action |
|---|---|
Stale FASSTAP_SECRET_KEY on worker |
Replaced with value from Soft Space email; wrangler secret put on prod + dev |
Still in play (likely cause of READY + attestation fail)¶
| Item | What to do |
|---|---|
| APK signing cert ≠ Soft Space whitelist | Compare apksigner on Chris’s APK vs sagio_app_final.cert in ~/wl — see softspace-exchange-archive.md |
| Debug / Diawi APK vs Play/HSM cert whitelisted | Whitelist the cert for the installed build channel (debug SHA 8B:22:30:7E:... vs Play app signing cert) |
| Soft Space UAT whitelist not active | Re-send fingerprint; confirm with Soft Space |
| Wrong MUID/SSO for portal user | Rare if READY; re-check FASSTAP_UNIQUE_ID / FASSTAP_DEVELOPER_ID vs Soft Space portal |
| Device: emulator, root, no Play services | Use physical device, updated Play services |
Network to mpos-uat.fasspay.com:9001 |
Test from device network |
Bottom line: Sagio Cloudflare + API path is verified working. Remaining work is Soft Space Play Integrity cert whitelist matched to Chris’s actual APK, not more Wrangler secrets.
2.4 Soft Space / Play Integrity requirement (from specs + SDK)¶
From docs/specs/tap2pay/003-fasstap-configuration.md and packages/worldline-sdk/src/constants/errorCodes.ts:
- Play Integrity uses Soft Space GCP project
757874674469(not SAGIO’s project). - Package must be whitelisted:
io.sagio.merchant. - Signing certificate SHA-256 for the installed APK must also be whitelisted.
- SDK error codes include
ATTESTATION_FAILED(8002),PLAY_INTEGRITY_FAILED(8006).
From docs/handoffs/merchant-terminal-launch.md troubleshooting row:
SDK init fails, Settings READY → Play Integrity / cert → Send SHA-256 to Soft Space
3. Investigating the Cloud HSM artifacts¶
3.1 Attestation file (*-CAVIUM_V2_COMPRESSED-attestation.dat)¶
Location checked: ~/Downloads/sagio-production-ring-sagio-app-signer-1-CAVIUM_V2_COMPRESSED-attestation.dat
Command used:
file sagio-production-ring-sagio-app-signer-1-CAVIUM_V2_COMPRESSED-attestation.dat
# → gzip compressed data, 1002 bytes → 2528 bytes decompressed
Decompressed with Python gzip.decompress():
- Binary TLV-style vendor payload (Cavium/Cloud HSM key attestation, format
CAVIUM_V2_COMPRESSED). - Contains label
app_keyand a 128-hex key ID (two concatenated SHA-256 digests per Google docs). - No PEM
BEGIN CERTIFICATEblock; no DER certificate (30 82) in the blob.
Key ID verified against KMS resource name (2026-05-22):
RESOURCE_NAME="projects/sagio-484122/locations/australia-southeast1/keyRings/sagio-production-ring/cryptoKeys/sagio-app-signer/cryptoKeyVersions/1"
echo -n "$RESOURCE_NAME" | openssl dgst -sha256 -hex
# SHA2-256(stdin)= f679bb1bba71cf6b2a16799b4121e5554ca8cbbfe6c41a6b838399803524ed72
Second half of the attestation key-id field matches exactly → this .dat is for sagio-app-signer cryptoKeyVersions/1.
Reference: Google Cloud KMS — Verifying attestations
Conclusion: This file proves the key material resides in Cloud HSM. It is not the Android signing certificate and cannot be pasted into Soft Space as a Play Integrity allowlist fingerprint. For Soft Space, use Chris’s sagio_app_final.cert instead (see §3.4).
3.4 Chris’s Soft Space exchange archive (~/wl)¶
Chris reported these are the exact files used in the Soft Space exchange:
| File | Role |
|---|---|
sagio_application_id.txt |
Package allowlist (io.sagio.merchant) |
sagio_pk.pem |
Public key paired with app signing cert |
sagio_app_final.cert |
X.509 cert whose SHA-256 Soft Space whitelists |
SAGIO_id_cert |
Integrator / identity cert (onboarding) |
softspace-attestation.asc |
Soft Space signed record (PGP/armored) |
Not present on SAGIO machine — only on Chris’s ~/wl. Full mapping and verification commands: softspace-exchange-archive.md.
Investigation implication: If Soft Space whitelisted sagio_app_final.cert but Chris installs a debug or upload-key APK, Play Integrity still fails with READY. The decisive test is apksigner on Chris’s APK vs openssl x509 -fingerprint -sha256 on sagio_app_final.cert.
3.2 Public key PEM (from Cloud HSM / console)¶
Stored in repo: apps/sagio/signing/sagio-app-signer.public.pem
Analysis:
openssl pkey -pubin -in sagio-app-signer.public.pem -outform DER | openssl dgst -sha256
# SPKI SHA-256: 649079b06f9210a42e482a638586d292247fd471675f08921d3b90fcaac0611c
# Android format: 64:90:79:B0:6F:92:10:A4:2E:48:2A:63:85:86:D2:92:24:7F:D4:71:67:5F:08:92:1D:3B:90:FC:AA:C0:61:1C
- Algorithm: RSA 2048 (modulus bit length 2048 via Python
cryptography).
Important: Android / Play Integrity / Soft Space whitelist use the X.509 certificate SHA-256 (DER of the whole cert), not the SPKI hash above. Those differ whenever the cert has issuer/subject/validity wrapping the same public key.
Authoritative cert fingerprint must come from one of:
gcloud kms keys versions get-certificate \
--project=sagio-484122 \
--location=australia-southeast1 \
--keyring=sagio-production-ring \
--key=sagio-app-signer \
--version=1 \
--certificate-output-file=/tmp/sagio-app-signer.crt
openssl x509 -in /tmp/sagio-app-signer.crt -noout -fingerprint -sha256
Or Play Console → App signing key certificate SHA-256 (must match this public key).
gcloud was not available in the investigation environment; SAGIO must run the command locally or read Play Console.
3.3 Unrelated private key in Downloads¶
A file key_e6cde843-cbdd-44ed-b0c5-875a2117ea64.pem was present locally. We compared RSA moduli:
Conclusion: That PEM is not the sagio-app-signer HSM key. Do not assume it is the Play upload key without checking against CI SAGIO_KEYSTORE / Play Console. Never commit private keys to the repo.
4. Three Android signing keys (root cause of confusion)¶
Investigation of apps/sagio/SIGNING.md and .github/workflows/sagio-android-build.yml shows three distinct keys:
flowchart LR
subgraph dev [Local debug]
D[debug.keystore] --> APKd[debug APK]
end
subgraph ci [CI release]
U[SAGIO_KEYSTORE secrets] --> APKr[release APK / Diawi]
end
subgraph play [Google Play]
U2[Upload key] --> AAB[AAB upload]
AAB --> GP[Play re-sign]
HSM[sagio-app-signer Cloud HSM] --> GP
GP --> APKp[Play-installed APK]
end
APKd --> PI[Play Integrity sees debug cert]
APKr --> PI2[Play Integrity sees upload cert]
APKp --> PI3[Play Integrity sees app signing cert]
| Key | Material location | Signs which APK | Soft Space must whitelist for that APK |
|---|---|---|---|
| Debug | ~/.android/debug.keystore |
./gradlew assembleDebug |
Debug SHA-256 (see below) |
| Play upload | GitHub SAGIO_KEYSTORE_* → release-keystore.jks |
CI assembleRelease, Diawi links |
Upload keystore SHA-256 |
| Play app signing | Cloud HSM sagio-app-signer |
APKs installed from Play after re-sign | gcloud get-certificate or Play Console |
Critical insight: Sending only the HSM / Play App Signing fingerprint to Soft Space does not fix attestation on a debug or Diawi release APK. Chris’s installed build type determines which fingerprint Soft Space needs.
4.1 Fingerprints computed during investigation¶
| Source | SHA-256 (colon-separated) | Notes |
|---|---|---|
| Debug keystore (this machine) | 8B:22:30:7E:BB:87:8E:A6:61:3B:51:44:AF:31:E7:EA:2A:99:49:AF:62:25:2C:E9:D5:02:8C:06:EB:FD:23:FD |
Standard Android debug; use for local UAT APKs |
| HSM public key SPKI only | 64:90:79:B0:6F:92:10:A4:2E:48:2A:63:85:86:D2:92:24:7F:D4:71:67:5F:08:92:1D:3B:90:FC:AA:C0:61:1C |
Reference only — not a substitute for cert fingerprint |
| HSM X.509 cert | Pending | Run gcloud kms keys versions get-certificate on SAGIO workstation |
| Upload keystore | Pending | keytool -list -v on CI release-keystore.jks or apksigner on Chris’s APK |
Best practice: apksigner verify --print-certs <exact.apk> on the APK Chris installed — removes all ambiguity.
Helper script added: apps/sagio/scripts/print-softspace-fingerprints.sh
5. How Sagio API config relates (layer 1 vs layer 2)¶
Layer 1 — Sagio API (working when READY)¶
| Item | Where |
|---|---|
| Terminal auth | POST /merchant-app/auth |
| Fasstap config | GET /merchant-app/fasspay-config |
| Worker secrets | FASSTAP_ACCESS_KEY, SECRET_KEY, UNIQUE_ID, DEVELOPER_ID, ENVIRONMENT |
| Baked UAT defaults | Attestation host, cert pinning, keyloading CA, 757874674469 |
Deploy: cd apps/api && bun run deploy:fasstap-secrets -- .env.local
Layer 2 — Soft Space SDK on device (attestation)¶
| Item | Where |
|---|---|
| SDK init | FasstapManager.initialize() |
| Attestation | refreshToken() → https://mpos-uat.fasspay.com:9001/v2 |
| Play Integrity | APK signing cert + package io.sagio.merchant |
| Whitelist owner | Soft Space (Worldline NZ) |
Chris’s case: Layer 1 OK (READY). Layer 2 blocked → signing cert whitelist (most likely) or MUID/network/device.
6. Final conclusions¶
| # | Conclusion |
|---|---|
| 1 | SAGIO API creds alone are insufficient for card tap; Fasstap worker secrets + terminal key are also required for READY. |
| 2 | READY + attestation fail is consistent with correct Sagio/Fasstap fetch and failed Play Integrity / Soft Space device trust. |
| 3 | The HSM .dat attestation file is not used for Soft Space whitelist. |
| 4 | The HSM public key is the Play App Signing key; cert SHA-256 must be exported via gcloud or Play Console. |
| 5 | Three signing keys exist; whitelisting the wrong one (e.g. HSM only while testing debug APK) preserves the symptom. |
| 6 | Fastest UAT unblock if Chris uses debug APK: whitelist debug SHA-256 8B:22:30:7E:... (see softspace-play-integrity-whitelist.md). |
| 7 | Production path: whitelist Play App Signing cert from HSM + distribute via Play internal testing so installed cert matches. |
7. Recommended actions (in order)¶
- Confirm Chris’s APK source — debug local build, Diawi release, or Play internal test.
- Extract fingerprint from that APK —
./apps/sagio/scripts/print-softspace-fingerprints.sh /path/to.apkoradb pull+apksigner. - Email Soft Space — package
io.sagio.merchant, Play project757874674469, that SHA-256, MUID/SSO from portal. Template in softspace-play-integrity-whitelist.md. - For Play/HSM track — run
gcloud kms keys versions get-certificateand match Play Console app signing cert toapps/sagio/signing/sagio-app-signer.public.pem. - After whitelist — reinstall APK, Settings → Refresh, retry card charge; collect logcat if still failing.
8. Artifacts added to the repo (this investigation)¶
| Path | Purpose |
|---|---|
docs/handoffs/merchant-terminal-attestation-investigation.md |
This document — reasoning and evidence |
docs/handoffs/softspace-play-integrity-whitelist.md |
Operational whitelist steps |
docs/handoffs/merchant-terminal-card-attestation.md |
Partner FAQ / config layers |
apps/sagio/signing/sagio-app-signer.public.pem |
HSM app signing public key (safe to commit) |
apps/sagio/scripts/print-softspace-fingerprints.sh |
Print debug / SPKI / APK cert fingerprints |
apps/sagio/SIGNING.md |
Updated with HSM KMS path and script link |
PR: docs/merchant-terminal-card-attestation branch → GitHub PR #223 (docs only; merge to master for team visibility).
9. Open items (need SAGIO input)¶
- Deploy / refresh
FASSTAP_*on Cloudflare (nzsagio@gmail.com) — prod + dev, 2026-05-22 - Align
FASSTAP_SECRET_KEYwith Soft Space UAT email (Liew Zhi Ying) and redeploy - Prod smoke: auth +
fasspay-configfor Chris Kwon merchant - Soft Space confirmed cert verified + app whitelisted in UAT (email to Deb)
- Chris zips
~/wland shares with SAGIO (five Soft Space exchange files). - Run checklist in softspace-exchange-archive.md — especially APK cert vs
sagio_app_final.cert. - Confirm how Chris installed the app (debug / Diawi / Play).
- Run
gcloud kms keys versions get-certificateand confirm cert matchessagio_app_final.cert/sagio_pk.pem. - Confirm Soft Space UAT whitelist is active for the cert on Chris’s actual APK.
- Optional: paste logcat
statusMessagefrom failedrefreshToken()for Soft Space ticket.
10. References¶
- Code:
PaymentViewModel.kt,FasstapManager.kt,MerchantConfig.kt,merchantAppService.ts,buildFasspayConfig() - Specs:
docs/specs/tap2pay/003-fasstap-configuration.md,004-fasstap-visual-architecture.md - Google: KMS attest-key, Android client auth / cert fingerprints
- Launch handoff:
docs/handoffs/merchant-terminal-launch.md