Skip to content

Commit ae1c44a

Browse files
feat(pollux): add support for sd-jwt
This commit adds support for sd-jwt. Receive issued credentials and present. Fixes ATL-7185
1 parent 8e68386 commit ae1c44a

29 files changed

+528
-258
lines changed

EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift

+8-9
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@ struct CreatePeerDIDOperation {
1717
agreementKeys: [keyAgreementFromPublicKey(publicKey: agreementPublicKey)],
1818
services: services.flatMap { service in
1919
service.serviceEndpoint.map {
20-
DIDCore.DIDDocument.Service(
21-
id: service.id,
22-
type: service.type.first ?? "",
23-
serviceEndpoint: AnyCodable(
24-
dictionaryLiteral:
25-
("uri", $0.uri),
26-
("accept", $0.accept),
27-
("routing_keys", $0.routingKeys)
28-
)
20+
AnyCodable(dictionaryLiteral:
21+
("id", service.id),
22+
("type", service.type.first ?? ""),
23+
("serviceEndpoint", [
24+
"uri" : $0.uri,
25+
"accept" : $0.accept,
26+
"routing_keys" : $0.routingKeys
27+
])
2928
)
3029
}
3130
}

EdgeAgentSDK/Castor/Sources/Resolvers/PeerDIDResolver.swift

+81-17
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ extension DIDCore.DIDDocument {
6060
assertionMethod: nil,
6161
capabilityDelegation: nil,
6262
keyAgreement: keyAgreementIds.map { .stringValue($0) },
63-
services: services
63+
services: services.map { $0.toAnyCodable() }
6464
)
6565
}
6666

@@ -98,23 +98,40 @@ extension DIDCore.DIDDocument {
9898
}
9999

100100
let services = try self.services?.map {
101-
guard
102-
let endpoint = $0.serviceEndpoint.value as? [String: Any],
103-
let uri = endpoint["uri"] as? String
104-
else {
105-
throw CastorError.notPossibleToResolveDID(did: $0.id, reason: "Invalid service")
101+
let service = try DIDCore.DIDDocument.Service(from: $0)
102+
switch service.serviceEndpoint.value {
103+
case let endpoint as [String: Any]:
104+
guard
105+
let uri = endpoint["uri"] as? String
106+
else {
107+
throw CastorError.notPossibleToResolveDID(did: service.id, reason: "Invalid service")
108+
}
109+
return Domain.DIDDocument.Service(
110+
id: service.id,
111+
type: [service.type],
112+
serviceEndpoint: [
113+
.init(
114+
uri: uri,
115+
accept: endpoint["accept"] as? [String] ?? [],
116+
routingKeys: endpoint["routing_keys"] as? [String] ?? []
117+
)
118+
]
119+
)
120+
case let endpoint as String:
121+
return Domain.DIDDocument.Service(
122+
id: service.id,
123+
type: [service.type],
124+
serviceEndpoint: [
125+
.init(
126+
uri: endpoint,
127+
accept: ($0.value as? [String: Any])?["accept"] as? [String] ?? [],
128+
routingKeys: ($0.value as? [String: Any])?["routing_keys"] as? [String] ?? []
129+
)
130+
]
131+
)
132+
default:
133+
throw CastorError.notPossibleToResolveDID(did: service.id, reason: "Invalid service")
106134
}
107-
return Domain.DIDDocument.Service(
108-
id: $0.id,
109-
type: [$0.type],
110-
serviceEndpoint: [
111-
.init(
112-
uri: uri,
113-
accept: endpoint["accept"] as? [String] ?? [],
114-
routingKeys: endpoint["routing_keys"] as? [String] ?? []
115-
)
116-
]
117-
)
118135
} ?? [Domain.DIDDocument.Service]()
119136

120137
return Domain.DIDDocument(
@@ -184,3 +201,50 @@ extension DIDCore.DIDDocument.VerificationMethod {
184201
}
185202
}
186203
}
204+
205+
extension DIDCore.DIDDocument.Service {
206+
init(from: AnyCodable) throws {
207+
guard
208+
let dic = from.value as? [String: Any],
209+
let id = dic["id"] as? String,
210+
let type = dic["type"] as? String,
211+
let serviceEndpoint = dic["serviceEndpoint"]
212+
else { throw CommonError.invalidCoding(message: "Could not decode service") }
213+
switch serviceEndpoint {
214+
case let value as AnyCodable:
215+
self = .init(
216+
id: id,
217+
type: type,
218+
serviceEndpoint: value
219+
)
220+
case let value as String:
221+
self = .init(
222+
id: id,
223+
type: type,
224+
serviceEndpoint: AnyCodable(value)
225+
)
226+
case let value as [String: Any]:
227+
self = .init(
228+
id: id,
229+
type: type,
230+
serviceEndpoint: AnyCodable(value)
231+
)
232+
case let value as [String]:
233+
self = .init(
234+
id: id,
235+
type: type,
236+
serviceEndpoint: AnyCodable(value)
237+
)
238+
default:
239+
throw CommonError.invalidCoding(message: "Could not decode service")
240+
}
241+
}
242+
243+
func toAnyCodable() -> AnyCodable {
244+
AnyCodable(dictionaryLiteral:
245+
("id", self.id),
246+
("type", self.type),
247+
("serviceEndpoint", self.serviceEndpoint.value)
248+
)
249+
}
250+
}

EdgeAgentSDK/Castor/Tests/PeerDIDCreationTests.swift

+4-12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ final class PeerDIDCreationTests: XCTestCase {
1414
KeyProperties.rawKey.rawValue: Data(base64URLEncoded: "COd9Xhr-amD7fuswWId2706JBUY_tmjp9eiNEieJeEE")!.base64Encoded()
1515
])
1616

