• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

input-output-hk / atala-prism-wallet-sdk-swift / 9466819027

11 Jun 2024 01:48PM UTC coverage: 40.328% (-0.5%) from 40.822%
9466819027

Pull #145

github

web-flow
Merge 1399bca59 into 8e68386ce
Pull Request #145: feat(pollux): add support for sd-jwt

76 of 394 new or added lines in 20 files covered. (19.29%)

2 existing lines in 2 files now uncovered.

4518 of 11203 relevant lines covered (40.33%)

16.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.73
/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift
1
import AnoncredsSwift
2
import Core
3
import Domain
4
import Foundation
5
import JSONWebAlgorithms
6
import JSONWebToken
7
import JSONWebSignature
8
import Sextant
9
import JSONSchema
10

11
extension PolluxImpl {
12

13
    private enum ValidJsonTypeFilter: String {
14
        case string
15
        case number
16
        case boolean
17
    }
18

19
    public func verifyPresentation(message: Message, options: [CredentialOperationsOptions]) async throws -> Bool {
4✔
20
        guard 
4✔
21
            let attachment = message.attachments.first,
4✔
22
            let requestId = message.thid
4✔
23
        else {
4✔
24
            throw PolluxError.couldNotFindPresentationInAttachments
×
25
        }
4✔
26

4✔
27
        let jsonData: Data
4✔
28
        switch attachment.data {
4✔
29
        case let attchedData as AttachmentBase64:
4✔
30
            guard let decoded = Data(fromBase64URL: attchedData.base64) else {
4✔
31
                throw CommonError.invalidCoding(message: "Invalid base64 url attachment")
×
32
            }
4✔
33
            jsonData = decoded
4✔
34
        case let attchedData as AttachmentJsonData:
4✔
35
            jsonData = attchedData.data
×
36
        default:
4✔
37
            throw PolluxError.invalidAttachmentType(supportedTypes: ["Json", "Base64"])
×
38
        }
4✔
39

4✔
40
        switch attachment.format {
4✔
41
        case "dif/presentation-exchange/submission@v1.0":
4✔
42
            return try await verifyPresentationSubmission(json: jsonData, requestId: requestId)
2✔
43
        case "anoncreds/proof@v1.0":
4✔
44
            return try await verifyAnoncreds(
2✔
45
                presentation: jsonData,
2✔
46
                requestId: requestId,
2✔
47
                options: options
2✔
48
            )
2✔
49
        default:
4✔
50
            throw PolluxError.unsupportedAttachmentFormat(attachment.format)
×
51
        }
4✔
52
    }
4✔
53

54
    private func verifyPresentationSubmission(json: Data, requestId: String) async throws -> Bool {
2✔
55
        let presentationContainer = try JSONDecoder.didComm().decode(PresentationContainer.self, from: json)
2✔
56
        let presentationRequest = try await getDefinition(id: requestId)
2✔
57
        guard let submission = presentationContainer.presentationSubmission else {
2✔
58
            throw PolluxError.presentationSubmissionNotAvailable
×
59
        }
2✔
60
        let credentials = try getCredentialJWT(submission: submission, presentationData: json)
2✔
61

2✔
62
        try VerifyPresentationSubmission.verifyPresentationSubmissionClaims(
2✔
63
            request: presentationRequest.presentationDefinition,
2✔
64
            credentials: try credentials.map {
2✔
65
                try JWT.getPayload(jwtString: $0)
2✔
66
            }
2✔
67
        )
2✔
68

2✔
69
        try await verifyJWTs(credentials: credentials)
2✔
70
        return true
2✔
71
    }
2✔
72

73
    private func getCredentialJWT(submission: PresentationSubmission, presentationData: Data) throws -> [String] {
2✔
74
        return submission.descriptorMap
2✔
75
            .filter({ $0.format == "jwt" || $0.format == "jwt_vc" || $0.format == "jwt_vp" })
2✔
76
            .compactMap { try? processJWTPath(descriptor: $0, presentationData: presentationData) }
2✔
77
    }
2✔
78

79
    private func processJWTPath(descriptor: PresentationSubmission.Descriptor, presentationData: Data) throws -> String {
4✔
80
        guard descriptor.format == "jwt" || descriptor.format == "jwt_vc" || descriptor.format == "jwt_vp" else {
4✔
81
            throw UnknownError.somethingWentWrongError(
×
82
                customMessage: "This should not happen since its filtered before",
×
83
                underlyingErrors: nil
×
84
            )
×
85
        }
4✔
86

4✔
87
        guard
4✔
88
            let jwts = presentationData.query(string: descriptor.path)
4✔
89
        else {
4✔
90
            throw PolluxError.credentialPathInvalid(path: descriptor.path)
×
91
        }
4✔
92

4✔
93
        guard let nestedDescriptor = descriptor.pathNested else {
4✔
94
            return jwts
2✔
95
        }
2✔
96
        let nestedPayload: Data = try JWT.getPayload(jwtString: jwts)
2✔
97
        return try processJWTPath(descriptor: nestedDescriptor, presentationData: nestedPayload)
2✔
98
    }
4✔
99

100
    private func verifyJWTs(credentials: [String]) async throws {
2✔
101
        var errors = [Error]()
2✔
102
        await credentials
2✔
103
            .asyncForEach {
2✔
104
                do {
2✔
105
                    guard try await verifyJWT(jwtString: $0) else {
2✔
106
                        errors.append(PolluxError.invalidSignature())
×
107
                        return
×
108
                    }
2✔
109
                } catch {
2✔
110
                    errors.append(error)
×
111
                }
2✔
112
            }
2✔
113
        guard errors.isEmpty else {
2✔
114
            throw PolluxError.invalidSignature(internalErrors: errors)
×
115
        }
2✔
116
    }
2✔
117

118
    private func verifyJWT(jwtString: String) async throws -> Bool {
2✔
119
        let payload: DefaultJWTClaimsImpl = try JWT.getPayload(jwtString: jwtString)
2✔
120
        guard let issuer = payload.iss else {
2✔
121
            throw PolluxError.requiresThatIssuerExistsAndIsAPrismDID
×
122
        }
2✔
123

2✔
124
        let issuerDID = try DID(string: issuer)
2✔
125
        let issuerKeys = try await castor.getDIDPublicKeys(did: issuerDID)
2✔
126
        
2✔
127
        ES256KVerifier.bouncyCastleFailSafe = true
2✔
128

2✔
129
        let validations = issuerKeys
2✔
130
            .compactMap(\.exporting)
4✔
131
            .compactMap {
4✔
132
                try? JWT.verify(jwtString: jwtString, senderKey: $0.jwk.toJoseJWK())
4✔
133
            }
4✔
134
        ES256KVerifier.bouncyCastleFailSafe = false
2✔
135
        return !validations.isEmpty
2✔
136
    }
2✔
137

138
    private func getDefinition(id: String) async throws -> PresentationExchangeRequest {
2✔
139
        guard 
2✔
140
            let request = try await pluto.getMessage(id: id).first().await(),
2✔
141
            let attachmentData = request.attachments.first?.data
2✔
142
        else {
2✔
143
            throw PolluxError.couldNotFindPresentationRequest(id: id)
×
144
        }
2✔
145

2✔
146
        let json: Data
2✔
147
        switch attachmentData {
2✔
148
        case let jsonData as AttachmentJsonData:
2✔
149
            json = jsonData.data
×
150
        case let base64Data as AttachmentBase64:
2✔
151
            json = try base64Data.decoded()
2✔
152
        default:
2✔
153
            throw PolluxError.invalidAttachmentType(supportedTypes: ["base64", "json"])
×
154
        }
2✔
155

2✔
156
        return try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: json)
2✔
157
    }
