Skip to content

Commit

Permalink
Presentation while issuance.
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Sorotokin <[email protected]>
  • Loading branch information
sorotokin committed Nov 7, 2024
1 parent e6bf257 commit a248e6a
Show file tree
Hide file tree
Showing 22 changed files with 1,151 additions and 155 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.android.identity.issuance.evidence

class EvidenceRequestOpenid4Vp(
val originUri: String,
val request: String
): EvidenceRequest()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.android.identity.issuance.evidence

class EvidenceResponseOpenid4Vp(
val response: String
) : EvidenceResponse()
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,14 @@ class FunkeIssuingAuthorityState(
val proofingInfo = performPushedAuthorizationRequest(env)
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
val authorizationMetadata = metadata.authorizationServers[0]
var openid4VpRequest: String? = null
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 @@ -255,15 +263,15 @@ class FunkeIssuingAuthorityState(
WalletApplicationCapabilities.fromCbor(it.toByteArray())
} ?: throw IllegalStateException("WalletApplicationCapabilities not found")
return FunkeProofingState(
credentialIssuerUri = credentialIssuerUri,
clientId = clientId,
issuanceClientId = issuanceClientId,
documentId = documentId,
proofingInfo = proofingInfo,
applicationCapabilities = applicationCapabilities,
tokenUri = authorizationMetadata.tokenEndpoint,
useGermanEId = authorizationMetadata.useGermanEId,
// Don't show TOS when using browser API
tosAcknowleged = !authorizationMetadata.useGermanEId
tosAcknowleged = !authorizationMetadata.useGermanEId,
openid4VpRequest = openid4VpRequest
)
}

Expand Down Expand Up @@ -566,7 +574,7 @@ class FunkeIssuingAuthorityState(
}

private suspend fun performPushedAuthorizationRequest(
env: FlowEnvironment,
env: FlowEnvironment
): ProofingInfo {
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
val config = metadata.credentialConfigurations[credentialConfigurationId]!!
Expand Down Expand Up @@ -613,31 +621,48 @@ class FunkeIssuingAuthorityState(
add("client_id", issuanceClientId)
}
val httpClient = env.getInterface(HttpClient::class)!!
val response = httpClient.post(authorizationMetadata.pushedAuthorizationRequestEndpoint) {
// 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())
}
if (response.status != HttpStatusCode.Created) {
val responseText = String(response.readBytes())
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(String(response.readBytes())) as JsonObject
val requestUri = parsedResponse["request_uri"]
if (requestUri !is JsonPrimitive) {
Logger.e(TAG, "PAR response error")
throw IllegalStateException("PAR response syntax error")
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")
}
}
Logger.i(TAG, "Request uri: ${requestUri.content}")
val authSession = parsedResponse["auth_session"]
val requestUri = parsedResponse["request_uri"]
val presentation = parsedResponse["presentation"]
return ProofingInfo(
authorizeUrl = "${authorizationMetadata.authorizationEndpoint}?" + FormUrlEncoder {
add("client_id", issuanceClientId)
add("request_uri", requestUri.content)
},
requestUri = requestUri?.jsonPrimitive?.content,
authSession = authSession?.jsonPrimitive?.content,
pkceCodeVerifier = pkceCodeVerifier,
landingUrl = landingUrl
landingUrl = landingUrl,
openid4VpPresentation = presentation?.jsonPrimitive?.content
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,32 @@ import com.android.identity.issuance.evidence.EvidenceRequest
import com.android.identity.issuance.evidence.EvidenceRequestGermanEid
import com.android.identity.issuance.evidence.EvidenceRequestMessage
import com.android.identity.issuance.evidence.EvidenceRequestNotificationPermission
import com.android.identity.issuance.evidence.EvidenceRequestOpenid4Vp
import com.android.identity.issuance.evidence.EvidenceRequestQuestionMultipleChoice
import com.android.identity.issuance.evidence.EvidenceRequestSetupCloudSecureArea
import com.android.identity.issuance.evidence.EvidenceRequestWeb
import com.android.identity.issuance.evidence.EvidenceResponse
import com.android.identity.issuance.evidence.EvidenceResponseGermanEid
import com.android.identity.issuance.evidence.EvidenceResponseMessage
import com.android.identity.issuance.evidence.EvidenceResponseNotificationPermission
import com.android.identity.issuance.evidence.EvidenceResponseOpenid4Vp
import com.android.identity.issuance.evidence.EvidenceResponseQuestionMultipleChoice
import com.android.identity.issuance.evidence.EvidenceResponseSetupCloudSecureArea
import com.android.identity.issuance.evidence.EvidenceResponseWeb
import com.android.identity.securearea.PassphraseConstraints
import com.android.identity.util.Logger
import com.android.identity.util.fromBase64Url
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.readBytes
import io.ktor.http.HttpStatusCode
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.net.URI
import java.net.URLEncoder


Expand All @@ -37,25 +48,25 @@ import java.net.URLEncoder
)
@CborSerializable
class FunkeProofingState(
val credentialIssuerUri: String,
val clientId: String,
val issuanceClientId: String,
val documentId: String,
val proofingInfo: ProofingInfo,
val applicationCapabilities: WalletApplicationCapabilities,
val tokenUri:String,
val useGermanEId: Boolean,
var access: FunkeAccess? = null,
var secureAreaIdentifier: String? = null,
var secureAreaSetupDone: Boolean = false,
var tosAcknowleged: Boolean = false,
var notificationPermissonRequested: Boolean = false,
var openid4VpRequest: String? = null
) {
companion object {
private const val TAG = "FunkeProofingState"
}

@FlowMethod
fun getEvidenceRequests(env: FlowEnvironment): List<EvidenceRequest> {
suspend fun getEvidenceRequests(env: FlowEnvironment): List<EvidenceRequest> {
return if (access == null) {
if (!tosAcknowleged) {
val message = env.getInterface(Resources::class)!!
Expand Down Expand Up @@ -83,10 +94,25 @@ class FunkeProofingState(
continueWithoutPermissionButtonText = "No Thanks",
assets = mapOf()
))
} else if (useGermanEId) {
listOf(EvidenceRequestGermanEid(proofingInfo.authorizeUrl, listOf()))
} else {
listOf(EvidenceRequestWeb(proofingInfo.authorizeUrl, proofingInfo.landingUrl))
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
val authorizationMetadata = metadata.authorizationServers[0]
val authorizeUrl = "${authorizationMetadata.authorizationEndpoint}?" + FormUrlEncoder {
add("client_id", issuanceClientId)
add("request_uri", proofingInfo.requestUri!!)
}
if (authorizationMetadata.useGermanEId) {
listOf(EvidenceRequestGermanEid(authorizeUrl, listOf()))
} else if (openid4VpRequest != null) {
val uri = URI(authorizationMetadata.authorizationChallengeEndpoint!!)
val origin = uri.scheme + ":" + uri.authority
listOf(
EvidenceRequestOpenid4Vp(origin, openid4VpRequest!!),
EvidenceRequestWeb(authorizeUrl, proofingInfo.landingUrl)
)
} else {
listOf(EvidenceRequestWeb(authorizeUrl, proofingInfo.landingUrl))
}
}
} else if (secureAreaIdentifier == null) {
listOf(
Expand Down Expand Up @@ -130,7 +156,14 @@ class FunkeProofingState(
is EvidenceResponseGermanEid -> if (evidenceResponse.url != null) {
processRedirectUrl(env, evidenceResponse.url)
}
is EvidenceResponseWeb -> obtainTokenUsingCode(env, evidenceResponse.response, null)
is EvidenceResponseWeb -> {
val index = evidenceResponse.response.indexOf("code=")
if (index < 0) {
throw IllegalStateException("No code after web authorization")
}
val authCode = evidenceResponse.response.substring(index + 5)
obtainTokenUsingCode(env, authCode, null)
}
is EvidenceResponseMessage -> {
if (!evidenceResponse.acknowledged) {
throw IssuingAuthorityException("Issuance rejected")
Expand All @@ -154,6 +187,9 @@ class FunkeProofingState(
is EvidenceResponseNotificationPermission -> {
notificationPermissonRequested = true
}
is EvidenceResponseOpenid4Vp -> {
processOpenid4VpResponse(env, evidenceResponse.response)
}
else -> throw IllegalArgumentException("Unexpected evidence type")
}
}
Expand Down Expand Up @@ -182,28 +218,68 @@ class FunkeProofingState(
Logger.e(TAG, "No DPoP nonce in authentication response")
throw IllegalStateException("No DPoP nonce in authentication response")
}
obtainTokenUsingCode(env, response.headers["Location"]!!, dpopNonce)
val location = response.headers["Location"]!!
val code = location.substring(location.indexOf("code=") + 5)
obtainTokenUsingCode(env, code, dpopNonce)
}

private suspend fun obtainTokenUsingCode(
env: FlowEnvironment,
location: String,
authCode: String,
dpopNonce: String?
) {
val index = location.indexOf("code=")
if (index < 0) {
throw IllegalStateException("No code after web authorization")
}
val code = location.substring(index + 5)
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
this.access = FunkeUtil.obtainToken(
env = env,
tokenUrl = tokenUri,
tokenUrl = metadata.authorizationServers[0].tokenEndpoint,
clientId = clientId,
issuanceClientId = issuanceClientId,
authorizationCode = code,
authorizationCode = authCode,
codeVerifier = proofingInfo.pkceCodeVerifier,
dpopNonce = dpopNonce
)
Logger.i(TAG, "Token request: success")
}

private suspend fun processOpenid4VpResponse(env: FlowEnvironment, response: String) {
val body = String(openid4VpRequest!!.split('.')[1].fromBase64Url())
val url = Json.parseToJsonElement(body).jsonObject["response_uri"]!!.jsonPrimitive.content
val state = Json.parseToJsonElement(body).jsonObject["state"]!!.jsonPrimitive.content
val httpClient = env.getInterface(HttpClient::class)!!
val resp = httpClient.post(url) {
headers {
append("Content-Type", "application/x-www-form-urlencoded")
}
setBody(FormUrlEncoder {
add("response", response)
add("state", state)
}.toString())
}
val parsedResponse = Json.parseToJsonElement(String(resp.readBytes())).jsonObject
val presentationCode =
parsedResponse["presentation_during_issuance_session"]!!.jsonPrimitive.content
val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri)
val authorizationMetadata = metadata.authorizationServers[0]
val dpop = FunkeUtil.generateDPoP(env, clientId,
authorizationMetadata.authorizationChallengeEndpoint!!, null, null)
val challengeRequest = FormUrlEncoder {
add("auth_session", proofingInfo.authSession!!)
add("presentation_during_issuance_session", presentationCode)
}.toString()
val challengeResponse = httpClient.post(
authorizationMetadata.authorizationChallengeEndpoint) {
headers {
append("DPoP", dpop)
append("Content-Type", "application/x-www-form-urlencoded")
}
setBody(challengeRequest)
}
if (challengeResponse.status != HttpStatusCode.OK) {
throw IllegalStateException("failed to authorize")
}
val parsedChallengeResponse =
Json.parseToJsonElement(String(challengeResponse.readBytes())).jsonObject
val authCode = parsedChallengeResponse["authorization_code"]!!.jsonPrimitive.content
obtainTokenUsingCode(env, authCode, null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import com.android.identity.cbor.annotation.CborSerializable

@CborSerializable
data class ProofingInfo(
val authorizeUrl: String,
val requestUri: String?,
val pkceCodeVerifier: String,
val landingUrl: String
val landingUrl: String,
val authSession: String?,
val openid4VpPresentation: String?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,12 @@ internal data class Openid4VciIssuerMetadata(
SUPPORTED_SIGNATURE_ALGORITHMS
) ?: return null
val authorizationEndpoint = jsonObject["authorization_endpoint"]!!.jsonPrimitive.content
val authorizationChallengeEndpoint =
jsonObject["authorization_challenge_endpoint"]?.jsonPrimitive?.content
return Openid4VciAuthorizationMetadata(
pushedAuthorizationRequestEndpoint = jsonObject["pushed_authorization_request_endpoint"]!!.jsonPrimitive.content,
authorizationEndpoint = authorizationEndpoint,
authorizationChallengeEndpoint = authorizationChallengeEndpoint,
tokenEndpoint = jsonObject["token_endpoint"]!!.jsonPrimitive.content,
responseType = responseType,
codeChallengeMethod = codeChallengeMethod,
Expand Down Expand Up @@ -178,6 +181,7 @@ internal data class Openid4VciIssuerMetadata(
internal data class Openid4VciAuthorizationMetadata(
val pushedAuthorizationRequestEndpoint: String,
val authorizationEndpoint: String,
val authorizationChallengeEndpoint: String?,
val tokenEndpoint: String,
val responseType: String,
val codeChallengeMethod: String,
Expand Down
Loading

0 comments on commit a248e6a

Please sign in to comment.