diff --git a/android/app/build.gradle b/android/app/build.gradle index b2c72a2e..39be08a3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -264,8 +264,10 @@ android { dependencies { implementation("io.mosip:inji-openid4vp:0.3.0-SNAPSHOT"){ exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on' + exclude group: 'org.bouncycastle', module: 'bcpkix-jdk18on' exclude group: 'io.mosip', module: 'vcverifier-jar' exclude group: 'com.google.crypto.tink', module: 'tink' + exclude group: 'com.augustcellars.cose', module:'cose-java' } implementation("com.facebook.react:react-android") implementation 'com.facebook.soloader:soloader:0.10.1+' diff --git a/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java b/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java index b2c5896f..19bbb7e7 100644 --- a/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java +++ b/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java @@ -1,6 +1,9 @@ package io.mosip.residentapp; + import static io.mosip.openID4VP.authorizationResponse.AuthorizationResponseUtilsKt.toJsonString; +import static io.mosip.openID4VP.constants.FormatType.LDP_VC; +import static io.mosip.openID4VP.constants.FormatType.MSO_MDOC; import android.util.Log; @@ -19,6 +22,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.util.ArrayList; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.List; @@ -29,15 +33,19 @@ import java.util.Optional; import io.mosip.openID4VP.OpenID4VP; import io.mosip.openID4VP.authorizationRequest.AuthorizationRequest; import io.mosip.openID4VP.authorizationRequest.VPFormatSupported; +import io.mosip.openID4VP.authorizationRequest.Verifier; import io.mosip.openID4VP.authorizationRequest.WalletMetadata; -import io.mosip.openID4VP.authorizationResponse.models.unsignedVPToken.UnsignedVPToken; -import io.mosip.openID4VP.constants.ClientIdScheme; +import io.mosip.openID4VP.authorizationResponse.unsignedVPToken.UnsignedVPToken; +import io.mosip.openID4VP.authorizationResponse.vpTokenSigningResult.VPTokenSigningResult; +import io.mosip.openID4VP.authorizationResponse.vpTokenSigningResult.types.ldp.LdpVPTokenSigningResult; +import io.mosip.openID4VP.authorizationResponse.vpTokenSigningResult.types.mdoc.DeviceAuthentication; +import io.mosip.openID4VP.authorizationResponse.vpTokenSigningResult.types.mdoc.MdocVPTokenSigningResult; import io.mosip.openID4VP.constants.FormatType; -import io.mosip.openID4VP.dto.Verifier; -import io.mosip.openID4VP.dto.vpResponseMetadata.VPResponseMetadata; -import io.mosip.openID4VP.dto.vpResponseMetadata.types.LdpVPResponseMetadata; public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { + private static final String TAG = "InjiOpenID4VPModule"; + private static final String MODULE_NAME = "InjiOpenID4VP"; + private OpenID4VP openID4VP; private Gson gson; @@ -48,12 +56,12 @@ public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { @NonNull @Override public String getName() { - return "InjiOpenID4VP"; + return MODULE_NAME; } @ReactMethod public void init(String appId) { - Log.d("InjiOpenID4VPModule", "Initializing InjiOpenID4VPModule with " + appId); + Log.d(TAG, "Initializing InjiOpenID4VPModule with " + appId); openID4VP = new OpenID4VP(appId); gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) @@ -62,59 +70,47 @@ public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { } @ReactMethod - public void authenticateVerifier(String urlEncodedAuthorizationRequest, ReadableArray trustedVerifiers, - ReadableMap walletMetadata, Boolean shouldValidateClient, Promise promise) { + public void authenticateVerifier(String urlEncodedAuthorizationRequest, + ReadableArray trustedVerifiers, + ReadableMap walletMetadata, + Boolean shouldValidateClient, + Promise promise) { try { - WalletMetadata walletMetadataObj = getWalletMetadataFromReadableMap(walletMetadata); - AuthorizationRequest authenticationResponse = openID4VP.authenticateVerifier(urlEncodedAuthorizationRequest, - convertReadableArrayToVerifierArray(trustedVerifiers), walletMetadataObj, shouldValidateClient); - String authenticationResponseAsJson = gson.toJson(authenticationResponse, AuthorizationRequest.class); - promise.resolve(authenticationResponseAsJson); - } catch (Exception exception) { - promise.reject(exception); + WalletMetadata walletMetadataObj = parseWalletMetadata(walletMetadata); + List verifierList = parseVerifiers(trustedVerifiers); + + AuthorizationRequest authRequest = openID4VP.authenticateVerifier( + urlEncodedAuthorizationRequest, + verifierList, + walletMetadataObj, + shouldValidateClient); + + String authRequestJson = gson.toJson(authRequest, AuthorizationRequest.class); + promise.resolve(authRequestJson); + } catch (Exception e) { + promise.reject(e); } } @ReactMethod public void constructUnsignedVPToken(ReadableMap selectedVCs, Promise promise) { try { - Map>> selectedVCsMap = new HashMap<>(); - - ReadableMapKeySetIterator iterator = selectedVCs.keySetIterator(); - while (iterator.hasNextKey()) { - String inputDescriptorId = iterator.nextKey(); - ReadableMap matchingVcsOfDifferentFormats = selectedVCs.getMap(inputDescriptorId); - - ReadableMapKeySetIterator matchingVcsIterator = Objects.requireNonNull(matchingVcsOfDifferentFormats).keySetIterator(); - - Map> formattedMatchingVcsOfDifferentFormats = new EnumMap<>(FormatType.class); - while (matchingVcsIterator.hasNextKey()) { - String credentialFormat = matchingVcsIterator.nextKey(); - List matchingVcsOfSpecificCredentialFormat = convertReadableArrayToList(Objects.requireNonNull(matchingVcsOfDifferentFormats.getArray(credentialFormat))); - if (credentialFormat.equals(FormatType.LDP_VC.getValue())) - formattedMatchingVcsOfDifferentFormats.put(FormatType.LDP_VC, matchingVcsOfSpecificCredentialFormat); - else - throw new UnsupportedOperationException("Credential format - " + credentialFormat + " is not supported"); - } - selectedVCsMap.put(inputDescriptorId, formattedMatchingVcsOfDifferentFormats); - } + Map>> selectedVCsMap = parseSelectedVCs(selectedVCs); Map vpTokens = openID4VP.constructUnsignedVPToken(selectedVCsMap); - promise.resolve(toJsonString(vpTokens)); - } catch (Exception exception) { - promise.reject(exception); + } catch (Exception e) { + promise.reject(e); } - } @ReactMethod - public void shareVerifiablePresentation(ReadableMap vpResponseMetadata, Promise promise) { + public void shareVerifiablePresentation(ReadableMap vpTokenSigningResultMap, Promise promise) { try { - Map vpResMetadata = getVPResponseMetadata(vpResponseMetadata); - String response = openID4VP.shareVerifiablePresentation(vpResMetadata); + Map authContainer = parseVPTokenSigningResult(vpTokenSigningResultMap); + String response = openID4VP.shareVerifiablePresentation(authContainer); promise.resolve(response); - } catch (Exception exception) { - promise.reject(exception); + } catch (Exception e) { + promise.reject(e); } } @@ -123,71 +119,26 @@ public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { openID4VP.sendErrorToVerifier(new Exception(errorMessage)); } - private Map getVPResponseMetadata(ReadableMap vpResponsesMetadata) throws IllegalArgumentException { - ReadableMapKeySetIterator vpResponseMetadataEntryIterator = vpResponsesMetadata.keySetIterator(); - Map formattedVpResponsesMetadata = new EnumMap<>(FormatType.class); - while (vpResponseMetadataEntryIterator.hasNextKey()) { - String credentialFormat = vpResponseMetadataEntryIterator.nextKey(); - ReadableMap responseMetadataMap = vpResponsesMetadata.getMap(credentialFormat); - if (credentialFormat.equals(FormatType.LDP_VC.getValue())) { - String jws = responseMetadataMap.getString("jws"); - String signatureAlgorithm = responseMetadataMap.getString("signatureAlgorithm"); - String publicKey = responseMetadataMap.getString("publicKey"); - String domain = responseMetadataMap.getString("domain"); - LdpVPResponseMetadata ldpVPResponseMetadata = new LdpVPResponseMetadata(Objects.requireNonNull(jws), Objects.requireNonNull(signatureAlgorithm), Objects.requireNonNull(publicKey), Objects.requireNonNull(domain)); - formattedVpResponsesMetadata.put(FormatType.LDP_VC, ldpVPResponseMetadata); - } - } - return formattedVpResponsesMetadata; - } - - private List convertReadableArrayToList(ReadableArray readableArray) { - List convertedList = new ArrayList<>(); - for (int i = 0; i < readableArray.size(); i++) { - convertedList.add(readableArray.getString(i)); - } - return convertedList; - } - - public List convertReadableArrayToVerifierArray(ReadableArray readableArray) { - List trustedVerifiersList = new ArrayList<>(); - for (int i = 0; i < readableArray.size(); i++) { - ReadableMap verifierMap = readableArray.getMap(i); - String clientId = verifierMap.getString("client_id"); - ReadableArray responseUris = verifierMap.getArray("response_uris"); - List responseUriList = new ArrayList<>(); - for (int j = 0; j < responseUris.size(); j++) { - responseUriList.add(responseUris.getString(j)); - } - trustedVerifiersList.add(new Verifier(clientId, responseUriList)); - } - return trustedVerifiersList; - } - - private WalletMetadata getWalletMetadataFromReadableMap(ReadableMap walletMetadata) { + private WalletMetadata parseWalletMetadata(ReadableMap walletMetadata) { Boolean presentationDefinitionUriSupported = walletMetadata.hasKey("presentation_definition_uri_supported") - ? walletMetadata.getBoolean("presentation_definition_uri_supported") : null; + ? walletMetadata.getBoolean("presentation_definition_uri_supported") + : null; Map vpFormatsSupportedMap = new HashMap<>(); - ReadableMap vpFormatsMap = walletMetadata.getMap("vp_formats_supported"); - - if (vpFormatsMap != null && vpFormatsMap.hasKey("ldp_vc")) { - ReadableMap ldpVc = vpFormatsMap.getMap("ldp_vc"); - if (ldpVc != null && ldpVc.hasKey("alg_values_supported")) { - ReadableArray ldpVcAlgArray = ldpVc.getArray("alg_values_supported"); - List algValuesList = new ArrayList<>(ldpVcAlgArray.size()); - - for (int i = 0; i < ldpVcAlgArray.size(); i++) { - algValuesList.add(ldpVcAlgArray.getString(i)); + if (walletMetadata.hasKey("vp_formats_supported")) { + ReadableMap vpFormatsMap = walletMetadata.getMap("vp_formats_supported"); + if (vpFormatsMap != null && vpFormatsMap.hasKey("ldp_vc")) { + ReadableMap ldpVc = vpFormatsMap.getMap("ldp_vc"); + if (ldpVc != null && ldpVc.hasKey("alg_values_supported")) { + ReadableArray ldpVcAlgArray = ldpVc.getArray("alg_values_supported"); + List algValuesList = ldpVcAlgArray != null + ? convertReadableArrayToList(ldpVcAlgArray) + : null; + vpFormatsSupportedMap.put("ldp_vc", new VPFormatSupported(algValuesList)); } - - vpFormatsSupportedMap.put("ldp_vc", new VPFormatSupported(algValuesList)); - } else { - vpFormatsSupportedMap.put("ldp_vc", new VPFormatSupported(null)); } } - return new WalletMetadata( presentationDefinitionUriSupported, vpFormatsSupportedMap, @@ -198,6 +149,119 @@ public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { ); } + private List parseVerifiers(ReadableArray verifiersArray) { + List verifiers = new ArrayList<>(); + + for (int i = 0; i < verifiersArray.size(); i++) { + ReadableMap verifierMap = verifiersArray.getMap(i); + String clientId = verifierMap.getString("client_id"); + ReadableArray responseUris = verifierMap.getArray("response_uris"); + List responseUriList = convertReadableArrayToList(responseUris); + + verifiers.add(new Verifier(clientId, responseUriList)); + } + + return verifiers; + } + + private Map>> parseSelectedVCs(ReadableMap selectedVCs) { + if (selectedVCs == null) { + return Collections.emptyMap(); + } + Map>> selectedVCsMap = new HashMap<>(); + ReadableMapKeySetIterator iterator = selectedVCs.keySetIterator(); + while (iterator.hasNextKey()) { + String inputDescriptorId = iterator.nextKey(); + ReadableMap formatMap = selectedVCs.getMap(inputDescriptorId); + if (formatMap == null) { + continue; + } + Map> formatTypeCredentialsMap = new EnumMap<>(FormatType.class); + ReadableMapKeySetIterator formatIterator = formatMap.keySetIterator(); + + while (formatIterator.hasNextKey()) { + String formatStr = formatIterator.nextKey(); + ReadableArray vcsArray = formatMap.getArray(formatStr); + if (vcsArray == null) { + continue; + } + FormatType formatType = getFormatType(formatStr); + if (formatType != null) { + List vcsList = convertReadableArrayToList(vcsArray); + formatTypeCredentialsMap.put(formatType, vcsList); + } + } + + if (!formatTypeCredentialsMap.isEmpty()) { + selectedVCsMap.put(inputDescriptorId, formatTypeCredentialsMap); + } + } + return selectedVCsMap; + } + + private Map parseVPTokenSigningResult(ReadableMap vpTokenSigningResultMap) { + if (vpTokenSigningResultMap == null) { + return Collections.emptyMap(); + } + Map formattedMetadata = new EnumMap<>(FormatType.class); + ReadableMapKeySetIterator iterator = vpTokenSigningResultMap.keySetIterator(); + while (iterator.hasNextKey()) { + String formatStr = iterator.nextKey(); + ReadableMap metadata = vpTokenSigningResultMap.getMap(formatStr); + if (metadata == null) { + continue; + } + FormatType formatType = getFormatType(formatStr); + VPTokenSigningResult vpTokenSigningResult = createVPTokenSigningResult(formatType, metadata); + if (vpTokenSigningResult != null) { + formattedMetadata.put(formatType, vpTokenSigningResult); + } + } + + return formattedMetadata; + } + + private VPTokenSigningResult createVPTokenSigningResult(FormatType formatType, ReadableMap metadata) { + switch (formatType) { + case LDP_VC: { + String jws = requireNonNullString(metadata, "jws"); + String signatureAlgorithm = requireNonNullString(metadata, "signatureAlgorithm"); + String publicKey = requireNonNullString(metadata, "publicKey"); + String domain = requireNonNullString(metadata, "domain"); + return new LdpVPTokenSigningResult(jws, signatureAlgorithm, publicKey, domain); + } + case MSO_MDOC: { + Map signatureData = new HashMap<>(); + ReadableMapKeySetIterator docTypeIterator = metadata.keySetIterator(); + while (docTypeIterator.hasNextKey()) { + String docType = docTypeIterator.nextKey(); + ReadableMap deviceAuthenticationMap = metadata.getMap(docType); + if (deviceAuthenticationMap != null) { + String signature = requireNonNullString(deviceAuthenticationMap, "signature"); + String algorithm = requireNonNullString(deviceAuthenticationMap, "mdocAuthenticationAlgorithm"); + DeviceAuthentication deviceAuthentication = new DeviceAuthentication( + signature = signature, + algorithm = algorithm + ); + signatureData.put(docType, deviceAuthentication); + } + } + return new MdocVPTokenSigningResult(signatureData); + } + default: + return null; + } + } + + private FormatType getFormatType(String formatStr) { + if (LDP_VC.getValue().equals(formatStr)) { + return LDP_VC; + } else if (MSO_MDOC.getValue().equals(formatStr)) { + return MSO_MDOC; + } + throw new UnsupportedOperationException("Credential format not supported: " + formatStr); + } + private List extractStringListOrNull(ReadableMap readableMap, String key) { return Optional.ofNullable(readableMap.getArray(key)) .map(this::convertReadableArrayToList) @@ -205,4 +269,18 @@ public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { .orElse(null); } -} + private List convertReadableArrayToList(ReadableArray readableArray) { + List list = new ArrayList<>(); + + for (int i = 0; i < readableArray.size(); i++) { + list.add(readableArray.getString(i)); + } + + return list; + } + + private String requireNonNullString(ReadableMap map, String key) { + String value = map.getString(key); + return Objects.requireNonNull(value, key + " cannot be null"); + } +} \ No newline at end of file diff --git a/components/VC/common/VCUtils.tsx b/components/VC/common/VCUtils.tsx index c2e25ce9..d70487a7 100644 --- a/components/VC/common/VCUtils.tsx +++ b/components/VC/common/VCUtils.tsx @@ -299,7 +299,7 @@ export const getCredentialTypeFromWellKnown = ( export class Display { private readonly textColor: string | undefined = undefined; private readonly backgroundColor: {backgroundColor: string}; - private readonly backgroundImage: { uri: string } | undefined = undefined; + private readonly backgroundImage: {uri: string} | undefined = undefined; private defaultBackgroundColor = Theme.Colors.whiteBackgroundColor; @@ -336,3 +336,34 @@ export class Display { return this.backgroundImage ?? defaultBackgroundImage; } } + +export function getIssuerAuthenticationAlorithmForMdocVC( + proofType: any, +): string { + return PROOF_TYPE_ALGORITHM_MAP[proofType] || ''; +} + +export function getMdocAuthenticationAlorithm(issuerAuth: any): string { + const deviceKey = issuerAuth?.deviceKeyInfo?.deviceKey; + + if (!deviceKey) return ''; + + const keyType = deviceKey['1']; + const curve = deviceKey['-1']; + + return keyType === ProtectedAlgorithm.EC2 && curve === ProtectedCurve.P256 + ? 'ES256' + : ''; +} + +const ProtectedAlgorithm = { + EC2: 2, +}; + +const ProtectedCurve = { + P256: 1, +}; + +const PROOF_TYPE_ALGORITHM_MAP = { + [-7]: 'ES256', +}; diff --git a/ios/Inji.xcodeproj/project.pbxproj b/ios/Inji.xcodeproj/project.pbxproj index 2b7b4473..444502c9 100644 --- a/ios/Inji.xcodeproj/project.pbxproj +++ b/ios/Inji.xcodeproj/project.pbxproj @@ -834,7 +834,7 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mosip/pixelpass-ios-swift"; requirement = { - branch = "release-0.6.x"; + branch = develop; kind = branch; }; }; diff --git a/ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved index 997e14f7..a451ba9b 100644 --- a/ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -52,7 +52,7 @@ "location" : "https://github.com/mosip/inji-openid4vp-ios-swift", "state" : { "branch" : "develop", - "revision" : "db16a9bf63b69942942a3e49bf7d40eaeba186bf" + "revision" : "820875cccb546d8c5aa666fcad9f910609091f44" } }, { @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/mosip/pixelpass-ios-swift", "state" : { - "branch" : "release-0.6.x", - "revision" : "15249dc4eecb7c2b6e6074193d3928b3402c9d20" + "branch" : "develop", + "revision" : "17361f74c582a0b92744dc386439b0f0b68c58fc" } }, { diff --git a/ios/RNOpenID4VPModule.swift b/ios/RNOpenID4VPModule.swift index 48d75147..f978c031 100644 --- a/ios/RNOpenID4VPModule.swift +++ b/ios/RNOpenID4VPModule.swift @@ -70,6 +70,8 @@ class RNOpenId4VpModule: NSObject, RCTBridgeModule { switch FormatType(rawValue: credentialFormat) { case .ldp_vc: result[.ldp_vc] = credentialsArray + case .mso_mdoc: + result[.mso_mdoc] = credentialsArray default: reject("OPENID4VP", "Credential format is not supported for OVP", nil) } @@ -98,16 +100,16 @@ class RNOpenId4VpModule: NSObject, RCTBridgeModule { } @objc - func shareVerifiablePresentation(_ vpResponsesMetadata: [String: Any], resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + func shareVerifiablePresentation(_ vpTokenSigningResults: [String: Any], resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { Task { do { - var formattedVPResponsesMetadata: [FormatType: VPResponseMetadata] = [:] + var formattedVPTokenSigningResults: [FormatType: VPTokenSigningResult] = [:] - for (credentialFormat, vpResponseMetadata) in vpResponsesMetadata { + for (credentialFormat, vpTokenSigningResult) in vpTokenSigningResults { switch credentialFormat { case FormatType.ldp_vc.rawValue: - guard let vpResponse = vpResponseMetadata as? [String:Any] else { - reject("OPENID4VP", "Invalid vp response meta format", nil) + guard let vpResponse = vpTokenSigningResult as? [String:Any] else { + reject("OPENID4VP", "Invalid VP token signing result format", nil) return } guard let jws = vpResponse["jws"] as! String?, @@ -115,17 +117,35 @@ class RNOpenId4VpModule: NSObject, RCTBridgeModule { let publicKey = vpResponse["publicKey"] as! String?, let domain = vpResponse["domain"] as! String? else { - reject("OPENID4VP", "Invalid vp response metadata", nil) + reject("OPENID4VP", "Invalid VP token signing result", nil) return } - formattedVPResponsesMetadata[FormatType.ldp_vc] = LdpVPResponseMetadata(jws: jws, signatureAlgorithm: signatureAlgorithm, publicKey: publicKey, domain: domain) + formattedVPTokenSigningResults[FormatType.ldp_vc] = LdpVPTokenSigningResult(jws: jws, signatureAlgorithm: signatureAlgorithm, publicKey: publicKey, domain: domain) + case FormatType.mso_mdoc.rawValue: + var docTypeToDeviceAuthentication : [String: DeviceAuthentication] = [:] + guard let vpResponse = vpTokenSigningResult as? [String:[String: String]] else { + reject("OPENID4VP", "Invalid VP token signing result format", nil) + return + } + for (docType, deviceAuthentication) in vpResponse { + guard let signature = deviceAuthentication["signature"], + let algorithm = deviceAuthentication["mdocAuthenticationAlgorithm"] else { + reject("OPENID4VP", "Invalid VP token signing result provided for mdoc format", nil) + return + } + + docTypeToDeviceAuthentication[docType] = DeviceAuthentication(signature: signature, algorithm: algorithm) + } + formattedVPTokenSigningResults[.mso_mdoc] = MdocVPTokenSigningResult(docTypeToDeviceAuthentication: docTypeToDeviceAuthentication) + + default: - reject("OPENID4VP", "Invalid vp response meta format", nil) + reject("OPENID4VP", "Invalid VP response meta format", nil) } } - let response = try await openID4VP?.shareVerifiablePresentation(vpResponsesMetadata: formattedVPResponsesMetadata) + let response = try await openID4VP?.shareVerifiablePresentation(vpTokenSigningResults: formattedVPTokenSigningResults) resolve(response) } catch { diff --git a/locales/ara.json b/locales/ara.json index a97d316c..51c5a08b 100644 --- a/locales/ara.json +++ b/locales/ara.json @@ -878,6 +878,10 @@ "noImage": { "title": "حدث خطأ!", "message": "يتطلب التحقق من الوجه صورة في بيانات الاعتماد المحددة. الرجاء استخدام خيار المشاركة أو تحديد بيانات اعتماد تتضمن صورة." + }, + "duplicateMdocCredential": { + "title": "حدث خطأ!", + "message": "يمكنك مشاركة رخصة قيادة متنقلة أو مستند واحد فقط في كل مرة. يرجى اختيار مستند واحد فقط للمتابعة." } }, "loaders": { diff --git a/locales/en.json b/locales/en.json index 4e3865fc..739f0bd8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -890,6 +890,10 @@ "noImage": { "title": "An Error Occured!", "message": "Face verification requires a photo in the selected credential(s). Please use the Share option or select a credential that includes an image." + }, + "duplicateMdocCredential": { + "title": "An Error Occured!", + "message": "You can share only one Mobile Driving License or document at a time. Please select just one to continue." } }, "loaders": { diff --git a/locales/fil.json b/locales/fil.json index 46c72415..a2165bfe 100644 --- a/locales/fil.json +++ b/locales/fil.json @@ -881,6 +881,10 @@ "noImage": { "title": "Isang Error ang Naganap!", "message": "Ang pag-verify ng mukha ay nangangailangan ng larawan sa napiling (mga) kredensyal. Pakigamit ang opsyong Ibahagi o pumili ng kredensyal na may kasamang larawan." + }, + "duplicateMdocCredential": { + "title": "May Naganap na Error!", + "message": "Maaari ka lamang magbahagi ng isang Mobile Driving License o dokumento sa bawat pagkakataon. Mangyaring pumili lamang ng isa upang magpatuloy." } }, "loaders": { diff --git a/locales/hin.json b/locales/hin.json index 906bfa6c..c13ece29 100644 --- a/locales/hin.json +++ b/locales/hin.json @@ -884,6 +884,10 @@ "noImage": { "title": "एक त्रुटि हुई!", "message": "चेहरे के सत्यापन के लिए चयनित क्रेडेंशियल में एक फोटो की आवश्यकता होती है। कृपया शेयर विकल्प का उपयोग करें या एक क्रेडेंशियल चुनें जिसमें एक छवि शामिल हो।" + }, + "duplicateMdocCredential": { + "title": "एक त्रुटि हुई!", + "message": "आप एक समय में केवल एक मोबाइल ड्राइविंग लाइसेंस या दस्तावेज़ साझा कर सकते हैं। कृपया जारी रखने के लिए केवल एक का चयन करें।" } }, "loaders": { diff --git a/locales/kan.json b/locales/kan.json index 43149c2e..9060408e 100644 --- a/locales/kan.json +++ b/locales/kan.json @@ -882,6 +882,10 @@ "noImage": { "title": "ಒಂದು ದೋಷ ಸಂಭವಿಸಿದೆ!", "message": "ಮುಖ ಪರಿಶೀಲನೆಗೆ ಆಯ್ಕೆಮಾಡಿದ ರುಜುವಾತು(ಗಳಲ್ಲಿ) ಫೋಟೋ ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು ಹಂಚಿಕೆ ಆಯ್ಕೆಯನ್ನು ಬಳಸಿ ಅಥವಾ ಚಿತ್ರವನ್ನು ಒಳಗೊಂಡಿರುವ ರುಜುವಾತುಗಳನ್ನು ಆಯ್ಕೆಮಾಡಿ." + }, + "duplicateMdocCredential": { + "title": "ದೋಷ ಸಂಭವಿಸಿದೆ!", + "message": "ಒಂದು ವೇಳೆ ಒಂದು ಮೊಬೈಲ್ ಡ್ರೈವಿಂಗ್ ಲೈಸೆನ್ಸ್ ಅಥವಾ ಡಾಕ್ಯುಮೆಂಟ್ ಅನ್ನು ಮಾತ್ರ ಹಂಚಬಹುದು. ಮುಂದುವರಿಸಲು ಒಂದು ಡಾಕ್ಯುಮೆಂಟ್ ಅನ್ನು ಮಾತ್ರ ಆಯ್ಕೆಮಾಡಿ." } }, "loaders": { diff --git a/locales/tam.json b/locales/tam.json index 55bc0afe..d29414ea 100644 --- a/locales/tam.json +++ b/locales/tam.json @@ -882,6 +882,10 @@ "noImage": { "title": "ஒரு பிழை ஏற்பட்டது!", "message": "முகம் சரிபார்ப்புக்கு தேர்ந்தெடுக்கப்பட்ட நற்சான்றிதழில்(களில்) ஒரு புகைப்படம் தேவை. பகிர் விருப்பத்தைப் பயன்படுத்தவும் அல்லது படத்தை உள்ளடக்கிய நற்சான்றிதழைத் தேர்ந்தெடுக்கவும்." + }, + "duplicateMdocCredential": { + "title": "ஒரு பிழை ஏற்பட்டது!", + "message": "ஒரே நேரத்தில் ஒரு மொபைல் டிரைவிங் லைசன்ஸ் அல்லது ஆவணத்தை மட்டுமே பகிர முடியும். தொடர ஒரு ஆவணத்தை மட்டும் தேர்ந்தெடுக்கவும்." } }, "loaders": { diff --git a/machines/Issuers/IssuersActions.ts b/machines/Issuers/IssuersActions.ts index 297067b8..7c196285 100644 --- a/machines/Issuers/IssuersActions.ts +++ b/machines/Issuers/IssuersActions.ts @@ -177,16 +177,12 @@ export const IssuersActions = (model: any) => { storeVerifiableCredentialData: send( (context: any) => { const vcMetadata = context.vcMetadata; - const { - verifiableCredential: { - processedCredential, - ...filteredVerifiableCredential - }, - ...rest - } = context.credentialWrapper; + const credentialWrapper = context.credentialWrapper; const storableData = { - ...rest, - verifiableCredential: filteredVerifiableCredential, + ...credentialWrapper, + verifiableCredential: { + ...credentialWrapper.verifiableCredential, + }, }; return StoreEvents.SET(vcMetadata.getVcKey(), { ...storableData, diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.ts b/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.ts index a3c306cb..d6a47ac6 100644 --- a/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.ts +++ b/machines/VerifiableCredential/VCItemMachine/VCItemSelectors.ts @@ -7,8 +7,6 @@ import { VerifiableCredential, VerifiableCredentialData, } from '../VCMetaMachine/vc'; -import {VCFormat} from '../../../shared/VCFormat'; -import {VCProcessor} from '../../../components/VC/common/VCProcessor'; type State = StateFrom; diff --git a/machines/openID4VP/openID4VPActions.ts b/machines/openID4VP/openID4VPActions.ts index 4bd0e52c..fee11481 100644 --- a/machines/openID4VP/openID4VPActions.ts +++ b/machines/openID4VP/openID4VPActions.ts @@ -12,12 +12,17 @@ import {VCShareFlowType} from '../../shared/Utils'; import {ActivityLogEvents} from '../activityLog'; import {VPShareActivityLog} from '../../components/VPShareActivityLogEvent'; import {OpenID4VP} from '../../shared/openID4VP/OpenID4VP'; +import {VCFormat} from '../../shared/VCFormat'; +import { + getIssuerAuthenticationAlorithmForMdocVC, + getMdocAuthenticationAlorithm, +} from '../../components/VC/common/VCUtils'; // TODO - get this presentation definition list which are alias for scope param // from the verifier end point after the endpoint is created and exposed. export const openID4VPActions = (model: any) => { - let requestedClaimsByVerifier; + let result; return { setAuthenticationResponse: model.assign({ authenticationResponse: (_, event) => event.data, @@ -33,59 +38,11 @@ export const openID4VPActions = (model: any) => { getVcsMatchingAuthRequest: model.assign({ vcsMatchingAuthRequest: (context, event) => { - let vcs = event.vcs; - let matchingVCs = {} as Record; - let presentationDefinition = - context.authenticationResponse['presentation_definition']; - let anyInputDescriptorHasFormatOrConstraints = false; - requestedClaimsByVerifier = new Set(); - vcs.forEach(vc => { - presentationDefinition['input_descriptors'].forEach( - inputDescriptor => { - const format = - inputDescriptor.format ?? presentationDefinition.format; - anyInputDescriptorHasFormatOrConstraints = - anyInputDescriptorHasFormatOrConstraints || - format !== undefined || - inputDescriptor.constraints.fields !== undefined; - - const isMatchingConstraints = isVCMatchingRequestConstraints( - inputDescriptor.constraints, - vc.verifiableCredential.credential, - requestedClaimsByVerifier, - ); - - const areMatchingFormatAndProofType = - areVCFormatAndProofTypeMatchingRequest( - format, - vc.format, - vc?.verifiableCredential?.credential?.proof?.type, - ); - - if (inputDescriptor.constraints.fields && format) { - if (isMatchingConstraints && areMatchingFormatAndProofType) { - matchingVCs[inputDescriptor.id]?.push(vc) || - (matchingVCs[inputDescriptor.id] = [vc]); - } - } else if ( - isMatchingConstraints || - areMatchingFormatAndProofType - ) { - matchingVCs[inputDescriptor.id]?.push(vc) || - (matchingVCs[inputDescriptor.id] = [vc]); - } - }, - ); - }); - if (!anyInputDescriptorHasFormatOrConstraints) { - matchingVCs[presentationDefinition['input_descriptors'][0].id] = vcs; - } - if (Object.keys(matchingVCs).length === 0) { - OpenID4VP.sendErrorToVerifier(OVP_ERROR_MESSAGES.NO_MATCHING_VCS); - } - return matchingVCs; + result = getVcsMatchingAuthRequest(context, event); + return result.matchingVCs; }, - requestedClaims: () => Array.from(requestedClaimsByVerifier).join(','), + requestedClaims: () => result.requestedClaimsByVerifier, + purpose: context => { const response = context.authenticationResponse; const pd = response['presentation_definition']; @@ -281,45 +238,163 @@ export const openID4VPActions = (model: any) => { }; }; +function getVcsMatchingAuthRequest(context, event) { + const vcs = event.vcs; + const matchingVCs: Record = {}; + const requestedClaimsByVerifier = new Set(); + const presentationDefinition = + context.authenticationResponse['presentation_definition']; + const inputDescriptors = presentationDefinition['input_descriptors']; + let hasFormatOrConstraints = false; + vcs.forEach(vc => { + inputDescriptors.forEach(inputDescriptor => { + const format = inputDescriptor.format ?? presentationDefinition.format; + hasFormatOrConstraints = + hasFormatOrConstraints || + format !== undefined || + inputDescriptor.constraints.fields !== undefined; + + const areMatchingFormatAndProofType = + areVCFormatAndProofTypeMatchingRequest(format, vc); + if (areMatchingFormatAndProofType == false) { + return; + } + const isMatchingConstraints = isVCMatchingRequestConstraints( + inputDescriptor.constraints, + vc, + requestedClaimsByVerifier, + ); + + let shouldInclude = false; + if (inputDescriptor.constraints.fields && format) { + shouldInclude = isMatchingConstraints && areMatchingFormatAndProofType; + } else { + shouldInclude = isMatchingConstraints || areMatchingFormatAndProofType; + } + + if (shouldInclude) { + if (!matchingVCs[inputDescriptor.id]) { + matchingVCs[inputDescriptor.id] = []; + } + matchingVCs[inputDescriptor.id].push(vc); + } + }); + }); + + if (!hasFormatOrConstraints && inputDescriptors.length > 0) { + matchingVCs[inputDescriptors[0].id] = vcs; + } + + if (Object.keys(matchingVCs).length === 0) { + OpenID4VP.sendErrorToVerifier(OVP_ERROR_MESSAGES.NO_MATCHING_VCS); + } + + return { + matchingVCs, + requestedClaims: Array.from(requestedClaimsByVerifier).join(','), + purpose: presentationDefinition.purpose ?? '', + }; +} + function areVCFormatAndProofTypeMatchingRequest( - format: Record, - vcFormatType: string, - vcProofType: string, + requestFormat: Record | undefined, + vc: any, ): boolean { - if (!format) { + if (!requestFormat) { return false; } - return Object.entries(format).some( - ([type, value]) => - type === vcFormatType && value.proof_type.includes(vcProofType), - ); + + const vcFormatType = vc.format; + + if (vcFormatType === VCFormat.ldp_vc) { + const vcProofType = vc?.verifiableCredential?.credential?.proof?.type; + return Object.entries(requestFormat).some( + ([type, value]) => + type === vcFormatType && value.proof_type.includes(vcProofType), + ); + } + + if (vcFormatType === VCFormat.mso_mdoc) { + const issuerAuth = + vc.verifiableCredential.processedCredential.issuerSigned.issuerAuth; + const issuerAuthenticationAlgorithm = + getIssuerAuthenticationAlorithmForMdocVC(issuerAuth[0]['1']); + const mdocAuthenticationAlgorithm = getMdocAuthenticationAlorithm( + issuerAuth[2], + ); + + return Object.entries(requestFormat).some( + ([type, value]) => + type === vcFormatType && + value.alg.includes(issuerAuthenticationAlgorithm) && + value.alg.includes(mdocAuthenticationAlgorithm), + ); + } + + return false; } function isVCMatchingRequestConstraints( - constraints, - credential, - requestedClaimsByVerifier, + constraints: any, + credential: any, + requestedClaimsByVerifier: Set, ): boolean { if (!constraints.fields) { return false; } - for (const field of constraints.fields) { - for (const path of field.path) { + return constraints.fields.every(field => { + return field.path.some(path => { const pathArray = JSONPath.toPathArray(path); - requestedClaimsByVerifier.add(pathArray[pathArray.length - 1]); - const valueMatchingPath = JSONPath({ + const claimName = pathArray[pathArray.length - 1]; + requestedClaimsByVerifier.add(claimName); + const processedCredential = fetchCredentialBasedOnFormat(credential); + const jsonPathMatches = JSONPath({ path: path, - json: credential, - })[0]; - - if ( - valueMatchingPath !== undefined && - field.filter?.type === typeof valueMatchingPath && - String(valueMatchingPath).includes(field.filter?.pattern) - ) { - return true; + json: processedCredential, + }); + if (!jsonPathMatches || jsonPathMatches.length === 0) { + return false; } + return jsonPathMatches.some(match => { + if (!field.filter) { + return true; + } + return ( + field.filter.type === undefined || field.filter.type === typeof match + ); + }); + }); + }); +} + +function fetchCredentialBasedOnFormat(vc: any) { + const format = vc.format; + let credential; + switch (format.toString()) { + case VCFormat.ldp_vc: { + credential = vc.verifiableCredential.credential; + break; + } + case VCFormat.mso_mdoc: { + credential = getProcessedDataForMdoc( + vc.verifiableCredential.processedCredential, + ); + break; } } - return false; + return credential; +} + +function getProcessedDataForMdoc(processedCredential: any) { + const namespaces = processedCredential.issuerSigned.nameSpaces; + const processedData = {...namespaces}; + for (const ns in processedData) { + const elementsArray = processedData[ns]; + const asObject: Record = {}; + elementsArray.forEach((item: any) => { + asObject[item.elementIdentifier] = item.elementValue; + }); + processedData[ns] = asObject; + } + return processedData; } diff --git a/machines/openID4VP/openID4VPGuards.ts b/machines/openID4VP/openID4VPGuards.ts index 85113ac4..9290451f 100644 --- a/machines/openID4VP/openID4VPGuards.ts +++ b/machines/openID4VP/openID4VPGuards.ts @@ -1,4 +1,3 @@ -import {isClientValidationRequired} from '../../shared/openID4VP/OpenID4VP'; import {VCShareFlowType} from '../../shared/Utils'; export const openID4VPGuards = () => { @@ -26,7 +25,7 @@ export const openID4VPGuards = () => { const hasImage = Object.values(context.selectedVCs) .flatMap(vc => vc) .some( - vc => vc.verifiableCredential?.credential?.credentialSubject.face, + vc => vc.verifiableCredential?.credential?.credentialSubject?.face, ); return !!hasImage; }, diff --git a/machines/openID4VP/openID4VPMachine.typegen.ts b/machines/openID4VP/openID4VPMachine.typegen.ts index 6c43e9eb..940fcf9f 100644 --- a/machines/openID4VP/openID4VPMachine.typegen.ts +++ b/machines/openID4VP/openID4VPMachine.typegen.ts @@ -8,6 +8,11 @@ export interface Typegen0 { data: unknown; __tip: 'See the XState TS docs to learn how to strongly type this.'; }; + 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]': { + type: 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]': { type: 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; data: unknown; @@ -57,6 +62,7 @@ export interface Typegen0 { getKeyPair: 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; getSelectedKey: 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; sendVP: 'done.invoke.OpenID4VP.sendingVP:invocation[0]'; + shouldValidateClient: 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]'; }; missingImplementations: { actions: @@ -75,10 +81,10 @@ export interface Typegen0 { | 'resetOpenID4VPRetryCount' | 'setAuthenticationError' | 'setAuthenticationResponse' - | 'setEncodedAuthorizationRequest' | 'setError' | 'setFlowType' | 'setIsFaceVerificationRetryAttempt' + | 'setIsOVPViaDeepLink' | 'setIsShareWithSelfie' | 'setIsShowLoadingScreen' | 'setMiniViewShareSelectedVC' @@ -88,6 +94,7 @@ export interface Typegen0 { | 'setShowFaceAuthConsent' | 'setTrustedVerifiers' | 'setTrustedVerifiersApiCallError' + | 'setUrlEncodedAuthorizationRequest' | 'shareDeclineStatus' | 'storeShowFaceAuthConsent' | 'updateFaceCaptureBannerStatus' @@ -96,6 +103,7 @@ export interface Typegen0 { guards: | 'hasKeyPair' | 'isAnyVCHasImage' + | 'isClientValidationRequred' | 'isFaceVerificationRetryAttempt' | 'isSelectedVCMatchingRequest' | 'isShareWithSelfie' @@ -106,7 +114,8 @@ export interface Typegen0 { | 'getAuthenticationResponse' | 'getKeyPair' | 'getSelectedKey' - | 'sendVP'; + | 'sendVP' + | 'shouldValidateClient'; }; eventsCausingActions: { compareAndStoreSelectedVC: 'SET_SELECTED_VC'; @@ -128,14 +137,14 @@ export interface Typegen0 { resetOpenID4VPRetryCount: 'RESET_RETRY_COUNT'; setAuthenticationError: 'error.platform.OpenID4VP.authenticateVerifier:invocation[0]'; setAuthenticationResponse: 'done.invoke.OpenID4VP.authenticateVerifier:invocation[0]'; - setEncodedAuthorizationRequest: 'AUTHENTICATE'; setError: | 'error.platform.OpenID4VP.checkKeyPair:invocation[0]' | 'error.platform.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; setFlowType: 'AUTHENTICATE'; setIsFaceVerificationRetryAttempt: 'FACE_INVALID'; + setIsOVPViaDeepLink: 'AUTHENTICATE'; setIsShareWithSelfie: 'AUTHENTICATE'; - setIsShowLoadingScreen: 'STORE_RESPONSE'; + setIsShowLoadingScreen: 'AUTHENTICATE'; setMiniViewShareSelectedVC: 'AUTHENTICATE'; setSelectedVCs: 'ACCEPT_REQUEST' | 'VERIFY_AND_ACCEPT_REQUEST'; setSendVPShareError: 'error.platform.OpenID4VP.sendingVP:invocation[0]'; @@ -143,10 +152,11 @@ export interface Typegen0 { setShowFaceAuthConsent: 'FACE_VERIFICATION_CONSENT'; setTrustedVerifiers: 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; setTrustedVerifiersApiCallError: 'error.platform.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + setUrlEncodedAuthorizationRequest: 'AUTHENTICATE'; shareDeclineStatus: 'CONFIRM'; storeShowFaceAuthConsent: 'FACE_VERIFICATION_CONSENT'; updateFaceCaptureBannerStatus: 'FACE_VALID'; - updateShowFaceAuthConsent: 'STORE_RESPONSE'; + updateShowFaceAuthConsent: 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]'; }; eventsCausingDelays: { SHARING_TIMEOUT: 'CONFIRM' | 'FACE_VALID' | 'RETRY'; @@ -156,6 +166,7 @@ export interface Typegen0 { | 'FACE_VALID' | 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; isAnyVCHasImage: 'CHECK_FOR_IMAGE'; + isClientValidationRequred: 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]'; isFaceVerificationRetryAttempt: 'FACE_INVALID'; isSelectedVCMatchingRequest: 'CHECK_SELECTED_VC'; isShareWithSelfie: @@ -170,18 +181,22 @@ export interface Typegen0 { showFaceAuthConsentScreen: 'CONFIRM'; }; eventsCausingServices: { - fetchTrustedVerifiers: 'STORE_RESPONSE'; + fetchTrustedVerifiers: 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]'; getAuthenticationResponse: 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; - getKeyPair: 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + getKeyPair: + | 'done.invoke.OpenID4VP.checkIfClientValidationIsRequired:invocation[0]' + | 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; getSelectedKey: | 'FACE_VALID' | 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; sendVP: 'CONFIRM' | 'FACE_VALID' | 'RETRY'; + shouldValidateClient: 'STORE_RESPONSE'; }; matchesStates: | 'authenticateVerifier' | 'checkFaceAuthConsent' | 'checkIfAnySelectedVCHasImage' + | 'checkIfClientValidationIsRequired' | 'checkIfMatchingVCsHasSelectedVC' | 'checkKeyPair' | 'faceVerificationConsent' diff --git a/machines/openID4VP/openID4VPServices.ts b/machines/openID4VP/openID4VPServices.ts index 974152c4..8cbbc128 100644 --- a/machines/openID4VP/openID4VPServices.ts +++ b/machines/openID4VP/openID4VPServices.ts @@ -1,5 +1,8 @@ import {CACHED_API} from '../../shared/api'; -import {fetchKeyPair} from '../../shared/cryptoutil/cryptoUtil'; +import { + createSignature, + fetchKeyPair, +} from '../../shared/cryptoutil/cryptoUtil'; import {getJWK, hasKeyPair} from '../../shared/openId4VCI/Utils'; import base64url from 'base64url'; import { @@ -9,8 +12,9 @@ import { OpenID4VP_Domain, OpenID4VP_Proof_Sign_Algo_Suite, } from '../../shared/openID4VP/OpenID4VP'; -import {VCFormat} from "../../shared/VCFormat"; +import {VCFormat} from '../../shared/VCFormat'; import {KeyTypes} from '../../shared/cryptoutil/KeyTypes'; +import {getMdocAuthenticationAlorithm} from '../../components/VC/common/VCUtils'; export const openID4VPServices = () => { return { @@ -42,35 +46,96 @@ export const openID4VPServices = () => { }, sendVP: (context: any) => async () => { - const vpTokens = await OpenID4VP.constructUnsignedVPToken( + const unSignedVpTokens = await OpenID4VP.constructUnsignedVPToken( context.selectedVCs, ); - let vpResponsesMetadata: Record = {} - for (const formatType in vpTokens){ - const value = vpTokens[formatType] - if(formatType === VCFormat.ldp_vc.valueOf()){ + let vpTokenSigningResultMap: Record = {}; + for (const formatType in unSignedVpTokens) { + const credentials = unSignedVpTokens[formatType]; + + if (formatType === VCFormat.ldp_vc.valueOf()) { const proofJWT = await constructProofJWT( - context.publicKey, - context.privateKey, - value, - context.keyType, + context.publicKey, + context.privateKey, + credentials, + context.keyType, ); - vpResponsesMetadata[formatType] = { + vpTokenSigningResultMap[formatType] = { jws: proofJWT, signatureAlgorithm: OpenID4VP_Proof_Sign_Algo_Suite, publicKey: - 'did:jwk:' + - base64url( - JSON.stringify(await getJWK(context.publicKey, KeyTypes.ED25519)), + 'did:jwk:' + + base64url( + JSON.stringify( + await getJWK(context.publicKey, KeyTypes.ED25519), ), + ), domain: OpenID4VP_Domain, - } + }; + } else if (formatType === VCFormat.mso_mdoc.valueOf()) { + const signedData: Record = {}; + + const mdocCredentialsByDocType = Object.values(context.selectedVCs) + .flat() + .reduce((acc, credential) => { + if (credential.format === 'mso_mdoc') { + const docType = + credential?.verifiableCredential?.processedCredential + ?.docType; + if (docType) { + acc[docType] = credential; + } + } + return acc; + }, {}); + + await Promise.all( + Object.entries(credentials.docTypeToDeviceAuthenticationBytes).map( + async ([docType, payload]) => { + const cred = mdocCredentialsByDocType[docType]; + + if (!cred) return; + + const mdocAuthenticationAlgorithm = + getMdocAuthenticationAlorithm( + cred.verifiableCredential.processedCredential.issuerSigned + .issuerAuth[2], + ); + + if (mdocAuthenticationAlgorithm === KeyTypes.ES256.valueOf()) { + const key = await fetchKeyPair(mdocAuthenticationAlgorithm); + const signature = await createSignature( + key.privateKey, + '', + payload, + mdocAuthenticationAlgorithm, + '', + payload, + ); + + if (signature) { + signedData[docType] = { + signature, + mdocAuthenticationAlgorithm, + }; + } + } else { + throw new Error( + `Unsupported algorithm: ${mdocAuthenticationAlgorithm}`, + ); + } + }, + ), + ); + + vpTokenSigningResultMap[formatType] = signedData; } } - - return await OpenID4VP.shareVerifiablePresentation(vpResponsesMetadata); + return await OpenID4VP.shareVerifiablePresentation( + vpTokenSigningResultMap, + ); }, }; }; diff --git a/screens/Scan/SendVPScreenController.ts b/screens/Scan/SendVPScreenController.ts index 948a8e9c..6979e032 100644 --- a/screens/Scan/SendVPScreenController.ts +++ b/screens/Scan/SendVPScreenController.ts @@ -192,7 +192,11 @@ export function useSendVPScreen() { errorModal.title = t('errors.invalidQrCode.title'); errorModal.message = t('errors.invalidQrCode.message'); generateAndStoreLogMessage('INVALID_AUTH_REQUEST'); - } else if (error.startsWith('send vp')) { + } else if (error.startsWith('send vp - Duplicate Mdoc Credentials')) { + errorModal.title = t('errors.duplicateMdocCredential.title'); + errorModal.message = t('errors.duplicateMdocCredential.message'); + errorModal.showRetryButton = false; + }else if (error.startsWith('send vp')) { errorModal.title = t('errors.genericError.title'); errorModal.message = t('errors.genericError.message'); errorModal.showRetryButton = true; diff --git a/shared/openID4VP/OpenID4VP.ts b/shared/openID4VP/OpenID4VP.ts index e9912a87..8b62466e 100644 --- a/shared/openID4VP/OpenID4VP.ts +++ b/shared/openID4VP/OpenID4VP.ts @@ -9,6 +9,7 @@ import {getJWK} from '../openId4VCI/Utils'; import getAllConfigurations from '../api'; import {parseJSON} from '../Utils'; import {walletMetadata} from './walletMetadata'; +import {VCFormat} from '../VCFormat'; export const OpenID4VP_Key_Ref = 'OpenID4VP_KeyPair'; export const OpenID4VP_Proof_Sign_Algo_Suite = 'Ed25519Signature2020'; @@ -44,17 +45,18 @@ export class OpenID4VP { this.processSelectedVCs(selectedVCs), ); - const vpTokens = await OpenID4VP.InjiOpenID4VP.constructUnsignedVPToken( - updatedSelectedVCs, - ); - return parseJSON(vpTokens); + const unSignedVpTokens = + await OpenID4VP.InjiOpenID4VP.constructUnsignedVPToken( + updatedSelectedVCs, + ); + return parseJSON(unSignedVpTokens); } static async shareVerifiablePresentation( - vpResponsesMetadata: Record, + vpTokenSigningResultMap: Record, ) { return await OpenID4VP.InjiOpenID4VP.shareVerifiablePresentation( - vpResponsesMetadata, + vpTokenSigningResultMap, ); } @@ -65,23 +67,27 @@ export class OpenID4VP { private static stringifyValues = ( data: Record>>, ): Record> => { - return Object.fromEntries( - Object.entries(data).map(([key, innerMap]) => [ - key, - Object.fromEntries( - Object.entries(innerMap).map(([innerKey, arr]) => [ - innerKey, - arr.map(item => JSON.stringify(item)), - ]), - ), - ]), - ); + const result = {}; + for (const [outerKey, innerObject] of Object.entries(data)) { + result[outerKey] = {}; + for (const [innerKey, array] of Object.entries(innerObject)) { + if (innerKey === VCFormat.ldp_vc.valueOf()) + result[outerKey][innerKey] = array.map(item => JSON.stringify(item)); + else result[outerKey][innerKey] = array; + } + } + return result; }; + private static processSelectedVCs(selectedVCs: Record) { const selectedVcsData: SelectedCredentialsForVPSharing = {}; Object.entries(selectedVCs).forEach(([inputDescriptorId, vcsArray]) => { vcsArray.forEach(vcData => { const credentialFormat = vcData.vcMetadata.format; + //TODO: this should be done irrespective of the format. + if (credentialFormat === VCFormat.mso_mdoc.valueOf()) { + vcData = vcData.verifiableCredential.credential; + } if (!selectedVcsData[inputDescriptorId]) { selectedVcsData[inputDescriptorId] = {}; } @@ -135,6 +141,9 @@ export async function isClientValidationRequired() { export async function getWalletMetadata() { const config = await getAllConfigurations(); + if (!config.walletMetadata) { + return null; + } const walletMetadata = JSON.parse(config.walletMetadata); return walletMetadata; } diff --git a/shared/openID4VP/walletMetadata.ts b/shared/openID4VP/walletMetadata.ts index aa566613..762f791f 100644 --- a/shared/openID4VP/walletMetadata.ts +++ b/shared/openID4VP/walletMetadata.ts @@ -8,6 +8,9 @@ export const walletMetadata = { 'RSASignature2018', ], }, + mso_mdoc: { + alg_values_supported: ['ES256'] + } }, client_id_schemes_supported: ['redirect_uri', 'did', 'pre-registered'], request_object_signing_alg_values_supported: ['EdDSA'], diff --git a/shared/telemetry/TelemetryUtils.js b/shared/telemetry/TelemetryUtils.js index 56f9d936..c75fab4b 100644 --- a/shared/telemetry/TelemetryUtils.js +++ b/shared/telemetry/TelemetryUtils.js @@ -16,31 +16,31 @@ import * as RNLocalize from 'react-native-localize'; import {TelemetryConstants} from './TelemetryConstants'; export function sendStartEvent(data) { - telemetry.start({}, '', '', data, {}); + //telemetry.start({}, '', '', data, {}); } export function sendEndEvent(data) { - telemetry.end(data, {}); + //telemetry.end(data, {}); } export function sendImpressionEvent(data) { - telemetry.impression(data, {}); + //telemetry.impression(data, {}); } export function sendInteractEvent(data) { - telemetry.interact(data, {}); + //telemetry.interact(data, {}); } export function sendAppInfoEvent(data) { - telemetry.appinfo(data); + //telemetry.appinfo(data); } export function sendErrorEvent(data) { - telemetry.error(data, {}); + //telemetry.error(data, {}); } export function initializeTelemetry(config) { - telemetry.initialize(config); + //telemetry.initialize(config); } export function getTelemetryConfigData() {