简体   繁体   English

Android-需要指纹认证才能访问(RSA / PSS)签名密钥

[英]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. 为此,我需要使用通过指纹进行身份验证的私钥创建RSA-PSS签名,以便仅当设备所有者实际在场时才可以使用它来创建签名。

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 ). 为此,我使用Android KeyStore (由ARM TrustZone中的Keymaster / Gatekeeper支持)生成RSA密钥对( KEY_ALGORITHM_RSA ),以与RSA-PSS签名算法( SIGNATURE_PADDING_RSA_PSS )一起使用来创建和验证签名( PURPOSE_SIGN | PURPOSE_VERIFY ) 。 I also require user authentication by setting the corresponding property to true . 我还需要通过将相应的属性设置为true来进行用户认证。

Later, to create the signature over a buffer final byte[] message , I ... 稍后,要在缓冲区的final byte[] message上创建签名,我...

  1. obtain an instance of the FingerprintManager service 获取FingerprintManager服务的实例
  2. create an instance of the SHA512withRSA/PSS signature algorithm ( Signature object) 创建SHA512withRSA/PSS签名算法的实例( Signature对象)
  3. initialize the Signature algorithm for signing with the private key ( initSign(...) ) 初始化用于使用私钥进行签名的Signature算法( initSign(...)
  4. wrap the Signature object into a CryptoObject Signature对象包装到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) 使用FingerprintManager的实例对CryptoObject进行authenticate(...) ,并(其中包括)传递一个FingerprintManager.AuthenticationCallback以在用户对密钥进行身份验证之后调用(通过触摸其设备上的指纹传感器)

Inside the callback, use of the key is authenticated, so I ... 在回调内部,密钥的使用已通过身份验证,因此我...

  1. extract the Signature object from the CryptoObject wrapper again 再次从CryptoObject包装器中提取Signature对象
  2. use the update(...) method on the Signature object to stream the data to be signed ( message ) into the signature algorithm 使用Signature对象上的update(...)方法将要Signature的数据( message )流式传输到签名算法
  3. use the sign() method on the Signature object to obtain the signature Signature对象上使用sign()方法获取签名
  4. encode that signature as Base64 and println(...) it out to StdErr so it appears in adb logcat 将该签名编码为Base64并将println(...)签出到StdErr,以便它出现在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.) (它仍然有大约200个LOC,但是经过指纹验证的加密需要一些代码才能使其起作用,所以我似乎无法使其变得更小/更简单。)

