Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made key assertion optional for hardcoded issuing authority to avoid the need for minimal wallet server. #825

Merged
merged 1 commit into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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!!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears the null value is treated as a special forking signal now. But here (and in other similar method that signal would throw RT NPE. Is that as expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forking signal is actually in CredentialConfiguration.keyAssertionRequired. If that is true, keysAssertion must be provided. If it is null, this will throw and at some point will be converted to HTTP status 500 - not the best error code, of course, but we did not do a pass through our APIs to make sure all error codes and messages are what we want them to be.

))
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()
)
}
}
Loading