简体   繁体   中英

Android - require fingerprint authentication to access (RSA/PSS) signing key

I'm currently creating a form of challenge-response authentication for a project I need for my Master thesis in computer science.

For this purpose, I need to create an RSA-PSS signature with a private key that is authenticated by a fingerprint so that it can only be used to create a signature when the owner of the device is physically present.

To achieve this, I use the Android KeyStore (backed by Keymaster/Gatekeeper in ARM TrustZone ) to generate an RSA key pair ( KEY_ALGORITHM_RSA ) for use with the RSA-PSS signature algorithm ( SIGNATURE_PADDING_RSA_PSS ) for creating and verifying signatures ( PURPOSE_SIGN | PURPOSE_VERIFY ). I also require user authentication by setting the corresponding property to true .

Later, to create the signature over a buffer final byte[] message , I ...

  1. obtain an instance of the FingerprintManager service
  2. create an instance of the SHA512withRSA/PSS signature algorithm ( Signature object)
  3. initialize the Signature algorithm for signing with the private key ( initSign(...) )
  4. wrap the Signature object into a CryptoObject
  5. (perform some additional checks)
  6. authenticate(...) the CryptoObject using the instance of FingerprintManager , passing (among others) a FingerprintManager.AuthenticationCallback to be called after the key has been authenticated by the user (by touching the fingerprint sensor on his/her device)

Inside the callback, use of the key is authenticated, so I ...

  1. extract the Signature object from the CryptoObject wrapper again
  2. use the update(...) method on the Signature object to stream the data to be signed ( message ) into the signature algorithm
  3. use the sign() method on the Signature object to obtain the signature
  4. encode that signature as Base64 and println(...) it out to StdErr so it appears in adb logcat

I created a sample code which is about as minimal as it gets.

package com.example.andre.minimalsignaturetest;

import android.content.Context;
import android.hardware.fingerprint.FingerprintManager;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;

import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Enumeration;

/*
 * Sample code to test generation of RSA signature authenticated by fingerprint.
 */
public final class MainActivity extends AppCompatActivity {
    private final String tag;

    /*
     * Creates a new main activity.
     */
    public MainActivity() {
        this.tag = "MinimalSignatureTest";
    }

    /*
     * Generate a 4096-bit key pair for use with the RSA-PSS signature scheme and store it in Android key store.
     *
     * (This is normally done asynchronously, in its own Thread (AsyncTask), with proper parametrization and error handling.)
     */
    public void generate(final View view) {

        /*
         * Generate RSA key pair.
         */
        try {
            KeyPairGenerator generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder("authKey", KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY);
            builder.setKeySize(4096);
            builder.setDigests(KeyProperties.DIGEST_SHA512);
            builder.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS);
            builder.setUserAuthenticationRequired(true);
            KeyGenParameterSpec spec = builder.build();
            generator.initialize(spec);
            KeyPair pair = generator.generateKeyPair();
            PublicKey publicKey = pair.getPublic();
            byte[] publicKeyBytes = publicKey.getEncoded();
            String publicKeyString = Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP);
            Log.d(this.tag, "Public key: " + publicKeyString);
        } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
            Log.d(this.tag, "Key generation failed!", e);
        }

    }

    /*
     * Returns the private key stored in the Android key store.
     */
    private PrivateKey getPrivateKey() {

        /*
         * Fetch private key from key store.
         */
        try {
            KeyStore store = KeyStore.getInstance("AndroidKeyStore");
            store.load(null);
            Enumeration<String> enumeration = store.aliases();
            String alias = null;

            /*
             * Find the last key in the key store.
             */
            while (enumeration.hasMoreElements())
                alias = enumeration.nextElement();

            /*
             * Check if we got a key.
             */
            if (alias == null)
                return null;
            else {
                Key key = store.getKey(alias, null);

                /*
                 * Check if it has a private part associated.
                 */
                if (key instanceof PrivateKey)
                    return (PrivateKey) key;
                else
                    return null;

            }

        } catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException | UnrecoverableKeyException e) {
            Log.d(this.tag, "Obtaining private key failed!", e);
            return null;
        }

    }

    /*
     * Create an RSA-PSS signature using a key from the Android key store.
     */
    public void sign(final View view) {
        final byte[] message = new byte[0];
        final PrivateKey privateKey = this.getPrivateKey();
        Context context = this.getApplicationContext();
        FingerprintManager manager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE);

        /*
         * Create RSA signature.
         */
        try {
            Signature rsa = Signature.getInstance("SHA512withRSA/PSS");
            rsa.initSign(privateKey);

            /*
             * Check if we have a fingerprint manager.
             */
            if (manager == null)
                Log.d(this.tag, "The fingerprint service is unavailable.");
            else {
                FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(rsa);
                CancellationSignal signal = new CancellationSignal();

                /*
                 * Create callback for fingerprint authentication.
                 */
                FingerprintManager.AuthenticationCallback callback = new FingerprintManager.AuthenticationCallback() {

                    /*
                     * This is called when access to the private key is granted.
                     */
                    @Override public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                        FingerprintManager.CryptoObject cryptoObject = result.getCryptoObject();
                        Signature rsa = cryptoObject.getSignature();

                        /*
                         * Sign the message.
                         */
                        try {
                            rsa.update(message);
                            byte[] signature = rsa.sign();
                            String signatureString = Base64.encodeToString(signature, Base64.NO_WRAP);
                            Log.d(tag, "Signature: " + signatureString);
                        } catch (SignatureException e) {
                            Log.d(tag, "Signature creation failed!", e);
                        }

                    }

                };

                /*
                 * Check if we have a fingerprint reader.
                 */
                if (!manager.isHardwareDetected())
                    Log.d(this.tag, "Your device does not have a fingerprint reader.");
                else {

                    /*
                     * Check if fingerprints are enrolled.
                     */
                    if (!manager.hasEnrolledFingerprints())
                        Log.d(this.tag, "Your device does not have fingerprints enrolled.");
                    else
                        manager.authenticate(cryptoObject, signal, 0, callback, null);

                }

            }

        } catch (NoSuchAlgorithmException | InvalidKeyException | SecurityException e) {
            Log.d(this.tag, "Signature creation failed!", e);
        }

    }

    /*
     * This is called when the user interface initializes.
     */
    @Override protected void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(R.layout.activity_main);
    }

}