To test it, just create a project with a single activity in Android Studio . 要对其进行测试,只需在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 ). 在此活动中插入两个按钮,一个按钮用于生成密钥(即标记为Generate ),另一个按钮用于创建签名(即标记为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. 然后将示例代码插入到您的主要活动中,并将onclick事件从“ 生成”按钮链接到public void generate(final View view)方法,从“ 签名”按钮链接到public void sign(final View view)方法。

Finally, insert the following into your AndroidManifest.xml , inside the top-level <manifest ...> ... </manifest> tag. 最后,将以下内容插入顶级<manifest ...> ... </manifest>标记内的AndroidManifest.xml

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

Run the project and let adb logcat run alongside it. 运行项目,并让adb logcat与它一起运行。

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. 然后,先点击Sign ,然后触摸传感器。 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 . 奇怪的是,由于SignatureException的消息,我无法进行Signature/MAC verification failed 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. 请注意,它说verification failed ,虽然我实际上在签名验证),并且整个堆栈跟踪都显示,但仅调用signSomething(...)函数。

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. 我已经在LG Nexus 5X上使用官方固件(Android 7.1.1, N2G47W )和其他(最新的) LineageOS夜间软件进行了尝试,但它们都在此时失败。 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. 但是,当我考虑API文档时,似乎我在做正确的事情,而且-老实说-实际上,您可以做的事情并不多。 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 . 需要注意的是,只要我要求用户身份验证-因此不会产生在回调方法的签名,但外面,右后initSign(...) -它工作得很好-即使硬件支持的密钥存储由TrustZone中的Keymaster / Gatekeeper提供。 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. 但是,一旦我需要身份验证,并因此对回调内的Signature对象进行了update(...)sign()调用,它们就会全部中断。

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. 我试图在OpenSSL库中查找错误,或找出-30响应代码的含义,但两者均无济于事。

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. 我已经走了很长一段路,并在服务器端和Android上实现了大量东西,以使该项目得以继续进行,但是现在我陷入了困境,似乎无法执行加密方式的用户身份验证。

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 . 我尝试将KeyProperties.SIGNATURE_PADDING_RSA_PSS替换为KeyProperties.SIGNATURE_PADDING_RSA_PKCS1SHA512withRSA/PSS替换为SHA512withRSA ,然后将KeyProperties.DIGEST_SHA512KeyProperties.DIGEST_SHA256SHA512withRSASHA256withRSA I also tried a smaller key size - 2048 bit instead of 4096 bit - all to no avail. 我还尝试了较小的密钥大小-2048位而不是4096位-都无济于事。

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. 我还尝试将命令从initSign(...)update(...)sign()过程从回调的外部转移到内部或相反,但是,这是唯一的组合,那就是应该工作。 When I move initSign(...) inside the callback as well, the call to authenticate(...) fails with java.lang.IllegalStateException: Crypto primitive not initialized . 当我也在回调内移动initSign(...) ,对authenticate(...)的调用也会失败,并出现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 . 当我将update(...)sign()移到回调之外时,对sign()的调用失败,并出现java.security.SignatureException: Key user not authenticated So initSign(...) has to be outside and sign() has to be inside. 因此, initSign(...) 必须在外部,而sign() 必须在内部。 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() . 在发生update(...) 似乎并不重要,但是,从语义的角度来看,将其与对sign()的调用保持在一起是有意义的。

Any help is really appreciated. 任何帮助都非常感谢。

Change your getPrivateKey method to: 将您的getPrivateKey方法更改为:

 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... 在您的代码中,您遍历所有密钥,并grep最后一个密钥,这不一定是您想要的那个-甚至更糟:如果该密钥没有私钥,则返回null

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. 我试图将SHA-512用作RSA / PSS的掩码生成功能, Android使用的加密库“可能”不支持SHA / 512

  2. I tried to sign an empty (0-byte) message, which somehow appears to be "problematic". 我试图签署一个空的(0字节)消息,该消息似乎有点“有问题”。

When I both changed the MGF to SHA-256 and made the message 64 bytes long, signature generation succeeded. 当我将MGF 更改为SHA-256 并将消息的长度设置为64字节时,签名生成就成功了。

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. 首先,你的确可以使用SHA-512作为MGF的RSA / PSS,只要你setUserAuthenticationRequired(假的),所以它必须通过密码库的支持。 It's only when you enable authentication that it suddenly fails and you have to fall back to SHA-256 . 仅当启用身份验证时,它突然失败,并且您必须退回到SHA-256 I did not perform extensive testing which hash functions work as MGFs for RSA/PSS with authentication and which do not. 我没有进行广泛的测试,哪些哈希函数可以用作带有身份验证的RSA / PSS的MGF,而哪些不能。 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. 我只是发现SHA-512不起作用,而SHA-256起作用,因此启用身份验证后,MGF的选择受到某种程度的“限制”。

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. 这对我完全没有意义,因为RSA / PSS的第一步是对消息应用加密哈希函数,该函数的输出是固定长度的,因此签名方案实际上不必关心消息的长度是多少。是,但显然确实如此。 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字节的消息进行签名,而对空白(0字节)的消息不能进行签名,因此最小长度在[1; 64] bytes, both limits inclusive. 64]个字节(包括两个上限)。

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. 它只是说“签名验证失败”(是的,即使我们实际上正在生成签名,它也说“ 验证 ”),因此您不知道必须更改MGF和要签名的消息的长度。

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. 我只是通过“尝试和错误”发现了这种参数化,因此不知道密码库的实际约束是什么样的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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