17+
print(keyAgreementPrivateKey.raw.base64URLEncoded())
18+
1719

1820
let authenticationPrivateKey = try apollo.createPrivateKey(parameters: [
1921
KeyProperties.type.rawValue: "EC",
@@ -35,28 +37,18 @@ final class PeerDIDCreationTests: XCTestCase {
3537
services: [service]
3638
)
3739

38-
XCTAssertEqual(did.string, validPeerDID)
40+
XCTAssertTrue(did.string.contains("did:peer:2.Ez6LSoHkfN1Y4nK9RCjx7vopWsLrMGNFNgTNZgoCNQrTzmb1n.Vz6MknRZmapV7uYZQuZez9n9N3tQotjRN18UGS68Vcfo6gR4h.SeyJ"))
3941
}
4042

4143
func testResolvePeerDID() async throws {
42-
let peerDIDString = "did:peer:2.Ez6LSci5EK4Ezue5QA72ZX71QUbXY2xr5ygRw7wM1WJigTNnd.Vz6MkqgCXHEGr2wJZANPZGC8WFmeVuS3abAD9uvh7mTXygCFv.SeyJ0IjoiZG0iLCJzIjoibG9jYWxob3N0OjgwODIiLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19"
43-
4444
let peerDID = DID(
4545
schema: "did",
4646
method: "peer",
4747
methodId: "2.Ez6LSci5EK4Ezue5QA72ZX71QUbXY2xr5ygRw7wM1WJigTNnd.Vz6MkqgCXHEGr2wJZANPZGC8WFmeVuS3abAD9uvh7mTXygCFv.SeyJ0IjoiZG0iLCJzIjoibG9jYWxob3N0OjgwODIiLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19"
4848
)
4949

50-
let mypeerDIDString = "did:peer:2.Ez6LSmx3k5X9xMos7VXdMDJx1CGNTd2tWfLTVyMtu3toJWqPo.Vz6Mkvcu3GqbvM3vr5W1sDVe41wmLeUL6a7b4wEcrGw6ULATR.SeyJ0IjoiZG0iLCJzIjoiazhzLWRldi5hdGFsYXByaXNtLmlvL3ByaXNtLWFnZW50L2RpZGNvbW0iLCJyIjpbXSwiYSI6WyJkaWRjb21tL3YyIl19"
51-
52-
let mypeerDID = DID(
53-
schema: "did",
54-
method: "peer",
55-
methodId: "2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOiJodHRwczovL21lZGlhdG9yLnJvb3RzaWQuY2xvdWQiLCJhIjpbImRpZGNvbW0vdjIiXX0"
56-
)
57-
5850
let apollo = ApolloImpl()
5951
let castor = CastorImpl(apollo: apollo)
60-
let document = try await castor.resolveDID(did: mypeerDID)
52+
let document = try await castor.resolveDID(did: peerDID)
6153
}
6254
}

EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public enum CredentialOperationsOptions {
1313
case signableKey(SignableKey) // A key that can be used for signing.
1414
case exportableKey(ExportableKey) // A key that can be exported.
1515
case zkpPresentationParams(attributes: [String: Bool], predicates: [String]) // Anoncreds zero-knowledge proof presentation parameters
16+
case disclosingClaims(claims: [String])
1617
case custom(key: String, data: Data) // Any custom data.
1718
}
1819

EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift

+2
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ public extension EdgeAgent {
198198
switch offerFormat {
199199
case "prism/jwt":
200200
format = "prism/jwt"
201+
case "vc+sd-jwt":
202+
format = "vc+sd-jwt"
201203
case "anoncreds/[email protected]":
202204
format = "anoncreds/[email protected]"
203205
default:

EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public extension EdgeAgent {
4444
.linkSecret(id: "", secret: linkSecretString)
4545
]
4646
)
47-
case "prism/jwt", "dif/presentation-exchange/[email protected]":
47+
case "prism/jwt", "vc+sd-jwt", "dif/presentation-exchange/[email protected]":
4848
guard
4949
let subjectDIDString = credential.subject
5050
else {

EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ public class EdgeAgent {
3232
}
3333

3434
let logger = SDKLogger(category: .edgeAgent)
35-
let apollo: Apollo & KeyRestoration
36-
let castor: Castor
37-
let pluto: Pluto
38-
let pollux: Pollux & CredentialImporter
39-
let mercury: Mercury
35+
public let apollo: Apollo & KeyRestoration
36+
public let castor: Castor
37+
public let pluto: Pluto
38+
public let pollux: Pollux & CredentialImporter
39+
public let mercury: Mercury
4040
var mediationHandler: MediatorHandler?
4141

4242
var connectionManager: ConnectionsManagerImpl?

EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ final class PresentationExchangeFlowTests: XCTestCase {
123123
}
124124
}
125125

126-
private struct MockCredentialClaim: JWTRegisteredFieldsClaims {
126+
private struct MockCredentialClaim: JWTRegisteredFieldsClaims, Codable {
127127
struct VC: Codable {
128128
let credentialSubject: [String: String]
129129
}

EdgeAgentSDK/Mercury/Sources/DIDCommWrappers/DIDCommDIDResolverWrapper.swift

+48-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ extension DIDCore.DIDDocument {
8585
verificationMethods: verificationMethods,
8686
authentication: authentications.map { .stringValue($0) },
8787
keyAgreement: keyAgreements.map { .stringValue($0) },
88-
services: services
88+
services: services.map { $0.toAnyCodable() }
8989
)
9090
}
9191
}
@@ -95,3 +95,50 @@ extension Dictionary where Key == String, Value == String {
9595
try Core.convertToJsonString(dic: self)
9696
}
9797
}
98+
99+
extension DIDCore.DIDDocument.Service {
100+
init(from: DIDCore.AnyCodable) throws {
101+
guard
102+
let dic = from.value as? [String: Any],
103+
let id = dic["id"] as? String,
104+
let type = dic["type"] as? String,
105+
let serviceEndpoint = dic["serviceEndpoint"]
106+
else { throw CommonError.invalidCoding(message: "Could not decode service") }
107+
switch serviceEndpoint {
108+
case let value as DIDCore.AnyCodable:
109+
self = .init(
110+
id: id,
111+
type: type,
112+
serviceEndpoint: value
113+
)
114+
case let value as String:
115+
self = .init(
116+
id: id,
117+
type: type,
118+
serviceEndpoint: AnyCodable(value)
119+
)
120+
case let value as [String: Any]:
121+
self = .init(
122+
id: id,
123+
type: type,
124+
serviceEndpoint: AnyCodable(value)
125+
)
126+
case let value as [String]:
127+
self = .init(
128+
id: id,
129+
type: type,
130+
serviceEndpoint: AnyCodable(value)
131+
)
132+
default:
133+
throw CommonError.invalidCoding(message: "Could not decode service")
134+
}
135+
}
136+
137+
func toAnyCodable() -> DIDCore.AnyCodable {
138+
AnyCodable(dictionaryLiteral:
139+
("id", self.id),
140+
("type", self.type),
141+
("serviceEndpoint", self.serviceEndpoint.value)
142+
)
143+
}
144+
}

