Skip to content

Commit 0b97df8

Browse files
committed
Prepare for hardware key attestation
1 parent a2b4d82 commit 0b97df8

File tree

8 files changed

+147
-59
lines changed

8 files changed

+147
-59
lines changed

app/src/main/java/com/credman/cmwallet/createcred/CreateCredentialViewModel.kt

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import com.credman.cmwallet.CmWalletApplication.Companion.computeClientId
2020
import com.credman.cmwallet.data.model.Credential
2121
import com.credman.cmwallet.data.model.CredentialDisplayData
2222
import com.credman.cmwallet.data.model.CredentialItem
23+
import com.credman.cmwallet.data.model.CredentialKeyHardware
2324
import com.credman.cmwallet.data.model.CredentialKeySoftware
2425
import com.credman.cmwallet.data.room.CredentialDatabaseItem
2526
import com.credman.cmwallet.decodeBase64UrlNoPadding
2627
import com.credman.cmwallet.getcred.createOpenID4VPResponse
2728
import com.credman.cmwallet.mdoc.MDoc
29+
import com.credman.cmwallet.openid4vci.HardwareKey
2830
import com.credman.cmwallet.openid4vci.OpenId4VCI
2931
import com.credman.cmwallet.openid4vci.OpenId4VCI.Companion.WALLET_CLIENT_ID
32+
import com.credman.cmwallet.openid4vci.SoftwareKey
3033
import com.credman.cmwallet.openid4vci.data.AuthorizationDetailResponseOpenIdCredential
3134
import com.credman.cmwallet.openid4vci.data.CredentialConfigurationMDoc
3235
import com.credman.cmwallet.openid4vci.data.CredentialConfigurationSdJwtVc
@@ -116,23 +119,19 @@ class CreateCredentialViewModel : ViewModel() {
116119
if (tokenResponse.authorizationDetails == null) {
117120
val scopes = (tokenResponse.scopes ?: tokenRequestScope)?.split(" ")
118121
scopes?.forEach { scope ->
119-
val deviceKeys: MutableList<KeyPair> = mutableListOf()
120-
val kpg = KeyPairGenerator.getInstance("EC")
121-
kpg.initialize(ECGenParameterSpec("secp256r1"))
122-
for (i in 0..< (openId4VCI.credentialOffer.issuerMetadata.batchCredentialIssuance?.batchSize ?: 1)) {
123-
deviceKeys.add(kpg.genKeyPair())
124-
}
122+
val keyPairsAndProofs = openId4VCI.createKeyProofs(scope)
125123
val credentialResponse = openId4VCI.requestCredentialFromEndpoint(
126124
accessToken = tokenResponse.accessToken,
127125
credentialRequest = CredentialRequest(
128126
credentialConfigurationId = scope,
129-
proofs = openId4VCI.createProofJwt(deviceKeys)
127+
proofs = keyPairsAndProofs.first
130128
)
131129
)
132130
Log.i(TAG, "credentialResponse $credentialResponse")
133131
val config = openId4VCI.credentialOffer.issuerMetadata.credentialConfigurationsSupported[scope]!!
134132
val display = credentialResponse.display?.firstOrNull()
135133
val configDisplay = config.credentialMetadata?.display?.firstOrNull()
134+
val deviceKeys = keyPairsAndProofs.second
136135
val newCredentialItem = CredentialItem(
137136
id = Uuid.random().toHexString(),
138137
config = config,
@@ -147,7 +146,7 @@ class CreateCredentialViewModel : ViewModel() {
147146
val mdoc = MDoc(it.credential.decodeBase64UrlNoPadding())
148147
val deviceKey = mdoc.deviceKey
149148
deviceKeys.firstOrNull {
150-
val public = it.public as ECPublicKey
149+
val public = it.publicKey as ECPublicKey
151150
val x = String(public.w.affineX.toFixedByteArray(32))
152151
val y = String(public.w.affineY.toFixedByteArray(32))
153152
x == deviceKey.first && y == deviceKey.second
@@ -157,7 +156,7 @@ class CreateCredentialViewModel : ViewModel() {
157156
val issuerJwtString = it.credential.split('~')[0]
158157
val cnfKey = IssuerJwt(issuerJwtString).payload.getJSONObject("cnf").getJSONObject("jwk")
159158
deviceKeys.firstOrNull {
160-
val public = it.public as ECPublicKey
159+
val public = it.publicKey as ECPublicKey
161160
val x = public.w.affineX.toFixedByteArray(32).toBase64UrlNoPadding()
162161
val y = public.w.affineY.toFixedByteArray(32).toBase64UrlNoPadding()
163162
x == cnfKey.getString("x") && y == cnfKey.getString("y")
@@ -166,10 +165,18 @@ class CreateCredentialViewModel : ViewModel() {
166165
else -> throw UnsupportedOperationException("Unknown configuration $config")
167166
}
168167
Credential(
169-
key = CredentialKeySoftware(
170-
publicKey = Base64.encodeToString(deviceKeyPair!!.public.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
171-
privateKey = Base64.encodeToString(deviceKeyPair.private.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
172-
),
168+
key = deviceKeyPair!!.let { deviceKey ->
169+
when (deviceKey) {
170+
is SoftwareKey -> CredentialKeySoftware(
171+
publicKey = Base64.encodeToString(deviceKey.publicKey.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
172+
privateKey = Base64.encodeToString(deviceKey.privateKey.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
173+
)
174+
is HardwareKey -> CredentialKeyHardware(
175+
publicKey = Base64.encodeToString(deviceKey.publicKey.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
176+
keyAlias = deviceKey.keyAlias
177+
)
178+
}
179+
},
173180
credential = it.credential
174181
)
175182
}
@@ -181,23 +188,19 @@ class CreateCredentialViewModel : ViewModel() {
181188
when (authDetail) {
182189
is AuthorizationDetailResponseOpenIdCredential -> {
183190
authDetail.credentialIdentifiers.forEach { credentialId ->
184-
val deviceKeys: MutableList<KeyPair> = mutableListOf()
185-
val kpg = KeyPairGenerator.getInstance("EC")
186-
kpg.initialize(ECGenParameterSpec("secp256r1"))
187-
for (i in 0..< (openId4VCI.credentialOffer.issuerMetadata.batchCredentialIssuance?.batchSize ?: 1)) {
188-
deviceKeys.add(kpg.genKeyPair())
189-
}
191+
val keyPairsAndProofs = openId4VCI.createKeyProofs(authDetail.credentialConfigurationId)
190192
val credentialResponse = openId4VCI.requestCredentialFromEndpoint(
191193
accessToken = tokenResponse.accessToken,
192194
credentialRequest = CredentialRequest(
193195
credentialIdentifier = credentialId,
194-
proofs = openId4VCI.createProofJwt(deviceKeys)
196+
proofs = keyPairsAndProofs.first
195197
),
196198
)
197199
Log.i(TAG, "credentialResponse $credentialResponse")
198200
val config = openId4VCI.credentialOffer.issuerMetadata.credentialConfigurationsSupported[authDetail.credentialConfigurationId]!!
199201
val display = credentialResponse.display?.firstOrNull()
200202
val displayFromOffer = config.credentialMetadata?.display?.firstOrNull()
203+
val deviceKeys = keyPairsAndProofs.second
201204
val newCredentialItem = CredentialItem(
202205
id = Uuid.random().toHexString(),
203206
config = config,
@@ -212,7 +215,7 @@ class CreateCredentialViewModel : ViewModel() {
212215
val mdoc = MDoc(it.credential.decodeBase64UrlNoPadding())
213216
val deviceKey = mdoc.deviceKey
214217
deviceKeys.firstOrNull {
215-
val public = it.public as ECPublicKey
218+
val public = it.publicKey as ECPublicKey
216219
val x = String(public.w.affineX.toFixedByteArray(32))
217220
val y = String(public.w.affineY.toFixedByteArray(32))
218221
x == deviceKey.first && y == deviceKey.second
@@ -222,7 +225,7 @@ class CreateCredentialViewModel : ViewModel() {
222225
val issuerJwtString = it.credential.split('~')[0]
223226
val cnfKey = IssuerJwt(issuerJwtString).payload.getJSONObject("cnf").getJSONObject("jwk")
224227
deviceKeys.firstOrNull {
225-
val public = it.public as ECPublicKey
228+
val public = it.publicKey as ECPublicKey
226229
val x = public.w.affineX.toFixedByteArray(32).toBase64UrlNoPadding()
227230
val y = public.w.affineY.toFixedByteArray(32).toBase64UrlNoPadding()
228231
x == cnfKey.getString("x") && y == cnfKey.getString("y")
@@ -232,10 +235,18 @@ class CreateCredentialViewModel : ViewModel() {
232235
}
233236

234237
Credential(
235-
key = CredentialKeySoftware(
236-
publicKey = Base64.encodeToString(deviceKeyPair!!.public.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
237-
privateKey = Base64.encodeToString(deviceKeyPair.private.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
238-
),
238+
key = deviceKeyPair!!.let { deviceKey ->
239+
when (deviceKey) {
240+
is SoftwareKey -> CredentialKeySoftware(
241+
publicKey = Base64.encodeToString(deviceKey.publicKey.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
242+
privateKey = Base64.encodeToString(deviceKey.privateKey.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
243+
)
244+
is HardwareKey -> CredentialKeyHardware(
245+
publicKey = Base64.encodeToString(deviceKey.publicKey.encoded, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
246+
keyAlias = deviceKey.keyAlias
247+
)
248+
}
249+
},
239250
credential = it.credential
240251
)
241252
}

app/src/main/java/com/credman/cmwallet/data/model/CredentialDataModel.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.credman.cmwallet.data.model
22

33
import com.credman.cmwallet.decodeBase64UrlNoPadding
4+
import com.credman.cmwallet.loadECPrivateKey
45
import com.credman.cmwallet.mdoc.MDoc
56
import com.credman.cmwallet.openid4vci.data.CredentialConfiguration
67
import kotlinx.serialization.SerialName
78
import kotlinx.serialization.Serializable
9+
import java.security.KeyStore
10+
import java.security.PrivateKey
811

912
@Serializable
1013
data class CredentialItem(
@@ -39,7 +42,7 @@ data class CredentialKeySoftware(
3942
@SerialName("HARDWARE")
4043
data class CredentialKeyHardware(
4144
val publicKey: String,
42-
val privateKey: String
45+
val keyAlias: String,
4346
) : CredentialKey()
4447

4548
@Serializable
@@ -50,3 +53,16 @@ data class CredentialDisplayData(
5053
val explainer: String? = null,
5154
val metadataDisplayText: String? = null,
5255
)
56+
57+
fun CredentialKey.toPrivateKey(): PrivateKey {
58+
return when (this) {
59+
is CredentialKeySoftware ->
60+
loadECPrivateKey(this.privateKey.decodeBase64UrlNoPadding())
61+
is CredentialKeyHardware -> {
62+
val keyStore = KeyStore.getInstance("AndroidKeyStore")
63+
keyStore.load(null)
64+
keyStore.getKey(this.keyAlias, null) as? PrivateKey
65+
?: throw IllegalArgumentException("No private key found for alias: $this.keyAlias")
66+
}
67+
}
68+
}

app/src/main/java/com/credman/cmwallet/data/repository/CredentialRepository.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.credman.cmwallet.CmWalletApplication
2121
import com.credman.cmwallet.data.model.CredentialDisplayData
2222
import com.credman.cmwallet.data.model.CredentialItem
2323
import com.credman.cmwallet.data.model.CredentialKeySoftware
24+
import com.credman.cmwallet.data.model.toPrivateKey
2425
import com.credman.cmwallet.data.source.CredentialDatabaseDataSource
2526
import com.credman.cmwallet.data.source.TestCredentialsDataSource
2627
import com.credman.cmwallet.decodeBase64
@@ -227,7 +228,7 @@ class CredentialRepository {
227228
items.forEach { item ->
228229
when (item.config) {
229230
is CredentialConfigurationSdJwtVc -> {
230-
val sdJwtVc = SdJwt(item.credentials.first().credential, (item.credentials.first().key as CredentialKeySoftware).privateKey)
231+
val sdJwtVc = SdJwt(item.credentials.first().credential, item.credentials.first().key.toPrivateKey())
231232
val rawJwt = sdJwtVc.verifiedResult.processedJwt
232233
val claims = mutableListOf<SdJwtClaim>()
233234
constructJwtClaims(rawJwt, item.config, claims, emptyList())

app/src/main/java/com/credman/cmwallet/getcred/GetCredentialActivity.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.credman.cmwallet.CmWalletApplication.Companion.computeClientId
2727
import com.credman.cmwallet.createcred.CreateCredentialActivity
2828
import com.credman.cmwallet.data.model.CredentialItem
2929
import com.credman.cmwallet.data.model.CredentialKeySoftware
30+
import com.credman.cmwallet.data.model.toPrivateKey
3031
import com.credman.cmwallet.decodeBase64UrlNoPadding
3132
import com.credman.cmwallet.getcred.GetCredentialActivity.DigitalCredentialResult
3233
import com.credman.cmwallet.loadECPrivateKey
@@ -75,17 +76,17 @@ fun createOpenID4VPResponse(
7576
(matchedCredential.matchedClaims as OpenId4VPMatchedSdJwtClaims).claimSets
7677
val sdJwtVc = SdJwt(
7778
selectedCredential.credentials.first().credential,
78-
(selectedCredential.credentials.first().key as CredentialKeySoftware).privateKey
79+
selectedCredential.credentials.first().key.toPrivateKey()
7980
)
80-
val transaction_data_hashes =
81+
val transactionDataHashes =
8182
openId4VPRequest.generateDeviceSignedTransactionData(matchedCredential.dcqlId).deviceSignedTransactionData
8283

8384
credentialResponse =
8485
sdJwtVc.present(
8586
claims,
8687
nonce = openId4VPRequest.nonce,
8788
aud = openId4VPRequest.getSdJwtKbAud(origin),
88-
transactionDataHashes = transaction_data_hashes
89+
transactionDataHashes = transactionDataHashes
8990
)
9091
}
9192

@@ -116,8 +117,7 @@ fun createOpenID4VPResponse(
116117
)
117118
)
118119
}
119-
val devicePrivateKey =
120-
loadECPrivateKey((selectedCredential.credentials.first().key as CredentialKeySoftware).privateKey.decodeBase64UrlNoPadding())
120+
val devicePrivateKey = selectedCredential.credentials.first().key.toPrivateKey()
121121
val deviceResponse = generateDeviceResponse(
122122
doctype = selectedCredential.config.doctype,
123123
issuerSigned = filteredIssuerSigned,

0 commit comments

Comments
 (0)