(It's still ~200 LOC long, but fingerprint-authenticated cryptography needs a bit of code to make it work, so I can't seem to get this any smaller/simpler.)

To test it, just create a project with a single activity in Android Studio . Insert two buttons into this activity, one for generating a key (ie labelled Generate ) and one for creating a signature (ie labelled Sign ).

Then insert the sample code into your main activity and link the onclick events from the Generate button to the public void generate(final View view) method and from the Sign button to the public void sign(final View view) method.

Finally, insert the following into your AndroidManifest.xml , inside the top-level <manifest ...> ... </manifest> tag.

<uses-permission android:name="android.permission.USE_FINGERPRINT" />

Run the project and let adb logcat run alongside it.

After you hit the Generate button, you should see an output like this in the logs.

07-04 14:46:18.475 6759 6759 D MinimalSignatureTest: Public key: MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICC...

This is the public key of the key pair that has been generated.

(You will also see some complaints about key generation taking place in the main thread, however, this is just to keep the sample code simple. The actual application performs key generation in its own thread.)

Then, first hit Sign , then touch the sensor. The following error will occur.

keymaster1_device: Update send cmd failed
keymaster1_device: ret: 0
keymaster1_device: resp->status: -30
SoftKeymaster: system/keymaster/openssl_err.cpp, Line 47: error:00000000:invalid library (0):OPENSSL_internal:invalid library (0)
SoftKeymaster: system/keymaster/openssl_err.cpp, Line 88: Openssl error 0, 0
MinimalSignatureTest: java.security.SignatureException: android.security.KeyStoreException: Signature/MAC verification failed
MinimalSignatureTest:   at android.security.keystore.AndroidKeyStoreSignatureSpiBase.engineSign(AndroidKeyStoreSignatureSpiBase.java:333)
MinimalSignatureTest:   at java.security.Signature$Delegate.engineSign(Signature.java:1263)
MinimalSignatureTest:   at java.security.Signature.sign(Signature.java:649)
MinimalSignatureTest:   at com.example.andre.minimalsignaturetest.MainActivity$1.onAuthenticationSucceeded(MainActivity.java:148)
MinimalSignatureTest:   at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:855)
MinimalSignatureTest:   at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:803)
MinimalSignatureTest:   at android.os.Handler.dispatchMessage(Handler.java:102)
MinimalSignatureTest:   at android.os.Looper.loop(Looper.java:154)
MinimalSignatureTest:   at android.app.ActivityThread.main(ActivityThread.java:6186)
MinimalSignatureTest:   at java.lang.reflect.Method.invoke(Native Method)
MinimalSignatureTest:   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
MinimalSignatureTest:   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
MinimalSignatureTest: Caused by: android.security.KeyStoreException: Signature/MAC verification failed
MinimalSignatureTest:   at android.security.KeyStore.getKeyStoreException(KeyStore.java:676)
MinimalSignatureTest:   at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:224)
MinimalSignatureTest:   at android.security.keystore.AndroidKeyStoreSignatureSpiBase.engineSign(AndroidKeyStoreSignatureSpiBase.java:328)
System.err:     ... 11 more