EdgeAgentSDK/Mercury/Sources/Helpers/SessionManager.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ struct SessionManager {
2121
headers: [String: String] = [:],
2222
parameters: [String: String] = [:]
2323
) async throws -> Data? {
24-
try await call(request: try makeRequest(
24+
let url = URL(string: url.absoluteString.replacingOccurrences(of: "host.docker.internal", with: "localhost"))!
25+
return try await call(request: try makeRequest(
2526
url: url,
2627
method: .post,
2728
body: body,

EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ extension JWTCredential: ProvableCredential {
3131
switch attachment.format {
3232
case "dif/presentation-exchange/[email protected]":
3333
let requestData = try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: jsonData)
34-
let payload = try JWT<DefaultJWTClaimsImpl>.getPayload(jwtString: jwtString)
34+
let payload: Data = try JWT.getPayload(jwtString: jwtString)
3535
do {
3636
try VerifyPresentationSubmission.verifyPresentationSubmissionClaims(
3737
request: requestData.presentationDefinition, credentials: [payload]

EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Core
22
import Domain
33
import Foundation
44
import JSONWebAlgorithms
5+
import JSONWebKey
56
import JSONWebSignature
67
import JSONWebToken
78
import Sextant
@@ -198,7 +199,7 @@ struct JWTPresentation {
198199
let jwt = try JWT.signed(
199200
payload: payload,
200201
protectedHeader: DefaultJWSHeaderImpl(algorithm: .ES256K),
201-
key: .init(
202+
key: JSONWebKey.JWK(
202203
keyType: .init(rawValue: keyJWK.kty)!,
203204
keyID: keyJWK.kid,
204205
x: keyJWK.x.flatMap { Data(fromBase64URL: $0) },
@@ -213,7 +214,7 @@ struct JWTPresentation {
213214
}
214215
}
215216

216-
struct ClaimsProofPresentationJWT: JWTRegisteredFieldsClaims {
217+
struct ClaimsProofPresentationJWT: JWTRegisteredFieldsClaims, Codable {
217218
struct VerifiablePresentation: Codable {
218219
enum CodingKeys: String, CodingKey {
219220
case context = "@context"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Foundation
2+
3+
extension SDJWTCredential: Codable {
4+
enum CodingKeys: String, CodingKey {
5+
case sdjwtString
6+
}
7+
8+
func encode(to encoder: any Encoder) throws {
9+
var container = encoder.container(keyedBy: CodingKeys.self)
10+
11+
try container.encode(sdjwtString, forKey: .sdjwtString)
12+
}
13+
14+
init(from decoder: any Decoder) throws {
15+
let container = try decoder.container(keyedBy: CodingKeys.self)
16+
17+
let sdjwtString = try container.decode(String.self, forKey: .sdjwtString)
18+
19+
try self.init(sdjwtString: sdjwtString)
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Domain
2+
import Foundation
3+
4+
extension SDJWTCredential: ExportableCredential {
5+
public var exporting: Data {
6+
(try? sdjwtString.tryToData()) ?? Data()
7+
}
8+
9+
public var restorationType: String { "sd-jwt" }
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Domain
2+
import Foundation
3+
4+
extension SDJWTCredential: ProvableCredential {
5+
func presentation(request: Domain.Message, options: [Domain.CredentialOperationsOptions]) throws -> String {
6+
try SDJWTPresentation().createPresentation(
7+
credential: self,
8+
request: request,
9+
options: options
10+
)
11+
}
12+
13+
func isValidForPresentation(request: Domain.Message, options: [Domain.CredentialOperationsOptions]) throws -> Bool {
14+
request.attachments.first.map { $0.format == "vc+sd-jwt"} ?? true
15+
}
16+
}

0 commit comments

Comments
 (0)