Android-需要指纹身份验证才能访问(RSA/PSS)签名密钥 [英] Android - require fingerprint authentication to access (RSA/PSS) signing key

查看:221
本文介绍了Android-需要指纹身份验证才能访问(RSA/PSS)签名密钥的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我目前正在为我的计算机科学硕士学位论文所需的项目创建一种挑战响应身份验证形式.

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

为此,我需要创建一个具有通过指纹进行身份验证的私钥的RSA-PSS签名,以便仅当该设备的所有者在场时才可以使用该签名来创建签名.

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.

要实现此目的,我使用 Android KeyStore (由 ARM TrustZone 中的Keymaster/Gatekeeper支持)来生成RSA密钥对(KEY_ALGORITHM_RSA),用于用于创建和验证签名(PURPOSE_SIGN | PURPOSE_VERIFY)的RSA-PSS签名算法(SIGNATURE_PADDING_RSA_PSS).我还需要通过将相应的属性设置为true来进行用户身份验证.

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.

稍后,要在缓冲区final byte[] message上创建签名,我...

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

  1. 获取FingerprintManager服务的实例
  2. 创建SHA512withRSA/PSS签名算法(Signature对象)的实例
  3. 初始化用于使用私钥(initSign(...))签名的Signature算法
  4. Signature对象包装为CryptoObject
  5. (执行一些其他检查)
  6. authenticate(...) CryptoObject使用FingerprintManager的实例,在用户验证了密钥之后(通过触摸其设备上的指纹传感器)传递了(其中包括)要调用的FingerprintManager.AuthenticationCallback )
  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. 再次从CryptoObject包装器中提取Signature对象
  2. 使用Signature对象上的update(...)方法将要签名的数据(message)流式传输到签名算法
  3. Signature对象上使用sign()方法获取签名
  4. 将该签名编码为Base64,然后将其println(...)编码为 StdErr ,以便它出现在adb logcat
  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);
    }

}

(它仍有〜200 LOC长,但是经过指纹验证的加密需要一些代码才能使其起作用,所以我似乎无法使它变得更小/更简单.)

(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.)

要对其进行测试,只需在 Android Studio 中创建一个具有单个活动的项目.在此活动中插入两个按钮,一个按钮用于生成密钥(即标记为 Generate ),另一个按钮用于创建签名(即标记为 Sign ).

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 (i. e. labelled Generate) and one for creating a signature (i. e. labelled Sign).

然后将示例代码插入到您的主要活动中,并将onclick事件从 Generate 按钮链接到public void generate(final View view)方法,从 Sign 按钮链接到public void sign(final View view)方法.

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.

最后,将以下内容插入顶级<manifest ...> ... </manifest>标记内的AndroidManifest.xml中.

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

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

运行项目,并让adb logcat与其一起运行.

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.)

然后,先按 Sign ,然后触摸传感器.将出现以下错误.

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.

奇怪的是,我得到Signature/MAC verification failed作为SignatureException的消息.请注意,它说的是verification failed,而实际上我正在签名(验证)并且整个堆栈跟踪都显示了,只有 signSomething(...) 函数被调用.

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.

我已经在 LG Nexus 5X 上使用官方固件(Android 7.1.1,N2G47W)和其他(最新) LineageOS 进行了尝试夜间,它们都在这一点上失败了.但是,当我考虑API文档时,似乎我在做正确的事情,而且-老实说-您实际上没有很多事情可以做不同的事情.实际上,它的工作原理似乎非常明显.

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.

请注意,只要我需要用户身份验证-因此不要在回调方法中创建签名,而是在initSign(...)之后立即在外部创建-可以正常工作-即使使用Keymaster/Gatekeeper在 TrustZone 中由硬件支持的密钥存储.但是,一旦我需要身份验证,就会对回调内的Signature对象进行update(...)sign()调用-一切都会崩溃.

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.

我试图在 OpenSSL 库中查找错误,或者找出-30响应代码的含义,但均无济于事.

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.

有什么建议吗?我已经走了很长一段路,并在服务器端和 Android 上实现了很多东西,以使该项目得以进行,但是现在我陷入了困境,似乎无法执行用户身份验证加密的声音.

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.

我尝试将KeyProperties.SIGNATURE_PADDING_RSA_PSS替换为KeyProperties.SIGNATURE_PADDING_RSA_PKCS1,将SHA512withRSA/PSS替换为SHA512withRSA,然后将KeyProperties.DIGEST_SHA512替换为KeyProperties.DIGEST_SHA256,将SHA512withRSA替换为SHA256withRSA.我还尝试了较小的密钥大小-2048位而不是4096位-都无济于事.

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.

我还尝试将initSign(...)update(...)sign()过程中的命令从回调的外部转移到内部或相反,但这是唯一的组合,应该可以使用.当我也在回调内移动initSign(...)时,对authenticate(...)的调用因java.lang.IllegalStateException: Crypto primitive not initialized而失败.当我将update(...)sign()移到回调之外时,对sign()的调用因java.security.SignatureException: Key user not authenticated而失败.因此,initSign(...) 必须在外面,sign() 必须在里面.在出现update(...)的地方,似乎是并不重要,但是,从语义的角度来看,将其与对sign()的调用保持在一起是有意义的.

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.

推荐答案

最终找到了解决方案.

这里实际上有两个问题.

  1. 我尝试使用 SHA-512 作为RSA/PSS的掩码生成功能, Android 的加密库可能"不支持该功能使用.

  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.

我试图签署一个空的(0字节)消息,该消息似乎有点有问题".

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

当我两者都将MGF更改为 SHA-256 并且使消息长64个字节时,签名生成就成功了.

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".

首先,您可以确实使用 SHA-512 作为RSA/PSS的MGF,只要您 setUserAuthenticationRequired(false),因此它受到密码库的支持.仅当启用身份验证时,它突然失败,并且您必须退回到 SHA-256 .我没有进行广泛的测试,哪些哈希函数可以用作带有身份验证的RSA/PSS的MGF,而哪些不能.我只是发现 SHA-512 不起作用,但 SHA-256 起作用,因此启用身份验证后,MGF的选择受到了某种限制.

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.

第二,您的消息需要具有一定的最小大小,以便在启用身份验证的情况下对其进行签名.例如,您不能签署一个空的缓冲区.这对我完全没有意义,因为RSA/PSS的第一步是对消息应用加密哈希函数,该函数的输出是固定长度的,因此签名方案实际上不必关心消息的长度是多少.是,但显然确实如此.像以前一样,我没有进行广泛的测试来找到消息变得足够长"以进行签名的确切截止点.但是,我发现可以对64字节的消息进行签名,而对空白(0字节)的消息不能进行签名,因此最小长度在[1; 64]个字节(包括两个字节在内).

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.

请注意,到目前为止,似乎无处可查,而且抛出异常也没有用.它只是说签名验证失败"(是的,即使我们实际上正在生成签名,它也说"验证"),所以您不知道自己必须更改MGF和要签名的邮件的长度.

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.

这篇关于Android-需要指纹身份验证才能访问(RSA/PSS)签名密钥的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