Skip to content

Instantly share code, notes, and snippets.

@abergs
Created March 13, 2024 13:22
Show Gist options
  • Save abergs/add887112aee601da55b294e5487b6d3 to your computer and use it in GitHub Desktop.
Save abergs/add887112aee601da55b294e5487b6d3 to your computer and use it in GitHub Desktop.
Passkey client/authenticator on Android

We are encountering an issue where if we try to supply our own Client JSON response, then somewhere between our code and the browser, the clientDataJSON bytes will have been replaced, causing the signature verification performed by the RP to fail.

Our questions are:

  • Is the clientDataJSON override intentional?
  • If we go straight for the authenticator, will the Android OS still implement the necessary Client protections (e.g. checking origin vs rpId)?

We provide our own JSON like this:

var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);

var credentialOption = getRequest?.CredentialOptions.FirstOrDefault();
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
            
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);

var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);

var origin = getRequest?.CallingAppInfo.Origin;

var assertParams = new Fido2ClientAssertCredentialParams()
{
    Challenge = requestOptions.GetChallenge(),
    Origin = origin,
    RpId = requestOptions.RpId,
    AllowCredentials = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] 
    {
        new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
        {
            Id = credentialId
        }
    },
    SameOriginWithAncestors = false, //TODO: Confirm if this is needed and where to get it.
    Timeout = Convert.ToInt32(requestOptions.Timeout),
    UserVerification = requestOptions.UserVerification
};
var assertResult = await _fido2ClientService.AssertCredentialAsync(assertParams);

//TODO: Code below until end of method is using alternative method of building Json ourselves
var responseInnerAndroidJson = new JSONObject();
responseInnerAndroidJson.Put("clientDataJSON", b64Encode(assertResult.ClientDataJSON));
responseInnerAndroidJson.Put("authenticatorData", b64Encode(assertResult.AuthenticatorData));
responseInnerAndroidJson.Put("signature", b64Encode(assertResult.Signature));
responseInnerAndroidJson.Put("userHandle", b64Encode(assertResult.UserHandle));

var rootAndroidJson = new JSONObject();
rootAndroidJson.Put("id", b64Encode(assertResult.RawId));
rootAndroidJson.Put("rawId", b64Encode(assertResult.RawId));
rootAndroidJson.Put("authenticatorAttachment", "platform");
rootAndroidJson.Put("type", "public-key");
rootAndroidJson.Put("clientExtensionResults", new JSONObject());
rootAndroidJson.Put("response", responseInnerAndroidJson);

var responseAndroidJson = rootAndroidJson.ToString();

var result = new Intent();
var cred = new PublicKeyCredential(responseAndroidJson);
var credResponse = new GetCredentialResponse(cred);
PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
SetResult(Result.Ok, result);
Finish();

On the other hand if we bypass our Client implementation and instead use our Authenticator directly, letting FidoPublicKeyCredential create the Client JSON response instead, everything works fine.

var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
var credentialOption = getRequest?.CredentialOptions.FirstOrDefault();
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
var clientDataHash = credentialPublic.GetClientDataHash();

// ...

var assertParams = new Fido2AuthenticatorGetAssertionParams
{
    Challenge = requestOptions.GetChallenge(),
    RpId = requestOptions.RpId,
    UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(requestOptions.UserVerification),
    Hash = credentialPublic.GetClientDataHash(),
    AllowCredentialDescriptorList = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
    {
        new Core.Utilities.Fido2.PublicKeyCredentialDescriptor
        {
            Id = credentialId
        }
    },
    Extensions = new object()
};
var assertResult = await _fido2AuthenticatorService.GetAssertionAsync(assertParams, userInterface);

var response = new AuthenticatorAssertionResponse(
    requestOptions,
    assertResult.SelectedCredential.Id,
    androidOrigin,
    false, // These flags have no effect, we set our own within `SetAuthenticatorData`
    false,
    false,
    false,
    assertResult.SelectedCredential.UserHandle,
    packageName,
    credentialPublic.GetClientDataHash()
);
response.SetAuthenticatorData(assertResult.AuthenticatorData);
response.SetSignature(assertResult.Signature);

var result = new Intent();
var fidoCredential = new FidoPublicKeyCredential(assertResult.SelectedCredential.Id, response, "platform");
var cred = new PublicKeyCredential(fidoCredential.Json());
var credResponse = new GetCredentialResponse(cred);
PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment