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 ...
- obtain an instance of the
FingerprintManager
service - create an instance of the
SHA512withRSA/PSS
signature algorithm (Signature
object) - initialize the
Signature
algorithm for signing with the private key (initSign(...)
) - wrap the
Signature
object into aCryptoObject
- (perform some additional checks)
authenticate(...)
theCryptoObject
using the instance ofFingerprintManager
, passing (among others) aFingerprintManager.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 ...
- extract the
Signature
object from theCryptoObject
wrapper again - use the
update(...)
method on theSignature
object to stream the data to be signed (message
) into the signature algorithm - use the
sign()
method on theSignature
object to obtain the signature - encode that signature as
Base64
andprintln(...)
it out to StdErr so it appears inadb 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 (i. e. labelled Generate) and one for creating a signature (i. e. 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.