Skip to content

Commit fcabc28

Browse files
authored
Merge pull request #105 from niscy-eudiw/feature/draft25-updates
Draft 24 updates
2 parents 4ffcbd9 + b393529 commit fcabc28

File tree

8 files changed

+211
-200
lines changed

8 files changed

+211
-200
lines changed

Gemfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
source "https://rubygems.org"
22

3-
gem "fastlane"
3+
gem "fastlane"
4+
gem "xcov"

Gemfile.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ GEM
184184
simctl (1.6.10)
185185
CFPropertyList
186186
naturally
187+
slack-notifier (2.4.0)
187188
terminal-notifier (2.0.0)
188189
terminal-table (3.0.2)
189190
unicode-display_width (>= 1.1.1, < 3)
@@ -202,17 +203,26 @@ GEM
202203
colored2 (~> 3.1)
203204
nanaimo (~> 0.3.0)
204205
rexml (>= 3.3.6, < 4.0)
206+
xcov (1.8.1)
207+
fastlane (>= 2.141.0, < 3.0.0)
208+
multipart-post
209+
slack-notifier
210+
terminal-table
211+
xcodeproj
212+
xcresult (~> 0.2.0)
205213
xcpretty (0.3.0)
206214
rouge (~> 2.0.7)
207215
xcpretty-travis-formatter (1.0.1)
208216
xcpretty (~> 0.2, >= 0.0.7)
217+
xcresult (0.2.2)
209218

210219
PLATFORMS
211220
arm64-darwin-23
212221
universal-darwin-21
213222

214223
DEPENDENCIES
215224
fastlane
225+
xcov
216226

217227
BUNDLED WITH
218228
2.4.22

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SIOPv2 OpenID4VP
1+
# OpenID4VP / SIOPv2
22

