Skip to content

Commit

Permalink
Allow instantiating SecureArea.CreateKeySettings object. (#409)
Browse files Browse the repository at this point in the history
In order to support new kinds of SecureArea without app changes,
change the SecureArea.CreateKeySettings class so it can be
instantiated by apps.

Also stop carrying the SecureArea class name in CreateKeySettings
subclasses, instead have users of CreateKeySettings take a separate
SecureArea class. Make it explicit that different SecureArea instances
can be used by CredentialKey and even for different Authentication
Keys.

Introduce the concept of an "identifier" for a SecureArea instead
of Credential, Credential.Authentiationkey, and
Credential.PendingAuthenticationKey classes all using the Java class
name when persisting on-disk. This paves the way to have multiple
SecureArea instances of the same type but with different configuration.
We'll need this for CloudSecureArea so an app can have two instances
pointing to different providers, for different credentials. Also
add "display name" since that is needed for Issue #380.

Unfortunately this breaks the on-disk format but this is going
to happen _anyway_ as we're renaming the project and moving all
the code to new packages as part of the contribution of the
code to the OpenWallet Foundation. In the near future we'll start
committing to stable on-disk formats. For now, just update
the directory used for storing data in appholder to a new
location to avoid breaking existing installations.

Test: All unit tests pass.
Test: Manually tested
  • Loading branch information
davidz25 authored Nov 7, 2023
1 parent e34a3ee commit 60fcadc
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 219 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ object PreferencesHelper {
// As per the docs, the credential data contains reference to Keystore aliases so ensure
// this is stored in a location where it's not automatically backed up and restored by
// Android Backup as per https://developer.android.com/guide/topics/data/autobackup
return context.noBackupFilesDir

val storageDir = File(context.noBackupFilesDir, "appholder")
if (!storageDir.exists()) {
storageDir.mkdir()
}
return storageDir;
}

fun isBleDataRetrievalEnabled(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,29 +92,33 @@ class ProvisioningUtil private constructor(
nameSpacedData: NameSpacedData,
provisionInfo: ProvisionInfo,
) {
val settings = when (provisionInfo.secureAreaImplementationStateType) {
SecureAreaImplementationState.Android -> createAndroidKeystoreSettings(
userAuthenticationRequired = provisionInfo.userAuthentication,
mDocAuthOption = provisionInfo.mDocAuthenticationOption,
authTimeoutMillis = provisionInfo.userAuthenticationTimeoutSeconds * 1000L,
userAuthenticationType = provisionInfo.userAuthType(),
useStrongBox = provisionInfo.useStrongBox,
ecCurve = provisionInfo.authKeyCurve,
validUntil = provisionInfo.validityInDays.toTimestampFromNow()
)

SecureAreaImplementationState.BouncyCastle -> createBouncyCastleKeystoreSettings(
passphrase = provisionInfo.passphrase,
mDocAuthOption = provisionInfo.mDocAuthenticationOption,
ecCurve = provisionInfo.authKeyCurve
)
val (secureArea, settings) = when (provisionInfo.secureAreaImplementationStateType) {
SecureAreaImplementationState.Android -> Pair(
androidKeystoreSecureArea,
createAndroidKeystoreSettings(
userAuthenticationRequired = provisionInfo.userAuthentication,
mDocAuthOption = provisionInfo.mDocAuthenticationOption,
authTimeoutMillis = provisionInfo.userAuthenticationTimeoutSeconds * 1000L,
userAuthenticationType = provisionInfo.userAuthType(),
useStrongBox = provisionInfo.useStrongBox,
ecCurve = provisionInfo.authKeyCurve,
validUntil = provisionInfo.validityInDays.toTimestampFromNow()
))

SecureAreaImplementationState.BouncyCastle -> Pair(
softwareSecureArea,
createBouncyCastleKeystoreSettings(
passphrase = provisionInfo.passphrase,
mDocAuthOption = provisionInfo.mDocAuthenticationOption,
ecCurve = provisionInfo.authKeyCurve
))
}

val credential = credentialStore.createCredential(provisionInfo.credentialName(), settings)
val credential = credentialStore.createCredential(provisionInfo.credentialName(), secureArea, settings)
credential.nameSpacedData = nameSpacedData

repeat(provisionInfo.numberMso) {
val pendingKey = credential.createPendingAuthenticationKey(settings, null)
val pendingKey = credential.createPendingAuthenticationKey(secureArea, settings, null)
pendingKey.applicationData.setBoolean(AUTH_KEY_DOMAIN, true)
}
provisionAuthKeys(credential, provisionInfo.docType, provisionInfo.validityInDays)
Expand Down Expand Up @@ -284,26 +288,30 @@ class ProvisioningUtil private constructor(

private fun manageKeysFor(credential: Credential): Int {
val mDocAuthOption = credential.applicationData.getString(MDOC_AUTHENTICATION)
val settings = when (credential.credentialSecureArea) {
val (secureArea, settings) = when (credential.credentialSecureArea) {
is AndroidKeystoreSecureArea -> {
val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo
createAndroidKeystoreSettings(
keyInfo.isUserAuthenticationRequired,
AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption),
keyInfo.userAuthenticationTimeoutMillis,
keyInfo.userAuthenticationType,
keyInfo.isStrongBoxBacked,
keyInfo.ecCurve,
keyInfo.validUntil ?: Timestamp.now()
)
Pair(
androidKeystoreSecureArea,
createAndroidKeystoreSettings(
keyInfo.isUserAuthenticationRequired,
AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption),
keyInfo.userAuthenticationTimeoutMillis,
keyInfo.userAuthenticationType,
keyInfo.isStrongBoxBacked,
keyInfo.ecCurve,
keyInfo.validUntil ?: Timestamp.now()
))
}

is SoftwareSecureArea -> {
val keyInfo = credential.credentialSecureArea.getKeyInfo(credential.credentialKeyAlias) as SoftwareSecureArea.KeyInfo
createBouncyCastleKeystoreSettings(
mDocAuthOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption),
ecCurve = keyInfo.ecCurve
)
Pair(
softwareSecureArea,
createBouncyCastleKeystoreSettings(
mDocAuthOption = AddSelfSignedScreenState.MdocAuthStateOption.valueOf(mDocAuthOption),
ecCurve = keyInfo.ecCurve
))
}

else -> throw IllegalStateException("Unknown keystore secure area implementation")
Expand All @@ -312,6 +320,7 @@ class ProvisioningUtil private constructor(
val maxUsagesPerKey = credential.applicationData.getNumber(MAX_USAGES_PER_KEY)
return CredentialUtil.managedAuthenticationKeyHelper(
credential,
secureArea,
settings,
AUTH_KEY_DOMAIN,
Timestamp.now(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public void testBasic() throws IOException {

Credential credential = credentialStore.createCredential(
"testCredential",
mSecureArea,
new AndroidKeystoreSecureArea.CreateKeySettings.Builder(credentialKeyAttestationChallenge).build());
Assert.assertEquals("testCredential", credential.getName());
List<X509Certificate> certChain = credential.getAttestation();
Expand All @@ -82,11 +83,13 @@ public void testBasic() throws IOException {
// Create pending authentication key and check its attestation
byte[] authKeyChallenge = new byte[] {20, 21, 22};
Credential.PendingAuthenticationKey pendingAuthenticationKey =
credential.createPendingAuthenticationKey(new AndroidKeystoreSecureArea.CreateKeySettings.Builder(authKeyChallenge)
.setUserAuthenticationRequired(true, 30*1000,
AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF
| AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC)
.build(),
credential.createPendingAuthenticationKey(
mSecureArea,
new AndroidKeystoreSecureArea.CreateKeySettings.Builder(authKeyChallenge)
.setUserAuthenticationRequired(true, 30*1000,
AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF
| AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC)
.build(),
null);
parser = new AndroidAttestationExtensionParser(pendingAuthenticationKey.getAttestation().get(0));
Assert.assertArrayEquals(authKeyChallenge,
Expand All @@ -109,6 +112,7 @@ public void testBasic() throws IOException {
// Check creating a credential with an existing name overwrites the existing one
credential = credentialStore.createCredential(
"testCredential",
mSecureArea,
new AndroidKeystoreSecureArea.CreateKeySettings.Builder(credentialKeyAttestationChallenge).build());
Assert.assertEquals("testCredential", credential.getName());
// At least the leaf certificate should be different
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,19 @@ public class MigrateFromKeystoreICStoreTest {
IdentityCredentialStore mICStore;
StorageEngine mStorageEngine;

AndroidKeystoreSecureArea mKeystoreEngine;
AndroidKeystoreSecureArea mAndroidKeystoreSecureArea;

SecureAreaRepository mKeystoreEngineRepository;
SecureAreaRepository mSecureAreaRepository;

@Before
public void setup() {
Context context = androidx.test.InstrumentationRegistry.getTargetContext();
File storageDir = new File(context.getDataDir(), "ic-testing");
mStorageEngine = new AndroidStorageEngine.Builder(context, storageDir).build();

mKeystoreEngineRepository = new SecureAreaRepository();
mKeystoreEngine = new AndroidKeystoreSecureArea(context, mStorageEngine);
mKeystoreEngineRepository.addImplementation(mKeystoreEngine);
mSecureAreaRepository = new SecureAreaRepository();
mAndroidKeystoreSecureArea = new AndroidKeystoreSecureArea(context, mStorageEngine);
mSecureAreaRepository.addImplementation(mAndroidKeystoreSecureArea);

mICStore = Utility.getIdentityCredentialStore(context);
}
Expand Down Expand Up @@ -240,8 +240,8 @@ private void migrateAndCheckResults(String credName,
// migrate
CredentialStore credentialStore = new CredentialStore(
mStorageEngine,
mKeystoreEngineRepository);
Credential migratedCred = keystoreCred.migrateToCredentialStore(credentialStore);
mSecureAreaRepository);
Credential migratedCred = keystoreCred.migrateToCredentialStore(mAndroidKeystoreSecureArea, credentialStore);

// check deletion
assertNull(mICStore.getCredentialByName(
Expand Down Expand Up @@ -302,9 +302,12 @@ private void migrateAndCheckResults(String credName,
assertNull(migratedCred.findAuthenticationKey(Timestamp.ofEpochMilli(100)));
byte[] authKeyChallenge = new byte[] {20, 21, 22};
Credential.PendingAuthenticationKey pendingAuthenticationKey =
migratedCred.createPendingAuthenticationKey(new AndroidKeystoreSecureArea.CreateKeySettings.Builder(authKeyChallenge)
migratedCred.createPendingAuthenticationKey(
mAndroidKeystoreSecureArea,
new AndroidKeystoreSecureArea.CreateKeySettings.Builder(authKeyChallenge)
.setUserAuthenticationRequired(true, 30*1000,
AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF)
AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF |
AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC)
.build(),
null);
parser = new AndroidAttestationExtensionParser(pendingAuthenticationKey.getAttestation().get(0));
Expand Down Expand Up @@ -547,8 +550,8 @@ public void deleteBeforeMigrationTest() throws Exception {
mICStore.deleteCredentialByName(credName);
CredentialStore credentialStore = new CredentialStore(
mStorageEngine,
mKeystoreEngineRepository);
mSecureAreaRepository);
assertNull(mICStore.getCredentialByName(credName, IdentityCredentialStore.CIPHERSUITE_ECDHE_HKDF_ECDSA_WITH_AES_256_GCM_SHA256));
assertThrows(IllegalStateException.class, () -> keystoreCred.migrateToCredentialStore(credentialStore));
assertThrows(IllegalStateException.class, () -> keystoreCred.migrateToCredentialStore(mAndroidKeystoreSecureArea, credentialStore));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import com.android.identity.AndroidAttestationExtensionParser;
import com.android.identity.android.storage.AndroidStorageEngine;
import com.android.identity.securearea.SecureArea;
import com.android.identity.securearea.SoftwareSecureArea;
import com.android.identity.storage.EphemeralStorageEngine;
import com.android.identity.storage.StorageEngine;
import com.android.identity.util.Timestamp;

Expand Down Expand Up @@ -882,4 +884,27 @@ public void testAttestKeyHelper(Context context, boolean useStrongBox) throws IO
: AndroidAttestationExtensionParser.SecurityLevel.TRUSTED_ENVIRONMENT, securityLevel);
}

@Test
public void testUsingGenericCreateKeySettings() throws IOException {
Context context = androidx.test.InstrumentationRegistry.getTargetContext();
File storageDir = new File(context.getDataDir(), "ic-testing");
StorageEngine storageEngine = new AndroidStorageEngine.Builder(context, storageDir).build();
AndroidKeystoreSecureArea ks = new AndroidKeystoreSecureArea(context, storageEngine);

byte[] challenge = new byte[] {1, 2, 3, 4};
ks.createKey("testKey", new SecureArea.CreateKeySettings(challenge));

AndroidKeystoreSecureArea.KeyInfo keyInfo = ks.getKeyInfo("testKey");
Assert.assertNotNull(keyInfo);
Assert.assertEquals(SecureArea.KEY_PURPOSE_SIGN, keyInfo.getKeyPurposes());
Assert.assertEquals(SecureArea.EC_CURVE_P256, keyInfo.getEcCurve());
Assert.assertTrue(keyInfo.isHardwareBacked());

AndroidAttestationExtensionParser parser =
new AndroidAttestationExtensionParser(keyInfo.getAttestation().get(0));
Assert.assertArrayEquals(challenge, parser.getAttestationChallenge());

// Now delete it...
ks.deleteKey("testKey");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -840,21 +840,26 @@ List<Calendar> getAuthenticationDataExpirations() {
* new {@link Credential}, while the access control profile information, per reader session/acp
* timeout/auth keys will not be transferred.
*
* @param androidKeystoreSecureArea an {@link AndroidKeystoreSecureArea} instance.
* @param credentialStore the credential store where the new {@link Credential} should be stored.
* @return the new {@link Credential}.
*/
public @NonNull Credential migrateToCredentialStore(@NonNull CredentialStore credentialStore) {
public @NonNull Credential migrateToCredentialStore(
@NonNull AndroidKeystoreSecureArea androidKeystoreSecureArea,
@NonNull CredentialStore credentialStore) {
loadData();

if (mData == null) {
throw new IllegalStateException("The credential has been deleted prior to migration.");
}
String aliasForOldCredKey = mData.getCredentialKeyAlias();
AndroidKeystoreSecureArea.CreateKeySettings.Builder keySettingsBuilder = Utility.extractKeySettings(aliasForOldCredKey);
keySettingsBuilder.setEcCurve(SecureArea.EC_CURVE_P256);
AndroidKeystoreSecureArea.CreateKeySettings.Builder ksSettingsBuilder = Utility.extractKeySettings(aliasForOldCredKey);
ksSettingsBuilder.setEcCurve(SecureArea.EC_CURVE_P256);

Credential newCred = credentialStore.createCredentialWithExistingKey(mCredentialName,
keySettingsBuilder.build(), aliasForOldCredKey);
androidKeystoreSecureArea,
ksSettingsBuilder.build(),
aliasForOldCredKey);

NameSpacedData.Builder nsBuilder = new NameSpacedData.Builder();
for (PersonalizationData.NamespaceData namespaceData : mData.getNamespaceDatas()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

import com.android.identity.internal.Util;
import com.android.identity.securearea.SecureArea;
import com.android.identity.securearea.SoftwareSecureArea;
import com.android.identity.storage.StorageEngine;
import com.android.identity.util.Logger;
import com.android.identity.util.Timestamp;
Expand Down Expand Up @@ -159,10 +160,31 @@ public AndroidKeystoreSecureArea(@NonNull Context context,
mKeymintSbFeatureLevel = getFeatureVersionKeystore(context, true);
}

@NonNull
@Override
public String getIdentifier() {
return "AndroidKeystoreSecureArea";
}

@NonNull
@Override
public String getDisplayName() {
return "Android Keystore Secure Area";
}

@Override
public void createKey(@NonNull String alias,
@NonNull SecureArea.CreateKeySettings createKeySettings) {
CreateKeySettings aSettings = (CreateKeySettings) createKeySettings;
CreateKeySettings aSettings;
if (createKeySettings instanceof CreateKeySettings) {
aSettings = (CreateKeySettings) createKeySettings;
} else {
// Use default settings if user passed in a generic SecureArea.CreateKeySettings.
aSettings = new AndroidKeystoreSecureArea.CreateKeySettings.Builder(
createKeySettings.getAttestationChallenge())
.build();
}

if (aSettings.getExistingKeyAlias() != null) {
createFromExistingKey(aSettings.getExistingKeyAlias(), aSettings);
return;
Expand Down Expand Up @@ -858,7 +880,6 @@ private void saveKeyMetadata(@NonNull String alias,
public static class CreateKeySettings extends SecureArea.CreateKeySettings {
private final @KeyPurpose int mKeyPurposes;
private final @EcCurve int mEcCurve;
private final byte[] mAttestationChallenge;
private final boolean mUserAuthenticationRequired;
private final long mUserAuthenticationTimeoutMillis;
private final @UserAuthenticationType int mUserAuthenticationType;
Expand All @@ -879,10 +900,9 @@ private CreateKeySettings(@KeyPurpose int keyPurpose,
@Nullable Timestamp validFrom,
@Nullable Timestamp validUntil,
@Nullable String existingKeyAlias) {
super(AndroidKeystoreSecureArea.class);
super(attestationChallenge);
mKeyPurposes = keyPurpose;
mEcCurve = ecCurve;
mAttestationChallenge = attestationChallenge;
mUserAuthenticationRequired = userAuthenticationRequired;
mUserAuthenticationTimeoutMillis = userAuthenticationTimeoutMillis;
mUserAuthenticationType = userAuthenticationType;
Expand All @@ -893,15 +913,6 @@ private CreateKeySettings(@KeyPurpose int keyPurpose,
mExistingKeyAlias = existingKeyAlias;
}

/**
* Gets the attestation challenge.
*
* @return the attestation challenge.
*/
public @NonNull byte[] getAttestationChallenge() {
return mAttestationChallenge;
}

/**
* Gets whether user authentication is required.
*
Expand Down
Loading

0 comments on commit 60fcadc

Please sign in to comment.