Skip to content

Commit 4bb90db

Browse files
feat(pollux): add verifiable credentials
Fixes ATL-2606
1 parent 620c82a commit 4bb90db

23 files changed

+379
-21
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
public extension JSONDecoder {
4+
static func didComm() -> JSONDecoder {
5+
var decoder = JSONDecoder()
6+
decoder.dataDecodingStrategy = .deferredToData
7+
decoder.keyDecodingStrategy = .convertFromSnakeCase
8+
return decoder
9+
}
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Foundation
2+
3+
public extension JSONEncoder {
4+
static func didComm() -> JSONEncoder {
5+
var encoder = JSONEncoder()
6+
encoder.dataEncodingStrategy = .deferredToData
7+
encoder.keyEncodingStrategy = .convertToSnakeCase
8+
encoder.outputFormatting = .prettyPrinted
9+
return encoder
10+
}
11+
}

Domain/Sources/BBs/Pollux.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
public protocol Pollux {
4+
func parseVerifiableCredential(jsonString: String) throws -> VerifiableCredential
5+
}

Domain/Sources/Models/DID.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,15 @@ public struct DID: Equatable {
2727

2828
/// String representation of this DID as specified in [w3 standards](https://www.w3.org/TR/did-core/#dfn-did-schemes)
2929
public var string: String { "\(schema):\(method):\(methodId)" }
30+
31+
/// Simple initializer that receives a String and returns a DID
32+
/// - Warning: This is not the preferable way of initializing a DID from a string. Please use `Castor.parseDID(str:)` for uknown strings.
33+
/// - Parameter string: DID String
34+
public init(string: String) throws {
35+
var aux = string.components(separatedBy: ":")
36+
guard aux.count > 3 else { throw CastorError.invalidDIDString }
37+
self.schema = aux.removeFirst()
38+
self.method = aux.remove(at: 1)
39+
self.methodId = aux.joined(separator: ":")
40+
}
3041
}

Domain/Sources/Models/Errors.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,7 @@ public enum PlutoError: Error {
3232
case didPairIsNotPersistedError
3333
case holderDIDAlreadyPairingError
3434
}
35+
36+
public enum PolluxError: Error {
37+
case invalidCredentialError
38+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
3+
public struct VerifiableCredentialTypeContainer: Codable {
4+
enum CodingKeys: String, CodingKey {
5+
case id = "@id"
6+
case type = "@type"
7+
}
8+
9+
let id: String
10+
let type: String
11+
12+
public func encode(to encoder: Encoder) throws {
13+
var container = encoder.container(keyedBy: CodingKeys.self)
14+
try container.encode(self.id, forKey: .id)
15+
try container.encode(self.type, forKey: .type)
16+
}
17+
18+
public init(from decoder: Decoder) throws {
19+
let container = try decoder.container(keyedBy: CodingKeys.self)
20+
self.id = try container.decode(String.self, forKey: .id)
21+
self.type = try container.decode(String.self, forKey: .type)
22+
}
23+
}
24+
25+
public protocol VerifiableCredential {
26+
var id: String? { get }
27+
var context: Set<String> { get }
28+
var type: Set<String> { get }
29+
var issuer: DID { get }
30+
var issuanceDate: Date { get }
31+
var expirationDate: Date? { get }
32+
var credentialSchema: VerifiableCredentialTypeContainer? { get }
33+
var credentialSubject: String { get }
34+
var credentialStatus: VerifiableCredentialTypeContainer? { get }
35+
var refreshService: VerifiableCredentialTypeContainer? { get }
36+
var evidence: VerifiableCredentialTypeContainer? { get }
37+
var termsOfUse: VerifiableCredentialTypeContainer? { get }
38+
var validFrom: VerifiableCredentialTypeContainer? { get }
39+
var validUntil: VerifiableCredentialTypeContainer? { get }
40+
41+
// JsonString containing proof content as per `https://www.w3.org/2018/credentials/v1`
42+
var proof: String? { get }
43+
44+
// Not part of W3C Credential but included to preserve in case of conversion from JWT.
45+
var aud: Set<String> { get }
46+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import Domain
2+
import Foundation
3+
4+
struct JWTCredentialPayload {
5+
struct JWTVerfiableCredential {
6+
let context: Set<String>
7+
let type: Set<String>
8+
let credentialSchema: VerifiableCredentialTypeContainer?
9+
let credentialSubject: String
10+
let credentialStatus: VerifiableCredentialTypeContainer?
11+
let refreshService: VerifiableCredentialTypeContainer?
12+
let evidence: VerifiableCredentialTypeContainer?
13+
let termsOfUse: VerifiableCredentialTypeContainer?
14+
}
15+
let iss: DID
16+
let sub: String?
17+
let verifiableCredential: JWTVerfiableCredential
18+
let nbf: Date
19+
let exp: Date?
20+
let jti: String?
21+
let aud: Set<String>
22+
}
23+
24+
extension JWTCredentialPayload: VerifiableCredential {
25+
var context: Set<String> { verifiableCredential.context }
26+
var type: Set<String> { verifiableCredential.type }
27+
var id: String? { jti }
28+
var issuer: DID { iss }
29+
var issuanceDate: Date { nbf }
30+
var expirationDate: Date? { exp }
31+
var credentialSchema: VerifiableCredentialTypeContainer? { verifiableCredential.credentialSchema }
32+
var credentialSubject: String { verifiableCredential.credentialSubject }
33+
var credentialStatus: VerifiableCredentialTypeContainer? { verifiableCredential.credentialStatus }
34+
var refreshService: VerifiableCredentialTypeContainer? { verifiableCredential.refreshService }
35+
var evidence: Domain.VerifiableCredentialTypeContainer? { verifiableCredential.evidence }
36+
var termsOfUse: Domain.VerifiableCredentialTypeContainer? { verifiableCredential.termsOfUse }
37+
var validFrom: Domain.VerifiableCredentialTypeContainer? { nil }
38+
var validUntil: Domain.VerifiableCredentialTypeContainer? { nil }
39+
var proof: String? { nil }
40+
}
41+
42+
extension JWTCredentialPayload.JWTVerfiableCredential: Codable {
43+
enum CodingKeys: String, CodingKey {
44+
case context = "@context"
45+
case type = "@type"
46+
case credentialSubject
47+
case credentialStatus
48+
case credentialSchema
49+
case refreshService
50+
case evidence
51+
case termsOfUse
52+
}
53+
54+
public func encode(to encoder: Encoder) throws {
55+
var container = encoder.container(keyedBy: CodingKeys.self)
56+
try container.encode(self.context, forKey: .context)
57+
try container.encode(self.type, forKey: .type)
58+
try container.encode(self.credentialSubject, forKey: .credentialSubject)
59+
try container.encode(self.credentialStatus, forKey: .credentialStatus)
60+
try container.encode(self.credentialSchema, forKey: .credentialSchema)
61+
try container.encode(self.refreshService, forKey: .refreshService)
62+
try container.encode(self.evidence, forKey: .evidence)
63+
try container.encode(self.termsOfUse, forKey: .termsOfUse)
64+
}
65+
66+
public init(from decoder: Decoder) throws {
67+
let container = try decoder.container(keyedBy: CodingKeys.self)
68+
self.context = try container.decode(Set<String>.self, forKey: .context)
69+
self.type = try container.decode(Set<String>.self, forKey: .type)
70+
self.credentialSubject = try container.decode(String.self, forKey: .credentialSubject)
71+
self.credentialStatus = try? container.decode(
72+
VerifiableCredentialTypeContainer.self,
73+
forKey: .credentialStatus
74+
)
75+
self.credentialSchema = try? container.decode(
76+
VerifiableCredentialTypeContainer.self,
77+
forKey: .credentialSchema
78+
)
79+
self.refreshService = try? container.decode(
80+
VerifiableCredentialTypeContainer.self,
81+
forKey: .refreshService
82+
)
83+
self.evidence = try? container.decode(
84+
VerifiableCredentialTypeContainer.self,
85+
forKey: .evidence
86+
)
87+
self.termsOfUse = try? container.decode(
88+
VerifiableCredentialTypeContainer.self,
89+
forKey: .termsOfUse
90+
)
91+
}
92+
}
93+
94+
extension JWTCredentialPayload: Codable {
95+
enum CodingKeys: String, CodingKey {
96+
case iss
97+
case sub
98+
case verfiableCredential = "vc"
99+
case nbf
100+
case exp
101+
case jti
102+
case aud
103+
}
104+
105+
public func encode(to encoder: Encoder) throws {
106+
var container = encoder.container(keyedBy: CodingKeys.self)
107+
try container.encode(self.iss.string, forKey: .iss)
108+
try container.encode(self.sub, forKey: .sub)
109+
try container.encode(self.verifiableCredential, forKey: .verfiableCredential)
110+
try container.encode(self.nbf, forKey: .nbf)
111+
try container.encode(self.exp, forKey: .exp)
112+
try container.encode(self.jti, forKey: .jti)
113+
try container.encode(self.aud, forKey: .aud)
114+
}
115+
116+
public init(from decoder: Decoder) throws {
117+
let container = try decoder.container(keyedBy: CodingKeys.self)
118+
let didString = try container.decode(String.self, forKey: .iss)
119+
self.iss = try DID(string: didString)
120+
self.sub = try? container.decode(String.self, forKey: .sub)
121+
self.verifiableCredential = try container.decode(JWTVerfiableCredential.self, forKey: .verfiableCredential)
122+
self.nbf = try container.decode(
123+
Date.self,
124+
forKey: .nbf
125+
)
126+
self.exp = try? container.decode(
127+
Date.self,
128+
forKey: .exp
129+
)
130+
self.jti = try? container.decode(
131+
String.self,
132+
forKey: .jti
133+
)
134+
self.aud = (try? container.decode(
135+
Set<String>.self,
136+
forKey: .aud
137+
)) ?? Set<String>()
138+
}
139+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Domain
2+
import Foundation
3+
4+
struct W3CVerifiableCredential: VerifiableCredential {
5+
let context: Set<String>
6+
let type: Set<String>
7+
let id: String?
8+
let issuer: DID
9+
let issuanceDate: Date
10+
let expirationDate: Date?
11+
let credentialSchema: VerifiableCredentialTypeContainer?
12+
let credentialSubject: String
13+
let credentialStatus: VerifiableCredentialTypeContainer?
14+
let refreshService: VerifiableCredentialTypeContainer?
15+
let evidence: VerifiableCredentialTypeContainer?
16+
let termsOfUse: VerifiableCredentialTypeContainer?
17+
let validFrom: VerifiableCredentialTypeContainer?
18+
let validUntil: VerifiableCredentialTypeContainer?
19+
let proof: String?
20+
let aud: Set<String>
21+
}
22+
23+
extension W3CVerifiableCredential: Codable {
24+
enum CodingKeys: String, CodingKey {
25+
case context = "@context"
26+
case type = "@type"
27+
case id
28+
case issuer
29+
case issuanceDate
30+
case expirationDate
31+
case validFrom
32+
case validUntil
33+
case proof
34+
case credentialSubject
35+
case credentialStatus
36+
case credentialSchema
37+
case refreshService
38+
case evidence
39+
case termsOfUse
40+
case aud
41+
}
42+
43+
public func encode(to encoder: Encoder) throws {
44+
var container = encoder.container(keyedBy: CodingKeys.self)
45+
try container.encode(self.context, forKey: .context)
46+
try container.encode(self.type, forKey: .type)
47+
try container.encode(self.id, forKey: .id)
48+
try container.encode(self.issuer.string, forKey: .issuer)
49+
try container.encode(self.issuanceDate, forKey: .issuanceDate)
50+
try container.encode(self.expirationDate, forKey: .expirationDate)
51+
try container.encode(self.validFrom, forKey: .validFrom)
52+
try container.encode(self.validUntil, forKey: .validUntil)
53+
try container.encode(self.proof, forKey: .proof)
54+
try container.encode(self.aud, forKey: .aud)
55+
try container.encode(self.credentialSubject, forKey: .credentialSubject)
56+
try container.encode(self.credentialStatus, forKey: .credentialStatus)
57+
try container.encode(self.credentialSchema, forKey: .credentialSchema)
58+
try container.encode(self.refreshService, forKey: .refreshService)
59+
try container.encode(self.evidence, forKey: .evidence)
60+
try container.encode(self.termsOfUse, forKey: .termsOfUse)
61+
}
62+
63+
public init(from decoder: Decoder) throws {
64+
let container = try decoder.container(keyedBy: CodingKeys.self)
65+
self.context = (try? container.decode(Set<String>.self, forKey: .context)) ?? Set()
66+
self.type = (try? container.decode(Set<String>.self, forKey: .type)) ?? Set()
67+
self.id = try container.decode(String.self, forKey: .id)
68+
let didString = try container.decode(String.self, forKey: .issuer)
69+
self.issuer = try DID(string: didString)
70+
self.issuanceDate = try container.decode(Date.self, forKey: .issuanceDate)
71+
self.expirationDate = try? container.decode(Date.self, forKey: .expirationDate)
72+
self.validFrom = try? container.decode(VerifiableCredentialTypeContainer.self, forKey: .validFrom)
73+
self.validUntil = try? container.decode(VerifiableCredentialTypeContainer.self, forKey: .validUntil)
74+
self.proof = try? container.decode(String.self, forKey: .proof)
75+
self.aud = (try? container.decode(Set<String>.self, forKey: .proof)) ?? Set()
76+
self.credentialSubject = try container.decode(String.self, forKey: .credentialSubject)
77+
self.credentialStatus = try? container.decode(
78+
VerifiableCredentialTypeContainer.self,
79+
forKey: .credentialStatus
80+
)
81+
self.credentialSchema = try? container.decode(
82+
VerifiableCredentialTypeContainer.self,
83+
forKey: .credentialSchema
84+
)
85+
self.refreshService = try? container.decode(
86+
VerifiableCredentialTypeContainer.self,
87+
forKey: .refreshService
88+
)
89+
self.evidence = try? container.decode(
90+
VerifiableCredentialTypeContainer.self,
91+
forKey: .evidence
92+
)
93+
self.termsOfUse = try? container.decode(
94+
VerifiableCredentialTypeContainer.self,
95+
forKey: .termsOfUse
96+
)
97+
}
98+
}

Pollux/Sources/Pollux.swift

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Domain
2+
import Foundation
3+
4+
extension PolluxImpl: Pollux {
5+
public func parseVerifiableCredential(jsonString: String) throws -> VerifiableCredential {
6+
guard let dataValue = jsonString.data(using: .utf8) else { throw PolluxError.invalidCredentialError }
7+
if let jwtCredential = try? JSONDecoder().decode(JWTCredentialPayload.self, from: dataValue)
8+
{
9+
return jwtCredential
10+
} else if let w3cCredential = try? JSONDecoder().decode(W3CVerifiableCredential.self, from: dataValue) {
11+
return w3cCredential
12+
} else {
13+
throw PolluxError.invalidCredentialError
14+
}
15+
}
16+
}

Pollux/Sources/PolluxImpl.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Domain
2+
3+
public struct PolluxImpl {
4+
5+
let castor: Castor
6+
7+
init(castor: Castor) {
8+
self.castor = castor
9+
}
10+
}

PrismAgent/Sources/Helpers/AttachmentDescriptor+Builder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ extension AttachmentDescriptor {
88
payload: T,
99
mediaType: String? = "application/json"
1010
) throws -> AttachmentDescriptor {
11-
let encoded = try JSONEncoder().encode(payload).base64UrlEncodedString()
11+
let encoded = try JSONEncoder.didComm().encode(payload).base64UrlEncodedString()
1212
return AttachmentDescriptor(
1313
id: id,
1414
mediaType: mediaType,

0 commit comments

Comments
 (0)