33
:heavy_exclamation_mark: **Important!** Before you proceed, please read
44
the [EUDI Wallet Reference Implementation project description](https://github.com/eu-digital-identity-wallet/.github/blob/main/profile/reference-implementation.md)
@@ -18,6 +18,12 @@ the [EUDI Wallet Reference Implementation project description](https://github.co
1818

1919
OpenID4VP is a Protocol that enables the presentation of Verifiable Credentials. It is built on top of OAuth 2.0 and supports multiple credential formats, including W3C Verifiable Credentials Data Model, ISO mdoc, and AnonCreds. This protocol allows for simple, secure, and developer-friendly credential presentation and can be used to support credential presentation and the issuance of access tokens for access to APIs based on Verifiable Credentials in the wallet
2020

21+
This is a swift library that supports
22+
the [SIOPv2 (draft 13)](https://openid.github.io/SIOPv2/openid-connect-self-issued-v2-wg-draft.html)
23+
and [OpenId4VP (draft 24)](https://openid.net/specs/openid-4-verifiable-presentations-1_0-24.html) protocols.
24+
In particular, the library focus on the wallet's role using those two protocols with constraints
25+
included in ISO 23220-4 and ISO-18013-7.
26+
2127
OpenID Connect for Verifiable Presentations (OIDC4VP) and Self-Issued OpenID Provider v2 (SIOP v2) are two specifications that have been approved as OpenID Implementer’s Drafts by the OpenID Foundation membership 1. SIOP v2 is an OpenID specification that allows end-users to act as their own OpenID Providers (OPs). Using Self-Issued OPs, end-users can authenticate themselves and present claims directly to a Relying Party (RP), typically a webapp, without involving a third-party Identity Provider 2. OIDC4VP enables the presentation of Verifiable Credentials using the OpenID Connect protocol.
2228

2329
## Disclaimer

Sources/DCQL/DCQL.swift

Lines changed: 75 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public enum DCQLError: Error {
2929
case emptyNamespace
3030
case emptyClaimName
3131
case emptyDocType
32+
case error(String)
3233
}
3334

3435
public struct CredentialQuery: Codable, Equatable, Sendable {
@@ -166,8 +167,8 @@ internal struct DCQLId {
166167
throw ValidationError.emptyValue
167168
}
168169

169-
let regex = "^[a-zA-Z0-9_-]+$" // Alphanumeric, underscore, hyphen
170-
let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
170+
let regex = "^[a-zA-Z0-9_-]+$"
171+
let predicate: NSPredicate = .init(format: "SELF MATCHES %@", regex)
171172

172173
guard predicate.evaluate(with: value) else {
173174
throw ValidationError.invalidFormat
@@ -195,6 +196,9 @@ public struct DCQL: Codable, Equatable, Sendable {
195196
if let credentialSets = credentialSets {
196197
try credentialSets.ensureValid(knownIds: uniqueIds)
197198
}
199+
200+
try credentials.ensureFormatsValid()
201+
198202
self.credentials = credentials
199203
self.credentialSets = credentialSets
200204
}
@@ -225,6 +229,16 @@ public extension Credentials {
225229
guard uniqueIds.count == count else { throw DCQLError.duplicateQueryId }
226230
return uniqueIds
227231
}
232+
233+
func ensureFormatsValid() throws {
234+
try self.forEach { credentialQuery in
235+
let credentialQueryFormat = credentialQuery.format
236+
switch credentialQueryFormat.format {
237+
case OpenId4VPSpec.FORMAT_MSO_MDOC: try credentialQuery.claims?.forEach { try _ = $0.ensureMsoMdoc() }
238+
default: try credentialQuery.claims?.forEach { try _ = $0.ensureNotMsoMdoc() }
239+
}
240+
}
241+
}
228242
}
229243

230244
public extension CredentialSets {
@@ -248,57 +262,36 @@ public extension CredentialSetQuery {
248262

249263
public struct ClaimsQuery: Codable, Equatable, Sendable {
250264
public let id: ClaimId?
251-
public let path: ClaimPath?
265+
public let path: ClaimPath
252266
public let values: [String]?
253-
public let namespace: MsoMdocNamespace?
254-
public let claimName: MsoMdocClaimName?
267+
public let intentToRetain: Bool?
255268

256269
enum CodingKeys: String, CodingKey {
257270
case id
258271
case path
259272
case values
260-
case namespace = "namespace"
261-
case claimName = "claim_name"
273+
case intentToRetain = "intent_to_retain"
262274
}
263275

264276
public init(
265277
id: ClaimId?,
266-
path: ClaimPath?,
278+
path: ClaimPath,
267279
values: [String]?,
268-
namespace: MsoMdocNamespace?,
269-
claimName: MsoMdocClaimName?
280+
intentToRetain: Bool? = nil
270281
) {
271282
self.id = id
272283
self.path = path
273284
self.values = values
274-
self.namespace = namespace
275-
self.claimName = claimName
285+
self.intentToRetain = intentToRetain
276286
}
277287

278288
public init(from decoder: Decoder) throws {
279289
let container = try decoder.container(keyedBy: CodingKeys.self)
280290

281291
self.id = try container.decodeIfPresent(ClaimId.self, forKey: .id)
282-
self.path = try container.decodeIfPresent(ClaimPath.self, forKey: .path)
292+
self.path = try container.decode(ClaimPath.self, forKey: .path)
283293
self.values = try container.decodeIfPresent([String].self, forKey: .values)
284-
self.namespace = try container.decodeIfPresent(MsoMdocNamespace.self, forKey: .namespace)
285-
self.claimName = try container.decodeIfPresent(MsoMdocClaimName.self, forKey: .claimName)
286-
287-
if namespace == nil && claimName != nil {
288-
throw DecodingError.dataCorruptedError(
289-
forKey: .path,
290-
in: container,
291-
debugDescription: "Namespace must be present when claim name is present"
292-
)
293-
}
294-
295-
if namespace != nil && claimName == nil {
296-
throw DecodingError.dataCorruptedError(
297-
forKey: .path,
298-
in: container,
299-
debugDescription: "Claim name must be present when namespace is present"
300-
)
301-
}
294+
self.intentToRetain = try container.decodeIfPresent(Bool.self, forKey: .intentToRetain)
302295
}
303296

304297
public func encode(to encoder: Encoder) throws {
@@ -307,112 +300,83 @@ public struct ClaimsQuery: Codable, Equatable, Sendable {
307300
try container.encodeIfPresent(id, forKey: .id)
308301
try container.encodeIfPresent(values, forKey: .values)
309302
try container.encodeIfPresent(path, forKey: .path)
310-
311-
// Enforce consistency: either both namespace and claimName must be present or both absent
312-
switch (namespace, claimName) {
313-
case (.some(let ns), .some(let name)):
314-
try container.encode(ns, forKey: .namespace)
315-
try container.encode(name, forKey: .claimName)
316-
case (nil, nil): break
317-
default:
318-
throw EncodingError.invalidValue(
319-
self,
320-
EncodingError.Context(
321-
codingPath: container.codingPath,
322-
debugDescription: "Namespace and claim name must either both be present or both be nil."
323-
)
324-
)
325-
}
303+
try container.encodeIfPresent(intentToRetain, forKey: .intentToRetain)
326304
}
327305

328306
public static func sdJwtVc(
329307
id: ClaimId? = nil,
330308
path: ClaimPath,
331309
values: [String]? = nil
332-
) -> ClaimsQuery {
333-
ClaimsQuery(
310+
) throws -> ClaimsQuery {
311+
try ClaimsQuery(
334312
id: id,
335313
path: path,
314+
values: values
315+
).ensureNotMsoMdoc()
316+
}
317+
318+
public static func mdoc(
319+
id: ClaimId? = nil,
320+
values: [String]? = nil,
321+
namespace: String,
322+
claimName: String,
323+
intentToRetain: Bool? = nil
324+
) throws -> ClaimsQuery {
325+
try ClaimsQuery(
326+
id: id,
327+
path: .claim(namespace).claim(claimName),
336328
values: values,
337-
namespace: nil,
338-
claimName: nil
339-
)
329+
intentToRetain: intentToRetain
330+
).ensureMsoMdoc()
340331
}
341332

342333
public static func mdoc(
343334
id: ClaimId? = nil,
344335
values: [String]? = nil,
345-
namespace: MsoMdocNamespace,
346-
claimName: MsoMdocClaimName
347-
) -> ClaimsQuery {
348-
ClaimsQuery(
336+
path: ClaimPath,
337+
intentToRetain: Bool? = nil
338+
) throws -> ClaimsQuery {
339+
try ClaimsQuery(
349340
id: id,
350-
path: nil,
341+
path: path,
351342
values: values,
352-
namespace: namespace,
353-
claimName: claimName
354-
)
343+
intentToRetain: intentToRetain
344+
).ensureMsoMdoc()
355345
}
356346
}
357347

358-
public struct MsoMdocNamespace: Codable, CustomStringConvertible, Equatable, Sendable {
359-
public let namespace: String
348+
extension ClaimsQuery {
360349

361-
public init(_ namespace: String) throws {
362-
guard !namespace.isEmpty else {
363-
throw DCQLError.emptyNamespace
350+
func ensureMsoMdoc() throws -> ClaimsQuery {
351+
if path.value.count != 2 {
352+
throw DCQLError.error(
353+
"Claim paths for mso mdoc based must have exactly two elements"
354+
)
364355
}
365-
self.namespace = namespace
366-
}
367-
368-
enum CodingKeys: String, CodingKey {
369-
case namespace = "namespace"
370-
}
371-
372-
public var description: String {
373-
return namespace
374-
}
375-
376-
public init(from decoder: Decoder) throws {
377-
let container = try decoder.singleValueContainer()
378-
let namespace = try container.decode(String.self)
379-
380-
if namespace.isEmpty {
381-
throw DCQLError.emptyNamespace
356+
357+
let claimsSatisfy = path.value.allSatisfy { element in
358+
switch element {
359+
case .claim:
360+
return true
361+
default:
362+
return false
363+
}
382364
}
383-
384-
self.namespace = namespace
385-
}
386-
}
387-
388-
public struct MsoMdocClaimName: Codable, CustomStringConvertible, Equatable, Sendable {
389-
390-
public let claimName: String
391-
392-
public init(claimName: String) throws {
393-
guard !claimName.isEmpty else {
394-
throw DCQLError.emptyClaimName
365+
if !claimsSatisfy {
366+
throw DCQLError.error(
367+
"ClaimPaths for MSO MDoc based formats must contain only Claim ClaimPathElements"
368+
)
395369
}
396-
self.claimName = claimName
370+
return self
397371
}
398372

399-
public var description: String {
400-
return claimName
401-
}
402-
403-
enum CodingKeys: String, CodingKey {
404-
case claimName = "claim_name"
405-
}
406-
407-
public init(from decoder: Decoder) throws {
408-
let container = try decoder.singleValueContainer()
409-
let decodedValue = try container.decode(String.self)
410-
411-
guard !decodedValue.isEmpty else {
412-
throw DCQLError.emptyClaimName
373+
func ensureNotMsoMdoc() throws -> ClaimsQuery {
374+
if intentToRetain != nil {
375+
throw DCQLError.error(
376+
"\(OpenId4VPSpec.DCQL_MSO_MDOC_INTENT_TO_RETAIN) can be used only with msp mdoc based formats"
377+
)
413378
}
414-
415-
self.claimName = decodedValue
379+
return self
416380
}
417381
}
418382

@@ -457,14 +421,3 @@ public struct MsoMdocDocType: CustomStringConvertible {
457421
return value
458422
}
459423
}
460-
461-
public struct MsoMdocClaimsQueryExtension: Decodable {
462-
463-
public let namespace: MsoMdocNamespace?
464-
public let claimName: MsoMdocClaimName?
465-
466-
enum CodingKeys: String, CodingKey {
467-
case namespace
468-
case claimName = "claim_name"
469-
}
470-
}

Sources/Entities/Types/OpenId4VPSpec.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public struct OpenId4VPSpec {
2323
public static let clientIdSchemeX509SanDns = "x509_san_dns"
2424
public static let clientIdSchemeVerifierAttestation = "verifier_attestation"
2525

26+
public static let AUTHORIZATION_REQUEST_OBJECT_TYPE = "oauth-authz-req+jwt"
27+
2628
public static let TRANSACTION_DATA_TYPE = "type"
2729
public static let TRANSACTION_DATA_CREDENTIAL_IDS = "credential_ids"
2830
public static let TRANSACTION_DATA_HASH_ALGORITHMS = "transaction_data_hashes_alg"
@@ -48,5 +50,7 @@ public struct OpenId4VPSpec {
4850
public static let DCQL_MSO_MDOC_DOCTYPE_VALUE: String = "doctype_value"
4951
public static let DCQL_MSO_MDOC_NAMESPACE: String = "namespace"
5052
public static let DCQL_MSO_MDOC_CLAIM_NAME: String = "claim_name"
53+
54+
public static let DCQL_MSO_MDOC_INTENT_TO_RETAIN = "intent_to_retain"
5155
}
5256

0 commit comments

Comments
 (0)