Skip to content
Open
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
1 change: 1 addition & 0 deletions vc-verifier/kotlin/example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ android {
resources {
excludes += "META-INF/*"
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "META-INF/versions/9/OSGI-INF/MANIFEST.MF"
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions vc-verifier/kotlin/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ annotationJvm = "1.9.1"
cbor = "0.9"
identity = "20231002"
authleteSdJwt = "1.5"
coseLibrary = "2.0.0"

[libraries]
junitJupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit" }
Expand Down Expand Up @@ -62,6 +63,7 @@ mockWebServer = { group = "com.squareup.okhttp3", name = "mockwebserver", versio
annotation-jvm = { group = "androidx.annotation", name = "annotation-jvm", version.ref = "annotationJvm" }
cbor = { group = "co.nstant.in", name = "cbor", version.ref = "cbor" }
identity = { group = "com.android.identity", name = "identity-credential", version.ref = "identity" }
cose-lib = { group = "se.digg.cose", name = "cose-lib", version.ref = "coseLibrary" }

[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
Expand Down
4 changes: 4 additions & 0 deletions vc-verifier/kotlin/vcverifier/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ plugins {

configurations.all {
resolutionStrategy.force( "com.fasterxml.jackson.core:jackson-core:2.14.0")
exclude(group = "org.bouncycastle", module = "bcprov-jdk15on")
exclude(group = "org.bouncycastle", module = "bcpkix-jdk15on")
exclude(group = "org.bouncycastle", module = "bcutil-jdk15on")
}

jacoco {
Expand Down Expand Up @@ -65,6 +68,7 @@ dependencies {
implementation(libs.annotation.jvm)
implementation(libs.authelete.sd.jwt)
implementation(libs.threetenbp)
implementation(libs.cose.lib)

testImplementation(libs.mockk)
testImplementation(libs.junitJupiter)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.mosip.vercred.vcverifier.constants

enum class CredentialFormat(val value: String) {
CWT_VC("cwt_vc"),
LDP_VC("ldp_vc"),
VC_SD_JWT("vc+sd-jwt"),
DC_SD_JWT("dc+sd-jwt"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ object CredentialValidatorConstants {
const val ERROR_VALID_FROM_INVALID = "${VALIDATION_ERROR}validFrom is not valid."
const val ERROR_VALID_UNTIL_INVALID = "${VALIDATION_ERROR}validUntil is not valid."

const val ERROR_MESSAGE_EMPTY_VC_CWT="${VALIDATION_ERROR}Input VC CWT string is null or empty."

const val ERROR_MESSAGE_INVALID_HEX_VC_CWT="${VALIDATION_ERROR}Invalid hexadecimal format"


const val ERROR_CODE_VC_EXPIRED = "ERR_VC_EXPIRED"
const val ERROR_MESSAGE_VC_EXPIRED = "VC is expired"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.mosip.vercred.vcverifier.credentialverifier

import io.mosip.vercred.vcverifier.constants.CredentialFormat
import io.mosip.vercred.vcverifier.credentialverifier.types.CwtVerifiableCredential
import io.mosip.vercred.vcverifier.credentialverifier.types.LdpVerifiableCredential
import io.mosip.vercred.vcverifier.credentialverifier.types.SdJwtVerifiableCredential
import io.mosip.vercred.vcverifier.credentialverifier.types.msomdoc.MsoMdocVerifiableCredential
Expand All @@ -12,6 +13,7 @@ class CredentialVerifierFactory {
CredentialFormat.MSO_MDOC -> MsoMdocVerifiableCredential()
CredentialFormat.VC_SD_JWT -> SdJwtVerifiableCredential()
CredentialFormat.DC_SD_JWT -> SdJwtVerifiableCredential()
CredentialFormat.CWT_VC -> CwtVerifiableCredential()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.mosip.vercred.vcverifier.credentialverifier.types

import io.mosip.vercred.vcverifier.credentialverifier.VerifiableCredential
import io.mosip.vercred.vcverifier.credentialverifier.validator.CwtValidator
import io.mosip.vercred.vcverifier.credentialverifier.verifier.CwtVerifier
import io.mosip.vercred.vcverifier.data.CredentialStatusResult
import io.mosip.vercred.vcverifier.data.ValidationStatus

class CwtVerifiableCredential: VerifiableCredential {
override fun validate(credential: String): ValidationStatus {
return CwtValidator().validate(credential)
}

override fun verify(credential: String): Boolean {
return CwtVerifier().verify(credential);
}

override fun checkStatus(credential: String, statusPurposes: List<String>?): Map<String, CredentialStatusResult> {
return emptyMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package io.mosip.vercred.vcverifier.credentialverifier.validator

import com.upokecenter.cbor.CBOREncodeOptions
import com.upokecenter.cbor.CBORObject
import com.upokecenter.cbor.CBORType
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CODE_CURRENT_DATE_BEFORE_PROCESSING_DATE
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CODE_GENERIC
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CODE_INVALID
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CODE_MISSING
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CODE_VC_EXPIRED
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_CURRENT_DATE_BEFORE_PROCESSING_DATE
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_INVALID_FIELD
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_MESSAGE_EMPTY_VC_CWT
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_MESSAGE_VC_EXPIRED
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.EXCEPTION_DURING_VALIDATION
import io.mosip.vercred.vcverifier.constants.CredentialValidatorConstants.ERROR_MESSAGE_INVALID_HEX_VC_CWT
import io.mosip.vercred.vcverifier.data.ValidationStatus

import io.mosip.vercred.vcverifier.exception.ValidationException
import io.mosip.vercred.vcverifier.utils.Util.hexToBytes
import io.mosip.vercred.vcverifier.utils.Util.validateNumericDate

class CwtValidator {

fun validate(credential: String): ValidationStatus {
try {
if (credential.isEmpty()) {
throw ValidationException(
ERROR_MESSAGE_EMPTY_VC_CWT,
ERROR_CODE_INVALID + "EMPTY"
)
}

if (!isValidHex(credential)) {
throw ValidationException(
ERROR_MESSAGE_INVALID_HEX_VC_CWT,
ERROR_CODE_INVALID + "HEX"
)
}

val coseObj = decodeCose(credential)

validateCoseStructure(coseObj)

val protectedHeader = decodeProtectedHeader(coseObj)
validateProtectedHeader(protectedHeader)

val claims = decodeCwtClaims(coseObj)
validateCwtStructure(claims)
validateNumericDates(claims)
return ValidationStatus("", "")
} catch (e: ValidationException) {
return ValidationStatus(e.errorMessage, e.errorCode)
} catch (e: Exception) {
return ValidationStatus(
"${EXCEPTION_DURING_VALIDATION}${e.message}",
ERROR_CODE_GENERIC
)
}
}


private fun validateCoseStructure(coseObj: CBORObject) {

if (coseObj.type != CBORType.Array) {
throw ValidationException(
ERROR_INVALID_FIELD + "COSE_Sign1 must be a CBOR array",
ERROR_CODE_INVALID + "COSE_STRUCTURE"
)
}

if (coseObj.size() != 4) {
throw ValidationException(
ERROR_INVALID_FIELD + "COSE_Sign1 must have exactly 4 elements",
ERROR_CODE_INVALID + "COSE_STRUCTURE"
)
}

// Index 0: Protected header
if (coseObj[0].type != CBORType.ByteString) {
throw ValidationException(
ERROR_INVALID_FIELD + "Protected header must be a CBOR byte string (bstr)",
ERROR_CODE_INVALID + "PROTECTED_HEADER"
)
}

// Index 1: Unprotected header
if (coseObj[1].type != CBORType.Map) {
throw ValidationException(
ERROR_INVALID_FIELD + "Unprotected header must be a CBOR map",
ERROR_CODE_INVALID + "UNPROTECTED_HEADER"
)
}

// Index 2: Payload (The CWT Claims)
if (coseObj[2].type != CBORType.ByteString) {
throw ValidationException(
ERROR_INVALID_FIELD + "Payload must be a CBOR byte string (bstr)",
ERROR_CODE_INVALID + "PAYLOAD"
)
}

// Index 3: Signature
if (coseObj[3].type != CBORType.ByteString) {
throw ValidationException(
ERROR_INVALID_FIELD + "Signature must be a CBOR byte string (bstr)",
ERROR_CODE_INVALID + "SIGNATURE"
)
}
}


private fun validateProtectedHeader(protectedHeader: CBORObject) {

if (protectedHeader.type != CBORType.Map) {
throw ValidationException(
ERROR_INVALID_FIELD + "Protected header must decode to a CBOR map",
ERROR_CODE_INVALID + "PROTECTED_HEADER"
)
}

val ALG = CBORObject.FromObject(1)

if (!protectedHeader.ContainsKey(ALG)) {
throw ValidationException(
ERROR_INVALID_FIELD + "Missing alg in protected header",
ERROR_CODE_MISSING + "ALG"
)
}

if (!protectedHeader[ALG].isNumber) {
throw ValidationException(
ERROR_INVALID_FIELD + "alg must be an integer",
ERROR_CODE_INVALID + "ALG"
)
}
}


private fun validateCwtStructure(claims: CBORObject) {

if (claims.type != CBORType.Map) {
throw ValidationException(
ERROR_INVALID_FIELD + "CWT payload must be a CBOR map",
ERROR_CODE_INVALID + "CWT_STRUCTURE"
)
}
}


private fun validateNumericDates(claims: CBORObject) {

val EXP = CBORObject.FromObject(4)
val NBF = CBORObject.FromObject(5)
val IAT = CBORObject.FromObject(6)

val now = System.currentTimeMillis() / 1000

val exp = validateNumericDate(claims, EXP, "exp")
val nbf = validateNumericDate(claims, NBF, "nbf")
val iat = validateNumericDate(claims, IAT, "iat")

// Expired Check
if (exp != null && exp <= now) {
throw ValidationException(
ERROR_MESSAGE_VC_EXPIRED + " (exp=$exp, now=$now)",
ERROR_CODE_VC_EXPIRED
)
}

// Not Before Check
if (nbf != null && nbf > now) {
throw ValidationException(
ERROR_CURRENT_DATE_BEFORE_PROCESSING_DATE + " (nbf=$nbf, now=$now)",
ERROR_CODE_CURRENT_DATE_BEFORE_PROCESSING_DATE
)
}

// Issued At Check
if (iat != null && iat > now) {
throw ValidationException(
ERROR_INVALID_FIELD + "CWT issued in the future (iat=$iat, now=$now)",
ERROR_CODE_INVALID + "IAT"
)
}
}



private fun decodeCose(credential: String): CBORObject {
val bytes = hexToBytes(credential)
return CBORObject.DecodeFromBytes(bytes)
}

private fun decodeProtectedHeader(coseObj: CBORObject): CBORObject {
val bytes = coseObj[0].GetByteString()
return CBORObject.DecodeFromBytes(bytes)
}

private fun decodeCwtClaims(coseObj: CBORObject): CBORObject {
val payloadBytes = coseObj[2].GetByteString()
return CBORObject.DecodeFromBytes(payloadBytes, CBOREncodeOptions("allowduplicatekeys=false"))
}

private fun isValidHex(credential: String): Boolean {
return credential.length % 2 == 0 &&
credential.matches(Regex("^[0-9a-fA-F]+$"))
}

}
Loading