Skip to content

SAGIO KMS APK Signing Handoff

Context

The SAGIO Android terminal (apps/terminal, package io.sagio.merchant) was failing on a Motorola test device with payment failed unlock fail.

The API-side FASSTAP credential path was checked first:

  • Local database contains merchant_worldline_onboarding and merchant_worldline_terminals.
  • Prod merchant app auth succeeds for merchant d365d71d-170d-4ad0-ba5e-ed549b33dc17.
  • Prod /api/merchant-app/fasspay-config returns all required fields.

That moved the active blocker from API config to Android signing / Soft Space Play Integrity allowlisting.

Signing Findings

The Soft Space bundle contained public signing material for SAGIO:

Artifact Meaning
sagio_app_final.cert Public X.509 app-signing certificate Soft Space whitelisted
sagio_pk.pem Public key matching the certificate
sagio_application_id.txt Application ID: io.sagio.merchant
*.asc files PGP exchange artifacts, not Android private keys

Soft Space whitelisted certificate SHA-256:

62:B4:BA:90:BF:83:83:47:8A:F2:B4:02:19:1E:DE:92:50:CE:35:C8:68:83:B4:D5:5D:57:12:24:BB:40:48:F7

The local debug APK from ./gradlew assembleDebug was signed with:

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

So a debug install does not match the Soft Space whitelist.

The 62:B4... certificate public key matches:

  • apps/terminal/signing/sagio-app-signer.public.pem
  • Google Cloud KMS key version: projects/sagio-484122/locations/australia-southeast1/keyRings/sagio-production-ring/cryptoKeys/sagio-app-signer/cryptoKeyVersions/1

The private key is HSM-backed and cannot be exported. It can only be used by calling Cloud KMS asymmetricSign.

Implemented Local KMS Signer

The custom signer lets us produce a direct-install APK signed with the Soft Space-whitelisted 62:B4... certificate without exporting private key material.

Files:

  • apps/terminal/tools/kms-apk-signer/src/main/java/io/sagio/tools/kmsapksigner/KmsApkSigner.java
  • apps/terminal/tools/kms-apk-signer/src/main/resources/META-INF/services/com.android.apksig.kms.KmsSignerEngineProvider
  • apps/terminal/scripts/sign-release-with-kms.sh

Local-only file:

  • sagio_app_final.cert, placed at apps/terminal/signing/sagio_app_final.cert, ~/wl/sagio_app_final.cert, or any path pointed to by SAGIO_APP_SIGNING_CERT

The certificate file is intentionally ignored by git.

How it works:

  1. Builds/uses an unsigned release APK.
  2. Uses Android apksig with a custom KMS SignerEngine.
  3. Sends SHA-256 digests to Google Cloud KMS asymmetricSign.
  4. Embeds sagio_app_final.cert in the APK signing block.
  5. Verifies the output APK with apksigner.

Prerequisites

Install Google Cloud CLI:

brew install --cask google-cloud-sdk

Authenticate and select the project:

gcloud auth login
gcloud config set project sagio-484122

The signing principal needs one of these roles on the KMS key:

  • Cloud KMS CryptoKey Signer
  • Cloud KMS CryptoKey Signer/Verifier

In the current setup, nzsagio@gmail.com has Cloud KMS CryptoKey Signer/Verifier.

Place the public app-signing certificate from the Soft Space exchange at one of these local paths:

mkdir -p apps/terminal/signing
cp /path/to/sagio_app_final.cert apps/terminal/signing/sagio_app_final.cert

or:

export SAGIO_APP_SIGNING_CERT="/path/to/sagio_app_final.cert"

Build And Sign

From the repo:

cd apps/terminal
./gradlew assembleRelease
./scripts/sign-release-with-kms.sh

Default input:

app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk

Default output:

app/build/outputs/apk/release/app-arm64-v8a-release-kms-signed.apk

Absolute output path from this workstation:

/Users/lulzx/work/sagio/apps/terminal/app/build/outputs/apk/release/app-arm64-v8a-release-kms-signed.apk

Successful signing output includes:

Signed APK: .../app-arm64-v8a-release-kms-signed.apk
Certificate SHA-256: 62:B4:BA:90:BF:83:83:47:8A:F2:B4:02:19:1E:DE:92:50:CE:35:C8:68:83:B4:D5:5D:57:12:24:BB:40:48:F7

apksigner verification should include:

Verifies
Verified using v3 scheme (APK Signature Scheme v3): true
Number of signers: 1
Signer #1 certificate DN: CN=SAGIO
Signer #1 certificate SHA-256 digest: 62b4ba90bf8383478af2b402191ede9250ce35c86883b4d55d571224bb4048f7

v3: true is acceptable because apps/terminal has minSdk = 29, and APK Signature Scheme v3 is supported on API 28+.

Install On Motorola

If a debug-signed build is already installed, uninstall first because Android will reject an update signed by a different cert:

adb uninstall io.sagio.merchant || true
adb install app/build/outputs/apk/release/app-arm64-v8a-release-kms-signed.apk

Then:

  1. Open SAGIO.
  2. Sign in.
  3. Go to Settings -> Refresh SDK Credentials.
  4. Try Charge again.

Troubleshooting

Cannot run program "gcloud":

  • Install Google Cloud CLI, or set KMS_SIGNER_ACCESS_TOKEN to a short-lived token from an authenticated environment.
export KMS_SIGNER_ACCESS_TOKEN="<token from: gcloud auth print-access-token>"
./scripts/sign-release-with-kms.sh

KMS returns 403:

  • The active Google account lacks cloudkms.cryptoKeyVersions.useToSign.
  • Grant Cloud KMS CryptoKey Signer or Cloud KMS CryptoKey Signer/Verifier on the sagio-app-signer key.

INSTALL_FAILED_UPDATE_INCOMPATIBLE:

  • A differently signed APK is installed. Run:
adb uninstall io.sagio.merchant
adb install app/build/outputs/apk/release/app-arm64-v8a-release-kms-signed.apk

Soft Space still reports unlock fail:

  • Capture a fresh logcat from the signed APK:
adb logcat -c
# trigger the Charge failure
adb logcat -d | grep -iE "FasstapManager|SSPOG|unlock|SDK initialized|RefreshToken|integrity|attestation|softspace|ssmpos"

If Settings -> Refresh SDK Credentials succeeds and this APK is signed with 62:B4..., the remaining blocker is likely Soft Space-side provisioning, device/network policy, or MUID/SSO alignment rather than Sagio API config.