diff --git a/tw2023_wallet.xcodeproj/project.pbxproj b/tw2023_wallet.xcodeproj/project.pbxproj index fbae92f..3e744cb 100644 --- a/tw2023_wallet.xcodeproj/project.pbxproj +++ b/tw2023_wallet.xcodeproj/project.pbxproj @@ -216,6 +216,11 @@ A8ADC69B2BCEAD090077A0C4 /* idTokenSharingHistories.json in Resources */ = {isa = PBXBuildFile; fileRef = A8ADC69A2BCEAD090077A0C4 /* idTokenSharingHistories.json */; }; A8AF22882C12A59B00D6EDA5 /* RestoreHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AF22872C12A59B00D6EDA5 /* RestoreHelper.swift */; }; A8AF228D2C12A6C100D6EDA5 /* RestoreHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8AF228C2C12A6C100D6EDA5 /* RestoreHelperTests.swift */; }; + A8B0D5A82CEF347800624E26 /* PresentationExchangeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B0D5A72CEF347000624E26 /* PresentationExchangeTest.swift */; }; + A8B0D5AA2CEF356800624E26 /* presentation_definition_multi_descriptors_2.json in Resources */ = {isa = PBXBuildFile; fileRef = A8B0D5A92CEF355800624E26 /* presentation_definition_multi_descriptors_2.json */; }; + A8B0D5AB2CEF356800624E26 /* presentation_definition_multi_descriptors_2.json in Resources */ = {isa = PBXBuildFile; fileRef = A8B0D5A92CEF355800624E26 /* presentation_definition_multi_descriptors_2.json */; }; + A8B0E66C2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json in Resources */ = {isa = PBXBuildFile; fileRef = A8B0E66B2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json */; }; + A8B0E66D2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json in Resources */ = {isa = PBXBuildFile; fileRef = A8B0E66B2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json */; }; A8C0A3D32CABD088008998C5 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C0A3D22CABD088008998C5 /* Errors.swift */; }; A8C0A3D42CABD089008998C5 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C0A3D22CABD088008998C5 /* Errors.swift */; }; A8C810422B82042300CF8CD6 /* SharingToViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8C8103C2B82042200CF8CD6 /* SharingToViewModel.swift */; }; @@ -449,6 +454,9 @@ A8ADC69A2BCEAD090077A0C4 /* idTokenSharingHistories.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = idTokenSharingHistories.json; sourceTree = ""; }; A8AF22872C12A59B00D6EDA5 /* RestoreHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreHelper.swift; sourceTree = ""; }; A8AF228C2C12A6C100D6EDA5 /* RestoreHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreHelperTests.swift; sourceTree = ""; }; + A8B0D5A72CEF347000624E26 /* PresentationExchangeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationExchangeTest.swift; sourceTree = ""; }; + A8B0D5A92CEF355800624E26 /* presentation_definition_multi_descriptors_2.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = presentation_definition_multi_descriptors_2.json; sourceTree = ""; }; + A8B0E66B2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = presentation_definition_multi_descriptors_1.json; sourceTree = ""; }; A8C0A3D22CABD088008998C5 /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; A8C810382B80951800CF8CD6 /* SharingToRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingToRow.swift; sourceTree = ""; }; A8C8103C2B82042200CF8CD6 /* SharingToViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingToViewModel.swift; sourceTree = ""; }; @@ -568,6 +576,7 @@ 8B297C2D2B393A4900D2998D /* Resources */ = { isa = PBXGroup; children = ( + A8B0D5AC2CEF407900624E26 /* presentation_definition */, A8AB4BD42C29853300C009A1 /* credential_response */, A88D323B2C26A75500429E75 /* metadata */, A82601B02C26908A00BF8139 /* credential_offer */, @@ -711,6 +720,7 @@ 8B81E2AC2B33CC4300ED3B4E /* tw2023_walletTests */ = { isa = PBXGroup; children = ( + A8B0D5A72CEF347000624E26 /* PresentationExchangeTest.swift */, A8EF7F2E2C75D76100BA9D9C /* Models */, A88779B92C33DBDA002EE9C2 /* Feature */, A8AF22892C12A61E00D6EDA5 /* Helper */, @@ -1137,6 +1147,15 @@ path = Helper; sourceTree = ""; }; + A8B0D5AC2CEF407900624E26 /* presentation_definition */ = { + isa = PBXGroup; + children = ( + A8B0D5A92CEF355800624E26 /* presentation_definition_multi_descriptors_2.json */, + A8B0E66B2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json */, + ); + path = presentation_definition; + sourceTree = ""; + }; A8C0A3C82CABCEA0008998C5 /* Provider */ = { isa = PBXGroup; children = ( @@ -1478,6 +1497,7 @@ A8AB4BDA2C29873400C009A1 /* credential_response_vc_sd_jwt.json in Resources */, 8B297C452B39779000D2998D /* credential_supported_ldp_vc.json in Resources */, 8B297C3F2B39746000D2998D /* credential_supported_jwt_vc.json in Resources */, + A8B0E66D2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json in Resources */, A8AB4BC52C294FFD00C009A1 /* credential_offer_minimum.json in Resources */, A8AB4BE82C2A62B000C009A1 /* credential_issuer_metadata_ldp_vc.json in Resources */, 8B43AE3B2B3AA1300016CF83 /* authorization_server.json in Resources */, @@ -1487,6 +1507,7 @@ F657D5C72B3C44F100901A6A /* sharingHistoryData.json in Resources */, A8AB4BC82C295C9500C009A1 /* credential_offer_tx_code_required.json in Resources */, A8AB4BD02C296AB900C009A1 /* claim_map_empty.json in Resources */, + A8B0D5AB2CEF356800624E26 /* presentation_definition_multi_descriptors_2.json in Resources */, 8BB513902B3B24C400D4EFB3 /* credential_response_mock.json in Resources */, F6A239A62B4E2A6500B09F17 /* clientInfo.json in Resources */, 8B81E2E62B34413A00ED3B4E /* credentialData.json in Resources */, @@ -1518,8 +1539,10 @@ A8AB4BE92C2A62B000C009A1 /* credential_issuer_metadata_ldp_vc.json in Resources */, 8B0E0AB32B4054E80080F6A3 /* presentation_definition.json in Resources */, A82601A92C267A2A00BF8139 /* credential_display_filled.json in Resources */, + A8B0D5AA2CEF356800624E26 /* presentation_definition_multi_descriptors_2.json in Resources */, A8AB4BD22C296C7800C009A1 /* claim_map_filled.json in Resources */, 8B43AE3A2B3A9DD20016CF83 /* authorization_server.json in Resources */, + A8B0E66C2CE078E800D9E823 /* presentation_definition_multi_descriptors_1.json in Resources */, 8B297C442B39778B00D2998D /* credential_supported_ldp_vc.json in Resources */, A8AB4BE02C298D3B00C009A1 /* credential_response_deferred.json in Resources */, A8AB4BE32C298D9700C009A1 /* credential_response_notification.json in Resources */, @@ -1779,6 +1802,7 @@ 8BB513942B3BB69A00D4EFB3 /* AsynTestRunner.swift in Sources */, 8BB5138D2B3AD0CC00D4EFB3 /* VCIClient.swift in Sources */, 8B5C65962B5237C200D72289 /* Types.swift in Sources */, + A8B0D5A82CEF347800624E26 /* PresentationExchangeTest.swift in Sources */, A8EF7F302C75D78000BA9D9C /* ModelDataTests.swift in Sources */, A84AB70B2B50B98C00E8C88B /* CredentialSharingHistoryManagerTest.swift in Sources */, 8B43AE2D2B3A5A730016CF83 /* VCIMetadataClient.swift in Sources */, diff --git a/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift b/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift index b6aecac..e0f4cfa 100644 --- a/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift +++ b/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift @@ -48,7 +48,7 @@ class CredentialDetailViewModel { if let pd = presentationDefinition { switch credential.format { case "vc+sd-jwt": - if let matched = pd.matchSdJwtVcToRequirement( + if let matched = pd.firstMatchedInputDescriptor( sdJwt: credential.payload) { let (inputDescriptors, disclosuresWithOptionality) = matched diff --git a/tw2023_wallet/Feature/Credentials/ViewModels/CredentialListViewModel.swift b/tw2023_wallet/Feature/Credentials/ViewModels/CredentialListViewModel.swift index 14c9902..a8d2ae2 100644 --- a/tw2023_wallet/Feature/Credentials/ViewModels/CredentialListViewModel.swift +++ b/tw2023_wallet/Feature/Credentials/ViewModels/CredentialListViewModel.swift @@ -51,7 +51,7 @@ class CredentialListViewModel { print("format: \(format)") do { if format == "vc+sd-jwt" { - let ret = presentationDefinition.matchSdJwtVcToRequirement( + let ret = presentationDefinition.firstMatchedInputDescriptor( sdJwt: credential.payload) if let (_, disclosures) = ret { return 0 diff --git a/tw2023_wallet/Services/OID/PresentationExchange.swift b/tw2023_wallet/Services/OID/PresentationExchange.swift index 3ea62a4..e662667 100644 --- a/tw2023_wallet/Services/OID/PresentationExchange.swift +++ b/tw2023_wallet/Services/OID/PresentationExchange.swift @@ -69,7 +69,7 @@ struct PresentationDefinition: Codable { // extension let submissionRequirements: [SubmissionRequirement]? - func matchSdJwtVcToRequirement(sdJwt: String) -> ( + func firstMatchedInputDescriptor(sdJwt: String) -> ( InputDescriptor, [DisclosureWithOptionality] )? { guard let sdJwtParts = try? SDJwtUtil.divideSDJwt(sdJwt: sdJwt) else { @@ -88,32 +88,64 @@ struct PresentationDefinition: Codable { } }) - // 各InputDescriptorをループ for inputDescriptor in inputDescriptors { - // fieldKeysを取得 - let requiredOrOptionalKeys = inputDescriptor.filterKeysWithOptionality( + + // inputDescriptorとsourcePayload(クレデンシャル側)に共通するkeyを、optionality付きで取得 + let commonKeysWithOptionality = inputDescriptor.filterKeysWithOptionality( from: sourcePayload) - let matchingDisclosures = createDisclosureWithOptionality( - from: allDisclosures, - with: requiredOrOptionalKeys - ) + // sourcePayload(クレデンシャル側)とinputDescriptorに + // 共通するキーがないならば、このループのinputDescriptorにマッチしていない。 + if commonKeysWithOptionality.isEmpty { + continue + } - if !matchingDisclosures.isEmpty { - return (inputDescriptor, matchingDisclosures) + // inputDescriptorで必須とされる全てのキーが、共通キーに含まれていないならば、 + // このループのinputDescriptorにマッチしていない + guard let fields = inputDescriptor.constraints.fields else { + continue + } + let allIncluded = fields.allSatisfy { field in + let optionalField = field.optional ?? false + if optionalField { + return true + } + return field.path.contains { jsonPath in + let simplifiedPath = String(jsonPath.dropFirst(2)) + return commonKeysWithOptionality.contains { (key, _) in key == simplifiedPath } + } + } + if !allIncluded { + continue } + + // 引数に与えられたクレデンシャルは、このループの inputDescriptor に合致している。 + // クレデンシャルの各クレームについて、「送信必須のクレーム」、「送信するか否かを選択できるクレーム」、「送信しないクレーム」の情報を返す。 + let claimDisclosability = createDisclosureWithOptionality( + from: allDisclosures, + with: commonKeysWithOptionality + ) + return (inputDescriptor, claimDisclosability) } return nil } + private func createDisclosureWithOptionality( - from allDisclosures: [Disclosure], with requiredOrOptionalKeys: [(String, Bool)] + from allDisclosures: [Disclosure], with commonKeysWithOptionality: [(String, Bool)] ) -> [DisclosureWithOptionality] { + // 自身が開示可能なSD-JWTクレーム(allDisclosures)について、 + // 1.送信が必須なもの、2.送信するか否かを選択できるもの、3.送信しないもの の情報をつけて返す。 + // + // 1. isSubmit: true, isUserSelectable: false + // 2. isSubmit: false(デフォルトでは送信しないということ), isUserSelectable: true + // 3. isSubmit: false, isUserSelectable: false + // return allDisclosures.map { disclosure in guard let dkey = disclosure.key else { return DisclosureWithOptionality( disclosure: disclosure, isSubmit: false, isUserSelectable: false) } - for (keyName, optionality) in requiredOrOptionalKeys { + for (keyName, optionality) in commonKeysWithOptionality { if keyName.contains(dkey) { return DisclosureWithOptionality( disclosure: disclosure, isSubmit: !optionality, diff --git a/tw2023_wallet/Signature/JWTUtil.swift b/tw2023_wallet/Signature/JWTUtil.swift index 2da9d53..0b1f348 100644 --- a/tw2023_wallet/Signature/JWTUtil.swift +++ b/tw2023_wallet/Signature/JWTUtil.swift @@ -71,7 +71,7 @@ func convertRstoDer(r: Data, s: Data) -> Data? { } enum JWTUtil { - + /* For verification-related methods, we plan to enhance the checking process and implement a mechanism to control the level of checking in the future. diff --git a/tw2023_walletTests/AuthorizationRquestTests.swift b/tw2023_walletTests/AuthorizationRquestTests.swift index 083a3ce..f4fd982 100644 --- a/tw2023_walletTests/AuthorizationRquestTests.swift +++ b/tw2023_walletTests/AuthorizationRquestTests.swift @@ -235,7 +235,7 @@ final class AuthorizationRquestTests: XCTestCase { } } } - + func testPresentationDefinitionMatchSdJwt() { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] @@ -266,8 +266,9 @@ final class AuthorizationRquestTests: XCTestCase { let pdOptional = try await processPresentationDefinition( authorizationRequest, requestObject, using: mockSession) let pd = try XCTUnwrap(pdOptional, "PresentationDefinition should not be nil.") - let sdJwt = "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiOWhnWm5VbGEyT1JhTHB3Wkp6T0pBTUZfVUd2dzVOekIwTEdmU1VaNTN6cyJdLCJfc2RfYWxnIjoiU0hBLTI1NiJ9.nzsiKRK39ijCaw0oD9nmhrB41HnZj_CiShckWZAVRW3tCDTm3vrJHyoVj4F7_2mx2aMvbT4iAekDGGtsXyhdvw~WyJmZWE3MTcwYTc3OGRiNzk1IiwiaXNfb2xkZXJfdGhhbl8xMyIsdHJ1ZV0~" - XCTAssertNotNil(pd.matchSdJwtVcToRequirement(sdJwt: sdJwt)) + let sdJwt = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiOWhnWm5VbGEyT1JhTHB3Wkp6T0pBTUZfVUd2dzVOekIwTEdmU1VaNTN6cyJdLCJfc2RfYWxnIjoiU0hBLTI1NiJ9.nzsiKRK39ijCaw0oD9nmhrB41HnZj_CiShckWZAVRW3tCDTm3vrJHyoVj4F7_2mx2aMvbT4iAekDGGtsXyhdvw~WyJmZWE3MTcwYTc3OGRiNzk1IiwiaXNfb2xkZXJfdGhhbl8xMyIsdHJ1ZV0~" + XCTAssertNotNil(pd.firstMatchedInputDescriptor(sdJwt: sdJwt)) XCTAssertEqual(pd.id, "12345") } catch { @@ -276,6 +277,78 @@ final class AuthorizationRquestTests: XCTestCase { } } + func testPresentationDefinitionProperDescriptorSelection() { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + let mockSession = URLSession(configuration: configuration) + + let testURL = URL(string: "https://example.com/presentation_definition.json")! + guard + let url = Bundle.main.url( + forResource: "presentation_definition_multi_descriptors_1", withExtension: "json"), + let mockData = try? Data(contentsOf: url) + else { + XCTFail("Cannot read presentation_definition.json") + return + } + let response = HTTPURLResponse( + url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + MockURLProtocol.mockResponses[testURL.absoluteString] = (mockData, response) + + let authorizationRequest = AuthorizationRequestPayloadImpl( + presentationDefinition: nil + ) + let requestObject = RequestObjectPayloadImpl( + presentationDefinitionUri: testURL.absoluteString + ) + + runAsyncTest { + do { + guard + let pd = try await processPresentationDefinition( + authorizationRequest, requestObject, using: mockSession) + else { + XCTFail("PresentationDefinition shoud be present") + return + } + + // is_older_than13 + let sdJwt1 = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiOWhnWm5VbGEyT1JhTHB3Wkp6T0pBTUZfVUd2dzVOekIwTEdmU1VaNTN6cyJdLCJfc2RfYWxnIjoiU0hBLTI1NiJ9.nzsiKRK39ijCaw0oD9nmhrB41HnZj_CiShckWZAVRW3tCDTm3vrJHyoVj4F7_2mx2aMvbT4iAekDGGtsXyhdvw~WyJmZWE3MTcwYTc3OGRiNzk1IiwiaXNfb2xkZXJfdGhhbl8xMyIsdHJ1ZV0~" + // postal_address + let sdJwt2 = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiQmVHOFVNc1VHdzJFUFNodUhGbDZsek9CZERnaW1LNzE3bjJ5VnN6SWRRbyJdLCJfc2RfYWxnIjoiU0hBLTI1NiJ9.FI4uBC_pL9nMcrzrrMBhZXrgNR6WEJNQxCruoz5gWlc4fDV7Y4UYj-NYJ0O_IVXkfvJJSG4mBRu63LJaTrKdGA~WyJhNDZjZmJlOTUyN2YzOWI3IiwicG9zdGFsX2FkZHJlc3MiLCJUb2t5byBKYXBhbiJd~" + + guard + let (inputDescriptor1, disclosureWithOptionallity1) = + pd.firstMatchedInputDescriptor(sdJwt: sdJwt1) + else { + XCTFail("inputDescriptor should not be nil") + return + } + guard + let (inputDescriptor2, disclosureWithOptionallity2) = + pd.firstMatchedInputDescriptor(sdJwt: sdJwt2) + else { + XCTFail("inputDescriptor should not be nil") + return + } + XCTAssertEqual(inputDescriptor1.id, "input2") + XCTAssertTrue(disclosureWithOptionallity1.count == 1) + XCTAssertTrue(!disclosureWithOptionallity1[0].isUserSelectable) + XCTAssertTrue(disclosureWithOptionallity1[0].isSubmit) + + XCTAssertEqual(inputDescriptor2.id, "input1") + XCTAssertTrue(disclosureWithOptionallity2.count == 1) + XCTAssertTrue(!disclosureWithOptionallity2[0].isUserSelectable) + XCTAssertTrue(disclosureWithOptionallity2[0].isSubmit) + } + catch { + XCTFail("Request should not fail. \(error)") + } + } + } + func testPresentationDefinitionFromQueryParameter() { let configuration = URLSessionConfiguration.ephemeral configuration.protocolClasses = [MockURLProtocol.self] diff --git a/tw2023_walletTests/OpenIdProviderTests.swift b/tw2023_walletTests/OpenIdProviderTests.swift index 5ca9c5f..e00a610 100644 --- a/tw2023_walletTests/OpenIdProviderTests.swift +++ b/tw2023_walletTests/OpenIdProviderTests.swift @@ -531,7 +531,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition1.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) XCTAssertNil(selected) } @@ -545,7 +545,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition1.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") @@ -579,7 +579,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition2.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") @@ -613,7 +613,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition3.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") @@ -647,7 +647,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition4.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") @@ -681,7 +681,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition4.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") @@ -709,7 +709,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition5.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") @@ -743,7 +743,7 @@ final class OpenIdProviderTests: XCTestCase { decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( PresentationDefinition.self, from: presentationDefinition6.data(using: .utf8)!) - let selected = presentationDefinition.matchSdJwtVcToRequirement( + let selected = presentationDefinition.firstMatchedInputDescriptor( sdJwt: sdJwt) if let (inputDescriptor, disclosures) = selected { XCTAssertEqual(inputDescriptor.id, "input1") diff --git a/tw2023_walletTests/PresentationExchangeTest.swift b/tw2023_walletTests/PresentationExchangeTest.swift new file mode 100644 index 0000000..209afe6 --- /dev/null +++ b/tw2023_walletTests/PresentationExchangeTest.swift @@ -0,0 +1,140 @@ +// +// PresentationExchangeTest.swift +// tw2023_wallet +// +// Created by katsuyoshi ozaki on 2024/11/21. +// + +import XCTest + +@testable import tw2023_wallet + +final class PresentationExchangeTest: XCTestCase { + + var subsetRelationship: PresentationDefinition? + + let sdJwtPrefecture = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiZnJpakxvSm1qQnhxTzk1c1A0WVAzMVJJNGJkd2ctdnlVZWpDZUlxTlJTYyJdLCJfc2RfYWxnIjoiU0hBLTI1NiJ9.WCGcU9Ox6PYKMtDazkxztXm0qUZ4nXeTDvx875ZNNQ5_dj4yyUCdZdEoCzPMkiySUo6hirMIliAK4EhG39g3sg~WyI0ZTk5MWMzNjQ4ZjU2ZTg4IiwicHJlZmVjdHVyZSIsIlRva3lvIl0~" + + let sdJwtPrefectureAndPostalCode = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiMlMxRXdhV1RBMEpGRlhSNXlySW1VbW9jekhwb3dNQ1c5OUw5SW5wUGdWbyIsIlNQVC0xMk81UU1YSHhseUNuLWtrTTRzM2FYa0s5ejZ5dzBuT01CMTdSVVkiXSwiX3NkX2FsZyI6IlNIQS0yNTYifQ.yoldrSUzadig98dyWm2CWoEOsWTuOD51qv5Q37dxIZUm-GTVnBjChLnYWZiXaTwcTqFrYKWnKDFusfPhltAV3g~WyJhNmFiODFkNTk2ODBkYjQ2IiwicHJlZmVjdHVyZSIsIlRva3lvIl0~WyJkYzgwYjZmZjI2OGQ2Y2M4IiwicG9zdGFsX2NvZGUiLCIxMjMiXQ~" + + let sdJwtPostalCode = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiWS1WUS1VZHBFVlhxazF1WWJoQnkwaFFkREdGVEwxRHE3UGRnT1JhUTFMUSJdLCJfc2RfYWxnIjoiU0hBLTI1NiJ9.mnjJ6fCfwphf4y4WKZ1zysDUDDlhPE1_pVD5ONufnzjqGMXlFKXxocv6LxBE5RpRiKK3O0uicwG09MrfHzUabw~WyJmNzNjZDg1ZWUyMjVjMDRjIiwicG9zdGFsX2NvZGUiLCIxMjMiXQ~" + + let sdJwtPostalCodeAndFamilyGivenName = + "eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJfc2QiOlsiSGQ4T0swS2FmMDJqd3BncnI4MlBsWnJTYUJlUXhmRXB1SlY5YzlBUi1jcyIsImJuenJQUDNBcGhyaTMtWi1uaHhFeEY2NXFZRHA3UnE5MktNem54aVBGMkkiLCJ0Qm9oZDFtVUlaLVAydERoYV9EVHpsaS1zQk5JYkhyMmh0Sm9NS3E1dmtjIl0sIl9zZF9hbGciOiJTSEEtMjU2In0.WBmCzLb19vpT_JVl6Ai9ObMW39V3U5l1PEBOUlpu58Wt77KR1KdYUPBAWQJxhNENzlMEn1RJeIN0RaXggdBIBQ~WyJkOWQ0Y2ZkNzljMjhkYWYzIiwicG9zdGFsX2NvZGUiLCIxMjMiXQ~WyIzOWNhNDcyNzAyZmEzZWZiIiwiZmFtaWx5X25hbWUiLCJZYW1hZGEiXQ~WyJmNWNlYzY1MTY2NTAyMzVjIiwiZ2l2ZW5fbmFtZSIsIlRhcm8iXQ~" + + private func decodePresentationDefinition(resourceFile: String) throws -> PresentationDefinition + { + let url = Bundle.main.url( + forResource: resourceFile, withExtension: "json")! + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + return try decoder.decode(PresentationDefinition.self, from: data) + } + + override func setUpWithError() throws { + decodeDisclosureFunction = SDJwtUtil.decodeDisclosure + subsetRelationship = try decodePresentationDefinition( + resourceFile: "presentation_definition_multi_descriptors_2") + } + + override func tearDownWithError() throws { + } + + func testSelectFirstInputDescriptor() throws { + guard let pd = subsetRelationship else { + XCTFail("The presentation definition has not been initialized correctly.") + return + } + + guard + let (firstMatchedInputDescriptor, disclosureWithOptionality) = + pd.firstMatchedInputDescriptor(sdJwt: sdJwtPrefectureAndPostalCode) + else { + XCTFail("should be matched") + return + } + + XCTAssertEqual(firstMatchedInputDescriptor.id, "input1") + XCTAssertTrue(disclosureWithOptionality.count == 2) + for d in disclosureWithOptionality { + XCTAssertTrue(d.isSubmit) + XCTAssertTrue(!d.isUserSelectable) + } + } + + func testSelectSecondInputDescriptor() throws { + guard let pd = subsetRelationship else { + XCTFail("The presentation definition has not been initialized correctly.") + return + } + + guard + let (firstMatchedInputDescriptor, disclosureWithOptionality) = + pd.firstMatchedInputDescriptor(sdJwt: sdJwtPrefecture) + else { + XCTFail("shoud be matched") + return + } + + XCTAssertEqual(firstMatchedInputDescriptor.id, "input2") + XCTAssertTrue(disclosureWithOptionality.count == 1) + XCTAssertTrue(disclosureWithOptionality[0].isSubmit) + XCTAssertTrue(!disclosureWithOptionality[0].isUserSelectable) + } + + func testSelectThirdInputDescriptor1() throws { + guard let pd = subsetRelationship else { + XCTFail("The presentation definition has not been initialized correctly.") + return + } + + guard + let (firstMatchedInputDescriptor, disclosureWithOptionality) = + pd.firstMatchedInputDescriptor(sdJwt: sdJwtPostalCode) + else { + XCTFail("shoud be matched") + return + } + + XCTAssertEqual(firstMatchedInputDescriptor.id, "input3") + XCTAssertTrue(disclosureWithOptionality.count == 1) + XCTAssertTrue(disclosureWithOptionality[0].isSubmit) + XCTAssertTrue(!disclosureWithOptionality[0].isUserSelectable) + } + + func testSelectThirdInputDescriptor2() throws { + guard let pd = subsetRelationship else { + XCTFail("The presentation definition has not been initialized correctly.") + return + } + + guard + let (firstMatchedInputDescriptor, disclosureWithOptionality) = + pd.firstMatchedInputDescriptor(sdJwt: sdJwtPostalCodeAndFamilyGivenName) + else { + XCTFail("shoud be matched") + return + } + + XCTAssertEqual(firstMatchedInputDescriptor.id, "input3") + XCTAssertTrue(disclosureWithOptionality.count == 3) + for d in disclosureWithOptionality { + if d.isSubmit { + XCTAssertTrue(!d.isUserSelectable) + XCTAssertEqual(d.disclosure.key, "postal_code") // 必須で求められている + } + else { + if d.isUserSelectable { + XCTAssertEqual(d.disclosure.key, "family_name") // オプション + } + else { + XCTAssertEqual(d.disclosure.key, "given_name") // 送信しない + } + } + } + } +} diff --git a/tw2023_walletTests/Resources/presentation_definition/presentation_definition_multi_descriptors_1.json b/tw2023_walletTests/Resources/presentation_definition/presentation_definition_multi_descriptors_1.json new file mode 100644 index 0000000..a63739f --- /dev/null +++ b/tw2023_walletTests/Resources/presentation_definition/presentation_definition_multi_descriptors_1.json @@ -0,0 +1,40 @@ +{ + "id": "12345", + "input_descriptors": [ + { + "id": "input1", + "name": "First Input", + "format": { + "vc+sd-jwt": {} + }, + "group": ["A"], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.postal_address"], + "filter": {"type": "string"} + } + ] + } + }, + { + "id": "input2", + "name": "Second Input", + "format": { + "vc+sd-jwt": {} + }, + "group": ["B"], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.is_older_than_13"], + "filter": {"type": "boolean"} + } + ] + } + } + ] +} + diff --git a/tw2023_walletTests/Resources/presentation_definition/presentation_definition_multi_descriptors_2.json b/tw2023_walletTests/Resources/presentation_definition/presentation_definition_multi_descriptors_2.json new file mode 100644 index 0000000..6a0205d --- /dev/null +++ b/tw2023_walletTests/Resources/presentation_definition/presentation_definition_multi_descriptors_2.json @@ -0,0 +1,66 @@ +{ + "id": "12345", + "input_descriptors": [ + { + "id": "input1", + "name": "First Input", + "format": { + "vc+sd-jwt": {} + }, + "group": ["A"], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.postal_code"], + "filter": {"type": "string"} + }, + { + "path": ["$.prefecture"], + "filter": {"type": "string"} + } + ] + } + }, + { + "id": "input2", + "name": "Second Input", + "format": { + "vc+sd-jwt": {} + }, + "group": ["B"], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.prefecture"], + "filter": {"type": "string"} + } + ] + } + }, + { + "id": "input3", + "name": "Third Input", + "format": { + "vc+sd-jwt": {} + }, + "group": ["C"], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": ["$.postal_code"], + "filter": {"type": "string"} + }, + { + "path": ["$.family_name"], + "filter": {"type": "string"}, + "optional": true + } + ] + } + } + ] +} +