2✔
158

159
    private func verifyAnoncreds(
160
        presentation: Data,
161
        requestId: String,
162
        options: [CredentialOperationsOptions]
163
    ) async throws -> Bool {
2✔
164
        let anonPresentation = try AnoncredsSwift.Presentation(jsonString: presentation.tryToString())
2✔
165
        let requestPresentation = try await getAnoncredsRequest(id: requestId)
2✔
166

2✔
167
        let internalPresentation = try JSONDecoder.didComm().decode(AnonPresentation.self, from: presentation)
2✔
168
        let schemaIds = internalPresentation.identifiers.map(\.schemaId)
2✔
169
        let credentialDefinitionsIds = internalPresentation.identifiers.map(\.credDefId)
2✔
170
        let anonSchemas = try await getSchemas(ids: schemaIds, options: options)
2✔
171
            .mapValues { try JSONDecoder.didComm().decode(AnonCredentialSchema.self, from: $0) }
2✔
172
            .mapValues {
2✔
173
                Schema(
2✔
174
                    name: $0.name,
2✔
175
                    version: $0.version,
2✔
176
                    attrNames: $0.attrNames.map { AttributeNames($0) } ?? AttributeNames(),
2✔
177
                    issuerId: $0.issuerId
2✔
178
                )
2✔
179
            }
2✔
180
        let anonCredentialDefinitions = try await getCredentialDefinitions(
2✔
181
            ids: credentialDefinitionsIds,
2✔
182
            options: options
2✔
183
        ).mapValues { try AnoncredsSwift.CredentialDefinition(jsonString: $0.tryToString()) }
2✔
184

2✔
185
        return try Verifier().verifyPresentation(
2✔
186
            presentation: anonPresentation,
2✔
187
            presentationRequest: requestPresentation,
2✔
188
            schemas: anonSchemas,
2✔
189
            credentialDefinitions: anonCredentialDefinitions
2✔
190
        )
2✔
191
    }
