[INJIMOB-3647] refactor: enhance response structure of credential status check (#2147)

* [INJIMOB-3647] refactor: enhance response structure of credential status check (#2145)

* [INJIMOB-3647] refactor: udpate vc verifier RN module

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: update reverification logic

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: modify status response structure

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: update response resolving of status check in swift bridge

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: update info logs to print status message

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: modify type from map to record

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: remove unnecessary async function

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: Credential summary result structure

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

---------

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

* [INJIMOB-3647] refactor: change statuslistVC type to record from string

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>

---------

Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>
This commit is contained in:
KiruthikaJeyashankar
2025-11-28 14:48:43 +05:30
committed by GitHub
parent ff9ee7184f
commit dab6c1b9a2
8 changed files with 146 additions and 129 deletions

View File

@@ -1,26 +1,25 @@
package io.mosip.residentapp;
import java.util.*;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import java.util.List;
import io.mosip.residentapp.Utils.FormatConverter;
import io.mosip.vercred.vcverifier.CredentialsVerifier;
import io.mosip.vercred.vcverifier.constants.CredentialFormat;
import io.mosip.vercred.vcverifier.data.VerificationResult;
import io.mosip.vercred.vcverifier.data.CredentialVerificationSummary;
import io.mosip.vercred.vcverifier.data.CredentialStatusResult;
import io.mosip.vercred.vcverifier.data.VerificationResult;
import io.mosip.vercred.vcverifier.exception.StatusCheckException;
public class RNVCVerifierModule extends ReactContextBaseJavaModule {
private CredentialsVerifier credentialsVerifier;
private final CredentialsVerifier credentialsVerifier;
public RNVCVerifierModule(ReactApplicationContext reactContext) {
super(reactContext);
credentialsVerifier = new CredentialsVerifier();
@@ -61,32 +60,27 @@ public void getVerificationSummary(String vc, String format, ReadableArray statu
resultMap.putString("verificationMessage", verificationResult.getVerificationMessage());
resultMap.putString("verificationErrorCode", verificationResult.getVerificationErrorCode());
WritableArray statusArray = Arguments.createArray();
for (CredentialStatusResult statusResult : summary.getCredentialStatus()) {
WritableMap statusMap = Arguments.createMap();
statusMap.putString("purpose", statusResult.getPurpose());
statusMap.putInt("status", statusResult.getStatus());
statusMap.putBoolean("valid", statusResult.getValid());
WritableMap statusMap = Arguments.createMap();
summary.getCredentialStatus().forEach((purpose, statusResult) -> {
WritableMap statusEntryMap = Arguments.createMap();
statusEntryMap.putBoolean("isValid", statusResult.isValid());
StatusCheckException error = statusResult.getError();
if (error != null) {
WritableMap errorMap = Arguments.createMap();
errorMap.putString("message", error.getMessage());
errorMap.putString("code", error.getErrorCode().name());
statusMap.putMap("error", errorMap);
statusEntryMap.putMap("error", errorMap);
} else {
statusMap.putNull("error");
statusEntryMap.putNull("error");
}
statusArray.pushMap(statusMap);
}
statusMap.putMap(purpose, statusEntryMap);
});
resultMap.putArray("credentialStatus", statusArray);
resultMap.putMap("credentialStatus", statusMap);
promise.resolve(resultMap);
} catch (Exception e) {
promise.reject("VERIFY_AND_GET_STATUS_ERROR", e.getMessage(), e);
}
}
}

View File

@@ -29,16 +29,19 @@ class VCVerifierModule: NSObject, RCTBridgeModule {
let verifier = CredentialsVerifier()
let results = try await verifier.getCredentialStatus(credential: credential, format: credentialFormat)
let responseArray = results.map { result in
return [
"status": result.status,
"purpose": result.purpose,
"errorCode": result.error?.errorCode.rawValue,
"errorMessage": result.error?.message,
"statusListVC": result.statusListVC
var response: [String: Any] = [:]
for (purpose, credentialStatusResult) in results {
let error : [String: Any]? = credentialStatusResult.error != nil ? [
"code": credentialStatusResult.error?.errorCode ?? "UNKNOWN_ERROR",
"message": credentialStatusResult.error?.message ?? "An unknown error occurred"
] : nil
response[purpose] = [
"isValid": credentialStatusResult.isValid,
"statusListVC": credentialStatusResult.statusListVC,
"error": error
]
}
resolve(responseArray)
resolve(response)
} catch {
reject("VERIFICATION_FAILED", "Verification threw an error: \(error.localizedDescription)", error)
}

View File

@@ -4,11 +4,11 @@ public struct CredentialsVerifier {
public init() {}
public func getCredentialStatus(credential: String, format: StatusCheckCredentialFormat, statusPurposeList: [String] = []) async throws-> [CredentialStatusResult] {
public func getCredentialStatus(credential: String, format: StatusCheckCredentialFormat, statusPurposeList: [String] = []) async throws-> [String: CredentialStatusResult] {
do {
let verifier = CredentialVerifierFactory().get(format: format)
let credentialStatusArray = try await verifier.checkStatus(credential: credential, statusPurposes: statusPurposeList)
return credentialStatusArray ?? []
let credentialStatuses = try await verifier.checkStatus(credential: credential, statusPurposes: statusPurposeList)
return credentialStatuses ?? [:]
} catch{
throw error
}

View File

@@ -1,5 +1,5 @@
import Foundation
protocol VerifiableCredential {
func checkStatus(credential: String, statusPurposes: [String]?) async throws-> [CredentialStatusResult]?
func checkStatus(credential: String, statusPurposes: [String]?) async throws-> [String: CredentialStatusResult]
}

View File

@@ -4,19 +4,15 @@ import Gzip
// MARK: - Credential Status Result
public struct CredentialStatusResult {
let purpose: String
let status: Int
let valid: Bool
let error: StatusCheckException?
let statusListVC: [String: Any]?
init(purpose: String, status: Int, valid: Bool, error: StatusCheckException?, statusListVC: [String: Any]? = nil) {
self.purpose = purpose
self.status = status
self.valid = valid
self.error = error
self.statusListVC = statusListVC
}
let isValid: Bool
let statusListVC: [String: Any]?
let error: StatusCheckException?
init(isValid: Bool, statusListVC: [String: Any]? = nil, error: StatusCheckException? = nil) {
self.isValid = isValid
self.statusListVC = statusListVC
self.error = error
}
}
// MARK: - Error Types
@@ -24,6 +20,7 @@ public struct CredentialStatusResult {
enum StatusCheckErrorCode: String {
case rangeError = "RANGE_ERROR"
case statusVerificationError = "STATUS_VERIFICATION_ERROR"
case invalidCredentialStatus = "INVALID_CREDENTIAL_STATUS"
case statusRetrievalError = "STATUS_RETRIEVAL_ERROR"
case invalidPurpose = "INVALID_PURPOSE"
case invalidIndex = "INVALID_INDEX"
@@ -33,7 +30,7 @@ enum StatusCheckErrorCode: String {
case unknownError = "UNKNOWN_ERROR"
}
struct StatusCheckException: Error {
public struct StatusCheckException: Error {
let message: String
let errorCode: StatusCheckErrorCode
}
@@ -49,7 +46,7 @@ final class LdpStatusChecker {
self.networkManager = networkManager
}
func getStatuses(credential: String, statusPurposes: [String]? = nil) async throws -> [CredentialStatusResult]? {
func getStatuses(credential: String, statusPurposes: [String]? = nil) async throws -> [String: CredentialStatusResult] {
guard
let data = credential.data(using: .utf8),
let vc = try JSONSerialization.jsonObject(with: data) as? [String: Any]
@@ -58,24 +55,33 @@ final class LdpStatusChecker {
}
let statusField = vc["credentialStatus"]
guard let statusEntries = normalizeStatusField(statusField) else { return nil }
guard let statusEntries = normalizeStatusField(statusField) else { throw StatusCheckException(
message: "No valid credentialStatus entries found",
errorCode: .invalidCredentialStatus
) }
let filteredEntries = filterEntries(statusEntries, statusPurposes)
guard !filteredEntries.isEmpty else { return nil }
var results: [CredentialStatusResult] = []
guard !filteredEntries.isEmpty else {
print("No matching credentialStatus entries found for purposes: \(statusPurposes ?? [])")
return [:]
}
var results: [String: CredentialStatusResult] = [:]
for entry in filteredEntries {
let purpose = (entry["statusPurpose"] as? String)?.lowercased() ?? ""
do {
let result = try await checkStatusEntry(entry: entry, purpose: purpose)
results.append(result)
} catch let error as StatusCheckException {
results.append(.init(purpose: purpose, status: -1, valid: false, error: error))
} catch {
let genericError = StatusCheckException(message: error.localizedDescription, errorCode: .unknownError)
results.append(.init(purpose: purpose, status: -1, valid: false, error: genericError))
}
guard let purpose = (entry["statusPurpose"] as? String)?.lowercased(), !purpose.isEmpty else {
print("Warning: Skipping entry with missing statusPurpose")
continue
}
do {
let result = try await checkStatusEntry(entry: entry, purpose: purpose)
results[purpose] = result
} catch let error as StatusCheckException {
results[purpose] = CredentialStatusResult(isValid: false, statusListVC: nil, error: error)
} catch {
let genericError = StatusCheckException(message: error.localizedDescription, errorCode: .unknownError)
results[purpose] = CredentialStatusResult(isValid: false, statusListVC: nil, error: genericError)
}
}
return results
}
@@ -171,6 +177,8 @@ final class LdpStatusChecker {
else {
throw StatusCheckException(message: "statusMessage count mismatch", errorCode: .statusVerificationError)
}
print("Status message for purpose '\(purpose): \(statusMessage)")
}
let bitSet = try decodeEncodedList(encodedList)
@@ -181,7 +189,10 @@ final class LdpStatusChecker {
}
let statusValue = readBits(from: bitSet, start: bitPosition, count: statusSize)
return .init(purpose: purpose, status: statusValue, valid: statusValue == 0, error: nil, statusListVC: statusListVC)
let isValid = (statusValue == 0)
print("Status value for purpose \(purpose) at index \(indexStr): \(statusValue)")
return .init(isValid: isValid, statusListVC: statusListVC, error: nil)
}
private func readBits(from bitSet: [UInt8], start: Int, count: Int) -> Int {

View File

@@ -1,7 +1,7 @@
import Foundation
struct LdpVerifiableCredential: VerifiableCredential {
func checkStatus(credential: String, statusPurposes: [String]?) async throws-> [CredentialStatusResult]? {
func checkStatus(credential: String, statusPurposes: [String]?) async throws-> [String: CredentialStatusResult] {
try await LdpStatusChecker().getStatuses(credential: credential)
}
}

View File

@@ -1,18 +1,21 @@
import {NativeModules} from 'react-native';
export type CredentialStatusResult = {
status: number;
purpose: string;
errorCode?: string;
errorMessage?: string;
statusListVC?: string;
isValid: boolean;
error?: ErrorResult;
statusListVC?: Record<string, any>; // Available only in iOS
};
export type ErrorResult = {
code: string;
message: string;
};
export type VerificationSummaryResult = {
verificationStatus: boolean;
verificationMessage: string;
verificationErrorCode: string;
credentialStatus: CredentialStatusResult[];
credentialStatus: Record<string, CredentialStatusResult>;
};
class VCVerifier {
@@ -33,14 +36,12 @@ class VCVerifier {
async getCredentialStatus(
credential: any,
format: string,
): Promise<CredentialStatusResult[]> {
): Promise<Record<string, CredentialStatusResult>> {
try {
const result: CredentialStatusResult[] =
await this.vcVerifier.getCredentialStatus(
JSON.stringify(credential),
format,
);
return result;
return await this.vcVerifier.getCredentialStatus(
JSON.stringify(credential),
format,
);
} catch (error) {
throw new Error(`Failed to get credential status: ${error}`);
}
@@ -51,12 +52,11 @@ class VCVerifier {
credentialFormat: string,
): Promise<VerificationSummaryResult> {
try {
const result = await this.vcVerifier.getVerificationSummary(
return await this.vcVerifier.getVerificationSummary(
credentialString,
credentialFormat,
[],
);
return result;
} catch (error) {
throw new Error(`Failed to get verification summary: ${error}`);
}

View File

@@ -14,7 +14,10 @@ import {getMosipIdentifier} from '../commonUtil';
import {NativeModules} from 'react-native';
import {isAndroid, isIOS} from '../constants';
import {VCFormat} from '../VCFormat';
import VCVerifier, {CredentialStatusResult, VerificationSummaryResult} from '../vcVerifier/VcVerifier';
import VCVerifier, {
CredentialStatusResult,
VerificationSummaryResult,
} from '../vcVerifier/VcVerifier';
// FIXME: Ed25519Signature2018 not fully supported yet.
// Ed25519Signature2018 proof type check is not tested with its real credential
@@ -62,10 +65,11 @@ async function verifyCredentialForAndroid(
typeof verifiableCredential === 'string'
? verifiableCredential
: JSON.stringify(verifiableCredential);
const vcVerifierResult = await VCVerifier.getInstance().getVerificationSummary(
credentialString,
credentialFormat,
);
const vcVerifierResult =
await VCVerifier.getInstance().getVerificationSummary(
credentialString,
credentialFormat,
);
return handleVcVerifierResponse(vcVerifierResult, verifiableCredential);
}
@@ -84,12 +88,13 @@ async function verifyCredentialForIos(
Since Digital Bazaar library is not able to verify ProofType: "Ed25519Signature2020",
defaulting it to return true until VcVerifier is implemented for iOS.
*/
let verificationResponse: VerificationResult;
let verificationResponse: VerificationResult;
if (verifiableCredential.proof.type === ProofType.ED25519_2020) {
verificationResponse = createSuccessfulVerificationResult();
}
else{
const purpose = getPurposeFromProof(verifiableCredential.proof.proofPurpose);
} else {
const purpose = getPurposeFromProof(
verifiableCredential.proof.proofPurpose,
);
const suite = selectVerificationSuite(verifiableCredential.proof);
const vcjsOptions = {
purpose,
@@ -97,19 +102,17 @@ async function verifyCredentialForIos(
credential: verifiableCredential,
documentLoader: jsonld.documentLoaders.xhr(),
};
const result = await vcjs.verifyCredential(vcjsOptions);
verificationResponse = handleResponse(result, verifiableCredential);
}
if (verificationResponse.isVerified) {
const statusArray = await VCVerifier.getInstance().getCredentialStatus(
verifiableCredential,
credentialFormat,
);
const isRevoked = await checkIsStatusRevoked(statusArray);
verificationResponse.isRevoked = isRevoked;
verificationResponse.isRevoked = await checkIsStatusRevoked(statusArray);
}
return verificationResponse;
}
@@ -192,7 +195,9 @@ async function handleVcVerifierResponse(
verifiableCredential,
);
}
const isRevoked = await checkIsStatusRevoked(verificationResult.credentialStatus)
const isRevoked = await checkIsStatusRevoked(
verificationResult.credentialStatus,
);
return {
isVerified: verificationResult.verificationStatus,
verificationMessage: verificationResult.verificationMessage,
@@ -213,48 +218,52 @@ async function handleVcVerifierResponse(
}
}
const handleStatusListVCVerification = (
status: CredentialStatusResult,
type: 'revoked' | 'valid',
) => {
const isValid = verifyStatusListVC(status.statusListVC);
if (!isValid) {
throw new Error(
`StatusListVC verification failed for ${type} entry ${status.error}`,
);
}
};
export async function checkIsStatusRevoked(
vcStatus: CredentialStatusResult[],
vcStatus: Record<string, CredentialStatusResult>,
): Promise<boolean> {
if (!vcStatus || vcStatus.length === 0) return false;
if (!Object.keys(vcStatus).length) return false;
const revocationStatuses = vcStatus.filter(
s => s.purpose?.toLowerCase() === 'revocation',
const revocationStatus = vcStatus['revocation'] as CredentialStatusResult;
if (!revocationStatus) return false;
const {isValid, error} = revocationStatus;
if (isValid) {
// Validate the valid statuses statusList VC for iOS
if (isIOS()) {
handleStatusListVCVerification(revocationStatus, 'valid');
}
return false;
}
console.error(
`Credential is revoked. Error: ${error?.code}, Message: ${error?.message}`,
);
let result = false;
for (const status of revocationStatuses) {
if (status.status > 0) {
if (isIOS()) {
const isValid = await verifyStatusListVC(status.statusListVC);
if (!isValid) {
throw new Error(`Revoked statusListVC verification failed`);
}
}
result = true;
} else if (status.status < 0) {
throw new Error(
`Error fetching revocation status : ${status.errorMessage}`,
);
}
// if there is an error fetching revocation status itself, throw error (isValid = false, error = Error)
if (error) {
throw new Error(
`Error fetching revocation status. Error: ${error.code}, Message: ${error.message}`,
);
}
if (result) return true;
// There is no error fetching revocation status, but the status is invalid (isValid = false, error = undefined) - VC is revoked
// Validate the valid statuses statusList VC for iOS
if (isIOS()) {
for (const status of revocationStatuses) {
if (status.status === 0) {
const isValid = await verifyStatusListVC(status.statusListVC);
if (!isValid) {
throw new Error(
`StatusListVC verification failed for valid entry ${status.errorMessage}`,
);
}
}
}
handleStatusListVCVerification(revocationStatus, 'revoked');
}
return false;
// If revocation status is invalid, the credential is revoked
return true;
}
function createSuccessfulVerificationResult(): VerificationResult {
@@ -310,7 +319,7 @@ export interface VerificationResult {
//TODO: Implement status list VC verification for iOS.
//Currently Digital Bazaar library does not support VC 2.0 status list VC verification.
function verifyStatusListVC(statusListVC: string | undefined) {
function verifyStatusListVC(statusListVC: Record<string, any> | undefined) {
return true;
}