microg/GmsCore · error · RuntimeException

Invalid result

Error message

Invalid result

What it means

In HybridAuthenticateActivity.handleMakeCredential, after deserializing the returned PublicKeyCredential, the code attempts a safe cast of its response to AuthenticatorAttestationResponse; if the cast fails (it is not an attestation response) it throws RuntimeException("Invalid result"). The result payload exists but has the wrong response type for a make-credential flow.

Source

Thrown at play-services-fido/core/src/main/kotlin/org/microg/gms/fido/core/ui/hybrid/HybridAuthenticateActivity.kt:177

        val browserOptions = BrowserPublicKeyCredentialCreationOptions.Builder()
            .setPublicKeyCredentialCreationOptions(publicKeyCredentialCreationOptions)
            .setOrigin("https://${request.rp.id}".toUri())
            .setClientDataHash(request.clientDataHash).build()

        val intent = Intent(this, AuthenticatorActivity::class.java)
            .putExtra(AuthenticatorActivity.KEY_SOURCE, AuthenticatorActivity.SOURCE_HYBRID)
            .putExtra(AuthenticatorActivity.KEY_TYPE, AuthenticatorActivity.TYPE_REGISTER)
            .putExtra(AuthenticatorActivity.KEY_OPTIONS, browserOptions.serializeToBytes())

        val result = suspendCancellableCoroutine { continuation ->
            waitingLauncherContinuation = continuation
            waitingLauncher.launch(intent)
        }

        val resultBytes = result.data?.getByteArrayExtra(FIDO2_KEY_CREDENTIAL_EXTRA) ?: throw RuntimeException("No result")
        val publicKeyCredential = PublicKeyCredential.deserializeFromBytes(resultBytes)
        val response = publicKeyCredential.response as? AuthenticatorAttestationResponse? ?: throw RuntimeException("Invalid result")
        val attestationObject = AttestationObject.decode(response.attestationObject)

        return AuthenticatorMakeCredentialResponse(
            authData = attestationObject.authData,
            fmt = attestationObject.fmt,
            attStmt = attestationObject.attStmt
        )
    }

    private suspend fun handleGetAssertion(request: AuthenticatorGetAssertionRequest): AuthenticatorGetAssertionResponse {
        val publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions.Builder()
            .setRpId(request.rpId)
            .setChallenge(request.clientDataHash)
            .setAllowList(request.allowList)
            .setRequireUserVerification(request.options?.userVerification?.takeIf { it }?.let { UserVerificationRequirement.REQUIRED })
            .build()

        val browserOptions = BrowserPublicKeyCredentialRequestOptions.Builder()

View on GitHub (pinned to 157c9d86ac)

Solutions

  1. Verify the launched intent requests a make-credential (register) operation, not get-assertion
  2. Log publicKeyCredential.response::class to see the actual type returned
  3. Ensure the FIDO request/options passed to the inner activity are PublicKeyCredentialCreationOptions, not RequestOptions
  4. Update microG so response deserialization matches the request type

Example fix

// before
val response = publicKeyCredential.response as? AuthenticatorAttestationResponse? ?: throw RuntimeException("Invalid result")
// after
val response = publicKeyCredential.response as? AuthenticatorAttestationResponse
    ?: run {
        Log.e(TAG, "Expected attestation response, got ${publicKeyCredential.response?.javaClass}")
        throw RuntimeException("Invalid result")
    }
Defensive patterns

Strategy: type-guard

Validate before calling

require(publicKeyCredential.response is AuthenticatorAttestationResponse) {
    "Expected attestation response, got ${publicKeyCredential.response?.javaClass}"
}

Type guard

fun PublicKeyCredential.isAttestation(): Boolean =
    response is AuthenticatorAttestationResponse

Try / catch

try {
    val response = handleMakeCredential()
} catch (e: RuntimeException) {
    if (e.message == "Invalid result") emit(FidoResult.WrongResponseType) else throw e
}

Prevention

When it happens

Trigger: The nested activity returned a credential whose response is an AuthenticatorAssertionResponse (get-assertion) instead of AuthenticatorAttestationResponse, or deserialization produced a response of an unexpected subtype.

Common situations: Mixing up sign-in and sign-up intents when launching the hybrid flow; a library version where deserializeFromBytes yields a different response class; the inner activity short-circuits with a previously-registered credential.

Related errors


AI-assisted analysis of microg/GmsCore@157c9d86ac (2026-09-06). Data as JSON: /api/errors/b6eda0ceca437be3. Report an issue: GitHub.