2✔
192

193
    private func getAnoncredsRequest(id: String) async throws -> AnoncredsSwift.PresentationRequest {
2✔
194
        guard
2✔
195
            let request = try await pluto.getMessage(id: id).first().await(),
2✔
196
            let attachmentData = request.attachments.first?.data
2✔
197
        else {
2✔
198
            throw PolluxError.couldNotFindPresentationRequest(id: id)
×
199
        }
2✔
200

2✔
201
        let json: Data
2✔
202
        switch attachmentData {
2✔
203
        case let jsonData as AttachmentJsonData:
2✔
204
            json = jsonData.data
×
205
        case let base64Data as AttachmentBase64:
2✔
206
            guard let data = try Data(fromBase64URL: base64Data.base64.tryToData()) else {
2✔
NEW
207
                throw CommonError.invalidCoding(message: "Could not decode base64 message attchment")
×
208
            }
2✔
209
            json = data
2✔
210
        default:
2✔
211
            throw PolluxError.invalidAttachmentType(supportedTypes: [])
×
212
        }
2✔
213

2✔
214
        return try PresentationRequest(jsonString: json.tryToString())
2✔
215
    }
2✔
216

217
    private func getSchemas(ids: [String], options: [CredentialOperationsOptions]) async throws -> [String: Data] {
2✔
218
        if
2✔
219
            let schemasOption = options.first(where: {
2✔
220
                if case .schema = $0 { return true }
2✔
221
                return false
×
222
            }),
2✔
223
            case let CredentialOperationsOptions.schema(id, json) = schemasOption
2✔
224
        {
2✔
225
            return try [id: json.tryToData()]
2✔
226
        }
2✔
227

×
228
        guard
×
229
            let schemaDownloaderOption = options.first(where: {
×
230
                if case .schemaDownloader = $0 { return true }
×
231
                return false
×
232
            }),
×
233
            case let CredentialOperationsOptions.schemaDownloader(downloader) = schemaDownloaderOption
×
234
        else {
×
235
            throw PolluxError.missingAndIsRequiredForOperation(type: "schemaDownloader")
×
236
        }
×
237
        let schemas = try await ids.asyncMap { ($0, try await downloader.downloadFromEndpoint(urlOrDID: $0)) }
×
238
        return schemas.reduce( [String: Data]()) { partialResult, schemas in
×
239
            var dic = partialResult
×
240
            dic[schemas.0] = schemas.1
×
241
            return dic
×
242
        }
×
243
    }
×
244

245
    private func getCredentialDefinitions(
246
        ids: [String],
247
        options: [CredentialOperationsOptions]
248
    ) async throws -> [String: Data] {
2✔
249
        if
2✔
250
            let credentialDefinitionsOption = options.first(where: {
4✔
251
                if case .credentialDefinition = $0 { return true }
4✔
252
                return false
2✔
253
            }),
4✔
254
            case let CredentialOperationsOptions.credentialDefinition(id, json) = credentialDefinitionsOption
2✔
255
        {
2✔
256
            return try [id: json.tryToData()]
2✔
257
        }
2✔
258

×
259
        guard
×
260
            let credentialDefinitionsDownloaderOption = options.first(where: {
×
261
                if case .credentialDefinitionDownloader = $0 { return true }
×
262
                return false
×
263
            }),
×
264
            case let CredentialOperationsOptions.credentialDefinitionDownloader(downloader) = credentialDefinitionsDownloaderOption
×
265
        else {
×
266
            throw PolluxError.missingAndIsRequiredForOperation(type: "credentialDefinitionDownloader")
×
267
        }
×
268
        let definitions = try await ids.asyncMap { ($0, try await downloader.downloadFromEndpoint(urlOrDID: $0)) }
×
269
        return definitions.reduce( [String: Data]()) { partialResult, definitions in
×
270
            var dic = partialResult
×
271
            dic[definitions.0] = definitions.1
×
272
            return dic
×
273
        }
×
274
    }
×
275
}
276

277
private struct AnonPresentation: Codable {
278
    struct Identifiers: Codable {
279
        let schemaId: String
280
        let credDefId: String
281
    }
282

283
    let identifiers: [Identifiers]
284
}
285

286
private struct ValidateJsonSchemaContainer: Codable {
287
    let value: AnyCodable
288
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc