Skip to content

Commit

Permalink
Add support for presentment of SD-JWT VCs via the W3C DC API.
Browse files Browse the repository at this point in the history
This is a complicated change with several steps:

- Rename Claim, MdocClaim, VcClaim in the request package to RequestedClaim,
  RequestedMdocClaim, RequestedVcClaim to better reflect it's used for a
  request (no value is provided) and to free up these names.

- Introduce Claim, MdocClaim, VcClaim which is used to model the claims /
  data elements in a credential.

- Add new Credential.getClaims() method to get all the claims in a credential.
  This involves requiring each concrete Credential subclass to specify how
  issuer-provided data is formatted b/c getClaims() now depends on it. We
  already have this for SD-JWT, add this for ISO mdoc and make it so it's the
  same format as used in OpenID4VCI: IssuerSigned CBOR according to ISO/IEC
  18013-5:2021. This makes Credential and SecureAreaBoundCredential abstract
  so update unit tests for this.

- To make it easier to work with IssuerSigned CBOR, introduce new class
  IssuerNamespaces, IssuerNamespacesBuilder, and IssuerSignedItem classes
  and add support for this in DocumentGenerator and
  MobileSecurityObjectGenerator.

- Extend DocumentType.addAttribute() to take different values for ISO mdoc
  and VC sample values.

- This allows us to now remove nameSpacedData from DocumentMetadata.

- Add new RenderClaimValue() composable to render a MdocClaim / VcClaim

- Add new screens in testapp (DocumentViewerScreen, CredentialViewerScreen,
  CredentialClaimsViewerScreen) to show docouments, credentials, and claims.

- Use this new machinery for presentment and exporting to the W3C DC API
  for both ISO mdoc and IETF SD-JWT credentials.

- Update Credman matcher to support IETF SD-JWT credentials.

- Update EUPersonalID to latest ARF (version 1.5)

- Make DrivingLicense mdoc-only (according to ARF it should only be a mdoc)

- Update JsonWebEncryption.encrypt to take APU and APV as strings.

Test: Manually tested (both samples/testapp and wallet module)
Test: New unit tests and all unit tests pass.
Signed-off-by: David Zeuthen <[email protected]>
  • Loading branch information
davidz25 committed Feb 25, 2025
1 parent 071119b commit 90f4f51
Show file tree
Hide file tree
Showing 95 changed files with 3,041 additions and 940 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ package com.android.identity.android.document

import androidx.test.platform.app.InstrumentationRegistry
import com.android.identity.android.TestUtil
import com.android.identity.claim.Claim
import com.android.identity.securearea.AndroidKeystoreCreateKeySettings
import com.android.identity.securearea.AndroidKeystoreSecureArea
import com.android.identity.credential.CredentialLoader
import com.android.identity.credential.SecureAreaBoundCredential
import com.android.identity.document.Document
import com.android.identity.document.DocumentStore
import com.android.identity.document.SimpleDocumentMetadata
import com.android.identity.documenttype.DocumentTypeRepository
import com.android.identity.securearea.CreateKeySettings
import com.android.identity.securearea.SecureArea
import com.android.identity.securearea.SecureAreaRepository
import com.android.identity.storage.Storage
import com.android.identity.storage.android.AndroidStorage
Expand Down Expand Up @@ -53,8 +58,8 @@ class AndroidKeystoreSecureAreaDocumentStoreTest {
add(AndroidKeystoreSecureArea.create(storage))
}
credentialLoader = CredentialLoader()
credentialLoader.addCredentialImplementation(SecureAreaBoundCredential::class) {
document -> SecureAreaBoundCredential(document)
credentialLoader.addCredentialImplementation(TestSecureAreaBoundCredential::class) {
document -> TestSecureAreaBoundCredential(document)
}
}

Expand All @@ -72,7 +77,7 @@ class AndroidKeystoreSecureAreaDocumentStoreTest {
val authKeyChallenge = byteArrayOf(20, 21, 22)
val secureArea =
secureAreaRepository.getImplementation(AndroidKeystoreSecureArea.IDENTIFIER)
val pendingCredential = SecureAreaBoundCredential.create(
val pendingCredential = TestSecureAreaBoundCredential.create(
document,
null,
CREDENTIAL_DOMAIN,
Expand Down Expand Up @@ -100,4 +105,41 @@ class AndroidKeystoreSecureAreaDocumentStoreTest {

Assert.assertNull(documentStore.lookupDocument("nonExistingDocument"))
}

class TestSecureAreaBoundCredential : SecureAreaBoundCredential {
companion object {
suspend fun create(
document: Document,
asReplacementForIdentifier: String?,
domain: String,
secureArea: SecureArea,
createKeySettings: CreateKeySettings
): TestSecureAreaBoundCredential {
return TestSecureAreaBoundCredential(
document,
asReplacementForIdentifier,
domain,
secureArea,
).apply {
generateKey(createKeySettings)
}
}
}

private constructor(
document: Document,
asReplacementForIdentifier: String?,
domain: String,
secureArea: SecureArea,
) : super(document, asReplacementForIdentifier, domain, secureArea) {
}

constructor(
document: Document
) : super(document) {}

override fun getClaims(documentTypeRepository: DocumentTypeRepository?): List<Claim> {
throw NotImplementedError()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class DeviceRetrievalHelperTest {
private lateinit var storage: Storage
private lateinit var secureAreaRepository: SecureAreaRepository
private lateinit var document: Document
private lateinit var nameSpacedData: NameSpacedData
private lateinit var mdocCredential: MdocCredential
private lateinit var timeSigned: Instant
private lateinit var timeValidityBegin: Instant
Expand Down Expand Up @@ -135,12 +136,11 @@ class DeviceRetrievalHelperTest {
private suspend fun asyncSetup() {
// Create the document...
document = documentStore.createDocument()
val nameSpacedData = NameSpacedData.Builder()
nameSpacedData = NameSpacedData.Builder()
.putEntryString(MDL_NAMESPACE, "given_name", "Erika")
.putEntryString(MDL_NAMESPACE, "family_name", "Mustermann")
.putEntryBoolean(AAMVA_NAMESPACE, "real_id", true)
.build()
(document.metadata as SimpleDocumentMetadata).setNameSpacedData(nameSpacedData)

// Create a credential... make sure the credential used supports both
// mdoc ECDSA and MAC authentication.
Expand Down Expand Up @@ -401,7 +401,7 @@ class DeviceRetrievalHelperTest {
val mergedIssuerNamespaces: Map<String, List<ByteArray>> =
mergeIssuerNamesSpaces(
generateDocumentRequest(request),
document.metadata.nameSpacedData,
nameSpacedData,
staticAuthData
)
generator.addDocument(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import androidx.annotation.RequiresApi
import com.android.identity.cbor.CborBuilder
import com.android.identity.cbor.DataItem
import com.android.identity.cbor.MapBuilder
import com.android.identity.claim.Claim
import com.android.identity.credential.Credential
import com.android.identity.credential.CredentialLoader
import com.android.identity.crypto.X509CertChain
import com.android.identity.document.Document
import com.android.identity.document.DocumentStore
import com.android.identity.documenttype.DocumentTypeRepository
import com.android.identity.securearea.KeyAttestation
import kotlinx.datetime.Instant

Expand Down Expand Up @@ -202,4 +204,10 @@ class DirectAccessCredential: Credential {
val documentSlot = metadata.directAccessDocumentSlot
DirectAccess.setActiveCredential(documentSlot, encryptedPresentationData)
}

override fun getClaims(
documentTypeRepository: DocumentTypeRepository?
): List<Claim> {
TODO("Not yet implemented")
}
}
1 change: 1 addition & 0 deletions identity-appsupport/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ kotlin {
dependencies {
implementation(project(":identity"))
implementation(project(":identity-mdoc"))
implementation(project(":identity-sdjwt"))
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.io.core)
Expand Down
Binary file not shown.
Loading

0 comments on commit 90f4f51

Please sign in to comment.