Skip to content

Commit

Permalink
Handle OpenId Credential Offer Urls for dynamic Issuing Authorities.
Browse files Browse the repository at this point in the history
WalletServer and WalletServerState can create an IssuingAuthority by Uri
that impact Funke IssuingAuthorityState, ProofingState, RequestCredenti-
als state. WalletServerProvider is used for obtaining IssuingAuthority's
by Uri. Current Pid-based approach Funke IssuingAuthority runs the same.

The function initiateCredentialOfferIssuance(.., issuer uri, config id)
enables any part of the app to get credentials from any arbitrary
Credential Issuer Uri as defined in the openid-credential-offer:// url.
The OID4VCI scheme lives in
WalletApplication.OID4VCI_CREDENTIAL_OFFER_URL_SCHEME. Added function
extractCredentialIssuerData() for extracting issuer uri and config id
from an OID4VCI Url.

Incoming Urls from deep links / qr code links are decoded using the
standard java.net library for parsing the public url and obtaining a
valid url query (avoiding any Url attack attempts). Added
getUrlQueryFromCustomSchemeUrl() that returns the query portion of
any custom-scheme encoded Url.

Extracted ScanQrDialog into a standalone Composable that can be re-used
anywhere and obtain the scanned text. Refactored ReaderScreen to use the
new ScanQrDialog. AddToWallet also uses this dialog when user taps on
the -Scan Credential Offer- button.

Tested by:
- Manual tests from a emulator
- Manual tests from a real device
- ./gradlew check

Signed-off-by: dritan-x <[email protected]>
  • Loading branch information
dritan-x committed Oct 8, 2024
1 parent 0a4eaec commit d957601
Show file tree
Hide file tree
Showing 19 changed files with 667 additions and 225 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,16 @@ interface WalletServer: FlowBase {
*/
@FlowMethod
suspend fun getIssuingAuthority(identifier: String): IssuingAuthority

/**
* Create the Issuing Authority from the payload of an OpenId Credential Offer (OID4VCI) that
* produces a credential issuer Uri and Credential config Id (such as pid-mso-mdoc or pid-sd-jwt).
*
* Like above, do not call this method directly, use WalletServerProvider that maintains a cache.
*/
@FlowMethod
suspend fun createIssuingAuthorityByUri(
credentialIssuerUri: String,
credentialConfigurationId: String
): IssuingAuthority
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ import kotlin.time.Duration.Companion.seconds
class FunkeIssuingAuthorityState(
val clientId: String,
val credentialFormat: CredentialFormat,
var issuanceClientId: String = "" // client id in OpenID4VCI protocol
var issuanceClientId: String = "", // client id in OpenID4VCI protocol
val credentialIssuerUri: String, // credential offer issuing authority path
) : AbstractIssuingAuthorityState() {
companion object {
private const val TAG = "FunkeIssuingAuthorityState"
Expand Down Expand Up @@ -204,7 +205,8 @@ class FunkeIssuingAuthorityState(
issuanceClientId = issuanceClientId,
documentId = documentId,
proofingInfo = proofingInfo,
applicationCapabilities = applicationCapabilities
applicationCapabilities = applicationCapabilities,
credentialIssuerUri = credentialIssuerUri
)
}

Expand Down Expand Up @@ -274,7 +276,13 @@ class FunkeIssuingAuthorityState(
)
)
}
return FunkeRequestCredentialsState(issuanceClientId, documentId, configuration, cNonce)
return FunkeRequestCredentialsState(
issuanceClientId,
documentId,
configuration,
cNonce,
credentialIssuerUri = credentialIssuerUri
)
}

@FlowJoin
Expand Down Expand Up @@ -315,7 +323,7 @@ class FunkeIssuingAuthorityState(
null -> throw IllegalStateException("Credential format was not specified")
}

val credentialUrl = "${FunkeUtil.BASE_URL}/credential"
val credentialUrl = "${credentialIssuerUri}/credential"
val access = document.access!!
val dpop = FunkeUtil.generateDPoP(
env,
Expand Down Expand Up @@ -475,8 +483,14 @@ class FunkeIssuingAuthorityState(
}

val clientKeyInfo = FunkeUtil.communicationKey(env, clientId)
val clientAssertion = applicationSupport?.createJwtClientAssertion(clientKeyInfo.attestation, FunkeUtil.BASE_URL) ?:
ApplicationSupportState(clientId).createJwtClientAssertion(env, clientKeyInfo.publicKey, FunkeUtil.BASE_URL )
val clientAssertion = applicationSupport?.createJwtClientAssertion(
clientKeyInfo.attestation,
credentialIssuerUri
) ?: ApplicationSupportState(clientId).createJwtClientAssertion(
env,
clientKeyInfo.publicKey,
credentialIssuerUri
)

issuanceClientId = extractIssuanceClientId(clientAssertion)

Expand All @@ -491,7 +505,7 @@ class FunkeIssuingAuthorityState(
add("client_id", issuanceClientId)
}
val httpClient = env.getInterface(HttpClient::class)!!
val response = httpClient.post("${FunkeUtil.BASE_URL}/par") {
val response = httpClient.post("${credentialIssuerUri}/par") {
headers {
append("Content-Type", "application/x-www-form-urlencoded")
}
Expand All @@ -510,7 +524,7 @@ class FunkeIssuingAuthorityState(
}
Logger.i(TAG, "Request uri: $requestUri")
return ProofingInfo(
authorizeUrl = "${FunkeUtil.BASE_URL}/authorize?" + FormUrlEncoder {
authorizeUrl = "${credentialIssuerUri}/authorize?" + FormUrlEncoder {
add("client_id", issuanceClientId)
add("request_uri", requestUri.content)
},
Expand Down Expand Up @@ -607,7 +621,7 @@ class FunkeIssuingAuthorityState(
env = env,
clientId = clientId,
issuanceClientId = issuanceClientId,
tokenUrl = "${FunkeUtil.BASE_URL}/token",
tokenUrl = "${credentialIssuerUri}/token",
refreshToken = refreshToken,
accessToken = access.accessToken
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ class FunkeProofingState(
var secureAreaIdentifier: String? = null,
var secureAreaSetupDone: Boolean = false,
var tosAcknowleged: Boolean = false,
var notificationPermissonRequested: Boolean = false
var notificationPermissonRequested: Boolean = false,
val credentialIssuerUri:String,
) {
companion object {
private const val TAG = "FunkeProofingState"
Expand Down Expand Up @@ -195,7 +196,7 @@ class FunkeProofingState(
val code = location.substring(index + 5)
this.access = FunkeUtil.obtainToken(
env = env,
tokenUrl = "${FunkeUtil.BASE_URL}/token",
tokenUrl = "${credentialIssuerUri}/token",
clientId = clientId,
issuanceClientId = issuanceClientId,
authorizationCode = code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class FunkeRequestCredentialsState(
val credentialConfiguration: CredentialConfiguration,
val nonce: String,
var format: CredentialFormat? = null,
var credentialRequests: List<FunkeCredentialRequest>? = null
var credentialRequests: List<FunkeCredentialRequest>? = null,
val credentialIssuerUri:String,
) {
companion object

Expand Down Expand Up @@ -55,7 +56,7 @@ class FunkeRequestCredentialsState(
)).toString().toByteArray().toBase64Url()
val body = JsonObject(mapOf(
"iss" to JsonPrimitive(issuanceClientId),
"aud" to JsonPrimitive(FunkeUtil.BASE_URL),
"aud" to JsonPrimitive(credentialIssuerUri),
"iat" to JsonPrimitive(Clock.System.now().epochSeconds),
"nonce" to JsonPrimitive(nonce)
)).toString().toByteArray().toBase64Url()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ import kotlinx.serialization.json.buildJsonObject
import kotlin.random.Random

internal object FunkeUtil {
// To use generic OpenID4VCI issuer, switch USE_AUSWEIS_SDK to false
//const val BASE_URL = "http://localhost:8080/server/openid4vci"

const val BASE_URL = "https://demo.pid-issuer.bundesdruckerei.de/c"

const val TAG = "FunkeUtil"

const val EU_PID_MDOC_DOCTYPE = "eu.europa.ec.eudi.pid.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import com.android.identity.flow.annotation.FlowState
import com.android.identity.flow.handler.FlowDispatcherLocal
import com.android.identity.flow.handler.FlowExceptionMap
import com.android.identity.flow.server.Configuration
import com.android.identity.flow.server.Resources
import com.android.identity.flow.server.FlowEnvironment
import com.android.identity.flow.server.Resources
import com.android.identity.issuance.ApplicationSupport
import com.android.identity.issuance.CredentialFormat
import com.android.identity.issuance.DocumentConfiguration
Expand All @@ -18,16 +18,16 @@ import com.android.identity.issuance.LandingUrlUnknownException
import com.android.identity.issuance.WalletServer
import com.android.identity.issuance.WalletServerSettings
import com.android.identity.issuance.common.AbstractIssuingAuthorityState
import com.android.identity.issuance.hardcoded.IssuingAuthorityState
import com.android.identity.issuance.hardcoded.ProofingState
import com.android.identity.issuance.hardcoded.RegistrationState
import com.android.identity.issuance.hardcoded.RequestCredentialsState
import com.android.identity.issuance.hardcoded.register
import com.android.identity.issuance.funke.FunkeIssuingAuthorityState
import com.android.identity.issuance.funke.FunkeProofingState
import com.android.identity.issuance.funke.FunkeRegistrationState
import com.android.identity.issuance.funke.FunkeRequestCredentialsState
import com.android.identity.issuance.funke.register
import com.android.identity.issuance.hardcoded.IssuingAuthorityState
import com.android.identity.issuance.hardcoded.ProofingState
import com.android.identity.issuance.hardcoded.RegistrationState
import com.android.identity.issuance.hardcoded.RequestCredentialsState
import com.android.identity.issuance.hardcoded.register
import com.android.identity.issuance.register
import kotlinx.io.bytestring.buildByteString
import kotlin.random.Random
Expand All @@ -43,6 +43,8 @@ class WalletServerState(
) {
companion object {
private const val TAG = "WalletServerState"
// Hard-coded Funke endpoint for PID issuer, could be /c or /c1
private const val FUNKE_BASE_URL = "https://demo.pid-issuer.bundesdruckerei.de/c"

private fun devConfig(env: FlowEnvironment): IssuingAuthorityConfiguration {
val resources = env.getInterface(Resources::class)!!
Expand Down Expand Up @@ -132,9 +134,47 @@ class WalletServerState(
fun getIssuingAuthority(env: FlowEnvironment, identifier: String): AbstractIssuingAuthorityState {
check(clientId.isNotEmpty())
return when (identifier) {
"funkeSdJwtVc" -> FunkeIssuingAuthorityState(clientId, CredentialFormat.SD_JWT_VC)
"funkeMdocMso" -> FunkeIssuingAuthorityState(clientId, CredentialFormat.MDOC_MSO)
"funkeSdJwtVc" -> FunkeIssuingAuthorityState(
clientId = clientId,
credentialFormat = CredentialFormat.SD_JWT_VC,
credentialIssuerUri = FUNKE_BASE_URL
)

"funkeMdocMso" -> FunkeIssuingAuthorityState(
clientId = clientId,
credentialFormat = CredentialFormat.MDOC_MSO,
credentialIssuerUri = FUNKE_BASE_URL
)

else -> IssuingAuthorityState(clientId, identifier)
}
}

/**
* Returns the Issuing Authority State [AbstractIssuingAuthorityState] created with a specific
* issuing authority Uri for mdoc or sd-jwt credential, such as from OID4VCI credential offer
* deep link / Qr code.
*/
@FlowMethod
fun createIssuingAuthorityByUri(
env: FlowEnvironment,
credentialIssuerUri: String,
credentialConfigurationId: String
): AbstractIssuingAuthorityState {
return when (credentialConfigurationId) {
"pid-sd-jwt" -> FunkeIssuingAuthorityState(
clientId = clientId,
credentialFormat = CredentialFormat.SD_JWT_VC,
credentialIssuerUri = credentialIssuerUri
)

"pid-mso-mdoc" -> FunkeIssuingAuthorityState(
clientId = clientId,
credentialFormat = CredentialFormat.MDOC_MSO,
credentialIssuerUri = credentialIssuerUri
)

else -> IssuingAuthorityState(clientId, credentialConfigurationId)
}
}
}
10 changes: 9 additions & 1 deletion wallet/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
tools:replace="android:name">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:exported="true"
android:label="@string/app_name"
android:windowSoftInputMode="adjustResize"
Expand All @@ -49,7 +50,14 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="openwallet" android:host="callback" />
<!-- Wallet server callback scheme "openwallet://" for handling callbacks from the
Wallet server via Url format "openwallet://callback" that is paired with host "*"
declared below -->
<data android:scheme="openwallet"/>
<!-- OpenId Credential Offer scheme (OID4VCI) -->
<data android:scheme="openid-credential-offer"/>
<!-- Accept all hosts for any of the defined schemes above -->
<data android:host="*"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ class WalletServerProvider(
}
}

/**
* Creates an Issuing Authority by the [credentialIssuerUri] and [credentialConfigurationId],
* caching instances. If unable to connect, suspend and wait until connecting is possible.
*/
suspend fun createIssuingAuthorityByUri(credentialIssuerUri:String, credentialConfigurationId: String): IssuingAuthority {
val instance = waitForWalletServer()
instanceLock.withLock {
var issuingAuthority = issuingAuthorityMap[credentialConfigurationId]
if (issuingAuthority == null) {
issuingAuthority = instance.createIssuingAuthorityByUri(credentialIssuerUri, credentialConfigurationId)
issuingAuthorityMap[credentialConfigurationId] = issuingAuthority
}
return issuingAuthority
}
}

/**
* Gets ApplicationSupport object. It always comes from the server (either full wallet server
* or minimal wallet server).
Expand Down
Loading

0 comments on commit d957601

Please sign in to comment.