diff --git a/tw2023_wallet.xcodeproj/project.pbxproj b/tw2023_wallet.xcodeproj/project.pbxproj index 1f878eb..fbae92f 100644 --- a/tw2023_wallet.xcodeproj/project.pbxproj +++ b/tw2023_wallet.xcodeproj/project.pbxproj @@ -158,7 +158,6 @@ A83039BF2B4E4229004139A7 /* ZipUtilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83039B92B4E4229004139A7 /* ZipUtilTest.swift */; }; A83039C02B4E4229004139A7 /* SDJwtUtilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83039BA2B4E4229004139A7 /* SDJwtUtilTest.swift */; }; A83039C12B4E4229004139A7 /* KeyPairUtilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83039BB2B4E4229004139A7 /* KeyPairUtilTest.swift */; }; - A83039C22B4E4229004139A7 /* SerializeUtilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83039BC2B4E4229004139A7 /* SerializeUtilTest.swift */; }; A83039C42B4E4829004139A7 /* SwiftASN1 in Frameworks */ = {isa = PBXBuildFile; productRef = A83039C32B4E4829004139A7 /* SwiftASN1 */; }; A83039C72B4E6E5D004139A7 /* id_token_sharing_history.proto in Sources */ = {isa = PBXBuildFile; fileRef = A83039C62B4E6E5D004139A7 /* id_token_sharing_history.proto */; }; A83039C92B4E6E7E004139A7 /* credential_data.proto in Sources */ = {isa = PBXBuildFile; fileRef = A83039C82B4E6E7E004139A7 /* credential_data.proto */; }; @@ -179,6 +178,7 @@ A87A957D2CACCDD500001D8F /* ProviderTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A87A957C2CACCDD000001D8F /* ProviderTypes.swift */; }; A87A957E2CACCDD500001D8F /* ProviderTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A87A957C2CACCDD000001D8F /* ProviderTypes.swift */; }; A88779BD2C33DC08002EE9C2 /* WebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88779BC2C33DC08002EE9C2 /* WebViewTests.swift */; }; + A88BF3532CB7807600401ACC /* SerializeUtilTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88BF3522CB7806B00401ACC /* SerializeUtilTest.swift */; }; A88D323A2C26997700429E75 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82ECC782C22738900B9784B /* Metadata.swift */; }; A88D323C2C26B0DF00429E75 /* UrlEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82ECC7F2C23A6AB00B9784B /* UrlEncoder.swift */; }; A88D32442C27001B00429E75 /* credential_display_filled.json in Resources */ = {isa = PBXBuildFile; fileRef = A82601A82C267A2A00BF8139 /* credential_display_filled.json */; }; @@ -412,7 +412,6 @@ A83039B92B4E4229004139A7 /* ZipUtilTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZipUtilTest.swift; sourceTree = ""; }; A83039BA2B4E4229004139A7 /* SDJwtUtilTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SDJwtUtilTest.swift; sourceTree = ""; }; A83039BB2B4E4229004139A7 /* KeyPairUtilTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPairUtilTest.swift; sourceTree = ""; }; - A83039BC2B4E4229004139A7 /* SerializeUtilTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerializeUtilTest.swift; sourceTree = ""; }; A83039C62B4E6E5D004139A7 /* id_token_sharing_history.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = id_token_sharing_history.proto; sourceTree = ""; }; A83039C82B4E6E7E004139A7 /* credential_data.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = credential_data.proto; sourceTree = ""; }; A83039CA2B4E6E92004139A7 /* credential_sharing_history.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = credential_sharing_history.proto; sourceTree = ""; }; @@ -429,6 +428,7 @@ A8509C282B82315D00B28C35 /* RecipientClaims.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientClaims.swift; sourceTree = ""; }; A87A957C2CACCDD000001D8F /* ProviderTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderTypes.swift; sourceTree = ""; }; A88779BC2C33DC08002EE9C2 /* WebViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewTests.swift; sourceTree = ""; }; + A88BF3522CB7806B00401ACC /* SerializeUtilTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerializeUtilTest.swift; sourceTree = ""; }; A89108012B82D1650060DD71 /* RecipientClaimsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientClaimsViewModel.swift; sourceTree = ""; }; A89108032B82D2880060DD71 /* RecipientClaimsPreviewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientClaimsPreviewModel.swift; sourceTree = ""; }; A891080B2B84785F0060DD71 /* credential_sharing_history.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = credential_sharing_history.pb.swift; sourceTree = ""; }; @@ -1033,12 +1033,12 @@ A83039B62B4E4229004139A7 /* Utils */ = { isa = PBXGroup; children = ( + A88BF3522CB7806B00401ACC /* SerializeUtilTest.swift */, A83039B72B4E4229004139A7 /* JWTTest.swift */, A83039B82B4E4229004139A7 /* CertificateUtilTest.swift */, A83039B92B4E4229004139A7 /* ZipUtilTest.swift */, A83039BA2B4E4229004139A7 /* SDJwtUtilTest.swift */, A83039BB2B4E4229004139A7 /* KeyPairUtilTest.swift */, - A83039BC2B4E4229004139A7 /* SerializeUtilTest.swift */, F6CC769D2B580EF600FA26A8 /* QRCodeGeneratorTest.swift */, A82ECC812C23A81C00B9784B /* UrlEncoderTest.swift */, ); @@ -1741,6 +1741,7 @@ 8B5C65992B5251C000D72289 /* KeyBindingImpl.swift in Sources */, 8B5C659C2B54D97400D72289 /* ES256K.swift in Sources */, 8B0E0AA92B3ED7D20080F6A3 /* AuthorizationRquestTests.swift in Sources */, + A88BF3532CB7807600401ACC /* SerializeUtilTest.swift in Sources */, 8BB2BA7F2B4C188E00C668BA /* EncryptionHelperTests.swift in Sources */, 8B43AE322B3A5B880016CF83 /* VCIMetadataClientTests.swift in Sources */, 8B5C65912B515BC300D72289 /* JWTUtil.swift in Sources */, @@ -1772,7 +1773,6 @@ A88D323A2C26997700429E75 /* Metadata.swift in Sources */, 8BB513982B3BB88900D4EFB3 /* VCIMetadataUtil.swift in Sources */, 8B43AE382B3A93C60016CF83 /* AuthServerMetadata.swift in Sources */, - A83039C22B4E4229004139A7 /* SerializeUtilTest.swift in Sources */, 8B0E0AB02B403D510080F6A3 /* PresentationExchange.swift in Sources */, A88779BD2C33DC08002EE9C2 /* WebViewTests.swift in Sources */, 8B297C372B39538500D2998D /* VCIMetadataTests.swift in Sources */, diff --git a/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift b/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift index 8adae77..b6aecac 100644 --- a/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift +++ b/tw2023_wallet/Feature/Credentials/ViewModels/CredentialDetailViewModel.swift @@ -7,6 +7,20 @@ import Foundation +func jwtVcJsonClaimsTobeDisclosed(jwt: String) -> [Disclosure] { + if let (_, body, _) = try? JWTUtil.decodeJwt(jwt: jwt), + let vc = body["vc"] as? [String: Any], + let credentialSubject = vc["credentialSubject"] as? [String: Any] + { + let disclosures = credentialSubject.map { key, value in + // valueがネストしていることは想定していない。 + return Disclosure(disclosure: nil, key: key, value: value as? String) + } + return disclosures + } + return [] +} + @Observable class CredentialDetailViewModel { var requiredClaims: [DisclosureWithOptionality] = [] @@ -53,9 +67,8 @@ class CredentialDetailViewModel { case "jwt_vc_json": inputDescriptor = pd.inputDescriptors[0] // 選択開示できないので先頭固定 self.undisclosedClaims = [] - - let jwt = credential.payload - self.requiredClaims = JWTUtil.convertJWTClaimsAsDisclosure(jwt: jwt).map { it in + self.requiredClaims = jwtVcJsonClaimsTobeDisclosed(jwt: credential.payload).map + { it in return DisclosureWithOptionality( disclosure: it, isSubmit: true, isUserSelectable: false) } diff --git a/tw2023_wallet/Feature/Credentials/ViewModels/DisplayQRCodeViewModel.swift b/tw2023_wallet/Feature/Credentials/ViewModels/DisplayQRCodeViewModel.swift index c501219..d30c5e1 100644 --- a/tw2023_wallet/Feature/Credentials/ViewModels/DisplayQRCodeViewModel.swift +++ b/tw2023_wallet/Feature/Credentials/ViewModels/DisplayQRCodeViewModel.swift @@ -61,15 +61,13 @@ class DisplayQRCodeViewModel: ObservableObject { // else { // return nil // } - let jsonDict = [ + let jsonDict: [String: Any] = [ "format": credential.format, "credential": credential.payload, "display": credential.qrDisplay, ] - guard let jsonData = try? JSONSerialization.data(withJSONObject: jsonDict, options: []), - let jsonString = String(data: jsonData, encoding: .utf8) - else { + guard let jsonString = try? jsonDict.toString() else { return nil } diff --git a/tw2023_wallet/Services/OID/JwtVpJsonGeneratorImpl.swift b/tw2023_wallet/Services/OID/JwtVpJsonGeneratorImpl.swift index 9964954..23db9fc 100644 --- a/tw2023_wallet/Services/OID/JwtVpJsonGeneratorImpl.swift +++ b/tw2023_wallet/Services/OID/JwtVpJsonGeneratorImpl.swift @@ -41,11 +41,12 @@ class JwtVpJsonGeneratorImpl: JwtVpJsonGenerator { vp: vpClaims ) let vpTokenPayload = jwtPayload.toDictionary() - do { - return try JWTUtil.sign(keyAlias: keyAlias, header: header, payload: vpTokenPayload) - } - catch { - fatalError("Failed to sign JWT: \(error)") + let result = JWTUtil.sign(keyAlias: keyAlias, header: header, payload: vpTokenPayload) + switch result { + case let .success(jwt): + return jwt + case let .failure(error): + fatalError("Failed to sign JWT: \(error)") } } diff --git a/tw2023_wallet/Services/OID/KeyBindingImpl.swift b/tw2023_wallet/Services/OID/KeyBindingImpl.swift index 099d1fc..1bbbb47 100644 --- a/tw2023_wallet/Services/OID/KeyBindingImpl.swift +++ b/tw2023_wallet/Services/OID/KeyBindingImpl.swift @@ -44,7 +44,13 @@ class KeyBindingImpl: KeyBinding { "_sd_hash": sdHash, "nonce": nonce, ] - return try JWTUtil.sign(keyAlias: keyAlias, header: header, payload: payload) + let result = JWTUtil.sign(keyAlias: keyAlias, header: header, payload: payload) + switch result { + case let .success(jwt): + return jwt + case let .failure(error): + throw error + } } } diff --git a/tw2023_wallet/Services/OID/PresentationExchange.swift b/tw2023_wallet/Services/OID/PresentationExchange.swift index 22d2772..eed4b92 100644 --- a/tw2023_wallet/Services/OID/PresentationExchange.swift +++ b/tw2023_wallet/Services/OID/PresentationExchange.swift @@ -351,6 +351,13 @@ class JwtVpJsonPresentation { path = "$[\(pathIndex)]" } + /* + Add a comment regarding the leading `$`. + In VP draft 18 (ID 2), when sending `vp_token` as an array, the correct notation is `$[N].`. + However, in draft 21, the correct notation is `$.`. (In other words, it is a relative path) + + For now, we will keep the current implementation, but it should be adjusted accordingly based on the specification we adopt. + */ let pathNested = Path( format: "jwt_vc_json", path: "$.vp.verifiableCredential[\(pathNestedIndex)]" diff --git a/tw2023_wallet/Services/OID/Provider/OpenIdProvider.swift b/tw2023_wallet/Services/OID/Provider/OpenIdProvider.swift index 85daf7e..e1e69c0 100644 --- a/tw2023_wallet/Services/OID/Provider/OpenIdProvider.swift +++ b/tw2023_wallet/Services/OID/Provider/OpenIdProvider.swift @@ -383,14 +383,17 @@ class OpenIdProvider { return .failure(OpenIdProviderIllegalStateException.illegalState) } - let preparedSubmissionData = try! credentials.compactMap { - credential -> PreparedSubmissionData? in + let isMultipleVpTokens = credentials.count > 1 + let preparedSubmissionData = try! credentials.enumerated().compactMap { + (index, credential) -> PreparedSubmissionData? in + let tokenIndex = isMultipleVpTokens ? index : index - 1 switch credential.format { case "vc+sd-jwt": return try credential.createVpTokenForSdJwtVc( clientId: clientId, nonce: nonce, + tokenIndex: index, keyBinding: keyBinding) case "jwt_vc_json": @@ -398,6 +401,7 @@ class OpenIdProvider { try credential.createVpTokenForJwtVc( clientId: clientId, nonce: nonce, + tokenIndex: index, jwtVpJsonGenerator: jwtVpJsonGenerator ) diff --git a/tw2023_wallet/Services/OID/Provider/ProviderTypes.swift b/tw2023_wallet/Services/OID/Provider/ProviderTypes.swift index 5e7caa3..cca3045 100644 --- a/tw2023_wallet/Services/OID/Provider/ProviderTypes.swift +++ b/tw2023_wallet/Services/OID/Provider/ProviderTypes.swift @@ -57,6 +57,7 @@ struct SubmissionCredential: Codable, Equatable { func createVpTokenForSdJwtVc( clientId: String, nonce: String, + tokenIndex: Int, keyBinding: KeyBinding? ) throws -> PreparedSubmissionData { guard let kb = keyBinding else { @@ -94,7 +95,7 @@ struct SubmissionCredential: Codable, Equatable { let dm = DescriptorMap( id: inputDescriptor.id, format: format, - path: "$", + path: tokenIndex > -1 ? "$[\(tokenIndex)]" : "$", pathNested: nil ) @@ -113,6 +114,7 @@ struct SubmissionCredential: Codable, Equatable { func createVpTokenForJwtVc( clientId: String, nonce: String, + tokenIndex: Int, jwtVpJsonGenerator: JwtVpJsonGenerator? ) throws -> PreparedSubmissionData { guard let generator = jwtVpJsonGenerator else { @@ -133,7 +135,9 @@ struct SubmissionCredential: Codable, Equatable { payloadOptions: JwtVpJsonPayloadOptions(aud: clientId, nonce: nonce)) let descriptorMap = JwtVpJsonPresentation.genDescriptorMap( - inputDescriptorId: inputDescriptor.id) + inputDescriptorId: inputDescriptor.id, + pathIndex: tokenIndex + ) return PreparedSubmissionData( credentialId: id, vpToken: vpToken, diff --git a/tw2023_wallet/Signature/ES256K.swift b/tw2023_wallet/Signature/ES256K.swift index 84f9ad4..5713d6e 100644 --- a/tw2023_wallet/Signature/ES256K.swift +++ b/tw2023_wallet/Signature/ES256K.swift @@ -81,8 +81,7 @@ class ES256K { ] // Encode header to Base64URL - let encodedHeader = try Data(JSONSerialization.data(withJSONObject: header)) - .base64URLEncodedString() + let encodedHeader = try header.toBase64UrlString() // Encode payload to Base64URL let encodedPayload = payload.data(using: .utf8)?.base64URLEncodedString() ?? "" diff --git a/tw2023_wallet/Signature/JWTUtil.swift b/tw2023_wallet/Signature/JWTUtil.swift index b5f5c8a..2da9d53 100644 --- a/tw2023_wallet/Signature/JWTUtil.swift +++ b/tw2023_wallet/Signature/JWTUtil.swift @@ -16,6 +16,13 @@ enum SignatureError: Error { case UnsupportedAlgorithmError case UnableToCreateSignatureError case VoidContentError + case SigningKeyNotFound +} + +enum JWTVerificationError: Error { + case unsupportedAlgorithm + case invalidPublicKeyType + case verificationFailed(String) } // See https://swiftpackageindex.com/apple/swift-asn1/main/documentation/swiftasn1/decodingasn1#Final-Result @@ -64,35 +71,32 @@ func convertRstoDer(r: Data, s: Data) -> Data? { } enum JWTUtil { - static func jsonString(from dictionary: [String: Any]) throws -> String { - let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: []) - guard let jsonString = String(data: jsonData, encoding: .utf8) else { - throw JwtError.JsonStringConversionError - } - return jsonString - } - - static func sign(keyAlias: String, header: [String: Any], payload: [String: Any]) throws - -> String + + /* + For verification-related methods, we plan to enhance the checking process + and implement a mechanism to control the level of checking in the future. + In addition, appropriate libraries may be introduced as a means to achieve this. + */ + + static func sign(keyAlias: String, header: [String: Any], payload: [String: Any]) + -> Result { guard let privateKey = KeyPairUtil.getPrivateKey(alias: keyAlias) else { - throw KeyError.KeyNotFound + return .failure(SignatureError.SigningKeyNotFound) } guard - let h = try? JWTUtil.jsonString(from: header).data(using: .utf8)? - .base64URLEncodedString(), - let p = try? JWTUtil.jsonString(from: payload).data(using: .utf8)? - .base64URLEncodedString() + let h = try? header.toBase64UrlString(), + let p = try? payload.toBase64UrlString() else { - throw SignatureError.VoidContentError + return .failure(SignatureError.VoidContentError) } let tbsContent = (h + "." + p).data(using: .utf8) let algorithm: SecKeyAlgorithm = .ecdsaSignatureMessageX962SHA256 guard SecKeyIsAlgorithmSupported(privateKey, .sign, algorithm) else { - throw SignatureError.UnsupportedAlgorithmError + return .failure(SignatureError.UnsupportedAlgorithmError) } var error: Unmanaged? @@ -103,26 +107,36 @@ enum JWTUtil { tbsContent! as CFData, &error) as Data? else { - throw error!.takeRetainedValue() as Error + return .failure(SignatureError.UnableToCreateSignatureError) } - let asn1Object = try ASN1DERDecoder.decode(data: signature) - assert(asn1Object.count == 1) - let sequence = asn1Object[0] - assert(sequence.subCount() == 2) + do { + let asn1Object = try ASN1DERDecoder.decode(data: signature) + assert(asn1Object.count == 1) + let sequence = asn1Object[0] + assert(sequence.subCount() == 2) - guard let firstElm = sequence.sub(0)?.value as? Data, - let secondElm = sequence.sub(1)?.value as? Data - else { - throw SignatureError.UnableToCreateSignatureError - } + guard let firstElm = sequence.sub(0)?.value as? Data, + let secondElm = sequence.sub(1)?.value as? Data + else { + return .failure(SignatureError.UnableToCreateSignatureError) + } - let combined = firstElm + secondElm + let combined = firstElm + secondElm + let jwt = + String(data: tbsContent!, encoding: .utf8)! + "." + + combined.base64URLEncodedString() - return String(data: tbsContent!, encoding: .utf8)! + "." + combined.base64URLEncodedString() + return .success(jwt) + } + catch { + return .failure(SignatureError.UnableToCreateSignatureError) + } } - static func verifyJwt(jwt: String, publicKey: SecKey) -> Result { + static func verifyJwt(jwt: String, publicKey: SecKey) -> Result< + JWT, JWTVerificationError + > { let parts = jwt.components(separatedBy: ".") if parts.count != 3 { return .failure(.verificationFailed("Malformed jwt")) @@ -257,74 +271,10 @@ enum JWTUtil { return .failure(.verificationFailed("Unable to verify jwt")) } - static func convertJWTClaimsAsDisclosure(jwt: String) -> [Disclosure] { - var payload: [String: Any]? - do { - let (_, second, _) = try decodeJwt(jwt: jwt) - payload = second - } - catch { - print("Unable to decode Jwt: \(jwt)") - return [] - } - - if let check = payload, - let vcDict = check["vc"] as? [String: Any], - let credentialSubject = vcDict["credentialSubject"] as? [String: Any] - { - let disclosures = credentialSubject.map { key, value in - // valueがネストしていることは想定していない。 - return Disclosure(disclosure: nil, key: key, value: value as? String) - } - return disclosures - } - - print("Unable to get credential payload : \(jwt)") - return [] - } - - static func decodeJwt(jwt: String) throws -> ([String: Any], [String: Any], String) { - let parts = jwt.split(separator: ".") - if parts.count != 3 { - throw NSError( - domain: "InvalidJWTFormatError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Invalid JWT format"]) - } - - let signature = String(parts[2]) - guard let headerJsonData = String(parts[0]).base64UrlDecoded(), - let payloadJsonData = String(parts[1]).base64UrlDecoded(), - let headerJson = String(data: headerJsonData, encoding: .utf8), - let payloadJson = String(data: payloadJsonData, encoding: .utf8) - else { - throw NSError( - domain: "InvalidJWTFormatError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Invalid JWT format"]) - } - - let headerMap = try jsonToMap(json: headerJson) - let payloadMap = try jsonToMap(json: payloadJson) - return (headerMap, payloadMap, signature) + static func decodeJwt(jwt: String) throws -> ([String: Any], [String: Any], String?) { + let decodedJwt = try decode(jwt: jwt) + return (decodedJwt.header, decodedJwt.body, decodedJwt.signature) } - - static func jsonToMap(json: String) throws -> [String: Any] { - let jsonData = json.data(using: .utf8)! - let jsonObject = try JSONSerialization.jsonObject(with: jsonData, options: []) - - guard let dictionary = jsonObject as? [String: Any] else { - throw NSError( - domain: "ParsingError", code: -1, - userInfo: [NSLocalizedDescriptionKey: "Failed to parse JSON"]) - } - - return dictionary - } -} - -enum JWTVerificationError: Error { - case unsupportedAlgorithm - case invalidPublicKeyType - case verificationFailed(String) } func getAlgorithm(publicKey: SecKey, algorithm: String) -> SecKeyAlgorithm? { diff --git a/tw2023_wallet/Utils/KeyPairUtil.swift b/tw2023_wallet/Utils/KeyPairUtil.swift index 9cc0981..598a8a5 100644 --- a/tw2023_wallet/Utils/KeyPairUtil.swift +++ b/tw2023_wallet/Utils/KeyPairUtil.swift @@ -12,11 +12,6 @@ enum KeyError: Error { case KeyNotFound } -enum JwtError: Error { - case JsonStringConversionError - case Base64ConversionError -} - enum JwkError: Error { case UnableToConversionError } @@ -127,12 +122,13 @@ class KeyPairUtil { "nonce": nonce, ] - let proofJwt = try JWTUtil.sign(keyAlias: keyAlias, header: header, payload: payload) - return proofJwt - } - - static func decodeJwt(jwt: String) throws -> ([String: Any], [String: Any], String) { - return try JWTUtil.decodeJwt(jwt: jwt) + let proofJwt = JWTUtil.sign(keyAlias: keyAlias, header: header, payload: payload) + switch proofJwt { + case let .success(jwt): + return jwt + case let .failure(error): + throw error + } } static func createPublicKey(jwk: [String: String]) throws -> SecKey { diff --git a/tw2023_wallet/Utils/SerializeUtil.swift b/tw2023_wallet/Utils/SerializeUtil.swift index da8418c..4845b62 100644 --- a/tw2023_wallet/Utils/SerializeUtil.swift +++ b/tw2023_wallet/Utils/SerializeUtil.swift @@ -7,21 +7,22 @@ import Foundation -class EnumDeserializer: JSONDecoder { - typealias EnumType = T - - init(enumType: EnumType.Type) { - self.enumType = enumType - super.init() - } +enum JsonSerializationError: Error { + case UnableToEncodeString +} - private let enumType: EnumType.Type +extension Dictionary where Key == String, Value == Any { - required init(from decoder: Decoder) throws { - fatalError("init(from:) has not been implemented") + public func toBase64UrlString() throws -> String { + let jsonData = try JSONSerialization.data(withJSONObject: self, options: []) + return jsonData.base64URLEncodedString() } - func deserialize(rawValue: String) -> EnumType? { - return enumType.init(rawValue: rawValue as! T.RawValue) + public func toString() throws -> String { + let jsonData = try JSONSerialization.data(withJSONObject: self, options: []) + guard let jsonString = String(data: jsonData, encoding: .utf8) else { + throw JsonSerializationError.UnableToEncodeString + } + return jsonString } } diff --git a/tw2023_walletTests/AuthorizationRquestTests.swift b/tw2023_walletTests/AuthorizationRquestTests.swift index f566d0b..c2b8551 100644 --- a/tw2023_walletTests/AuthorizationRquestTests.swift +++ b/tw2023_walletTests/AuthorizationRquestTests.swift @@ -440,7 +440,7 @@ func generateTestJWKSetString(rsaKeyId: String, ecKeyId: String) -> (String, Key return nil } rsaJWKDict["kid"] = rsaKeyId - let rsaJWKString = dictionaryToJSONString(rsaJWKDict) + let rsaJWKString = (try? rsaJWKDict.toString()) ?? "{}" // EC鍵ペアの生成 guard let ecKeyPair = createRandomECKeyPair() else { @@ -455,7 +455,7 @@ func generateTestJWKSetString(rsaKeyId: String, ecKeyId: String) -> (String, Key return nil } ecJWKDict["kid"] = ecKeyId - let ecJWKString = dictionaryToJSONString(ecJWKDict) + let ecJWKString = (try? ecJWKDict.toString()) ?? "{}" // JWKセット文字列の生成 let jwkSetString = """ @@ -470,15 +470,6 @@ func generateTestJWKSetString(rsaKeyId: String, ecKeyId: String) -> (String, Key return (jwkSetString, rsaKeyPair, ecKeyPair) } -func dictionaryToJSONString(_ dict: [String: Any]) -> String { - if let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: []), - let jsonString = String(data: jsonData, encoding: .utf8) - { - return jsonString - } - return "{}" -} - func generateTestJWT(kid: String, privateKey: SecKey) -> String? { // ヘッダーとペイロードの設定 var header = JWSHeader(algorithm: .RS512) diff --git a/tw2023_walletTests/OpenIdProviderTests.swift b/tw2023_walletTests/OpenIdProviderTests.swift index c0e86e2..5ca9c5f 100644 --- a/tw2023_walletTests/OpenIdProviderTests.swift +++ b/tw2023_walletTests/OpenIdProviderTests.swift @@ -407,6 +407,121 @@ final class OpenIdProviderTests: XCTestCase { """ + let presentationDefinition8 = """ + { + "id": "12345", + "submission_requirements": [ + { + "name": "Comment submission", + "rule": "pick", + "count": 1, + "from": "COMMENT" + }, + { + "name": "Affiliation info for your comment", + "rule": "pick", + "max": 1, + "from": "AFFILIATION" + } + ], + "input_descriptors": [ + { + "id": "comment_input", + "group": [ + "COMMENT" + ], + "format": { + "jwt_vc_json": { + "proof_type": [ + "JsonWebSignature2020" + ] + } + }, + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.vc.type" + ], + "filter": { + "type": "array", + "contains": { + "const": "CommentCredential" + } + } + }, + { + "path": [ + "$.vc.credentialSubject.comment" + ], + "filter": { + "type": "string", + "const": "This is comment" + } + }, + { + "path": [ + "$.vc.credentialSubject.url" + ], + "filter": { + "type": "string", + "const": "https://example.com/" + } + }, + { + "path": [ + "$.vc.credentialSubject.bool_value" + ], + "filter": { + "type": "string", + "pattern": "^[012]$" + } + } + ] + } + }, + { + "id": "affiliation_input", + "group": [ + "AFFILIATION" + ], + "format": { + "vc+sd-jwt": {} + }, + "constraints": { + "fields": [ + { + "path": [ + "$.vct" + ], + "filter": { + "type": "string", + "const": "affiliation_credential" + } + }, + { + "path": [ + "$.organization_name" + ] + }, + { + "path": [ + "$.family_name" + ] + }, + { + "path": [ + "$.given_name" + ] + } + ] + } + } + ] + } + """ + func testSelectDisclosureNoSelected() throws { // mock up decodeDisclosureFunction = mockDecodeDisclosure0 @@ -682,6 +797,7 @@ final class OpenIdProviderTests: XCTestCase { let preparedData = try credential.createVpTokenForSdJwtVc( clientId: "https://rp.example.com", nonce: "dummy-nonce", + tokenIndex: -1, keyBinding: keyBinding ) let parts = preparedData.vpToken.split(separator: "~").map(String.init) @@ -729,6 +845,7 @@ final class OpenIdProviderTests: XCTestCase { let preparedData = try credential.createVpTokenForSdJwtVc( clientId: "https://rp.example.com", nonce: "dummy-nonce", + tokenIndex: -1, keyBinding: keyBinding ) let parts = preparedData.vpToken.split(separator: "~").map(String.init) @@ -759,7 +876,13 @@ final class OpenIdProviderTests: XCTestCase { let vc: [String: Any] = ["credentialSubject": credentialSubject] let payload: [String: Any] = ["vc": vc] - let vcJwt = try! JWTUtil.sign(keyAlias: tag, header: header, payload: payload) + let tmp = JWTUtil.sign(keyAlias: tag, header: header, payload: payload) + + guard let vcJwt = try? tmp.get() else { + XCTFail() + return + } + let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let presentationDefinition = try decoder.decode( @@ -781,6 +904,7 @@ final class OpenIdProviderTests: XCTestCase { let preparedData = try credential.createVpTokenForJwtVc( clientId: "https://rp.example.com", nonce: "dummy-nonce", + tokenIndex: -1, jwtVpJsonGenerator: jwtVpJsonGenerator ) do { @@ -904,12 +1028,31 @@ final class OpenIdProviderTests: XCTestCase { switch result { case .success(let data): + if let lastRequestData = MockURLProtocol.lastRequestBody, - let postBodyString = String(data: lastRequestData, encoding: .utf8) + let postBodyString = String(data: lastRequestData, encoding: .utf8), + let components = URLComponents(string: "?\(postBodyString)"), + let parsed = components.queryItems?.reduce( + into: [String: String](), + { result, item in + result[item.name] = item.value + }), + let vpToken = parsed["vp_token"], + let rawPresentationSubmission = parsed["presentation_submission"], + let presentationSubmissionData = rawPresentationSubmission.data( + using: .utf8), + let presentationSubmission = try? JSONSerialization.jsonObject( + with: presentationSubmissionData, options: []) as? [String: Any], + let descriptorMap = presentationSubmission["descriptor_map"] + as? [[String: Any]] { - XCTAssertFalse(postBodyString.contains("id_token=")) - XCTAssertTrue(postBodyString.contains("vp_token=")) + XCTAssertTrue(vpToken.hasPrefix("issuer-jwt")) + XCTAssertTrue(descriptorMap.count == 1) } + else { + XCTFail() + } + if let sharedContents = data.sharedCredentials { XCTAssertEqual(sharedContents.count, 1) XCTAssertEqual(sharedContents[0].id, "internal-id-1") @@ -1031,6 +1174,9 @@ final class OpenIdProviderTests: XCTestCase { XCTAssertFalse(postBodyString.contains("id_token=")) XCTAssertTrue(postBodyString.contains("vp_token=")) } + else { + XCTFail() + } if let sharedContents = data.sharedCredentials { XCTAssertEqual(sharedContents.count, 2) XCTAssertEqual(sharedContents[0].id, "internal-id-1") @@ -1061,6 +1207,200 @@ final class OpenIdProviderTests: XCTestCase { } } + func testDirectPostJwtVcJsonAndSdJwt() throws { + // mock up + decodeDisclosureFunction = mockDecodeDisclosure2Records + let requestObject = RequestObjectPayloadImpl( + responseType: "vp_token id_token", + clientId: "https://rp.example.com", + redirectUri: "https://rp.example.com/cb", + nonce: "dummy-nonce", + responseMode: ResponseMode.directPost, + responseUri: "https://rp.example.com/cb" + ) + + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + let mockSession = URLSession(configuration: configuration) + + let urlString = "https://rp.example.com/cb" + let testURL = URL(string: urlString)! + let mockData = "dummy response".data(using: .utf8) + let response = HTTPURLResponse( + url: testURL, statusCode: 200, httpVersion: nil, headerFields: nil) + MockURLProtocol.mockResponses[testURL.absoluteString] = (mockData, response) + + let vc1 = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI8aWRfdG9rZW4ncyBzdWI-Iiwic3ViIjoiPGlkX3Rva2VuJ3Mgc3ViPiIsIm5iZiI6MTI2MjMwNDAwMCwidmMiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIkNvbW1lbnRDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7InVybCI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJjb21tZW50Ijoi44GT44Gu44K144Kk44OI44Gv56eB5pys5Lq644GuWOOCouOCq-OCpuODs-ODiOOBp-OBmSIsImJvb2xfdmFsdWUiOjF9fX0.6EPvpqZS2QQwVFp5rqa8Mh6QdP6mUyjKFGvvnZxE180" + let vc2 = "issuer-jwt~dummy-claim3-digest~dummy-claim4-digest~" + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + let presentationDefinition = try decoder.decode( + PresentationDefinition.self, from: presentationDefinition8.data(using: .utf8)!) + + let credential1 = SubmissionCredential( + id: "internal-id-1", + format: "jwt_vc_json", + types: [], + credential: vc1, + inputDescriptor: presentationDefinition.inputDescriptors[0], + discloseClaims: jwtVcJsonClaimsTobeDisclosed(jwt: vc1).map { + return DisclosureWithOptionality( + disclosure: Disclosure( + disclosure: $0.disclosure, key: $0.key, value: $0.value), + isSubmit: true, isUserSelectable: false) + } + ) + + let credential2 = SubmissionCredential( + id: "internal-id-2", + format: "vc+sd-jwt", + types: [], + credential: vc2, + inputDescriptor: presentationDefinition.inputDescriptors[1], + discloseClaims: [ + DisclosureWithOptionality( + disclosure: + Disclosure( + disclosure: "organization-name-digest", key: "organization_name", + value: "org name"), + isSubmit: true, isUserSelectable: false), + DisclosureWithOptionality( + disclosure: + Disclosure( + disclosure: "family-name-digest", key: "family_name", + value: "family name"), + isSubmit: true, isUserSelectable: false), + DisclosureWithOptionality( + disclosure: + Disclosure( + disclosure: "given-name-digest", key: "given_name", value: "given_name"), + isSubmit: true, isUserSelectable: false), + + ] + ) + + let authRequestProcessedData = ProcessedRequestData( + authorizationRequest: AuthorizationRequestPayloadImpl(), + requestObjectJwt: "dummy-jwt", + requestObject: requestObject, + clientMetadata: RPRegistrationMetadataPayload(), + presentationDefinition: presentationDefinition, + requestIsSigned: false + ) + + runAsyncTest { + let idProvider = OpenIdProvider(ProviderOption()) + idProvider.authRequestProcessedData = authRequestProcessedData + + let requestObj = authRequestProcessedData.requestObject + let authRequest = authRequestProcessedData.authorizationRequest + idProvider.clientId = requestObj?.clientId ?? authRequest.clientId + idProvider.responseType = requestObj?.responseType ?? authRequest.responseType + idProvider.responseUri = requestObj?.responseUri ?? authRequest.responseUri + idProvider.responseMode = requestObj?.responseMode ?? authRequest.responseMode + idProvider.nonce = requestObj?.nonce ?? authRequest.nonce + idProvider.presentationDefinition = authRequestProcessedData.presentationDefinition + + try KeyPairUtil.generateSignVerifyKeyPair(alias: Constants.Cryptography.KEY_BINDING) + let keyBinding = KeyBindingImpl(keyAlias: Constants.Cryptography.KEY_BINDING) + idProvider.setKeyBinding(keyBinding: keyBinding) + + guard let accountManager = PairwiseAccount(mnemonicWords: nil) else { + XCTFail("unable to create account manager") + return + } + let newAccount = accountManager.nextAccount() + + let publicKey = accountManager.getPublicKey(index: newAccount.index) + let privateKey = accountManager.getPrivateKey(index: newAccount.index) + let keyPair = KeyPairData(publicKey: publicKey, privateKey: privateKey) + idProvider.setSecp256k1KeyPair(keyPair: keyPair) + + let jwtVpJsonGenerator = JwtVpJsonGeneratorImpl( + keyAlias: Constants.Cryptography.KEY_PAIR_ALIAS_FOR_KEY_JWT_VP_JSON) + idProvider.setJwtVpJsonGenerator(jwtVpJsonGenerator: jwtVpJsonGenerator) + + let result = await idProvider.respondToken( + credentials: [credential1, credential2], using: mockSession) + switch result { + case .success(let data): + if let lastRequestData = MockURLProtocol.lastRequestBody, + let postBodyString = String(data: lastRequestData, encoding: .utf8), + let components = URLComponents(string: "?\(postBodyString)"), + let parsed = components.queryItems?.reduce( + into: [String: String](), + { result, item in + result[item.name] = item.value + }), + let idToken = parsed["id_token"], + let vpToken = parsed["vp_token"], + let rawPresentationSubmission = parsed["presentation_submission"], + let presentationSubmissionData = rawPresentationSubmission.data( + using: .utf8), + let presentationSubmission = try? JSONSerialization.jsonObject( + with: presentationSubmissionData, options: []) as? [String: Any], + let descriptorMap = presentationSubmission["descriptor_map"] + as? [[String: Any]] + { + XCTAssertTrue(idToken.hasPrefix("eyJ")) + XCTAssertTrue(vpToken.hasPrefix("[")) + XCTAssertTrue(descriptorMap.count == 2) + + let firstDescriptorMap = descriptorMap[0] + guard let path1 = firstDescriptorMap["path"] as? String else { + XCTFail() + return + } + XCTAssertEqual(path1, "$[0]") + + let secondDescriptorMap = descriptorMap[1] + guard let path2 = secondDescriptorMap["path"] as? String else { + XCTFail() + return + } + XCTAssertEqual(path2, "$[1]") + + // add more test for "nested_path"."path" + } + else { + XCTFail() + } + if let sharedContents = data.sharedCredentials { + XCTAssertEqual(sharedContents.count, 2) + XCTAssertEqual(sharedContents[0].id, "internal-id-1") + XCTAssertEqual(sharedContents[0].sharedClaims.count, 3) + + for claim in sharedContents[0].sharedClaims { + XCTAssertTrue(["comment", "url", "bool_value"].contains(claim.name)) + } + + XCTAssertEqual(sharedContents[1].id, "internal-id-2") + XCTAssertEqual(sharedContents[1].sharedClaims.count, 3) + for claim in sharedContents[1].sharedClaims { + XCTAssertTrue( + ["organization_name", "given_name", "family_name"].contains( + claim.name)) + } + + if let lastRequest = MockURLProtocol.lastRequest { + XCTAssertEqual(lastRequest.httpMethod, "POST") + XCTAssertEqual(lastRequest.url, testURL) + } + else { + XCTFail("No request was made") + } + + } + else { + XCTFail("sharedContents must be exist") + } + case .failure(let error): + XCTFail() + } + } + } + func testDirectPostIdTokenAndVpToken() throws { // mock up decodeDisclosureFunction = mockDecodeDisclosure2Records @@ -1149,6 +1489,9 @@ final class OpenIdProviderTests: XCTestCase { XCTAssertTrue(postBodyString.contains("id_token=")) XCTAssertTrue(postBodyString.contains("vp_token=")) } + else { + XCTFail() + } if let sharedContents = data.sharedCredentials { XCTAssertEqual(sharedContents.count, 1) diff --git a/tw2023_walletTests/Utils/JWTTest.swift b/tw2023_walletTests/Utils/JWTTest.swift index 4d2114a..fed4af7 100644 --- a/tw2023_walletTests/Utils/JWTTest.swift +++ b/tw2023_walletTests/Utils/JWTTest.swift @@ -22,15 +22,23 @@ final class JWTUtilTest: XCTestCase { ] let payload: [String: String] = [:] - let jwt = try! JWTUtil.sign(keyAlias: tag, header: header, payload: payload) - let signatureVerification = JWTUtil.verifyJwt(jwt: jwt, publicKey: publicKey) - - switch signatureVerification { - case .success(let jwt): - XCTAssertTrue(jwt.header["alg"] as! String == "ES256") - case .failure(let error): + let result = JWTUtil.sign(keyAlias: tag, header: header, payload: payload) + + switch result { + case let .success(jwt): + let signatureVerification = JWTUtil.verifyJwt( + jwt: jwt, publicKey: publicKey) + switch signatureVerification { + case .success(let jwt): + XCTAssertTrue(jwt.header["alg"] as! String == "ES256") + case .failure(let error): + XCTFail() + } + + case let .failure(error): XCTFail() } + } func testDecodeJwt() { diff --git a/tw2023_walletTests/Utils/SerializeUtilTest.swift b/tw2023_walletTests/Utils/SerializeUtilTest.swift index 5367d30..f616523 100644 --- a/tw2023_walletTests/Utils/SerializeUtilTest.swift +++ b/tw2023_walletTests/Utils/SerializeUtilTest.swift @@ -1,37 +1,21 @@ // // SerializeUtilTest.swift -// tw2023_walletTests +// tw2023_wallet // -// Created by katsuyoshi ozaki on 2023/12/26. +// Created by katsuyoshi ozaki on 2024/10/10. // import XCTest @testable import tw2023_wallet -enum TestEnum: String { - case case1 = "Value1" - case case2 = "Value2" - case case3 = "Value3" -} - -final class EnumDeserializerTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. +final class SerializeUtilTest: XCTestCase { + func testToString() { + let data: [String: Any] = ["foo": 123] + XCTAssertTrue((try! data.toString()) == "{\"foo\":123}") } - - func testDeserialization() { - let deserializer = EnumDeserializer(enumType: TestEnum.self) - - let stringValue = "Value2" - let deserializedEnum = deserializer.deserialize(rawValue: stringValue) - - XCTAssertNotNil(deserializedEnum, "Enum deserialization failed") - XCTAssertEqual(deserializedEnum, TestEnum.case2, "Incorrect enum case deserialized") + func testToBase64UrlString() { + let data: [String: Any] = ["foo": 123] + XCTAssertTrue((try! data.toBase64UrlString()) == "eyJmb28iOjEyM30") } }