Skip to content

Commit

Permalink
Implemented basic OpenID4VCI issuing server and beefed up existing Wa…
Browse files Browse the repository at this point in the history
…llet Server to use it. In

the process, also added "application support" interface to Wallet Server that performs
Client Attestation and helps creating redirect urls to use with OpenID-style web-based
authorization workflows. Also, a minor fix to flow-processor to allow nullable parameters
and return values in flow RPC methods.

Signed-off-by: Peter Sorotokin <[email protected]>
  • Loading branch information
sorotokin committed Sep 24, 2024
1 parent 80187f8 commit 93eb7b9
Show file tree
Hide file tree
Showing 55 changed files with 1,884 additions and 273 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ interface Storage {
/**
* Inserts a new record.
*
* If the [key] is empty, a new unique key is generated.
* If the [key] is empty, a new unique key is generated. New key is guaranteed to only use
* URL-safe characters.
*/
suspend fun insert(table: String, peerId: String, data: ByteString, key: String = ""): String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ interface HttpTransport {
}

companion object {
fun processStatus(status: Int, statusText: String) {
fun processStatus(url: String, status: Int, statusText: String) {
when (status) {
200 -> {} // noop
404 -> throw UnsupportedOperationException(statusText)
405 -> throw IllegalStateException(statusText)
else -> throw RemoteException("$status $statusText")
404 -> throw UnsupportedOperationException("$url [$statusText]")
405 -> throw IllegalStateException("$url [$statusText]")
else -> throw RemoteException("$url [$status $statusText]")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ interface SolverFactoryFlow: FlowBase {

@FlowMethod
suspend fun getCount(): Int

// Just to test nullable parameters and return values
@FlowMethod
suspend fun nullableIdentity(value: String?): String?
}

// The following definitions are server-side (although we can patch them to run on the client
Expand Down Expand Up @@ -193,6 +197,9 @@ data class SolverFactoryState(

@FlowMethod
fun getCount(env: FlowEnvironment): Int = totalCount

@FlowMethod
fun nullableIdentity(env: FlowEnvironment, value: String?): String? = value
}

// FlowHandlerLocal handles flow calls that executed locally. It is for use on the server,
Expand Down Expand Up @@ -251,6 +258,16 @@ class FlowProcessorTest {
testFlow("Mock", remoteFactory())
}

@Test
fun nullable() {
runBlocking {
val factory = remoteFactory()
Assert.assertEquals("Foo", factory.nullableIdentity("Foo"))
Assert.assertNull(factory.nullableIdentity(null))
}
}


@Test
fun remoteUnexpectedNameException() {
runBlocking {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.android.identity.issuance

import com.android.identity.flow.annotation.FlowInterface
import com.android.identity.flow.annotation.FlowMethod
import com.android.identity.flow.client.FlowNotifiable
import com.android.identity.securearea.KeyAttestation

/**
* Server-side support functionality for the wallet mobile app. This is needed even if the
* full-blown wallet server is not used.
*/
@FlowInterface
interface ApplicationSupport : FlowNotifiable<LandingUrlNotification> {
/**
* Creates a "landing" URL suitable for web redirects. When a landing URL is navigated to,
* [LandingUrlNotification] is sent to the client.
*
* NB: this method returns the relative URL, server base URL should be prepended to it before
* use.
*/
@FlowMethod
suspend fun createLandingUrl(): String

/**
* Returns the query portion of the URL which was actually used when navigating to a landing
* URL, or null if navigation did not occur yet.
*
* [relativeUrl] relative URL of the landing page as returned by [createLandingUrl].
*/
@FlowMethod
suspend fun getLandingUrlStatus(relativeUrl: String): String?

/**
* Creates OAuth JWT client assertion based on the mobile-platform-specific [KeyAttestation].
*/
@FlowMethod
suspend fun createJwtClientAssertion(
clientAttestation: KeyAttestation,
targetIssuanceUrl: String
): String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.android.identity.issuance

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

@CborSerializable
class LandingUrlNotification(
val baseUrl: String
) {
companion object
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.android.identity.issuance

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

@FlowException
@CborSerializable
class LandingUrlUnknownException(message: String?) : Exception(message) {
companion object
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ interface WalletServer: FlowBase {
@FlowMethod
suspend fun authenticate(): AuthenticationFlow

/**
* General-purpose server-side application support.
*
* This is the only interface supported by the minimal wallet server.
*/
@FlowMethod
suspend fun applicationSupport(): ApplicationSupport

/**
* Static information about the available Issuing Authorities.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,11 @@ import kotlinx.datetime.Instant
/**
* Information about the capabilities of the wallet server, for the wallet application.
*
* If [waitForNotificationSupported] is `false` it means that the wallet application
* should use push notifications instead of long polling on [WalletServer.waitForNotification].
* This is normally only set to `true` for development instances of the server because
* of the fact that things like [WalletServer.waitForNotification] doesn't work efficiently
* at scale or when the application is not running.
*
* @property generatedAt The point in time this data was generated.
* @property waitForNotificationSupported Whether the [WalletServer.waitForNotification] method is
* supported by the server.
*/
@CborSerializable
data class WalletServerCapabilities(
val generatedAt: Instant,
val waitForNotificationSupported: Boolean,
val generatedAt: Instant
) {
companion object
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fun extractAttestationSequence(chain: X509CertChain): ASN1Sequence {

fun validateKeyAttestation(
chain: X509CertChain,
clientId: String,
clientId: String?,
requireGmsAttestation: Boolean,
requireVerifiedBootGreen: Boolean,
requireAppSignatureCertificateDigests: List<String>,
Expand Down Expand Up @@ -69,7 +69,7 @@ fun validateKeyAttestation(
val parser = AndroidAttestationExtensionParser(x509certs[0])

// Challenge must match...
check(clientId.toByteArray() contentEquals parser.attestationChallenge)
check(clientId == null || clientId.toByteArray() contentEquals parser.attestationChallenge)
{ "Challenge didn't match what was expected" }

if (requireVerifiedBootGreen) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.android.identity.issuance.evidence

/**
* Launch a browser using the given URL.
*/
data class EvidenceRequestWeb(
val url: String,
val redirectUri: String,
) : EvidenceRequest()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.android.identity.issuance.evidence

data class EvidenceResponseWeb(
/** Portion of the URL used for redirecting (not including EvidenceRequestWeb.redirectUri) */
val response: String
) : EvidenceResponse()
Loading

0 comments on commit 93eb7b9

Please sign in to comment.