Skip to content

Commit

Permalink
Made key assertion optional for hardcoded issuing authority to avoid …
Browse files Browse the repository at this point in the history
…the need for minimal wallet server. (#825)

Signed-off-by: Peter Sorotokin <[email protected]>
  • Loading branch information
sorotokin authored Dec 19, 2024
1 parent ef558dc commit c2045f2
Show file tree
Hide file tree
Showing 14 changed files with 82 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ data class CredentialConfiguration(
*/
val challenge: ByteString,

/**
* Key assertion parameter to [RequestCredentialsFlow.sendCredentials] is required.
*/
val keyAssertionRequired: Boolean,

/**
* The configuration for the device-bound key for e.g. access control.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,14 @@ interface RequestCredentialsFlow : FlowBase {
*
* @param credentialRequests a list of credentials requests, each representing a
* request for a issuer data generation along with the format requested.
* @param keysAssertion DeviceAssertion that wraps AssertionBindingKeys, only required
* if [CredentialConfiguration.keyAssertionRequired] is true
* @throws IllegalArgumentException if the issuer rejects the one or more of the requests.
*/
@FlowMethod
suspend fun sendCredentials(
credentialRequests: List<CredentialRequest>,
keysAssertion: DeviceAssertion // wraps AssertionBindingKeys
keysAssertion: DeviceAssertion?
): List<KeyPossessionChallenge>

@FlowMethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,9 @@ class FunkeIssuingAuthorityState(
val purposes = setOf(KeyPurpose.SIGN, KeyPurpose.AGREE_KEY)
val configuration = if (document.secureAreaIdentifier!!.startsWith("CloudSecureArea?")) {
CredentialConfiguration(
ByteString(cNonce.toByteArray()),
SecureAreaConfigurationCloud(
challenge = ByteString(cNonce.toByteArray()),
keyAssertionRequired = true,
secureAreaConfiguration = SecureAreaConfigurationCloud(
purposes = KeyPurpose.encodeSet(purposes),
curve = EcCurve.P256.coseCurveIdentifier,
cloudSecureAreaId = document.secureAreaIdentifier!!,
Expand All @@ -343,8 +344,9 @@ class FunkeIssuingAuthorityState(
)
} else {
CredentialConfiguration(
ByteString(cNonce.toByteArray()),
SecureAreaConfigurationAndroidKeystore(
challenge = ByteString(cNonce.toByteArray()),
keyAssertionRequired = true,
secureAreaConfiguration = SecureAreaConfigurationAndroidKeystore(
purposes = KeyPurpose.encodeSet(purposes),
curve = EcCurve.P256.coseCurveIdentifier,
useStrongBox = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,12 @@ class FunkeProofingState(
val assertionMaker = env.getInterface(DeviceAssertionMaker::class)!!
applicationSupport.createJwtClientAssertion(
clientKeyInfo.attestation,
assertionMaker.makeDeviceAssertion(AssertionDPoPKey(
clientKeyInfo.publicKey,
credentialIssuerUri
))
assertionMaker.makeDeviceAssertion {
AssertionDPoPKey(
clientKeyInfo.publicKey,
credentialIssuerUri
)
}
)
} else {
ApplicationSupportState(clientId).createJwtClientAssertion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ class RequestCredentialsUsingKeyAttestation(
fun sendCredentials(
env: FlowEnvironment,
credentialRequests: List<CredentialRequest>,
keysAssertion: DeviceAssertion // holds AssertionBingingKeys
keysAssertion: DeviceAssertion? // holds AssertionBingingKeys
): List<KeyPossessionChallenge> {
credentialRequestSets.add(CredentialRequestSet(
format = format!!,
keyAttestations = credentialRequests.map { it.secureAreaBoundKeyAttestation },
keysAssertion = keysAssertion
keysAssertion = keysAssertion!!
))
return emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class RequestCredentialsUsingProofOfPossession(
suspend fun sendCredentials(
env: FlowEnvironment,
newCredentialRequests: List<CredentialRequest>,
keysAssertion: DeviceAssertion
keysAssertion: DeviceAssertion?
): List<KeyPossessionChallenge> {
if (credentialRequests != null) {
throw IllegalStateException("Credentials were already sent")
Expand All @@ -61,7 +61,7 @@ class RequestCredentialsUsingProofOfPossession(
env = env,
deviceAttestation = clientRecord.deviceAttestation,
keyAttestations = newCredentialRequests.map { it.secureAreaBoundKeyAttestation },
deviceAssertion = keysAssertion,
deviceAssertion = keysAssertion!!,
nonce = credentialConfiguration.challenge
)
val nonce = JsonPrimitive(String(credentialConfiguration.challenge.toByteArray()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,31 +289,31 @@ class IssuingAuthorityState(
walletApplicationCapabilities,
issuerDocument.collectedEvidence
)
return RequestCredentialsState(clientId, documentId, credentialConfiguration)
return RequestCredentialsState(documentId, credentialConfiguration)
}

@FlowJoin
suspend fun completeRequestCredentials(env: FlowEnvironment, state: RequestCredentialsState) {
val issuerDocument = loadIssuerDocument(env, state.documentId)
for (bindingKeySet in state.bindingKeys) {
for (request in state.credentialRequests) {
// Skip if we already have a request for the authentication key
for (authenticationKey in bindingKeySet.publicKeys) {
if (hasCpoRequestForAuthenticationKey(issuerDocument, authenticationKey)) {
continue
}
val presentationData = createPresentationData(
env,
state.format!!,
issuerDocument.documentConfiguration!!,
authenticationKey
)
val simpleCredentialRequest = SimpleCredentialRequest(
authenticationKey,
CredentialFormat.MDOC_MSO,
presentationData,
)
issuerDocument.simpleCredentialRequests.add(simpleCredentialRequest)
if (hasCpoRequestForAuthenticationKey(issuerDocument,
request.secureAreaBoundKeyAttestation.publicKey)) {
continue
}
val authenticationKey = request.secureAreaBoundKeyAttestation.publicKey
val presentationData = createPresentationData(
env,
state.format!!,
issuerDocument.documentConfiguration!!,
authenticationKey
)
val simpleCredentialRequest = SimpleCredentialRequest(
authenticationKey,
CredentialFormat.MDOC_MSO,
presentationData,
)
issuerDocument.simpleCredentialRequests.add(simpleCredentialRequest)
}
updateIssuerDocument(env, state.documentId, issuerDocument)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
package com.android.identity.issuance.hardcoded

import com.android.identity.cbor.annotation.CborSerializable
import com.android.identity.device.AssertionBindingKeys
import com.android.identity.device.DeviceAssertion
import com.android.identity.flow.annotation.FlowMethod
import com.android.identity.flow.annotation.FlowState
import com.android.identity.flow.server.FlowEnvironment
import com.android.identity.flow.server.Storage
import com.android.identity.issuance.CredentialConfiguration
import com.android.identity.issuance.CredentialFormat
import com.android.identity.issuance.CredentialRequest
import com.android.identity.issuance.KeyPossessionChallenge
import com.android.identity.issuance.KeyPossessionProof
import com.android.identity.issuance.RequestCredentialsFlow
import com.android.identity.issuance.validateDeviceAssertionBindingKeys
import com.android.identity.issuance.wallet.ClientRecord
import com.android.identity.issuance.wallet.fromCbor
import com.android.identity.securearea.config.SecureAreaConfigurationSoftware

/**
* State of [RequestCredentialsFlow] RPC implementation.
*/
@FlowState(flowInterface = RequestCredentialsFlow::class)
@CborSerializable
class RequestCredentialsState(
val clientId: String,
val documentId: String,
val credentialConfiguration: CredentialConfiguration,
val bindingKeys: MutableList<AssertionBindingKeys> = mutableListOf(),
val credentialRequests: MutableList<CredentialRequest> = mutableListOf(),
var format: CredentialFormat? = null
) {
companion object {}
Expand All @@ -46,25 +39,9 @@ class RequestCredentialsState(
suspend fun sendCredentials(
env: FlowEnvironment,
credentialRequests: List<CredentialRequest>,
keysAssertion: DeviceAssertion
keysAssertion: DeviceAssertion?
): List<KeyPossessionChallenge> {
val storage = env.getInterface(Storage::class)!!
val clientRecord = ClientRecord.fromCbor(
storage.get("Clients", "", clientId)!!.toByteArray())
val assertion = if (credentialConfiguration.secureAreaConfiguration is SecureAreaConfigurationSoftware) {
// if explicitly asked for software secure area, don't validate
// (it will fail), just blindly trust.
keysAssertion.assertion as AssertionBindingKeys
} else {
validateDeviceAssertionBindingKeys(
env = env,
deviceAttestation = clientRecord.deviceAttestation,
keyAttestations = credentialRequests.map { it.secureAreaBoundKeyAttestation },
deviceAssertion = keysAssertion,
nonce = credentialConfiguration.challenge
)
}
bindingKeys.add(assertion)
this.credentialRequests.addAll(credentialRequests)
return emptyList()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,9 @@ fun defaultCredentialConfiguration(
val challenge = ByteString(Random.nextBytes(16))
if (!collectedEvidence.containsKey("devmode_sa")) {
return CredentialConfiguration(
challenge,
SecureAreaConfigurationAndroidKeystore(
challenge = challenge,
keyAssertionRequired = false,
secureAreaConfiguration = SecureAreaConfigurationAndroidKeystore(
purposes = KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN)),
curve = EcCurve.P256.coseCurveIdentifier,
useStrongBox = true,
Expand Down Expand Up @@ -299,8 +300,9 @@ fun defaultCredentialConfiguration(
else -> throw IllegalStateException()
}
return CredentialConfiguration(
challenge,
SecureAreaConfigurationAndroidKeystore(
challenge = challenge,
keyAssertionRequired = false,
secureAreaConfiguration = SecureAreaConfigurationAndroidKeystore(
curve = curve.coseCurveIdentifier,
purposes = KeyPurpose.encodeSet(purposes),
useStrongBox = useStrongBox,
Expand Down Expand Up @@ -439,8 +441,9 @@ fun defaultCredentialConfiguration(
builder.put("passphraseConstraints", passphraseConstraints.toDataItem())
}
return CredentialConfiguration(
challenge,
SecureAreaConfigurationSoftware()
challenge = challenge,
keyAssertionRequired = false,
secureAreaConfiguration = SecureAreaConfigurationSoftware()
)
}

Expand All @@ -459,8 +462,9 @@ fun defaultCredentialConfiguration(
val cloudSecureAreaId = (collectedEvidence["devmode_sa_cloud_setup_csa"] as EvidenceResponseSetupCloudSecureArea)
.cloudSecureAreaIdentifier
return CredentialConfiguration(
challenge,
SecureAreaConfigurationCloud(
challenge = challenge,
keyAssertionRequired = false,
secureAreaConfiguration = SecureAreaConfigurationCloud(
purposes = KeyPurpose.encodeSet(purposes),
curve = EcCurve.P256.coseCurveIdentifier,
cloudSecureAreaId = cloudSecureAreaId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.android.identity.device

fun interface DeviceAssertionMaker {
suspend fun makeDeviceAssertion(assertion: Assertion): DeviceAssertion
suspend fun makeDeviceAssertion(
assertion: (clientId: String) -> Assertion
): DeviceAssertion
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ class WalletServerProvider(

private val storage = StorageImpl(context, "wallet_servers")

val assertionMaker = DeviceAssertionMaker { assertion ->
val assertionMaker = DeviceAssertionMaker { assertionFactory ->
val applicationSupportConnection = applicationSupportSupplier!!.getApplicationSupport()
DeviceCheck.generateAssertion(
secureArea = secureArea,
deviceAttestationId = applicationSupportConnection.deviceAttestationId,
assertion = assertion
assertion = assertionFactory(applicationSupportConnection.clientId)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -892,20 +892,22 @@ class DocumentModel(
)
)
}
val applicationSupportConnection =
walletServerProvider.getApplicationSupportConnection()
val keysAssertion = walletServerProvider.assertionMaker.makeDeviceAssertion(
AssertionBindingKeys(
publicKeys = credentialRequests.map { request ->
request.secureAreaBoundKeyAttestation.publicKey
},
nonce = credConfig.challenge,
clientId = applicationSupportConnection.clientId,
keyStorage = listOf(),
userAuthentication = listOf(),
issuedAt = Clock.System.now()
)
)
val keysAssertion = if (credConfig.keyAssertionRequired) {
walletServerProvider.assertionMaker.makeDeviceAssertion { clientId ->
AssertionBindingKeys(
publicKeys = credentialRequests.map { request ->
request.secureAreaBoundKeyAttestation.publicKey
},
nonce = credConfig.challenge,
clientId = clientId,
keyStorage = listOf(),
userAuthentication = listOf(),
issuedAt = Clock.System.now()
)
}
} else {
null
}
val challenges = requestCredentialsFlow.sendCredentials(
credentialRequests = credentialRequests,
keysAssertion = keysAssertion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class SimpleIssuingAuthorityRequestCredentialsFlow(

override suspend fun sendCredentials(
credentialRequests: List<CredentialRequest>,
keysAssertion: DeviceAssertion
keysAssertion: DeviceAssertion?
): List<KeyPossessionChallenge> {
// TODO: should check attestations
issuingAuthority.addCpoRequests(documentId, format, credentialRequests)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,9 @@ class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {})
collectedEvidence: MutableMap<String, EvidenceResponse>
): CredentialConfiguration {
return CredentialConfiguration(
ByteString(byteArrayOf(1, 2, 3)),
SecureAreaConfigurationSoftware()
challenge = ByteString(byteArrayOf(1, 2, 3)),
keyAssertionRequired = false,
secureAreaConfiguration = SecureAreaConfigurationSoftware()
)
}
}

0 comments on commit c2045f2

Please sign in to comment.