Signing not possible with an AndroidKeyStore key that has .setUserAuthenticationRequired(true)

Issue #176 closed
Joseph Heenan created an issue

The AndroidKeyStore has some particularly funky stuff, and I’ve run into a brick wall trying to figuring out how to use it to sign a jws without doing some really quite nasty things.

In particular the Android HSM/TEE can (if you do setUserAuthenticationRequired(true)when creating the key in the HSM) protect a key completely from the OS, to the point where the hardware insists that every signature operation requires the user to present their biometric. (This is a pretty great thing for security, as even if the device is thoroughly compromised, not only is the attacker unable to extract the private key material, they can’t even sign something with the key without the user that owns the relevant finger/other appendage being actively involved.)

The problem arises around this little bit of code:

public byte[] sign(Key key, byte[] securedInputBytes, ProviderContext providerContext) throws JoseException
{
    Signature signature = getSignature(providerContext);
    initForSign(signature, key, providerContext);
    try
    {
        signature.update(securedInputBytes);
        return signature.sign();
    }
    catch (SignatureException e)
    {
        throw new JoseException("Problem creating signature.", e);
    }
}

(from BaseSignatureAlgorithm.java )

In particular after the .update() on line 6, it’s necessary to call Android’s BiometricPrompt.authenticate() passing it signature - it then completes asynchronously and only when it calls back to the onAuthenticationSucceeded method (once the user has provided their finger/iris/face/whatever) can you go on to call signature.sign()to retrieve the signature.

The asynchronous nature makes it particularly tricky to do - the only solutions I’ve come up with is:

  1. A fairly nasty subclassing of JsonWebStructure to essentially expose some of it’s privates - so you do the actual underlying signature operation outside of jose4j and inject it back in
  2. Something fairly nasty with running jws.getCompactSerialization(); on a background thread/executor and blocking that thread whilst the BiometricPrompt part happens.
  3. Some kind of fairly invasive looking refactor to split sign() up into a ‘pre sign step’ and ‘signature available’ step.

I’m not sure if I’ve missed some easier solution though - can you see a better one?

Comments (17)

  1. Brian Campbell repo owner

    Hello there @josephheenan, this is a fun one for sure. Ideally we could support this in way that’s not too awkward to use but also doesn’t break the current APIs and won’t impact folks doing things the regular old way. I’m honestly not sure how to accomplish that though. And think I’d need to spend some time digging into it (fighting with it). Would you by chance have anything that could help bootstrap my efforts? My Android skills are pretty slim and basically amount to having Android Studio installed. Like perhaps a project you’ve been using to experiment that would show usage of the Android HSM/TEE and Bio Prompt stuff and be runnable in the emulator. That’d help me get to a place where I can play around with it and try and come up with something clean.

  2. Joseph Heenan

    Hi Brian

    Thanks! Yes, I can certainly share an example project, once I have one working (for reasons currently unclear to me my current test apps seem to end up generating an invalid signature, which I wasn’t worrying about too much because one of my colleagues said he’d got everything fully working in a different way in the production full app, but further investigation revealed that app was also generating invalid signatures…).

    The good news is I’m fairly certain I got the place where the biometric authentication needs to happen slightly wrong and it actually can/should/must be before the .update() call, which means the solution is I think easier (perhaps an API to create a signature object, then adding a new variant of sign() that accepts the Signature object once it’s been biometric authorized).

    I’ll get back to you with a test project once I’ve actually managed to produce a valid signed JWT 🙂

  3. Brian Campbell repo owner

    Thanks! I look forward to it (kinda). And that is likely good news regrading the timing of the call.

  4. Brian Campbell

    I don’t even necessarily need something that’s working to produce a valid signed JWT. Just having the scaffolding around the android stuff would be super helpful.

  5. Joseph Heenan

    I figured out the stupid thing I did. Should have some working (but very hacky) code for you shortly 🙂

  6. Brian Campbell repo owner

    Kinda clever what you did to get it to work without changing the library 🙂

    But ugh… the abstractions don’t line up quite right. I’m struggling to come up with an approach that’s not too ugly. I’m working on it though.

  7. Brian Campbell repo owner

    958d339 introduces a new prepareSigningPrimitive method on JsonWebSignature that gives access to the underlying Signature instance, which should much more easily allow for setting up the BiometricPrompt with the Signature. Using it might look something like this:

    jws.setPayload(...);
    jws.setKey(privateKey);
    jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256);
    
    CryptoPrimitive cryptoPrimitive = jws.prepareSigningPrimitive();
    Signature signature = cryptoPrimitive.getSignature();
    biometricPrompt.authenticate(promptInfo, new BiometricPrompt.CryptoObject(signature));
    

    and then in the onAuthenticationSucceeded(...) callback you can finish the job by calling getCompactSerialization() on the JWS.

  8. Joseph Heenan

    Nice! Thanks Brian. That’s a much neater change to the library than I was fearing, I struggled to see a route that didn’t involve a major refactor but it’s actually shaken out well.

    Any chance of a release soon please? 🙂

  9. Brian Campbell repo owner

    Some of the “internal” changes hurt my aesthetic sensibilities quite a lot but the ‘external’ API change didn’t turn out too bad at all.

    I’m working on similar updates for decryption on the JWE side with a708d41 but should be able to do a release pretty soon.

  10. Log in to comment