From c2045f276521fbba7eae4fac0c6addc16524a7ef Mon Sep 17 00:00:00 2001 From: Peter Sorotokin Date: Thu, 19 Dec 2024 11:09:58 -0800 Subject: [PATCH] Made key assertion optional for hardcoded issuing authority to avoid the need for minimal wallet server. (#825) Signed-off-by: Peter Sorotokin --- .../issuance/CredentialConfiguration.kt | 5 +++ .../issuance/RequestCredentialsFlow.kt | 4 ++- .../funke/FunkeIssuingAuthorityState.kt | 10 +++--- .../issuance/funke/FunkeProofingState.kt | 10 +++--- .../RequestCredentialsUsingKeyAttestation.kt | 4 +-- ...equestCredentialsUsingProofOfPossession.kt | 4 +-- .../hardcoded/IssuingAuthorityState.kt | 36 +++++++++---------- .../hardcoded/RequestCredentialsState.kt | 29 ++------------- .../issuance/proofing/defaultGraph.kt | 20 ++++++----- .../identity/device/DeviceAssertionMaker.kt | 4 ++- .../issuance/remote/WalletServerProvider.kt | 4 +-- .../wallet/DocumentModel.kt | 30 ++++++++-------- ...eIssuingAuthorityRequestCredentialsFlow.kt | 2 +- .../wallet/TestIssuingAuthority.kt | 5 +-- 14 files changed, 82 insertions(+), 85 deletions(-) diff --git a/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt index 4a4db73d4..040defb95 100644 --- a/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt @@ -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. * diff --git a/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt index caccac9e7..69d9240f8 100644 --- a/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt @@ -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, - keysAssertion: DeviceAssertion // wraps AssertionBindingKeys + keysAssertion: DeviceAssertion? ): List @FlowMethod diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt index 575c4f50f..aab9f5607 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt @@ -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!!, @@ -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, diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt index 36d5ea4fa..c8c2dc9e1 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt @@ -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( diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt index 6efa1f476..da7ecb0dd 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt @@ -38,12 +38,12 @@ class RequestCredentialsUsingKeyAttestation( fun sendCredentials( env: FlowEnvironment, credentialRequests: List, - keysAssertion: DeviceAssertion // holds AssertionBingingKeys + keysAssertion: DeviceAssertion? // holds AssertionBingingKeys ): List { credentialRequestSets.add(CredentialRequestSet( format = format!!, keyAttestations = credentialRequests.map { it.secureAreaBoundKeyAttestation }, - keysAssertion = keysAssertion + keysAssertion = keysAssertion!! )) return emptyList() } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt index 203a1d03f..6dc83afd7 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt @@ -49,7 +49,7 @@ class RequestCredentialsUsingProofOfPossession( suspend fun sendCredentials( env: FlowEnvironment, newCredentialRequests: List, - keysAssertion: DeviceAssertion + keysAssertion: DeviceAssertion? ): List { if (credentialRequests != null) { throw IllegalStateException("Credentials were already sent") @@ -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())) diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt index e8e9afe87..c0ec8c134 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt @@ -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) } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt index dc49546c1..3033eea18 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt @@ -1,22 +1,16 @@ 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. @@ -24,10 +18,9 @@ import com.android.identity.securearea.config.SecureAreaConfigurationSoftware @FlowState(flowInterface = RequestCredentialsFlow::class) @CborSerializable class RequestCredentialsState( - val clientId: String, val documentId: String, val credentialConfiguration: CredentialConfiguration, - val bindingKeys: MutableList = mutableListOf(), + val credentialRequests: MutableList = mutableListOf(), var format: CredentialFormat? = null ) { companion object {} @@ -46,25 +39,9 @@ class RequestCredentialsState( suspend fun sendCredentials( env: FlowEnvironment, credentialRequests: List, - keysAssertion: DeviceAssertion + keysAssertion: DeviceAssertion? ): List { - 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() } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt index 0abc781bc..4b63bd463 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt @@ -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, @@ -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, @@ -439,8 +441,9 @@ fun defaultCredentialConfiguration( builder.put("passphraseConstraints", passphraseConstraints.toDataItem()) } return CredentialConfiguration( - challenge, - SecureAreaConfigurationSoftware() + challenge = challenge, + keyAssertionRequired = false, + secureAreaConfiguration = SecureAreaConfigurationSoftware() ) } @@ -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, diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt index f3655cf55..3a5c89935 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt @@ -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 } \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt b/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt index eee4e2052..a4e12c3af 100644 --- a/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt +++ b/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt @@ -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) ) } diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt b/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt index bef648f1f..4da6f437d 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt @@ -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 diff --git a/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt index 13151d7a9..13d7f4eda 100644 --- a/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt +++ b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt @@ -23,7 +23,7 @@ class SimpleIssuingAuthorityRequestCredentialsFlow( override suspend fun sendCredentials( credentialRequests: List, - keysAssertion: DeviceAssertion + keysAssertion: DeviceAssertion? ): List { // TODO: should check attestations issuingAuthority.addCpoRequests(documentId, format, credentialRequests) diff --git a/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt b/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt index 62abbd322..be48620e6 100644 --- a/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt +++ b/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt @@ -125,8 +125,9 @@ class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {}) collectedEvidence: MutableMap ): CredentialConfiguration { return CredentialConfiguration( - ByteString(byteArrayOf(1, 2, 3)), - SecureAreaConfigurationSoftware() + challenge = ByteString(byteArrayOf(1, 2, 3)), + keyAssertionRequired = false, + secureAreaConfiguration = SecureAreaConfigurationSoftware() ) } } \ No newline at end of file