Skip to content

Commit

Permalink
OpenId4VCI interoperability fixes.
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Sorotokin <[email protected]>
  • Loading branch information
sorotokin committed Dec 19, 2024
1 parent b42a116 commit 82e1bfb
Show file tree
Hide file tree
Showing 14 changed files with 429 additions and 269 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ package com.android.identity.issuance.evidence
* Request pre-authorized code from the client. Pre-authorized code is given to a wallet app
* as a part of an OpenId4VCI credential offer.
*/
class EvidenceRequestPreauthorizedCode : EvidenceRequest()
class EvidenceRequestCredentialOffer : EvidenceRequest()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.android.identity.issuance.evidence

/**
* Provides OpenId4VCI credential offer data.
*/
data class EvidenceResponseCredentialOffer(
val credentialOffer: Openid4VciCredentialOffer
) : EvidenceResponse()

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.android.identity.issuance.evidence

import com.android.identity.cbor.annotation.CborSerializable

/**
* Credential offer as described in OpenId4Vci spec:
* https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-offer-parameters
*/
@CborSerializable
sealed class Openid4VciCredentialOffer(
val issuerUri: String,
val configurationId: String,
val authorizationServer: String?
) {
companion object
}

/**
* Credential offer with Grant Type `urn:ietf:params:oauth:grant-type:pre-authorized_code`.
*/
class Openid4VciCredentialOfferPreauthorizedCode(
issuerUri: String,
configurationId: String,
authorizationServer: String?,
val preauthorizedCode: String,
val txCode: Openid4VciTxCode?
) : Openid4VciCredentialOffer(issuerUri, configurationId, authorizationServer)

/**
* Credential offer with Grant Type `authorization_code`.
*/
class Openid4VciCredentialOfferAuthorizationCode(
issuerUri: String,
configurationId: String,
authorizationServer: String?,
val issuerState: String?
) : Openid4VciCredentialOffer(issuerUri, configurationId, authorizationServer)


/**
* Describes tx_code parameter (see OpenId4Vci spec referenced above).
*/
@CborSerializable
data class Openid4VciTxCode(
val description: String,
val isNumeric: Boolean,
val length: Int
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ class FunkeAccess(
val accessTokenExpiration: Instant,
var dpopNonce: String?,
var cNonce: String?,
var tokenEndpoint: String,
var refreshToken: String?,
) {
companion object {
suspend fun parseResponse(tokenResponse: HttpResponse): FunkeAccess {
suspend fun parseResponse(tokenEndpoint: String, tokenResponse: HttpResponse): FunkeAccess {
val dpopNonce = tokenResponse.headers["DPoP-Nonce"]
val tokenString = String(tokenResponse.readBytes())
val token = Json.parseToJsonElement(tokenString) as JsonObject
Expand All @@ -31,7 +32,8 @@ class FunkeAccess(
val duration = getField(token, "expires_in").long
val accessTokenExpiration = Clock.System.now() + duration.seconds
val refreshToken = token["refresh_token"]?.jsonPrimitive?.content
return FunkeAccess(accessToken, accessTokenExpiration, dpopNonce, cNonce, refreshToken)
return FunkeAccess(accessToken, accessTokenExpiration, dpopNonce,
cNonce, tokenEndpoint, refreshToken)
}

private fun getField(jsonElement: JsonObject, name: String): JsonPrimitive {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import com.android.identity.documenttype.DocumentTypeRepository
import com.android.identity.documenttype.knowntypes.DrivingLicense
import com.android.identity.documenttype.knowntypes.EUCertificateOfResidence
import com.android.identity.documenttype.knowntypes.EUPersonalID
import com.android.identity.documenttype.knowntypes.GermanPersonalID
import com.android.identity.documenttype.knowntypes.PhotoID
import com.android.identity.documenttype.knowntypes.UtopiaNaturalization
import com.android.identity.flow.annotation.FlowJoin
Expand Down Expand Up @@ -187,7 +186,6 @@ class FunkeIssuingAuthorityState(

val documentTypeRepository = DocumentTypeRepository().apply {
addDocumentType(EUPersonalID.getDocumentType())
addDocumentType(GermanPersonalID.getDocumentType())
addDocumentType(DrivingLicense.getDocumentType())
addDocumentType(PhotoID.getDocumentType())
addDocumentType(EUCertificateOfResidence.getDocumentType())
Expand Down Expand Up @@ -257,15 +255,6 @@ class FunkeIssuingAuthorityState(
@FlowMethod
suspend fun proof(env: FlowEnvironment, documentId: String): FunkeProofingState {
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
var openid4VpRequest: String? = null
val proofingInfo = performPushedAuthorizationRequest(env)
if (proofingInfo?.authSession != null && proofingInfo.openid4VpPresentation != null) {
val httpClient = env.getInterface(HttpClient::class)!!
val presentationResponse = httpClient.get(proofingInfo.openid4VpPresentation) {}
if (presentationResponse.status == HttpStatusCode.OK) {
openid4VpRequest = String(presentationResponse.readBytes())
}
}
val storage = env.getInterface(Storage::class)!!
val applicationCapabilities = storage.get(
"WalletApplicationCapabilities",
Expand All @@ -274,19 +263,13 @@ class FunkeIssuingAuthorityState(
)?.let {
WalletApplicationCapabilities.fromCbor(it.toByteArray())
} ?: throw IllegalStateException("WalletApplicationCapabilities not found")
val useGermanId = metadata.authorizationServers.isNotEmpty()
&& metadata.authorizationServers[0].useGermanEId
return FunkeProofingState(
clientId = clientId,
credentialConfigurationId = credentialConfigurationId,
issuanceClientId = issuanceClientId,
documentId = documentId,
credentialIssuerUri = credentialIssuerUri,
proofingInfo = proofingInfo,
applicationCapabilities = applicationCapabilities,
tokenUri = metadata.tokenEndpoint,
openid4VpRequest = openid4VpRequest,
useGermanEId = useGermanId
applicationCapabilities = applicationCapabilities
)
}

Expand All @@ -309,8 +292,8 @@ class FunkeIssuingAuthorityState(
issuerDocument.state = DocumentCondition.READY
if (issuerDocument.documentConfiguration == null) {
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
if (metadata.authorizationServers.isNotEmpty() &&
metadata.authorizationServers[0].useGermanEId) {
if (metadata.authorizationServerList.isNotEmpty() &&
metadata.authorizationServerList[0].useGermanEId) {
val isCloudSecureArea =
issuerDocument.secureAreaIdentifier!!.startsWith("CloudSecureArea?")
issuerDocument.documentConfiguration =
Expand Down Expand Up @@ -673,108 +656,6 @@ class FunkeIssuingAuthorityState(
}
}

private suspend fun performPushedAuthorizationRequest(env: FlowEnvironment): ProofingInfo? {
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
if (metadata.authorizationServers.isEmpty()) {
return null
}
val config = metadata.credentialConfigurations[credentialConfigurationId]!!
val authorizationMetadata = metadata.authorizationServers[0]
val pkceCodeVerifier = Random.Default.nextBytes(32).toBase64Url()
val codeChallenge = Crypto.digest(Algorithm.SHA256, pkceCodeVerifier.toByteArray()).toBase64Url()

// NB: applicationSupport will only be non-null when running this code locally in the
// Android Wallet app.
val applicationSupport = env.getInterface(ApplicationSupport::class)
val parRedirectUrl: String
val landingUrl: String
if (authorizationMetadata.useGermanEId) {
landingUrl = ""
// Does not matter, but must be https
parRedirectUrl = "https://secure.redirect.com"
} else {
landingUrl = applicationSupport?.createLandingUrl() ?:
ApplicationSupportState(clientId).createLandingUrl(env)
parRedirectUrl = landingUrl
}

val clientKeyInfo = FunkeUtil.communicationKey(env, clientId)
val clientAssertion = if (applicationSupport != null) {
// Required when applicationSupport is exposed
val assertionMaker = env.getInterface(DeviceAssertionMaker::class)!!
applicationSupport.createJwtClientAssertion(
clientKeyInfo.attestation,
assertionMaker.makeDeviceAssertion(AssertionDPoPKey(
clientKeyInfo.publicKey,
credentialIssuerUri
))
)
} else {
ApplicationSupportState(clientId).createJwtClientAssertion(
env,
clientKeyInfo.publicKey,
credentialIssuerUri
)
}

val req = FormUrlEncoder {
add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation")
if (config.scope != null) {
add("scope", config.scope)
}
add("response_type", "code")
add("code_challenge_method", "S256")
add("redirect_uri", parRedirectUrl)
add("client_assertion", clientAssertion)
add("code_challenge", codeChallenge)
add("client_id", issuanceClientId)
}
val httpClient = env.getInterface(HttpClient::class)!!
// Use authorization challenge if available, as we want to try it first before falling
// back to web-based authorization.
val (endpoint, expectedResponseStatus) =
if (authorizationMetadata.authorizationChallengeEndpoint != null) {
Pair(
authorizationMetadata.authorizationChallengeEndpoint,
HttpStatusCode.BadRequest
)
} else {
Pair(
authorizationMetadata.pushedAuthorizationRequestEndpoint,
HttpStatusCode.Created
)
}
val response = httpClient.post(endpoint) {
headers {
append("Content-Type", "application/x-www-form-urlencoded")
}
setBody(req.toString())
}
val responseText = String(response.readBytes())
if (response.status != expectedResponseStatus) {
Logger.e(TAG, "PAR request error: ${response.status}: $responseText")
throw IssuingAuthorityException("Error establishing authenticated channel with issuer")
}
val parsedResponse = Json.parseToJsonElement(responseText) as JsonObject
if (response.status == HttpStatusCode.BadRequest) {
val errorCode = parsedResponse["error"]
if (errorCode !is JsonPrimitive || errorCode.content != "insufficient_authorization") {
Logger.e(TAG, "PAR request error: ${response.status}: $responseText")
throw IssuingAuthorityException("Error establishing authenticated channel with issuer")
}
}
val authSession = parsedResponse["auth_session"]
val requestUri = parsedResponse["request_uri"]
val presentation = parsedResponse["presentation"]
return ProofingInfo(
requestUri = requestUri?.jsonPrimitive?.content,
authSession = authSession?.jsonPrimitive?.content,
pkceCodeVerifier = pkceCodeVerifier,
landingUrl = landingUrl,
openid4VpPresentation = presentation?.jsonPrimitive?.content
)
}

private suspend fun generateFunkeDocumentConfiguration(
env: FlowEnvironment,
isCloudSecureArea: Boolean
Expand Down Expand Up @@ -904,7 +785,7 @@ class FunkeIssuingAuthorityState(
env = env,
clientId = clientId,
issuanceClientId = issuanceClientId,
tokenUrl = metadata.tokenEndpoint,
tokenUrl = access.tokenEndpoint,
refreshToken = refreshToken,
accessToken = access.accessToken
)
Expand Down
Loading

0 comments on commit 82e1bfb

Please sign in to comment.