Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented basic OpenID4VCI issuing server. #740

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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