Skip to content

Commit

Permalink
Niganesh/validate open id4 vci network responses (#73)
Browse files Browse the repository at this point in the history
Co-authored-by: Nithya Ganesh <[email protected]>
Co-authored-by: Logan <[email protected]>
  • Loading branch information
3 people committed Apr 2, 2024
1 parent d2a61ff commit 28ab3a8
Show file tree
Hide file tree
Showing 21 changed files with 1,145 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.microsoft.walletlibrary.did.sdk.credential.service.models.linkedDomai
import com.microsoft.walletlibrary.did.sdk.credential.service.validators.DomainLinkageCredentialValidator
import com.microsoft.walletlibrary.did.sdk.datasource.network.apis.HttpAgentApiProvider
import com.microsoft.walletlibrary.did.sdk.datasource.network.linkedDomainsOperations.FetchWellKnownConfigDocumentNetworkOperation
import com.microsoft.walletlibrary.did.sdk.identifier.models.identifierdocument.IdentifierDocument
import com.microsoft.walletlibrary.did.sdk.identifier.resolvers.Resolver
import com.microsoft.walletlibrary.did.sdk.identifier.resolvers.RootOfTrustResolver
import com.microsoft.walletlibrary.did.sdk.util.Constants
Expand Down Expand Up @@ -58,16 +59,22 @@ internal class LinkedDomainsService @Inject constructor(
}
}

private suspend fun verifyLinkedDomains(domainUrls: List<String>, relyingPartyDid: String): Result<LinkedDomainResult> {
internal suspend fun verifyLinkedDomains(
domainUrls: List<String>,
relyingPartyDid: String
): Result<LinkedDomainResult> {
if (domainUrls.isEmpty())
return Result.success(LinkedDomainMissing)
val domainUrl = domainUrls.first()
val hostname = URL(domainUrl).host
return getWellKnownConfigDocument(domainUrl)
.map {
wellKnownConfigDocument ->
.map { wellKnownConfigDocument ->
wellKnownConfigDocument.linkedDids.firstNotNullOf { linkedDidJwt ->
val isDomainLinked = jwtDomainLinkageCredentialValidator.validate(linkedDidJwt, relyingPartyDid, domainUrl)
val isDomainLinked = jwtDomainLinkageCredentialValidator.validate(
linkedDidJwt,
relyingPartyDid,
domainUrl
)
if (isDomainLinked)
LinkedDomainVerified(hostname)
else
Expand All @@ -79,16 +86,30 @@ internal class LinkedDomainsService @Inject constructor(
}

private suspend fun getLinkedDomainsFromDid(relyingPartyDid: String): Result<List<String>> {
val didDocumentResult = resolver.resolve(relyingPartyDid)
val didDocumentResult = resolveIdentifierDocument(relyingPartyDid)
return didDocumentResult.map { didDocument ->
val linkedDomainsServices =
didDocument.service.filter { service -> service.type.equals(Constants.LINKED_DOMAINS_SERVICE_ENDPOINT_TYPE, true) }
linkedDomainsServices.map { it.serviceEndpoint }.flatten()
getLinkedDomainsFromDidDocument(didDocument)
}
}

private suspend fun getWellKnownConfigDocument(domainUrl: String) = FetchWellKnownConfigDocumentNetworkOperation(
domainUrl,
apiProvider
).fire()
internal suspend fun resolveIdentifierDocument(relyingPartyDid: String): Result<IdentifierDocument> {
return resolver.resolve(relyingPartyDid)
}

internal fun getLinkedDomainsFromDidDocument(didDocument: IdentifierDocument): List<String> {
val linkedDomainsServices =
didDocument.service.filter { service ->
service.type.equals(
Constants.LINKED_DOMAINS_SERVICE_ENDPOINT_TYPE,
true
)
}
return linkedDomainsServices.map { it.serviceEndpoint }.flatten()
}

private suspend fun getWellKnownConfigDocument(domainUrl: String) =
FetchWellKnownConfigDocumentNetworkOperation(
domainUrl,
apiProvider
).fire()
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ internal object VerifiableCredentialSdk {
@JvmStatic
internal lateinit var presentationService: PresentationService

@JvmStatic
internal lateinit var linkedDomainsService: LinkedDomainsService

@JvmStatic
internal lateinit var revocationService: RevocationService

Expand Down Expand Up @@ -90,6 +93,7 @@ internal object VerifiableCredentialSdk {

issuanceService = sdkComponent.issuanceService()
presentationService = sdkComponent.presentationService()
linkedDomainsService = sdkComponent.linkedDomainsService()
revocationService = sdkComponent.revocationService()
correlationVectorService = sdkComponent.correlationVectorService()
identifierService = sdkComponent.identifierManager()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.microsoft.walletlibrary.mappings

import com.microsoft.walletlibrary.did.sdk.identifier.models.identifierdocument.IdentifierDocument
import com.nimbusds.jose.jwk.JWK

internal fun IdentifierDocument.getJwk(id: String): JWK? {
if (verificationMethod.isNullOrEmpty()) return null
for (publicKey in verificationMethod) {
if (publicKey.id == id) {
return publicKey.publicKeyJwk
}
}
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.microsoft.walletlibrary.mappings

import com.microsoft.walletlibrary.did.sdk.LinkedDomainsService
import com.microsoft.walletlibrary.did.sdk.credential.service.models.linkedDomains.LinkedDomainResult
import com.microsoft.walletlibrary.did.sdk.identifier.models.identifierdocument.IdentifierDocument
import com.microsoft.walletlibrary.did.sdk.util.controlflow.SdkException

internal suspend fun LinkedDomainsService.fetchAndVerifyLinkedDomains(identifierDocument: IdentifierDocument): Result<LinkedDomainResult> {
val linkedDomains = getLinkedDomainsFromDidDocument(identifierDocument)
verifyLinkedDomains(linkedDomains, identifierDocument.id)
.onSuccess { return Result.success(it) }
.onFailure { return Result.failure(it) }
return Result.failure(SdkException("Failed while verifying linked domains"))
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package com.microsoft.walletlibrary.networking.entities.openid4vci.credentialmetadata

import android.content.res.Resources
import androidx.core.os.ConfigurationCompat
import com.microsoft.walletlibrary.networking.entities.openid4vci.credentialoffer.CredentialOffer
import com.microsoft.walletlibrary.requests.styles.RequesterStyle
import com.microsoft.walletlibrary.requests.styles.VerifiedIdManifestIssuerStyle
import com.microsoft.walletlibrary.util.OpenId4VciValidationException
import com.microsoft.walletlibrary.util.VerifiedIdExceptions
import kotlinx.serialization.Serializable

/**
Expand Down Expand Up @@ -27,4 +34,63 @@ internal data class CredentialMetadata(

// Display information for the issuer.
val display: List<LocalizedIssuerDisplayDefinition>? = null,
)
) {

fun transformLocalizedIssuerDisplayDefinitionToRequesterStyle(): RequesterStyle {
val preferredLocalesList =
ConfigurationCompat.getLocales(Resources.getSystem().configuration)
for (index in 0 until preferredLocalesList.size()) {
val preferredLocale = preferredLocalesList[index]
display?.forEach { displayDefinition ->
displayDefinition.locale?.let { language ->
if (language == preferredLocale?.language) {
return VerifiedIdManifestIssuerStyle(displayDefinition.name ?: "")
}
}
}
}
return VerifiedIdManifestIssuerStyle(display?.first()?.name ?: "")
}

fun validateAuthorizationServers(credentialOffer: CredentialOffer) {
if (authorization_servers.isNullOrEmpty()) {
throw OpenId4VciValidationException(
"Authorization servers property missing in credential metadata.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
credentialOffer.grants.forEach {
if (!authorization_servers.contains(it.value.authorization_server))
throw OpenId4VciValidationException(
"Authorization server ${it.value.authorization_server} not found in Credential Metadata.",
VerifiedIdExceptions.MALFORMED_CREDENTIAL_METADATA_EXCEPTION.value
)
}
}

fun getSupportedCredentialConfigurations(credentialConfigurationIds: List<String>): List<CredentialConfiguration> {
val supportedConfigIds = mutableListOf<CredentialConfiguration>()
if (credential_configurations_supported == null)
return supportedConfigIds
credentialConfigurationIds.forEach { id ->
credential_configurations_supported[id]?.let { supportedConfigIds.add(it) }
}
return supportedConfigIds
}

fun verifyIfCredentialIssuerExist() {
if (credential_issuer == null)
throw OpenId4VciValidationException(
"Credential metadata does not contain credential_issuer.",
VerifiedIdExceptions.MALFORMED_CREDENTIAL_METADATA_EXCEPTION.value
)
}

fun verifyIfSignedMetadataExist() {
if (signed_metadata == null)
throw OpenId4VciValidationException(
"Credential metadata does not contain signed_metadata.",
VerifiedIdExceptions.MALFORMED_CREDENTIAL_METADATA_EXCEPTION.value
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.microsoft.walletlibrary.networking.entities.openid4vci.credentialmetadata

import com.microsoft.walletlibrary.util.TokenValidationException
import com.microsoft.walletlibrary.util.VerifiedIdExceptions
import kotlinx.serialization.Serializable

@Serializable
data class SignedMetadataTokenClaims(
val sub: String?,
val iat: String?,
val exp: String? = null,
val iss: String?,
val nbf: String? = null
) {
fun validateSignedMetadataTokenClaims(expectedSubject: String, expectedIssuer: String) {
validateSubject(expectedSubject)
validateIssuer(expectedIssuer)
validateIssuedAtTime()
validateExpiryTime()
}

private fun validateIssuer(expectedIssuer: String) {
if (iss == null) {
throw TokenValidationException(
"Issuer property missing in signed metadata.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
if (iss != expectedIssuer) {
throw TokenValidationException(
"Invalid issuer property in signed metadata.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
}

private fun validateSubject(expectedSubject: String) {
if (sub == null) {
throw TokenValidationException(
"Subject property missing in signed metadata.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
if (sub != expectedSubject) {
throw TokenValidationException(
"Invalid subject property in signed metadata.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
}

private fun validateIssuedAtTime() {
if (iat != null && iat.toLong() >= getCurrentTimeInSecondsWithSkew()) {
throw TokenValidationException(
"Issued at time is in the future.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
}

private fun validateExpiryTime() {
if (exp != null && exp.toLong() <= getCurrentTimeInSecondsWithSkew()) {
throw TokenValidationException(
"Token has expired.",
VerifiedIdExceptions.INVALID_PROPERTY_EXCEPTION.value
)
}
}

private fun getCurrentTimeInSecondsWithSkew(skew: Long = 300): Long {
return (System.currentTimeMillis() / 1000) + skew
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import com.microsoft.walletlibrary.networking.entities.openid4vci.credentialoffe
import com.microsoft.walletlibrary.networking.operations.FetchCredentialMetadataNetworkOperation
import com.microsoft.walletlibrary.requests.VerifiedIdRequest
import com.microsoft.walletlibrary.util.LibraryConfiguration
import com.microsoft.walletlibrary.util.OpenId4VciException
import com.microsoft.walletlibrary.util.OpenId4VciRequestException
import com.microsoft.walletlibrary.util.OpenId4VciValidationException
import com.microsoft.walletlibrary.util.VerifiedIdExceptions

internal class OpenId4VCIRequestHandler(private val libraryConfiguration: LibraryConfiguration) : RequestHandler {

private val signedMetadataProcessor = SignedMetadataProcessor(libraryConfiguration)

// Indicates whether the provided raw request can be handled by this handler.
// This method checks if the raw request can be cast to CredentialOffer successfully, and if it contains the required fields.
override fun canHandle(rawRequest: Any): Boolean {
Expand All @@ -27,24 +31,47 @@ internal class OpenId4VCIRequestHandler(private val libraryConfiguration: Librar
override suspend fun handleRequest(rawRequest: Any): VerifiedIdRequest<*> {
val credentialOffer: CredentialOffer
try {
// Deserialize the raw request to a CredentialOffer object.
credentialOffer = libraryConfiguration.serializer.decodeFromString(
CredentialOffer.serializer(),
rawRequest as String
)
} catch (exception: Exception) {
throw OpenId4VciException(
throw OpenId4VciValidationException(
"Failed to decode CredentialOffer ${exception.message}",
VerifiedIdExceptions.MALFORMED_CREDENTIAL_OFFER_EXCEPTION.value,
exception
)
}

// Fetch the credential metadata from the credential issuer in credential offer object.
fetchCredentialMetadata(credentialOffer.credential_issuer)
.onSuccess { }
.onFailure { }
TODO(
"Validate credential metadata, gather more inform, map data models and " +
"finally return VerifiedIdRequest."
)
.onSuccess { credentialMetadata ->
// Validate Credential Metadata to verify if credential issuer and Signed Metadata exist.
credentialMetadata.verifyIfCredentialIssuerExist()
credentialMetadata.verifyIfSignedMetadataExist()

// Get only the supported credential configuration ids from the credential metadata from the list in credential offer.
val configIds = credentialOffer.credential_configuration_ids
val supportedCredentialConfigurationIds =
credentialMetadata.getSupportedCredentialConfigurations(configIds)

// Validate the authorization servers in the credential metadata.
credentialMetadata.validateAuthorizationServers(credentialOffer)

// Get the root of trust from the signed metadata.
val rootOfTrust = credentialMetadata.signed_metadata?.let {
signedMetadataProcessor.process(it, credentialOffer.credential_issuer)
}
}
.onFailure {
throw OpenId4VciRequestException(
"Failed to fetch credential metadata ${it.message}",
VerifiedIdExceptions.CREDENTIAL_METADATA_FETCH_EXCEPTION.value,
it as Exception
)
}
TODO("Map data models and finally return VerifiedIdRequest.")
}

private suspend fun fetchCredentialMetadata(metadataUrl: String): Result<CredentialMetadata> {
Expand All @@ -56,6 +83,7 @@ internal class OpenId4VCIRequestHandler(private val libraryConfiguration: Librar
).fire()
}

// Build the credential metadata url from the provided credential issuer.
private fun buildCredentialMetadataUrl(credentialIssuer: String): String {
val suffix = "/.well-known/openid-credential-issuer"
if (!credentialIssuer.endsWith(suffix))
Expand Down
Loading

0 comments on commit 28ab3a8

Please sign in to comment.