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

Niganesh/validate open id4 vci network responses #73

Merged
merged 64 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
3b58c5b
increment the library version to be published
May 24, 2023
c22c6f5
reverted version increment change
May 25, 2023
c9c2c86
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
May 30, 2023
ae8e4da
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jun 15, 2023
6d28d10
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jun 15, 2023
a41ccdf
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jun 15, 2023
95e67e9
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jun 16, 2023
ced50d2
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jun 16, 2023
14b4056
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jun 30, 2023
94e1e53
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jul 25, 2023
564ca6f
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Jul 27, 2023
49408ae
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Aug 8, 2023
e0c73cc
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Aug 10, 2023
9dd97df
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Oct 4, 2023
61cf500
Adding interface and current base client
logangirvin Dec 1, 2023
c924998
Adding implementations for APIs
logangirvin Dec 5, 2023
dcad9a2
Moving correlation vector to remain internal.
logangirvin Dec 8, 2023
ce6d228
Adding support for network operations to use HttpAgent
logangirvin Dec 14, 2023
d5cf576
Adding support in all of the service endpoints
logangirvin Dec 14, 2023
b58b798
Lifting the default headers out of the http agent
logangirvin Jan 17, 2024
b5d3f52
Forgot to actually call the constructor from the builder
logangirvin Jan 17, 2024
f550009
Adding default content type text/plain
logangirvin Jan 17, 2024
d7fc295
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Feb 6, 2024
cce64fc
addressing PR comments, fixing unit tests
logangirvin Feb 9, 2024
a9e6ec7
Adding basic unit tests
logangirvin Feb 12, 2024
8f06d9e
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Mar 4, 2024
439a2d2
merged latest dev branch
Mar 4, 2024
5efb226
classes to fetch credential offer using resolver
Mar 6, 2024
52b3242
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Mar 6, 2024
e375239
merge latest dev branch and fix conflicts
Mar 7, 2024
4029eb4
added unit tests
Mar 7, 2024
abed40f
networking classes to fetch credential metadata
Mar 8, 2024
a0eb184
created a new api class for OpenID4VCI related network calls
Mar 8, 2024
043195f
changes logic to check if response is valid json
Mar 8, 2024
abae27b
merged latest fetch credential offer changes
Mar 8, 2024
267e183
Added exceptions for failure cases and changes fields to match latest…
Mar 8, 2024
f7557eb
perform validations on credential metadata and credential offer
Mar 11, 2024
fc16965
remove empty line
Mar 12, 2024
af2e9a1
added comments
Mar 13, 2024
9f0233c
added unit tests for more failure cases
Mar 13, 2024
2b37644
Merge branch 'niganesh/fetch-credential-offer' into niganesh/fetch-cr…
Mar 13, 2024
4bb6bb3
merge credential metadata changes and fix merge conflicts
Mar 13, 2024
539f766
Added class comments for wrapper classes
Mar 13, 2024
ff9281d
remove repetitive lines of code
Mar 14, 2024
a4c91c8
Merge branch 'niganesh/fetch-credential-offer' into niganesh/fetch-cr…
Mar 14, 2024
f7267ab
Added unit tests for fetch credential metadata operation
Mar 14, 2024
87076ca
reverted formatting changes
Mar 14, 2024
e850897
Merge branch 'niganesh/fetch-credential-metadata' into niganesh/valid…
Mar 14, 2024
373a515
Adding comments where required.
Mar 14, 2024
fb9d39d
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Mar 14, 2024
5f8bbc2
Merged dev branch and fixed merge conflicts
Mar 14, 2024
13c8321
Added unit tests for request handler method
Mar 14, 2024
1ed52cf
merged latest changes from metadata branch
Mar 14, 2024
0d382a0
minor refactoring
Mar 14, 2024
df0f683
merged latest changes from metadata branch
Mar 14, 2024
1d05f7f
Merge branch 'dev' of https://github.com/microsoft/entra-verifiedid-w…
Mar 14, 2024
6117bdf
Merged dev branch and fixed merge conflicts
Mar 14, 2024
e5881f1
Added unit tests for new classes
Mar 14, 2024
35fab43
Added unit tests for signed metadata processor
Mar 15, 2024
e8ea56a
Refactored unit tests.
Mar 15, 2024
817f908
Added unit tests for new classes
Mar 16, 2024
6d8d632
Reverted the changes to demo app
Mar 16, 2024
bcd3b49
add unit tests to handler
Mar 18, 2024
c170035
minor refactorings
Mar 18, 2024
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 @@ -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.util.Constants
import com.microsoft.walletlibrary.did.sdk.util.controlflow.SdkException
Expand All @@ -34,16 +35,22 @@ internal class LinkedDomainsService @Inject constructor(
return Result.failure(SdkException("Failed while verifying linked domains"))
}

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 @@ -57,16 +64,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 @@ -88,6 +91,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"))
nithyaganeshng marked this conversation as resolved.
Show resolved Hide resolved
}
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 ?: "")
nithyaganeshng marked this conversation as resolved.
Show resolved Hide resolved
}

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)
nithyaganeshng marked this conversation as resolved.
Show resolved Hide resolved

// 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