This is where I'm stuck.

The weird thing is that I get Signature/MAC verification failed as the message of the SignatureException . Note that it says verification failed , while I'm actually signing ( not verifying) and the entire stack trace shows, that only signSomething(...) functions are called.

I've tried this on the LG Nexus 5X with both the official firmware (Android 7.1.1, N2G47W ) and different (up-to-date) LineageOS nightlies and they all fail at this point. However, when I consider the API documentation, it seems as if I'm doing the right stuff, and - to be honest - there's not a lot of stuff you could actually do differently. It actually seems to be pretty obvious how it works.

Note that, as long as I do not require user authentication - and therefore don't create the signature in the callback method, but outside, right after the initSign(...) - it works fine - even with hardware-backed key storage by Keymaster/Gatekeeper in TrustZone . But as soon as I require authentication, - and therefore do the update(...) and sign() calls on the Signature object inside the callback - it all breaks apart.

I tried to trace down the error in the OpenSSL library or to find out, what that -30 response code means, but both to no avail.

Any suggestions? I've gone a long way and implemented a ton of stuff, both server-side and on Android , to get this project going forward, but now I'm stuck and seem unable to perform user authentication that's cryptographically sound.

I tried replacing KeyProperties.SIGNATURE_PADDING_RSA_PSS with KeyProperties.SIGNATURE_PADDING_RSA_PKCS1 and SHA512withRSA/PSS with SHA512withRSA , then KeyProperties.DIGEST_SHA512 with KeyProperties.DIGEST_SHA256 and SHA512withRSA with SHA256withRSA . I also tried a smaller key size - 2048 bit instead of 4096 bit - all to no avail.

I also tried to shift commands from the initSign(...) , update(...) , sign() procedure from the outside of the callback to the inside or the other way round, however, this is the only combination, that's supposed to work. When I move initSign(...) inside the callback as well, the call to authenticate(...) fails with java.lang.IllegalStateException: Crypto primitive not initialized . When I move update(...) and sign() outside the callback, the call to sign() fails with java.security.SignatureException: Key user not authenticated . So initSign(...) has to be outside and sign() has to be inside. Where update(...) happens, appears to be uncritical, however, from a semantic point of view, it makes sense to keep it together with the call to sign() .

Any help is really appreciated.

Change your getPrivateKey method to:

 private PrivateKey getPrivateKey() {

     KeyStore store = KeyStore.getInstance("AndroidKeyStore");
     store.load(null);

     return (PrivateKey) keyStore.getKey("authKey", null));
 }

In your code, you iterate through all keys and grep the last one, which isn't necessarily the one that you want - or even worse: return null if that key doesn't have a private key...

If you want to check whether a key exist:

 if (store.containsAlias(keyName)) {
     ...
 }

I finally found a solution.

There were actually two problems at hand here.

  1. I tried to use SHA-512 as a mask-generation function for RSA/PSS, which is "probably" unsupported by the cryptographic library that Android uses.

  2. I tried to sign an empty (0-byte) message, which somehow appears to be "problematic".

When I both changed the MGF to SHA-256 and made the message 64 bytes long, signature generation succeeded.

Now, both "requirements" appear to be a bit "weird".

First, you can indeed use SHA-512 as an MGF for RSA/PSS, as long as you setUserAuthenticationRequired(false) , so it has to be supported by the cryptographic library. It's only when you enable authentication that it suddenly fails and you have to fall back to SHA-256 . I did not perform extensive testing which hash functions work as MGFs for RSA/PSS with authentication and which do not. I just found that SHA-512 does not work but SHA-256 does, so the choice of MGF is somehow "restricted" when authentication is enabled.

Second, your message needs to have a certain minimal size in order for it to be signed with authentication enabled. For example, you cannot sign an empty buffer. This makes no sense to me at all since the first step in RSA/PSS is to apply a cryptographic hash function to the message, the output of which is fixed length, so the signature scheme really shouldn't care how long or short the message is, but apparently it does. Like before, I didn't perform extensive testing to find the exact cutoff point where the message becomes "long enough" for signing. However, I found that a 64 byte message can be signed, while an empty (0 byte) message cannot, so the minimal length is somewhere within [1; 64] bytes, both limits inclusive.

Note that, as of now, this seems to be documented nowhere and also the exception thrown is of no use. It just says "signature verification failed" (yes, it says " verification " even though we're actually generating a signature), so you have no idea that you have to change the MGF and the length of the message to be signed.

Due to this, there might be more to it that I haven't found. I just found this parametrization by "trial and error" and thus have no idea what the actual constraints of the cryptographic library look like.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM