Skip to content

Commit

Permalink
Refactor consent prompt.
Browse files Browse the repository at this point in the history
The consent prompt currently has mdoc-specific code deep in its bowels
to figure out `displayName` and also if a requested data element
exists in the credential to be presented. This makes it tricky to
reuse for SD-JWT VC presentations in a clean way.

Simplify all this code and introduce `ConsentField` type so the caller
can just pass in which fields the consent prompt should show. Also
provide credential-specific specializations - `MdocConsentField` and
`VcConsentField` - so we can use the same data structures for
generating credential-specific responses. Finally provide helpers to
build lists of these given a credential request and modify all
presentation paths to use this.

Other fixes:
- Use correct VCT in EuPersonalId definition and our own SD-JWT PID issuer.
- Generate all claims in own SD-JWT issuer, not just strings and numbers.
- Use DocumentTypeRepository in DocumentDetailsScreen for SD-JWT.
- Fix age_over_21 mDL requests to use age 21 instead of 18
- Comment out creation time check as it seems flaky, added a TODO

Manully tested for Credman mdoc, OpenID4VP mdoc, OpenID4VP SD-JWT,
Proximity mdoc presentations. For SD-JWT tested both our own issuer
and Funke.

Test: Manully tested, see above.
Test: ./gradlew check
Test: ./gradlew connectedCheck

Signed-off-by: David Zeuthen <[email protected]>
  • Loading branch information
davidz25 committed Aug 26, 2024
1 parent 9b38d5a commit d3a7b91
Show file tree
Hide file tree
Showing 17 changed files with 552 additions and 419 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ object EUPersonalID {
fun getDocumentType(): DocumentType {
return DocumentType.Builder("EU Personal ID")
.addMdocDocumentType(EUPID_DOCTYPE)
.addVcDocumentType("EuPersonalID")
.addVcDocumentType(EUPID_VCT)
.addAttribute(
DocumentAttributeType.String,
"family_name",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.android.identity.cbor.Cbor
import com.android.identity.cbor.CborArray
import com.android.identity.cbor.CborInt
import com.android.identity.cbor.DataItem
import com.android.identity.cbor.DiagnosticOption
import com.android.identity.cbor.Simple
import com.android.identity.cbor.Tagged
import com.android.identity.cbor.Tstr
import com.android.identity.cbor.annotation.CborSerializable
Expand Down Expand Up @@ -510,18 +512,40 @@ class IssuingAuthorityState(
documentConfiguration: DocumentConfiguration,
authenticationKey: EcPublicKey
): ByteArray {
// For now, just use the mdoc data element names and only import tstr and numbers
// For now, just use the mdoc data element names and pretty print its value
//
val identityAttributes = buildJsonObject {
for (nsName in documentConfiguration.mdocConfiguration!!.staticData.nameSpaceNames) {
for (deName in documentConfiguration.mdocConfiguration!!.staticData.getDataElementNames(nsName)) {
val value = Cbor.decode(
documentConfiguration.mdocConfiguration!!.staticData.getDataElement(nsName, deName)
)
// TODO: This will need support for bstr once we support that in a DocumentType.
when (value) {
is Tstr -> put(deName, value.asTstr)
is CborInt -> put(deName, value.asNumber.toString())
else -> {} /* do nothing */
is CborInt -> put(deName, value.asNumber)
is Simple -> {
if (value == Simple.TRUE) {
put(deName, true)
} else if (value == Simple.FALSE){
put(deName, false)
} else {
put(deName, value.toString())
}
}
else -> {
put(
deName,
Cbor.toDiagnostics(
value,
setOf(
DiagnosticOption.PRETTY_PRINT,
DiagnosticOption.BSTR_PRINT_LENGTH,
DiagnosticOption.EMBEDDED_CBOR
)
)
)
}
}
}
}
Expand Down Expand Up @@ -706,7 +730,7 @@ class IssuingAuthorityState(
staticData = staticData,
),
sdJwtVcDocumentConfiguration = SdJwtVcDocumentConfiguration(
"https://example.bmi.bund.de/credential/pid/1.0"
EUPersonalID.EUPID_VCT
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,65 @@ object MdocUtil {
return issuerSignedData
}

/**
* Combines document data with static authentication data for a given request.
*
* This goes through all data element name in a given set of name spaces and data elements
* and for each data element name, looks up `documentData` and `staticAuthData`
* for the value and if present, will include that in the result.
*
* The result is intended to mimic `IssuerNameSpaces` CBOR as defined
* in ISO 18013-5 except that the data is returned using a native maps and lists.
* The returned data is a map from name spaces into a list of the bytes of the
* `IssuerSignedItemBytes` CBOR.
*
* @param request a [DocumentRequest] indicating which name spaces and data
* element names to include in the result.
* @param documentData Document data, organized by name space.
* @param staticAuthData Static authentication data.
* @return A map described above.
*/
fun mergeIssuerNamesSpaces(
dataElements: Map<String, List<String>>,
documentData: NameSpacedData,
staticAuthData: StaticAuthData
): Map<String, MutableList<ByteArray>> {
val issuerSignedItemMap = calcIssuerSignedItemMap(staticAuthData.digestIdMapping)
val issuerSignedData: MutableMap<String, MutableList<ByteArray>> = LinkedHashMap()
for ((nameSpaceName, dataElementsInNamespace) in dataElements) {
for (dataElementName in dataElementsInNamespace) {
if (!documentData.hasDataElement(nameSpaceName, dataElementName)) {
Logger.d(TAG,
"No data element in document for nameSpace $nameSpaceName "
+ " dataElementName $dataElementName"
)
continue
}
val value = documentData.getDataElement(nameSpaceName, dataElementName)
val encodedIssuerSignedItemMaybeWithoutValue =
lookupIssuerSignedMap(issuerSignedItemMap, nameSpaceName, dataElementName)
if (encodedIssuerSignedItemMaybeWithoutValue == null) {
Logger.w(TAG, "No IssuerSignedItem for $nameSpaceName $dataElementName")
continue
}
var encodedIssuerSignedItem: ByteArray?
encodedIssuerSignedItem =
if (hasElementValue(encodedIssuerSignedItemMaybeWithoutValue)) {
encodedIssuerSignedItemMaybeWithoutValue
} else {
issuerSignedItemSetValue(encodedIssuerSignedItemMaybeWithoutValue, value)
}
val list = issuerSignedData.getOrPut(nameSpaceName) { ArrayList() }

// We need a tagged bstr here
val taggedEncodedIssuerSignedItem =
Cbor.encode(Tagged(24, Bstr(encodedIssuerSignedItem)))
list.add(taggedEncodedIssuerSignedItem)
}
}
return issuerSignedData
}

private fun hasElementValue(encodedIssuerSignedItem: ByteArray): Boolean =
Cbor.decode(encodedIssuerSignedItem)["elementValue"] != Simple.NULL

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ package com.android.identity.document
* to generate the resulting `DeviceResponse`. This CBOR can then be sent to the remote
* reader.
*
* TODO: Should be removed since this is now obsoleted by List<ConsentField> in the wallet app.
*
* @param requestedDataElements A list of [DocumentRequest.DataElement] instances.
*/
class DocumentRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ class DocumentTypeRepository {
it.mdocDocumentType?.docType?.equals(mdocDocType) ?: false
}

/**
* Gets the first [DocumentType] in [documentTypes] with a given VC vct.
*
* @param vct the Verification Credential Type.
* @return the [DocumentType] or null if not found.
*/
fun getDocumentTypeForVc(vct: String): DocumentType? =
_documentTypes.find {
it.vcDocumentType?.type?.equals(vct) ?: false
}

/**
* Gets the first [DocumentType] in [documentTypes] with a given mdoc namespace.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,7 @@ class VerifierServlet : HttpServlet() {
presentation.verifyKeyBinding(
checkAudience = { clientId == it },
checkNonce = { nonceStr == it },
checkCreationTime = { it < Clock.System.now() }
checkCreationTime = { true /* TODO: sometimes flaky it < Clock.System.now() */ }
)

// also on the verifier, check the signature over the SD-JWT from the issuer
Expand Down Expand Up @@ -1060,7 +1060,7 @@ private fun mdocCalcDcRequestString(
RequestType.PID_MDOC_MANDATORY -> pid?.sampleRequests?.first { it.id == "mandatory" }
RequestType.PID_MDOC_FULL -> pid?.sampleRequests?.first { it.id == "full" }
RequestType.MDL_MDOC_AGE_OVER_18 -> mdl?.sampleRequests?.first { it.id == "age_over_18_and_portrait" }
RequestType.MDL_MDOC_AGE_OVER_21 -> mdl?.sampleRequests?.first { it.id == "age_over_18_and_portrait" }
RequestType.MDL_MDOC_AGE_OVER_21 -> mdl?.sampleRequests?.first { it.id == "age_over_21_and_portrait" }
RequestType.MDL_MDOC_MANDATORY -> mdl?.sampleRequests?.first { it.id == "mandatory" }
RequestType.MDL_MDOC_FULL -> mdl?.sampleRequests?.first { it.id == "full" }
else -> null
Expand Down Expand Up @@ -1108,7 +1108,7 @@ private fun mdocCalcPresentationDefinition(
RequestType.PID_MDOC_MANDATORY -> pid?.sampleRequests?.first { it.id == "mandatory" }
RequestType.PID_MDOC_FULL -> pid?.sampleRequests?.first { it.id == "full" }
RequestType.MDL_MDOC_AGE_OVER_18 -> mdl?.sampleRequests?.first { it.id == "age_over_18_and_portrait" }
RequestType.MDL_MDOC_AGE_OVER_21 -> mdl?.sampleRequests?.first { it.id == "age_over_18_and_portrait" }
RequestType.MDL_MDOC_AGE_OVER_21 -> mdl?.sampleRequests?.first { it.id == "age_over_21_and_portrait" }
RequestType.MDL_MDOC_MANDATORY -> mdl?.sampleRequests?.first { it.id == "mandatory" }
RequestType.MDL_MDOC_FULL -> mdl?.sampleRequests?.first { it.id == "full" }
else -> null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ fun Document.renderDocumentDetails(
context: Context,
documentTypeRepository: DocumentTypeRepository
): DocumentDetails {
// TODO: use DocumentConfiguration instead of pulling it out of a certified credential.
// TODO: maybe use DocumentConfiguration instead of pulling data out of a certified credential.

if (certifiedCredentials.size == 0) {
return DocumentDetails(null, null, mapOf())
Expand All @@ -117,7 +117,7 @@ fun Document.renderDocumentDetails(
return renderDocumentDetailsForMdoc(context, documentTypeRepository, credential)
}
is SdJwtVcCredential -> {
return renderDocumentDetailsForSdJwt(context, credential)
return renderDocumentDetailsForSdJwt(documentTypeRepository, credential)
}
else -> {
return DocumentDetails(
Expand Down Expand Up @@ -169,34 +169,30 @@ private fun Document.renderDocumentDetailsForMdoc(
}

private fun Document.renderDocumentDetailsForSdJwt(
context: Context,
documentTypeRepository: DocumentTypeRepository,
credential: SdJwtVcCredential
): DocumentDetails {
val kvPairs = mutableMapOf<String, String>()

val vcType = documentTypeRepository.getDocumentTypeForVc(credential.vct)?.vcDocumentType

val sdJwt = SdJwtVerifiableCredential.fromString(
String(credential.issuerProvidedData, Charsets.US_ASCII))

for (disclosure in sdJwt.disclosures) {
// TODO: replace this ad-hoc mapping with document type based mapping like what is done
// for mdoc
val content = if (disclosure.value is JsonPrimitive) {
disclosure.value.jsonPrimitive.content
} else {
disclosure.value.toString()
}
when (disclosure.key) {
"family_name" -> kvPairs["Family Name"] = content
"given_name" -> kvPairs["First Name"] = content
"birthdate" -> kvPairs["Date of Birth"] = content
"age_in_years" -> kvPairs["Age"] = content
"birth_family_name" -> kvPairs["Maiden Name"] = content
else -> if (disclosure.key.matches(Regex("^\\d+\$"))) {
kvPairs["Over ${disclosure.key}"] = content
} else {
kvPairs[disclosure.key] = content
}
}
val claimName = disclosure.key
val displayName = vcType
?.claims
?.get(claimName)
?.displayName
?: claimName

kvPairs[displayName] = content
}

return DocumentDetails(null, null, kvPairs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import com.android.identity.util.Constants
import com.android.identity.util.Logger
import com.android.identity_credential.wallet.presentation.UserCanceledPromptException
import com.android.identity_credential.wallet.presentation.showMdocPresentmentFlow
import com.android.identity_credential.wallet.ui.prompt.consent.ConsentField
import com.android.identity_credential.wallet.ui.prompt.consent.MdocConsentField
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.datetime.Clock
Expand Down Expand Up @@ -359,14 +361,20 @@ class PresentationActivity : FragmentActivity() {
}
}

val consentFields = MdocConsentField.generateConsentFields(
docRequest,
walletApp.documentTypeRepository,
mdocCredential
)

// show the Presentation Flow for and get the response bytes for
// the generated Document
val documentCborBytes = showMdocPresentmentFlow(
activity = this@PresentationActivity,
walletApp = walletApp,
documentRequest = MdocUtil.generateDocumentRequest(docRequest),
credential = mdocCredential,
consentFields = consentFields,
documentName = mdocCredential.document.documentConfiguration.displayName,
trustPoint = trustPoint,
credential = mdocCredential,
encodedSessionTranscript = deviceRetrievalHelper!!.sessionTranscript
)
deviceResponseGenerator.addDocument(documentCborBytes)
Expand Down
Loading

0 comments on commit d3a7b91

Please sign in to comment.