Skip to content

Commit

Permalink
Assorted Credential and Auth Key changes. (#415)
Browse files Browse the repository at this point in the history
Introduce the concept of "domains" on authentication keys. This already existed
for CredentialUtil.managedAuthenticationKeyHelper() so port this method to use it.
This new concept makes it easier to have a single Credential support multiple
credential formats, for example one auth key domain could be "mdoc/mDL", another
could be "SD-JWT", a third could be something else.

Add support for NameSpacedData on ApplicationData. Also remove the nameSpacedData
member on Credential and port users to use the "credentialData" key on the associated
aplicationData member.

Application changes:
 - Auth key inspection UI: show counter instead of alias, show domain and Secure Area
 - Rework how SecureArea.CreateKeySettings instances are created for auth keys
 - Always use Android Keystore and P-256 for CredentialKey, regardless of Secure Area used
 - Move generation of response off the UI thread

With these changes, the CloudSecureArea from the experimental-cloud-secure-area
branch works out of the box, just add a couple of lines of code in
HolderApp.createCredentialStore() to add it to the SecureAreaRepository and things
work out of the box, for example

```
            val cloudSecureArea = CloudSecureArea(
                context,
                storageEngine,
                "https://your-csa-server-here.example.com/csa-server/"
            ) { /* code for checking Root Of Trust for Cloud Secure Area */ }
            Executors.newSingleThreadExecutor().execute(kotlinx.coroutines.Runnable {
                if (!cloudSecureArea.isRegistered) {
                    cloudSecureArea.register()
                }
            })
            secureAreaRepository.addImplementation(cloudSecureArea)
```

This changes the on-disk format so change the location of where local data is stored.
Users upgrading to this new version will have to reprovision their credentials..

Test: New unit tests and all unit tests pass
Test: Manually tested using both Android Keystore and Software Secure Area
  • Loading branch information
davidz25 authored Nov 16, 2023
1 parent 83840ae commit 85ef1e2
Show file tree
Hide file tree
Showing 41 changed files with 683 additions and 425 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.android.identity.securearea.SoftwareSecureArea
import com.android.identity.util.Logger
import com.android.identity.wallet.util.PeriodicKeysRefreshWorkRequest
import com.android.identity.wallet.util.PreferencesHelper
import com.android.identity.wallet.util.ProvisioningUtil
import com.google.android.material.color.DynamicColors
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
Expand All @@ -33,17 +32,17 @@ class HolderApp: Application() {
companion object {
fun createCredentialStore(
context: Context,
keystoreEngineRepository: SecureAreaRepository
secureAreaRepository: SecureAreaRepository
): CredentialStore {
val storageDir = PreferencesHelper.getKeystoreBackedStorageLocation(context)
val storageEngine = AndroidStorageEngine.Builder(context, storageDir).build()

val androidKeystoreSecureArea = AndroidKeystoreSecureArea(context, storageEngine)
val softwareSecureArea = SoftwareSecureArea(storageEngine)

keystoreEngineRepository.addImplementation(androidKeystoreSecureArea)
keystoreEngineRepository.addImplementation(softwareSecureArea)
return CredentialStore(storageEngine, keystoreEngineRepository)
secureAreaRepository.addImplementation(androidKeystoreSecureArea)
secureAreaRepository.addImplementation(softwareSecureArea)
return CredentialStore(storageEngine, secureAreaRepository)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.android.identity.wallet.support.SecureAreaSupport
import com.android.identity.wallet.theme.HolderAppTheme
import com.android.identity.wallet.transfer.AddDocumentToResponseResult
import com.android.identity.wallet.util.DocumentData
import com.android.identity.wallet.util.ProvisioningUtil
import com.android.identity.wallet.viewmodel.TransferDocumentViewModel
import com.google.android.material.bottomsheet.BottomSheetDialogFragment

Expand Down Expand Up @@ -91,8 +92,10 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {

private fun sendResponse() {
isSendingInProgress.value = true
val result = viewModel.sendResponseForSelection()
onSendResponseResult(result)
viewModel.sendResponseForSelection(
onResultReady = {
onSendResponseResult(it)
})
}

private fun getSubtitle(): String {
Expand All @@ -112,16 +115,22 @@ class AuthConfirmationFragment : BottomSheetDialogFragment() {
private fun onSendResponseResult(result: AddDocumentToResponseResult) {
when (result) {
is AddDocumentToResponseResult.DocumentLocked -> {

val secureAreaSupport = SecureAreaSupport.getInstance(
requireContext(),
result.credential
result.authKey.secureArea
)
with(secureAreaSupport) {
unlockKey(
credential = result.credential,
authKey = result.authKey,
onKeyUnlocked = { keyUnlockData ->
val responseResult = viewModel.sendResponseForSelection(keyUnlockData)
onSendResponseResult(responseResult)
viewModel.sendResponseForSelection(
onResultReady = {
onSendResponseResult(it)
},
result.authKey,
keyUnlockData
)
},
onUnlockFailure = { wasCancelled ->
if (wasCancelled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private fun PassphrasePromptUI(
) {
Text(
text = stringResource(id = R.string.passphrase_prompt_title),
style = MaterialTheme.typography.displaySmall
style = MaterialTheme.typography.titleLarge
)
Text(
text = stringResource(id = R.string.passphrase_prompt_message),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,21 @@ data class DocumentInformation(
val documentColor: Int,
val maxUsagesPerKey: Int,
val lastTimeUsed: String,
val mDocAuthOption: String,
val currentSecureArea: CurrentSecureArea,
val authKeys: List<KeyData>
) {

data class KeyData(
val alias: String,
val counter: Int,
val validFrom: String,
val validUntil: String,
val domain: String,
val issuerDataBytesCount: Int,
val usagesCount: Int,
val keyPurposes: Int,
val ecCurve: Int,
val isHardwareBacked: Boolean
val isHardwareBacked: Boolean,
val secureAreaDisplayName: String
)
}

Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,6 @@ class DocumentManager private constructor(private val context: Context) {
fun refreshAuthKeys(documentName: String) {
val documentInformation = requireNotNull(getDocumentInformation(documentName))
val credential = requireNotNull(getCredentialByName(documentName))
ProvisioningUtil.getInstance(context).refreshAuthKeys(credential, documentInformation)
ProvisioningUtil.getInstance(context).refreshAuthKeys(credential, documentInformation.docType)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ private fun AuthenticationKeyInfo(
.size(48.dp)
.padding(horizontal = 8.dp),
imageVector = Icons.Default.Key,
contentDescription = authKeyInfo.alias,
contentDescription = "${authKeyInfo.counter}",
tint = MaterialTheme.colorScheme.primary.copy(alpha = .5f)
)
Column(
Expand All @@ -321,8 +321,16 @@ private fun AuthenticationKeyInfo(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
LabeledValue(
label = stringResource(id = R.string.document_info_alias),
value = authKeyInfo.alias
label = stringResource(id = R.string.txt_keystore_implementation),
value = authKeyInfo.secureAreaDisplayName
)
LabeledValue(
label = stringResource(id = R.string.document_info_counter),
value = "${authKeyInfo.counter}"
)
LabeledValue(
label = stringResource(id = R.string.document_info_domain),
value = authKeyInfo.domain
)
LabeledValue(
label = stringResource(id = R.string.document_info_valid_from),
Expand Down Expand Up @@ -455,24 +463,29 @@ private fun PreviewDocumentInfoScreen() {
isSelfSigned = true,
authKeys = listOf(
DocumentInfoScreenState.KeyInformation(
alias = "Key Alias 1",
counter = 1,
validFrom = "16-07-2023",
validUntil = "23-07-2023",
domain = "Domain",
usagesCount = 1,
issuerDataBytesCount = "Issuer 1".toByteArray().count(),
keyPurposes = KEY_PURPOSE_AGREE_KEY,
ecCurve = EC_CURVE_P256,
isHardwareBacked = false
isHardwareBacked = false,
secureAreaDisplayName = "Secure Area Name"
),
DocumentInfoScreenState.KeyInformation(
alias = "Key Alias 2",
counter = 2,
validFrom = "16-07-2023",
validUntil = "23-07-2023",
domain = "Domain",
usagesCount = 0,
issuerDataBytesCount = "Issuer 2".toByteArray().count(),
keyPurposes = KEY_PURPOSE_SIGN,
ecCurve = EC_CURVE_ED25519,
isHardwareBacked = true
isHardwareBacked = true,
secureAreaDisplayName = "Secure Area Name"

)
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ data class DocumentInfoScreenState(

@Immutable
data class KeyInformation(
val alias: String,
val counter: Int,
val validFrom: String,
val validUntil: String,
val domain: String,
val issuerDataBytesCount: Int,
val usagesCount: Int,
@KeyPurpose val keyPurposes: Int,
@EcCurve val ecCurve: Int,
val isHardwareBacked: Boolean
val isHardwareBacked: Boolean,
val secureAreaDisplayName: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,16 @@ class DocumentInfoViewModel(
private fun List<DocumentInformation.KeyData>.asScreenStateKeys(): List<DocumentInfoScreenState.KeyInformation> {
return map { keyData ->
DocumentInfoScreenState.KeyInformation(
alias = keyData.alias,
counter = keyData.counter,
validFrom = keyData.validFrom,
validUntil = keyData.validUntil,
domain = keyData.domain,
issuerDataBytesCount = keyData.issuerDataBytesCount,
usagesCount = keyData.usagesCount,
keyPurposes = keyData.keyPurposes,
ecCurve = keyData.ecCurve,
isHardwareBacked = keyData.isHardwareBacked
isHardwareBacked = keyData.isHardwareBacked,
secureAreaDisplayName = keyData.secureAreaDisplayName
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,10 @@ class TransferDocumentFragment : Fragment() {
findNavController().navigate(direction)
} else {
// Send response with 0 documents
viewModel.sendResponseForSelection()
viewModel.sendResponseForSelection(
onResultReady = {
}
)
}
// TODO: this is kind of a hack but we really need to move the sending of the
// message to here instead of in the auth confirmation dialog
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ data class AddSelfSignedScreenState(
val cardArt: DocumentColor = DocumentColor.Green,
val documentName: String = "Driving License",
val currentSecureArea: CurrentSecureArea = ProvisioningUtil.defaultSecureArea.toSecureAreaState(),
val numberOfMso: Int = 10,
val numberOfMso: Int = 3,
val maxUseOfMso: Int = 1,
val validityInDays: Int = 30,
val minValidityInDays: Int = 10,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import co.nstant.`in`.cbor.CborBuilder
import co.nstant.`in`.cbor.CborDecoder
import co.nstant.`in`.cbor.model.Map
import com.android.identity.android.securearea.AndroidKeystoreSecureArea
import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC
import com.android.identity.android.securearea.AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF
import com.android.identity.credential.Credential
import com.android.identity.internal.Util
import com.android.identity.securearea.SecureArea
import com.android.identity.util.Timestamp
import com.android.identity.wallet.R
Expand All @@ -28,6 +32,8 @@ import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveOp
import com.android.identity.wallet.support.androidkeystore.AndroidAuthKeyCurveState
import com.android.identity.wallet.composables.state.AuthTypeState
import com.android.identity.wallet.composables.state.MdocAuthOption
import com.android.identity.wallet.util.FormatUtil
import java.io.ByteArrayInputStream

class AndroidKeystoreSecureAreaSupport(
private val capabilities: AndroidKeystoreSecureArea.Capabilities
Expand All @@ -42,12 +48,11 @@ class AndroidKeystoreSecureAreaSupport(
)

override fun Fragment.unlockKey(
credential: Credential,
authKey: Credential.AuthenticationKey,
onKeyUnlocked: (unlockData: SecureArea.KeyUnlockData?) -> Unit,
onUnlockFailure: (wasCancelled: Boolean) -> Unit
) {
val authKey = credential.findAuthenticationKey(Timestamp.now()) ?: throw IllegalStateException("No auth key available")
val keyInfo = credential.credentialSecureArea.getKeyInfo(authKey.alias) as AndroidKeystoreSecureArea.KeyInfo
val keyInfo = authKey.secureArea.getKeyInfo(authKey.alias) as AndroidKeystoreSecureArea.KeyInfo
val unlockData = AndroidKeystoreSecureArea.KeyUnlockData(authKey.alias)

val allowLskf = keyInfo.userAuthenticationType == USER_AUTHENTICATION_TYPE_LSKF
Expand All @@ -65,7 +70,7 @@ class AndroidKeystoreSecureAreaSupport(
.withCancelledCallback {
if (allowLskfUnlock) {
val runnable = {
unlockKey(credential, onKeyUnlocked, onUnlockFailure)
unlockKey(authKey, onKeyUnlocked, onUnlockFailure)
}
// Without this delay, the prompt won't reshow
Handler(Looper.getMainLooper()).postDelayed(runnable, 100)
Expand Down Expand Up @@ -165,4 +170,53 @@ class AndroidKeystoreSecureAreaSupport(
override fun getSecureAreaSupportState(): SecureAreaSupportState {
return screenState
}

override fun createAuthKeySettingsConfiguration(secureAreaSupportState: SecureAreaSupportState): ByteArray {
val state = secureAreaSupportState as AndroidKeystoreSecureAreaSupportState

var userAuthSettings = 0
if (state.allowLSKFUnlocking.isEnabled) {
userAuthSettings += AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF
}
if (state.allowBiometricUnlocking.isEnabled) {
userAuthSettings += AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC
}

return FormatUtil.cborEncode(CborBuilder()
.addMap()
.put("curve", state.authKeyCurveState.authCurve.toEcCurve().toLong())
.put("purposes", state.mDocAuthOption.mDocAuthentication.toKeyPurpose().toLong())
.put("userAuthEnabled", state.userAuthentication)
.put("userAuthTimeoutMillis", state.userAuthenticationTimeoutSeconds.toLong() * 1000L)
.put("userAuthSettings", userAuthSettings.toLong())
.put("useStrongBox", state.useStrongBox.isEnabled)
.end()
.build().get(0))
}

override fun createAuthKeySettingsFromConfiguration(
encodedConfiguration: ByteArray,
challenge: ByteArray,
validFrom: Timestamp,
validUntil: Timestamp
): SecureArea.CreateKeySettings {
val map = CborDecoder(ByteArrayInputStream(encodedConfiguration)).decode().get(0) as Map
val curve = Util.cborMapExtractNumber(map, "curve").toInt()
val purposes = Util.cborMapExtractNumber(map, "purposes").toInt()
val userAuthEnabled = Util.cborMapExtractBoolean(map, "userAuthEnabled")
val userAuthTimeoutMillis = Util.cborMapExtractNumber(map, "userAuthTimeoutMillis")
val userAuthSettings = Util.cborMapExtractNumber(map, "userAuthSettings").toInt()
val useStrongBox = Util.cborMapExtractBoolean(map, "useStrongBox")
return AndroidKeystoreSecureArea.CreateKeySettings.Builder(challenge)
.setEcCurve(curve)
.setKeyPurposes(purposes)
.setValidityPeriod(validFrom, validUntil)
.setUseStrongBox(useStrongBox)
.setUserAuthenticationRequired(
userAuthEnabled,
userAuthTimeoutMillis,
userAuthSettings
)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,48 +31,4 @@ data class AndroidKeystoreSecureAreaSupportState(
val authKeyCurveState: AndroidAuthKeyCurveState = AndroidAuthKeyCurveState(),
) : SecureAreaSupportState {

override fun createKeystoreSettings(validityInDays: Int): SecureArea.CreateKeySettings {
return AndroidKeystoreSecureArea.CreateKeySettings.Builder("challenge".toByteArray())
.setKeyPurposes(mDocAuthOption.mDocAuthentication.toKeyPurpose())
.setUseStrongBox(useStrongBox.isEnabled)
.setEcCurve(authKeyCurveState.authCurve.toEcCurve())
.setValidityPeriod(Timestamp.now(), validityInDays.toTimestampFromNow())
.setUserAuthenticationRequired(
userAuthentication,
userAuthenticationTimeoutSeconds * 1000L,
userAuthType()
).build()
}

override fun createKeystoreSettingForCredential(
mDocAuthOption: String,
credential: Credential
): SecureArea.CreateKeySettings {
val keyInfo = credential.credentialSecureArea
.getKeyInfo(credential.credentialKeyAlias) as AndroidKeystoreSecureArea.KeyInfo
val builder = AndroidKeystoreSecureArea.CreateKeySettings.Builder("challenge".toByteArray())
.setKeyPurposes(MdocAuthStateOption.valueOf(mDocAuthOption).toKeyPurpose())
.setUseStrongBox(keyInfo.isStrongBoxBacked)
.setEcCurve(keyInfo.ecCurve)
.setValidityPeriod(Timestamp.now(), keyInfo.validUntil ?: Timestamp.now())
.setUserAuthenticationRequired(
keyInfo.isUserAuthenticationRequired,
keyInfo.userAuthenticationTimeoutMillis,
keyInfo.userAuthenticationType
)
return builder.build()
}

private fun userAuthType(): Int {
var userAuthenticationType = 0
if (allowLSKFUnlocking.isEnabled) {
userAuthenticationType =
userAuthenticationType or AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_LSKF
}
if (allowBiometricUnlocking.isEnabled) {
userAuthenticationType =
userAuthenticationType or AndroidKeystoreSecureArea.USER_AUTHENTICATION_TYPE_BIOMETRIC
}
return userAuthenticationType
}
}
Loading

0 comments on commit 85ef1e2

Please sign in to comment.