Skip to content

Commit

Permalink
Request and store a vci credential.
Browse files Browse the repository at this point in the history
  • Loading branch information
QZHelen committed Nov 22, 2024
1 parent c6e45da commit 4f98763
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 21 deletions.
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">

<application
android:usesCleartextTraffic="true"
android:name=".CmWalletApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down Expand Up @@ -42,5 +43,5 @@
</intent-filter>
</activity>
</application>

<uses-permission android:name="android.permission.INTERNET" />
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import com.credman.cmwallet.openid4vci.DATA
import com.credman.cmwallet.openid4vci.OpenId4VCI
import com.credman.cmwallet.openid4vci.PROTOCOL
import kotlinx.coroutines.runBlocking
import org.json.JSONObject

@OptIn(ExperimentalDigitalCredentialApi::class)
Expand Down Expand Up @@ -54,10 +55,14 @@ class CreateCredentialActivity : ComponentActivity() {

val openId4VCI = OpenId4VCI(requestJson.getString(DATA))

runBlocking {
openId4VCI.requestAndSaveCredential()
}

val testResponse = CreateCustomCredentialResponse(
type = DigitalCredential.TYPE_DIGITAL_CREDENTIAL,
data = Bundle().apply {
putString("androidx.credentials.BUNDLE_KEY_RESPONSE_JSON", "test response")
putString("androidx.credentials.BUNDLE_KEY_RESPONSE_JSON", "successful response")
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ class CredentialItem(

// Returns the credential that should be persisted in the app database, i.e.
// [com.credman.cmwallet.data.room.Credential.credJson]
fun toJson(): String = JSONObject()
.put(CREDENTIAL, credential.toJson())
fun toJson(): String = credential.toJson()
.put(METADATA, metadata.toJson())
.toString()
}
Expand Down Expand Up @@ -144,10 +143,12 @@ class PaymentMetadata(
val cardNetworkArt: String?, // b64 encoding
) : CredentialMetadata(title, subtitle, icon) {
override fun toJson(): JSONObject = JSONObject()
.put(TITLE, title)
.putOpt(SUBTITLE, subtitle)
.putOpt(CARD_ART, icon)
.putOpt(CARD_NETWORK_ART, cardNetworkArt)
.put(PAYMENT, JSONObject().apply {
put(TITLE, title)
putOpt(SUBTITLE, subtitle)
putOpt(CARD_ART, icon)
putOpt(CARD_NETWORK_ART, cardNetworkArt)
})
}

class VerificationMetadata(
Expand All @@ -156,12 +157,15 @@ class VerificationMetadata(
icon: String?,
) : CredentialMetadata(title, subtitle, icon) {
override fun toJson(): JSONObject = JSONObject()
.put(TITLE, title)
.putOpt(SUBTITLE, subtitle)
.putOpt(CARD_ICON, icon)
.put(VERIFICATION, JSONObject().apply {
put(TITLE, title)
putOpt(SUBTITLE, subtitle)
putOpt(CARD_ICON, icon)
})
}

const val MSO_MDOC = "mso_mdoc"
const val PAYMENT_CARD_DOC_TYPE = "com.emvco.payment_card"
private const val DEVICE_KEY = "deviceKey"
private const val ISSUER_SIGNED = "issuerSigned"
private const val METADATA = "metadata"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.credman.cmwallet.data.room

import android.util.Log
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
Expand Down
95 changes: 93 additions & 2 deletions app/src/main/java/com/credman/cmwallet/mdoc/MDoc.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
package com.credman.cmwallet.mdoc

import android.net.Uri
import android.util.Log
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import com.credman.cmwallet.cbor.CborTag
import com.credman.cmwallet.cbor.cborDecode
import com.credman.cmwallet.cbor.cborEncode
import com.credman.cmwallet.data.model.CredentialItem
import com.credman.cmwallet.data.model.MSO_MDOC
import com.credman.cmwallet.data.model.MdocCredential
import com.credman.cmwallet.data.model.MdocField
import com.credman.cmwallet.data.model.MdocNameSpace
import com.credman.cmwallet.data.model.PAYMENT_CARD_DOC_TYPE
import com.credman.cmwallet.data.model.PaymentMetadata
import com.credman.cmwallet.data.model.VerificationMetadata
import com.credman.cmwallet.openid4vci.CredConfigsSupportedItem
import com.credman.cmwallet.openid4vci.DATA
import com.credman.cmwallet.openid4vci.MdocCredConfigsSupportedItem
import java.security.PrivateKey
import java.security.Signature

Expand All @@ -15,6 +29,80 @@ fun createSessionTranscript(handover: Any): ByteArray {
return cborEncode(CborTag(24, cborEncode(sessionTranscript)))
}

fun toCredentialItem(
issuerSigned: ByteArray,
deviceKey: PrivateKey,
credentialConfiguration: CredConfigsSupportedItem,
): CredentialItem {
require(credentialConfiguration is MdocCredConfigsSupportedItem) { "Credential configuration should be"}
require(credentialConfiguration.format == MSO_MDOC) { "Expect mdo_mdoc format but got ${credentialConfiguration.format}"}
val issuerSignedDict = cborDecode(issuerSigned) as Map<*, *>
val doctype = credentialConfiguration.doctype
val claims = credentialConfiguration.claims
val issuerSignedNamespaces = (issuerSignedDict.toMutableMap()["nameSpaces"] as Map<String, *>)
val nameSpaces: Map<String, MdocNameSpace> = issuerSignedNamespaces.mapValues { (key, value) ->
Log.d(TAG, "Processing namespacedData for namespace $key")
val issuerSignedElements = value as List<*>
Log.d(TAG, "Keys ${claims?.keys}")
val namespacedClaims = claims!![key]!!
val namespacedData = mutableMapOf<String, MdocField>()
issuerSignedElements.forEach { element ->
if (element is CborTag) {
val elementDict = cborDecode(element.item as ByteArray) as Map<*, *>
val elementIdentifier = elementDict[ELEMENT_IDENTIFYIER] as String
val elementValue = elementDict[ELEMENT_VALUE] as Any
val elementDisplay = namespacedClaims.values[elementIdentifier]?.display?.firstOrNull()
if (elementDisplay?.name == null) {
Log.w(TAG, "Skipping element $elementIdentifier because it doesn't have a display value")
} else {
namespacedData[elementIdentifier] = MdocField(
value = elementValue,
display = elementDisplay.name,
displayValue = elementValue.toString() // TODO: This technically isn't right but we can't get this display value from elsewhere.
)
}
}
}
Log.d(TAG, "Added namespacedData for namespace $key: $namespacedData")
return@mapValues MdocNameSpace(data = namespacedData)
}
val cred = MdocCredential(
docType = doctype,
nameSpaces = nameSpaces,
deviceKey = deviceKey,
issuerSigned = issuerSigned
)

val regex = "image/.*,".toRegex()
val title = credentialConfiguration.display?.firstOrNull()?.name
val subtitle = credentialConfiguration.display?.firstOrNull()?.description
val itemIcon = credentialConfiguration.display?.firstOrNull()?.backgroundImage?.let {
val imageUri = Uri.parse(it)
require(imageUri.scheme == DATA) { "only image data scheme is supported for now"}
val ssp = Uri.parse(it).schemeSpecificPart
ssp.replace(regex, "")
}
val credMetadata = when (doctype) {
PAYMENT_CARD_DOC_TYPE -> PaymentMetadata(
title = title ?: "Payment Card",
subtitle = subtitle,
icon = itemIcon,
cardNetworkArt = null,
)
else -> VerificationMetadata(
title = title ?: "Card",
subtitle = subtitle,
icon = itemIcon
)
}

return CredentialItem(
id = 0L.toString(), // Auto-gen
credential = cred,
metadata = credMetadata,
)
}

fun filterIssuerSigned(
issuerSigned: ByteArray,
requiredElements: Map<String, List<String>>
Expand All @@ -31,7 +119,7 @@ fun filterIssuerSigned(
elements.forEach { element ->
if (element is CborTag) {
val elementDict = cborDecode(element.item as ByteArray) as Map<*, *>
val elementIdentifier = elementDict["elementIdentifier"] as String
val elementIdentifier = elementDict[ELEMENT_IDENTIFYIER] as String
if (elementIdentifier == requiredElement) {
newElements.add(element)
}
Expand Down Expand Up @@ -136,4 +224,7 @@ fun convertDerToRaw(signature: ByteArray): ByteArray {
signature.copyInto(ret, 32 + sPad, sOffset, sOffset + sLen)

return ret
}
}

internal const val ELEMENT_IDENTIFYIER = "elementIdentifier"
internal const val ELEMENT_VALUE = "elementValue"
36 changes: 28 additions & 8 deletions app/src/main/java/com/credman/cmwallet/openid4vci/OpenId4VCI.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.credman.cmwallet.openid4vci

import android.util.Base64
import android.util.Log
import com.credman.cmwallet.CmWalletApplication
import com.credman.cmwallet.CmWalletApplication.Companion.TAG
import com.credman.cmwallet.data.room.Credential
import com.credman.cmwallet.loadECPrivateKey
import com.credman.cmwallet.mdoc.toCredentialItem
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.cio.CIO
Expand All @@ -12,6 +17,7 @@ import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.http.headers
import org.json.JSONObject
import java.security.PrivateKey

class OpenId4VCI(val request: String) {
val requestJson: JSONObject = JSONObject(request)
Expand All @@ -23,6 +29,8 @@ class OpenId4VCI(val request: String) {
val credentialEndpoint: String
val credentialConfigurationsSupportedMap: Map<String, CredConfigsSupportedItem>

private lateinit var deviceKey: PrivateKey

init {
require(requestJson.has(CREDENTIAL_ISSUER)) { "Issuance request must contain $CREDENTIAL_ISSUER" }
require(requestJson.has(CREDENTIAL_CONFIGURATION_IDS)) { "Issuance request must contain $CREDENTIAL_CONFIGURATION_IDS" }
Expand Down Expand Up @@ -54,16 +62,18 @@ class OpenId4VCI(val request: String) {
credentialConfigurationsSupportedMap = tmpMap
}

suspend fun requestCredential() {
suspend fun requestAndSaveCredential() {
val client = HttpClient(CIO)
Log.d(TAG, "Requesting to credential endpoint $credentialEndpoint")
val credConfigId = credentialConfigurationIds.first()
val httpResponse = client.post(credentialEndpoint) {
headers {
append(HttpHeaders.Authorization, getAuthToken())
}
contentType(ContentType.Application.Json)
setBody(
CredentialRequest(
credentialConfigurationIds.first(),
credConfigId,
proof = Proof(
JWT,
jwt = generateDeviceKeyJwt()
Expand All @@ -74,19 +84,29 @@ class OpenId4VCI(val request: String) {

if (httpResponse.status.value == 202) {
Log.d(TAG, "Successful credential endpoint response." +
"Content type: ${httpResponse.headers[HttpHeaders.ContentType]}, " +
"Content body: ${httpResponse.body<String>()}")
val credResponse = httpResponse.body<String>().toCredentialResponse()
// TODO: remove !!
val credential = credResponse!!.credentials!!.first().credential
TODO()
" Content type: ${httpResponse.headers[HttpHeaders.ContentType]}.")
val stringBody: String = httpResponse.body()
Log.d(TAG, "Response body: $stringBody")
val credResponse = stringBody.toCredentialResponse()
val credentialIssuerSigned = Base64.decode(
credResponse!!.credentials!!.first().credential,
Base64.URL_SAFE
)
val credItem = toCredentialItem(
credentialIssuerSigned,
deviceKey,
credentialConfigurationsSupportedMap[credConfigId]!!)
CmWalletApplication.database.credentialDao().insertAll(Credential(0L, credItem.toJson()))
} else {
Log.e(TAG, "Error credential endpoint code: ${httpResponse.status.value}")
TODO()
}
}

private fun generateDeviceKeyJwt(): String {
// TODO: generate real device key
val tmpKey = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg6ef4-enmfQHRWUW40-Soj3aFB0rsEOp3tYMW-HJPBvChRANCAAT5N1NLZcub4bOgWfBwF8MHPGkfJ8Dm300cioatq9XovaLgG205FEXUOuNMEMQuLbrn8oiOC0nTnNIVn-OtSmSb"
deviceKey = loadECPrivateKey(Base64.decode(tmpKey, Base64.URL_SAFE))
return "TODO"
}

Expand Down

0 comments on commit 4f98763

Please sign in to comment.