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

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

我目前正在为我的计算机科学硕士论文所需的项目创建一种质询-响应身份验证形式。

为此,我需要使用通过指纹验证的私钥创建 RSA-PSS 签名,以便它只能在设备所有者实际存在时用于创建签名。

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

来要求用户身份验证

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

  1. 获取 FingerprintManager 服务的一个实例
  2. 创建 SHA512withRSA/PSS 签名算法的实例(Signature 对象)
  3. 初始化 Signature 算法以使用私钥签名 (initSign(...))
  4. Signature 对象包装成 CryptoObject
  5. (执行一些额外的检查)
  6. authenticate(...) CryptoObject 使用 FingerprintManager 的实例,传递(除其他外)一个 FingerprintManager.AuthenticationCallback 在用户验证密钥后调用(通过触摸 his/her 设备上的指纹传感器)

在回调中,密钥的使用是经过验证的,所以我...

  1. 再次从 CryptoObject 包装器中提取 Signature 对象
  2. Signature 对象上使用 update(...) 方法将要签名的数据 (message) 流式传输到签名算法中
  3. Signature对象上使用sign()方法获取签名
  4. 将该签名编码为 Base64 并将其 println(...) 编码为 StdErr 因此它出现在 adb logcat

我创建了一个尽可能精简的示例代码。

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 长,但是指纹验证加密需要一些代码才能使其工作,所以我似乎无法得到它 smaller/simpler。)

要测试它,只需在 Android Studio 中创建一个包含单个 activity 的项目。在此 activity 中插入两个按钮,一个用于生成密钥(即标记为 Generate),另一个用于创建签名(即标记为 Sign).

然后将示例代码插入到 activity 和 link onclick 事件中,从 Generate 按钮到 public void generate(final View view) 方法和从 Sign 按钮到 public void sign(final View view) 方法。

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

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

运行 项目并让 adb logcat 运行 与它并排。

点击 Generate 按钮后,您应该会在日志中看到类似这样的输出。

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

这是生成的密钥对的public密钥。

(您还会看到一些关于密钥生成发生在主线程中的抱怨,然而,这只是为了使示例代码简单。实际应用程序在其自己的线程中执行密钥生成。)

然后,先点击签名,然后触摸传感器。会出现以下错误。

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

这就是我卡住的地方。

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

我已经在 LG Nexus 5X 上用官方固件(Android 7.1.1,N2G47W)和不同的(up -to-date) LineageOS nightlies 并且它们在这一点上都失败了。但是,当我考虑 API 文档时,似乎我在做正确的事情,而且 - 老实说 - 实际上你可以做很多不同的事情。它实际上似乎很明显它是如何工作的。

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

我试图追查 OpenSSL 库中的错误或找出 -30 响应代码的含义,但都无济于事。

有什么建议吗?我已经走了很长一段路,在服务器端和 Android 上实现了大量的东西来推进这个项目,但现在我被困住了似乎无法执行加密可靠的用户身份验证。

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

我还尝试将来自 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() 的调用放在一起是有意义的。 =79=]

非常感谢任何帮助。

将您的 getPrivateKey 方法更改为:

 private PrivateKey getPrivateKey() {

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

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

在您的代码中,您遍历所有键并 grep 最后一个,这不一定是您想要的 - 或者更糟:return null 如果该键不是没有私钥...

如果要检查key是否存在:

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

终于找到了解决办法。

这里实际上有 两个 个问题。

  1. 我尝试使用 SHA-512 作为 RSA/PSS 的掩码生成函数,"probably" 不受Android 使用的密码库。

  2. 我试图签署一个空的(0 字节)消息,不知何故似乎是 "problematic"。

当我both 将 MGF 更改为 SHA-256and 生成了消息 64字节长,签名生成成功。

现在,"requirements" 看起来有点 "weird"。

首先,您可以确实使用SHA-512作为RSA/PSS的MGF,只要您setUserAuthenticationRequired(false),因此它必须 得到密码库的支持。只有当您启用身份验证时,它突然失败并且您 必须 退回到 SHA-256。我没有执行广泛的测试,哪些散列函数可以作为 RSA/PSS 的 MGF 使用身份验证,哪些不能。我刚刚发现 SHA-512 不起作用,但 SHA-256 起作用,所以 MGF 的选择在某种程度上是 "restricted" 身份验证时已启用。

其次,您的邮件需要有一定的最小大小,以便在启用身份验证的情况下对其进行签名。例如,您不能签署空缓冲区。这对我来说毫无意义,因为 RSA/PSS 的第一步是对消息应用加密哈希函数,其输出是固定长度的,所以签名方案真的不应该关心多长或多短消息是,但显然是这样。和以前一样,我没有执行广泛的测试来找到消息变为 "long enough" 以进行签名的确切截止点。但是,我发现可以对 64 字节的消息进行签名,而不能对空(0 字节)消息进行签名,因此最小长度在 [1; 64] 字节,包括两个限制。

请注意,截至目前,这似乎无处记录,而且抛出的异常也没有用。它只是说 "signature verification failed"(是的,它说“验证”,即使我们实际上是 生成 签名),所以你有不知道您必须更改 MGF 和要签名的消息的长度。

因此,可能还有更多我没有找到的内容。我刚刚通过 "trial and error" 发现了这个参数化,因此不知道密码库的实际约束是什么样的。